From 65542808e28bb091e3374fb34e78f56a84a4a7e7 Mon Sep 17 00:00:00 2001 From: Osei Fortune Date: Sat, 14 Nov 2026 01:33:14 -0400 Subject: [PATCH] perf(webgpu): no native round trip per setVertexBuffer/setIndexBuffer; per-call benchmark - GPUBuffer caches size and usage once at creation. The wrappers read buffer.size on every setVertexBuffer(slot, buffer) and setIndexBuffer(buffer, format) call to default the size, which crossed into native each time. They now pass "rest of the buffer" through, and the Rust entry points hand wgpu None instead of the whole buffer size, which was also wrong whenever an offset was given. - The pass encoder entry points cloned the pass label and fetched the error sink on every draw and state call without using them. - tools/demo/canvas/webgpu/perf.ts: a per-call WebGPU benchmark next to the 2D and WebGL ones (canvas-perf page, suite "webgpu"). iOS simulator, ns per call, before -> after: setVertexBuffer(slot, buffer) 449 -> 397, setIndexBuffer(buffer, format) 444 -> 395, buffer.size 51 -> 31; the other calls unchanged. A three.js scene at about 4k draws on Linux: median render 40.4 -> 38.3 ms. --- apps/demo/src/plugin-demos/canvas-perf.ts | 4 +- .../src/webgpu/gpu_compute_pass_encoder.rs | 9 - .../src/webgpu/gpu_render_bundle_encoder.rs | 56 +--- .../src/webgpu/gpu_render_pass_encoder.rs | 75 +---- packages/canvas/WebGPU/GPUBuffer.ts | 12 +- .../canvas/WebGPU/GPURenderBundleEncoder.ts | 5 +- .../canvas/WebGPU/GPURenderPassEncoder.ts | 5 +- tools/demo/canvas/index.ts | 1 + tools/demo/canvas/webgpu/perf.ts | 268 ++++++++++++++++++ 9 files changed, 304 insertions(+), 131 deletions(-) create mode 100644 tools/demo/canvas/webgpu/perf.ts 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; +}