summaryrefslogtreecommitdiffstats
path: root/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/memory_sync/buffer/single_buffer.spec.ts
blob: e05a441dc3f67a82d052e152499c8dc9d0b3889f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
export const description = `
Memory Synchronization Tests for Buffer: read before write, read after write, and write after write.

- Create a src buffer and initialize it to 0, wait on the fence to ensure the data is initialized.
Write Op: write a value (say 1) into the src buffer via render pass, copmute pass, copy, write buffer, etc.
Read Op: read the value from the src buffer and write it to dst buffer via render pass (vertex, index, indirect input, uniform, storage), compute pass, copy etc.
Wait on another fence, then call expectContents to verify the dst buffer value.
  - x= write op: {storage buffer in {compute, render, render-via-bundle}, t2b copy dst, b2b copy dst, writeBuffer}
  - x= read op: {index buffer, vertex buffer, indirect buffer (draw, draw indexed, dispatch), uniform buffer, {readonly, readwrite} storage buffer in {compute, render, render-via-bundle}, b2b copy src, b2t copy src}
  - x= read-write sequence: {read then write, write then read, write then write}
  - x= op context: {queue, command-encoder, compute-pass-encoder, render-pass-encoder, render-bundle-encoder}, x= op boundary: {queue-op, command-buffer, pass, execute-bundles, render-bundle}
    - Not every context/boundary combinations are valid. We have the checkOpsValidForContext func to do the filtering.
  - If two writes are in the same passes, render result has loose guarantees.
`;

import { makeTestGroup } from '../../../../../common/framework/test_group.js';
import {
  kOperationBoundaries,
  kBoundaryInfo,
  OperationContextHelper,
} from '../operation_context_helper.js';

import {
  kAllReadOps,
  kAllWriteOps,
  BufferSyncTest,
  checkOpsValidForContext,
} from './buffer_sync_test.js';

// The src value is what stores in the src buffer before any operation.
const kSrcValue = 0;
// The op value is what the read/write operation write into the target buffer.
const kOpValue = 1;

export const g = makeTestGroup(BufferSyncTest);

g.test('rw')
  .desc(
    `
    Perform a 'read' operations on a buffer, 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 readOp of kAllReadOps) {
          for (const writeOp of kAllWriteOps) {
            if (checkOpsValidForContext([readOp, writeOp], _context)) {
              yield {
                readOp,
                readContext: _context[0],
                writeOp,
                writeContext: _context[1],
              };
            }
          }
        }
      })
  )
  .fn(async t => {
    const { readContext, readOp, writeContext, writeOp, boundary } = t.params;
    const helper = new OperationContextHelper(t);

    const { srcBuffer, dstBuffer } = await t.createBuffersForReadOp(readOp, kSrcValue, kOpValue);
    await t.createIntermediateBuffersAndTexturesForWriteOp(writeOp, 0, kOpValue);

    // The read op will read from src buffer and write to dst buffer based on what it reads.
    // The write op will write the given op value into src buffer as well.
    // The write op happens after read op. So we are expecting the src value to be in the dst buffer.
    t.encodeReadOp(helper, readOp, readContext, srcBuffer, dstBuffer);
    helper.ensureBoundary(boundary);
    t.encodeWriteOp(helper, writeOp, writeContext, srcBuffer, 0, kOpValue);
    helper.ensureSubmit();
    // Only verify the value of the first element of the dstBuffer
    t.verifyData(dstBuffer, kSrcValue);
  });

g.test('wr')
  .desc(
    `
    Perform a 'write' operation on a buffer, 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.`
  )
  .params(u =>
    u //
      .combine('boundary', kOperationBoundaries)
      .expand('_context', p => kBoundaryInfo[p.boundary].contexts)
      .expandWithParams(function* ({ _context }) {
        for (const readOp of kAllReadOps) {
          for (const writeOp of kAllWriteOps) {
            if (checkOpsValidForContext([readOp, writeOp], _context)) {
              yield {
                readOp,
                readContext: _context[0],
                writeOp,
                writeContext: _context[1],
              };
            }
          }
        }
      })
  )
  .fn(async t => {
    const { readContext, readOp, writeContext, writeOp, boundary } = t.params;
    const helper = new OperationContextHelper(t);

    const { srcBuffer, dstBuffer } = await t.createBuffersForReadOp(readOp, kSrcValue, kOpValue);
    await t.createIntermediateBuffersAndTexturesForWriteOp(writeOp, 0, kOpValue);

    // The write op will write the given op value into src buffer.
    // The read op will read from src buffer and write to dst buffer based on what it reads.
    // The write op happens before read op. So we are expecting the op value to be in the dst buffer.
    t.encodeWriteOp(helper, writeOp, writeContext, srcBuffer, 0, kOpValue);
    helper.ensureBoundary(boundary);
    t.encodeReadOp(helper, readOp, readContext, srcBuffer, dstBuffer);
    helper.ensureSubmit();
    // Only verify the value of the first element of the dstBuffer
    t.verifyData(dstBuffer, kOpValue);
  });

g.test('ww')
  .desc(
    `
    Perform a 'first' write operation on a buffer, 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 firstWriteOp of kAllWriteOps) {
          for (const secondWriteOp of kAllWriteOps) {
            if (checkOpsValidForContext([firstWriteOp, secondWriteOp], _context)) {
              yield {
                writeOps: [firstWriteOp, secondWriteOp],
                contexts: _context,
              };
            }
          }
        }
      })
  )
  .fn(async t => {
    const { writeOps, contexts, boundary } = t.params;
    const helper = new OperationContextHelper(t);

    const buffer = await t.createBufferWithValue(0);
    await t.createIntermediateBuffersAndTexturesForWriteOp(writeOps[0], 0, 1);
    await t.createIntermediateBuffersAndTexturesForWriteOp(writeOps[1], 1, 2);

    t.encodeWriteOp(helper, writeOps[0], contexts[0], buffer, 0, 1);
    helper.ensureBoundary(boundary);
    t.encodeWriteOp(helper, writeOps[1], contexts[1], buffer, 1, 2);
    helper.ensureSubmit();
    t.verifyData(buffer, 2);
  });

