summaryrefslogtreecommitdiffstats
path: root/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/canvas/getCurrentTexture.spec.ts
blob: 609dacb90761eb0ce6ecd434864e90bc4b69fedc (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
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
export const description = `
Tests for GPUCanvasContext.getCurrentTexture.
`;

import { SkipTestCase } from '../../../common/framework/fixture.js';
import { makeTestGroup } from '../../../common/framework/test_group.js';
import { timeout } from '../../../common/util/timeout.js';
import { assert, unreachable } from '../../../common/util/util.js';
import { GPUTest } from '../../gpu_test.js';
import { kAllCanvasTypes, createCanvas, CanvasType } from '../../util/create_elements.js';

const kFormat = 'bgra8unorm';

class GPUContextTest extends GPUTest {
  initCanvasContext(canvasType: CanvasType = 'onscreen'): GPUCanvasContext {
    const canvas = createCanvas(this, canvasType, 2, 2);
    if (canvasType === 'onscreen') {
      // To make sure onscreen canvas are visible
      const onscreencanvas = canvas as HTMLCanvasElement;
      onscreencanvas.style.position = 'fixed';
      onscreencanvas.style.top = '0';
      onscreencanvas.style.left = '0';
      // Set it to transparent so that if multiple canvas are created, they are still visible.
      onscreencanvas.style.opacity = '50%';
      document.body.appendChild(onscreencanvas);
      this.trackForCleanup({
        close() {
          document.body.removeChild(onscreencanvas);
        },
      });
    }
    const ctx = canvas.getContext('webgpu');
    assert(ctx instanceof GPUCanvasContext, 'Failed to get WebGPU context from canvas');

    ctx.configure({
      device: this.device,
      format: kFormat,
      usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
    });

    return ctx;
  }
}

export const g = makeTestGroup(GPUContextTest);

g.test('configured')
  .desc(
    `Checks that calling getCurrentTexture requires the context to be configured first, and
  that each call to configure causes getCurrentTexture to return a new texture.`
  )
  .params(u =>
    u //
      .combine('canvasType', kAllCanvasTypes)
  )
  .fn(t => {
    const canvas = createCanvas(t, t.params.canvasType, 2, 2);
    const ctx = canvas.getContext('webgpu');
    assert(ctx instanceof GPUCanvasContext, 'Failed to get WebGPU context from canvas');

    // Calling getCurrentTexture prior to configuration should throw an InvalidStateError exception.
    t.shouldThrow('InvalidStateError', () => {
      ctx.getCurrentTexture();
    });

    // Once the context has been configured getCurrentTexture can be called.
    ctx.configure({
      device: t.device,
      format: kFormat,
    });

    let prevTexture = ctx.getCurrentTexture();

    // Calling configure again with different values will change the texture returned.
    ctx.configure({
      device: t.device,
      format: 'bgra8unorm',
    });

    let currentTexture = ctx.getCurrentTexture();
    t.expect(prevTexture !== currentTexture);
    prevTexture = currentTexture;

    // Calling configure again with the same values will still change the texture returned.
    ctx.configure({
      device: t.device,
      format: 'bgra8unorm',
    });

    currentTexture = ctx.getCurrentTexture();
    t.expect(prevTexture !== currentTexture);
    prevTexture = currentTexture;

    // Calling getCurrentTexture after calling unconfigure should throw an InvalidStateError exception.
    ctx.unconfigure();

    t.shouldThrow('InvalidStateError', () => {
      ctx.getCurrentTexture();
    });
  });

g.test('single_frames')
  .desc(`Checks that the value of getCurrentTexture is consistent within a single frame.`)
  .params(u =>
    u //
      .combine('canvasType', kAllCanvasTypes)
  )
  .fn(t => {
    const ctx = t.initCanvasContext(t.params.canvasType);
    const frameTexture = ctx.getCurrentTexture();

    // Calling getCurrentTexture a second time returns the same texture.
    t.expect(frameTexture === ctx.getCurrentTexture());

    const encoder = t.device.createCommandEncoder();
    const pass = encoder.beginRenderPass({
      colorAttachments: [
        {
          view: frameTexture.createView(),
          clearValue: [1.0, 0.0, 0.0, 1.0],
          loadOp: 'clear',
          storeOp: 'store',
        },
      ],
    });
    pass.end();
    t.device.queue.submit([encoder.finish()]);

    // Calling getCurrentTexture after performing some work on the texture returns the same texture.
    t.expect(frameTexture === ctx.getCurrentTexture());

    // Ensure that getCurrentTexture does not clear the texture.
    t.expectSingleColor(frameTexture, frameTexture.format, {
      size: [frameTexture.width, frameTexture.height, 1],
      exp: { R: 1, G: 0, B: 0, A: 1 },
    });

    frameTexture.destroy();

    // Calling getCurrentTexture after destroying the texture still returns the same texture.
    t.expect(frameTexture === ctx.getCurrentTexture());
  });

g.test('multiple_frames')
  .desc(`Checks that the value of getCurrentTexture differs across multiple frames.`)
  .params(u =>
    u //
      .combine('canvasType', kAllCanvasTypes)
      .beginSubcases()
      .combine('clearTexture', [true, false])
  )
  .beforeAllSubcases(t => {
    const { canvasType } = t.params;
    if (canvasType === 'offscreen' && !('transferToImageBitmap' in OffscreenCanvas.prototype)) {
      throw new SkipTestCase('transferToImageBitmap not supported');
    }
  })
  .fn(t => {
    const { canvasType, clearTexture } = t.params;

    return new Promise(resolve => {
      const ctx = t.initCanvasContext(canvasType);
      let prevTexture: GPUTexture | undefined;
      let frameCount = 0;

      function frameCheck() {
        const currentTexture = ctx.getCurrentTexture();

        if (prevTexture) {
          // Ensure that each frame a new texture object is returned.
          t.expect(currentTexture !== prevTexture);

          // Ensure that texture contents are transparent black.
          t.expectSingleColor(currentTexture, currentTexture.format, {
            size: [currentTexture.width, currentTexture.height, 1],
            exp: { R: 0, G: 0, B: 0, A: 0 },
          });
        }

        if (clearTexture) {
          // Clear the texture to test that texture contents don't carry over from frame to frame.
          const encoder = t.device.createCommandEncoder();
          const pass = encoder.beginRenderPass({
            colorAttachments: [
              {
                view: currentTexture.createView(),
                clearValue: [1.0, 0.0, 0.0, 1.0],
                loadOp: 'clear',
                storeOp: 'store',
              },
            ],
          });
          pass.end();
          t.device.queue.submit([encoder.finish()]);
        }

        prevTexture = currentTexture;

        if (frameCount++ < 5) {
          // Which method will be used to begin a new "frame"?
          switch (canvasType) {
            case 'onscreen':
              requestAnimationFrame(frameCheck);
              break;
            case 'offscreen': {
              (ctx.canvas as OffscreenCanvas).transferToImageBitmap();
              frameCheck();
              break;
            }
            default:
              unreachable();
          }
        } else {
          resolve();
        }
      }

      // Call frameCheck for the first time from requestAnimationFrame
      // To make sure two frameChecks are run in different frames for onscreen canvas.
      // offscreen canvas doesn't care.
      requestAnimationFrame(frameCheck);
    });
  });

g.test('resize')
  .desc(`Checks the value of getCurrentTexture differs when the canvas is resized.`)
  .params(u =>
    u //
      .combine('canvasType', kAllCanvasTypes)
  )
  .fn(t => {
    const ctx = t.initCanvasContext(t.params.canvasType);
    let prevTexture = ctx.getCurrentTexture();

    // Trigger a resize by changing the width.
    ctx.canvas.width = 4;

    // When the canvas resizes the texture returned by getCurrentTexture should immediately begin
    // returning a new texture matching the update dimensions.
    let currentTexture = ctx.getCurrentTexture();
    t.expect(prevTexture !== currentTexture);
    t.expect(currentTexture.width === ctx.canvas.width);
    t.expect(currentTexture.height === ctx.canvas.height);

    // The width and height of the previous texture should remain unchanged.
    t.expect(prevTexture.width === 2);
    t.expect(prevTexture.height === 2);
    prevTexture = currentTexture;

    // Ensure that texture contents are transparent black.
    t.expectSingleColor(currentTexture, currentTexture.format, {
      size: [currentTexture.width, currentTexture.height, 1],
      exp: { R: 0, G: 0, B: 0, A: 0 },
    });

    // Trigger a resize by changing the height.
    ctx.canvas.height = 4;

    // Check to ensure the texture is resized again.
    currentTexture = ctx.getCurrentTexture();
    t.expect(prevTexture !== currentTexture);
    t.expect(currentTexture.width === ctx.canvas.width);
    t.expect(currentTexture.height === ctx.canvas.height);
    t.expect(prevTexture.width === 4);
    t.expect(prevTexture.height === 2);
    prevTexture = currentTexture;

    // Ensure that texture contents are transparent black.
    t.expectSingleColor(currentTexture, currentTexture.format, {
      size: [currentTexture.width, currentTexture.height, 1],
      exp: { R: 0, G: 0, B: 0, A: 0 },
    });

    // Simply setting the canvas width and height values to their current values should not trigger
    // a change in the texture.
    ctx.canvas.width = 4;
    ctx.canvas.height = 4;

    currentTexture = ctx.getCurrentTexture();
    t.expect(prevTexture === currentTexture);
  });

g.test('expiry')
  .desc(
    `
Test automatic WebGPU canvas texture expiry on all canvas types with the following requirements:
- getCurrentTexture returns the same texture object until the next task:
  - after previous frame update the rendering
  - before current frame update the rendering
  - in a microtask off the current frame task
- getCurrentTexture returns a new texture object and the old texture object becomes invalid
  as soon as possible after HTML update the rendering.

TODO: test more canvas types, and ways to update the rendering
- if on a different thread, expiry happens when the worker updates its rendering (worker "rPAF") OR transferToImageBitmap is called
- [draw, transferControlToOffscreen, then canvas is displayed] on either {main thread, or transferred to worker}
- [draw, canvas is displayed, then transferControlToOffscreen] on either {main thread, or transferred to worker}
- reftests for the above 2 (what gets displayed when the canvas is displayed)
- with canvas element added to DOM or not (applies to other canvas tests as well)
  - canvas is added to DOM after being rendered
  - canvas is already in DOM but becomes visible after being rendered
  `
  )
  .params(u =>
    u //
      .combine('canvasType', kAllCanvasTypes)
      .combine('prevFrameCallsite', ['runInNewCanvasFrame', 'requestAnimationFrame'] as const)
      .combine('getCurrentTextureAgain', [true, false] as const)
  )
  .fn(t => {
    const { canvasType, prevFrameCallsite, getCurrentTextureAgain } = t.params;
    const ctx = t.initCanvasContext(t.params.canvasType);
    // Create a bindGroupLayout to test invalid texture view usage later.
    const bgl = t.device.createBindGroupLayout({
      entries: [
        {
          binding: 0,
          visibility: GPUShaderStage.COMPUTE,
          texture: {},
        },
      ],
    });

    // The fn is called immediately after previous frame updating the rendering.
    // Polyfill by calling the callback by setTimeout, in the requestAnimationFrame callback (for onscreen canvas)
    // or after transferToImageBitmap (for offscreen canvas).
    function runInNewCanvasFrame(fn: () => void) {
      switch (canvasType) {
        case 'onscreen':
          requestAnimationFrame(() => timeout(fn));
          break;
        case 'offscreen':
          // for offscreen canvas, after calling transferToImageBitmap, we are in a new frame immediately
          (ctx.canvas as OffscreenCanvas).transferToImageBitmap();
          fn();
          break;
        default:
          unreachable();
      }
    }

    function checkGetCurrentTexture() {
      // Call getCurrentTexture on previous frame.
      const prevTexture = ctx.getCurrentTexture();

      // Call getCurrentTexture immediately after the frame, the texture object should stay the same.
      queueMicrotask(() => {
        if (getCurrentTextureAgain) {
          t.expect(prevTexture === ctx.getCurrentTexture());
        }

        // Call getCurrentTexture in a new frame.
        // It should expire the previous texture object return a new texture object by the next frame by then.
        // Call runInNewCanvasFrame in the micro task to make sure the new frame run after the getCurrentTexture in the micro task for offscreen canvas.
        runInNewCanvasFrame(() => {
          if (getCurrentTextureAgain) {
            t.expect(prevTexture !== ctx.getCurrentTexture());
          }

          // Event when prevTexture expired, createView should still succeed anyway.
          const prevTextureView = prevTexture.createView();
          // Using the invalid view should fail if it expires.
          t.expectValidationError(() => {
            t.device.createBindGroup({
              layout: bgl,
              entries: [{ binding: 0, resource: prevTextureView }],
            });
          });
        });
      });
    }

    switch (prevFrameCallsite) {
      case 'runInNewCanvasFrame':
        runInNewCanvasFrame(checkGetCurrentTexture);
        break;
      case 'requestAnimationFrame':
        requestAnimationFrame(checkGetCurrentTexture);
        break;
      default:
        break;
    }
  });