From 40d0e32190eb3bcc9c71b408d7b67302477c05ad Mon Sep 17 00:00:00 2001 From: Greggman Date: Tue, 17 Dec 2024 14:50:00 -0800 Subject: [PATCH] Refactor discard test for no storage buffers (#4093) --- .../execution/statement/discard.spec.ts | 261 ++++++++++++------ 1 file changed, 182 insertions(+), 79 deletions(-) diff --git a/src/webgpu/shader/execution/statement/discard.spec.ts b/src/webgpu/shader/execution/statement/discard.spec.ts index 53f03b7edca0..92bf53ad01d2 100644 --- a/src/webgpu/shader/execution/statement/discard.spec.ts +++ b/src/webgpu/shader/execution/statement/discard.spec.ts @@ -15,10 +15,10 @@ Conditions that still occur: import { makeTestGroup } from '../../../../common/framework/test_group.js'; import { iterRange } from '../../../../common/util/util.js'; -import { GPUTest } from '../../../gpu_test.js'; +import { GPUTest, MaxLimitsTestMixin } from '../../../gpu_test.js'; import { checkElementsPassPredicate } from '../../../util/check_contents.js'; -export const g = makeTestGroup(GPUTest); +export const g = makeTestGroup(MaxLimitsTestMixin(GPUTest)); // Framebuffer dimensions const kWidth = 64; @@ -27,7 +27,7 @@ const kHeight = 64; const kSharedCode = ` @group(0) @binding(0) var output: array; @group(0) @binding(1) var atomicIndex : atomic; -@group(0) @binding(2) var uniformValues : array; +@group(0) @binding(2) var uniformValues : array; @vertex fn vsMain(@builtin(vertex_index) index : u32) -> @builtin(position) vec4f { @@ -51,9 +51,17 @@ fn vsMain(@builtin(vertex_index) index : u32) -> @builtin(position) vec4f { function drawFullScreen( t: GPUTest, code: string, + useStorageBuffers: boolean, dataChecker: (a: Float32Array) => Error | undefined, framebufferChecker: (a: Uint32Array) => Error | undefined ) { + t.skipIf( + useStorageBuffers && + t.isCompatibility && + !(t.device.limits.maxStorageBuffersInFragmentStage! >= 2), + `maxStorageBuffersInFragmentStage${t.device.limits.maxStorageBuffersInFragmentStage} is less than 2` + ); + const pipeline = t.device.createRenderPipeline({ layout: 'auto', vertex: { @@ -63,7 +71,7 @@ function drawFullScreen( fragment: { module: t.device.createShaderModule({ code }), entryPoint: 'fsMain', - targets: [{ format: 'r32uint' }], + targets: [{ format: 'rg32uint' }], }, primitive: { topology: 'triangle-list', @@ -78,13 +86,13 @@ function drawFullScreen( GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING, - format: 'r32uint', + format: 'rg32uint', }); // Create a buffer to copy the framebuffer contents into. // Initialize with a sentinel value and load this buffer to detect unintended writes. const fbBuffer = t.makeBufferWithContents( - new Uint32Array([...iterRange(kWidth * kHeight, x => kWidth * kHeight)]), + new Uint32Array([...iterRange(kWidth * kHeight * 2, x => kWidth * kHeight)]), GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST ); @@ -98,33 +106,44 @@ function drawFullScreen( usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST | GPUBufferUsage.STORAGE, }); - const uniformSize = bytesPerWord * 5; + const uniformSize = bytesPerWord * 5 * 4; const uniformBuffer = t.makeBufferWithContents( // Loop bound, [derivative constants]. - new Uint32Array([4, 1, 4, 4, 7]), - GPUBufferUsage.COPY_DST | GPUBufferUsage.STORAGE + // prettier-ignore + new Uint32Array([ + 4, 0, 0, 0, + 1, 0, 0, 0, + 4, 0, 0, 0, + 4, 0, 0, 0, + 7, 0, 0, 0, + ]), + GPUBufferUsage.COPY_DST | GPUBufferUsage.UNIFORM ); // 'atomicIndex' packed at the end of the buffer. const bg = t.device.createBindGroup({ layout: pipeline.getBindGroupLayout(0), entries: [ - { - binding: 0, - resource: { - buffer: dataBuffer, - offset: 0, - size: dataSize, - }, - }, - { - binding: 1, - resource: { - buffer: dataBuffer, - offset: dataSize, - size: bytesPerWord, - }, - }, + ...(useStorageBuffers + ? [ + { + binding: 0, + resource: { + buffer: dataBuffer, + offset: 0, + size: dataSize, + }, + }, + { + binding: 1, + resource: { + buffer: dataBuffer, + offset: dataSize, + size: bytesPerWord, + }, + }, + ] + : []), { binding: 2, resource: { @@ -141,7 +160,7 @@ function drawFullScreen( { buffer: fbBuffer, offset: 0, - bytesPerRow: kWidth * bytesPerWord, + bytesPerRow: kWidth * bytesPerWord * 2, rowsPerImage: kHeight, }, { texture: framebuffer }, @@ -165,37 +184,47 @@ function drawFullScreen( { buffer: fbBuffer, offset: 0, - bytesPerRow: kWidth * bytesPerWord, + bytesPerRow: kWidth * bytesPerWord * 2, rowsPerImage: kHeight, }, { width: kWidth, height: kHeight } ); t.queue.submit([encoder.finish()]); - t.expectGPUBufferValuesPassCheck(dataBuffer, dataChecker, { - type: Float32Array, - typedLength: dataSize / bytesPerWord, - }); + if (useStorageBuffers) { + t.expectGPUBufferValuesPassCheck(dataBuffer, dataChecker, { + type: Float32Array, + typedLength: dataSize / bytesPerWord, + }); + } t.expectGPUBufferValuesPassCheck(fbBuffer, framebufferChecker, { type: Uint32Array, - typedLength: kWidth * kHeight, + typedLength: kWidth * kHeight * 2, }); } g.test('all') .desc('Test a shader that discards all fragments') + .params(u => u.combine('useStorageBuffers', [false, true])) .fn(t => { + const { useStorageBuffers } = t.params; const code = ` ${kSharedCode} @fragment -fn fsMain(@builtin(position) pos : vec4f) -> @location(0) u32 { +fn fsMain(@builtin(position) pos : vec4f) -> @location(0) vec2u { _ = uniformValues[0]; discard; + ${ + useStorageBuffers + ? ` let idx = atomicAdd(&atomicIndex, 1); output[idx] = pos.xy; - return 1; + ` + : '' + } + return vec2u(1); } `; @@ -239,24 +268,35 @@ fn fsMain(@builtin(position) pos : vec4f) -> @location(0) u32 { ); }; - drawFullScreen(t, code, dataChecker, fbChecker); + drawFullScreen(t, code, useStorageBuffers, dataChecker, fbChecker); }); g.test('three_quarters') .desc('Test a shader that discards all but the upper-left quadrant fragments') + .params(u => u.combine('useStorageBuffers', [false, true])) .fn(t => { + const { useStorageBuffers } = t.params; const code = ` ${kSharedCode} @fragment -fn fsMain(@builtin(position) pos : vec4f) -> @location(0) u32 { +fn fsMain(@builtin(position) pos : vec4f) -> @location(0) vec2u { _ = uniformValues[0]; if (pos.x >= 0.5 * ${kWidth} || pos.y >= 0.5 * ${kHeight}) { discard; } + ${ + useStorageBuffers + ? ` let idx = atomicAdd(&atomicIndex, 1); output[idx] = pos.xy; - return idx; + return vec2u(idx); + ` + : ` + return vec2(u32(pos.x), u32(pos.y)); + + ` + } } `; @@ -301,17 +341,22 @@ fn fsMain(@builtin(position) pos : vec4f) -> @location(0) u32 { ); }; const fbChecker = (a: Uint32Array) => { + const discarded = (x: number, y: number) => x >= kWidth / 2 || y >= kHeight / 2; return checkElementsPassPredicate( a, (idx: number, value: number | bigint) => { - const x = idx % kWidth; - const y = Math.floor(idx / kWidth); - if (x < kWidth / 2 && y < kHeight / 2) { - return value < (kWidth * kHeight) / 4; - } else { + const fragId = (idx / 2) | 0; + const x = fragId % kWidth; + const y = (fragId / kWidth) | 0; + if (discarded(x, y)) { return value === kWidth * kHeight; + } else { + if (useStorageBuffers) { + return value < (kWidth * kHeight) / 4; + } else { + return value === (idx % 2 ? y : x); + } } - return value < (kWidth * kHeight) / 4; }, { predicatePrinter: [ @@ -320,10 +365,10 @@ fn fsMain(@builtin(position) pos : vec4f) -> @location(0) u32 { getValueForCell: (idx: number) => { const x = idx % kWidth; const y = Math.floor(idx / kWidth); - if (x < kWidth / 2 && y < kHeight / 2) { - return 'any'; - } else { + if (discarded(x, y)) { return 0; + } else { + return useStorageBuffers ? 'any' : idx % 2 ? y : x; } }, }, @@ -332,12 +377,14 @@ fn fsMain(@builtin(position) pos : vec4f) -> @location(0) u32 { ); }; - drawFullScreen(t, code, dataChecker, fbChecker); + drawFullScreen(t, code, useStorageBuffers, dataChecker, fbChecker); }); g.test('function_call') .desc('Test discards happening in a function call') + .params(u => u.combine('useStorageBuffers', [false, true])) .fn(t => { + const { useStorageBuffers } = t.params; const code = ` ${kSharedCode} @@ -352,12 +399,20 @@ fn foo(pos : vec2f) { } @fragment -fn fsMain(@builtin(position) pos : vec4f) -> @location(0) u32 { +fn fsMain(@builtin(position) pos : vec4f) -> @location(0) vec2u { _ = uniformValues[0]; foo(pos.xy); + ${ + useStorageBuffers + ? ` let idx = atomicAdd(&atomicIndex, 1); output[idx] = pos.xy; - return idx; + return vec2u(idx); + ` + : ` + return vec2u(u32(pos.x), u32(pos.y)); + ` + } } `; @@ -396,15 +451,22 @@ fn fsMain(@builtin(position) pos : vec4f) -> @location(0) u32 { ); }; const fbChecker = (a: Uint32Array) => { + const discarded = (x: number, y: number) => + (x >= kWidth / 2 && y >= kHeight / 2) || (x <= kWidth / 2 && y <= kHeight / 2); return checkElementsPassPredicate( a, (idx: number, value: number | bigint) => { - const x = idx % kWidth; - const y = Math.floor(idx / kWidth); - if ((x >= kWidth / 2 && y >= kHeight / 2) || (x <= kWidth / 2 && y <= kHeight / 2)) { + const fragId = (idx / 2) | 0; + const x = fragId % kWidth; + const y = (fragId / kWidth) | 0; + if (discarded(x, y)) { return value === kWidth * kHeight; } else { - return value < (kWidth * kHeight) / 2; + if (useStorageBuffers) { + return value < (kWidth * kHeight) / 2; + } else { + return value === (idx % 2 ? y : x); + } } }, { @@ -414,13 +476,10 @@ fn fsMain(@builtin(position) pos : vec4f) -> @location(0) u32 { getValueForCell: (idx: number) => { const x = idx % kWidth; const y = Math.floor(idx / kWidth); - if ( - (x <= kWidth / 2 && y <= kHeight / 2) || - (x >= kWidth / 2 && y >= kHeight / 2) - ) { + if (discarded(x, y)) { return kWidth * kHeight; } - return 'any'; + return useStorageBuffers ? 'any' : idx % 2 ? y : x; }, }, ], @@ -428,26 +487,34 @@ fn fsMain(@builtin(position) pos : vec4f) -> @location(0) u32 { ); }; - drawFullScreen(t, code, dataChecker, fbChecker); + drawFullScreen(t, code, useStorageBuffers, dataChecker, fbChecker); }); g.test('loop') .desc('Test discards in a loop') + .params(u => u.combine('useStorageBuffers', [false, true])) .fn(t => { + const { useStorageBuffers } = t.params; const code = ` ${kSharedCode} @fragment -fn fsMain(@builtin(position) pos : vec4f) -> @location(0) u32 { +fn fsMain(@builtin(position) pos : vec4f) -> @location(0) vec2u { _ = uniformValues[0]; for (var i = 0; i < 2; i++) { if i > 0 { discard; } } + ${ + useStorageBuffers + ? ` let idx = atomicAdd(&atomicIndex, 1); output[idx] = pos.xy; - return 1; + ` + : '' + } + return vec2u(1); } `; @@ -491,17 +558,19 @@ fn fsMain(@builtin(position) pos : vec4f) -> @location(0) u32 { ); }; - drawFullScreen(t, code, dataChecker, fbChecker); + drawFullScreen(t, code, useStorageBuffers, dataChecker, fbChecker); }); g.test('continuing') .desc('Test discards in a loop') + .params(u => u.combine('useStorageBuffers', [false, true])) .fn(t => { + const { useStorageBuffers } = t.params; const code = ` ${kSharedCode} @fragment -fn fsMain(@builtin(position) pos : vec4f) -> @location(0) u32 { +fn fsMain(@builtin(position) pos : vec4f) -> @location(0) vec2u { _ = uniformValues[0]; var i = 0; loop { @@ -513,9 +582,15 @@ fn fsMain(@builtin(position) pos : vec4f) -> @location(0) u32 { break if i >= 2; } } + ${ + useStorageBuffers + ? ` let idx = atomicAdd(&atomicIndex, 1); output[idx] = pos.xy; - return 1; + ` + : '' + } + return vec2u(1); } `; @@ -559,23 +634,31 @@ fn fsMain(@builtin(position) pos : vec4f) -> @location(0) u32 { ); }; - drawFullScreen(t, code, dataChecker, fbChecker); + drawFullScreen(t, code, useStorageBuffers, dataChecker, fbChecker); }); g.test('uniform_read_loop') .desc('Test that helpers read a uniform value in a loop') + .params(u => u.combine('useStorageBuffers', [false, true])) .fn(t => { + const { useStorageBuffers } = t.params; const code = ` ${kSharedCode} @fragment -fn fsMain(@builtin(position) pos : vec4f) -> @location(0) u32 { +fn fsMain(@builtin(position) pos : vec4f) -> @location(0) vec2u { discard; - for (var i = 0u; i < uniformValues[0]; i++) { + for (var i = 0u; i < uniformValues[0].x; i++) { } + ${ + useStorageBuffers + ? ` let idx = atomicAdd(&atomicIndex, 1); output[idx] = pos.xy; - return 1; + ` + : '' + } + return vec2u(1); } `; @@ -619,17 +702,19 @@ fn fsMain(@builtin(position) pos : vec4f) -> @location(0) u32 { ); }; - drawFullScreen(t, code, dataChecker, fbChecker); + drawFullScreen(t, code, useStorageBuffers, dataChecker, fbChecker); }); g.test('derivatives') .desc('Test that derivatives are correct in the presence of discard') + .params(u => u.combine('useStorageBuffers', [false, true])) .fn(t => { + const { useStorageBuffers } = t.params; const code = ` ${kSharedCode} @fragment -fn fsMain(@builtin(position) pos : vec4f) -> @location(0) u32 { +fn fsMain(@builtin(position) pos : vec4f) -> @location(0) vec2u { let ipos = vec2i(pos.xy); let lsb = ipos & vec2(0x1); let left_sel = select(2, 4, lsb.y == 1); @@ -639,12 +724,20 @@ fn fsMain(@builtin(position) pos : vec4f) -> @location(0) u32 { discard; } - let v = uniformValues[uidx]; - let idx = atomicAdd(&atomicIndex, 1); + let v = uniformValues[uidx].x; let dx = dpdx(f32(v)); let dy = dpdy(f32(v)); + ${ + useStorageBuffers + ? ` + let idx = atomicAdd(&atomicIndex, 1); output[idx] = vec2(dx, dy); - return idx; + return vec2u(idx); + ` + : ` + return bitcast(vec2f(dx, dy)); + ` + } } `; @@ -678,15 +771,25 @@ fn fsMain(@builtin(position) pos : vec4f) -> @location(0) u32 { // 3/4 of the fragments are written. const fbChecker = (a: Uint32Array) => { + const discarded = (x: number, y: number) => ((x | y) & 0x1) === 0; + const asF32 = new Float32Array(1); + const asU32 = new Uint32Array(asF32.buffer); return checkElementsPassPredicate( a, (idx: number, value: number | bigint) => { - const x = idx % kWidth; - const y = Math.floor(idx / kWidth); - if (((x | y) & 0x1) === 0) { + const fragId = (idx / 2) | 0; + const x = fragId % kWidth; + const y = (fragId / kWidth) | 0; + if (discarded(x, y)) { return value === kWidth * kHeight; } else { - return value < (3 * (kWidth * kHeight)) / 4; + if (useStorageBuffers) { + return value < (3 * (kWidth * kHeight)) / 4; + } else { + asU32[0] = value as number; + const v = asF32[0]; + return v === -3 || v === 3; + } } }, { @@ -699,7 +802,7 @@ fn fsMain(@builtin(position) pos : vec4f) -> @location(0) u32 { if (((x | y) & 0x1) === 0) { return kWidth * kHeight; } else { - return 'any'; + return useStorageBuffers ? 'any' : '+/- 3'; } }, }, @@ -708,5 +811,5 @@ fn fsMain(@builtin(position) pos : vec4f) -> @location(0) u32 { ); }; - drawFullScreen(t, code, dataChecker, fbChecker); + drawFullScreen(t, code, useStorageBuffers, dataChecker, fbChecker); });