diff options
Diffstat (limited to 'dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/rendering/stencil.spec.ts')
-rw-r--r-- | dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/rendering/stencil.spec.ts | 583 |
1 files changed, 583 insertions, 0 deletions
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/rendering/stencil.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/rendering/stencil.spec.ts new file mode 100644 index 0000000000..5985616a54 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/rendering/stencil.spec.ts @@ -0,0 +1,583 @@ +export const description = ` +Test related to stencil states, stencil op, compare func, etc. +`; + +import { makeTestGroup } from '../../../../common/framework/test_group.js'; +import { TypedArrayBufferView } from '../../../../common/util/util.js'; +import { + DepthStencilFormat, + kDepthStencilFormats, + kTextureFormatInfo, +} from '../../../capability_info.js'; +import { GPUTest } from '../../../gpu_test.js'; +import { TexelView } from '../../../util/texture/texel_view.js'; +import { textureContentIsOKByT2B } from '../../../util/texture/texture_ok.js'; + +const kStencilFormats = kDepthStencilFormats.filter(format => kTextureFormatInfo[format].stencil); + +const kBaseColor = new Float32Array([1.0, 1.0, 1.0, 1.0]); +const kRedStencilColor = new Float32Array([1.0, 0.0, 0.0, 1.0]); +const kGreenStencilColor = new Float32Array([0.0, 1.0, 0.0, 1.0]); + +type TestStates = { + state: GPUDepthStencilState; + color: Float32Array; + stencil: number | undefined; +}; + +class StencilTest extends GPUTest { + checkStencilOperation( + depthStencilFormat: DepthStencilFormat, + testStencilState: GPUStencilFaceState, + initialStencil: number, + _expectedStencil: number, + depthCompare: GPUCompareFunction = 'always' + ) { + const kReferenceStencil = 3; + + const baseStencilState = { + compare: 'always', + failOp: 'keep', + passOp: 'replace', + } as const; + + const stencilState = { + compare: 'equal', + failOp: 'keep', + passOp: 'keep', + } as const; + + const baseState = { + format: depthStencilFormat, + depthWriteEnabled: false, + depthCompare: 'always', + stencilFront: baseStencilState, + stencilBack: baseStencilState, + } as const; + + const testState = { + format: depthStencilFormat, + depthWriteEnabled: false, + depthCompare, + stencilFront: testStencilState, + stencilBack: testStencilState, + } as const; + + const testState2 = { + format: depthStencilFormat, + depthWriteEnabled: false, + depthCompare: 'always', + stencilFront: stencilState, + stencilBack: stencilState, + } as const; + + const testStates = [ + // Draw the base triangle with stencil reference 1. This clears the stencil buffer to 1. + { state: baseState, color: kBaseColor, stencil: initialStencil }, + { state: testState, color: kRedStencilColor, stencil: kReferenceStencil }, + { state: testState2, color: kGreenStencilColor, stencil: _expectedStencil }, + ]; + this.runStencilStateTest(depthStencilFormat, testStates, kGreenStencilColor); + } + + checkStencilCompareFunction( + depthStencilFormat: DepthStencilFormat, + compareFunction: GPUCompareFunction, + stencilRefValue: number, + expectedColor: Float32Array + ) { + const baseStencilState = { + compare: 'always', + failOp: 'keep', + passOp: 'replace', + } as const; + + const stencilState = { + compare: compareFunction, + failOp: 'keep', + passOp: 'keep', + } as const; + + const baseState = { + format: depthStencilFormat, + depthWriteEnabled: false, + depthCompare: 'always', + stencilFront: baseStencilState, + stencilBack: baseStencilState, + } as const; + + const testState = { + format: depthStencilFormat, + depthWriteEnabled: false, + depthCompare: 'always', + stencilFront: stencilState, + stencilBack: stencilState, + } as const; + + const testStates = [ + // Draw the base triangle with stencil reference 1. This clears the stencil buffer to 1. + { state: baseState, color: kBaseColor, stencil: 1 }, + { state: testState, color: kGreenStencilColor, stencil: stencilRefValue }, + ]; + this.runStencilStateTest(depthStencilFormat, testStates, expectedColor); + } + + runStencilStateTest( + depthStencilFormat: DepthStencilFormat, + testStates: TestStates[], + expectedColor: Float32Array, + isSingleEncoderMultiplePass: boolean = false + ) { + const renderTargetFormat = 'rgba8unorm'; + const renderTarget = this.device.createTexture({ + format: renderTargetFormat, + size: { width: 1, height: 1, depthOrArrayLayers: 1 }, + usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT, + }); + + const depthTexture = this.device.createTexture({ + size: { width: 1, height: 1, depthOrArrayLayers: 1 }, + format: depthStencilFormat, + sampleCount: 1, + mipLevelCount: 1, + usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_DST, + }); + + const depthStencilAttachment: GPURenderPassDepthStencilAttachment = { + view: depthTexture.createView(), + depthLoadOp: 'load', + depthStoreOp: 'store', + stencilLoadOp: 'load', + stencilStoreOp: 'store', + }; + + const encoder = this.device.createCommandEncoder(); + let pass = encoder.beginRenderPass({ + colorAttachments: [ + { + view: renderTarget.createView(), + storeOp: 'store', + loadOp: 'load', + }, + ], + depthStencilAttachment, + }); + + if (isSingleEncoderMultiplePass) { + pass.end(); + } + + // Draw a triangle with the given stencil reference and the comparison function. + // The color will be kGreenStencilColor if the stencil test passes, and kBaseColor if not. + for (const test of testStates) { + if (isSingleEncoderMultiplePass) { + pass = encoder.beginRenderPass({ + colorAttachments: [ + { + view: renderTarget.createView(), + storeOp: 'store', + loadOp: 'load', + }, + ], + depthStencilAttachment, + }); + } + const testPipeline = this.createRenderPipelineForTest(test.state); + pass.setPipeline(testPipeline); + if (test.stencil !== undefined) { + pass.setStencilReference(test.stencil); + } + pass.setBindGroup( + 0, + this.createBindGroupForTest(testPipeline.getBindGroupLayout(0), test.color) + ); + pass.draw(1); + + if (isSingleEncoderMultiplePass) { + pass.end(); + } + } + + if (!isSingleEncoderMultiplePass) { + pass.end(); + } + this.device.queue.submit([encoder.finish()]); + + const expColor = { + R: expectedColor[0], + G: expectedColor[1], + B: expectedColor[2], + A: expectedColor[3], + }; + const expTexelView = TexelView.fromTexelsAsColors(renderTargetFormat, coords => expColor); + + const result = textureContentIsOKByT2B( + this, + { texture: renderTarget }, + [1, 1], + { expTexelView }, + { maxDiffULPsForNormFormat: 1 } + ); + this.eventualExpectOK(result); + this.trackForCleanup(renderTarget); + } + + createRenderPipelineForTest(depthStencil: GPUDepthStencilState): GPURenderPipeline { + return this.device.createRenderPipeline({ + layout: 'auto', + vertex: { + module: this.device.createShaderModule({ + code: ` + @vertex + fn main(@builtin(vertex_index) VertexIndex : u32) -> @builtin(position) vec4<f32> { + return vec4<f32>(0.0, 0.0, 0.0, 1.0); + } + `, + }), + entryPoint: 'main', + }, + fragment: { + targets: [{ format: 'rgba8unorm' }], + module: this.device.createShaderModule({ + code: ` + struct Params { + color : vec4<f32> + } + @group(0) @binding(0) var<uniform> params : Params; + + @fragment fn main() -> @location(0) vec4<f32> { + return vec4<f32>(params.color); + }`, + }), + entryPoint: 'main', + }, + primitive: { topology: 'point-list' }, + depthStencil, + }); + } + + createBindGroupForTest(layout: GPUBindGroupLayout, data: TypedArrayBufferView): GPUBindGroup { + return this.device.createBindGroup({ + layout, + entries: [ + { + binding: 0, + resource: { + buffer: this.makeBufferWithContents(data, GPUBufferUsage.UNIFORM), + }, + }, + ], + }); + } +} + +export const g = makeTestGroup(StencilTest); + +g.test('stencil_compare_func') + .desc( + ` + Tests that stencil comparison functions with the stencil reference value works as expected. + ` + ) + .params(u => + u // + .combine('format', kStencilFormats) + .combineWithParams([ + { stencilCompare: 'always', stencilRefValue: 0, _expectedColor: kGreenStencilColor }, + { stencilCompare: 'always', stencilRefValue: 1, _expectedColor: kGreenStencilColor }, + { stencilCompare: 'always', stencilRefValue: 2, _expectedColor: kGreenStencilColor }, + { stencilCompare: 'equal', stencilRefValue: 0, _expectedColor: kBaseColor }, + { stencilCompare: 'equal', stencilRefValue: 1, _expectedColor: kGreenStencilColor }, + { stencilCompare: 'equal', stencilRefValue: 2, _expectedColor: kBaseColor }, + { stencilCompare: 'greater', stencilRefValue: 0, _expectedColor: kBaseColor }, + { stencilCompare: 'greater', stencilRefValue: 1, _expectedColor: kBaseColor }, + { stencilCompare: 'greater', stencilRefValue: 2, _expectedColor: kGreenStencilColor }, + { stencilCompare: 'greater-equal', stencilRefValue: 0, _expectedColor: kBaseColor }, + { stencilCompare: 'greater-equal', stencilRefValue: 1, _expectedColor: kGreenStencilColor }, + { stencilCompare: 'greater-equal', stencilRefValue: 2, _expectedColor: kGreenStencilColor }, + { stencilCompare: 'less', stencilRefValue: 0, _expectedColor: kGreenStencilColor }, + { stencilCompare: 'less', stencilRefValue: 1, _expectedColor: kBaseColor }, + { stencilCompare: 'less', stencilRefValue: 2, _expectedColor: kBaseColor }, + { stencilCompare: 'less-equal', stencilRefValue: 0, _expectedColor: kGreenStencilColor }, + { stencilCompare: 'less-equal', stencilRefValue: 1, _expectedColor: kGreenStencilColor }, + { stencilCompare: 'less-equal', stencilRefValue: 2, _expectedColor: kBaseColor }, + { stencilCompare: 'never', stencilRefValue: 0, _expectedColor: kBaseColor }, + { stencilCompare: 'never', stencilRefValue: 1, _expectedColor: kBaseColor }, + { stencilCompare: 'never', stencilRefValue: 2, _expectedColor: kBaseColor }, + { stencilCompare: 'not-equal', stencilRefValue: 0, _expectedColor: kGreenStencilColor }, + { stencilCompare: 'not-equal', stencilRefValue: 1, _expectedColor: kBaseColor }, + { stencilCompare: 'not-equal', stencilRefValue: 2, _expectedColor: kGreenStencilColor }, + ] as const) + ) + .beforeAllSubcases(t => { + t.selectDeviceForTextureFormatOrSkipTestCase(t.params.format); + }) + .fn(async t => { + const { format, stencilCompare, stencilRefValue, _expectedColor } = t.params; + + t.checkStencilCompareFunction(format, stencilCompare, stencilRefValue, _expectedColor); + }); + +g.test('stencil_passOp_operation') + .desc( + ` + Test that the stencil operation is executed on stencil pass. A triangle is drawn with the 'always' + comparison function, so it should pass. Then, test that each pass stencil operation works with the + given stencil values correctly as expected. For example, + - If the pass operation is 'keep', it keeps the initial stencil value. + - If the pass operation is 'replace', it replaces the initial stencil value with the reference + stencil value. + ` + ) + .params(u => + u // + .combine('format', kStencilFormats) + .combineWithParams([ + { passOp: 'keep', initialStencil: 1, _expectedStencil: 1 }, + { passOp: 'zero', initialStencil: 1, _expectedStencil: 0 }, + { passOp: 'replace', initialStencil: 1, _expectedStencil: 3 }, + { passOp: 'invert', initialStencil: 0xf0, _expectedStencil: 0x0f }, + { passOp: 'increment-clamp', initialStencil: 1, _expectedStencil: 2 }, + { passOp: 'increment-clamp', initialStencil: 0xff, _expectedStencil: 0xff }, + { passOp: 'increment-wrap', initialStencil: 1, _expectedStencil: 2 }, + { passOp: 'increment-wrap', initialStencil: 0xff, _expectedStencil: 0 }, + { passOp: 'decrement-clamp', initialStencil: 1, _expectedStencil: 0 }, + { passOp: 'decrement-clamp', initialStencil: 0, _expectedStencil: 0 }, + { passOp: 'decrement-wrap', initialStencil: 1, _expectedStencil: 0 }, + { passOp: 'decrement-wrap', initialStencil: 0, _expectedStencil: 0xff }, + ] as const) + ) + .beforeAllSubcases(t => { + t.selectDeviceForTextureFormatOrSkipTestCase(t.params.format); + }) + .fn(async t => { + const { format, passOp, initialStencil, _expectedStencil } = t.params; + + const stencilState = { + compare: 'always', + failOp: 'keep', + passOp, + } as const; + + t.checkStencilOperation(format, stencilState, initialStencil, _expectedStencil); + }); + +g.test('stencil_failOp_operation') + .desc( + ` + Test that the stencil operation is executed on stencil fail. A triangle is drawn with the 'never' + comparison function, so it should fail. Then, test that each fail stencil operation works with the + given stencil values correctly as expected. For example, + - If the fail operation is 'keep', it keeps the initial stencil value. + - If the fail operation is 'replace', it replaces the initial stencil value with the reference + stencil value. + ` + ) + .params(u => + u // + .combine('format', kStencilFormats) + .combineWithParams([ + { failOp: 'keep', initialStencil: 1, _expectedStencil: 1 }, + { failOp: 'zero', initialStencil: 1, _expectedStencil: 0 }, + { failOp: 'replace', initialStencil: 1, _expectedStencil: 3 }, + { failOp: 'invert', initialStencil: 0xf0, _expectedStencil: 0x0f }, + { failOp: 'increment-clamp', initialStencil: 1, _expectedStencil: 2 }, + { failOp: 'increment-clamp', initialStencil: 0xff, _expectedStencil: 0xff }, + { failOp: 'increment-wrap', initialStencil: 1, _expectedStencil: 2 }, + { failOp: 'increment-wrap', initialStencil: 0xff, _expectedStencil: 0 }, + { failOp: 'decrement-clamp', initialStencil: 1, _expectedStencil: 0 }, + { failOp: 'decrement-clamp', initialStencil: 0, _expectedStencil: 0 }, + { failOp: 'decrement-wrap', initialStencil: 2, _expectedStencil: 1 }, + { failOp: 'decrement-wrap', initialStencil: 1, _expectedStencil: 0 }, + { failOp: 'decrement-wrap', initialStencil: 0, _expectedStencil: 0xff }, + ] as const) + ) + .beforeAllSubcases(t => { + t.selectDeviceForTextureFormatOrSkipTestCase(t.params.format); + }) + .fn(async t => { + const { format, failOp, initialStencil, _expectedStencil } = t.params; + + const stencilState = { + compare: 'never', + failOp, + passOp: 'keep', + } as const; + + // Draw the base triangle with stencil reference 1. This clears the stencil buffer to 1. + // Always fails because the comparison never passes. Therefore red is never drawn, and the + // stencil contents may be updated according to `operation`. + t.checkStencilOperation(format, stencilState, initialStencil, _expectedStencil); + }); + +g.test('stencil_depthFailOp_operation') + .desc( + ` + Test that the stencil operation is executed on depthCompare fail. A triangle is drawn with the + 'never' depthCompare, so it should fail the depth test. Then, test that each 'depthFailOp' stencil operation + works with the given stencil values correctly as expected. For example, + - If the depthFailOp operation is 'keep', it keeps the initial stencil value. + - If the depthFailOp operation is 'replace', it replaces the initial stencil value with the + reference stencil value. + ` + ) + .params(u => + u // + .combine( + 'format', + kDepthStencilFormats.filter(format => { + const info = kTextureFormatInfo[format]; + return info.depth && info.stencil; + }) + ) + .combineWithParams([ + { depthFailOp: 'keep', initialStencil: 1, _expectedStencil: 1 }, + { depthFailOp: 'zero', initialStencil: 1, _expectedStencil: 0 }, + { depthFailOp: 'replace', initialStencil: 1, _expectedStencil: 3 }, + { depthFailOp: 'invert', initialStencil: 0xf0, _expectedStencil: 0x0f }, + { depthFailOp: 'increment-clamp', initialStencil: 1, _expectedStencil: 2 }, + { depthFailOp: 'increment-clamp', initialStencil: 0xff, _expectedStencil: 0xff }, + { depthFailOp: 'increment-wrap', initialStencil: 1, _expectedStencil: 2 }, + { depthFailOp: 'increment-wrap', initialStencil: 0xff, _expectedStencil: 0 }, + { depthFailOp: 'decrement-clamp', initialStencil: 1, _expectedStencil: 0 }, + { depthFailOp: 'decrement-clamp', initialStencil: 0, _expectedStencil: 0 }, + { depthFailOp: 'decrement-wrap', initialStencil: 2, _expectedStencil: 1 }, + { depthFailOp: 'decrement-wrap', initialStencil: 1, _expectedStencil: 0 }, + { depthFailOp: 'decrement-wrap', initialStencil: 0, _expectedStencil: 0xff }, + ] as const) + ) + .beforeAllSubcases(t => { + t.selectDeviceForTextureFormatOrSkipTestCase(t.params.format); + }) + .fn(async t => { + const { format, depthFailOp, initialStencil, _expectedStencil } = t.params; + + const stencilState = { + compare: 'always', + failOp: 'keep', + passOp: 'keep', + depthFailOp, + } as const; + + // Call checkStencilOperation function with enabling the depthTest to test that the depthFailOp + // stencil operation works as expected. + t.checkStencilOperation(format, stencilState, initialStencil, _expectedStencil, 'never'); + }); + +g.test('stencil_read_write_mask') + .desc( + ` + Tests that setting a stencil read/write masks work. Basically, The base triangle sets 3 to the + stencil, and then try to draw a triangle with different stencil values. + - In case that 'write' mask is 1, + * If the stencil of the triangle is 1, it draws because + 'base stencil(3) & write mask(1) == triangle stencil(1)'. + * If the stencil of the triangle is 2, it does not draw because + 'base stencil(3) & write mask(1) != triangle stencil(2)'. + + - In case that 'read' mask is 2, + * If the stencil of the triangle is 1, it does not draw because + 'base stencil(3) & read mask(2) != triangle stencil(1)'. + * If the stencil of the triangle is 2, it draws because + 'base stencil(3) & read mask(2) == triangle stencil(2)'. + ` + ) + .params(u => + u // + .combine('format', kStencilFormats) + .combineWithParams([ + { maskType: 'write', stencilRefValue: 1, _expectedColor: kRedStencilColor }, + { maskType: 'write', stencilRefValue: 2, _expectedColor: kBaseColor }, + { maskType: 'read', stencilRefValue: 1, _expectedColor: kBaseColor }, + { maskType: 'read', stencilRefValue: 2, _expectedColor: kRedStencilColor }, + ]) + ) + .beforeAllSubcases(t => { + t.selectDeviceForTextureFormatOrSkipTestCase(t.params.format); + }) + .fn(async t => { + const { format, maskType, stencilRefValue, _expectedColor } = t.params; + + const baseStencilState = { + compare: 'always', + failOp: 'keep', + passOp: 'replace', + } as const; + + const stencilState = { + compare: 'equal', + failOp: 'keep', + passOp: 'keep', + } as const; + + const baseState = { + format, + depthWriteEnabled: false, + depthCompare: 'always', + stencilFront: baseStencilState, + stencilBack: baseStencilState, + stencilReadMask: 0xff, + stencilWriteMask: maskType === 'write' ? 0x1 : 0xff, + } as const; + + const testState = { + format, + depthWriteEnabled: false, + depthCompare: 'always', + stencilFront: stencilState, + stencilBack: stencilState, + stencilReadMask: maskType === 'read' ? 0x2 : 0xff, + stencilWriteMask: 0xff, + } as const; + + const testStates = [ + // Draw the base triangle with stencil reference 3. This clears the stencil buffer to 3. + { state: baseState, color: kBaseColor, stencil: 3 }, + { state: testState, color: kRedStencilColor, stencil: stencilRefValue }, + ]; + + t.runStencilStateTest(format, testStates, _expectedColor); + }); + +g.test('stencil_reference_initialized') + .desc('Test that stencil reference is initialized as zero for new render pass.') + .params(u => u.combine('format', kStencilFormats)) + .beforeAllSubcases(t => { + t.selectDeviceForTextureFormatOrSkipTestCase(t.params.format); + }) + .fn(async t => { + const { format } = t.params; + + const baseStencilState = { + compare: 'always', + passOp: 'replace', + } as const; + + const testStencilState = { + compare: 'equal', + passOp: 'keep', + } as const; + + const baseState = { + format, + stencilFront: baseStencilState, + stencilBack: baseStencilState, + } as const; + + const testState = { + format, + stencilFront: testStencilState, + stencilBack: testStencilState, + } as const; + + // First pass sets the stencil to 0x1, the second pass sets the stencil to its default + // value, and the third pass tests if the stencil is zero. + const testStates = [ + { state: baseState, color: kBaseColor, stencil: 0x1 }, + { state: baseState, color: kRedStencilColor, stencil: undefined }, + { state: testState, color: kGreenStencilColor, stencil: 0x0 }, + ]; + + // The third draw should pass the stencil test since the second pass set it to default zero. + t.runStencilStateTest(format, testStates, kGreenStencilColor, true); + }); |