| export const description = ` |
| Testing render pipeline using overridable constants in vertex stage and fragment stage. |
| `; |
| |
| import { makeTestGroup } from '../../../../common/framework/test_group.js'; |
| import { AllFeaturesMaxLimitsGPUTest } from '../../../gpu_test.js'; |
| import { PerTexelComponent } from '../../../util/texture/texel_data.js'; |
| |
| class F extends AllFeaturesMaxLimitsGPUTest { |
| async expectShaderOutputWithConstants( |
| isAsync: boolean, |
| format: GPUTextureFormat, |
| expected: PerTexelComponent<number>, |
| vertex: GPUVertexState, |
| fragment: GPUFragmentState |
| ) { |
| const renderTarget = this.createTextureTracked({ |
| format, |
| size: { width: 1, height: 1, depthOrArrayLayers: 1 }, |
| usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT, |
| }); |
| |
| const descriptor: GPURenderPipelineDescriptor = { |
| layout: 'auto', |
| vertex, |
| fragment, |
| primitive: { |
| topology: 'triangle-list', |
| frontFace: 'ccw', |
| cullMode: 'back', |
| }, |
| }; |
| |
| const promise = isAsync |
| ? this.device.createRenderPipelineAsync(descriptor) |
| : Promise.resolve(this.device.createRenderPipeline(descriptor)); |
| |
| const pipeline = await promise; |
| const encoder = this.device.createCommandEncoder(); |
| const pass = encoder.beginRenderPass({ |
| colorAttachments: [ |
| { |
| view: renderTarget.createView(), |
| clearValue: { |
| r: kClearValueResult.R, |
| g: kClearValueResult.G, |
| b: kClearValueResult.B, |
| a: kClearValueResult.A, |
| }, |
| loadOp: 'clear', |
| storeOp: 'store', |
| }, |
| ], |
| }); |
| pass.setPipeline(pipeline); |
| pass.draw(3); |
| pass.end(); |
| this.device.queue.submit([encoder.finish()]); |
| |
| this.expectSingleColor(renderTarget, format, { |
| size: [1, 1, 1], |
| exp: expected, |
| }); |
| } |
| } |
| |
| export const g = makeTestGroup(F); |
| |
| const kClearValueResult = { R: 0.2, G: 0.4, B: 0.6, A: 0.8 }; |
| const kDefaultValueResult = { R: 1.0, G: 1.0, B: 1.0, A: 1.0 }; |
| |
| const kFullScreenTriangleVertexShader = ` |
| override xright: f32 = 3.0; |
| override ytop: f32 = 3.0; |
| |
| @vertex fn main( |
| @builtin(vertex_index) VertexIndex : u32 |
| ) -> @builtin(position) vec4<f32> { |
| var pos : array<vec2<f32>, 3> = array<vec2<f32>, 3>( |
| vec2<f32>(-1.0, ytop), |
| vec2<f32>(-1.0, -ytop), |
| vec2<f32>(xright, 0.0)); |
| return vec4<f32>(pos[VertexIndex], 0.0, 1.0); |
| } |
| `; |
| |
| const kFullScreenTriangleFragmentShader = ` |
| override R: f32 = 1.0; |
| override G: f32 = 1.0; |
| override B: f32 = 1.0; |
| override A: f32 = 1.0; |
| |
| @fragment fn main() |
| -> @location(0) vec4<f32> { |
| return vec4<f32>(R, G, B, A); |
| } |
| `; |
| |
| g.test('basic') |
| .desc( |
| `Test that either correct constants override values or default values when no constants override value are provided at pipeline creation time are used correctly in vertex and fragment shader.` |
| ) |
| .params(u => |
| u |
| .combine('isAsync', [true, false]) |
| .beginSubcases() |
| .combineWithParams([ |
| { |
| expected: kDefaultValueResult, |
| vertexConstants: {}, |
| fragmentConstants: {}, |
| }, |
| { |
| expected: kClearValueResult, |
| vertexConstants: { |
| xright: -3.0, |
| } as Record<string, GPUPipelineConstantValue>, |
| fragmentConstants: {}, |
| }, |
| { |
| expected: kClearValueResult, |
| vertexConstants: { |
| ytop: -3.0, |
| } as Record<string, GPUPipelineConstantValue>, |
| fragmentConstants: {}, |
| }, |
| { |
| expected: kDefaultValueResult, |
| vertexConstants: { |
| xright: 4.0, |
| ytop: 4.0, |
| } as Record<string, GPUPipelineConstantValue>, |
| fragmentConstants: {}, |
| }, |
| { |
| expected: { R: 0.0, G: 1.0, B: 0.0, A: 1.0 }, |
| vertexConstants: {}, |
| fragmentConstants: { R: 0.0, B: 0.0 } as Record<string, GPUPipelineConstantValue>, |
| }, |
| { |
| expected: { R: 0.0, G: 0.0, B: 0.0, A: 0.0 }, |
| vertexConstants: {}, |
| fragmentConstants: { R: 0.0, G: 0.0, B: 0.0, A: 0.0 } as Record< |
| string, |
| GPUPipelineConstantValue |
| >, |
| }, |
| ]) |
| ) |
| .fn(async t => { |
| const format = 'bgra8unorm'; |
| await t.expectShaderOutputWithConstants( |
| t.params.isAsync, |
| format, |
| t.params.expected, |
| { |
| module: t.device.createShaderModule({ |
| code: kFullScreenTriangleVertexShader, |
| }), |
| entryPoint: 'main', |
| constants: t.params.vertexConstants, |
| }, |
| { |
| module: t.device.createShaderModule({ |
| code: kFullScreenTriangleFragmentShader, |
| }), |
| entryPoint: 'main', |
| constants: t.params.fragmentConstants, |
| targets: [{ format }], |
| } |
| ); |
| }); |
| |
| const kPrecisionTestFormat = 'rgba32float'; |
| g.test('precision') |
| .desc(`Test that the float number precision is preserved for constants`) |
| .params(u => |
| u |
| .combine('isAsync', [true, false]) |
| .beginSubcases() |
| .combineWithParams([ |
| { |
| expected: { R: 3.14159, G: 1.0, B: 1.0, A: 1.0 }, |
| vertexConstants: {}, |
| fragmentConstants: { R: 3.14159 } as Record<string, GPUPipelineConstantValue>, |
| }, |
| { |
| expected: { R: 3.141592653589793, G: 1.0, B: 1.0, A: 1.0 }, |
| vertexConstants: {}, |
| fragmentConstants: { R: 3.141592653589793 } as Record<string, GPUPipelineConstantValue>, |
| }, |
| ]) |
| ) |
| .fn(async t => { |
| const format = kPrecisionTestFormat; |
| await t.expectShaderOutputWithConstants( |
| t.params.isAsync, |
| format, |
| t.params.expected, |
| { |
| module: t.device.createShaderModule({ |
| code: kFullScreenTriangleVertexShader, |
| }), |
| entryPoint: 'main', |
| constants: t.params.vertexConstants, |
| }, |
| { |
| module: t.device.createShaderModule({ |
| code: kFullScreenTriangleFragmentShader, |
| }), |
| entryPoint: 'main', |
| constants: t.params.fragmentConstants, |
| targets: [{ format }], |
| } |
| ); |
| }); |
| |
| g.test('shared_shader_module') |
| .desc( |
| `Test that when the same module is shared by different pipelines, the constant values are still being used correctly.` |
| ) |
| .params(u => |
| u |
| .combine('isAsync', [true, false]) |
| .beginSubcases() |
| .combineWithParams([ |
| { |
| expected0: kClearValueResult, |
| vertexConstants0: { |
| xright: -3.0, |
| } as Record<string, GPUPipelineConstantValue>, |
| fragmentConstants0: {}, |
| |
| expected1: kDefaultValueResult, |
| vertexConstants1: {}, |
| fragmentConstants1: {}, |
| }, |
| { |
| expected0: { R: 0.0, G: 0.0, B: 0.0, A: 0.0 }, |
| vertexConstants0: {}, |
| fragmentConstants0: { R: 0.0, G: 0.0, B: 0.0, A: 0.0 } as Record< |
| string, |
| GPUPipelineConstantValue |
| >, |
| |
| expected1: kDefaultValueResult, |
| vertexConstants1: {}, |
| fragmentConstants1: {}, |
| }, |
| { |
| expected0: { R: 1.0, G: 0.0, B: 1.0, A: 0.0 }, |
| vertexConstants0: {}, |
| fragmentConstants0: { R: 1.0, G: 0.0, B: 1.0, A: 0.0 } as Record< |
| string, |
| GPUPipelineConstantValue |
| >, |
| |
| expected1: { R: 0.0, G: 1.0, B: 0.0, A: 1.0 }, |
| vertexConstants1: {}, |
| fragmentConstants1: { R: 0.0, G: 1.0, B: 0.0, A: 1.0 } as Record< |
| string, |
| GPUPipelineConstantValue |
| >, |
| }, |
| ]) |
| ) |
| .fn(async t => { |
| const format = 'bgra8unorm'; |
| const vertexModule = t.device.createShaderModule({ |
| code: kFullScreenTriangleVertexShader, |
| }); |
| |
| const fragmentModule = t.device.createShaderModule({ |
| code: kFullScreenTriangleFragmentShader, |
| }); |
| |
| const createPipelineFn = async ( |
| vertexConstants: Record<string, GPUPipelineConstantValue>, |
| fragmentConstants: Record<string, GPUPipelineConstantValue> |
| ) => { |
| const descriptor: GPURenderPipelineDescriptor = { |
| layout: 'auto', |
| vertex: { |
| module: vertexModule, |
| entryPoint: 'main', |
| constants: vertexConstants, |
| }, |
| fragment: { |
| module: fragmentModule, |
| entryPoint: 'main', |
| targets: [{ format }], |
| constants: fragmentConstants, |
| }, |
| primitive: { |
| topology: 'triangle-list', |
| frontFace: 'ccw', |
| cullMode: 'back', |
| }, |
| }; |
| |
| return t.params.isAsync |
| ? t.device.createRenderPipelineAsync(descriptor) |
| : t.device.createRenderPipeline(descriptor); |
| }; |
| |
| const pipeline0 = await createPipelineFn( |
| t.params.vertexConstants0, |
| t.params.fragmentConstants0 |
| ); |
| const pipeline1 = await createPipelineFn( |
| t.params.vertexConstants1, |
| t.params.fragmentConstants1 |
| ); |
| |
| const renderTarget0 = t.createTextureTracked({ |
| format, |
| size: { width: 1, height: 1, depthOrArrayLayers: 1 }, |
| usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT, |
| }); |
| const renderTarget1 = t.createTextureTracked({ |
| format, |
| size: { width: 1, height: 1, depthOrArrayLayers: 1 }, |
| usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT, |
| }); |
| |
| const encoder = t.device.createCommandEncoder(); |
| |
| const pass0 = encoder.beginRenderPass({ |
| colorAttachments: [ |
| { |
| view: renderTarget0.createView(), |
| clearValue: { |
| r: kClearValueResult.R, |
| g: kClearValueResult.G, |
| b: kClearValueResult.B, |
| a: kClearValueResult.A, |
| }, |
| loadOp: 'clear', |
| storeOp: 'store', |
| }, |
| ], |
| }); |
| pass0.setPipeline(pipeline0); |
| pass0.draw(3); |
| pass0.end(); |
| |
| const pass1 = encoder.beginRenderPass({ |
| colorAttachments: [ |
| { |
| view: renderTarget1.createView(), |
| clearValue: { |
| r: kClearValueResult.R, |
| g: kClearValueResult.G, |
| b: kClearValueResult.B, |
| a: kClearValueResult.A, |
| }, |
| loadOp: 'clear', |
| storeOp: 'store', |
| }, |
| ], |
| }); |
| pass1.setPipeline(pipeline1); |
| pass1.draw(3); |
| pass1.end(); |
| |
| t.device.queue.submit([encoder.finish()]); |
| |
| t.expectSingleColor(renderTarget0, format, { |
| size: [1, 1, 1], |
| exp: t.params.expected0, |
| }); |
| t.expectSingleColor(renderTarget1, format, { |
| size: [1, 1, 1], |
| exp: t.params.expected1, |
| }); |
| }); |
| |
| g.test('multi_entry_points') |
| .desc( |
| `Test that when the same module is shared by vertex and fragment shader, the constant values are still being used correctly.` |
| ) |
| .params(u => |
| u |
| .combine('isAsync', [true, false]) |
| .beginSubcases() |
| .combineWithParams([ |
| { |
| expected: { R: 0.8, G: 0.4, B: 0.2, A: 1.0 }, |
| vertexConstants: { A: 4.0, B: 4.0 } as Record<string, GPUPipelineConstantValue>, |
| fragmentConstants: { A: 0.8, B: 0.4, C: 0.2, D: 1.0 } as Record< |
| string, |
| GPUPipelineConstantValue |
| >, |
| }, |
| { |
| expected: { R: 0.8, G: 0.4, B: 0.2, A: 1.0 }, |
| vertexConstants: {}, |
| fragmentConstants: { A: 0.8, B: 0.4, C: 0.2, D: 1.0 } as Record< |
| string, |
| GPUPipelineConstantValue |
| >, |
| }, |
| { |
| expected: kClearValueResult, |
| vertexConstants: { A: -3.0 }, |
| fragmentConstants: { A: 0.8, B: 0.4, C: 0.2, D: 1.0 } as Record< |
| string, |
| GPUPipelineConstantValue |
| >, |
| }, |
| ]) |
| ) |
| .fn(async t => { |
| const format = 'bgra8unorm'; |
| const module = t.device.createShaderModule({ |
| code: ` |
| override A: f32 = 3.0; |
| override B: f32 = 3.0; |
| override C: f32; |
| override D: f32; |
| |
| @vertex fn vertexMain( |
| @builtin(vertex_index) VertexIndex : u32 |
| ) -> @builtin(position) vec4<f32> { |
| var pos : array<vec2<f32>, 3> = array<vec2<f32>, 3>( |
| vec2<f32>(-1.0, A), |
| vec2<f32>(-1.0, -A), |
| vec2<f32>(B, 0.0)); |
| return vec4<f32>(pos[VertexIndex], 0.0, 1.0); |
| } |
| |
| @fragment fn fragmentMain() |
| -> @location(0) vec4<f32> { |
| return vec4<f32>(A, B, C, D); |
| } |
| `, |
| }); |
| await t.expectShaderOutputWithConstants( |
| t.params.isAsync, |
| format, |
| t.params.expected, |
| { |
| module, |
| entryPoint: 'vertexMain', |
| constants: t.params.vertexConstants, |
| }, |
| { |
| module, |
| entryPoint: 'fragmentMain', |
| constants: t.params.fragmentConstants, |
| targets: [{ format }], |
| } |
| ); |
| }); |