summaryrefslogtreecommitdiffstats
path: root/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/copyToTexture/canvas.spec.ts
blob: babecb17fb2a8d38285476ce88f17805fdbf1136 (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
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
export const description = `
copyToTexture with HTMLCanvasElement and OffscreenCanvas sources.
`;

import { makeTestGroup } from '../../../common/framework/test_group.js';
import {
  kCanvasAlphaModes,
  kTextureFormatInfo,
  kValidTextureFormatsForCopyE2T,
  RegularTextureFormat,
} from '../../capability_info.js';
import { CopyToTextureUtils } from '../../util/copy_to_texture.js';
import { CanvasType, kAllCanvasTypes, createCanvas } from '../../util/create_elements.js';
import { TexelCompareOptions } from '../../util/texture/texture_ok.js';

class F extends CopyToTextureUtils {
  init2DCanvasContentWithColorSpace({
    width,
    height,
    colorSpace,
  }: {
    width: number;
    height: number;
    colorSpace: 'srgb' | 'display-p3';
  }): {
    canvas: HTMLCanvasElement | OffscreenCanvas;
    expectedSourceData: Uint8ClampedArray;
  } {
    const canvas = createCanvas(this, 'onscreen', width, height);

    let canvasContext = null;
    canvasContext = canvas.getContext('2d', { colorSpace });

    if (canvasContext === null) {
      this.skip('onscreen canvas 2d context not available');
    }

    if (
      typeof canvasContext.getContextAttributes === 'undefined' ||
      typeof canvasContext.getContextAttributes().colorSpace === 'undefined'
    ) {
      this.skip('color space attr is not supported for canvas 2d context');
    }

    const SOURCE_PIXEL_BYTES = 4;
    const imagePixels = new Uint8ClampedArray(SOURCE_PIXEL_BYTES * width * height);

    const rectWidth = Math.floor(width / 2);
    const rectHeight = Math.floor(height / 2);

    const alphaValue = 153;

    let pixelStartPos = 0;
    // Red;
    for (let i = 0; i < rectHeight; ++i) {
      for (let j = 0; j < rectWidth; ++j) {
        pixelStartPos = (i * width + j) * SOURCE_PIXEL_BYTES;
        imagePixels[pixelStartPos] = 255;
        imagePixels[pixelStartPos + 1] = 0;
        imagePixels[pixelStartPos + 2] = 0;
        imagePixels[pixelStartPos + 3] = alphaValue;
      }
    }

    // Lime;
    for (let i = 0; i < rectHeight; ++i) {
      for (let j = rectWidth; j < width; ++j) {
        pixelStartPos = (i * width + j) * SOURCE_PIXEL_BYTES;
        imagePixels[pixelStartPos] = 0;
        imagePixels[pixelStartPos + 1] = 255;
        imagePixels[pixelStartPos + 2] = 0;
        imagePixels[pixelStartPos + 3] = alphaValue;
      }
    }

    // Blue
    for (let i = rectHeight; i < height; ++i) {
      for (let j = 0; j < rectWidth; ++j) {
        pixelStartPos = (i * width + j) * SOURCE_PIXEL_BYTES;
        imagePixels[pixelStartPos] = 0;
        imagePixels[pixelStartPos + 1] = 0;
        imagePixels[pixelStartPos + 2] = 255;
        imagePixels[pixelStartPos + 3] = alphaValue;
      }
    }

    // Fuchsia
    for (let i = rectHeight; i < height; ++i) {
      for (let j = rectWidth; j < width; ++j) {
        pixelStartPos = (i * width + j) * SOURCE_PIXEL_BYTES;
        imagePixels[pixelStartPos] = 255;
        imagePixels[pixelStartPos + 1] = 0;
        imagePixels[pixelStartPos + 2] = 255;
        imagePixels[pixelStartPos + 3] = alphaValue;
      }
    }

    const imageData = new ImageData(imagePixels, width, height, { colorSpace });
    // MAINTENANCE_TODO: Remove as any when tsc support imageData.colorSpace
    /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
    if (typeof (imageData as any).colorSpace === 'undefined') {
      this.skip('color space attr is not supported for ImageData');
    }

    const ctx = canvasContext as CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D;
    ctx.putImageData(imageData, 0, 0);

    return {
      canvas,
      expectedSourceData: this.getExpectedReadbackFor2DCanvas(canvasContext, width, height),
    };
  }

  // MAINTENANCE_TODO: Cache the generated canvas to avoid duplicated initialization.
  init2DCanvasContent({
    canvasType,
    width,
    height,
  }: {
    canvasType: CanvasType;
    width: number;
    height: number;
  }): {
    canvas: HTMLCanvasElement | OffscreenCanvas;
    expectedSourceData: Uint8ClampedArray;
  } {
    const canvas = createCanvas(this, canvasType, width, height);

    let canvasContext = null;
    canvasContext = canvas.getContext('2d');

    if (canvasContext === null) {
      this.skip(canvasType + ' canvas 2d context not available');
    }

    const ctx = canvasContext;
    this.paint2DCanvas(ctx, width, height, 0.6);

    return {
      canvas,
      expectedSourceData: this.getExpectedReadbackFor2DCanvas(canvasContext, width, height),
    };
  }

  private paint2DCanvas(
    ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D,
    width: number,
    height: number,
    alphaValue: number
  ) {
    const rectWidth = Math.floor(width / 2);
    const rectHeight = Math.floor(height / 2);

    // Red
    ctx.fillStyle = `rgba(255, 0, 0, ${alphaValue})`;
    ctx.fillRect(0, 0, rectWidth, rectHeight);
    // Lime
    ctx.fillStyle = `rgba(0, 255, 0, ${alphaValue})`;
    ctx.fillRect(rectWidth, 0, width - rectWidth, rectHeight);
    // Blue
    ctx.fillStyle = `rgba(0, 0, 255, ${alphaValue})`;
    ctx.fillRect(0, rectHeight, rectWidth, height - rectHeight);
    // Fuchsia
    ctx.fillStyle = `rgba(255, 0, 255, ${alphaValue})`;
    ctx.fillRect(rectWidth, rectHeight, width - rectWidth, height - rectHeight);
  }

  // MAINTENANCE_TODO: Cache the generated canvas to avoid duplicated initialization.
  initGLCanvasContent({
    canvasType,
    contextName,
    width,
    height,
    premultiplied,
  }: {
    canvasType: CanvasType;
    contextName: 'webgl' | 'webgl2';
    width: number;
    height: number;
    premultiplied: boolean;
  }): {
    canvas: HTMLCanvasElement | OffscreenCanvas;
    expectedSourceData: Uint8ClampedArray;
  } {
    const canvas = createCanvas(this, canvasType, width, height);

    // MAINTENANCE_TODO: Workaround for @types/offscreencanvas missing an overload of
    // `OffscreenCanvas.getContext` that takes `string` or a union of context types.
    const gl = (canvas as HTMLCanvasElement).getContext(contextName, {
      premultipliedAlpha: premultiplied,
    }) as WebGLRenderingContext | WebGL2RenderingContext | null;

    if (gl === null) {
      this.skip(canvasType + ' canvas ' + contextName + ' context not available');
    }
    this.trackForCleanup(gl);

    const rectWidth = Math.floor(width / 2);
    const rectHeight = Math.floor(height / 2);

    const alphaValue = 0.6;
    const colorValue = premultiplied ? alphaValue : 1.0;

    // For webgl/webgl2 context canvas, if the context created with premultipliedAlpha attributes,
    // it means that the value in drawing buffer is premultiplied or not. So we should set
    // premultipliedAlpha value for premultipliedAlpha true gl context and unpremultipliedAlpha value
    // for the premultipliedAlpha false gl context.
    gl.enable(gl.SCISSOR_TEST);
    gl.scissor(0, 0, rectWidth, rectHeight);
    gl.clearColor(colorValue, 0.0, 0.0, alphaValue);
    gl.clear(gl.COLOR_BUFFER_BIT);

    gl.scissor(rectWidth, 0, width - rectWidth, rectHeight);
    gl.clearColor(0.0, colorValue, 0.0, alphaValue);
    gl.clear(gl.COLOR_BUFFER_BIT);

    gl.scissor(0, rectHeight, rectWidth, height - rectHeight);
    gl.clearColor(0.0, 0.0, colorValue, alphaValue);
    gl.clear(gl.COLOR_BUFFER_BIT);

    gl.scissor(rectWidth, rectHeight, width - rectWidth, height - rectHeight);
    gl.clearColor(colorValue, colorValue, colorValue, alphaValue);
    gl.clear(gl.COLOR_BUFFER_BIT);

    return {
      canvas,
      expectedSourceData: this.getExpectedReadbackForWebGLCanvas(gl, width, height),
    };
  }

  private getDataToInitSourceWebGPUCanvas(
    width: number,
    height: number,
    alphaMode: GPUCanvasAlphaMode
  ): Uint8ClampedArray {
    const rectWidth = Math.floor(width / 2);
    const rectHeight = Math.floor(height / 2);

    const alphaValue = 153;
    // Always output [153, 153, 153, 153]. When the alphaMode is...
    //   - premultiplied: the readback is CSS `rgba(255, 255, 255, 60%)`.
    //   - opaque: the readback is CSS `rgba(153, 153, 153, 100%)`.
    // getExpectedReadbackForWebGPUCanvas matches this.
    const colorValue = alphaValue;

    // BGRA8Unorm texture
    const initialData = new Uint8ClampedArray(4 * width * height);
    const maxRectHeightIndex = width * rectHeight;
    for (let pixelIndex = 0; pixelIndex < initialData.length / 4; ++pixelIndex) {
      const index = pixelIndex * 4;

      // Top-half two rectangles
      if (pixelIndex < maxRectHeightIndex) {
        // top-left side rectangle
        if (pixelIndex % width < rectWidth) {
          // top-left side rectangle
          initialData[index] = colorValue;
          initialData[index + 1] = 0;
          initialData[index + 2] = 0;
          initialData[index + 3] = alphaValue;
        } else {
          // top-right side rectangle
          initialData[index] = 0;
          initialData[index + 1] = colorValue;
          initialData[index + 2] = 0;
          initialData[index + 3] = alphaValue;
        }
      } else {
        // Bottom-half two rectangles
        // bottom-left side rectangle
        if (pixelIndex % width < rectWidth) {
          initialData[index] = 0;
          initialData[index + 1] = 0;
          initialData[index + 2] = colorValue;
          initialData[index + 3] = alphaValue;
        } else {
          // bottom-right side rectangle
          initialData[index] = colorValue;
          initialData[index + 1] = colorValue;
          initialData[index + 2] = colorValue;
          initialData[index + 3] = alphaValue;
        }
      }
    }
    return initialData;
  }

  initSourceWebGPUCanvas({
    device,
    canvasType,
    width,
    height,
    alphaMode,
  }: {
    device: GPUDevice;
    canvasType: CanvasType;
    width: number;
    height: number;
    alphaMode: GPUCanvasAlphaMode;
  }): {
    canvas: HTMLCanvasElement | OffscreenCanvas;
    expectedSourceData: Uint8ClampedArray;
  } {
    const canvas = createCanvas(this, canvasType, width, height);

    const gpuContext = canvas.getContext('webgpu');

    if (!(gpuContext instanceof GPUCanvasContext)) {
      this.skip(canvasType + ' canvas webgpu context not available');
    }

    gpuContext.configure({
      device,
      format: 'bgra8unorm',
      usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC,
      alphaMode,
    });

    // BGRA8Unorm texture
    const initialData = this.getDataToInitSourceWebGPUCanvas(width, height, alphaMode);
    const canvasTexture = gpuContext.getCurrentTexture();
    device.queue.writeTexture(
      { texture: canvasTexture },
      initialData,
      {
        bytesPerRow: width * 4,
        rowsPerImage: height,
      },
      {
        width,
        height,
        depthOrArrayLayers: 1,
      }
    );

    return {
      canvas,
      expectedSourceData: this.getExpectedReadbackForWebGPUCanvas(width, height, alphaMode),
    };
  }

  private getExpectedReadbackFor2DCanvas(
    context: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D,
    width: number,
    height: number
  ): Uint8ClampedArray {
    // Always read back the raw data from canvas
    return context.getImageData(0, 0, width, height).data;
  }

  private getExpectedReadbackForWebGLCanvas(
    gl: WebGLRenderingContext | WebGL2RenderingContext,
    width: number,
    height: number
  ): Uint8ClampedArray {
    const bytesPerPixel = 4;

    const sourcePixels = new Uint8ClampedArray(width * height * bytesPerPixel);
    gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, sourcePixels);

    return this.doFlipY(sourcePixels, width, height, bytesPerPixel);
  }

  private getExpectedReadbackForWebGPUCanvas(
    width: number,
    height: number,
    alphaMode: GPUCanvasAlphaMode
  ): Uint8ClampedArray {
    const bytesPerPixel = 4;

    const rgbaPixels = this.getDataToInitSourceWebGPUCanvas(width, height, alphaMode);

    // The source canvas has bgra8unorm back resource. We
    // swizzle the channels to align with 2d/webgl canvas and
    // clear alpha to 255 (1.0) when context alphaMode
    // is set to opaque (follow webgpu spec).
    for (let i = 0; i < height; ++i) {
      for (let j = 0; j < width; ++j) {
        const pixelPos = i * width + j;
        const r = rgbaPixels[pixelPos * bytesPerPixel + 2];
        if (alphaMode === 'opaque') {
          rgbaPixels[pixelPos * bytesPerPixel + 3] = 255;
        }

        rgbaPixels[pixelPos * bytesPerPixel + 2] = rgbaPixels[pixelPos * bytesPerPixel];
        rgbaPixels[pixelPos * bytesPerPixel] = r;
      }
    }

    return rgbaPixels;
  }

  doCopyContentsTest(
    source: HTMLCanvasElement | OffscreenCanvas,
    expectedSourceImage: Uint8ClampedArray,
    p: {
      width: number;
      height: number;
      dstColorFormat: RegularTextureFormat;
      srcDoFlipYDuringCopy: boolean;
      srcPremultiplied: boolean;
      dstPremultiplied: boolean;
    }
  ) {
    const dst = this.device.createTexture({
      size: {
        width: p.width,
        height: p.height,
        depthOrArrayLayers: 1,
      },
      format: p.dstColorFormat,
      usage:
        GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
    });

    // Construct expected value for different dst color format
    const info = kTextureFormatInfo[p.dstColorFormat];
    const expFormat = info.baseFormat ?? p.dstColorFormat;

    // For 2d canvas, get expected pixels with getImageData(), which returns unpremultiplied
    // values.
    const expectedDestinationImage = this.getExpectedDstPixelsFromSrcPixels({
      srcPixels: expectedSourceImage,
      srcOrigin: [0, 0],
      srcSize: [p.width, p.height],
      dstOrigin: [0, 0],
      dstSize: [p.width, p.height],
      subRectSize: [p.width, p.height],
      format: expFormat,
      flipSrcBeforeCopy: false,
      srcDoFlipYDuringCopy: p.srcDoFlipYDuringCopy,
      conversion: {
        srcPremultiplied: p.srcPremultiplied,
        dstPremultiplied: p.dstPremultiplied,
      },
    });

    this.doTestAndCheckResult(
      { source, origin: { x: 0, y: 0 }, flipY: p.srcDoFlipYDuringCopy },
      {
        texture: dst,
        origin: { x: 0, y: 0 },
        colorSpace: 'srgb',
        premultipliedAlpha: p.dstPremultiplied,
      },
      expectedDestinationImage,
      { width: p.width, height: p.height, depthOrArrayLayers: 1 },
      // 1.0 and 0.6 are representable precisely by all formats except rgb10a2unorm, but
      // allow diffs of 1ULP since that's the generally-appropriate threshold.
      { maxDiffULPsForNormFormat: 1, maxDiffULPsForFloatFormat: 1 }
    );
  }
}

