Skip to content

Commit d288b5e

Browse files
committed
Add texture-component-swizzle tests
1 parent 2e418e2 commit d288b5e

File tree

2 files changed

+347
-1
lines changed

2 files changed

+347
-1
lines changed
Lines changed: 345 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,345 @@
1+
export const description = `
2+
Tests for capability checking for the 'texture-component-swizzle' feature.
3+
4+
Test that when the feature is off, swizzling is not allowed, even the identity swizzle.
5+
When the feature is on, swizzling is applied correctly.
6+
`;
7+
8+
import { makeTestGroup } from '../../../../../common/framework/test_group.js';
9+
import { GPUConst } from '../../../../constants.js';
10+
import { EncodableTextureFormat, isSintOrUintFormat } from '../../../../format_info.js';
11+
import { UniqueFeaturesOrLimitsGPUTest } from '../../../../gpu_test.js';
12+
import { convertPerTexelComponentToResultFormat } from '../../../../shader/execution/expression/call/builtin/texture_utils.js';
13+
import * as ttu from '../../../../texture_test_utils.js';
14+
import { PerTexelComponent } from '../../../../util/texture/texel_data.js';
15+
import { TexelView } from '../../../../util/texture/texel_view.js';
16+
import { createTextureFromTexelViews } from '../../../../util/texture.js';
17+
18+
export const g = makeTestGroup(UniqueFeaturesOrLimitsGPUTest);
19+
20+
// MAINTENANCE_TODO: Remove these types once texture-component-swizzle is added to @webgpu/types
21+
/* prettier-ignore */
22+
type GPUComponentSwizzle =
23+
| 'zero' // Force its value to 0.
24+
| 'one' // Force its value to 1.
25+
| 'r' // Take its value from the red channel of the texture.
26+
| 'g' // Take its value from the green channel of the texture.
27+
| 'b' // Take its value from the blue channel of the texture.
28+
| 'a' // Take its value from the alpha channel of the texture.
29+
;
30+
31+
type GPUTextureComponentSwizzle = {
32+
r?: GPUComponentSwizzle;
33+
g?: GPUComponentSwizzle;
34+
b?: GPUComponentSwizzle;
35+
a?: GPUComponentSwizzle;
36+
};
37+
38+
declare global {
39+
interface GPUTextureViewDescriptor {
40+
swizzle?: GPUTextureComponentSwizzle | undefined;
41+
}
42+
}
43+
44+
// Note: There are 4 settings with 7 options each including undefined
45+
// which is 2401 combinations. So we don't check them all. Just a few below.
46+
const kSwizzleTests = [
47+
'uuuu',
48+
'rgba',
49+
'0000',
50+
'1111',
51+
'rrrr',
52+
'gggg',
53+
'bbbb',
54+
'aaaa',
55+
'abgr',
56+
'gbar',
57+
'barg',
58+
'argb',
59+
'0gba',
60+
'r0ba',
61+
'rg0a',
62+
'rgb0',
63+
'1gba',
64+
'r1ba',
65+
'rg1a',
66+
'rgb1',
67+
'ubga',
68+
'ruga',
69+
'rbua',
70+
'rbgu',
71+
] as const;
72+
73+
const kSwizzleLetterToComponent: Record<string, GPUComponentSwizzle | undefined> = {
74+
u: undefined,
75+
r: 'r',
76+
g: 'g',
77+
b: 'b',
78+
a: 'a',
79+
'0': 'zero',
80+
'1': 'one',
81+
} as const;
82+
83+
const kComponents = ['r', 'g', 'b', 'a'] as const;
84+
85+
function swizzleSpecToGPUTextureComponentSwizzle(spec: string): GPUTextureComponentSwizzle {
86+
const swizzle: Record<string, string> = {};
87+
kComponents.forEach((component, i) => {
88+
const v = kSwizzleLetterToComponent[spec[i]];
89+
if (v) {
90+
swizzle[component] = v;
91+
}
92+
});
93+
return swizzle as GPUTextureComponentSwizzle;
94+
}
95+
96+
function swizzleComponentToTexelComponent(
97+
src: PerTexelComponent<number>,
98+
component: GPUComponentSwizzle
99+
): number {
100+
switch (component) {
101+
case 'zero':
102+
return 0;
103+
case 'one':
104+
return 1;
105+
case 'r':
106+
return src.R!;
107+
case 'g':
108+
return src.G!;
109+
case 'b':
110+
return src.B!;
111+
case 'a':
112+
return src.A!;
113+
}
114+
}
115+
116+
function swizzleTexel(
117+
src: PerTexelComponent<number>,
118+
swizzle: GPUTextureComponentSwizzle
119+
): PerTexelComponent<number> {
120+
return {
121+
R: swizzle.r ? swizzleComponentToTexelComponent(src, swizzle.r) : src.R,
122+
G: swizzle.g ? swizzleComponentToTexelComponent(src, swizzle.g) : src.G,
123+
B: swizzle.b ? swizzleComponentToTexelComponent(src, swizzle.b) : src.B,
124+
A: swizzle.a ? swizzleComponentToTexelComponent(src, swizzle.a) : src.A,
125+
};
126+
}
127+
128+
function isIdentitySwizzle(swizzle: GPUTextureComponentSwizzle): boolean {
129+
return (
130+
(swizzle.r === undefined || swizzle.r === 'r') &&
131+
(swizzle.g === undefined || swizzle.g === 'g') &&
132+
(swizzle.b === undefined || swizzle.b === 'b') &&
133+
(swizzle.a === undefined || swizzle.a === 'a')
134+
);
135+
}
136+
137+
function normalizeSwizzle(swizzle: GPUTextureComponentSwizzle): GPUTextureComponentSwizzle {
138+
return {
139+
r: swizzle.r ?? 'r',
140+
g: swizzle.g ?? 'g',
141+
b: swizzle.b ?? 'b',
142+
a: swizzle.a ?? 'a',
143+
};
144+
}
145+
146+
function swizzlesAreTheSame(a: GPUTextureComponentSwizzle, b: GPUTextureComponentSwizzle): boolean {
147+
a = normalizeSwizzle(a);
148+
b = normalizeSwizzle(b);
149+
return a.r === b.r && a.g === b.g && a.b === b.b && a.a === b.a;
150+
}
151+
152+
g.test('no_swizzle')
153+
.desc(
154+
`
155+
Test that if texture-component-swizzle is not enabled, having a swizzle property generates a validation error.
156+
`
157+
)
158+
.fn(t => {
159+
const texture = t.createTextureTracked({
160+
format: 'rgba8unorm',
161+
size: [1],
162+
usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING,
163+
});
164+
t.expectValidationError(() => {
165+
texture.createView({ swizzle: {} });
166+
});
167+
});
168+
169+
g.test('no_render_nor_storage')
170+
.desc(
171+
`
172+
Test that setting the swizzle on the texture with RENDER_ATTACHMENT or STORAGE_BINDING usage works
173+
if the swizzle is the identity but generates a validation error otherwise.
174+
`
175+
)
176+
.params(u =>
177+
u
178+
.combine('usage', [
179+
GPUConst.TextureUsage.COPY_SRC,
180+
GPUConst.TextureUsage.COPY_DST,
181+
GPUConst.TextureUsage.TEXTURE_BINDING,
182+
GPUConst.TextureUsage.RENDER_ATTACHMENT,
183+
GPUConst.TextureUsage.STORAGE_BINDING,
184+
] as const)
185+
.beginSubcases()
186+
.combine('swizzleSpec', kSwizzleTests)
187+
)
188+
.beforeAllSubcases(t => {
189+
// MAINTENANCE_TODO: Remove this cast once texture-component-swizzle is added to @webgpu/types
190+
t.selectDeviceOrSkipTestCase('texture-component-swizzle' as GPUFeatureName);
191+
})
192+
.fn(t => {
193+
const { swizzleSpec, usage } = t.params;
194+
const swizzle = swizzleSpecToGPUTextureComponentSwizzle(swizzleSpec);
195+
const texture = t.createTextureTracked({
196+
format: 'rgba8unorm',
197+
size: [1],
198+
usage,
199+
});
200+
const badUsage =
201+
(usage &
202+
(GPUConst.TextureUsage.RENDER_ATTACHMENT | GPUConst.TextureUsage.STORAGE_BINDING)) !==
203+
0;
204+
const shouldError = badUsage && !isIdentitySwizzle(swizzle);
205+
t.expectValidationError(() => {
206+
texture.createView({ swizzle });
207+
}, shouldError);
208+
});
209+
210+
g.test('read_swizzle')
211+
.desc(
212+
`
213+
Test reading textures with swizzles.
214+
* Test that multiple swizzles of the same texture work.
215+
* Test that multiple swizzles of the same fails in compat if the swizzles are different.
216+
`
217+
)
218+
.params(u =>
219+
u
220+
.combine('format', [
221+
'rgba8unorm',
222+
'bgra8unorm',
223+
'r8unorm',
224+
'rg8unorm',
225+
'r8uint',
226+
'rgba8uint',
227+
] as const)
228+
.beginSubcases()
229+
.combine('sampled', [false, true] as const)
230+
.combine('swizzleSpec', kSwizzleTests)
231+
.combine('otherSwizzleIndexOffset', [0, 1, 5]) // used to choose a different 2nd swizzle. 0 = same swizzle as 1st
232+
.unless(t => isSintOrUintFormat(t.format) && t.sampled)
233+
)
234+
.beforeAllSubcases(t => {
235+
// MAINTENANCE_TODO: Remove this cast once texture-component-swizzle is added to @webgpu/types
236+
t.selectDeviceOrSkipTestCase('texture-component-swizzle' as GPUFeatureName);
237+
})
238+
.fn(t => {
239+
const { format, sampled, swizzleSpec, otherSwizzleIndexOffset } = t.params;
240+
241+
const isIntFormat = isSintOrUintFormat(format);
242+
const srcColor = isIntFormat
243+
? { R: 20, G: 40, B: 60, A: 80 }
244+
: { R: 0.2, G: 0.4, B: 0.6, A: 0.8 };
245+
const srcTexelView = TexelView.fromTexelsAsColors(format, _coords => srcColor);
246+
247+
const texture = createTextureFromTexelViews(t, [srcTexelView], {
248+
format,
249+
size: [1],
250+
usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING,
251+
});
252+
253+
const otherSwizzleSpec =
254+
kSwizzleTests[
255+
(kSwizzleTests.indexOf(swizzleSpec) + otherSwizzleIndexOffset) % kSwizzleTests.length
256+
];
257+
258+
const expFormat: EncodableTextureFormat = isIntFormat ? 'rgba32uint' : 'rgba32float';
259+
const data = [swizzleSpec, otherSwizzleSpec].map(swizzleSpec => {
260+
const swizzle = swizzleSpecToGPUTextureComponentSwizzle(swizzleSpec);
261+
const expColor = swizzleTexel(
262+
convertPerTexelComponentToResultFormat(srcColor, format),
263+
swizzle
264+
);
265+
const expTexelView = TexelView.fromTexelsAsColors(expFormat, _coords => expColor);
266+
const textureView = texture.createView({ swizzle });
267+
return { swizzle, expFormat, expTexelView, textureView };
268+
});
269+
270+
const loadWGSL = sampled
271+
? (v: number) => `textureSampleLevel(tex${v}, smp, vec2f(0), 0)`
272+
: (v: number) => `textureLoad(tex${v}, vec2u(0), 0)`;
273+
const module = t.device.createShaderModule({
274+
code: `
275+
// These are intentionally in different bindGroups to test in compat that different swizzles
276+
// of the same texture are not allowed.
277+
@group(0) @binding(0) var tex0: texture_2d<${isIntFormat ? 'u32' : 'f32'}>;
278+
@group(1) @binding(0) var tex1: texture_2d<${isIntFormat ? 'u32' : 'f32'}>;
279+
@group(0) @binding(1) var smp: sampler;
280+
@group(0) @binding(2) var result: texture_storage_2d<${expFormat}, write>;
281+
282+
@compute @workgroup_size(1) fn cs() {
283+
_ = smp;
284+
let c0 = ${loadWGSL(0)};
285+
let c1 = ${loadWGSL(1)};
286+
textureStore(result, vec2u(0, 0), c0);
287+
textureStore(result, vec2u(1, 0), c1);
288+
}
289+
`,
290+
});
291+
292+
const pipeline = t.device.createComputePipeline({
293+
layout: 'auto',
294+
compute: { module },
295+
});
296+
297+
const outputTexture = t.createTextureTracked({
298+
format: expFormat,
299+
size: [2],
300+
usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.STORAGE_BINDING,
301+
});
302+
303+
const sampler = t.device.createSampler();
304+
305+
const bindGroup0 = t.device.createBindGroup({
306+
layout: pipeline.getBindGroupLayout(0),
307+
entries: [
308+
{ binding: 0, resource: data[0].textureView },
309+
{ binding: 1, resource: sampler },
310+
{ binding: 2, resource: outputTexture },
311+
],
312+
});
313+
314+
const bindGroup1 = t.device.createBindGroup({
315+
layout: pipeline.getBindGroupLayout(1),
316+
entries: [{ binding: 0, resource: data[1].textureView }],
317+
});
318+
319+
const encoder = t.device.createCommandEncoder();
320+
const pass = encoder.beginComputePass();
321+
pass.setPipeline(pipeline);
322+
pass.setBindGroup(0, bindGroup0);
323+
pass.setBindGroup(1, bindGroup1);
324+
pass.dispatchWorkgroups(1);
325+
pass.end();
326+
327+
if (t.isCompatibility && !swizzlesAreTheSame(data[0].swizzle, data[1].swizzle)) {
328+
// Swizzles can not be different in compatibility mode
329+
t.expectValidationError(() => {
330+
t.device.queue.submit([encoder.finish()]);
331+
});
332+
} else {
333+
t.device.queue.submit([encoder.finish()]);
334+
335+
data.forEach(({ expTexelView }, i) => {
336+
if (i === 0) return;
337+
ttu.expectTexelViewComparisonIsOkInTexture(
338+
t,
339+
{ texture: outputTexture, origin: [i, 0, 0] },
340+
expTexelView,
341+
[1, 1, 1]
342+
);
343+
});
344+
}
345+
});

src/webgpu/util/texture/base.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,8 @@ export function reifyTextureDescriptor(
216216
export function reifyTextureViewDescriptor(
217217
textureDescriptor: Readonly<GPUTextureDescriptor>,
218218
view: Readonly<GPUTextureViewDescriptor>
219-
): Required<Omit<GPUTextureViewDescriptor, 'label'>> {
219+
// MAINTENANCE_TODO: Remove | swizzle cast once texture-component-swizzle is added to @webgpu/types
220+
): Required<Omit<GPUTextureViewDescriptor, 'label' | 'swizzle'>> {
220221
const texture = reifyTextureDescriptor(textureDescriptor);
221222

222223
// IDL defaulting

0 commit comments

Comments
 (0)