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