export const g = makeTestGroup(F);

g.test('copy_contents_from_2d_context_canvas')
  .desc(
    `
  Test HTMLCanvasElement and OffscreenCanvas with 2d context
  can be copied to WebGPU texture correctly.

  It creates HTMLCanvasElement/OffscreenCanvas with '2d'.
  Use fillRect(2d context) to render red rect for top-left,
  green rect for top-right, blue rect for bottom-left and white for bottom-right.

  Then call copyExternalImageToTexture() to do a full copy to the 0 mipLevel
  of dst texture, and read the contents out to compare with the canvas contents.

  Provide premultiplied input if 'premultipliedAlpha' in 'GPUImageCopyTextureTagged'
  is set to 'true' and unpremultiplied input if it is set to 'false'.

  If 'flipY' in 'GPUImageCopyExternalImage' is set to 'true', copy will ensure the result
  is flipped.

  The tests covers:
  - Valid canvas type
  - Valid 2d context type
  - Valid dstColorFormat of copyExternalImageToTexture()
  - Valid dest alphaMode
  - Valid 'flipY' config in 'GPUImageCopyExternalImage' (named 'srcDoFlipYDuringCopy' in cases)
  - TODO(#913): color space tests need to be added

  And the expected results are all passed.
  `
  )
  .params(u =>
    u
      .combine('canvasType', kAllCanvasTypes)
      .combine('dstColorFormat', kValidTextureFormatsForCopyE2T)
      .combine('dstAlphaMode', kCanvasAlphaModes)
      .combine('srcDoFlipYDuringCopy', [true, false])
      .beginSubcases()
      .combine('width', [1, 2, 4, 15])
      .combine('height', [1, 2, 4, 15])
  )
  .fn(async t => {
    const { width, height, canvasType, dstAlphaMode } = t.params;

    const { canvas, expectedSourceData } = t.init2DCanvasContent({
      canvasType,
      width,
      height,
    });

    t.doCopyContentsTest(canvas, expectedSourceData, {
      srcPremultiplied: false,
      dstPremultiplied: dstAlphaMode === 'premultiplied',
      ...t.params,
    });
  });

