export const description = `
Operational tests for the 'texture-component-swizzle' feature.
Test that:
* when the feature is on, swizzling is applied correctly.
Note: for texture_depth_xxx we only get f32
What happens in the GPU (at least in Metal)
1. we start with [depthOrCompareResult, depthOrCompareResult, depthOrCompareResult, 1]
2. we then swizzle
3. we then read the RED channel
Gather will do this 4 times and give us the result of step 3 in each channel.
The WebGPU spec says we should be starting with [depthOrCompare, 0, 0, 1] and the
implementation should deal with this.
`;
import { makeTestGroup } from '../../../../common/framework/test_group.js';
import { assert, range, unreachable } from '../../../../common/util/util.js';
import {
isSintOrUintFormat,
isDepthOrStencilTextureFormat,
kAllTextureFormats,
isDepthTextureFormat,
getBlockInfoForTextureFormat,
isStencilTextureFormat,
isDepthStencilTextureFormat,
isTextureFormatPossiblyMultisampled,
isTextureFormatUsableAsRenderAttachment,
} from '../../../format_info.js';
import { AllFeaturesMaxLimitsGPUTest } from '../../../gpu_test.js';
import {
applyCompareToTexel,
chooseTextureSize,
convertPerTexelComponentToResultFormat,
createTextureWithRandomDataAndGetTexelsForEachAspect,
getTextureFormatTypeInfo,
isBuiltinComparison,
isBuiltinGather,
isFillable,
swizzleTexel,
TextureBuiltin,
} from '../../../shader/execution/expression/call/builtin/texture_utils.js';
import * as ttu from '../../../texture_test_utils.js';
import { PerTexelComponent, TexelComponent } from '../../../util/texture/texel_data.js';
import { TexelView } from '../../../util/texture/texel_view.js';
import {
kSwizzleTests,
SwizzleSpec,
} from '../../validation/capability_checks/features/texture_component_swizzle_utils.js';
type TextureInput =
| 'texture_2d<f32>'
| 'texture_2d<u32>'
| 'texture_2d<i32>'
| 'texture_depth_2d'
| 'texture_multisampled_2d<f32>'
| 'texture_multisampled_2d<u32>'
| 'texture_multisampled_2d<i32>'
| 'texture_depth_multisampled_2d';
function isSingleChannelInput(input: TextureInput) {
return input === 'texture_depth_2d' || input === 'texture_depth_multisampled_2d';
}
function isMultisampledInput(input: TextureInput) {
return (
input === 'texture_multisampled_2d<f32>' ||
input === 'texture_multisampled_2d<u32>' ||
input === 'texture_multisampled_2d<i32>' ||
input === 'texture_depth_multisampled_2d'
);
}
function getSwizzleByOffsetFromSwizzle(swizzle: SwizzleSpec, offset: number): SwizzleSpec {
return kSwizzleTests[(kSwizzleTests.indexOf(swizzle) + offset) % kSwizzleTests.length];
}
const kTextureBuiltinFunctions = [
'textureGather',
'textureGatherCompare',
'textureLoad',
'textureSample',
'textureSampleBias',
'textureSampleCompare',
'textureSampleCompareLevel',
'textureSampleGrad',
'textureSampleLevel',
] as const;
function canBuiltinTakeTextureDepth(func: TextureBuiltin) {
return (
func === 'textureGather' ||
func === 'textureGatherCompare' ||
func === 'textureLoad' ||
func === 'textureSample' ||
func === 'textureSampleCompare' ||
func === 'textureSampleCompareLevel' ||
func === 'textureSampleLevel'
);
}
function canUseBuiltinFuncWithFormat(
func: TextureBuiltin,
format: GPUTextureFormat,
aspect: GPUTextureAspect
) {
const effectiveFormat = aspect === 'stencil-only' ? 'stencil8' : format;
if (isSintOrUintFormat(effectiveFormat)) {
return func === 'textureGather' || func === 'textureLoad';
} else if (!isDepthTextureFormat(effectiveFormat)) {
return (
func !== 'textureGatherCompare' &&
func !== 'textureSampleCompare' &&
func !== 'textureSampleCompareLevel'
);
} else {
return true;
}
}
function channelIndexToTexelComponent(channel: number): TexelComponent {
switch (channel) {
case 0:
return TexelComponent.R;
case 1:
return TexelComponent.G;
case 2:
return TexelComponent.B;
case 3:
return TexelComponent.A;
default:
throw new Error(`Invalid channel index: ${channel}`);
}
}
function getColorByChannelIndex(texel: PerTexelComponent<number>, channel: number): number {
const component = channelIndexToTexelComponent(channel);
const v = texel[component];
assert(v !== undefined);
return v;
}
function gather(
srcColors: PerTexelComponent<number>[],
channel: number
): PerTexelComponent<number> {
return {
R: getColorByChannelIndex(srcColors[2], channel),
G: getColorByChannelIndex(srcColors[3], channel),
B: getColorByChannelIndex(srcColors[1], channel),
A: getColorByChannelIndex(srcColors[0], channel),
};
}
const kGatherComponentOrder = ['B', 'A', 'R', 'G'] as const;
type PipelineCache = Map<string, GPUComputePipeline | GPURenderPipeline>;
const s_deviceToPipelines = new WeakMap<GPUDevice, PipelineCache>();
export const g = makeTestGroup(AllFeaturesMaxLimitsGPUTest);
g.test('read_swizzle')
.desc(
`
Test reading textures with swizzles.
* Test that multiple swizzles of the same texture work.
* Test that multiple swizzles of the same fails in compat if the swizzles are different.
`
)
.params(
u =>
u
.combine('format', kAllTextureFormats)
.filter(t => isFillable(t.format))
.combine('func', kTextureBuiltinFunctions)
.beginSubcases()
.expand('compare', function* (t) {
if (isBuiltinComparison(t.func)) {
yield 'less';
yield 'greater';
} else {
yield 'always';
}
})
.expand('aspect', function* (t) {
if (isDepthOrStencilTextureFormat(t.format)) {
if (isDepthTextureFormat(t.format)) {
yield 'depth-only';
}
if (isStencilTextureFormat(t.format)) {
yield 'stencil-only';
}
} else {
yield 'all';
}
})
.filter(t => canUseBuiltinFuncWithFormat(t.func, t.format, t.aspect))
.expand('input', function* (t) {
if (!isBuiltinComparison(t.func)) {
const { componentType } = getTextureFormatTypeInfo(t.format, t.aspect);
switch (componentType) {
case 'f32':
yield `texture_2d<f32>`;
break;
case 'u32':
yield `texture_2d<u32>`;
break;
case 'i32':
yield `texture_2d<i32>`;
break;
default:
unreachable();
}
}
if (
isDepthTextureFormat(t.format) &&
canBuiltinTakeTextureDepth(t.func) &&
t.aspect === 'depth-only'
) {
yield `texture_depth_2d`;
}
if (t.func === 'textureLoad' && isTextureFormatPossiblyMultisampled(t.format)) {
const { componentType } = getTextureFormatTypeInfo(t.format, t.aspect);
switch (componentType) {
case 'f32':
yield `texture_multisampled_2d<f32>`;
break;
case 'u32':
yield `texture_multisampled_2d<u32>`;
break;
case 'i32':
yield `texture_multisampled_2d<i32>`;
break;
default:
unreachable();
}
if (
isDepthTextureFormat(t.format) &&
canBuiltinTakeTextureDepth(t.func) &&
t.aspect === 'depth-only'
) {
yield `texture_depth_multisampled_2d`;
}
}
})
.expand('channel', function* (t) {
if (t.func === 'textureGather' && !isSingleChannelInput(t.input)) {
yield 0;
yield 1;
yield 2;
yield 3;
} else {
yield 0;
}
})
.combine('swizzle', kSwizzleTests)
.combine('otherSwizzleIndexOffset', [0, 1, 5])
)
.fn(async t => {
t.skipIfDeviceDoesNotHaveFeature('texture-component-swizzle');
const { format, func, channel, compare, input, aspect, swizzle, otherSwizzleIndexOffset } =
t.params;
t.skipIfTextureFormatNotSupported(format);
if (func === 'textureLoad') {
t.skipIfTextureLoadNotSupportedForTextureType(input);
}
if (isMultisampledInput(input)) {
t.skipIfTextureFormatNotMultisampled(format);
}
const otherSwizzle = getSwizzleByOffsetFromSwizzle(swizzle, otherSwizzleIndexOffset);
t.debug(() => `swizzle: ${swizzle}, otherSwizzle: ${otherSwizzle}`);
if (t.isCompatibility) {
t.skipIf(
swizzle !== otherSwizzle,
`swizzles must be equivalent in compatibility mode: ${swizzle} != ${otherSwizzle}`
);
t.skipIf(
!isBuiltinComparison(func) && input === 'texture_depth_2d',
'can not use depth textures with non-comparison samplers in compatibility mode'
);
}
const depthRef = 0.5;
const size = chooseTextureSize({ minSize: 2, minBlocks: 2, format });
const { blockWidth, blockHeight } = getBlockInfoForTextureFormat(format);
const tx = blockWidth - 0.4;
const ty = blockHeight - 0.4;
const descriptor: GPUTextureDescriptor = {
label: 'swizzle test texture',
format,
size,
usage:
GPUTextureUsage.COPY_DST |
GPUTextureUsage.TEXTURE_BINDING |
(isTextureFormatUsableAsRenderAttachment(t.device.features, format)
? GPUTextureUsage.RENDER_ATTACHMENT
: 0),
sampleCount: isMultisampledInput(input) ? 4 : 1,
};
const { texels: srcTexelViews, texture } =
await createTextureWithRandomDataAndGetTexelsForEachAspect(t, descriptor);
const aspectNdx = isDepthStencilTextureFormat(format) && aspect === 'stencil-only' ? 1 : 0;
const srcTexelView = srcTexelViews[aspectNdx];
const samples = [];
const sampledColors = range(4, i => {
const x = (tx | 0) + (i % 2);
const y = (ty | 0) + ((i / 2) | 0);
const sample = srcTexelView[0].color({ x, y, z: 0 });
samples.push(sample);
return convertPerTexelComponentToResultFormat(sample, format, aspect);
});
t.debug(
() => `samples:
${sampledColors.map((c, i) => `${i % 2}, ${(i / 2) | 0}, ${JSON.stringify(c)}`).join('\n')}`
);
const components = [TexelComponent.R, TexelComponent.G, TexelComponent.B, TexelComponent.A];
const readColors = sampledColors.map(sampledColor =>
isBuiltinComparison(func)
? applyCompareToTexel(components, sampledColor, compare, depthRef)
: sampledColor
);
const {
resultType,
sampleType: srcSampleType,
resultFormat: expFormat,
} = getTextureFormatTypeInfo(format, aspect);
const testData = [swizzle, otherSwizzle].map(swizzle => {
const swizzledColors = readColors.map(readColor => swizzleTexel(readColor, swizzle));
const expColor = isBuiltinGather(func)
? gather(swizzledColors, channel)
: isSingleChannelInput(input)
? {
R: swizzledColors[0].R,
G: swizzledColors[0].R,
B: swizzledColors[0].R,
A: swizzledColors[0].R,
}
: swizzledColors[0];
const expTexelView = TexelView.fromTexelsAsColors(expFormat, _coords => expColor);
const textureView = texture.createView({
label: `swizzle texture view(${swizzle})`,
swizzle,
aspect,
usage: GPUTextureUsage.TEXTURE_BINDING,
});
t.debug(
() => `\
swizzle: ${swizzle}, channel: ${channel}, ${
compare === 'always' ? '' : `compare: ${depthRef} is ${compare} than Texel`
}
readColors:
${readColors
.map((c, i) => `${i % 2}, ${(i / 2) | 0}, ${JSON.stringify(c)} ${kGatherComponentOrder[i]}`)
.join('\n')}
swizzledColors:
${swizzledColors
.map((c, i) => `${i % 2}, ${(i / 2) | 0}, ${JSON.stringify(c)} ${kGatherComponentOrder[i]}`)
.join('\n')}
`
);
return { swizzle, expColor, expFormat, expTexelView, textureView };
});
t.debug(
() => `expColors:
${testData
.map(({ expColor }, i) => `${i % 2}, ${(i / 2) | 0}, ${JSON.stringify(expColor)}`)
.join('\n')}`
);
const loadFn = ((func: TextureBuiltin) => {
switch (func) {
case 'textureGather':
return (v: number) =>
isSingleChannelInput(input)
? `textureGather(tex${v}, smp, uni.texCoord)`
: `textureGather(${channel}, tex${v}, smp, uni.texCoord)`;
case 'textureGatherCompare':
return (v: number) => `textureGatherCompare(tex${v}, smp, uni.texCoord, ${depthRef})`;
case 'textureLoad':
return (v: number) =>
`textureLoad(tex${v}, vec2u(uni.texCoord * vec2f(textureDimensions(tex${v}))), 0)`;
case 'textureSample':
return (v: number) => `textureSample(tex${v}, smp, uni.texCoord)`;
case 'textureSampleBias':
return (v: number) => `textureSampleBias(tex${v}, smp, uni.texCoord, 0)`;
case 'textureSampleCompare':
return (v: number) => `textureSampleCompare(tex${v}, smp, uni.texCoord, ${depthRef})`;
case 'textureSampleCompareLevel':
return (v: number) =>
`textureSampleCompareLevel(tex${v}, smp, uni.texCoord, ${depthRef})`;
case 'textureSampleGrad':
return (v: number) => `textureSampleGrad(tex${v}, smp, uni.texCoord, vec2f(0), vec2f(0))`;
case 'textureSampleLevel':
return (v: number) => `textureSampleLevel(tex${v}, smp, uni.texCoord, 0)`;
default:
throw new Error(`Unsupported texture builtin function: ${func}`);
}
})(func);
const loadWGSL = (v: number) => `${resultType}(${loadFn(v)})`;
const samplerWGSL = isBuiltinComparison(func) ? 'sampler_comparison' : 'sampler';
const code = `
struct Uniforms {
texCoord: vec2f,
};
// These are intentionally in different bindGroups to test in compat that different swizzles
// of the same texture are not allowed.
@group(0) @binding(0) var tex0: ${input};
@group(1) @binding(0) var tex1: ${input};
@group(0) @binding(1) var smp: ${samplerWGSL};
@group(0) @binding(2) var<uniform> uni: Uniforms;
@group(0) @binding(3) var result: texture_storage_2d<${expFormat}, write>;
@vertex fn vsFSResults() -> @builtin(position) vec4f {
return vec4f(0, 0, 0, 1);
}
@fragment fn fsFSResults() -> @location(0) vec4f {
let c0 = ${loadWGSL(0)};
let c1 = ${loadWGSL(1)};
textureStore(result, vec2u(0, 0), c0);
textureStore(result, vec2u(1, 0), c1);
return vec4f(0, 0, 0, 1);
}
`;
const sampleType = isSingleChannelInput(input)
? 'depth'
: srcSampleType === 'depth'
? isBuiltinComparison(func)
? 'depth'
: 'unfilterable-float'
: srcSampleType === 'float' && isMultisampledInput(input)
? 'unfilterable-float'
: srcSampleType;
const samplerType = isBuiltinComparison(func) ? 'comparison' : 'non-filtering';
const pipelineId = `${sampleType}:${samplerType}${code}`;
const cache = s_deviceToPipelines.get(t.device) ?? new Map();
s_deviceToPipelines.set(t.device, cache);
let pipeline = cache.get(pipelineId);
if (!pipeline) {
const module = t.device.createShaderModule({ code });
const bgl0 = t.device.createBindGroupLayout({
entries: [
{
binding: 0,
visibility: GPUShaderStage.COMPUTE | GPUShaderStage.FRAGMENT | GPUShaderStage.VERTEX,
texture: {
sampleType,
multisampled: isMultisampledInput(input),
},
},
{
binding: 1,
visibility: GPUShaderStage.COMPUTE | GPUShaderStage.FRAGMENT | GPUShaderStage.VERTEX,
sampler: {
type: samplerType,
},
},
{
binding: 2,
visibility: GPUShaderStage.COMPUTE | GPUShaderStage.FRAGMENT | GPUShaderStage.VERTEX,
buffer: {},
},
{
binding: 3,
visibility: GPUShaderStage.COMPUTE | GPUShaderStage.FRAGMENT,
storageTexture: {
format: expFormat,
},
},
],
});
const bgl1 = t.device.createBindGroupLayout({
entries: [
{
binding: 0,
visibility: GPUShaderStage.COMPUTE | GPUShaderStage.FRAGMENT | GPUShaderStage.VERTEX,
texture: {
sampleType,
multisampled: isMultisampledInput(input),
},
},
],
});
const layout = t.device.createPipelineLayout({
bindGroupLayouts: [bgl0, bgl1],
});
pipeline = t.device.createRenderPipeline({
layout,
vertex: { module },
fragment: { module, targets: [{ format: 'rgba8unorm' }] },
primitive: { topology: 'point-list' },
});
cache.set(pipelineId, pipeline);
}
const outputTexture = t.createTextureTracked({
format: expFormat,
size: [2],
usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.STORAGE_BINDING,
});
const sampler = t.device.createSampler(
isBuiltinComparison(func)
? {
compare,
}
: {}
);
const uniformBuffer = t.createBufferTracked({
size: (2 + 2) * 4,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
const uniformValues = new ArrayBuffer(uniformBuffer.size);
const asF32 = new Float32Array(uniformValues);
asF32.set([tx / texture.width, ty / texture.height]);
t.debug(
() =>
`texcoords: ${[...asF32]} tx = ${tx}, ty = ${ty}, size: ${texture.width}, ${
texture.height
}`
);
t.device.queue.writeBuffer(uniformBuffer, 0, new Uint32Array(uniformValues));
const bindGroup0 = t.device.createBindGroup({
layout: pipeline.getBindGroupLayout(0),
entries: [
{ binding: 0, resource: testData[0].textureView },
{ binding: 1, resource: sampler },
{ binding: 2, resource: uniformBuffer },
{ binding: 3, resource: outputTexture },
],
});
const bindGroup1 = t.device.createBindGroup({
layout: pipeline.getBindGroupLayout(1),
entries: [{ binding: 0, resource: testData[1].textureView }],
});
const encoder = t.device.createCommandEncoder();
const pass = encoder.beginRenderPass({
colorAttachments: [
{
view: t.createTextureTracked({
format: 'rgba8unorm',
size: [1],
usage: GPUTextureUsage.RENDER_ATTACHMENT,
}),
loadOp: 'clear',
storeOp: 'store',
},
],
});
pass.setPipeline(pipeline);
pass.setBindGroup(0, bindGroup0);
pass.setBindGroup(1, bindGroup1);
pass.draw(1);
pass.end();
if (t.isCompatibility && testData[0].swizzle !== testData[1].swizzle) {
t.expectValidationError(() => {
t.device.queue.submit([encoder.finish()]);
});
} else {
t.device.queue.submit([encoder.finish()]);
testData.forEach(({ swizzle, expTexelView }, i) => {
t.debug(() => `${i}: ${swizzle}`);
ttu.expectTexelViewComparisonIsOkInTexture(
t,
{ texture: outputTexture, origin: [i, 0, 0] },
expTexelView,
[1, 1, 1],
{ maxFractionalDiff: 0.01 }
);
});
}
});