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