export const description = ` Test the result of writing textures through texture views with various options. Reads value from a shader array, writes the value via various write methods. Check the texture result with the expected texel view. All x= every possible view write method: { - storage write {fragment, compute} - render pass store - render pass resolve } Format reinterpretation is not tested here. It is in format_reinterpretation.spec.ts. TODO: Write helper for this if not already available (see resource_init, buffer_sync_test for related code). `; import { makeTestGroup } from '../../../../common/framework/test_group.js'; import { unreachable } from '../../../../common/util/util.js'; import { getTextureFormatType, kRegularTextureFormats, RegularTextureFormat, } from '../../../format_info.js'; import { AllFeaturesMaxLimitsGPUTest, GPUTest } from '../../../gpu_test.js'; import * as ttu from '../../../texture_test_utils.js'; import { kFullscreenQuadVertexShaderCode } from '../../../util/shader.js'; import { TexelView } from '../../../util/texture/texel_view.js'; export const g = makeTestGroup(AllFeaturesMaxLimitsGPUTest); const kTextureViewWriteMethods = [ 'storage-write-fragment', 'storage-write-compute', 'render-pass-store', 'render-pass-resolve', ] as const; type TextureViewWriteMethod = (typeof kTextureViewWriteMethods)[number]; const kTextureViewUsageMethods = ['inherit', 'minimal'] as const; type TextureViewUsageMethod = (typeof kTextureViewUsageMethods)[number]; // Src color values to read from a shader array. const kColorsFloat = [ { R: 1.0, G: 0.0, B: 0.0, A: 0.8 }, { R: 0.0, G: 1.0, B: 0.0, A: 0.7 }, { R: 0.0, G: 0.0, B: 0.0, A: 0.6 }, { R: 0.0, G: 0.0, B: 0.0, A: 0.5 }, { R: 1.0, G: 1.0, B: 1.0, A: 0.4 }, { R: 0.7, G: 0.0, B: 0.0, A: 0.3 }, { R: 0.0, G: 0.8, B: 0.0, A: 0.2 }, { R: 0.0, G: 0.0, B: 0.9, A: 0.1 }, { R: 0.1, G: 0.2, B: 0.0, A: 0.3 }, { R: 0.4, G: 0.3, B: 0.6, A: 0.8 }, ]; function floatToIntColor(c: number) { return Math.floor(c * 100); } const kColorsInt = kColorsFloat.map(c => { return { R: floatToIntColor(c.R), G: floatToIntColor(c.G), B: floatToIntColor(c.B), A: floatToIntColor(c.A), }; }); const kTextureSize = 16; function writeTextureAndGetExpectedTexelView( t: GPUTest, method: TextureViewWriteMethod, view: GPUTextureView, format: RegularTextureFormat, sampleCount: number ) { const type = getTextureFormatType(format); const isFloatType = type === 'float' || type === 'unfilterable-float'; const kColors = isFloatType ? kColorsFloat : kColorsInt; const expectedTexelView = TexelView.fromTexelsAsColors( format, coords => { const pixelPos = coords.y * kTextureSize + coords.x; return kColors[pixelPos % kColors.length]; }, { clampToFormatRange: true } ); const vecType = isFloatType ? 'vec4f' : type === 'sint' ? 'vec4i' : 'vec4u'; const kColorArrayShaderString = `array<${vecType}, ${kColors.length}>( ${kColors.map(t => `${vecType}(${t.R}, ${t.G}, ${t.B}, ${t.A}) `).join(',')} )`; switch (method) { case 'storage-write-compute': { const pipeline = t.device.createComputePipeline({ layout: 'auto', compute: { module: t.device.createShaderModule({ code: ` @group(0) @binding(0) var dst: texture_storage_2d<${format}, write>; @compute @workgroup_size(1, 1) fn main( @builtin(global_invocation_id) global_id: vec3, ) { const src = ${kColorArrayShaderString}; let coord = vec2u(global_id.xy); let idx = coord.x + coord.y * ${kTextureSize}; textureStore(dst, coord, src[idx % ${kColors.length}]); }`, }), entryPoint: 'main', }, }); const commandEncoder = t.device.createCommandEncoder(); const pass = commandEncoder.beginComputePass(); pass.setPipeline(pipeline); pass.setBindGroup( 0, t.device.createBindGroup({ layout: pipeline.getBindGroupLayout(0), entries: [ { binding: 0, resource: view, }, ], }) ); pass.dispatchWorkgroups(kTextureSize, kTextureSize); pass.end(); t.device.queue.submit([commandEncoder.finish()]); } break; case 'storage-write-fragment': { // Create a placeholder color attachment texture, // The size of which equals that of format texture we are testing, // so that we have the same number of fragments and texels. const kPlaceholderTextureFormat = 'rgba8unorm'; const placeholderTexture = t.createTextureTracked({ format: kPlaceholderTextureFormat, size: [kTextureSize, kTextureSize], usage: GPUTextureUsage.RENDER_ATTACHMENT, }); const pipeline = t.device.createRenderPipeline({ layout: 'auto', vertex: { module: t.device.createShaderModule({ code: kFullscreenQuadVertexShaderCode, }), }, fragment: { module: t.device.createShaderModule({ code: ` @group(0) @binding(0) var dst: texture_storage_2d<${format}, write>; @fragment fn main( @builtin(position) fragCoord: vec4, ) { const src = ${kColorArrayShaderString}; let coord = vec2u(fragCoord.xy); let idx = coord.x + coord.y * ${kTextureSize}; textureStore(dst, coord, src[idx % ${kColors.length}]); }`, }), // Set writeMask to 0 as the fragment shader has no output. targets: [ { format: kPlaceholderTextureFormat, writeMask: 0, }, ], }, }); const commandEncoder = t.device.createCommandEncoder(); const pass = commandEncoder.beginRenderPass({ colorAttachments: [ { view: placeholderTexture.createView(), loadOp: 'clear', storeOp: 'discard', }, ], }); pass.setPipeline(pipeline); pass.setBindGroup( 0, t.device.createBindGroup({ layout: pipeline.getBindGroupLayout(0), entries: [ { binding: 0, resource: view, }, ], }) ); pass.draw(6); pass.end(); t.device.queue.submit([commandEncoder.finish()]); } break; case 'render-pass-store': case 'render-pass-resolve': { // Create a placeholder color attachment texture for the store target when tesing texture is used as resolve target. const targetView = method === 'render-pass-store' ? view : t .createTextureTracked({ format, size: [kTextureSize, kTextureSize], usage: GPUTextureUsage.RENDER_ATTACHMENT, sampleCount: 4, }) .createView(); const resolveView = method === 'render-pass-store' ? undefined : view; const multisampleCount = method === 'render-pass-store' ? sampleCount : 4; const pipeline = t.device.createRenderPipeline({ layout: 'auto', vertex: { module: t.device.createShaderModule({ code: kFullscreenQuadVertexShaderCode, }), }, fragment: { module: t.device.createShaderModule({ code: ` @fragment fn main( @builtin(position) fragCoord: vec4, ) -> @location(0) ${vecType} { const src = ${kColorArrayShaderString}; let coord = vec2u(fragCoord.xy); let idx = coord.x + coord.y * ${kTextureSize}; return src[idx % ${kColors.length}]; }`, }), targets: [ { format, }, ], }, multisample: { count: multisampleCount, }, }); const commandEncoder = t.device.createCommandEncoder(); const pass = commandEncoder.beginRenderPass({ colorAttachments: [ { view: targetView, resolveTarget: resolveView, loadOp: 'clear', storeOp: 'store', }, ], }); pass.setPipeline(pipeline); pass.draw(6); pass.end(); t.device.queue.submit([commandEncoder.finish()]); } break; default: unreachable(); } return expectedTexelView; } function getTextureViewUsage( viewUsageMethod: TextureViewUsageMethod, minimalUsageForTest: GPUTextureUsageFlags ) { switch (viewUsageMethod) { case 'inherit': return 0; case 'minimal': return minimalUsageForTest; default: unreachable(); } } g.test('format') .desc( `Views of every allowed format. Read values from color array in the shader, and write it to the texture view via different write methods. - x= every texture format - x= sampleCount {1, 4} if valid - x= every possible view write method (see above) - x= inherited or minimal texture view usage TODO: Test sampleCount > 1 for 'render-pass-store' after extending copySinglePixelTextureToBufferUsingComputePass to read multiple pixels from multisampled textures. [1] TODO: Test rgb10a2uint when TexelRepresentation.numericRange is made per-component. [2] ` ) .params(u => u // .combine('method', kTextureViewWriteMethods) .combine('format', kRegularTextureFormats) .combine('sampleCount', [1, 4]) .filter(({ format, method, sampleCount }) => { // [2] if (format === 'rgb10a2uint') { return false; } switch (method) { case 'storage-write-compute': case 'storage-write-fragment': return sampleCount === 1; case 'render-pass-resolve': return sampleCount === 1; case 'render-pass-store': // [1] if (sampleCount > 1) { return false; } break; } return true; }) .combine('viewUsageMethod', kTextureViewUsageMethods) ) .fn(t => { const { format, method, sampleCount, viewUsageMethod } = t.params; t.skipIfTextureFormatNotSupported(format); if (sampleCount > 1) { t.skipIfTextureFormatNotMultisampled(format); } switch (method) { case 'storage-write-compute': case 'storage-write-fragment': t.skipIfTextureFormatNotUsableWithStorageAccessMode('write-only', format); break; case 'render-pass-store': t.skipIfTextureFormatNotUsableAsRenderAttachment(format); break; case 'render-pass-resolve': // Requires multisample in `writeTextureAndGetExpectedTexelView` t.skipIfTextureFormatNotUsableAsRenderAttachment(format); t.skipIfTextureFormatNotResolvable(format); break; } t.skipIf( t.isCompatibility && method === 'storage-write-fragment' && !(t.device.limits.maxStorageBuffersInFragmentStage! > 0), `maxStorageBuffersInFragmentStage(${t.device.limits.maxStorageBuffersInFragmentStage}) < 1` ); const textureUsageForMethod = method.includes('storage') ? GPUTextureUsage.STORAGE_BINDING : GPUTextureUsage.RENDER_ATTACHMENT; const usage = GPUTextureUsage.COPY_SRC | textureUsageForMethod; const texture = t.createTextureTracked({ format, usage, size: [kTextureSize, kTextureSize], sampleCount, }); const view = texture.createView({ usage: getTextureViewUsage(viewUsageMethod, textureUsageForMethod), }); const expectedTexelView = writeTextureAndGetExpectedTexelView( t, method, view, format, sampleCount ); // [1] Use copySinglePixelTextureToBufferUsingComputePass to check multisampled texture. ttu.expectTexelViewComparisonIsOkInTexture(t, { texture }, expectedTexelView, [ kTextureSize, kTextureSize, ]); }); g.test('dimension') .desc( `Views of every allowed dimension. - x= a representative subset of formats - x= {every texture dimension} x {every valid view dimension} (per gpuweb#79 no dimension-count reinterpretations, like 2d-array <-> 3d, are possible) - x= sampleCount {1, 4} if valid - x= every possible view write method (see above) ` ) .unimplemented(); g.test('aspect') .desc( `Views of every allowed aspect of depth/stencil textures. - x= every depth/stencil format - x= {"all", "stencil-only", "depth-only"} where valid for the format - x= sampleCount {1, 4} if valid - x= every possible view write method (see above) ` ) .unimplemented();