1import { TestCaseRecorder } from '../../../../common/framework/logging/test_case_recorder.js'; 2import { params, poptions, pbool } from '../../../../common/framework/params_builder.js'; 3import { CaseParams } from '../../../../common/framework/params_utils.js'; 4import { assert, unreachable } from '../../../../common/framework/util/util.js'; 5import { 6 kTextureAspects, 7 kUncompressedTextureFormatInfo, 8 kUncompressedTextureFormats, 9 EncodableTextureFormat, 10 UncompressedTextureFormat, 11} from '../../../capability_info.js'; 12import { GPUConst } from '../../../constants.js'; 13import { GPUTest } from '../../../gpu_test.js'; 14import { createTextureUploadBuffer } from '../../../util/texture/layout.js'; 15import { BeginEndRange, SubresourceRange } from '../../../util/texture/subresource.js'; 16import { PerTexelComponent, getTexelDataRepresentation } from '../../../util/texture/texelData.js'; 17 18enum UninitializeMethod { 19 Creation = 'Creation', // The texture was just created. It is uninitialized. 20 StoreOpClear = 'StoreOpClear', // The texture was rendered to with GPUStoreOp "clear" 21} 22const kUninitializeMethods = Object.keys(UninitializeMethod) as UninitializeMethod[]; 23 24export enum ReadMethod { 25 Sample = 'Sample', // The texture is sampled from 26 CopyToBuffer = 'CopyToBuffer', // The texture is copied to a buffer 27 CopyToTexture = 'CopyToTexture', // The texture is copied to another texture 28 DepthTest = 'DepthTest', // The texture is read as a depth buffer 29 StencilTest = 'StencilTest', // The texture is read as a stencil buffer 30 ColorBlending = 'ColorBlending', // Read the texture by blending as a color attachment 31 Storage = 'Storage', // Read the texture as a storage texture 32} 33 34// Test with these mip level counts 35type MipLevels = 1 | 5; 36const kMipLevelCounts: MipLevels[] = [1, 5]; 37 38// For each mip level count, define the mip ranges to leave uninitialized. 39const kUninitializedMipRangesToTest: { [k in MipLevels]: BeginEndRange[] } = { 40 1: [{ begin: 0, end: 1 }], // Test the only mip 41 5: [ 42 { begin: 0, end: 2 }, 43 { begin: 3, end: 4 }, 44 ], // Test a range and a single mip 45}; 46 47// Test with these sample counts. 48type SampleCounts = 1 | 4; 49const kSampleCounts: SampleCounts[] = [1, 4]; 50 51// Test with these slice counts. This means the depth of a 3d texture or the number 52// or layers in a 2D or a 1D texture array. 53type SliceCounts = 1 | 7; 54 55// For each slice count, define the slices to leave uninitialized. 56const kUninitializedSliceRangesToTest: { [k in SliceCounts]: BeginEndRange[] } = { 57 1: [{ begin: 0, end: 1 }], // Test the only slice 58 7: [ 59 { begin: 2, end: 4 }, 60 { begin: 6, end: 7 }, 61 ], // Test a range and a single slice 62}; 63 64// Test with these combinations of texture dimension and sliceCount. 65const kCreationSizes: Array<{ 66 dimension: GPUTextureDimension; 67 sliceCount: SliceCounts; 68}> = [ 69 // { dimension: '1d', sliceCount: 7 }, // TODO: 1d textures 70 { dimension: '2d', sliceCount: 1 }, // 2d textures 71 { dimension: '2d', sliceCount: 7 }, // 2d array textures 72 // { dimension: '3d', sliceCount: 7 }, // TODO: 3d textures 73]; 74 75// Enums to abstract over color / depth / stencil values in textures. Depending on the texture format, 76// the data for each value may have a different representation. These enums are converted to a 77// representation such that their values can be compared. ex.) An integer is needed to upload to an 78// unsigned normalized format, but its value is read as a float in the shader. 79export const enum InitializedState { 80 Canary, // Set on initialized subresources. It should stay the same. On discarded resources, we should observe zero. 81 Zero, // We check that uninitialized subresources are in this state when read back. 82} 83 84export function initializedStateAsFloat(state: InitializedState): number { 85 switch (state) { 86 case InitializedState.Zero: 87 return 0; 88 case InitializedState.Canary: 89 return 1; 90 default: 91 unreachable(); 92 } 93} 94 95export function initializedStateAsUint(state: InitializedState): number { 96 switch (state) { 97 case InitializedState.Zero: 98 return 0; 99 case InitializedState.Canary: 100 return 255; 101 default: 102 unreachable(); 103 } 104} 105 106export function initializedStateAsSint(state: InitializedState): number { 107 switch (state) { 108 case InitializedState.Zero: 109 return 0; 110 case InitializedState.Canary: 111 return -1; 112 default: 113 unreachable(); 114 } 115} 116 117export function initializedStateAsColor( 118 state: InitializedState, 119 format: GPUTextureFormat 120): [number, number, number, number] { 121 let value; 122 if (format.indexOf('uint') !== -1) { 123 value = initializedStateAsUint(state); 124 } else if (format.indexOf('sint') !== -1) { 125 value = initializedStateAsSint(state); 126 } else { 127 value = initializedStateAsFloat(state); 128 } 129 return [value, value, value, value]; 130} 131 132export function initializedStateAsDepth(state: InitializedState): number { 133 switch (state) { 134 case InitializedState.Zero: 135 return 0; 136 case InitializedState.Canary: 137 return 1; 138 default: 139 unreachable(); 140 } 141} 142 143export function initializedStateAsStencil(state: InitializedState): number { 144 switch (state) { 145 case InitializedState.Zero: 146 return 0; 147 case InitializedState.Canary: 148 return 42; 149 default: 150 unreachable(); 151 } 152} 153 154interface TestParams { 155 format: EncodableTextureFormat; 156 aspect: GPUTextureAspect; 157 dimension: GPUTextureDimension; 158 sliceCount: SliceCounts; 159 mipLevelCount: MipLevels; 160 sampleCount: SampleCounts; 161 uninitializeMethod: UninitializeMethod; 162 readMethod: ReadMethod; 163 nonPowerOfTwo: boolean; 164} 165 166function getRequiredTextureUsage( 167 format: UncompressedTextureFormat, 168 sampleCount: SampleCounts, 169 uninitializeMethod: UninitializeMethod, 170 readMethod: ReadMethod 171): GPUTextureUsageFlags { 172 let usage: GPUTextureUsageFlags = GPUConst.TextureUsage.COPY_DST; 173 174 switch (uninitializeMethod) { 175 case UninitializeMethod.Creation: 176 break; 177 case UninitializeMethod.StoreOpClear: 178 usage |= GPUConst.TextureUsage.OUTPUT_ATTACHMENT; 179 break; 180 default: 181 unreachable(); 182 } 183 184 switch (readMethod) { 185 case ReadMethod.CopyToBuffer: 186 case ReadMethod.CopyToTexture: 187 usage |= GPUConst.TextureUsage.COPY_SRC; 188 break; 189 case ReadMethod.Sample: 190 usage |= GPUConst.TextureUsage.SAMPLED; 191 break; 192 case ReadMethod.Storage: 193 usage |= GPUConst.TextureUsage.STORAGE; 194 break; 195 case ReadMethod.DepthTest: 196 case ReadMethod.StencilTest: 197 case ReadMethod.ColorBlending: 198 usage |= GPUConst.TextureUsage.OUTPUT_ATTACHMENT; 199 break; 200 default: 201 unreachable(); 202 } 203 204 if (sampleCount > 1) { 205 // Copies to multisampled textures are not allowed. We need OutputAttachment to initialize 206 // canary data in multisampled textures. 207 usage |= GPUConst.TextureUsage.OUTPUT_ATTACHMENT; 208 } 209 210 if (!kUncompressedTextureFormatInfo[format].copyDst) { 211 // Copies are not possible. We need OutputAttachment to initialize 212 // canary data. 213 assert(kUncompressedTextureFormatInfo[format].renderable); 214 usage |= GPUConst.TextureUsage.OUTPUT_ATTACHMENT; 215 } 216 217 return usage; 218} 219 220export abstract class TextureZeroInitTest extends GPUTest { 221 protected stateToTexelComponents: { [k in InitializedState]: PerTexelComponent<number> }; 222 223 constructor(rec: TestCaseRecorder, params: CaseParams) { 224 super(rec, params); 225 226 const stateToTexelComponents = (state: InitializedState) => { 227 const [R, G, B, A] = initializedStateAsColor(state, this.params.format); 228 return { 229 R, 230 G, 231 B, 232 A, 233 Depth: initializedStateAsDepth(state), 234 Stencil: initializedStateAsStencil(state), 235 }; 236 }; 237 238 this.stateToTexelComponents = { 239 [InitializedState.Zero]: stateToTexelComponents(InitializedState.Zero), 240 [InitializedState.Canary]: stateToTexelComponents(InitializedState.Canary), 241 }; 242 } 243 244 get params(): TestParams { 245 return super.params as TestParams; 246 } 247 248 get textureWidth(): number { 249 let width = 1 << this.params.mipLevelCount; 250 if (this.params.nonPowerOfTwo) { 251 width = 2 * width - 1; 252 } 253 return width; 254 } 255 256 get textureHeight(): number { 257 let height = 1 << this.params.mipLevelCount; 258 if (this.params.nonPowerOfTwo) { 259 height = 2 * height - 1; 260 } 261 return height; 262 } 263 264 // Used to iterate subresources and check that their uninitialized contents are zero when accessed 265 *iterateUninitializedSubresources(): Generator<SubresourceRange> { 266 for (const mipRange of kUninitializedMipRangesToTest[this.params.mipLevelCount]) { 267 for (const sliceRange of kUninitializedSliceRangesToTest[this.params.sliceCount]) { 268 yield new SubresourceRange({ mipRange, sliceRange }); 269 } 270 } 271 } 272 273 // Used to iterate and initialize other subresources not checked for zero-initialization. 274 // Zero-initialization of uninitialized subresources should not have side effects on already 275 // initialized subresources. 276 *iterateInitializedSubresources(): Generator<SubresourceRange> { 277 const uninitialized: boolean[][] = new Array(this.params.mipLevelCount); 278 for (let level = 0; level < uninitialized.length; ++level) { 279 uninitialized[level] = new Array(this.params.sliceCount); 280 } 281 for (const subresources of this.iterateUninitializedSubresources()) { 282 for (const { level, slice } of subresources.each()) { 283 uninitialized[level][slice] = true; 284 } 285 } 286 for (let level = 0; level < uninitialized.length; ++level) { 287 for (let slice = 0; slice < uninitialized[level].length; ++slice) { 288 if (!uninitialized[level][slice]) { 289 yield new SubresourceRange({ 290 mipRange: { begin: level, count: 1 }, 291 sliceRange: { begin: slice, count: 1 }, 292 }); 293 } 294 } 295 } 296 } 297 298 *generateTextureViewDescriptorsForRendering( 299 aspect: GPUTextureAspect, 300 subresourceRange?: SubresourceRange 301 ): Generator<GPUTextureViewDescriptor> { 302 const viewDescriptor: GPUTextureViewDescriptor = { 303 dimension: '2d', 304 aspect, 305 }; 306 307 if (subresourceRange === undefined) { 308 return viewDescriptor; 309 } 310 311 for (const { level, slice } of subresourceRange.each()) { 312 yield { 313 ...viewDescriptor, 314 baseMipLevel: level, 315 mipLevelCount: 1, 316 baseArrayLayer: slice, 317 arrayLayerCount: 1, 318 }; 319 } 320 } 321 322 abstract checkContents( 323 texture: GPUTexture, 324 state: InitializedState, 325 subresourceRange: SubresourceRange 326 ): void; 327 328 private initializeWithStoreOp( 329 state: InitializedState, 330 texture: GPUTexture, 331 subresourceRange?: SubresourceRange 332 ): void { 333 const commandEncoder = this.device.createCommandEncoder(); 334 for (const viewDescriptor of this.generateTextureViewDescriptorsForRendering( 335 this.params.aspect, 336 subresourceRange 337 )) { 338 if (kUncompressedTextureFormatInfo[this.params.format].color) { 339 commandEncoder 340 .beginRenderPass({ 341 colorAttachments: [ 342 { 343 attachment: texture.createView(viewDescriptor), 344 storeOp: 'store', 345 loadValue: initializedStateAsColor(state, this.params.format), 346 }, 347 ], 348 }) 349 .endPass(); 350 } else { 351 commandEncoder 352 .beginRenderPass({ 353 colorAttachments: [], 354 depthStencilAttachment: { 355 attachment: texture.createView(viewDescriptor), 356 depthStoreOp: 'store', 357 depthLoadValue: initializedStateAsDepth(state), 358 stencilStoreOp: 'store', 359 stencilLoadValue: initializedStateAsStencil(state), 360 }, 361 }) 362 .endPass(); 363 } 364 } 365 this.queue.submit([commandEncoder.finish()]); 366 } 367 368 private initializeWithCopy( 369 texture: GPUTexture, 370 state: InitializedState, 371 subresourceRange: SubresourceRange 372 ): void { 373 if (this.params.dimension === '1d' || this.params.dimension === '3d') { 374 // TODO: https://github.com/gpuweb/gpuweb/issues/69 375 // Copies with 1D and 3D textures are not yet specified 376 unreachable(); 377 } 378 379 const firstSubresource = subresourceRange.each().next().value; 380 assert(typeof firstSubresource !== 'undefined'); 381 382 const largestWidth = this.textureWidth >> firstSubresource.level; 383 const largestHeight = this.textureHeight >> firstSubresource.level; 384 385 const texelData = new Uint8Array( 386 getTexelDataRepresentation(this.params.format).getBytes(this.stateToTexelComponents[state]) 387 ); 388 const { buffer, bytesPerRow, rowsPerImage } = createTextureUploadBuffer( 389 texelData, 390 this.device, 391 this.params.format, 392 this.params.dimension, 393 [largestWidth, largestHeight, 1] 394 ); 395 396 const commandEncoder = this.device.createCommandEncoder(); 397 398 for (const { level, slice } of subresourceRange.each()) { 399 const width = this.textureWidth >> level; 400 const height = this.textureHeight >> level; 401 402 commandEncoder.copyBufferToTexture( 403 { 404 buffer, 405 bytesPerRow, 406 rowsPerImage, 407 }, 408 { texture, mipLevel: level, origin: { x: 0, y: 0, z: slice } }, 409 { width, height, depth: 1 } 410 ); 411 } 412 this.queue.submit([commandEncoder.finish()]); 413 buffer.destroy(); 414 } 415 416 initializeTexture( 417 texture: GPUTexture, 418 state: InitializedState, 419 subresourceRange: SubresourceRange 420 ): void { 421 if ( 422 this.params.sampleCount > 1 || 423 !kUncompressedTextureFormatInfo[this.params.format].copyDst 424 ) { 425 // Copies to multisampled textures not yet specified. 426 // Use a storeOp for now. 427 assert(kUncompressedTextureFormatInfo[this.params.format].renderable); 428 this.initializeWithStoreOp(state, texture, subresourceRange); 429 } else { 430 this.initializeWithCopy(texture, state, subresourceRange); 431 } 432 } 433 434 discardTexture(texture: GPUTexture, subresourceRange: SubresourceRange): void { 435 const commandEncoder = this.device.createCommandEncoder(); 436 437 for (const desc of this.generateTextureViewDescriptorsForRendering( 438 this.params.aspect, 439 subresourceRange 440 )) { 441 if (kUncompressedTextureFormatInfo[this.params.format].color) { 442 commandEncoder 443 .beginRenderPass({ 444 colorAttachments: [ 445 { 446 attachment: texture.createView(desc), 447 storeOp: 'clear', 448 loadValue: 'load', 449 }, 450 ], 451 }) 452 .endPass(); 453 } else { 454 commandEncoder 455 .beginRenderPass({ 456 colorAttachments: [], 457 depthStencilAttachment: { 458 attachment: texture.createView(desc), 459 depthStoreOp: 'clear', 460 depthLoadValue: 'load', 461 stencilStoreOp: 'clear', 462 stencilLoadValue: 'load', 463 }, 464 }) 465 .endPass(); 466 } 467 } 468 this.queue.submit([commandEncoder.finish()]); 469 } 470 471 static generateParams(readMethods: ReadMethod[]) { 472 return ( 473 // TODO: Consider making a list of "valid" texture descriptors in capability_info. 474 params() 475 .combine(poptions('format', kUncompressedTextureFormats)) 476 .combine(poptions('aspect', kTextureAspects)) 477 .unless( 478 ({ format, aspect }) => 479 (aspect === 'depth-only' && !kUncompressedTextureFormatInfo[format].depth) || 480 (aspect === 'stencil-only' && !kUncompressedTextureFormatInfo[format].stencil) 481 ) 482 .combine(poptions('mipLevelCount', kMipLevelCounts)) 483 .combine(poptions('sampleCount', kSampleCounts)) 484 // Multisampled textures may only have one mip 485 .unless(({ sampleCount, mipLevelCount }) => sampleCount > 1 && mipLevelCount > 1) 486 .combine(poptions('uninitializeMethod', kUninitializeMethods)) 487 .combine(poptions('readMethod', readMethods)) 488 .unless( 489 ({ readMethod, format }) => 490 // It doesn't make sense to copy from a packed depth format. 491 // This is not specified yet, but it will probably be disallowed as the bits may 492 // be vendor-specific. 493 // TODO: Test copying out of the stencil aspect. 494 (readMethod === ReadMethod.CopyToBuffer || readMethod === ReadMethod.CopyToTexture) && 495 (format === 'depth24plus' || format === 'depth24plus-stencil8') 496 ) 497 .unless(({ readMethod, format }) => { 498 const info = kUncompressedTextureFormatInfo[format]; 499 return ( 500 (readMethod === ReadMethod.DepthTest && !info.depth) || 501 (readMethod === ReadMethod.StencilTest && !info.stencil) || 502 (readMethod === ReadMethod.ColorBlending && !info.color) || 503 // TODO: Test with depth sampling 504 (readMethod === ReadMethod.Sample && info.depth) 505 ); 506 }) 507 .unless( 508 ({ readMethod, sampleCount }) => 509 // We can only read from multisampled textures by sampling. 510 sampleCount > 1 && 511 (readMethod === ReadMethod.CopyToBuffer || readMethod === ReadMethod.CopyToTexture) 512 ) 513 .combine(kCreationSizes) 514 // Multisampled 3D / 2D array textures not supported. 515 .unless(({ sampleCount, sliceCount }) => sampleCount > 1 && sliceCount > 1) 516 .filter(({ format, sampleCount, uninitializeMethod, readMethod }) => { 517 const usage = getRequiredTextureUsage( 518 format, 519 sampleCount, 520 uninitializeMethod, 521 readMethod 522 ); 523 const info = kUncompressedTextureFormatInfo[format]; 524 525 if (usage & GPUConst.TextureUsage.OUTPUT_ATTACHMENT && !info.renderable) { 526 return false; 527 } 528 529 if (usage & GPUConst.TextureUsage.STORAGE && !info.storage) { 530 return false; 531 } 532 533 return true; 534 }) 535 .combine(pbool('nonPowerOfTwo')) 536 ); 537 } 538 539 run(): void { 540 const { 541 format, 542 dimension, 543 mipLevelCount, 544 sliceCount, 545 sampleCount, 546 uninitializeMethod, 547 readMethod, 548 } = this.params; 549 550 const usage = getRequiredTextureUsage(format, sampleCount, uninitializeMethod, readMethod); 551 552 const texture = this.device.createTexture({ 553 size: [this.textureWidth, this.textureHeight, sliceCount], 554 format, 555 dimension, 556 usage, 557 mipLevelCount, 558 sampleCount, 559 }); 560 561 // Initialize some subresources with canary values 562 for (const subresourceRange of this.iterateInitializedSubresources()) { 563 this.initializeTexture(texture, InitializedState.Canary, subresourceRange); 564 } 565 566 switch (uninitializeMethod) { 567 case UninitializeMethod.Creation: 568 break; 569 case UninitializeMethod.StoreOpClear: 570 // Initialize the rest of the resources. 571 for (const subresourceRange of this.iterateUninitializedSubresources()) { 572 this.initializeTexture(texture, InitializedState.Canary, subresourceRange); 573 } 574 // Then use a store op to discard their contents. 575 for (const subresourceRange of this.iterateUninitializedSubresources()) { 576 this.discardTexture(texture, subresourceRange); 577 } 578 break; 579 default: 580 unreachable(); 581 } 582 583 // Check that all uninitialized resources are zero. 584 for (const subresourceRange of this.iterateUninitializedSubresources()) { 585 this.checkContents(texture, InitializedState.Zero, subresourceRange); 586 } 587 588 // Check the all other resources are unchanged. 589 for (const subresourceRange of this.iterateInitializedSubresources()) { 590 this.checkContents(texture, InitializedState.Canary, subresourceRange); 591 } 592 } 593} 594