export const description = ` Execution tests for discard. The discard statement converts invocations into helpers. This results in the following conditions: * No outputs are written * No resources are written * Atomics are undefined Conditions that still occur: * Derivative calculations are correct * Reads * Writes to non-external memory `; import { makeTestGroup } from '../../../../common/framework/test_group.js'; import { iterRange } from '../../../../common/util/util.js'; import { AllFeaturesMaxLimitsGPUTest, GPUTestBase } from '../../../gpu_test.js'; import { checkElementsPassPredicate } from '../../../util/check_contents.js'; export const g = makeTestGroup(AllFeaturesMaxLimitsGPUTest); // Framebuffer dimensions const kWidth = 64; 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; @vertex fn vsMain(@builtin(vertex_index) index : u32) -> @builtin(position) vec4f { const vertices = array( vec2(-1, -1), vec2(-1, 0), vec2( 0, -1), vec2(-1, 0), vec2( 0, 0), vec2( 0, -1), vec2( 0, -1), vec2( 0, 0), vec2( 1, -1), vec2( 0, 0), vec2( 1, 0), vec2( 1, -1), vec2(-1, 0), vec2(-1, 1), vec2( 0, 0), vec2(-1, 1), vec2( 0, 1), vec2( 0, 0), vec2( 0, 0), vec2( 0, 1), vec2( 1, 0), vec2( 0, 1), vec2( 1, 1), vec2( 1, 0), ); return vec4f(vec2f(vertices[index]), 0, 1); } `; function drawFullScreen( t: GPUTestBase, 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: { module: t.device.createShaderModule({ code }), entryPoint: 'vsMain', }, fragment: { module: t.device.createShaderModule({ code }), entryPoint: 'fsMain', targets: [{ format: 'rg32uint' }], }, primitive: { topology: 'triangle-list', }, }); const bytesPerWord = 4; const framebuffer = t.createTextureTracked({ size: [kWidth, kHeight], usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING, 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 * 2, x => kWidth * kHeight)]), GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST ); // Create a buffer to hold the storage shader resources. // (0,0) = vec2u width * height // (0,1) = u32 const dataSize = 2 * kWidth * kHeight * bytesPerWord; const dataBufferSize = dataSize + bytesPerWord; const dataBuffer = t.createBufferTracked({ size: dataBufferSize, usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST | GPUBufferUsage.STORAGE, }); const uniformSize = bytesPerWord * 5 * 4; const uniformBuffer = t.makeBufferWithContents( // Loop bound, [derivative constants]. // 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: [ ...(useStorageBuffers ? [ { binding: 0, resource: { buffer: dataBuffer, offset: 0, size: dataSize, }, }, { binding: 1, resource: { buffer: dataBuffer, offset: dataSize, size: bytesPerWord, }, }, ] : []), { binding: 2, resource: { buffer: uniformBuffer, offset: 0, size: uniformSize, }, }, ], }); const encoder = t.device.createCommandEncoder(); encoder.copyBufferToTexture( { buffer: fbBuffer, offset: 0, bytesPerRow: kWidth * bytesPerWord * 2, rowsPerImage: kHeight, }, { texture: framebuffer }, { width: kWidth, height: kHeight } ); const pass = encoder.beginRenderPass({ colorAttachments: [ { view: framebuffer.createView(), loadOp: 'load', storeOp: 'store', }, ], }); pass.setPipeline(pipeline); pass.setBindGroup(0, bg); pass.draw(24); pass.end(); encoder.copyTextureToBuffer( { texture: framebuffer }, { buffer: fbBuffer, offset: 0, bytesPerRow: kWidth * bytesPerWord * 2, rowsPerImage: kHeight, }, { width: kWidth, height: kHeight } ); t.queue.submit([encoder.finish()]); if (useStorageBuffers) { t.expectGPUBufferValuesPassCheck(dataBuffer, dataChecker, { type: Float32Array, typedLength: dataSize / bytesPerWord, }); } t.expectGPUBufferValuesPassCheck(fbBuffer, framebufferChecker, { type: Uint32Array, 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) vec2u { _ = uniformValues[0]; discard; ${ useStorageBuffers ? ` let idx = atomicAdd(&atomicIndex, 1); output[idx] = pos.xy; ` : '' } return vec2u(1); } `; // No storage writes occur. const dataChecker = (a: Float32Array) => { return checkElementsPassPredicate( a, (idx: number, value: number | bigint) => { return value === 0; }, { predicatePrinter: [ { leftHeader: 'data exp ==', getValueForCell: (idx: number) => { return 0; }, }, ], } ); }; // No fragment outputs occur. const fbChecker = (a: Uint32Array) => { return checkElementsPassPredicate( a, (idx: number, value: number | bigint) => { return value === kWidth * kHeight; }, { predicatePrinter: [ { leftHeader: 'fb exp ==', getValueForCell: (idx: number) => { return 0; }, }, ], } ); }; 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) 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 vec2u(idx); ` : ` return vec2(u32(pos.x), u32(pos.y)); ` } } `; // Only the the upper left quadrant is kept. const dataChecker = (a: Float32Array) => { return checkElementsPassPredicate( a, (idx: number, value: number | bigint) => { const is_x = idx % 2 === 0; if (is_x) { return value < 0.5 * kWidth; } else { return value < 0.5 * kHeight; } }, { predicatePrinter: [ { leftHeader: 'data exp ==', getValueForCell: (idx: number): number | string => { const is_x = idx % 2 === 0; if (is_x) { const x = Math.floor(idx / 2) % kWidth; if (x >= kWidth / 2) { return 0; } } else { const y = Math.floor((idx - 1) / kWidth); if (y >= kHeight / 2) { return 0; } } if (is_x) { return `< ${0.5 * kWidth}`; } else { return `< ${0.5 * kHeight}`; } }, }, ], } ); }; 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 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); } } }, { predicatePrinter: [ { leftHeader: 'fb exp ==', getValueForCell: (idx: number) => { const x = idx % kWidth; const y = Math.floor(idx / kWidth); if (discarded(x, y)) { return 0; } else { return useStorageBuffers ? 'any' : idx % 2 ? y : x; } }, }, ], } ); }; 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} fn foo(pos : vec2f) { let p = vec2i(pos); if p.x <= ${kWidth} / 2 && p.y <= ${kHeight} / 2 { discard; } if p.x >= ${kWidth} / 2 && p.y >= ${kHeight} / 2 { discard; } } @fragment 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 vec2u(idx); ` : ` return vec2u(u32(pos.x), u32(pos.y)); ` } } `; // Only the upper right and bottom left quadrants are kept. const dataChecker = (a: Float32Array) => { return checkElementsPassPredicate( a, (idx: number, value: number | bigint) => { const is_x = idx % 2 === 0; if (value === 0.0) { return is_x ? a[idx + 1] === 0 : a[idx - 1] === 0; } let expect = is_x ? kWidth : kHeight; expect = 0.5 * expect; if (value < expect) { return is_x ? a[idx + 1] > 0.5 * kWidth : a[idx - 1] > 0.5 * kHeight; } else { return is_x ? a[idx + 1] < 0.5 * kWidth : a[idx - 1] < 0.5 * kHeight; } }, { predicatePrinter: [ { leftHeader: 'data exp ==', getValueForCell: (idx: number): number | string => { if (idx < (kWidth * kHeight) / 2) { return 'any'; } else { return 0; } }, }, ], } ); }; 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 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) / 2; } else { return value === (idx % 2 ? y : x); } } }, { predicatePrinter: [ { leftHeader: 'fb exp ==', getValueForCell: (idx: number) => { const x = idx % kWidth; const y = Math.floor(idx / kWidth); if (discarded(x, y)) { return kWidth * kHeight; } return useStorageBuffers ? 'any' : idx % 2 ? y : x; }, }, ], } ); }; 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) 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 vec2u(1); } `; // No storage writes occur. const dataChecker = (a: Float32Array) => { return checkElementsPassPredicate( a, (idx: number, value: number | bigint) => { return value === 0; }, { predicatePrinter: [ { leftHeader: 'data exp ==', getValueForCell: (idx: number) => { return 0; }, }, ], } ); }; // No fragment outputs occur. const fbChecker = (a: Uint32Array) => { return checkElementsPassPredicate( a, (idx: number, value: number | bigint) => { return value === kWidth * kHeight; }, { predicatePrinter: [ { leftHeader: 'fb exp ==', getValueForCell: (idx: number) => { return kWidth * kHeight; }, }, ], } ); }; 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) vec2u { _ = uniformValues[0]; var i = 0; loop { continuing { if i > 0 { discard; } i++; break if i >= 2; } } ${ useStorageBuffers ? ` let idx = atomicAdd(&atomicIndex, 1); output[idx] = pos.xy; ` : '' } return vec2u(1); } `; // No storage writes occur. const dataChecker = (a: Float32Array) => { return checkElementsPassPredicate( a, (idx: number, value: number | bigint) => { return value === 0; }, { predicatePrinter: [ { leftHeader: 'data exp ==', getValueForCell: (idx: number) => { return 0; }, }, ], } ); }; // No fragment outputs occur. const fbChecker = (a: Uint32Array) => { return checkElementsPassPredicate( a, (idx: number, value: number | bigint) => { return value === kWidth * kHeight; }, { predicatePrinter: [ { leftHeader: 'fb exp ==', getValueForCell: (idx: number) => { return kWidth * kHeight; }, }, ], } ); }; 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) vec2u { discard; for (var i = 0u; i < uniformValues[0].x; i++) { } ${ useStorageBuffers ? ` let idx = atomicAdd(&atomicIndex, 1); output[idx] = pos.xy; ` : '' } return vec2u(1); } `; // No storage writes occur. const dataChecker = (a: Float32Array) => { return checkElementsPassPredicate( a, (idx: number, value: number | bigint) => { return value === 0; }, { predicatePrinter: [ { leftHeader: 'data exp ==', getValueForCell: (idx: number) => { return 0; }, }, ], } ); }; // No fragment outputs occur. const fbChecker = (a: Uint32Array) => { return checkElementsPassPredicate( a, (idx: number, value: number | bigint) => { return value === kWidth * kHeight; }, { predicatePrinter: [ { leftHeader: 'fb exp ==', getValueForCell: (idx: number) => { return kWidth * kHeight; }, }, ], } ); }; 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) vec2u { let ipos = vec2i(pos.xy); let lsb = ipos & vec2(0x1); let left_sel = select(2, 4, lsb.y == 1); let right_sel = select(1, 3, lsb.y == 1); let uidx = select(left_sel, right_sel, lsb.x == 1); if ((lsb.x | lsb.y) & 0x1) == 0 { discard; } 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 vec2u(idx); ` : ` return bitcast(vec2f(dx, dy)); ` } } `; // One pixel per quad is discarded. The derivatives values are always the same +/- 3. const dataChecker = (a: Float32Array) => { return checkElementsPassPredicate( a, (idx: number, value: number | bigint) => { if (idx < (3 * (2 * kWidth * kHeight)) / 4) { return value === -3 || value === 3; } else { return value === 0; } }, { predicatePrinter: [ { leftHeader: 'data exp ==', getValueForCell: (idx: number) => { if (idx < (3 * (2 * kWidth * kHeight)) / 4) { return '+/- 3'; } else { return 0; } }, }, ], } ); }; // 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 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 < (3 * (kWidth * kHeight)) / 4; } else { asU32[0] = value as number; const v = asF32[0]; return v === -3 || v === 3; } } }, { predicatePrinter: [ { leftHeader: 'fb exp ==', getValueForCell: (idx: number) => { const x = idx % kWidth; const y = Math.floor(idx / kWidth); if (((x | y) & 0x1) === 0) { return kWidth * kHeight; } else { return useStorageBuffers ? 'any' : '+/- 3'; } }, }, ], } ); }; drawFullScreen(t, code, useStorageBuffers, dataChecker, fbChecker); });