| export const description = ` |
| Test related to stencil states, stencil op, compare func, etc. |
| `; |
| |
| import { makeTestGroup } from '../../../../common/framework/test_group.js'; |
| import { assert, TypedArrayBufferView } from '../../../../common/util/util.js'; |
| import { |
| kBufferSizeAlignment, |
| kMinDynamicBufferOffsetAlignment, |
| } from '../../../capability_info.js'; |
| import { |
| kStencilTextureFormats, |
| DepthStencilFormat, |
| isDepthTextureFormat, |
| isStencilTextureFormat, |
| kDepthStencilFormats, |
| depthStencilFormatAspectSize, |
| } from '../../../format_info.js'; |
| import { AllFeaturesMaxLimitsGPUTest } from '../../../gpu_test.js'; |
| import * as ttu from '../../../texture_test_utils.js'; |
| import { align } from '../../../util/math.js'; |
| import { DataArrayGenerator } from '../../../util/texture/data_generation.js'; |
| import { dataBytesForCopyOrFail, getTextureCopyLayout } from '../../../util/texture/layout.js'; |
| import { TexelView } from '../../../util/texture/texel_view.js'; |
| |
| 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 AllFeaturesMaxLimitsGPUTest { |
| 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.createTextureTracked({ |
| format: renderTargetFormat, |
| size: { width: 1, height: 1, depthOrArrayLayers: 1 }, |
| usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT, |
| }); |
| |
| const depthTexture = this.createTextureTracked({ |
| size: { width: 1, height: 1, depthOrArrayLayers: 1 }, |
| format: depthStencilFormat, |
| sampleCount: 1, |
| mipLevelCount: 1, |
| usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_DST, |
| }); |
| |
| const hasDepth = isDepthTextureFormat(depthStencilFormat); |
| const depthStencilAttachment: GPURenderPassDepthStencilAttachment = { |
| view: depthTexture.createView(), |
| depthLoadOp: hasDepth ? 'load' : undefined, |
| depthStoreOp: hasDepth ? 'store' : undefined, |
| stencilLoadOp: 'load', |
| stencilStoreOp: 'store', |
| }; |
| |
| const encoder = this.device.createCommandEncoder(); |
| let pass = encoder.beginRenderPass({ |
| colorAttachments: [ |
| { |
| view: renderTarget.createView(), |
| loadOp: 'load', |
| storeOp: 'store', |
| }, |
| ], |
| 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(), |
| loadOp: 'load', |
| storeOp: 'store', |
| }, |
| ], |
| 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); |
| ttu.expectTexelViewComparisonIsOkInTexture( |
| this, |
| { texture: renderTarget }, |
| expTexelView, |
| [1, 1] |
| ); |
| } |
| |
| 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', kStencilTextureFormats) |
| .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) |
| ) |
| .fn(t => { |
| const { format, stencilCompare, stencilRefValue, _expectedColor } = t.params; |
| t.skipIfTextureFormatNotSupported(format); |
| |
| 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', kStencilTextureFormats) |
| .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) |
| ) |
| .fn(t => { |
| const { format, passOp, initialStencil, _expectedStencil } = t.params; |
| t.skipIfTextureFormatNotSupported(format); |
| |
| 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', kStencilTextureFormats) |
| .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) |
| ) |
| .fn(t => { |
| const { format, failOp, initialStencil, _expectedStencil } = t.params; |
| t.skipIfTextureFormatNotSupported(format); |
| |
| 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 => isDepthTextureFormat(format) && isStencilTextureFormat(format) |
| ) |
| ) |
| .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) |
| ) |
| .fn(t => { |
| const { format, depthFailOp, initialStencil, _expectedStencil } = t.params; |
| t.skipIfTextureFormatNotSupported(format); |
| |
| 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', kStencilTextureFormats) |
| .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 }, |
| ]) |
| ) |
| .fn(t => { |
| const { format, maskType, stencilRefValue, _expectedColor } = t.params; |
| t.skipIfTextureFormatNotSupported(format); |
| |
| 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', kStencilTextureFormats)) |
| .fn(t => { |
| const { format } = t.params; |
| t.skipIfTextureFormatNotSupported(format); |
| |
| const baseStencilState = { |
| compare: 'always', |
| passOp: 'replace', |
| } as const; |
| |
| const testStencilState = { |
| compare: 'equal', |
| passOp: 'keep', |
| } as const; |
| |
| const hasDepth = isDepthTextureFormat(format); |
| |
| const baseState = { |
| format, |
| depthWriteEnabled: hasDepth, |
| depthCompare: 'always', |
| stencilFront: baseStencilState, |
| stencilBack: baseStencilState, |
| } as const; |
| |
| const testState = { |
| format, |
| depthWriteEnabled: hasDepth, |
| depthCompare: 'always', |
| 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); |
| }); |
| |
| const dataGenerator = new DataArrayGenerator(); |
| g.test('stencil_accumulation') |
| .desc( |
| `A variation of a technique previously used to verify stencil texture copy that was observed to |
| fail on some Qualcomm chipsets. Attempts to read back from a stencil texture by doing multiple |
| fullscreen renders, one per stencil bit. Each pass sets the stencil mask and stencil reference |
| to 1 << passIndex, and renders 1 << passIndex into the red channel with additive blending. This |
| should duplicate the stencil values into the red channel of the texture, and on most GPUs it |
| works. Technique is replicated here to catch driver bugs, while the original stencil copy test |
| has been updated to verify the stencil values using copyTextureToBuffer, which is more |
| straightforward and allows those tests to focus on the behavior of the APIs in question. |
| ` |
| ) |
| .params(u => |
| u |
| .combine('format', kDepthStencilFormats) |
| .filter(t => isStencilTextureFormat(t.format)) |
| .beginSubcases() |
| .combineWithParams([ |
| { offsetInBlocks: 0, dataPaddingInBytes: 0 }, |
| { offsetInBlocks: 16, dataPaddingInBytes: 0 }, |
| { offsetInBlocks: 768, dataPaddingInBytes: 0 }, |
| { offsetInBlocks: 0, dataPaddingInBytes: 1 }, |
| ]) |
| .combine('copyDepth', [1, 2]) |
| .combine('mipLevel', [0, 2]) |
| ) |
| .fn(t => { |
| const { format, offsetInBlocks, dataPaddingInBytes, copyDepth, mipLevel } = t.params; |
| t.skipIfTextureFormatNotSupported(format); |
| const aspect = 'stencil-only'; |
| const bytesPerBlock = depthStencilFormatAspectSize(format, aspect); |
| const initialDataOffset = offsetInBlocks * bytesPerBlock; |
| const copySize = [3, 3, copyDepth] as const; |
| const rowsPerImage = 3; |
| const bytesPerRow = 256; |
| |
| const textureSize = [copySize[0] << mipLevel, copySize[1] << mipLevel, copyDepth] as const; |
| |
| const minDataSize = dataBytesForCopyOrFail({ |
| layout: { offset: initialDataOffset, bytesPerRow, rowsPerImage }, |
| format: 'stencil8', |
| copySize, |
| method: 'WriteTexture', |
| }); |
| const initialDataSize = minDataSize + dataPaddingInBytes; |
| |
| const srcTexture = t.createTextureTracked({ |
| size: textureSize, |
| usage: |
| GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT, |
| format, |
| mipLevelCount: mipLevel + 1, |
| }); |
| |
| const writeSize = [textureSize[0] >> mipLevel, textureSize[1] >> mipLevel, textureSize[2]]; |
| const initialData = dataGenerator.generateView( |
| align(initialDataSize, kBufferSizeAlignment), |
| 0, |
| initialDataOffset |
| ); |
| |
| t.queue.writeTexture( |
| { texture: srcTexture, aspect: 'stencil-only', mipLevel }, |
| initialData, |
| { |
| offset: initialDataOffset, |
| bytesPerRow, |
| rowsPerImage, |
| }, |
| writeSize |
| ); |
| |
| // Everything below here was previously checkStencilTextureContent in image_copy.spec.ts |
| const stencilBitCount = 8; |
| |
| // Prepare the uniform buffer that stores the bit indices (from 0 to 7) at stride 256 (required |
| // by Dynamic Buffer Offset). |
| const uniformBufferSize = kMinDynamicBufferOffsetAlignment * (stencilBitCount - 1) + 4; |
| const uniformBufferData = new Uint32Array(uniformBufferSize / 4); |
| for (let i = 1; i < stencilBitCount; ++i) { |
| uniformBufferData[(kMinDynamicBufferOffsetAlignment / 4) * i] = i; |
| } |
| const uniformBuffer = t.makeBufferWithContents( |
| uniformBufferData, |
| GPUBufferUsage.COPY_DST | GPUBufferUsage.UNIFORM |
| ); |
| |
| // Prepare the base render pipeline descriptor (all the settings expect stencilReadMask). |
| const bindGroupLayout = t.device.createBindGroupLayout({ |
| entries: [ |
| { |
| binding: 0, |
| visibility: GPUShaderStage.FRAGMENT, |
| buffer: { |
| type: 'uniform', |
| minBindingSize: 4, |
| hasDynamicOffset: true, |
| }, |
| }, |
| ], |
| }); |
| const renderPipelineDescriptorBase: GPURenderPipelineDescriptor = { |
| layout: t.device.createPipelineLayout({ bindGroupLayouts: [bindGroupLayout] }), |
| vertex: { |
| module: t.device.createShaderModule({ |
| code: ` |
| @vertex |
| fn main(@builtin(vertex_index) VertexIndex : u32)-> @builtin(position) vec4<f32> { |
| var pos : array<vec2<f32>, 6> = array<vec2<f32>, 6>( |
| vec2<f32>(-1.0, 1.0), |
| vec2<f32>(-1.0, -1.0), |
| vec2<f32>( 1.0, 1.0), |
| vec2<f32>(-1.0, -1.0), |
| vec2<f32>( 1.0, 1.0), |
| vec2<f32>( 1.0, -1.0)); |
| return vec4<f32>(pos[VertexIndex], 0.0, 1.0); |
| }`, |
| }), |
| entryPoint: 'main', |
| }, |
| |
| fragment: { |
| module: t.device.createShaderModule({ |
| code: ` |
| struct Params { |
| stencilBitIndex: u32 |
| }; |
| @group(0) @binding(0) var<uniform> param: Params; |
| @fragment |
| fn main() -> @location(0) vec4<f32> { |
| return vec4<f32>(f32(1u << param.stencilBitIndex) / 255.0, 0.0, 0.0, 0.0); |
| }`, |
| }), |
| entryPoint: 'main', |
| targets: [ |
| { |
| // As we implement "rendering one bit in each draw() call" with blending operation |
| // 'add', the format of outputTexture must support blending. |
| format: 'r8unorm', |
| blend: { |
| color: { srcFactor: 'one', dstFactor: 'one', operation: 'add' }, |
| alpha: {}, |
| }, |
| }, |
| ], |
| }, |
| |
| primitive: { |
| topology: 'triangle-list', |
| }, |
| |
| depthStencil: { |
| depthWriteEnabled: false, |
| depthCompare: 'always', |
| format, |
| stencilFront: { |
| compare: 'equal', |
| }, |
| stencilBack: { |
| compare: 'equal', |
| }, |
| }, |
| }; |
| |
| // Prepare the bindGroup that contains uniformBuffer and referenceTexture. |
| const bindGroup = t.device.createBindGroup({ |
| layout: bindGroupLayout, |
| entries: [ |
| { |
| binding: 0, |
| resource: { |
| buffer: uniformBuffer, |
| size: 4, |
| }, |
| }, |
| ], |
| }); |
| |
| // "Copy" the stencil value into the color attachment with 8 draws in one render pass. Each draw |
| // will "Copy" one bit of the stencil value into the color attachment. The bit of the stencil |
| // value is specified by setStencilReference(). |
| const copyFromOutputTextureLayout = getTextureCopyLayout( |
| format, |
| '2d', |
| [textureSize[0], textureSize[1], 1], |
| { |
| mipLevel, |
| aspect: 'stencil-only', |
| } |
| ); |
| const outputTextureSize = [ |
| copyFromOutputTextureLayout.mipSize[0], |
| copyFromOutputTextureLayout.mipSize[1], |
| 1, |
| ]; |
| const outputTexture = t.createTextureTracked({ |
| format: 'r8unorm', |
| size: outputTextureSize, |
| usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT, |
| }); |
| |
| for (let stencilTextureLayer = 0; stencilTextureLayer < textureSize[2]; ++stencilTextureLayer) { |
| const encoder = t.device.createCommandEncoder(); |
| const depthStencilAttachment: GPURenderPassDepthStencilAttachment = { |
| view: srcTexture.createView({ |
| baseMipLevel: mipLevel, |
| mipLevelCount: 1, |
| baseArrayLayer: stencilTextureLayer, |
| arrayLayerCount: 1, |
| }), |
| }; |
| if (isDepthTextureFormat(format)) { |
| depthStencilAttachment.depthClearValue = 0; |
| depthStencilAttachment.depthLoadOp = 'clear'; |
| depthStencilAttachment.depthStoreOp = 'store'; |
| } |
| if (isStencilTextureFormat(format)) { |
| depthStencilAttachment.stencilLoadOp = 'load'; |
| depthStencilAttachment.stencilStoreOp = 'store'; |
| } |
| const renderPass = encoder.beginRenderPass({ |
| colorAttachments: [ |
| { |
| view: outputTexture.createView(), |
| clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 0.0 }, |
| loadOp: 'clear', |
| storeOp: 'store', |
| }, |
| ], |
| depthStencilAttachment, |
| }); |
| |
| for (let stencilBitIndex = 0; stencilBitIndex < stencilBitCount; ++stencilBitIndex) { |
| const renderPipelineDescriptor = renderPipelineDescriptorBase; |
| assert(renderPipelineDescriptor.depthStencil !== undefined); |
| renderPipelineDescriptor.depthStencil.stencilReadMask = 1 << stencilBitIndex; |
| const renderPipeline = t.device.createRenderPipeline(renderPipelineDescriptor); |
| |
| renderPass.setPipeline(renderPipeline); |
| renderPass.setStencilReference(1 << stencilBitIndex); |
| renderPass.setBindGroup(0, bindGroup, [stencilBitIndex * kMinDynamicBufferOffsetAlignment]); |
| renderPass.draw(6); |
| } |
| renderPass.end(); |
| |
| // Check outputTexture by copying the content of outputTexture into outputStagingBuffer and |
| // checking all the data in outputStagingBuffer. |
| const outputStagingBuffer = t.createBufferTracked({ |
| size: copyFromOutputTextureLayout.byteLength, |
| usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST, |
| }); |
| encoder.copyTextureToBuffer( |
| { |
| texture: outputTexture, |
| }, |
| { |
| buffer: outputStagingBuffer, |
| bytesPerRow: copyFromOutputTextureLayout.bytesPerRow, |
| rowsPerImage: copyFromOutputTextureLayout.rowsPerImage, |
| }, |
| outputTextureSize |
| ); |
| |
| t.queue.submit([encoder.finish()]); |
| |
| // Check the valid data in outputStagingBuffer once per row. |
| for (let y = 0; y < copyFromOutputTextureLayout.mipSize[1]; ++y) { |
| const dataStart = |
| initialDataOffset + bytesPerRow * rowsPerImage * stencilTextureLayer + bytesPerRow * y; |
| t.expectGPUBufferValuesEqual( |
| outputStagingBuffer, |
| initialData.slice(dataStart, dataStart + copyFromOutputTextureLayout.mipSize[0]), |
| copyFromOutputTextureLayout.bytesPerRow * y |
| ); |
| } |
| } |
| }); |