export const description = ` Stress tests covering GPURenderPassEncoder usage. `; import { makeTestGroup } from '../../common/framework/test_group.js'; import { range } from '../../common/util/util.js'; import { GPUTest } from '../../webgpu/gpu_test.js'; export const g = makeTestGroup(GPUTest); g.test('many') .desc( `Tests execution of a huge number of render passes using the same GPURenderPipeline. This uses a single render pass for every output fragment, with each pass executing a one-vertex draw call.` ) .fn(t => { const kSize = 1024; const module = t.device.createShaderModule({ code: ` @vertex fn vmain(@builtin(vertex_index) index: u32) -> @builtin(position) vec4 { let position = vec2(f32(index % ${kSize}u), f32(index / ${kSize}u)); let r = vec2(1.0 / f32(${kSize})); let a = 2.0 * r; let b = r - vec2(1.0); return vec4(fma(position, a, b), 0.0, 1.0); } @fragment fn fmain() -> @location(0) vec4 { return vec4(1.0, 0.0, 1.0, 1.0); } `, }); const pipeline = t.device.createRenderPipeline({ layout: 'auto', vertex: { module, entryPoint: 'vmain', buffers: [] }, primitive: { topology: 'point-list' }, fragment: { targets: [{ format: 'rgba8unorm' }], module, entryPoint: 'fmain', }, }); const renderTarget = t.device.createTexture({ size: [kSize, kSize], usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC, format: 'rgba8unorm', }); const renderPassDescriptor: GPURenderPassDescriptor = { colorAttachments: [ { view: renderTarget.createView(), loadOp: 'load', storeOp: 'store', }, ], }; const encoder = t.device.createCommandEncoder(); range(kSize * kSize, i => { const pass = encoder.beginRenderPass(renderPassDescriptor); pass.setPipeline(pipeline); pass.draw(1, 1, i); pass.end(); }); t.device.queue.submit([encoder.finish()]); t.expectSingleColor(renderTarget, 'rgba8unorm', { size: [kSize, kSize, 1], exp: { R: 1, G: 0, B: 1, A: 1 }, }); }); g.test('pipeline_churn') .desc( `Tests execution of a large number of render pipelines, each within its own render pass. Each pass does a single draw call, with one pass per output fragment.` ) .fn(t => { const kWidth = 64; const kHeight = 8; const module = t.device.createShaderModule({ code: ` @vertex fn vmain(@builtin(vertex_index) index: u32) -> @builtin(position) vec4 { let position = vec2(f32(index % ${kWidth}u), f32(index / ${kWidth}u)); let size = vec2(f32(${kWidth}), f32(${kHeight})); let r = vec2(1.0) / size; let a = 2.0 * r; let b = r - vec2(1.0); return vec4(fma(position, a, b), 0.0, 1.0); } @fragment fn fmain() -> @location(0) vec4 { return vec4(1.0, 0.0, 1.0, 1.0); } `, }); const renderTarget = t.device.createTexture({ size: [kWidth, kHeight], usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC, format: 'rgba8unorm', }); const depthTarget = t.device.createTexture({ size: [kWidth, kHeight], usage: GPUTextureUsage.RENDER_ATTACHMENT, format: 'depth24plus-stencil8', }); const renderPassDescriptor: GPURenderPassDescriptor = { colorAttachments: [ { view: renderTarget.createView(), loadOp: 'load', storeOp: 'store', }, ], depthStencilAttachment: { view: depthTarget.createView(), depthLoadOp: 'load', depthStoreOp: 'store', stencilLoadOp: 'load', stencilStoreOp: 'discard', }, }; const encoder = t.device.createCommandEncoder(); range(kWidth * kHeight, i => { const pipeline = t.device.createRenderPipeline({ layout: 'auto', vertex: { module, entryPoint: 'vmain', buffers: [] }, primitive: { topology: 'point-list' }, depthStencil: { format: 'depth24plus-stencil8', depthCompare: 'always', depthWriteEnabled: false, // Not really used, but it ensures that each pipeline is unique. depthBias: i, }, fragment: { targets: [{ format: 'rgba8unorm' }], module, entryPoint: 'fmain', }, }); const pass = encoder.beginRenderPass(renderPassDescriptor); pass.setPipeline(pipeline); pass.draw(1, 1, i); pass.end(); }); t.device.queue.submit([encoder.finish()]); t.expectSingleColor(renderTarget, 'rgba8unorm', { size: [kWidth, kHeight, 1], exp: { R: 1, G: 0, B: 1, A: 1 }, }); }); g.test('bind_group_churn') .desc( `Tests execution of render passes which switch between a huge number of bind groups. This uses a single render pass with a single pipeline, and one draw call per fragment of the output texture. Each draw call is made with a unique bind group 0, with binding 0 referencing a unique uniform buffer.` ) .fn(t => { const kSize = 128; const module = t.device.createShaderModule({ code: ` struct Uniforms { index: u32, }; @group(0) @binding(0) var uniforms: Uniforms; @vertex fn vmain() -> @builtin(position) vec4 { let index = uniforms.index; let position = vec2(f32(index % ${kSize}u), f32(index / ${kSize}u)); let r = vec2(1.0 / f32(${kSize})); let a = 2.0 * r; let b = r - vec2(1.0); return vec4(fma(position, a, b), 0.0, 1.0); } @fragment fn fmain() -> @location(0) vec4 { return vec4(1.0, 0.0, 1.0, 1.0); } `, }); const layout = t.device.createBindGroupLayout({ entries: [ { binding: 0, visibility: GPUShaderStage.VERTEX, buffer: { type: 'uniform' }, }, ], }); const pipeline = t.device.createRenderPipeline({ layout: t.device.createPipelineLayout({ bindGroupLayouts: [layout] }), vertex: { module, entryPoint: 'vmain', buffers: [] }, primitive: { topology: 'point-list' }, fragment: { targets: [{ format: 'rgba8unorm' }], module, entryPoint: 'fmain', }, }); const renderTarget = t.device.createTexture({ size: [kSize, kSize], usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC, format: 'rgba8unorm', }); const renderPassDescriptor: GPURenderPassDescriptor = { colorAttachments: [ { view: renderTarget.createView(), loadOp: 'load', storeOp: 'store', }, ], }; const encoder = t.device.createCommandEncoder(); const pass = encoder.beginRenderPass(renderPassDescriptor); pass.setPipeline(pipeline); range(kSize * kSize, i => { const buffer = t.device.createBuffer({ size: 4, usage: GPUBufferUsage.UNIFORM, mappedAtCreation: true, }); new Uint32Array(buffer.getMappedRange())[0] = i; buffer.unmap(); pass.setBindGroup( 0, t.device.createBindGroup({ layout, entries: [{ binding: 0, resource: { buffer } }] }) ); pass.draw(1, 1); }); pass.end(); t.device.queue.submit([encoder.finish()]); t.expectSingleColor(renderTarget, 'rgba8unorm', { size: [kSize, kSize, 1], exp: { R: 1, G: 0, B: 1, A: 1 }, }); }); g.test('many_draws') .desc( `Tests execution of render passes with a huge number of draw calls. This uses a single render pass with a single pipeline, and one draw call per fragment of the output texture.` ) .fn(t => { const kSize = 4096; const module = t.device.createShaderModule({ code: ` @vertex fn vmain(@builtin(vertex_index) index: u32) -> @builtin(position) vec4 { let position = vec2(f32(index % ${kSize}u), f32(index / ${kSize}u)); let r = vec2(1.0 / f32(${kSize})); let a = 2.0 * r; let b = r - vec2(1.0); return vec4(fma(position, a, b), 0.0, 1.0); } @fragment fn fmain() -> @location(0) vec4 { return vec4(1.0, 0.0, 1.0, 1.0); } `, }); const pipeline = t.device.createRenderPipeline({ layout: 'auto', vertex: { module, entryPoint: 'vmain', buffers: [] }, primitive: { topology: 'point-list' }, fragment: { targets: [{ format: 'rgba8unorm' }], module, entryPoint: 'fmain', }, }); const renderTarget = t.device.createTexture({ size: [kSize, kSize], usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC, format: 'rgba8unorm', }); const renderPassDescriptor: GPURenderPassDescriptor = { colorAttachments: [ { view: renderTarget.createView(), loadOp: 'load', storeOp: 'store', }, ], }; const encoder = t.device.createCommandEncoder(); const pass = encoder.beginRenderPass(renderPassDescriptor); pass.setPipeline(pipeline); range(kSize * kSize, i => pass.draw(1, 1, i)); pass.end(); t.device.queue.submit([encoder.finish()]); t.expectSingleColor(renderTarget, 'rgba8unorm', { size: [kSize, kSize, 1], exp: { R: 1, G: 0, B: 1, A: 1 }, }); }); g.test('huge_draws') .desc( `Tests execution of several render passes with huge draw calls. Each pass uses a single draw call which draws multiple vertices for each fragment of a large output texture.` ) .fn(t => { const kSize = 32768; const kTextureSize = 4096; const kVertsPerFragment = (kSize * kSize) / (kTextureSize * kTextureSize); const module = t.device.createShaderModule({ code: ` @vertex fn vmain(@builtin(vertex_index) vert_index: u32) -> @builtin(position) vec4 { let index = vert_index / ${kVertsPerFragment}u; let position = vec2(f32(index % ${kTextureSize}u), f32(index / ${kTextureSize}u)); let r = vec2(1.0 / f32(${kTextureSize})); let a = 2.0 * r; let b = r - vec2(1.0); return vec4(fma(position, a, b), 0.0, 1.0); } @fragment fn fmain() -> @location(0) vec4 { return vec4(1.0, 0.0, 1.0, 1.0); } `, }); const pipeline = t.device.createRenderPipeline({ layout: 'auto', vertex: { module, entryPoint: 'vmain', buffers: [] }, primitive: { topology: 'point-list' }, fragment: { targets: [{ format: 'rgba8unorm' }], module, entryPoint: 'fmain', }, }); const renderTarget = t.device.createTexture({ size: [kTextureSize, kTextureSize], usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC, format: 'rgba8unorm', }); const renderPassDescriptor: GPURenderPassDescriptor = { colorAttachments: [ { view: renderTarget.createView(), loadOp: 'load', storeOp: 'store', }, ], }; const encoder = t.device.createCommandEncoder(); const pass = encoder.beginRenderPass(renderPassDescriptor); pass.setPipeline(pipeline); pass.draw(kSize * kSize); pass.end(); t.device.queue.submit([encoder.finish()]); t.expectSingleColor(renderTarget, 'rgba8unorm', { size: [kTextureSize, kTextureSize, 1], exp: { R: 1, G: 0, B: 1, A: 1 }, }); });