g.test('copy_contents_from_gl_context_canvas')
  .desc(
    `
  Test HTMLCanvasElement and OffscreenCanvas with webgl/webgl2 context
  can be copied to WebGPU texture correctly.

  It creates HTMLCanvasElement/OffscreenCanvas with webgl'/'webgl2'.
  Use scissor + clear to render red rect for top-left, green rect
  for top-right, blue rect for bottom-left and white for bottom-right.
  And do premultiply alpha in advance if the webgl/webgl2 context is created
  with premultipliedAlpha : true.

  Then call copyExternalImageToTexture() to do a full copy to the 0 mipLevel
  of dst texture, and read the contents out to compare with the canvas contents.

  Provide premultiplied input if 'premultipliedAlpha' in 'GPUImageCopyTextureTagged'
  is set to 'true' and unpremultiplied input if it is set to 'false'.

  If 'flipY' in 'GPUImageCopyExternalImage' is set to 'true', copy will ensure the result
  is flipped.

  The tests covers:
  - Valid canvas type
  - Valid webgl/webgl2 context type
  - Valid dstColorFormat of copyExternalImageToTexture()
  - Valid source image alphaMode
  - Valid dest alphaMode
  - Valid 'flipY' config in 'GPUImageCopyExternalImage'(named 'srcDoFlipYDuringCopy' in cases)
  - TODO: color space tests need to be added

  And the expected results are all passed.
  `
  )
  .params(u =>
    u
      .combine('canvasType', kAllCanvasTypes)
      .combine('contextName', ['webgl', 'webgl2'] as const)
      .combine('dstColorFormat', kValidTextureFormatsForCopyE2T)
      .combine('srcPremultiplied', [true, false])
      .combine('dstAlphaMode', kCanvasAlphaModes)
      .combine('srcDoFlipYDuringCopy', [true, false])
      .beginSubcases()
      .combine('width', [1, 2, 4, 15])
      .combine('height', [1, 2, 4, 15])
  )
  .fn(async t => {
    const { width, height, canvasType, contextName, srcPremultiplied, dstAlphaMode } = t.params;

    const { canvas, expectedSourceData } = t.initGLCanvasContent({
      canvasType,
      contextName,
      width,
      height,
      premultiplied: srcPremultiplied,
    });

    t.doCopyContentsTest(canvas, expectedSourceData, {
      dstPremultiplied: dstAlphaMode === 'premultiplied',
      ...t.params,
    });
  });

