diff --git a/apps/demo/src/plugin-demos/canvas-perf.ts b/apps/demo/src/plugin-demos/canvas-perf.ts index a57f4becb..e4ab31ea1 100644 --- a/apps/demo/src/plugin-demos/canvas-perf.ts +++ b/apps/demo/src/plugin-demos/canvas-perf.ts @@ -1,5 +1,5 @@ import { EventData, Observable, Page } from '@nativescript/core'; -import { runCanvasPerf, runWebGLPerf, runImageBitmapPerf, profileDemo, swarm, touchParticles, runBoundsProbe, runCallBound, runWebGPUSample } from '@demo/shared'; +import { runCanvasPerf, runWebGLPerf, runWebGPUPerf, runImageBitmapPerf, profileDemo, swarm, touchParticles, runBoundsProbe, runCallBound, runWebGPUSample } from '@demo/shared'; import { launchArgs } from '../launch-args'; export function navigatingTo(args: EventData) { @@ -70,6 +70,8 @@ class PerfModel extends Observable { profileDemo(canvas, start, profile, launchArgs.frames ?? 180, () => { this.set('status', `${profile} profiled — see logcat (PROF|…)`); }); + } else if (suite === 'webgpu') { + runWebGPUPerf(canvas).then(() => this.set('status', 'webgpu perf done — see log (PERF|…)')); } else if (suite?.startsWith('webgpu:')) { const sample = suite.substring('webgpu:'.length); const started = runWebGPUSample(sample, canvas); diff --git a/crates/canvas-c/src/webgpu/gpu_compute_pass_encoder.rs b/crates/canvas-c/src/webgpu/gpu_compute_pass_encoder.rs index 2e25d9602..139ddbc02 100644 --- a/crates/canvas-c/src/webgpu/gpu_compute_pass_encoder.rs +++ b/crates/canvas-c/src/webgpu/gpu_compute_pass_encoder.rs @@ -73,7 +73,6 @@ pub unsafe extern "C" fn canvas_native_webgpu_compute_pass_encoder_dispatch_work } let compute_pass = &*compute_pass; - let error_sink = compute_pass.error_sink.as_ref(); let mut lock = compute_pass.pass.lock(); if let Some(compute_pass) = lock.as_mut() { @@ -93,7 +92,6 @@ pub unsafe extern "C" fn canvas_native_webgpu_compute_pass_encoder_dispatch_work } let compute_pass = &*compute_pass; - let error_sink = compute_pass.error_sink.as_ref(); let mut lock = compute_pass.pass.lock(); if let Some(compute_pass) = lock.as_mut() { let indirect_buffer = &*indirect_buffer; @@ -113,8 +111,6 @@ pub unsafe extern "C" fn canvas_native_webgpu_compute_pass_encoder_end( } let compute_pass = &*compute_pass; - let label = compute_pass.label.clone(); - let error_sink = compute_pass.error_sink.as_ref(); let mut lock = compute_pass.pass.lock(); @@ -135,7 +131,6 @@ pub unsafe extern "C" fn canvas_native_webgpu_compute_pass_encoder_insert_debug_ } let compute_pass = &*compute_pass; - let error_sink = compute_pass.error_sink.as_ref(); let label = CStr::from_ptr(label); let label = label.to_str().unwrap(); @@ -154,7 +149,6 @@ pub unsafe extern "C" fn canvas_native_webgpu_compute_pass_encoder_pop_debug_gro } let compute_pass = &*compute_pass; - let error_sink = compute_pass.error_sink.as_ref(); let mut lock = compute_pass.pass.lock(); if let Some(compute_pass) = lock.as_mut() { @@ -171,7 +165,6 @@ pub unsafe extern "C" fn canvas_native_webgpu_compute_pass_encoder_push_debug_gr } let compute_pass = &*compute_pass; - let error_sink = compute_pass.error_sink.as_ref(); let mut lock = compute_pass.pass.lock(); if let Some(compute_pass) = lock.as_mut() { @@ -197,7 +190,6 @@ pub unsafe extern "C" fn canvas_native_webgpu_compute_pass_encoder_set_bind_grou let compute_pass = &*compute_pass; - let error_sink = compute_pass.error_sink.as_ref(); let mut lock = compute_pass.pass.lock(); if let Some(compute_pass) = lock.as_mut() { @@ -237,7 +229,6 @@ pub unsafe extern "C" fn canvas_native_webgpu_compute_pass_encoder_set_pipeline( } let compute_pass = &*compute_pass; - let error_sink = compute_pass.error_sink.as_ref(); let mut lock = compute_pass.pass.lock(); if let Some(compute_pass) = lock.as_mut() { diff --git a/crates/canvas-c/src/webgpu/gpu_render_bundle_encoder.rs b/crates/canvas-c/src/webgpu/gpu_render_bundle_encoder.rs index 640c2106b..1d7a48fc4 100644 --- a/crates/canvas-c/src/webgpu/gpu_render_bundle_encoder.rs +++ b/crates/canvas-c/src/webgpu/gpu_render_bundle_encoder.rs @@ -326,43 +326,13 @@ pub unsafe extern "C" fn canvas_native_webgpu_render_bundle_encoder_set_index_bu let buffer_id = Arc::clone(&buffer.buffer); let offset: u64 = offset.try_into().unwrap_or_default(); + // <= 0: the rest of the buffer. + let size = u64::try_from(size).ok().filter(|v| *v > 0); - let size: Option = size.try_into().ok(); - - let mut sizeValue: Option> = None; - - if let Some(value) = size { - sizeValue = std::num::NonZero::new(value); - } - - if size.is_some() { - if let Some(size) = sizeValue { - if let Some(encoder) = render_bundle.encoder.as_mut() { - if let Some(encoder) = encoder { - if let Some(encoder) = encoder.as_mut() { - encoder.set_index_buffer( - buffer_id, - index_format.into(), - offset, - Some(size.get()), - ); - } - } - } - } else { - // todo error ?? - } - } else { - if let Some(encoder) = render_bundle.encoder.as_mut() { - if let Some(encoder) = encoder { - if let Some(encoder) = encoder.as_mut() { - encoder.set_index_buffer( - buffer_id, - index_format.into(), - offset, - None, - ); - } + if let Some(encoder) = render_bundle.encoder.as_mut() { + if let Some(encoder) = encoder { + if let Some(encoder) = encoder.as_mut() { + encoder.set_index_buffer(buffer_id, index_format.into(), offset, size); } } } @@ -424,16 +394,8 @@ pub unsafe extern "C" fn canvas_native_webgpu_render_bundle_encoder_set_vertex_b } }; - let size: Option = size.try_into().ok(); - - let sizeValue = if let Some(value) = size { - std::num::NonZero::new(value) - } else if !buffer.is_null() { - let buffer = &*buffer; - std::num::NonZero::new(buffer.size) - } else { - None - }; + // <= 0: the rest of the buffer. + let size = u64::try_from(size).ok().filter(|v| *v > 0); if let Some(encoder) = render_bundle.encoder.as_mut() { if let Some(encoder) = encoder { @@ -442,7 +404,7 @@ pub unsafe extern "C" fn canvas_native_webgpu_render_bundle_encoder_set_vertex_b slot, buffer_id, offset.try_into().unwrap_or_default(), - sizeValue.map(|v| v.get()), + size, ); } } diff --git a/crates/canvas-c/src/webgpu/gpu_render_pass_encoder.rs b/crates/canvas-c/src/webgpu/gpu_render_pass_encoder.rs index 236d740ba..0494138df 100644 --- a/crates/canvas-c/src/webgpu/gpu_render_pass_encoder.rs +++ b/crates/canvas-c/src/webgpu/gpu_render_pass_encoder.rs @@ -56,8 +56,6 @@ pub unsafe extern "C" fn canvas_native_webgpu_render_pass_encoder_begin_occlusio } let render_pass = &*render_pass; - let label = render_pass.label.clone(); - let error_sink = render_pass.error_sink.as_ref(); let mut pass = render_pass.pass.lock(); @@ -78,8 +76,6 @@ pub unsafe extern "C" fn canvas_native_webgpu_render_pass_encoder_draw( } let render_pass = &*render_pass; - let label = render_pass.label.clone(); - let error_sink = render_pass.error_sink.as_ref(); let mut pass = render_pass.pass.lock(); @@ -105,8 +101,6 @@ pub unsafe extern "C" fn canvas_native_webgpu_render_pass_encoder_draw_indexed( } let render_pass = &*render_pass; - let label = render_pass.label.clone(); - let error_sink = render_pass.error_sink.as_ref(); let mut pass = render_pass.pass.lock(); @@ -130,8 +124,6 @@ pub unsafe extern "C" fn canvas_native_webgpu_render_pass_encoder_draw_indexed_i } let render_pass = &*render_pass; - let label = render_pass.label.clone(); - let error_sink = render_pass.error_sink.as_ref(); let indirect_buffer = &*indirect_buffer; @@ -155,8 +147,6 @@ pub unsafe extern "C" fn canvas_native_webgpu_render_pass_encoder_multi_draw_ind } let render_pass = &*render_pass; - let label = render_pass.label.clone(); - let error_sink = render_pass.error_sink.as_ref(); let indirect_buffer = &*indirect_buffer; @@ -179,8 +169,6 @@ pub unsafe extern "C" fn canvas_native_webgpu_render_pass_encoder_draw_indirect( } let render_pass = &*render_pass; - let label = render_pass.label.clone(); - let error_sink = render_pass.error_sink.as_ref(); let indirect_buffer = &*indirect_buffer; @@ -203,8 +191,6 @@ pub unsafe extern "C" fn canvas_native_webgpu_render_pass_encoder_multi_draw_ind } let render_pass = &*render_pass; - let label = render_pass.label.clone(); - let error_sink = render_pass.error_sink.as_ref(); let indirect_buffer = &*indirect_buffer; @@ -224,8 +210,6 @@ pub unsafe extern "C" fn canvas_native_webgpu_render_pass_encoder_end( } let render_pass = &*render_pass; - let label = render_pass.label.clone(); - let error_sink = render_pass.error_sink.as_ref(); let mut lock = render_pass.pass.lock(); @@ -247,8 +231,6 @@ pub unsafe extern "C" fn canvas_native_webgpu_render_pass_encoder_end_occlusion_ } let render_pass = &*render_pass; - let label = render_pass.label.clone(); - let error_sink = render_pass.error_sink.as_ref(); let mut pass = render_pass.pass.lock(); if let Some(pass) = pass.as_mut() { @@ -267,8 +249,6 @@ pub unsafe extern "C" fn canvas_native_webgpu_render_pass_encoder_execute_bundle } let render_pass = &*render_pass; - let label = render_pass.label.clone(); - let error_sink = render_pass.error_sink.as_ref(); let mut pass = render_pass.pass.lock(); if let Some(pass) = pass.as_mut() { @@ -290,8 +270,6 @@ pub unsafe extern "C" fn canvas_native_webgpu_render_pass_encoder_insert_debug_m } let render_pass = &*render_pass; - let label = render_pass.label.clone(); - let error_sink = render_pass.error_sink.as_ref(); let mut pass = render_pass.pass.lock(); if let Some(pass) = pass.as_mut() { @@ -310,8 +288,6 @@ pub unsafe extern "C" fn canvas_native_webgpu_render_pass_encoder_pop_debug_grou } let render_pass = &*render_pass; - let label = render_pass.label.clone(); - let error_sink = render_pass.error_sink.as_ref(); let mut pass = render_pass.pass.lock(); if let Some(pass) = pass.as_mut() { @@ -328,8 +304,6 @@ pub unsafe extern "C" fn canvas_native_webgpu_render_pass_encoder_push_debug_gro } let render_pass = &*render_pass; - let label = render_pass.label.clone(); - let error_sink = render_pass.error_sink.as_ref(); let mut pass = render_pass.pass.lock(); if let Some(pass) = pass.as_mut() { @@ -354,8 +328,6 @@ pub unsafe extern "C" fn canvas_native_webgpu_render_pass_encoder_set_bind_group } let render_pass = &*render_pass; - let label = render_pass.label.clone(); - let error_sink = render_pass.error_sink.as_ref(); let mut pass = render_pass.pass.lock(); if let Some(pass) = pass.as_mut() { @@ -394,8 +366,6 @@ pub unsafe extern "C" fn canvas_native_webgpu_render_pass_encoder_set_blend_cons } let render_pass = &*render_pass; - let label = render_pass.label.clone(); - let error_sink = render_pass.error_sink.as_ref(); let mut pass = render_pass.pass.lock(); if let Some(pass) = pass.as_mut() { @@ -417,8 +387,6 @@ pub unsafe extern "C" fn canvas_native_webgpu_render_pass_encoder_set_index_buff } let render_pass = &*render_pass; - let label = render_pass.label.clone(); - let error_sink = render_pass.error_sink.as_ref(); let mut pass = render_pass.pass.lock(); if let Some(pass) = pass.as_mut() { @@ -426,19 +394,10 @@ pub unsafe extern "C" fn canvas_native_webgpu_render_pass_encoder_set_index_buff let buffer_id = Arc::clone(&buffer.buffer); let offset: u64 = offset.try_into().unwrap_or_default(); + // <= 0: the rest of the buffer. + let size = u64::try_from(size).ok().filter(|v| *v > 0); - let size: Option = size.try_into().ok(); - - // wgpu takes Option now; zero means "no explicit size", - // which is what NonZero used to encode. - let sizeValue = size.filter(|value| *value > 0); - - pass.set_index_buffer( - Arc::clone(&buffer_id), - index_format.into(), - offset, - sizeValue, - ); + pass.set_index_buffer(buffer_id, index_format.into(), offset, size); } } @@ -452,8 +411,6 @@ pub unsafe extern "C" fn canvas_native_webgpu_render_pass_encoder_set_pipeline( } let render_pass = &*render_pass; - let label = render_pass.label.clone(); - let error_sink = render_pass.error_sink.as_ref(); let mut pass = render_pass.pass.lock(); if let Some(pass) = pass.as_mut() { @@ -476,8 +433,6 @@ pub unsafe extern "C" fn canvas_native_webgpu_render_pass_encoder_set_scissor_re } let render_pass = &*render_pass; - let label = render_pass.label.clone(); - let error_sink = render_pass.error_sink.as_ref(); let mut pass = render_pass.pass.lock(); if let Some(pass) = pass.as_mut() { @@ -494,8 +449,6 @@ pub unsafe extern "C" fn canvas_native_webgpu_render_pass_encoder_set_stencil_re } let render_pass = &*render_pass; - let label = render_pass.label.clone(); - let error_sink = render_pass.error_sink.as_ref(); let mut pass = render_pass.pass.lock(); if let Some(pass) = pass.as_mut() { @@ -515,8 +468,6 @@ pub unsafe extern "C" fn canvas_native_webgpu_render_pass_encoder_set_vertex_buf } let render_pass = &*render_pass; - let label = render_pass.label.clone(); - let error_sink = render_pass.error_sink.as_ref(); let mut pass = render_pass.pass.lock(); if let Some(pass) = pass.as_mut() { @@ -529,21 +480,11 @@ pub unsafe extern "C" fn canvas_native_webgpu_render_pass_encoder_set_vertex_buf } }; - let size: Option = size.try_into().ok(); + // <= 0: the rest of the buffer. + let size = u64::try_from(size).ok().filter(|v| *v > 0); - let sizeValue = if let Some(value) = size { - Some(value).filter(|v| *v > 0) - } else if !buffer.is_null() { - let buffer = &*buffer; - Some(buffer.size).filter(|v| *v > 0) - } else { - None - }; - - pass.set_vertex_buffer(slot, - buffer_id, - offset.try_into().unwrap_or_default(), - sizeValue);} + pass.set_vertex_buffer(slot, buffer_id, offset.try_into().unwrap_or_default(), size); + } } #[no_mangle] @@ -561,8 +502,6 @@ pub unsafe extern "C" fn canvas_native_webgpu_render_pass_encoder_set_viewport( } let render_pass = &*pass; - let label = render_pass.label.clone(); - let error_sink = render_pass.error_sink.as_ref(); let mut pass = render_pass.pass.lock(); if let Some(pass) = pass.as_mut() { diff --git a/packages/canvas/WebGPU/GPUBuffer.ts b/packages/canvas/WebGPU/GPUBuffer.ts index cddd797be..2eee0d6da 100644 --- a/packages/canvas/WebGPU/GPUBuffer.ts +++ b/packages/canvas/WebGPU/GPUBuffer.ts @@ -1,5 +1,8 @@ import { native_, mapState_ } from './Constants'; +const size_ = Symbol('[[size]]'); +const usage_ = Symbol('[[usage]]'); + export class GPUBuffer { [native_]; [mapState_]: 'unmapped' | 'mapped' | 'pending' = 'unmapped'; @@ -8,12 +11,15 @@ export class GPUBuffer { return this[native_]?.label ?? ''; } + [size_] = 0; + [usage_] = 0; + get size() { - return this[native_]?.size ?? 0; + return this[size_]; } get usage() { - return this[native_]?.usage ?? 0; + return this[usage_]; } get native() { @@ -63,6 +69,8 @@ export class GPUBuffer { if (buffer) { const ret = new GPUBuffer(); ret[native_] = buffer; + ret[size_] = buffer.size ?? 0; + ret[usage_] = buffer.usage ?? 0; if (mappedAtCreation) { ret[mapState_] = 'mapped'; } diff --git a/packages/canvas/WebGPU/GPURenderBundleEncoder.ts b/packages/canvas/WebGPU/GPURenderBundleEncoder.ts index 470d5a44b..06a709982 100644 --- a/packages/canvas/WebGPU/GPURenderBundleEncoder.ts +++ b/packages/canvas/WebGPU/GPURenderBundleEncoder.ts @@ -61,7 +61,8 @@ export class GPURenderBundleEncoder { setIndexBuffer(buffer: GPUBuffer, indexFormat: 'uint16' | 'uint32', offset?: number, size?: number) { // 0 = uint16, 1 = uint32: an int keeps the call on the fast path. - this[native_].setIndexBuffer(buffer[native_], indexFormat === 'uint16' ? 0 : 1, offset ?? 0, size ?? buffer.size - (offset ?? 0)); + // -1: the rest of the buffer. + this[native_].setIndexBuffer(buffer[native_], indexFormat === 'uint16' ? 0 : 1, offset ?? 0, size ?? -1); } setPipeline(renderPipeline: GPURenderPipeline) { @@ -69,7 +70,7 @@ export class GPURenderBundleEncoder { } setVertexBuffer(slot: number, buffer: GPUBuffer, offset?: number, size?: number) { - this[native_].setVertexBuffer(Number(slot), buffer[native_], offset ?? 0, size ?? buffer.size - (offset ?? 0)); + this[native_].setVertexBuffer(Number(slot), buffer[native_], offset ?? 0, size ?? -1); } static fromNative(encoder) { diff --git a/packages/canvas/WebGPU/GPURenderPassEncoder.ts b/packages/canvas/WebGPU/GPURenderPassEncoder.ts index 617205fe1..6356a6ab1 100644 --- a/packages/canvas/WebGPU/GPURenderPassEncoder.ts +++ b/packages/canvas/WebGPU/GPURenderPassEncoder.ts @@ -106,7 +106,8 @@ export class GPURenderPassEncoder { setIndexBuffer(buffer: GPUBuffer, indexFormat: 'uint16' | 'uint32', offset?: number, size?: number) { // 0 = uint16, 1 = uint32: an int keeps the call on the fast path. - this[native_].setIndexBuffer(buffer[native_], indexFormat === 'uint16' ? 0 : 1, offset ?? 0, size ?? buffer.size - (offset ?? 0)); + // -1: the rest of the buffer. + this[native_].setIndexBuffer(buffer[native_], indexFormat === 'uint16' ? 0 : 1, offset ?? 0, size ?? -1); } setPipeline(renderPipeline: GPURenderPipeline) { @@ -122,7 +123,7 @@ export class GPURenderPassEncoder { } setVertexBuffer(slot: number, buffer: GPUBuffer, offset?: number, size?: number) { - this[native_].setVertexBuffer(Number(slot), buffer[native_], offset ?? 0, size ?? buffer.size - (offset ?? 0)); + this[native_].setVertexBuffer(Number(slot), buffer[native_], offset ?? 0, size ?? -1); } setViewport(x, y, width, height, minDepth, maxDepth) { diff --git a/tools/demo/canvas/index.ts b/tools/demo/canvas/index.ts index 4e1b3300b..4c549368d 100644 --- a/tools/demo/canvas/index.ts +++ b/tools/demo/canvas/index.ts @@ -74,6 +74,7 @@ export { runSpecTests, specResults, setSpecPageCanvas } from './spec'; export { runCanvasPerf } from './canvas2d/perf'; export { runImageBitmapPerf } from './canvas2d/imagebitmap-perf'; export { runWebGLPerf } from './webgl/perf'; +export { runWebGPUPerf } from './webgpu/perf'; export { WEBGPU_SAMPLES, runWebGPUSample } from './webgpu/samples'; export { runBoundsProbe } from './bounds-probe'; export { runCallBound } from './callbound-native'; diff --git a/tools/demo/canvas/webgpu/perf.ts b/tools/demo/canvas/webgpu/perf.ts new file mode 100644 index 000000000..12c141d08 --- /dev/null +++ b/tools/demo/canvas/webgpu/perf.ts @@ -0,0 +1,268 @@ +/** + * WebGPU binding-layer benchmark: the calls a three.js or PixiJS frame repeats thousands of + * times, timed through the public wrappers an app uses. + * + * Every result is one `PERF|...` line so a run can be scraped off logcat or the sim log: + * + * PERF|||||| + * + * Groups: + * state - render pass state calls: closest to pure call overhead. + * draw - draw calls on a 4x4 target, so the GPU is not what is timed. + * upload - queue.writeBuffer of small payloads, as per-object uniforms are. + * query - wrapper getters a hot path may touch per call. + * + * Only the JS side is timed: the queue thread encodes and submits after the loop. + */ + +const MAGLEV = 1 << 5; +const TURBOFAN = 1 << 6; + +let getOptimizationStatus: ((f: Function) => number) | null = null; +try { + getOptimizationStatus = new Function('f', 'return %GetOptimizationStatus(f);') as (f: Function) => number; + getOptimizationStatus(function () {}); +} catch (e) { + getOptimizationStatus = null; +} + +function tier(fn: Function): string { + if (!getOptimizationStatus) { + return 'unknown'; + } + try { + const status = getOptimizationStatus(fn); + if (status & TURBOFAN) { + return 'turbofan'; + } + if (status & MAGLEV) { + return 'maglev'; + } + return `tier:${status}`; + } catch (e) { + return 'unknown'; + } +} + +interface BenchOptions { + iters?: number; + reps?: number; +} + +const results: Array<{ group: string; name: string; nsPerOp: number }> = []; + +function bench(group: string, name: string, fn: (n: number) => void, opts: BenchOptions = {}) { + const iters = opts.iters ?? 50_000; + const reps = opts.reps ?? 5; + + // Warm into the optimizing tier: fast-API calls are only emitted from optimized code. + fn(Math.min(iters, 20_000)); + fn(Math.min(iters, 20_000)); + + let best = Infinity; + for (let r = 0; r < reps; r++) { + const start = performance.now(); + fn(iters); + const elapsed = performance.now() - start; + if (elapsed < best) { + best = elapsed; + } + } + + const nsPerOp = (best * 1e6) / iters; + results.push({ group, name, nsPerOp }); + console.log(`PERF|${group}|${name}|${iters}|${best.toFixed(3)}|${nsPerOp.toFixed(1)}|${tier(fn)}`); +} + +const SHADER = /* wgsl */ ` +struct Uniforms { offset: vec4f }; +@group(0) @binding(0) var uniforms: Uniforms; + +@vertex +fn vs(@location(0) position: vec2f) -> @builtin(position) vec4f { + return vec4f(position + uniforms.offset.xy, 0.0, 1.0); +} + +@fragment +fn fs() -> @location(0) vec4f { + return vec4f(1.0, 0.0, 0.0, 1.0); +}`; + +export async function runWebGPUPerf(canvas: any) { + const gpu = (globalThis as any).navigator?.gpu; + const adapter = await gpu?.requestAdapter?.(); + const device = await adapter?.requestDevice?.(); + if (!device) { + console.log('PERF|error|no webgpu device'); + return; + } + + console.log(`PERF|meta|context|webgpu`); + console.log(`PERF|meta|canvas|${canvas.width}x${canvas.height}`); + console.log(`PERF|meta|natives|${getOptimizationStatus ? 'available' : 'unavailable'}`); + console.log(`PERF|header|group|name|calls|bestMs|nsPerOp|tier`); + + const GPUBufferUsage = (globalThis as any).GPUBufferUsage; + const GPUTextureUsage = (globalThis as any).GPUTextureUsage; + const GPUShaderStage = (globalThis as any).GPUShaderStage; + + const module = device.createShaderModule({ code: SHADER }); + const layout = device.createBindGroupLayout({ + entries: [{ binding: 0, visibility: GPUShaderStage.VERTEX, buffer: { type: 'uniform', hasDynamicOffset: false } }], + }); + const dynamicLayout = device.createBindGroupLayout({ + entries: [{ binding: 0, visibility: GPUShaderStage.VERTEX, buffer: { type: 'uniform', hasDynamicOffset: true } }], + }); + const vertexLayout = { arrayStride: 8, attributes: [{ shaderLocation: 0, offset: 0, format: 'float32x2' }] }; + const pipeline = device.createRenderPipeline({ + layout: device.createPipelineLayout({ bindGroupLayouts: [layout] }), + vertex: { module, entryPoint: 'vs', buffers: [vertexLayout] }, + fragment: { module, entryPoint: 'fs', targets: [{ format: 'rgba8unorm' }] }, + primitive: { topology: 'triangle-list' }, + }); + const dynamicPipeline = device.createRenderPipeline({ + layout: device.createPipelineLayout({ bindGroupLayouts: [dynamicLayout] }), + vertex: { module, entryPoint: 'vs', buffers: [vertexLayout] }, + fragment: { module, entryPoint: 'fs', targets: [{ format: 'rgba8unorm' }] }, + primitive: { topology: 'triangle-list' }, + }); + + // A single degenerate triangle: the draw benches are about the call, not pixels. + const vertices = device.createBuffer({ size: 24, usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST }); + device.queue.writeBuffer(vertices, 0, new Float32Array([0, 0, 0.001, 0, 0, 0.001])); + const indices = device.createBuffer({ size: 8, usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST }); + device.queue.writeBuffer(indices, 0, new Uint16Array([0, 1, 2, 0])); + const uniforms = device.createBuffer({ size: 256 * 4, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }); + const group = device.createBindGroup({ layout, entries: [{ binding: 0, resource: { buffer: uniforms, size: 16 } }] }); + const dynamicGroup = device.createBindGroup({ layout: dynamicLayout, entries: [{ binding: 0, resource: { buffer: uniforms, size: 16 } }] }); + const target = device.createTexture({ size: { width: 4, height: 4 }, format: 'rgba8unorm', usage: GPUTextureUsage.RENDER_ATTACHMENT }); + const view = target.createView(); + + let pass: any = null; + let encoder: any = null; + function begin() { + encoder = device.createCommandEncoder(); + pass = encoder.beginRenderPass({ colorAttachments: [{ view, loadOp: 'clear', storeOp: 'store', clearValue: { r: 0, g: 0, b: 0, a: 1 } }] }); + pass.setPipeline(pipeline); + pass.setBindGroup(0, group); + pass.setVertexBuffer(0, vertices); + pass.setIndexBuffer(indices, 'uint16'); + } + function end() { + pass.end(); + device.queue.submit([encoder.finish()]); + pass = null; + encoder = null; + } + /** Runs `op` n times inside one pass. */ + function inPass(op: (i: number) => void) { + return (n: number) => { + begin(); + for (let i = 0; i < n; i++) { + op(i); + } + end(); + }; + } + + const offsets = new Uint32Array([0]); + + bench( + 'state', + 'setPipeline', + inPass(() => pass.setPipeline(pipeline)), + ); + bench( + 'state', + 'setBindGroup', + inPass(() => pass.setBindGroup(0, group)), + ); + bench( + 'state', + 'setBindGroup(dynamic offsets)', + inPass((i) => { + offsets[0] = (i & 3) * 256; + pass.setBindGroup(0, dynamicGroup, offsets, 0, 1); + }), + ); + bench( + 'state', + 'setVertexBuffer(slot, buffer)', + inPass(() => pass.setVertexBuffer(0, vertices)), + ); + bench( + 'state', + 'setVertexBuffer(slot, buffer, offset, size)', + inPass(() => pass.setVertexBuffer(0, vertices, 0, 24)), + ); + bench( + 'state', + 'setIndexBuffer(buffer, format)', + inPass(() => pass.setIndexBuffer(indices, 'uint16')), + ); + bench( + 'state', + 'setScissorRect', + inPass(() => pass.setScissorRect(0, 0, 4, 4)), + ); + bench( + 'state', + 'setViewport', + inPass(() => pass.setViewport(0, 0, 4, 4, 0, 1)), + ); + + bench( + 'draw', + 'draw(3)', + inPass(() => pass.draw(3)), + ); + bench( + 'draw', + 'drawIndexed(3)', + inPass(() => pass.drawIndexed(3)), + ); + bench( + 'draw', + 'setBindGroup+drawIndexed', + inPass(() => { + pass.setBindGroup(0, group); + pass.drawIndexed(3); + }), + ); + + const bytes = new Uint8Array(256); + const floats = new Float32Array(16); + bench('upload', 'writeBuffer(Uint8Array 256B)', (n) => { + for (let i = 0; i < n; i++) { + device.queue.writeBuffer(uniforms, 0, bytes); + } + }); + bench('upload', 'writeBuffer(Float32Array 64B)', (n) => { + for (let i = 0; i < n; i++) { + device.queue.writeBuffer(uniforms, 0, floats); + } + }); + bench('upload', 'writeBuffer(Float32Array 64B, offset)', (n) => { + for (let i = 0; i < n; i++) { + device.queue.writeBuffer(uniforms, (i & 3) * 256, floats, 0, 16); + } + }); + + let sink = 0; + bench('query', 'buffer.size', (n) => { + for (let i = 0; i < n; i++) { + sink += vertices.size; + } + }); + bench('query', 'buffer.usage', (n) => { + for (let i = 0; i < n; i++) { + sink += vertices.usage; + } + }); + void sink; + + // Drain the queue so a crash in encoding shows up here, not in the next demo. + await device.queue.onSubmittedWorkDone?.(); + console.log('PERF|done|webgpu'); + return results; +}