summaryrefslogtreecommitdiffstats
path: root/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/memory_sync/buffer/multiple_buffers.spec.ts
blob: b3ed8424365d000bc5cf79357862429e4aa405a9 (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
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
export const description = `
Memory Synchronization Tests for multiple buffers: read before write, read after write, and write after write.

- Create multiple src buffers 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.

TODO: Tests with more than one buffer to try to stress implementations a little bit more.
`;

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 multiple buffers, 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 srcBuffers: GPUBuffer[] = [];
    const dstBuffers: GPUBuffer[] = [];

    const kBufferCount = 4;
    for (let i = 0; i < kBufferCount; i++) {
      const { srcBuffer, dstBuffer } = await t.createBuffersForReadOp(readOp, kSrcValue, kOpValue);
      srcBuffers.push(srcBuffer);
      dstBuffers.push(dstBuffer);
    }

    await t.createIntermediateBuffersAndTexturesForWriteOp(writeOp, 0, kOpValue);

    // The read op will read from src buffers and write to dst buffers based on what it reads.
    // A boundary will separate multiple read and write operations. The write op will write the
    // given op value into each src buffer as well. The write op happens after read op. So we are
    // expecting each src value to be in the mapped dst buffer.
    for (let i = 0; i < kBufferCount; i++) {
      t.encodeReadOp(helper, readOp, readContext, srcBuffers[i], dstBuffers[i]);
    }

    helper.ensureBoundary(boundary);

    for (let i = 0; i < kBufferCount; i++) {
      t.encodeWriteOp(helper, writeOp, writeContext, srcBuffers[i], 0, kOpValue);
    }

    helper.ensureSubmit();

    for (let i = 0; i < kBufferCount; i++) {
      // Only verify the value of the first element of each dstBuffer.
      t.verifyData(dstBuffers[i], kSrcValue);
    }
  });

g.test('wr')
  .desc(
    `
    Perform a 'write' operation on on multiple buffers, 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 srcBuffers: GPUBuffer[] = [];
    const dstBuffers: GPUBuffer[] = [];

    const kBufferCount = 4;

    for (let i = 0; i < kBufferCount; i++) {
      const { srcBuffer, dstBuffer } = await t.createBuffersForReadOp(readOp, kSrcValue, kOpValue);

      srcBuffers.push(srcBuffer);
      dstBuffers.push(dstBuffer);
    }

    await t.createIntermediateBuffersAndTexturesForWriteOp(writeOp, 0, kOpValue);

    // The write op will write the given op value into src buffers.
    // The read op will read from src buffers and write to dst buffers based on what it reads.
    // The write op happens before read op. So we are expecting the op value to be in the dst
    // buffers.
    for (let i = 0; i < kBufferCount; i++) {
      t.encodeWriteOp(helper, writeOp, writeContext, srcBuffers[i], 0, kOpValue);
    }

    helper.ensureBoundary(boundary);

    for (let i = 0; i < kBufferCount; i++) {
      t.encodeReadOp(helper, readOp, readContext, srcBuffers[i], dstBuffers[i]);
    }

    helper.ensureSubmit();

    for (let i = 0; i < kBufferCount; i++) {
      // Only verify the value of the first element of the dstBuffer
      t.verifyData(dstBuffers[i], kOpValue);
    }
  });

g.test('ww')
  .desc(
    `
    Perform a 'first' write operation on multiple buffers, 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 buffers: GPUBuffer[] = [];

    const kBufferCount = 4;

    for (let i = 0; i < kBufferCount; i++) {
      const buffer = await t.createBufferWithValue(0);

      buffers.push(buffer);
    }

    await t.createIntermediateBuffersAndTexturesForWriteOp(writeOps[0], 0, 1);
    await t.createIntermediateBuffersAndTexturesForWriteOp(writeOps[1], 1, 2);

    for (let i = 0; i < kBufferCount; i++) {
      t.encodeWriteOp(helper, writeOps[0], contexts[0], buffers[i], 0, 1);
    }

    helper.ensureBoundary(boundary);

    for (let i = 0; i < kBufferCount; i++) {
      t.encodeWriteOp(helper, writeOps[1], contexts[1], buffers[i], 1, 2);
    }

    helper.ensureSubmit();

    for (let i = 0; i < kBufferCount; i++) {
      t.verifyData(buffers[i], 2);
    }
  });

g.test('multiple_pairs_of_draws_in_one_render_pass')
  .desc(
    `
  Test write-after-write operations on multiple buffers via the one render pass. The first write
  will write the buffer index * 2 + 1 into all storage buffers. The second write will write the
  buffer index * 2 + 2 into the all buffers in the same pass. Expected data in all buffers is either
  buffer index * 2 + 1 or buffer index * 2 + 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 encoder = t.device.createCommandEncoder();
    const passEncoder = t.beginSimpleRenderPass(encoder);

    const kBufferCount = 4;
    const buffers: GPUBuffer[] = [];
    for (let b = 0; b < kBufferCount; ++b) {
      const buffer = await t.createBufferWithValue(0);
      buffers.push(buffer);

      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(2 * b + 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()]);
    for (let b = 0; b < kBufferCount; ++b) {
      t.verifyDataTwoValidValues(buffers[b], 2 * b + 1, 2 * b + 2);
    }
  });

g.test('multiple_pairs_of_draws_in_one_render_bundle')
  .desc(
    `
  Test write-after-write operations on multiple buffers via the one render bundle. The first write
  will write the buffer index * 2 + 1 into all storage buffers. The second write will write the
  buffer index * 2 + 2 into the all buffers in the same pass. Expected data in all buffers is either
  buffer index * 2 + 1 or buffer index * 2 + 2.
  `
  )
  .fn(async t => {
    const encoder = t.device.createCommandEncoder();
    const passEncoder = t.beginSimpleRenderPass(encoder);
    const renderEncoder = t.device.createRenderBundleEncoder({
      colorFormats: ['rgba8unorm'],
    });

    const kBufferCount = 4;
    const buffers: GPUBuffer[] = [];
    for (let b = 0; b < kBufferCount; ++b) {
      const buffer = await t.createBufferWithValue(0);
      buffers.push(buffer);

      for (let i = 0; i < 2; ++i) {
        const pipeline = t.createStorageWriteRenderPipeline(2 * b + 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()]);
    for (let b = 0; b < kBufferCount; ++b) {
      t.verifyDataTwoValidValues(buffers[b], 2 * b + 1, 2 * b + 2);
    }
  });

g.test('multiple_pairs_of_dispatches_in_one_compute_pass')
  .desc(
    `
  Test write-after-write operations on multiple buffers via the one compute pass. The first write
  will write the buffer index * 2 + 1 into all storage buffers. The second write will write the
  buffer index * 2 + 2 into the all buffers in the same pass. Expected data in all buffers is the
  buffer index * 2 + 2.
  `
  )
  .fn(async t => {
    const encoder = t.device.createCommandEncoder();
    const pass = encoder.beginComputePass();

    const kBufferCount = 4;
    const buffers: GPUBuffer[] = [];
    for (let b = 0; b < kBufferCount; ++b) {
      const buffer = await t.createBufferWithValue(0);
      buffers.push(buffer);

      for (let i = 0; i < 2; ++i) {
        const pipeline = t.createStorageWriteComputePipeline(2 * b + 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()]);
    for (let b = 0; b < kBufferCount; ++b) {
      t.verifyData(buffers[b], 2 * b + 2);
    }
  });