g.test('copy_contents_from_gpu_context_canvas')
  .desc(
    `
  Test HTMLCanvasElement and OffscreenCanvas with webgpu context
  can be copied to WebGPU texture correctly.

  It creates HTMLCanvasElement/OffscreenCanvas with 'webgpu'.
  Use writeTexture to copy pixels to back buffer. The results are:
  red rect for top-left, green rect for top-right, blue rect for bottom-left
  and white for bottom-right.

  TODO: Actually test alphaMode = opaque.
  And do premultiply alpha in advance if the webgpu context is created
  with alphaMode="premultiplied".

  Then call copyExternalImageToTexture() to do a full copy to the 0 mipLevel
  of dst texture, and read the contents out to compare with the canvas contents.

  Provide premultiplied input if 'premultipliedAlpha' in 'GPUImageCopyTextureTagged'
  is set to 'true' and unpremultiplied input if it is set to 'false'.

  If 'flipY' in 'GPUImageCopyExternalImage' is set to 'true', copy will ensure the result
  is flipped.

  The tests covers:
  - Valid canvas type
  - Source WebGPU Canvas lives in the same GPUDevice or different GPUDevice as test
  - Valid dstColorFormat of copyExternalImageToTexture()
  - TODO: test more source image alphaMode
  - Valid dest alphaMode
  - Valid 'flipY' config in 'GPUImageCopyExternalImage'(named 'srcDoFlipYDuringCopy' in cases)
  - TODO: color space tests need to be added

  And the expected results are all passed.
  `
  )
  .params(u =>
    u
      .combine('canvasType', kAllCanvasTypes)
      .combine('srcAndDstInSameGPUDevice', [true, false])
      .combine('dstColorFormat', kValidTextureFormatsForCopyE2T)
      // .combine('srcAlphaMode', kCanvasAlphaModes)
      .combine('srcAlphaMode', ['premultiplied'] as const)
      .combine('dstAlphaMode', kCanvasAlphaModes)
      .combine('srcDoFlipYDuringCopy', [true, false])
      .beginSubcases()
      .combine('width', [1, 2, 4, 15])
      .combine('height', [1, 2, 4, 15])
  )
  .beforeAllSubcases(t => {
    t.selectMismatchedDeviceOrSkipTestCase(undefined);
  })
  .fn(async t => {
    const {
      width,
      height,
      canvasType,
      srcAndDstInSameGPUDevice,
      srcAlphaMode,
      dstAlphaMode,
    } = t.params;

    const device = srcAndDstInSameGPUDevice ? t.device : t.mismatchedDevice;
    const { canvas: source, expectedSourceData } = t.initSourceWebGPUCanvas({
      device,
      canvasType,
      width,
      height,
      alphaMode: srcAlphaMode,
    });

    t.doCopyContentsTest(source, expectedSourceData, {
      srcPremultiplied: srcAlphaMode === 'premultiplied',
      dstPremultiplied: dstAlphaMode === 'premultiplied',
      ...t.params,
    });
  });