// Cases with loose render result guarentees.

g.test('two_draws_in_the_same_render_pass')
  .desc(
    `Test write-after-write operations in the same render pass. The first write will write 1 into
    a storage buffer. The second write will write 2 into the same buffer in the same pass. Expected
    data in buffer is either 1 or 2. It may use bundle in each draw.`
  )
  .paramsSubcasesOnly(u =>
    u //
      .combine('firstDrawUseBundle', [false, true])
      .combine('secondDrawUseBundle', [false, true])
  )
  .fn(async t => {
    const { firstDrawUseBundle, secondDrawUseBundle } = t.params;
    const buffer = await t.createBufferWithValue(0);
    const encoder = t.device.createCommandEncoder();
    const passEncoder = t.beginSimpleRenderPass(encoder);

    const useBundle = [firstDrawUseBundle, secondDrawUseBundle];
    for (let i = 0; i < 2; ++i) {
      const renderEncoder = useBundle[i]
        ? t.device.createRenderBundleEncoder({
            colorFormats: ['rgba8unorm'],
          })
        : passEncoder;
      const pipeline = t.createStorageWriteRenderPipeline(i + 1);
      const bindGroup = t.createBindGroup(pipeline, buffer);
      renderEncoder.setPipeline(pipeline);
      renderEncoder.setBindGroup(0, bindGroup);
      renderEncoder.draw(1, 1, 0, 0);
      if (useBundle[i])
        passEncoder.executeBundles([(renderEncoder as GPURenderBundleEncoder).finish()]);
    }

    passEncoder.end();
    t.device.queue.submit([encoder.finish()]);
    t.verifyDataTwoValidValues(buffer, 1, 2);
  });

g.test('two_draws_in_the_same_render_bundle')
  .desc(
    `Test write-after-write operations in the same render bundle. The first write will write 1 into
    a storage buffer. The second write will write 2 into the same buffer in the same pass. Expected
    data in buffer is either 1 or 2.`
  )
  .fn(async t => {
    const buffer = await t.createBufferWithValue(0);
    const encoder = t.device.createCommandEncoder();
    const passEncoder = t.beginSimpleRenderPass(encoder);
    const renderEncoder = t.device.createRenderBundleEncoder({
      colorFormats: ['rgba8unorm'],
    });

    for (let i = 0; i < 2; ++i) {
      const pipeline = t.createStorageWriteRenderPipeline(i + 1);
      const bindGroup = t.createBindGroup(pipeline, buffer);
      renderEncoder.setPipeline(pipeline);
      renderEncoder.setBindGroup(0, bindGroup);
      renderEncoder.draw(1, 1, 0, 0);
    }

    passEncoder.executeBundles([renderEncoder.finish()]);
    passEncoder.end();
    t.device.queue.submit([encoder.finish()]);
    t.verifyDataTwoValidValues(buffer, 1, 2);
  });

g.test('two_dispatches_in_the_same_compute_pass')
  .desc(
    `Test write-after-write operations in the same compute pass. The first write will write 1 into
    a storage buffer. The second write will write 2 into the same buffer in the same pass. Expected
    data in buffer is 2.`
  )
  .fn(async t => {
    const buffer = await t.createBufferWithValue(0);
    const encoder = t.device.createCommandEncoder();
    const pass = encoder.beginComputePass();

    for (let i = 0; i < 2; ++i) {
      const pipeline = t.createStorageWriteComputePipeline(i + 1);
      const bindGroup = t.createBindGroup(pipeline, buffer);
      pass.setPipeline(pipeline);
      pass.setBindGroup(0, bindGroup);
      pass.dispatchWorkgroups(1);
    }

    pass.end();
    t.device.queue.submit([encoder.finish()]);
    t.verifyData(buffer, 2);
  });