summaryrefslogtreecommitdiffstats
path: root/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/memory_sync/texture/same_subresource.spec.ts
diff options
context:
space:
mode:
Diffstat (limited to 'dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/memory_sync/texture/same_subresource.spec.ts')
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/memory_sync/texture/same_subresource.spec.ts709
1 files changed, 709 insertions, 0 deletions
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/memory_sync/texture/same_subresource.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/memory_sync/texture/same_subresource.spec.ts
new file mode 100644
index 0000000000..38b5bf3bcc
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/memory_sync/texture/same_subresource.spec.ts
@@ -0,0 +1,709 @@
+export const description = `
+Memory Synchronization Tests for Texture: read before write, read after write, and write after write to the same subresource.
+
+- TODO: Test synchronization between multiple queues.
+- TODO: Test depth/stencil attachments.
+- TODO: Use non-solid-color texture contents [2]
+`;
+
+import { makeTestGroup } from '../../../../../common/framework/test_group.js';
+import { assert, memcpy, unreachable } from '../../../../../common/util/util.js';
+import { EncodableTextureFormat } from '../../../../capability_info.js';
+import { GPUTest } from '../../../../gpu_test.js';
+import { align } from '../../../../util/math.js';
+import { getTextureCopyLayout } from '../../../../util/texture/layout.js';
+import {
+ kTexelRepresentationInfo,
+ PerTexelComponent,
+} from '../../../../util/texture/texel_data.js';
+import {
+ kOperationBoundaries,
+ OperationContext,
+ kBoundaryInfo,
+ OperationContextHelper,
+} from '../operation_context_helper.js';
+
+import {
+ kAllReadOps,
+ kAllWriteOps,
+ checkOpsValidForContext,
+ Op,
+ kOpInfo,
+} from './texture_sync_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+const fullscreenQuadWGSL = `
+ struct VertexOutput {
+ @builtin(position) Position : vec4<f32>
+ };
+
+ @vertex fn vert_main(@builtin(vertex_index) VertexIndex : u32) -> VertexOutput {
+ var pos = 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));
+
+ var output : VertexOutput;
+ output.Position = vec4<f32>(pos[VertexIndex], 0.0, 1.0);
+ return output;
+ }
+`;
+
+class TextureSyncTestHelper extends OperationContextHelper {
+ private texture: GPUTexture;
+
+ public readonly kTextureSize = [4, 4] as const;
+ public readonly kTextureFormat: EncodableTextureFormat = 'rgba8unorm';
+
+ constructor(
+ t: GPUTest,
+ textureCreationParams: {
+ usage: GPUTextureUsageFlags;
+ }
+ ) {
+ super(t);
+ this.texture = t.trackForCleanup(
+ t.device.createTexture({
+ size: this.kTextureSize,
+ format: this.kTextureFormat,
+ ...textureCreationParams,
+ })
+ );
+ }
+
+ /**
+ * Perform a read operation on the test texture.
+ * @return GPUTexture copy containing the contents.
+ */
+ performReadOp({ op, in: context }: { op: Op; in: OperationContext }): GPUTexture {
+ this.ensureContext(context);
+ switch (op) {
+ case 't2t-copy': {
+ const texture = this.t.trackForCleanup(
+ this.device.createTexture({
+ size: this.kTextureSize,
+ format: this.kTextureFormat,
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST,
+ })
+ );
+
+ assert(this.commandEncoder !== undefined);
+ this.commandEncoder.copyTextureToTexture(
+ {
+ texture: this.texture,
+ },
+ { texture },
+ this.kTextureSize
+ );
+ return texture;
+ }
+ case 't2b-copy': {
+ const { byteLength, bytesPerRow } = getTextureCopyLayout(this.kTextureFormat, '2d', [
+ ...this.kTextureSize,
+ 1,
+ ]);
+ const buffer = this.t.trackForCleanup(
+ this.device.createBuffer({
+ size: byteLength,
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
+ })
+ );
+
+ const texture = this.t.trackForCleanup(
+ this.device.createTexture({
+ size: this.kTextureSize,
+ format: this.kTextureFormat,
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST,
+ })
+ );
+
+ assert(this.commandEncoder !== undefined);
+ this.commandEncoder.copyTextureToBuffer(
+ {
+ texture: this.texture,
+ },
+ { buffer, bytesPerRow },
+ this.kTextureSize
+ );
+ this.commandEncoder.copyBufferToTexture(
+ { buffer, bytesPerRow },
+ { texture },
+ this.kTextureSize
+ );
+ return texture;
+ }
+ case 'sample': {
+ const texture = this.t.trackForCleanup(
+ this.device.createTexture({
+ size: this.kTextureSize,
+ format: this.kTextureFormat,
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.STORAGE_BINDING,
+ })
+ );
+
+ const bindGroupLayout = this.device.createBindGroupLayout({
+ entries: [
+ {
+ binding: 0,
+ visibility: GPUShaderStage.FRAGMENT | GPUShaderStage.COMPUTE,
+ texture: {
+ sampleType: 'unfilterable-float',
+ },
+ },
+ {
+ binding: 1,
+ visibility: GPUShaderStage.FRAGMENT | GPUShaderStage.COMPUTE,
+ storageTexture: {
+ access: 'write-only',
+ format: this.kTextureFormat,
+ },
+ },
+ ],
+ });
+
+ const bindGroup = this.device.createBindGroup({
+ layout: bindGroupLayout,
+ entries: [
+ {
+ binding: 0,
+ resource: this.texture.createView(),
+ },
+ {
+ binding: 1,
+ resource: texture.createView(),
+ },
+ ],
+ });
+
+ switch (context) {
+ case 'render-pass-encoder':
+ case 'render-bundle-encoder': {
+ const module = this.device.createShaderModule({
+ code: `${fullscreenQuadWGSL}
+
+ @group(0) @binding(0) var inputTex: texture_2d<f32>;
+ @group(0) @binding(1) var outputTex: texture_storage_2d<rgba8unorm, write>;
+
+ @fragment fn frag_main(@builtin(position) fragCoord: vec4<f32>) -> @location(0) vec4<f32> {
+ let coord = vec2<i32>(fragCoord.xy);
+ textureStore(outputTex, coord, textureLoad(inputTex, coord, 0));
+ return vec4<f32>();
+ }
+ `,
+ });
+ const renderPipeline = this.device.createRenderPipeline({
+ layout: this.device.createPipelineLayout({
+ bindGroupLayouts: [bindGroupLayout],
+ }),
+ vertex: {
+ module,
+ entryPoint: 'vert_main',
+ },
+ fragment: {
+ module,
+ entryPoint: 'frag_main',
+
+ // Unused attachment since we can't use textureStore in the vertex shader.
+ // Set writeMask to zero.
+ targets: [
+ {
+ format: this.kTextureFormat,
+ writeMask: 0,
+ },
+ ],
+ },
+ });
+
+ switch (context) {
+ case 'render-bundle-encoder':
+ assert(this.renderBundleEncoder !== undefined);
+ this.renderBundleEncoder.setPipeline(renderPipeline);
+ this.renderBundleEncoder.setBindGroup(0, bindGroup);
+ this.renderBundleEncoder.draw(6);
+ break;
+ case 'render-pass-encoder':
+ assert(this.renderPassEncoder !== undefined);
+ this.renderPassEncoder.setPipeline(renderPipeline);
+ this.renderPassEncoder.setBindGroup(0, bindGroup);
+ this.renderPassEncoder.draw(6);
+ break;
+ }
+ break;
+ }
+ case 'compute-pass-encoder': {
+ const module = this.device.createShaderModule({
+ code: `
+ @group(0) @binding(0) var inputTex: texture_2d<f32>;
+ @group(0) @binding(1) var outputTex: texture_storage_2d<rgba8unorm, write>;
+
+ @compute @workgroup_size(8, 8)
+ fn main(@builtin(global_invocation_id) gid : vec3<u32>) {
+ if (any(gid.xy >= vec2<u32>(textureDimensions(inputTex)))) {
+ return;
+ }
+ let coord = vec2<i32>(gid.xy);
+ textureStore(outputTex, coord, textureLoad(inputTex, coord, 0));
+ }
+ `,
+ });
+ const computePipeline = this.device.createComputePipeline({
+ layout: this.device.createPipelineLayout({
+ bindGroupLayouts: [bindGroupLayout],
+ }),
+ compute: {
+ module,
+ entryPoint: 'main',
+ },
+ });
+
+ assert(this.computePassEncoder !== undefined);
+ this.computePassEncoder.setPipeline(computePipeline);
+ this.computePassEncoder.setBindGroup(0, bindGroup);
+ this.computePassEncoder.dispatchWorkgroups(
+ Math.ceil(this.kTextureSize[0] / 8),
+ Math.ceil(this.kTextureSize[1] / 8)
+ );
+ break;
+ }
+ default:
+ unreachable();
+ }
+
+ return texture;
+ }
+ case 'b2t-copy':
+ case 'attachment-resolve':
+ case 'attachment-store':
+ unreachable();
+ }
+ unreachable();
+ }
+
+ performWriteOp(
+ { op, in: context }: { op: Op; in: OperationContext },
+ data: PerTexelComponent<number>
+ ) {
+ this.ensureContext(context);
+ switch (op) {
+ case 'attachment-store': {
+ assert(this.commandEncoder !== undefined);
+ this.renderPassEncoder = this.commandEncoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: this.texture.createView(),
+ // [2] Use non-solid-color texture values
+ clearValue: [data.R ?? 0, data.G ?? 0, data.B ?? 0, data.A ?? 0],
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ });
+ this.currentContext = 'render-pass-encoder';
+ break;
+ }
+ case 'write-texture': {
+ // [2] Use non-solid-color texture values
+ const rep = kTexelRepresentationInfo[this.kTextureFormat];
+ const texelData = rep.pack(rep.encode(data));
+ const numTexels = this.kTextureSize[0] * this.kTextureSize[1];
+ const fullTexelData = new ArrayBuffer(texelData.byteLength * numTexels);
+ for (let i = 0; i < numTexels; ++i) {
+ memcpy({ src: texelData }, { dst: fullTexelData, start: i * texelData.byteLength });
+ }
+
+ this.queue.writeTexture(
+ { texture: this.texture },
+ fullTexelData,
+ {
+ bytesPerRow: texelData.byteLength * this.kTextureSize[0],
+ },
+ this.kTextureSize
+ );
+ break;
+ }
+ case 't2t-copy': {
+ const texture = this.device.createTexture({
+ size: this.kTextureSize,
+ format: this.kTextureFormat,
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST,
+ });
+
+ // [2] Use non-solid-color texture values
+ const rep = kTexelRepresentationInfo[this.kTextureFormat];
+ const texelData = rep.pack(rep.encode(data));
+ const numTexels = this.kTextureSize[0] * this.kTextureSize[1];
+ const fullTexelData = new ArrayBuffer(texelData.byteLength * numTexels);
+ for (let i = 0; i < numTexels; ++i) {
+ memcpy({ src: texelData }, { dst: fullTexelData, start: i * texelData.byteLength });
+ }
+
+ this.queue.writeTexture(
+ { texture },
+ fullTexelData,
+ {
+ bytesPerRow: texelData.byteLength * this.kTextureSize[0],
+ },
+ this.kTextureSize
+ );
+
+ assert(this.commandEncoder !== undefined);
+ this.commandEncoder.copyTextureToTexture(
+ { texture },
+ { texture: this.texture },
+ this.kTextureSize
+ );
+ break;
+ }
+ case 'b2t-copy': {
+ // [2] Use non-solid-color texture values
+ const rep = kTexelRepresentationInfo[this.kTextureFormat];
+ const texelData = rep.pack(rep.encode(data));
+ const bytesPerRow = align(texelData.byteLength, 256);
+ const fullTexelData = new ArrayBuffer(bytesPerRow * this.kTextureSize[1]);
+ for (let i = 0; i < this.kTextureSize[1]; ++i) {
+ for (let j = 0; j < this.kTextureSize[0]; ++j) {
+ memcpy(
+ { src: texelData },
+ {
+ dst: fullTexelData,
+ start: i * bytesPerRow + j * texelData.byteLength,
+ }
+ );
+ }
+ }
+
+ const buffer = this.t.trackForCleanup(
+ this.device.createBuffer({
+ size: fullTexelData.byteLength,
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
+ })
+ );
+
+ this.queue.writeBuffer(buffer, 0, fullTexelData);
+
+ assert(this.commandEncoder !== undefined);
+ this.commandEncoder.copyBufferToTexture(
+ { buffer, bytesPerRow },
+ { texture: this.texture },
+ this.kTextureSize
+ );
+ break;
+ }
+ case 'attachment-resolve': {
+ assert(this.commandEncoder !== undefined);
+ const renderTarget = this.t.trackForCleanup(
+ this.device.createTexture({
+ format: this.kTextureFormat,
+ size: this.kTextureSize,
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
+ sampleCount: 4,
+ })
+ );
+ this.renderPassEncoder = this.commandEncoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: renderTarget.createView(),
+ resolveTarget: this.texture.createView(),
+ // [2] Use non-solid-color texture values
+ clearValue: [data.R ?? 0, data.G ?? 0, data.B ?? 0, data.A ?? 0],
+ loadOp: 'clear',
+ storeOp: 'discard',
+ },
+ ],
+ });
+ this.currentContext = 'render-pass-encoder';
+ break;
+ }
+ case 'storage': {
+ const bindGroupLayout = this.device.createBindGroupLayout({
+ entries: [
+ {
+ binding: 0,
+ visibility: GPUShaderStage.FRAGMENT | GPUShaderStage.COMPUTE,
+ storageTexture: {
+ access: 'write-only',
+ format: this.kTextureFormat,
+ },
+ },
+ ],
+ });
+
+ const bindGroup = this.device.createBindGroup({
+ layout: bindGroupLayout,
+ entries: [
+ {
+ binding: 0,
+ resource: this.texture.createView(),
+ },
+ ],
+ });
+
+ // [2] Use non-solid-color texture values
+ const storedValue = `vec4<f32>(${[data.R ?? 0, data.G ?? 0, data.B ?? 0, data.A ?? 0]
+ .map(x => x.toFixed(5))
+ .join(', ')})`;
+
+ switch (context) {
+ case 'render-pass-encoder':
+ case 'render-bundle-encoder': {
+ const module = this.device.createShaderModule({
+ code: `${fullscreenQuadWGSL}
+
+ @group(0) @binding(0) var outputTex: texture_storage_2d<rgba8unorm, write>;
+
+ @fragment fn frag_main(@builtin(position) fragCoord: vec4<f32>) -> @location(0) vec4<f32> {
+ textureStore(outputTex, vec2<i32>(fragCoord.xy), ${storedValue});
+ return vec4<f32>();
+ }
+ `,
+ });
+ const renderPipeline = this.device.createRenderPipeline({
+ layout: this.device.createPipelineLayout({
+ bindGroupLayouts: [bindGroupLayout],
+ }),
+ vertex: {
+ module,
+ entryPoint: 'vert_main',
+ },
+ fragment: {
+ module,
+ entryPoint: 'frag_main',
+
+ // Unused attachment since we can't use textureStore in the vertex shader.
+ // Set writeMask to zero.
+ targets: [
+ {
+ format: this.kTextureFormat,
+ writeMask: 0,
+ },
+ ],
+ },
+ });
+
+ switch (context) {
+ case 'render-bundle-encoder':
+ assert(this.renderBundleEncoder !== undefined);
+ this.renderBundleEncoder.setPipeline(renderPipeline);
+ this.renderBundleEncoder.setBindGroup(0, bindGroup);
+ this.renderBundleEncoder.draw(6);
+ break;
+ case 'render-pass-encoder':
+ assert(this.renderPassEncoder !== undefined);
+ this.renderPassEncoder.setPipeline(renderPipeline);
+ this.renderPassEncoder.setBindGroup(0, bindGroup);
+ this.renderPassEncoder.draw(6);
+ break;
+ }
+ break;
+ }
+ case 'compute-pass-encoder': {
+ const module = this.device.createShaderModule({
+ code: `
+ @group(0) @binding(0) var outputTex: texture_storage_2d<rgba8unorm, write>;
+
+ @compute @workgroup_size(8, 8)
+ fn main(@builtin(global_invocation_id) gid : vec3<u32>) {
+ if (any(gid.xy >= vec2<u32>(textureDimensions(outputTex)))) {
+ return;
+ }
+ let coord = vec2<i32>(gid.xy);
+ textureStore(outputTex, coord, ${storedValue});
+ }
+ `,
+ });
+ const computePipeline = this.device.createComputePipeline({
+ layout: this.device.createPipelineLayout({
+ bindGroupLayouts: [bindGroupLayout],
+ }),
+ compute: {
+ module,
+ entryPoint: 'main',
+ },
+ });
+
+ assert(this.computePassEncoder !== undefined);
+ this.computePassEncoder.setPipeline(computePipeline);
+ this.computePassEncoder.setBindGroup(0, bindGroup);
+ this.computePassEncoder.dispatchWorkgroups(
+ Math.ceil(this.kTextureSize[0] / 8),
+ Math.ceil(this.kTextureSize[1] / 8)
+ );
+ break;
+ }
+ default:
+ unreachable();
+ }
+ break;
+ }
+ case 't2b-copy':
+ case 'sample':
+ unreachable();
+ }
+ }
+}
+
+g.test('rw')
+ .desc(
+ `
+ Perform a 'read' operations on a texture subresource, followed by a 'write' operation.
+ Operations are separated by a 'boundary' (pass, encoder, queue-op, etc.).
+ Test that the results are synchronized.
+ The read should not see the contents written by the subsequent write.`
+ )
+ .params(u =>
+ u
+ .combine('boundary', kOperationBoundaries)
+ .expand('_context', p => kBoundaryInfo[p.boundary].contexts)
+ .expandWithParams(function* ({ _context }) {
+ for (const read of kAllReadOps) {
+ for (const write of kAllWriteOps) {
+ if (checkOpsValidForContext([read, write], _context)) {
+ yield {
+ read: { op: read, in: _context[0] },
+ write: { op: write, in: _context[1] },
+ };
+ }
+ }
+ }
+ })
+ )
+ .fn(t => {
+ const helper = new TextureSyncTestHelper(t, {
+ usage:
+ GPUTextureUsage.COPY_DST |
+ kOpInfo[t.params.read.op].readUsage |
+ kOpInfo[t.params.write.op].writeUsage,
+ });
+ // [2] Use non-solid-color texture value.
+ const texelValue1 = { R: 0, G: 1, B: 0, A: 1 } as const;
+ const texelValue2 = { R: 1, G: 0, B: 0, A: 1 } as const;
+
+ // Initialize the texture with something.
+ helper.performWriteOp({ op: 'write-texture', in: 'queue' }, texelValue1);
+ const readbackTexture = helper.performReadOp(t.params.read);
+ helper.ensureBoundary(t.params.boundary);
+ helper.performWriteOp(t.params.write, texelValue2);
+ helper.ensureSubmit();
+
+ // Contents should be the first value written, not the second.
+ t.expectSingleColor(readbackTexture, helper.kTextureFormat, {
+ size: [...helper.kTextureSize, 1],
+ exp: texelValue1,
+ });
+ });
+
+g.test('wr')
+ .desc(
+ `
+ Perform a 'write' operation on a texture subresource, followed by a 'read' operation.
+ Operations are separated by a 'boundary' (pass, encoder, queue-op, etc.).
+ Test that the results are synchronized.
+ The read should see exactly the contents written by the previous write.
+
+ - TODO: Use non-solid-color texture contents [2]`
+ )
+ .params(u =>
+ u
+ .combine('boundary', kOperationBoundaries)
+ .expand('_context', p => kBoundaryInfo[p.boundary].contexts)
+ .expandWithParams(function* ({ _context }) {
+ for (const read of kAllReadOps) {
+ for (const write of kAllWriteOps) {
+ if (checkOpsValidForContext([write, read], _context)) {
+ yield {
+ write: { op: write, in: _context[0] },
+ read: { op: read, in: _context[1] },
+ };
+ }
+ }
+ }
+ })
+ )
+ .fn(t => {
+ const helper = new TextureSyncTestHelper(t, {
+ usage: kOpInfo[t.params.read.op].readUsage | kOpInfo[t.params.write.op].writeUsage,
+ });
+ // [2] Use non-solid-color texture value.
+ const texelValue = { R: 0, G: 1, B: 0, A: 1 } as const;
+
+ helper.performWriteOp(t.params.write, texelValue);
+ helper.ensureBoundary(t.params.boundary);
+ const readbackTexture = helper.performReadOp(t.params.read);
+ helper.ensureSubmit();
+
+ // Contents should be exactly the values written.
+ t.expectSingleColor(readbackTexture, helper.kTextureFormat, {
+ size: [...helper.kTextureSize, 1],
+ exp: texelValue,
+ });
+ });
+
+g.test('ww')
+ .desc(
+ `
+ Perform a 'first' write operation on a texture subresource, followed by a 'second' write operation.
+ Operations are separated by a 'boundary' (pass, encoder, queue-op, etc.).
+ Test that the results are synchronized.
+ The second write should overwrite the contents of the first.`
+ )
+ .params(u =>
+ u
+ .combine('boundary', kOperationBoundaries)
+ .expand('_context', p => kBoundaryInfo[p.boundary].contexts)
+ .expandWithParams(function* ({ _context }) {
+ for (const first of kAllWriteOps) {
+ for (const second of kAllWriteOps) {
+ if (checkOpsValidForContext([first, second], _context)) {
+ yield {
+ first: { op: first, in: _context[0] },
+ second: { op: second, in: _context[1] },
+ };
+ }
+ }
+ }
+ })
+ )
+ .fn(t => {
+ const helper = new TextureSyncTestHelper(t, {
+ usage:
+ GPUTextureUsage.COPY_SRC |
+ kOpInfo[t.params.first.op].writeUsage |
+ kOpInfo[t.params.second.op].writeUsage,
+ });
+ // [2] Use non-solid-color texture value.
+ const texelValue1 = { R: 1, G: 0, B: 0, A: 1 } as const;
+ const texelValue2 = { R: 0, G: 1, B: 0, A: 1 } as const;
+
+ helper.performWriteOp(t.params.first, texelValue1);
+ helper.ensureBoundary(t.params.boundary);
+ helper.performWriteOp(t.params.second, texelValue2);
+ helper.ensureSubmit();
+
+ // Read back the contents so we can test the result.
+ const readbackTexture = helper.performReadOp({ op: 't2t-copy', in: 'command-encoder' });
+ helper.ensureSubmit();
+
+ // Contents should be the second value written.
+ t.expectSingleColor(readbackTexture, helper.kTextureFormat, {
+ size: [...helper.kTextureSize, 1],
+ exp: texelValue2,
+ });
+ });
+
+g.test('rw,single_pass,load_store')
+ .desc(
+ `
+ TODO: Test memory synchronization when loading from a texture subresource in a single pass and storing to it.`
+ )
+ .unimplemented();
+
+g.test('rw,single_pass,load_resolve')
+ .desc(
+ `
+ TODO: Test memory synchronization when loading from a texture subresource in a single pass and resolving to it.`
+ )
+ .unimplemented();