g.test('color_space_conversion')
  .desc(
    `
    Test HTMLCanvasElement with 2d context can created with 'colorSpace' attribute.
    Using CopyExternalImageToTexture to copy from such type of canvas needs
    to do color space converting correctly.

    It creates HTMLCanvasElement/OffscreenCanvas with '2d' and 'colorSpace' attributes.
    Use fillRect(2d context) to render red rect for top-left,
    green rect for top-right, blue rect for bottom-left and white for bottom-right.

    Then call copyExternalImageToTexture() to do a full copy to the 0 mipLevel
    of dst texture, and read the contents out to compare with the canvas contents.

    Provide premultiplied input if 'premultipliedAlpha' in 'GPUImageCopyTextureTagged'
    is set to 'true' and unpremultiplied input if it is set to 'false'.

    If 'flipY' in 'GPUImageCopyExternalImage' is set to 'true', copy will ensure the result
    is flipped.

    If color space from source input and user defined dstTexture color space are different, the
    result must convert the content to user defined color space

    The tests covers:
    - Valid dstColorFormat of copyExternalImageToTexture()
    - Valid dest alphaMode
    - Valid 'flipY' config in 'GPUImageCopyExternalImage' (named 'srcDoFlipYDuringCopy' in cases)
    - Valid 'colorSpace' config in 'dstColorSpace'

    And the expected results are all passed.

    TODO: Enhance test data with colors that aren't always opaque and fully saturated.
    TODO: Consider refactoring src data setup with TexelView.writeTextureData.
  `
  )
  .params(u =>
    u
      .combine('srcColorSpace', ['srgb', 'display-p3'] as const)
      .combine('dstColorSpace', ['srgb'] as const)
      .combine('dstColorFormat', kValidTextureFormatsForCopyE2T)
      .combine('dstPremultiplied', [true, false])
      .combine('srcDoFlipYDuringCopy', [true, false])
      .beginSubcases()
      .combine('width', [1, 2, 4, 15, 255, 256])
      .combine('height', [1, 2, 4, 15, 255, 256])
  )
  .fn(async t => {
    const {
      width,
      height,
      srcColorSpace,
      dstColorSpace,
      dstColorFormat,
      dstPremultiplied,
      srcDoFlipYDuringCopy,
    } = t.params;
    const { canvas, expectedSourceData } = t.init2DCanvasContentWithColorSpace({
      width,
      height,
      colorSpace: srcColorSpace,
    });

    const dst = t.device.createTexture({
      size: { width, height },
      format: dstColorFormat,
      usage:
        GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
    });

    const expectedDestinationImage = t.getExpectedDstPixelsFromSrcPixels({
      srcPixels: expectedSourceData,
      srcOrigin: [0, 0],
      srcSize: [width, height],
      dstOrigin: [0, 0],
      dstSize: [width, height],
      subRectSize: [width, height],
      // copyExternalImageToTexture does not perform gamma-encoding into `-srgb` formats.
      format: kTextureFormatInfo[dstColorFormat].baseFormat ?? dstColorFormat,
      flipSrcBeforeCopy: false,
      srcDoFlipYDuringCopy,
      conversion: {
        srcPremultiplied: false,
        dstPremultiplied,
        srcColorSpace,
        dstColorSpace,
      },
    });

    const texelCompareOptions: TexelCompareOptions = {
      maxFractionalDiff: 0,
      maxDiffULPsForNormFormat: 1,
    };
    if (srcColorSpace !== dstColorSpace) {
      // Color space conversion seems prone to errors up to about 0.0003 on f32, 0.0007 on f16.
      texelCompareOptions.maxFractionalDiff = 0.001;
    } else {
      texelCompareOptions.maxDiffULPsForFloatFormat = 1;
    }

    t.doTestAndCheckResult(
      { source: canvas, origin: { x: 0, y: 0 }, flipY: srcDoFlipYDuringCopy },
      {
        texture: dst,
        origin: { x: 0, y: 0 },
        colorSpace: dstColorSpace,
        premultipliedAlpha: dstPremultiplied,
      },
      expectedDestinationImage,
      { width, height, depthOrArrayLayers: 1 },
      texelCompareOptions
    );
  });