| export const description = ` |
| Tests for the general aspects of draw/drawIndexed/drawIndirect/drawIndexedIndirect. |
| |
| Primitive topology tested in api/operation/render_pipeline/primitive_topology.spec.ts. |
| Index format tested in api/operation/command_buffer/render/state_tracking.spec.ts. |
| `; |
| |
| import { makeTestGroup } from '../../../../common/framework/test_group.js'; |
| import { |
| assert, |
| TypedArrayBufferView, |
| TypedArrayBufferViewConstructor, |
| } from '../../../../common/util/util.js'; |
| import { AllFeaturesMaxLimitsGPUTest } from '../../../gpu_test.js'; |
| import * as ttu from '../../../texture_test_utils.js'; |
| import { PerPixelComparison } from '../../../util/texture/texture_ok.js'; |
| |
| class DrawTest extends AllFeaturesMaxLimitsGPUTest { |
| checkTriangleDraw(opts: { |
| firstIndex: number | undefined; |
| count: number; |
| firstInstance: number | undefined; |
| instanceCount: number | undefined; |
| indexed: boolean; |
| indirect: boolean; |
| vertexBufferOffset: number; |
| indexBufferOffset: number | undefined; |
| baseVertex: number | undefined; |
| }): void { |
| // Set fallbacks when parameters are undefined in order to calculate the expected values. |
| const defaulted = { |
| firstIndex: opts.firstIndex ?? 0, |
| count: opts.count, |
| firstInstance: opts.firstInstance ?? 0, |
| instanceCount: opts.instanceCount ?? 1, |
| indexed: opts.indexed, |
| indirect: opts.indirect, |
| vertexBufferOffset: opts.vertexBufferOffset, |
| indexBufferOffset: opts.indexBufferOffset ?? 0, |
| baseVertex: opts.baseVertex ?? 0, |
| }; |
| |
| const haveStorageBuffersInFragmentStage = |
| !this.isCompatibility || this.device.limits.maxStorageBuffersInFragmentStage! > 0; |
| const renderTargetSize = [72, 36]; |
| |
| // The test will split up the render target into a grid where triangles of |
| // increasing primitive id will be placed along the X axis, and triangles |
| // of increasing instance id will be placed along the Y axis. The size of the |
| // grid is based on the max primitive id and instance id used. |
| const numX = 6; |
| const numY = 6; |
| const tileSizeX = renderTargetSize[0] / numX; |
| const tileSizeY = renderTargetSize[1] / numY; |
| |
| // |\ |
| // | \ |
| // |______\ |
| // Unit triangle shaped like this. 0-1 Y-down. |
| /* prettier-ignore */ |
| const triangleVertices = [ |
| 0.0, 0.0, |
| 0.0, 1.0, |
| 1.0, 1.0, |
| ]; |
| |
| const renderTarget = this.createTextureTracked({ |
| size: renderTargetSize, |
| usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC, |
| format: 'rgba8unorm', |
| }); |
| |
| const vertexModule = this.device.createShaderModule({ |
| code: ` |
| struct Inputs { |
| @builtin(vertex_index) vertex_index : u32, |
| @builtin(instance_index) instance_id : u32, |
| @location(0) vertexPosition : vec2<f32>, |
| }; |
| |
| @vertex fn vert_main(input : Inputs |
| ) -> @builtin(position) vec4<f32> { |
| // 3u is the number of points in a triangle to convert from index |
| // to id. |
| var vertex_id : u32 = input.vertex_index / 3u; |
| |
| var x : f32 = (input.vertexPosition.x + f32(vertex_id)) / ${numX}.0; |
| var y : f32 = (input.vertexPosition.y + f32(input.instance_id)) / ${numY}.0; |
| |
| // (0,1) y-down space to (-1,1) y-up NDC |
| x = 2.0 * x - 1.0; |
| y = -2.0 * y + 1.0; |
| return vec4<f32>(x, y, 0.0, 1.0); |
| } |
| `, |
| }); |
| |
| const fragmentModule = this.device.createShaderModule({ |
| code: ` |
| struct Output { |
| value : u32 |
| }; |
| |
| @group(0) @binding(0) var<storage, read_write> output : Output; |
| |
| @fragment fn frag_main() -> @location(0) vec4<f32> { |
| ${haveStorageBuffersInFragmentStage ? 'output.value = 1u;' : ''} |
| return vec4<f32>(0.0, 1.0, 0.0, 1.0); |
| } |
| `, |
| }); |
| |
| const pipeline = this.device.createRenderPipeline({ |
| layout: 'auto', |
| vertex: { |
| module: vertexModule, |
| entryPoint: 'vert_main', |
| buffers: [ |
| { |
| attributes: [ |
| { |
| shaderLocation: 0, |
| format: 'float32x2', |
| offset: 0, |
| }, |
| ], |
| arrayStride: 2 * Float32Array.BYTES_PER_ELEMENT, |
| }, |
| ], |
| }, |
| fragment: { |
| module: fragmentModule, |
| entryPoint: 'frag_main', |
| targets: [ |
| { |
| format: 'rgba8unorm', |
| }, |
| ], |
| }, |
| }); |
| |
| const resultBuffer = haveStorageBuffersInFragmentStage |
| ? this.createBufferTracked({ |
| size: Uint32Array.BYTES_PER_ELEMENT, |
| usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC, |
| }) |
| : null; |
| |
| const resultBindGroup = haveStorageBuffersInFragmentStage |
| ? this.device.createBindGroup({ |
| layout: pipeline.getBindGroupLayout(0), |
| entries: [ |
| { |
| binding: 0, |
| resource: { |
| buffer: resultBuffer!, |
| }, |
| }, |
| ], |
| }) |
| : null; |
| |
| const commandEncoder = this.device.createCommandEncoder(); |
| const renderPass = commandEncoder.beginRenderPass({ |
| colorAttachments: [ |
| { |
| view: renderTarget.createView(), |
| clearValue: [0, 0, 0, 0], |
| loadOp: 'clear', |
| storeOp: 'store', |
| }, |
| ], |
| }); |
| |
| renderPass.setPipeline(pipeline); |
| renderPass.setBindGroup(0, resultBindGroup); |
| |
| if (defaulted.indexed) { |
| // INDEXED DRAW |
| assert(defaulted.baseVertex !== undefined); |
| assert(defaulted.indexBufferOffset !== undefined); |
| |
| renderPass.setIndexBuffer( |
| this.makeBufferWithContents( |
| /* prettier-ignore */ new Uint32Array([ |
| // Offset the index buffer contents by empty data. |
| ...new Array(defaulted.indexBufferOffset / Uint32Array.BYTES_PER_ELEMENT), |
| |
| 0, 1, 2, // |
| 3, 4, 5, // |
| 6, 7, 8, // |
| ]), |
| GPUBufferUsage.INDEX |
| ), |
| 'uint32', |
| defaulted.indexBufferOffset |
| ); |
| |
| renderPass.setVertexBuffer( |
| 0, |
| this.makeBufferWithContents( |
| /* prettier-ignore */ new Float32Array([ |
| // Offset the vertex buffer contents by empty data. |
| ...new Array(defaulted.vertexBufferOffset / Float32Array.BYTES_PER_ELEMENT), |
| |
| // selected with base_vertex=0 |
| // count=6 |
| ...triangleVertices, // | count=6;first=3 |
| ...triangleVertices, // | | |
| ...triangleVertices, // | |
| |
| // selected with base_vertex=9 |
| // count=6 |
| ...triangleVertices, // | count=6;first=3 |
| ...triangleVertices, // | | |
| ...triangleVertices, // | |
| ]), |
| GPUBufferUsage.VERTEX |
| ), |
| defaulted.vertexBufferOffset |
| ); |
| |
| if (defaulted.indirect) { |
| const args = [ |
| defaulted.count, |
| defaulted.instanceCount, |
| defaulted.firstIndex, |
| defaulted.baseVertex, |
| defaulted.firstInstance, |
| ] as const; |
| renderPass.drawIndexedIndirect( |
| this.makeBufferWithContents(new Uint32Array(args), GPUBufferUsage.INDIRECT), |
| 0 |
| ); |
| } else { |
| const args = [ |
| opts.count, |
| opts.instanceCount, |
| opts.firstIndex, |
| opts.baseVertex, |
| opts.firstInstance, |
| ] as const; |
| renderPass.drawIndexed.apply(renderPass, [...args]); |
| } |
| } else { |
| // NON-INDEXED DRAW |
| renderPass.setVertexBuffer( |
| 0, |
| this.makeBufferWithContents( |
| /* prettier-ignore */ new Float32Array([ |
| // Offset the vertex buffer contents by empty data. |
| ...new Array(defaulted.vertexBufferOffset / Float32Array.BYTES_PER_ELEMENT), |
| |
| // count=6 |
| ...triangleVertices, // | count=6;first=3 |
| ...triangleVertices, // | | |
| ...triangleVertices, // | |
| ]), |
| GPUBufferUsage.VERTEX |
| ), |
| defaulted.vertexBufferOffset |
| ); |
| |
| if (defaulted.indirect) { |
| const args = [ |
| defaulted.count, |
| defaulted.instanceCount, |
| defaulted.firstIndex, |
| defaulted.firstInstance, |
| ] as const; |
| renderPass.drawIndirect( |
| this.makeBufferWithContents(new Uint32Array(args), GPUBufferUsage.INDIRECT), |
| 0 |
| ); |
| } else { |
| const args = [opts.count, opts.instanceCount, opts.firstIndex, opts.firstInstance] as const; |
| renderPass.draw.apply(renderPass, [...args]); |
| } |
| } |
| |
| renderPass.end(); |
| this.queue.submit([commandEncoder.finish()]); |
| |
| const green = new Uint8Array([0, 255, 0, 255]); |
| const transparentBlack = new Uint8Array([0, 0, 0, 0]); |
| |
| const didDraw = defaulted.count && defaulted.instanceCount; |
| |
| if (resultBuffer) { |
| this.expectGPUBufferValuesEqual(resultBuffer, new Uint32Array([didDraw ? 1 : 0])); |
| } |
| |
| const baseVertexCount = defaulted.baseVertex ?? 0; |
| const pixelComparisons: PerPixelComparison<Uint8Array>[] = []; |
| for (let primitiveId = 0; primitiveId < numX; ++primitiveId) { |
| for (let instanceId = 0; instanceId < numY; ++instanceId) { |
| let expectedColor = didDraw ? green : transparentBlack; |
| if ( |
| primitiveId * 3 < defaulted.firstIndex + baseVertexCount || |
| primitiveId * 3 >= defaulted.firstIndex + baseVertexCount + defaulted.count |
| ) { |
| expectedColor = transparentBlack; |
| } |
| |
| if ( |
| instanceId < defaulted.firstInstance || |
| instanceId >= defaulted.firstInstance + defaulted.instanceCount |
| ) { |
| expectedColor = transparentBlack; |
| } |
| |
| pixelComparisons.push({ |
| coord: { x: (1 / 3 + primitiveId) * tileSizeX, y: (2 / 3 + instanceId) * tileSizeY }, |
| exp: expectedColor, |
| }); |
| } |
| } |
| ttu.expectSinglePixelComparisonsAreOkInTexture( |
| this, |
| { texture: renderTarget }, |
| pixelComparisons |
| ); |
| } |
| } |
| |
| export const g = makeTestGroup(DrawTest); |
| |
| g.test('arguments') |
| .desc( |
| `Test that draw arguments are passed correctly by drawing triangles in a grid. |
| Horizontally across the texture are triangles with increasing "primitive id". |
| Vertically down the screen are triangles with increasing instance id. |
| Increasing the |first| param should skip some of the beginning triangles on the horizontal axis. |
| Increasing the |first_instance| param should skip of the beginning triangles on the vertical axis. |
| The vertex buffer contains two sets of disjoint triangles, and base_vertex is used to select the second set. |
| The test checks that the center of all of the expected triangles is drawn, and the others are empty. |
| The fragment shader also writes out to a storage buffer if it can. If the draw is zero-sized, check that no value is written. |
| |
| Params: |
| - first= {0, 3} - either the firstVertex or firstIndex |
| - count= {0, 3, 6} - either the vertexCount or indexCount |
| - first_instance= {0, 2} |
| - instance_count= {0, 1, 4} |
| - indexed= {true, false} |
| - indirect= {true, false} |
| - vertex_buffer_offset= {0, 32} |
| - index_buffer_offset= {0, 16} - only for indexed draws |
| - base_vertex= {0, 9} - only for indexed draws |
| ` |
| ) |
| .params(u => |
| u |
| .combine('first', [0, 3] as const) |
| .combine('count', [0, 3, 6] as const) |
| .combine('first_instance', [0, 2] as const) |
| .combine('instance_count', [0, 1, 4] as const) |
| .combine('indexed', [false, true]) |
| .combine('indirect', [false, true]) |
| .combine('vertex_buffer_offset', [0, 32] as const) |
| .expand('index_buffer_offset', p => (p.indexed ? ([0, 16] as const) : [undefined])) |
| .expand('base_vertex', p => (p.indexed ? ([0, 9] as const) : [undefined])) |
| ) |
| .fn(t => { |
| if (t.params.first_instance > 0 && t.params.indirect) { |
| t.skipIfDeviceDoesNotHaveFeature('indirect-first-instance'); |
| } |
| t.checkTriangleDraw({ |
| firstIndex: t.params.first, |
| count: t.params.count, |
| firstInstance: t.params.first_instance, |
| instanceCount: t.params.instance_count, |
| indexed: t.params.indexed, |
| indirect: t.params.indirect, |
| vertexBufferOffset: t.params.vertex_buffer_offset, |
| indexBufferOffset: t.params.index_buffer_offset, |
| baseVertex: t.params.base_vertex, |
| }); |
| }); |
| |
| g.test('default_arguments') |
| .desc( |
| ` |
| Test that defaults arguments are passed correctly by drawing triangles in a grid when they are not |
| defined. This test is written based on the 'arguments' with 'undefined' value in the parameters. |
| - mode= {draw, drawIndexed} |
| - arg= {instance_count, first_index, first_instance, base_vertex} |
| ` |
| ) |
| .params(u => |
| u |
| .combine('mode', ['draw', 'drawIndexed']) |
| .beginSubcases() |
| .combine('instance_count', [undefined, 4] as const) |
| .combine('first_index', [undefined, 3] as const) |
| .combine('first_instance', [undefined, 2] as const) |
| .expand('base_vertex', p => |
| p.mode === 'drawIndexed' ? ([undefined, 9] as const) : [undefined] |
| ) |
| ) |
| .fn(t => { |
| const kVertexCount = 3; |
| const kVertexBufferOffset = 32; |
| const kIndexBufferOffset = 16; |
| |
| t.checkTriangleDraw({ |
| firstIndex: t.params.first_index, |
| count: kVertexCount, |
| firstInstance: t.params.first_instance, |
| instanceCount: t.params.instance_count, |
| indexed: t.params.mode === 'drawIndexed', |
| indirect: false, // indirect |
| vertexBufferOffset: kVertexBufferOffset, |
| indexBufferOffset: kIndexBufferOffset, |
| baseVertex: t.params.base_vertex, |
| }); |
| }); |
| |
| g.test('vertex_attributes,basic') |
| .desc( |
| `Test basic fetching of vertex attributes. |
| Each vertex attribute is a single value and written out into a storage buffer. |
| Tests that vertices with offsets/strides for instanced/non-instanced attributes are |
| fetched correctly. Not all vertex formats are tested. |
| |
| Params: |
| - vertex_attribute_count= {1, 4, 8, 16} |
| - vertex_buffer_count={1, 4, 8} - where # attributes is > 0 |
| - vertex_format={uint32, float32} |
| - step_mode= {undefined, vertex, instance, mixed} - where mixed only applies for vertex_buffer_count > 1 |
| ` |
| ) |
| .params(u => |
| u |
| .combine('vertex_attribute_count', [1, 4, 8, 16]) |
| .combine('vertex_buffer_count', [1, 4, 8]) |
| .combine('vertex_format', ['uint32', 'float32'] as const) |
| .combine('step_mode', [undefined, 'vertex', 'instance', 'mixed'] as const) |
| .unless(p => p.vertex_attribute_count < p.vertex_buffer_count) |
| .unless(p => p.step_mode === 'mixed' && p.vertex_buffer_count <= 1) |
| ) |
| .fn(t => { |
| // MAINTENANCE_TODO: refactor this test so it doesn't need a storage buffer OR |
| // consider removing it. It's possible the tests in src/webgpu/api/operation/vertex_state/correctness.spec.ts |
| // already test this. |
| t.skipIf( |
| t.isCompatibility && !(t.device.limits.maxStorageBuffersInFragmentStage! > 0), |
| `maxStorageBuffersInFragmentStage(${t.device.limits.maxStorageBuffersInFragmentStage}) is 0` |
| ); |
| const vertexCount = 4; |
| const instanceCount = 4; |
| |
| // In compat mode, @builtin(vertex_index) and @builtin(instance_index) each take an attribute. |
| const maxAttributes = t.device.limits.maxVertexAttributes - (t.isCompatibility ? 2 : 0); |
| const numAttributes = Math.min(maxAttributes, t.params.vertex_attribute_count); |
| const maxAttributesPerVertexBuffer = Math.ceil(numAttributes / t.params.vertex_buffer_count); |
| |
| let shaderLocation = 0; |
| let attributeValue = 0; |
| const bufferLayouts: GPUVertexBufferLayout[] = []; |
| |
| let ExpectedDataConstructor: TypedArrayBufferViewConstructor; |
| switch (t.params.vertex_format) { |
| case 'uint32': |
| ExpectedDataConstructor = Uint32Array; |
| break; |
| case 'float32': |
| ExpectedDataConstructor = Float32Array; |
| break; |
| } |
| |
| // Populate |bufferLayouts|, |vertexBufferData|, and |vertexBuffers|. |
| // We will use this to both create the render pipeline, and produce the |
| // expected data on the CPU. |
| // Attributes in each buffer will be interleaved. |
| const vertexBuffers: GPUBuffer[] = []; |
| const vertexBufferData: TypedArrayBufferView[] = []; |
| for (let b = 0; b < t.params.vertex_buffer_count; ++b) { |
| const vertexBufferValues: number[] = []; |
| |
| let offset = 0; |
| let stepMode = t.params.step_mode; |
| |
| // If stepMode is mixed, alternate between vertex and instance. |
| if (stepMode === 'mixed') { |
| stepMode = (['vertex', 'instance'] as const)[b % 2]; |
| } |
| |
| let vertexOrInstanceCount: number; |
| switch (stepMode) { |
| case undefined: |
| case 'vertex': |
| vertexOrInstanceCount = vertexCount; |
| break; |
| case 'instance': |
| vertexOrInstanceCount = instanceCount; |
| break; |
| } |
| |
| const attributes: GPUVertexAttribute[] = []; |
| const numAttributesForBuffer = Math.min( |
| maxAttributesPerVertexBuffer, |
| maxAttributes - b * maxAttributesPerVertexBuffer |
| ); |
| |
| for (let a = 0; a < numAttributesForBuffer; ++a) { |
| const attribute: GPUVertexAttribute = { |
| format: t.params.vertex_format, |
| shaderLocation, |
| offset, |
| }; |
| attributes.push(attribute); |
| |
| offset += ExpectedDataConstructor.BYTES_PER_ELEMENT; |
| shaderLocation += 1; |
| } |
| |
| for (let v = 0; v < vertexOrInstanceCount; ++v) { |
| for (let a = 0; a < numAttributesForBuffer; ++a) { |
| vertexBufferValues.push(attributeValue); |
| attributeValue += 1.234; // Values will get rounded later if we make a Uint32Array. |
| } |
| } |
| |
| bufferLayouts.push({ |
| attributes, |
| arrayStride: offset, |
| stepMode, |
| }); |
| |
| const data = new ExpectedDataConstructor(vertexBufferValues); |
| vertexBufferData.push(data); |
| vertexBuffers.push(t.makeBufferWithContents(data, GPUBufferUsage.VERTEX)); |
| } |
| |
| // Create an array of shader locations [0, 1, 2, 3, ...] for easy iteration. |
| const vertexInputShaderLocations = new Array(shaderLocation).fill(0).map((_, i) => i); |
| |
| // Create the expected data buffer. |
| const expectedData = new ExpectedDataConstructor( |
| vertexCount * instanceCount * vertexInputShaderLocations.length |
| ); |
| |
| // Populate the expected data. This is a CPU-side version of what we expect the shader |
| // to do. |
| for (let vertexIndex = 0; vertexIndex < vertexCount; ++vertexIndex) { |
| for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) { |
| bufferLayouts.forEach((bufferLayout, b) => { |
| for (const attribute of bufferLayout.attributes) { |
| const primitiveId = vertexCount * instanceIndex + vertexIndex; |
| const outputIndex = |
| primitiveId * vertexInputShaderLocations.length + attribute.shaderLocation; |
| |
| let vertexOrInstanceIndex: number; |
| switch (bufferLayout.stepMode) { |
| case undefined: |
| case 'vertex': |
| vertexOrInstanceIndex = vertexIndex; |
| break; |
| case 'instance': |
| vertexOrInstanceIndex = instanceIndex; |
| break; |
| } |
| |
| const view = new ExpectedDataConstructor( |
| vertexBufferData[b].buffer, |
| bufferLayout.arrayStride * vertexOrInstanceIndex + attribute.offset, |
| 1 |
| ); |
| expectedData[outputIndex] = view[0]; |
| } |
| }); |
| } |
| } |
| |
| let wgslFormat: string; |
| switch (t.params.vertex_format) { |
| case 'uint32': |
| wgslFormat = 'u32'; |
| break; |
| case 'float32': |
| wgslFormat = 'f32'; |
| break; |
| } |
| |
| // Maximum inter-stage shader location is 14, and we need to consume one for primitiveId, 12 for |
| // location 0 to 11, and combine the remaining vertex inputs into one location (one |
| // vec4<wgslFormat> when vertex_attribute_count === 16). |
| const interStageScalarShaderLocation = Math.min(shaderLocation, 12); |
| const interStageScalarShaderLocations = new Array(interStageScalarShaderLocation) |
| .fill(0) |
| .map((_, i) => i); |
| |
| let accumulateVariableDeclarationsInVertexShader = ''; |
| let accumulateVariableAssignmentsInVertexShader = ''; |
| let accumulateVariableDeclarationsInFragmentShader = ''; |
| let accumulateVariableAssignmentsInFragmentShader = ''; |
| // The remaining 3 vertex attributes |
| if (numAttributes === 16) { |
| accumulateVariableDeclarationsInVertexShader = ` |
| @location(13) @interpolate(flat, either) outAttrib13 : vec4<${wgslFormat}>, |
| `; |
| accumulateVariableAssignmentsInVertexShader = ` |
| output.outAttrib13 = |
| vec4<${wgslFormat}>(input.attrib12, input.attrib13, input.attrib14, input.attrib15); |
| `; |
| accumulateVariableDeclarationsInFragmentShader = ` |
| @location(13) @interpolate(flat, either) attrib13 : vec4<${wgslFormat}>, |
| `; |
| accumulateVariableAssignmentsInFragmentShader = ` |
| outBuffer.primitives[input.primitiveId].attrib12 = input.attrib13.x; |
| outBuffer.primitives[input.primitiveId].attrib13 = input.attrib13.y; |
| outBuffer.primitives[input.primitiveId].attrib14 = input.attrib13.z; |
| outBuffer.primitives[input.primitiveId].attrib15 = input.attrib13.w; |
| `; |
| } else if (numAttributes === 14) { |
| accumulateVariableDeclarationsInVertexShader = ` |
| @location(13) @interpolate(flat, either) outAttrib13 : vec4<${wgslFormat}>, |
| `; |
| accumulateVariableAssignmentsInVertexShader = ` |
| output.outAttrib13 = |
| vec4<${wgslFormat}>(input.attrib12, input.attrib13, 0, 0); |
| `; |
| accumulateVariableDeclarationsInFragmentShader = ` |
| @location(13) @interpolate(flat, either) attrib13 : vec4<${wgslFormat}>, |
| `; |
| accumulateVariableAssignmentsInFragmentShader = ` |
| outBuffer.primitives[input.primitiveId].attrib12 = input.attrib13.x; |
| outBuffer.primitives[input.primitiveId].attrib13 = input.attrib13.y; |
| `; |
| } |
| |
| const pipeline = t.device.createRenderPipeline({ |
| layout: 'auto', |
| vertex: { |
| module: t.device.createShaderModule({ |
| code: ` |
| struct Inputs { |
| @builtin(vertex_index) vertexIndex : u32, |
| @builtin(instance_index) instanceIndex : u32, |
| ${vertexInputShaderLocations.map(i => ` @location(${i}) attrib${i} : ${wgslFormat},`).join('\n')} |
| }; |
| |
| struct Outputs { |
| @builtin(position) Position : vec4<f32>, |
| ${interStageScalarShaderLocations |
| .map(i => ` @location(${i}) @interpolate(flat, either) outAttrib${i} : ${wgslFormat},`) |
| .join('\n')} |
| @location(${interStageScalarShaderLocations.length}) @interpolate(flat, either) primitiveId : u32, |
| ${accumulateVariableDeclarationsInVertexShader} |
| }; |
| |
| @vertex fn main(input : Inputs) -> Outputs { |
| var output : Outputs; |
| ${interStageScalarShaderLocations.map(i => ` output.outAttrib${i} = input.attrib${i};`).join('\n')} |
| ${accumulateVariableAssignmentsInVertexShader} |
| |
| output.primitiveId = input.instanceIndex * ${instanceCount}u + input.vertexIndex; |
| output.Position = vec4<f32>(0.0, 0.0, 0.5, 1.0); |
| return output; |
| } |
| `, |
| }), |
| entryPoint: 'main', |
| buffers: bufferLayouts, |
| }, |
| fragment: { |
| module: t.device.createShaderModule({ |
| code: ` |
| struct Inputs { |
| ${interStageScalarShaderLocations |
| .map(i => ` @location(${i}) @interpolate(flat, either) attrib${i} : ${wgslFormat},`) |
| .join('\n')} |
| @location(${interStageScalarShaderLocations.length}) @interpolate(flat, either) primitiveId : u32, |
| ${accumulateVariableDeclarationsInFragmentShader} |
| }; |
| |
| struct OutPrimitive { |
| ${vertexInputShaderLocations.map(i => ` attrib${i} : ${wgslFormat},`).join('\n')} |
| }; |
| struct OutBuffer { |
| primitives : array<OutPrimitive> |
| }; |
| @group(0) @binding(0) var<storage, read_write> outBuffer : OutBuffer; |
| |
| @fragment fn main(input : Inputs) { |
| ${interStageScalarShaderLocations |
| .map(i => ` outBuffer.primitives[input.primitiveId].attrib${i} = input.attrib${i};`) |
| .join('\n')} |
| ${accumulateVariableAssignmentsInFragmentShader} |
| } |
| `, |
| }), |
| entryPoint: 'main', |
| targets: [ |
| { |
| format: 'rgba8unorm', |
| writeMask: 0, |
| }, |
| ], |
| }, |
| primitive: { |
| topology: 'point-list', |
| }, |
| }); |
| |
| const resultBuffer = t.createBufferTracked({ |
| usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC, |
| size: vertexCount * instanceCount * vertexInputShaderLocations.length * 4, |
| }); |
| |
| const resultBindGroup = t.device.createBindGroup({ |
| layout: pipeline.getBindGroupLayout(0), |
| entries: [ |
| { |
| binding: 0, |
| resource: { |
| buffer: resultBuffer, |
| }, |
| }, |
| ], |
| }); |
| |
| const commandEncoder = t.device.createCommandEncoder(); |
| const renderPass = commandEncoder.beginRenderPass({ |
| colorAttachments: [ |
| { |
| // Dummy render attachment - not used (WebGPU doesn't allow using a render pass with no |
| // attachments) |
| view: t |
| .createTextureTracked({ |
| usage: GPUTextureUsage.RENDER_ATTACHMENT, |
| size: [1], |
| format: 'rgba8unorm', |
| }) |
| .createView(), |
| clearValue: [0, 0, 0, 0], |
| loadOp: 'clear', |
| storeOp: 'store', |
| }, |
| ], |
| }); |
| |
| renderPass.setPipeline(pipeline); |
| renderPass.setBindGroup(0, resultBindGroup); |
| for (let i = 0; i < t.params.vertex_buffer_count; ++i) { |
| renderPass.setVertexBuffer(i, vertexBuffers[i]); |
| } |
| renderPass.draw(vertexCount, instanceCount); |
| renderPass.end(); |
| t.device.queue.submit([commandEncoder.finish()]); |
| |
| t.expectGPUBufferValuesEqual(resultBuffer, expectedData); |
| }); |
| |
| g.test('vertex_attributes,formats') |
| .desc( |
| `Test all vertex formats are fetched correctly. |
| |
| Runs a basic vertex shader which loads vertex data from two attributes which |
| may have different formats. Write data out to a storage buffer and check that |
| it was loaded correctly. |
| |
| Params: |
| - vertex_format_1={...all_vertex_formats} |
| - vertex_format_2={...all_vertex_formats} |
| ` |
| ) |
| .unimplemented(); |
| |
| g.test(`largeish_buffer`) |
| .desc( |
| ` |
| Test a very large range of buffer is bound. |
| For a render pipeline that use a vertex step mode and a instance step mode vertex buffer, test |
| that : |
| - For draw, drawIndirect, drawIndexed and drawIndexedIndirect: |
| - The bound range of vertex step mode vertex buffer is significantly larger than necessary |
| - The bound range of instance step mode vertex buffer is significantly larger than necessary |
| - A large buffer is bound to an unused slot |
| - For drawIndexed and drawIndexedIndirect: |
| - The bound range of index buffer is significantly larger than necessary |
| - For drawIndirect and drawIndexedIndirect: |
| - The indirect buffer is significantly larger than necessary |
| ` |
| ) |
| .unimplemented(); |