1export const description = `
2copyImageBitmapToTexture Validation Tests in Queue.
3
4Test Plan:
5- For source.imageBitmap:
6  - imageBitmap generated from ImageData:
7    - Check that an error is generated when imageBitmap is closed.
8
9- For destination.texture:
10  - For 2d destination textures:
11    - Check that an error is generated when texture is in destroyed state.
12    - Check that an error is generated when texture is an error texture.
13    - Check that an error is generated when texture is created without usage COPY_DST.
14    - Check that an error is generated when sample count is not 1.
15    - Check that an error is generated when mipLevel is too large.
16    - Check that an error is generated when texture format is not valid.
17
18- For copySize:
19  - Noop copy shouldn't throw any exception or return any validation error.
20  - Check that an error is generated when destination.texture.origin + copySize is too large.
21
22TODO: 1d, 3d texture and 2d array textures.
23`;
24
25import { poptions, params, pbool } from '../../../../common/framework/params_builder.js';
26import { makeTestGroup } from '../../../../common/framework/test_group.js';
27import { kAllTextureFormats, kTextureUsages } from '../../../capability_info.js';
28import { ValidationTest } from '../validation_test.js';
29
30const kDefaultBytesPerPixel = 4; // using 'bgra8unorm' or 'rgba8unorm'
31const kDefaultWidth = 32;
32const kDefaultHeight = 32;
33const kDefaultDepth = 1;
34const kDefaultMipLevelCount = 6;
35
36// From spec
37const kValidTextureFormatsForCopyIB2T = [
38  'rgba8unorm',
39  'rgba8unorm-srgb',
40  'bgra8unorm',
41  'bgra8unorm-srgb',
42  'rgb10a2unorm',
43  'rgba16float',
44  'rgba32float',
45  'rg8unorm',
46  'rg16float',
47];
48
49function computeMipMapSize(width: number, height: number, mipLevel: number) {
50  return {
51    mipWidth: Math.max(width >> mipLevel, 1),
52    mipHeight: Math.max(height >> mipLevel, 1),
53  };
54}
55
56interface WithMipLevel {
57  mipLevel: number;
58}
59
60interface WithDstOriginMipLevel extends WithMipLevel {
61  dstOrigin: Required<GPUOrigin3DDict>;
62}
63
64// Helper function to generate copySize for src OOB test
65function generateCopySizeForSrcOOB({ srcOrigin }: { srcOrigin: Required<GPUOrigin2DDict> }) {
66  // OOB origin fails even with noop copy.
67  if (srcOrigin.x > kDefaultWidth || srcOrigin.y > kDefaultHeight) {
68    return poptions('copySize', [{ width: 0, height: 0, depth: 0 }]);
69  }
70
71  const justFitCopySize = {
72    width: kDefaultWidth - srcOrigin.x,
73    height: kDefaultHeight - srcOrigin.y,
74    depth: 1,
75  };
76
77  return poptions('copySize', [
78    justFitCopySize, // correct size, maybe noop copy.
79    { width: justFitCopySize.width + 1, height: justFitCopySize.height, depth: 1 }, // OOB in width
80    { width: justFitCopySize.width, height: justFitCopySize.height + 1, depth: 1 }, // OOB in height
81    { width: justFitCopySize.width, height: justFitCopySize.height, depth: 2 }, // OOB in depth
82  ]);
83}
84
85// Helper function to generate dst origin value based on mipLevel.
86function generateDstOriginValue({ mipLevel }: WithMipLevel) {
87  const origin = computeMipMapSize(kDefaultWidth, kDefaultHeight, mipLevel);
88
89  return poptions('dstOrigin', [
90    { x: 0, y: 0, z: 0 },
91    { x: origin.mipWidth - 1, y: 0, z: 0 },
92    { x: 0, y: origin.mipHeight - 1, z: 0 },
93    { x: origin.mipWidth, y: 0, z: 0 },
94    { x: 0, y: origin.mipHeight, z: 0 },
95    { x: 0, y: 0, z: kDefaultDepth },
96    { x: origin.mipWidth + 1, y: 0, z: 0 },
97    { x: 0, y: origin.mipHeight + 1, z: 0 },
98    { x: 0, y: 0, z: kDefaultDepth + 1 },
99  ]);
100}
101
102// Helper function to generate copySize for dst OOB test
103function generateCopySizeForDstOOB({ mipLevel, dstOrigin }: WithDstOriginMipLevel) {
104  const dstMipMapSize = computeMipMapSize(kDefaultWidth, kDefaultHeight, mipLevel);
105
106  // OOB origin fails even with noop copy.
107  if (
108    dstOrigin.x > dstMipMapSize.mipWidth ||
109    dstOrigin.y > dstMipMapSize.mipHeight ||
110    dstOrigin.z > kDefaultDepth
111  ) {
112    return poptions('copySize', [{ width: 0, height: 0, depth: 0 }]);
113  }
114
115  const justFitCopySize = {
116    width: dstMipMapSize.mipWidth - dstOrigin.x,
117    height: dstMipMapSize.mipHeight - dstOrigin.y,
118    depth: kDefaultDepth - dstOrigin.z,
119  };
120
121  return poptions('copySize', [
122    justFitCopySize,
123    {
124      width: justFitCopySize.width + 1,
125      height: justFitCopySize.height,
126      depth: justFitCopySize.depth,
127    }, // OOB in width
128    {
129      width: justFitCopySize.width,
130      height: justFitCopySize.height + 1,
131      depth: justFitCopySize.depth,
132    }, // OOB in height
133    {
134      width: justFitCopySize.width,
135      height: justFitCopySize.height,
136      depth: justFitCopySize.depth + 1,
137    }, // OOB in depth
138  ]);
139}
140
141class CopyImageBitmapToTextureTest extends ValidationTest {
142  getImageData(width: number, height: number): ImageData {
143    const pixelSize = kDefaultBytesPerPixel * width * height;
144    const imagePixels = new Uint8ClampedArray(pixelSize);
145    return new ImageData(imagePixels, width, height);
146  }
147
148  runTest(
149    imageBitmapCopyView: GPUImageBitmapCopyView,
150    textureCopyView: GPUTextureCopyView,
151    copySize: GPUExtent3D,
152    validationScopeSuccess: boolean,
153    exceptionName?: string
154  ): void {
155    // CopyImageBitmapToTexture will generate two types of errors. One is synchronous exceptions;
156    // the other is asynchronous validation error scope errors.
157    if (exceptionName) {
158      this.shouldThrow(exceptionName, () => {
159        this.device.defaultQueue.copyImageBitmapToTexture(
160          imageBitmapCopyView,
161          textureCopyView,
162          copySize
163        );
164      });
165    } else {
166      this.expectValidationError(() => {
167        this.device.defaultQueue.copyImageBitmapToTexture(
168          imageBitmapCopyView,
169          textureCopyView,
170          copySize
171        );
172      }, !validationScopeSuccess);
173    }
174  }
175}
176
177export const g = makeTestGroup(CopyImageBitmapToTextureTest);
178
179g.test('source_imageBitmap,state')
180  .params(
181    params()
182      .combine(pbool('closed'))
183      .combine(
184        poptions('copySize', [
185          { width: 0, height: 0, depth: 0 },
186          { width: 1, height: 1, depth: 1 },
187        ])
188      )
189  )
190  .fn(async t => {
191    const { closed, copySize } = t.params;
192    const imageBitmap = await createImageBitmap(t.getImageData(1, 1));
193    const dstTexture = t.device.createTexture({
194      size: { width: 1, height: 1, depth: 1 },
195      format: 'bgra8unorm',
196      usage: GPUTextureUsage.COPY_DST,
197    });
198
199    if (closed) imageBitmap.close();
200
201    t.runTest(
202      { imageBitmap },
203      { texture: dstTexture },
204      copySize,
205      true, // No validation errors.
206      closed ? 'InvalidStateError' : ''
207    );
208  });
209
210g.test('destination_texture,state')
211  .params(
212    params()
213      .combine(poptions('state', ['valid', 'invalid', 'destroyed'] as const))
214      .combine(
215        poptions('copySize', [
216          { width: 0, height: 0, depth: 0 },
217          { width: 1, height: 1, depth: 1 },
218        ])
219      )
220  )
221  .fn(async t => {
222    const { state, copySize } = t.params;
223    const imageBitmap = await createImageBitmap(t.getImageData(1, 1));
224    const dstTexture = t.createTextureWithState(state);
225
226    t.runTest({ imageBitmap }, { texture: dstTexture }, copySize, state === 'valid');
227  });
228
229g.test('destination_texture,usage')
230  .params(
231    params()
232      .combine(poptions('usage', kTextureUsages))
233      .combine(
234        poptions('copySize', [
235          { width: 0, height: 0, depth: 0 },
236          { width: 1, height: 1, depth: 1 },
237        ])
238      )
239  )
240  .fn(async t => {
241    const { usage, copySize } = t.params;
242    const imageBitmap = await createImageBitmap(t.getImageData(1, 1));
243    const dstTexture = t.device.createTexture({
244      size: { width: 1, height: 1, depth: 1 },
245      format: 'rgba8unorm',
246      usage,
247    });
248
249    t.runTest(
250      { imageBitmap },
251      { texture: dstTexture },
252      copySize,
253      !!(usage & GPUTextureUsage.COPY_DST)
254    );
255  });
256
257g.test('destination_texture,sample_count')
258  .params(
259    params()
260      .combine(poptions('sampleCount', [1, 4]))
261      .combine(
262        poptions('copySize', [
263          { width: 0, height: 0, depth: 0 },
264          { width: 1, height: 1, depth: 1 },
265        ])
266      )
267  )
268  .fn(async t => {
269    const { sampleCount, copySize } = t.params;
270    const imageBitmap = await createImageBitmap(t.getImageData(1, 1));
271    const dstTexture = t.device.createTexture({
272      size: { width: 1, height: 1, depth: 1 },
273      sampleCount,
274      format: 'bgra8unorm',
275      usage: GPUTextureUsage.COPY_DST,
276    });
277
278    t.runTest({ imageBitmap }, { texture: dstTexture }, copySize, sampleCount === 1);
279  });
280
281g.test('destination_texture,mipLevel')
282  .params(
283    params()
284      .combine(poptions('mipLevel', [0, kDefaultMipLevelCount - 1, kDefaultMipLevelCount]))
285      .combine(
286        poptions('copySize', [
287          { width: 0, height: 0, depth: 0 },
288          { width: 1, height: 1, depth: 1 },
289        ])
290      )
291  )
292  .fn(async t => {
293    const { mipLevel, copySize } = t.params;
294    const imageBitmap = await createImageBitmap(t.getImageData(1, 1));
295    const dstTexture = t.device.createTexture({
296      size: { width: kDefaultWidth, height: kDefaultHeight, depth: kDefaultDepth },
297      mipLevelCount: kDefaultMipLevelCount,
298      format: 'bgra8unorm',
299      usage: GPUTextureUsage.COPY_DST,
300    });
301
302    t.runTest(
303      { imageBitmap },
304      { texture: dstTexture, mipLevel },
305      copySize,
306      mipLevel < kDefaultMipLevelCount
307    );
308  });
309
310g.test('destination_texture,format')
311  .params(
312    params()
313      .combine(poptions('format', kAllTextureFormats))
314      .combine(
315        poptions('copySize', [
316          { width: 0, height: 0, depth: 0 },
317          { width: 1, height: 1, depth: 1 },
318        ])
319      )
320  )
321  .fn(async t => {
322    const { format, copySize } = t.params;
323    const imageBitmap = await createImageBitmap(t.getImageData(1, 1));
324
325    // createTexture with all possible texture format may have validation error when using
326    // compressed texture format.
327    t.device.pushErrorScope('validation');
328    const dstTexture = t.device.createTexture({
329      size: { width: 1, height: 1, depth: 1 },
330      format,
331      usage: GPUTextureUsage.COPY_DST,
332    });
333    t.device.popErrorScope();
334
335    const success = kValidTextureFormatsForCopyIB2T.includes(format);
336
337    t.runTest(
338      { imageBitmap },
339      { texture: dstTexture },
340      copySize,
341      true, // No validation errors.
342      success ? '' : 'TypeError'
343    );
344  });
345
346g.test('OOB,source')
347  .params(
348    params()
349      .combine(
350        poptions('srcOrigin', [
351          { x: 0, y: 0 }, // origin is on top-left
352          { x: kDefaultWidth - 1, y: 0 }, // x near the border
353          { x: 0, y: kDefaultHeight - 1 }, // y is near the border
354          { x: kDefaultWidth, y: kDefaultHeight }, // origin is on bottom-right
355          { x: kDefaultWidth + 1, y: 0 }, // x is too large
356          { x: 0, y: kDefaultHeight + 1 }, // y is too large
357        ])
358      )
359      .expand(generateCopySizeForSrcOOB)
360  )
361  .fn(async t => {
362    const { srcOrigin, copySize } = t.params;
363    const imageBitmap = await createImageBitmap(t.getImageData(kDefaultWidth, kDefaultHeight));
364    const dstTexture = t.device.createTexture({
365      size: { width: kDefaultWidth + 1, height: kDefaultHeight + 1, depth: kDefaultDepth },
366      mipLevelCount: kDefaultMipLevelCount,
367      format: 'bgra8unorm',
368      usage: GPUTextureUsage.COPY_DST,
369    });
370
371    let success = true;
372
373    if (
374      srcOrigin.x + copySize.width > kDefaultWidth ||
375      srcOrigin.y + copySize.height > kDefaultHeight ||
376      copySize.depth > 1
377    ) {
378      success = false;
379    }
380
381    t.runTest({ imageBitmap, origin: srcOrigin }, { texture: dstTexture }, copySize, success);
382  });
383
384g.test('OOB,destination')
385  .params(
386    params()
387      .combine(poptions('mipLevel', [0, 1, kDefaultMipLevelCount - 2]))
388      .expand(generateDstOriginValue)
389      .expand(generateCopySizeForDstOOB)
390  )
391  .fn(async t => {
392    const { mipLevel, dstOrigin, copySize } = t.params;
393
394    const imageBitmap = await createImageBitmap(
395      t.getImageData(kDefaultWidth + 1, kDefaultHeight + 1)
396    );
397    const dstTexture = t.device.createTexture({
398      size: {
399        width: kDefaultWidth,
400        height: kDefaultHeight,
401        depth: kDefaultDepth,
402      },
403      format: 'bgra8unorm',
404      mipLevelCount: kDefaultMipLevelCount,
405      usage: GPUTextureUsage.COPY_DST,
406    });
407
408    let success = true;
409    const dstMipMapSize = computeMipMapSize(kDefaultWidth, kDefaultHeight, mipLevel);
410
411    if (
412      copySize.depth > 1 ||
413      dstOrigin.x + copySize.width > dstMipMapSize.mipWidth ||
414      dstOrigin.y + copySize.height > dstMipMapSize.mipHeight ||
415      dstOrigin.z + copySize.depth > kDefaultDepth
416    ) {
417      success = false;
418    }
419
420    t.runTest(
421      { imageBitmap },
422      {
423        texture: dstTexture,
424        mipLevel,
425        origin: dstOrigin,
426      },
427      copySize,
428      success
429    );
430  });
431