diff --git a/src/webgpu/api/operation/rendering/3d_texture_slices.spec.ts b/src/webgpu/api/operation/rendering/3d_texture_slices.spec.ts index ad3d5f1b54cf..34598c06741e 100644 --- a/src/webgpu/api/operation/rendering/3d_texture_slices.spec.ts +++ b/src/webgpu/api/operation/rendering/3d_texture_slices.spec.ts @@ -6,40 +6,35 @@ Test rendering to 3d texture slices. import { makeTestGroup } from '../../../../common/framework/test_group.js'; import { GPUTest } from '../../../gpu_test.js'; -import { - kBytesPerRowAlignment, -} from '../../../util/texture/layout.js'; +import { kBytesPerRowAlignment } from '../../../util/texture/layout.js'; + +const kSize = 4; class F extends GPUTest { - createTexture( + create3DTexture( options: { format?: GPUTextureFormat; - dimension?: GPUTextureDimension; width?: number; height?: number; - arrayLayerCount?: number; + depthOrArrayLayers?: number; mipLevelCount?: number; - sampleCount?: number; usage?: GPUTextureUsageFlags; } = {} ): GPUTexture { const { format = 'rgba8unorm', - dimension = '2d', - width = 16, - height = 16, - arrayLayerCount = 1, + width = kSize, + height = kSize, + depthOrArrayLayers = 1, mipLevelCount = 1, - sampleCount = 1, usage = GPUTextureUsage.RENDER_ATTACHMENT, } = options; return this.device.createTexture({ - size: { width, height, depthOrArrayLayers: arrayLayerCount }, + size: [width, height, depthOrArrayLayers], + dimension: '3d', format, - dimension, mipLevelCount, - sampleCount, usage, }); } @@ -48,160 +43,35 @@ class F extends GPUTest { texture: GPUTexture, textureViewDescriptor?: GPUTextureViewDescriptor ): GPURenderPassColorAttachment { - const view = texture.createView(textureViewDescriptor); - return { - view, + view: texture.createView(textureViewDescriptor), clearValue: { r: 1.0, g: 0.0, b: 0.0, a: 1.0 }, loadOp: 'clear', storeOp: 'store', }; } -} - -export const g = makeTestGroup(F); - -g.test('mip_levels') - .desc( - ` -Render to 3d texture slices with mipLevel 0. -` - ) - .params(u => - u - .combine('mipLevel', [0, 1]) - .combine('depthSlice', [0, 1]) - ) - .fn(async t => { - const { mipLevel, depthSlice } = t.params; - const baseWidth = 4; - const baseHeight = 4; - const texture = t.device.createTexture({ - size: { width: baseWidth << mipLevel, height: baseHeight << mipLevel, depthOrArrayLayers: (depthSlice + 1) << mipLevel }, - format: 'rgba8unorm', - dimension: '3d', - mipLevelCount: mipLevel + 1, - sampleCount: 1, - usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC, - }); - const viewDiscriptor: GPUTextureViewDescriptor = { - baseMipLevel: mipLevel, - mipLevelCount: 1, - baseArrayLayer: 0, - arrayLayerCount: 1, - }; - const dst = t.device.createBuffer({ - size: (baseHeight - 1) * kBytesPerRowAlignment + baseWidth * 4, - usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST, - }); - - const pipeline = t.device.createRenderPipeline({ - layout: 'auto', - vertex: { - module: t.device.createShaderModule({ - code: ` - @vertex fn main( - @builtin(vertex_index) VertexIndex : u32 - ) -> @builtin(position) vec4 { - var pos : array, 3> = array, 3>( - vec2(-1.0, 1.0), - vec2(1.0, -1.0), - vec2(-1.0, -1.0)); - return vec4(pos[VertexIndex], 0.0, 1.0); - } - `, - }), - entryPoint: 'main', - }, - fragment: { - module: t.device.createShaderModule({ - code: ` - @fragment fn main() -> @location(0) vec4 { - return vec4(0.0, 1.0, 0.0, 1.0); - } - `, - }), - entryPoint: 'main', - targets: [{ format: 'rgba8unorm' }], - }, - primitive: { topology: 'triangle-list' }, - }); - - const encoder = t.device.createCommandEncoder(); - const pass = encoder.beginRenderPass({ - colorAttachments: [ - { - view: texture.createView(viewDiscriptor), - depthSlice, - clearValue: { r: 1.0, g: 0.0, b: 0.0, a: 1.0 }, - loadOp: 'clear', - storeOp: 'store', - }, - ], - }); - pass.setPipeline(pipeline); - pass.draw(3); - pass.end(); - encoder.copyTextureToBuffer( - { texture: texture, mipLevel, origin: { x: 0, y: 0, z: depthSlice } }, - { buffer: dst, bytesPerRow: 256 }, - { width: baseWidth, height: baseHeight, depthOrArrayLayers: 1 } - ); - t.device.queue.submit([encoder.finish()]); - - let expectedData = new Uint8Array(dst.size); - for (let i = 0; i < baseHeight; i++) { - for (let j = 0; j < baseWidth; j++) { - expectedData[i * 256 + j * 4] = j < i ? 0x00 : 0xff; - expectedData[i * 256 + j * 4 + 1] = j < i ? 0xff : 0x00; - expectedData[i * 256 + j * 4 + 2] = 0x00; - expectedData[i * 256 + j * 4 + 3] = 0xff; - } - } - - t.expectGPUBufferValuesEqual(dst, expectedData); - }); - - g.test('multiple_attachments') - .desc( - ` -Render to 3d texture slices in multiple attachments. -` - ) - .params(u => - u - .combine('attachmentNum', [1, 2, 4]) - .beginSubcases() - .combine('sameDepthSlice', [true, false]) - .combine('sameTexture', [true, false]) - .combine('samePass', [true, false]) - .filter(t => { - return t.sameDepthSlice === false || t.sameTexture === false || t.samePass === false; - }) - ) - .fn(t => { - const {attachmentNum, sameDepthSlice, sameTexture, samePass } = t.params; + createRenderPipeline( + options: { + format?: GPUTextureFormat; + attachmentCount?: number; + } = {} + ): GPURenderPipeline { + const { format = 'rgba8unorm', attachmentCount = 1 } = options; let locations = ''; let outputs = ''; - let targets: GPUColorTargetState[] | null = []; - if (samePass) { - for (let i = 0; i < attachmentNum; i++) { - locations = locations + `@location(${i}) color${i} : vec4f, \n`; - outputs = outputs + `output.color${i} = vec4f(0.0, 1.0, 0.0, 1.0);\n`; - targets.push({ format: 'rgba8unorm'} as GPUColorTargetState); - } - } else { - locations = `@location(0) color0 : vec4f, \n`; - outputs = `output.color0 = vec4f(0.0, 1.0, 0.0, 1.0);\n`; - targets.push({ format: 'rgba8unorm'} as GPUColorTargetState); + const targets: GPUColorTargetState[] | null = []; + for (let i = 0; i < attachmentCount; i++) { + locations = locations + `@location(${i}) color${i} : vec4f, \n`; + outputs = outputs + `output.color${i} = vec4f(0.0, 1.0, 0.0, 1.0);\n`; + targets.push({ format } as GPUColorTargetState); } - const pipeline = t.device.createRenderPipeline({ + return this.device.createRenderPipeline({ layout: 'auto', vertex: { - module: t.device.createShaderModule({ + module: this.device.createShaderModule({ code: ` @vertex fn main( @builtin(vertex_index) VertexIndex : u32 @@ -217,7 +87,7 @@ Render to 3d texture slices in multiple attachments. entryPoint: 'main', }, fragment: { - module: t.device.createShaderModule({ + module: this.device.createShaderModule({ code: ` struct Output { ${locations} @@ -230,42 +100,264 @@ Render to 3d texture slices in multiple attachments. `, }), entryPoint: 'main', - targets: targets, + targets, }, primitive: { topology: 'triangle-list' }, }); + } + + getBufferSizeAndOffset( + attachmentWidth: number, + attachmentHeight: number, + attachmentCount: number + ): { bufferSize: number; bufferOffset: number } { + const bufferSize = + (attachmentCount * attachmentHeight - 1) * kBytesPerRowAlignment + attachmentWidth * 4; + const bufferOffset = attachmentCount > 1 ? attachmentHeight * kBytesPerRowAlignment : 0; + return { bufferSize, bufferOffset }; + } + + checkAttachmentResult( + attachmentWidth: number, + attachmentHeight: number, + attachmentCount: number, + buffer: GPUBuffer + ) { + const { bufferSize, bufferOffset } = this.getBufferSizeAndOffset( + attachmentWidth, + attachmentHeight, + attachmentCount + ); + const expectedData = new Uint8Array(bufferSize); + for (let i = 0; i < attachmentCount; i++) { + for (let j = 0; j < attachmentHeight; j++) { + for (let k = 0; k < attachmentWidth; k++) { + expectedData[i * bufferOffset + j * 256 + k * 4] = k < j ? 0x00 : 0xff; + expectedData[i * bufferOffset + j * 256 + k * 4 + 1] = k < j ? 0xff : 0x00; + expectedData[i * bufferOffset + j * 256 + k * 4 + 2] = 0x00; + expectedData[i * bufferOffset + j * 256 + k * 4 + 3] = 0xff; + } + } + } - const texDescriptor = { - dimension: '3d' as GPUTextureDimension, - arrayLayerCount: attachmentNum, + this.expectGPUBufferValuesEqual(buffer, expectedData); + } +} + +export const g = makeTestGroup(F); + +g.test('one_color_attachment,mip_levels') + .desc( + ` + Render to a 3d texture slice with mip levels. + ` + ) + .params(u => u.combine('mipLevel', [0, 1, 2]).combine('depthSlice', [0, 1])) + .fn(t => { + const { mipLevel, depthSlice } = t.params; + + const texture = t.create3DTexture({ + width: kSize << mipLevel, + height: kSize << mipLevel, + depthOrArrayLayers: 2 << mipLevel, + mipLevelCount: mipLevel + 1, + usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC, + }); + + const { bufferSize } = t.getBufferSizeAndOffset(kSize, kSize, 1); + + const buffer = t.device.createBuffer({ + size: bufferSize, + usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST, + }); + const pipeline = t.createRenderPipeline(); + + const colorAttachment = t.getColorAttachment(texture, { + baseMipLevel: mipLevel, + mipLevelCount: 1, + }); + colorAttachment.depthSlice = depthSlice; + + const encoder = t.createEncoder('non-pass'); + const pass = encoder.encoder.beginRenderPass({ colorAttachments: [colorAttachment] }); + pass.setPipeline(pipeline); + pass.draw(3); + pass.end(); + encoder.encoder.copyTextureToBuffer( + { texture, mipLevel, origin: { x: 0, y: 0, z: depthSlice } }, + { buffer, bytesPerRow: 256 }, + { width: kSize, height: kSize, depthOrArrayLayers: 1 } + ); + t.device.queue.submit([encoder.finish()]); + + t.checkAttachmentResult(kSize, kSize, 1, buffer); + }); + +g.test('multiple_color_attachments,same_mip_level') + .desc( + ` + Render to the different slices of 3d texture in multiple color attachments. + - Same 3d texture with different slices at same mip level + - Different 3d textures with same slice at same mip level + ` + ) + .params(u => + u + .combine('sameTexture', [true, false]) + .beginSubcases() + .combine('samePass', [true, false]) + .combine('mipLevel', [0, 1]) + ) + .fn(t => { + const { sameTexture, samePass, mipLevel } = t.params; + + const format = 'rgba8unorm' as GPUTextureFormat; + const formatByteCost = 8; + const maxAttachmentCountPerSample = Math.trunc( + t.device.limits.maxColorAttachmentBytesPerSample / formatByteCost + ); + const attachmentCount = Math.min( + maxAttachmentCountPerSample, + t.device.limits.maxColorAttachments + ); + + const descriptor = { + format, + width: kSize << mipLevel, + height: kSize << mipLevel, + depthOrArrayLayers: (1 << attachmentCount) << mipLevel, + mipLevelCount: mipLevel + 1, + usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC, }; - const texture = t.createTexture(texDescriptor); + const texture = t.create3DTexture(descriptor); - const colorAttachments = []; - for (let i = 0; i < attachmentNum; i++) { - const colorAttachment = t.getColorAttachment( - sameTexture ? texture : t.createTexture(texDescriptor) - ); - colorAttachment.depthSlice = sameDepthSlice ? 0 : i; + const textures: GPUTexture[] = []; + const colorAttachments: GPURenderPassColorAttachment[] = []; + for (let i = 0; i < attachmentCount; i++) { + if (sameTexture) { + textures.push(texture); + } else { + const diffTexture = t.create3DTexture(descriptor); + textures.push(diffTexture); + } + + const colorAttachment = t.getColorAttachment(textures[i], { + baseMipLevel: mipLevel, + mipLevelCount: 1, + }); + colorAttachment.depthSlice = sameTexture ? i : 0; colorAttachments.push(colorAttachment); } const encoder = t.createEncoder('non-pass'); + if (samePass) { + const pipeline = t.createRenderPipeline({ attachmentCount }); + const pass = encoder.encoder.beginRenderPass({ colorAttachments }); pass.setPipeline(pipeline); pass.draw(3); pass.end(); } else { - for (let i = 0; i < attachmentNum; i++) { + for (let i = 0; i < attachmentCount; i++) { + const pipeline = t.createRenderPipeline(); + const pass = encoder.encoder.beginRenderPass({ colorAttachments: [colorAttachments[i]] }); pass.setPipeline(pipeline); pass.draw(3); pass.end(); } } + + const { bufferSize, bufferOffset } = t.getBufferSizeAndOffset(kSize, kSize, attachmentCount); + const buffer = t.device.createBuffer({ + size: bufferSize, + usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST, + }); + for (let i = 0; i < attachmentCount; i++) { + encoder.encoder.copyTextureToBuffer( + { + texture: textures[i], + mipLevel, + origin: { x: 0, y: 0, z: sameTexture ? i : 0 }, + }, + { buffer, bytesPerRow: 256, offset: bufferOffset * i }, + { width: kSize, height: kSize, depthOrArrayLayers: 1 } + ); + } + t.device.queue.submit([encoder.finish()]); - + t.checkAttachmentResult(kSize, kSize, attachmentCount, buffer); + }); + +g.test('multiple_color_attachments,same_slice_with_diff_mip_levels') + .desc( + ` + Render to the same slice of a 3d texture at different mip levels in multiple color attachments. + - For texture size with 1x1xN, the same depth slice of different mip levels can be rendered. + ` + ) + .params(u => u.combine('depthSlice', [0, 1])) + .fn(t => { + const { depthSlice } = t.params; + + const kBaseSize = 1; + + const format = 'rgba8unorm' as GPUTextureFormat; + const formatByteCost = 8; + const maxAttachmentCountPerSample = Math.trunc( + t.device.limits.maxColorAttachmentBytesPerSample / formatByteCost + ); + const attachmentCount = Math.min( + maxAttachmentCountPerSample, + t.device.limits.maxColorAttachments + ); + + const descriptor = { + format, + width: kBaseSize, + height: kBaseSize, + depthOrArrayLayers: (depthSlice + 1) << attachmentCount, + mipLevelCount: attachmentCount, + usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC, + }; + const texture = t.create3DTexture(descriptor); + + const colorAttachments: GPURenderPassColorAttachment[] = []; + for (let i = 0; i < attachmentCount; i++) { + const colorAttachment = t.getColorAttachment(texture, { baseMipLevel: i, mipLevelCount: 1 }); + colorAttachment.depthSlice = depthSlice; + colorAttachments.push(colorAttachment); + } + + const pipeline = t.createRenderPipeline({ attachmentCount }); + + const encoder = t.createEncoder('non-pass'); + + const pass = encoder.encoder.beginRenderPass({ colorAttachments }); + pass.setPipeline(pipeline); + pass.draw(3); + pass.end(); - }); \ No newline at end of file + const { bufferSize, bufferOffset } = t.getBufferSizeAndOffset( + kBaseSize, + kBaseSize, + attachmentCount + ); + const buffer = t.device.createBuffer({ + size: bufferSize, + usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST, + }); + for (let i = 0; i < attachmentCount; i++) { + encoder.encoder.copyTextureToBuffer( + { texture, mipLevel: i, origin: { x: 0, y: 0, z: depthSlice } }, + { buffer, bytesPerRow: 256, offset: bufferOffset * i }, + { width: kBaseSize, height: kBaseSize, depthOrArrayLayers: 1 } + ); + } + + t.device.queue.submit([encoder.finish()]); + + t.checkAttachmentResult(kBaseSize, kBaseSize, attachmentCount, buffer); + }); diff --git a/src/webgpu/listing_meta.json b/src/webgpu/listing_meta.json index f0585f3365e3..c78a27a748f4 100644 --- a/src/webgpu/listing_meta.json +++ b/src/webgpu/listing_meta.json @@ -140,6 +140,9 @@ "webgpu:api,operation,render_pipeline,sample_mask:alpha_to_coverage_mask:*": { "subcaseMS": 68.512 }, "webgpu:api,operation,render_pipeline,sample_mask:fragment_output_mask:*": { "subcaseMS": 6.154 }, "webgpu:api,operation,render_pipeline,vertex_only_render_pipeline:draw_depth_and_stencil_with_vertex_only_pipeline:*": { "subcaseMS": 14.100 }, + "webgpu:api,operation,rendering,3d_texture_slices:one_color_attachment,mip_levels:*": { "subcaseMS": 54.100 }, + "webgpu:api,operation,rendering,3d_texture_slices:multiple_color_attachments,same_mip_level:*": { "subcaseMS": 69.400 }, + "webgpu:api,operation,rendering,3d_texture_slices:one_color_attachment,mip_levels:*": { "subcaseMS": 9.800 }, "webgpu:api,operation,rendering,basic:clear:*": { "subcaseMS": 3.700 }, "webgpu:api,operation,rendering,basic:fullscreen_quad:*": { "subcaseMS": 16.601 }, "webgpu:api,operation,rendering,basic:large_draw:*": { "subcaseMS": 2335.425 },