1import { poptions } from '../../../../common/framework/params_builder.js';
2import { assert } from '../../../../common/framework/util/util.js';
3import { kSizedTextureFormatInfo, SizedTextureFormat } from '../../../capability_info.js';
4import { ValidationTest } from '../validation_test.js';
5
6export const kAllTestMethods = [
7  'WriteTexture',
8  'CopyBufferToTexture',
9  'CopyTextureToBuffer',
10] as const;
11
12export class CopyBetweenLinearDataAndTextureTest extends ValidationTest {
13  bytesInACompleteRow(copyWidth: number, format: SizedTextureFormat): number {
14    const info = kSizedTextureFormatInfo[format];
15    assert(copyWidth % info.blockWidth === 0);
16    return (info.bytesPerBlock * copyWidth) / info.blockWidth;
17  }
18
19  requiredBytesInCopy(
20    layout: Required<GPUTextureDataLayout>,
21    format: SizedTextureFormat,
22    copyExtent: GPUExtent3DDict
23  ): number {
24    const info = kSizedTextureFormatInfo[format];
25    assert(layout.rowsPerImage % info.blockHeight === 0);
26    assert(copyExtent.height % info.blockHeight === 0);
27    assert(copyExtent.width % info.blockWidth === 0);
28    if (copyExtent.width === 0 || copyExtent.height === 0 || copyExtent.depth === 0) {
29      return 0;
30    } else {
31      const texelBlockRowsPerImage = layout.rowsPerImage / info.blockHeight;
32      const bytesPerImage = layout.bytesPerRow * texelBlockRowsPerImage;
33      const bytesInLastSlice =
34        layout.bytesPerRow * (copyExtent.height / info.blockHeight - 1) +
35        (copyExtent.width / info.blockWidth) * info.bytesPerBlock;
36      return bytesPerImage * (copyExtent.depth - 1) + bytesInLastSlice;
37    }
38  }
39
40  testRun(
41    textureCopyView: GPUTextureCopyView,
42    textureDataLayout: GPUTextureDataLayout,
43    size: GPUExtent3D,
44    {
45      dataSize,
46      method,
47      success,
48      submit = false, // If submit is true, the validaton error is expected to come from the submit and encoding should succeed.
49    }: { dataSize: number; method: string; success: boolean; submit?: boolean }
50  ): void {
51    switch (method) {
52      case 'WriteTexture': {
53        const data = new Uint8Array(dataSize);
54
55        this.expectValidationError(() => {
56          this.device.defaultQueue.writeTexture(textureCopyView, data, textureDataLayout, size);
57        }, !success);
58
59        break;
60      }
61      case 'CopyBufferToTexture': {
62        const buffer = this.device.createBuffer({
63          size: dataSize,
64          usage: GPUBufferUsage.COPY_SRC,
65        });
66
67        const encoder = this.device.createCommandEncoder();
68        encoder.copyBufferToTexture({ buffer, ...textureDataLayout }, textureCopyView, size);
69
70        if (submit) {
71          const cmd = encoder.finish();
72          this.expectValidationError(() => {
73            this.device.defaultQueue.submit([cmd]);
74          }, !success);
75        } else {
76          this.expectValidationError(() => {
77            encoder.finish();
78          }, !success);
79        }
80
81        break;
82      }
83      case 'CopyTextureToBuffer': {
84        const buffer = this.device.createBuffer({
85          size: dataSize,
86          usage: GPUBufferUsage.COPY_DST,
87        });
88
89        const encoder = this.device.createCommandEncoder();
90        encoder.copyTextureToBuffer(textureCopyView, { buffer, ...textureDataLayout }, size);
91
92        if (submit) {
93          const cmd = encoder.finish();
94          this.expectValidationError(() => {
95            this.device.defaultQueue.submit([cmd]);
96          }, !success);
97        } else {
98          this.expectValidationError(() => {
99            encoder.finish();
100          }, !success);
101        }
102
103        break;
104      }
105    }
106  }
107
108  // This is a helper function used for creating a texture when we don't have to be very
109  // precise about its size as long as it's big enough and properly aligned.
110  createAlignedTexture(
111    format: SizedTextureFormat,
112    copySize: GPUExtent3DDict = { width: 1, height: 1, depth: 1 },
113    origin: Required<GPUOrigin3DDict> = { x: 0, y: 0, z: 0 }
114  ): GPUTexture {
115    const info = kSizedTextureFormatInfo[format];
116    return this.device.createTexture({
117      size: {
118        width: Math.max(1, copySize.width + origin.x) * info.blockWidth,
119        height: Math.max(1, copySize.height + origin.y) * info.blockHeight,
120        depth: Math.max(1, copySize.depth + origin.z),
121      },
122      format,
123      usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST,
124    });
125  }
126}
127
128// For testing divisibility by a number we test all the values returned by this function:
129function valuesToTestDivisibilityBy(number: number): Iterable<number> {
130  const values = [];
131  for (let i = 0; i <= 2 * number; ++i) {
132    values.push(i);
133  }
134  values.push(3 * number);
135  return values;
136}
137
138interface WithFormat {
139  format: SizedTextureFormat;
140}
141
142interface WithFormatAndCoordinate extends WithFormat {
143  coordinateToTest: keyof GPUOrigin3DDict | keyof GPUExtent3DDict;
144}
145
146interface WithFormatAndMethod extends WithFormat {
147  method: string;
148}
149
150// This is a helper function used for expanding test parameters for texel block alignment tests on offset
151export function texelBlockAlignmentTestExpanderForOffset({ format }: WithFormat) {
152  return poptions(
153    'offset',
154    valuesToTestDivisibilityBy(kSizedTextureFormatInfo[format].bytesPerBlock)
155  );
156}
157
158// This is a helper function used for expanding test parameters for texel block alignment tests on rowsPerImage
159export function texelBlockAlignmentTestExpanderForRowsPerImage({ format }: WithFormat) {
160  return poptions(
161    'rowsPerImage',
162    valuesToTestDivisibilityBy(kSizedTextureFormatInfo[format].blockHeight)
163  );
164}
165
166// This is a helper function used for expanding test parameters for texel block alignment tests on origin and size
167export function texelBlockAlignmentTestExpanderForValueToCoordinate({
168  format,
169  coordinateToTest,
170}: WithFormatAndCoordinate) {
171  switch (coordinateToTest) {
172    case 'x':
173    case 'width':
174      return poptions(
175        'valueToCoordinate',
176        valuesToTestDivisibilityBy(kSizedTextureFormatInfo[format].blockWidth!)
177      );
178
179    case 'y':
180    case 'height':
181      return poptions(
182        'valueToCoordinate',
183        valuesToTestDivisibilityBy(kSizedTextureFormatInfo[format].blockHeight!)
184      );
185
186    case 'z':
187    case 'depth':
188      return poptions('valueToCoordinate', valuesToTestDivisibilityBy(1));
189  }
190}
191
192// This is a helper function used for filtering test parameters
193export function formatCopyableWithMethod({ format, method }: WithFormatAndMethod): boolean {
194  if (method === 'CopyTextureToBuffer') {
195    return kSizedTextureFormatInfo[format].copySrc;
196  } else {
197    return kSizedTextureFormatInfo[format].copyDst;
198  }
199}
200