blob: 1d56c634fdf68d262e3a83f87f1d336da5b556d4 [file] [log] [blame]
export const description = 'Test helpers for texel data produce the expected data in the shader';
import { makeTestGroup } from '../../../common/framework/test_group.js';
import { assert } from '../../../common/util/util.js';
import {
kEncodableTextureFormats,
EncodableTextureFormat,
isColorTextureFormat,
canCopyToAllAspectsOfTextureFormat,
} from '../../format_info.js';
import { AllFeaturesMaxLimitsGPUTest, GPUTest } from '../../gpu_test.js';
import { gammaCompress, floatAsNormalizedIntegerUnquantized } from '../conversion.js';
import {
kTexelRepresentationInfo,
getSingleDataType,
getComponentReadbackTraits,
ComponentDataType,
} from './texel_data.js';
export const g = makeTestGroup(AllFeaturesMaxLimitsGPUTest);
function isCopyDstColorTextureFormatOfType(
format: EncodableTextureFormat,
type: ComponentDataType
) {
return (
isColorTextureFormat(format) &&
canCopyToAllAspectsOfTextureFormat(format) &&
getSingleDataType(format) === type
);
}
async function doTest(
t: GPUTest & {
params: {
format: EncodableTextureFormat;
componentData: {
R?: number;
G?: number;
B?: number;
A?: number;
};
};
}
) {
const { format } = t.params;
t.skipIfTextureFormatNotSupported(format);
const componentData = t.params.componentData;
const rep = kTexelRepresentationInfo[format];
const texelData = rep.pack(componentData);
const texture = t.createTextureTracked({
format,
size: [1, 1, 1],
usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING,
});
t.device.queue.writeTexture(
{ texture },
texelData,
{
bytesPerRow: texelData.byteLength,
},
[1]
);
const { ReadbackTypedArray, shaderType } = getComponentReadbackTraits(getSingleDataType(format));
const shader = `
@group(0) @binding(0) var tex : texture_2d<${shaderType}>;
struct Output {
${rep.componentOrder.map(C => `result${C} : ${shaderType},`).join('\n')}
};
@group(0) @binding(1) var<storage, read_write> output : Output;
@compute @workgroup_size(1)
fn main() {
var texel : vec4<${shaderType}> = textureLoad(tex, vec2<i32>(0, 0), 0);
${rep.componentOrder.map(C => `output.result${C} = texel.${C.toLowerCase()};`).join('\n')}
return;
}`;
const pipeline = t.device.createComputePipeline({
layout: 'auto',
compute: {
module: t.device.createShaderModule({
code: shader,
}),
entryPoint: 'main',
},
});
const outputBuffer = t.createBufferTracked({
size: rep.componentOrder.length * 4,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
});
const bindGroup = t.device.createBindGroup({
layout: pipeline.getBindGroupLayout(0),
entries: [
{
binding: 0,
resource: texture.createView(),
},
{
binding: 1,
resource: {
buffer: outputBuffer,
},
},
],
});
const encoder = t.device.createCommandEncoder();
const pass = encoder.beginComputePass();
pass.setPipeline(pipeline);
pass.setBindGroup(0, bindGroup);
pass.dispatchWorkgroups(1);
pass.end();
t.device.queue.submit([encoder.finish()]);
const idealReadbackData = new ReadbackTypedArray(
rep.componentOrder.map(c => {
const value = rep.decode(componentData)[c];
assert(value !== undefined);
return value;
})
);
if (format === 'rgba8unorm-srgb' || format === 'bgra8unorm-srgb') {
// The SRGB -> float conversion is permitted a tolerance of 0.5f ULP on the SRGB side. The
// procedure for measuring this tolerance is to convert the result back into SRGB space
// using the ideal float -> SRGB conversion specified below but WITHOUT the rounding to integer,
// and taking the floating point difference versus the original SRGB value to yield the error.
// Exact conversion is required: 0.0f and 1.0f (the ends) must be exactly achievable.
const readBackValue = await t.readGPUBufferRangeTyped(outputBuffer, {
type: Float32Array,
typedLength: 4,
});
// Gamma correction shouldn't be applied to the alpha channel.
t.expect(idealReadbackData[3] === readBackValue.data[3]);
// Do the ideal float -> 8unorm-srgb conversion on the readback float value without the rounding
// to integer.
const readBackValueToSRGB = new Array(3);
for (let i = 0; i < 3; ++i) {
const gammaCompressed = gammaCompress(readBackValue.data[i]);
let outputIndex = i;
if (format === 'bgra8unorm-srgb' && (i === 0 || i === 2)) {
outputIndex = 2 - i;
}
readBackValueToSRGB[outputIndex] = floatAsNormalizedIntegerUnquantized(
gammaCompressed,
8,
false
);
}
readBackValue.cleanup();
// Take the floating point difference versus the original SRGB value to yield the error.
const check8UnormSRGB = (inputValue: number, idealValue: number) => {
const kToleranceULP = 0.5;
if (idealValue === 0 || idealValue === 255) {
t.expect(inputValue === idealValue);
} else {
t.expect(Math.abs(inputValue - idealValue) <= kToleranceULP);
}
};
assert(
componentData.R !== undefined &&
componentData.G !== undefined &&
componentData.B !== undefined
);
check8UnormSRGB(readBackValueToSRGB[0], componentData.R);
check8UnormSRGB(readBackValueToSRGB[1], componentData.G);
check8UnormSRGB(readBackValueToSRGB[2], componentData.B);
} else {
t.expectGPUBufferValuesEqual(outputBuffer, idealReadbackData);
}
}
// Make a test parameter by mapping a format and each component to a texel component
// data value.
function makeParam(
format: EncodableTextureFormat,
fn: (bitLength: number, index: number) => number
) {
const rep = kTexelRepresentationInfo[format];
return {
R: rep.componentInfo.R ? fn(rep.componentInfo.R.bitLength, 0) : undefined,
G: rep.componentInfo.G ? fn(rep.componentInfo.G.bitLength, 1) : undefined,
B: rep.componentInfo.B ? fn(rep.componentInfo.B.bitLength, 2) : undefined,
A: rep.componentInfo.A ? fn(rep.componentInfo.A.bitLength, 3) : undefined,
};
}
g.test('unorm_texel_data_in_shader')
.params(u =>
u
.combine('format', kEncodableTextureFormats)
.filter(({ format }) => isCopyDstColorTextureFormatOfType(format, 'unorm'))
.beginSubcases()
.expand('componentData', ({ format }) => {
const max = (bitLength: number) => Math.pow(2, bitLength) - 1;
return [
// Test extrema
makeParam(format, () => 0),
makeParam(format, bitLength => max(bitLength)),
// Test a middle value
makeParam(format, bitLength => Math.floor(max(bitLength) / 2)),
// Test mixed values
makeParam(format, (bitLength, i) => {
const offset = [0.13, 0.63, 0.42, 0.89];
return Math.floor(offset[i] * max(bitLength));
}),
];
})
)
.fn(doTest);
g.test('snorm_texel_data_in_shader')
.params(u =>
u
.combine('format', kEncodableTextureFormats)
.filter(({ format }) => isCopyDstColorTextureFormatOfType(format, 'snorm'))
.beginSubcases()
.expand('componentData', ({ format }) => {
const max = (bitLength: number) => Math.pow(2, bitLength - 1) - 1;
return [
// Test extrema
makeParam(format, () => 0),
makeParam(format, bitLength => max(bitLength)),
makeParam(format, bitLength => -max(bitLength)),
makeParam(format, bitLength => -max(bitLength) - 1),
// Test a middle value
makeParam(format, bitLength => Math.floor(max(bitLength) / 2)),
// Test mixed values
makeParam(format, (bitLength, i) => {
const offset = [0.13, 0.63, 0.42, 0.89];
const range = 2 * max(bitLength);
return -max(bitLength) + Math.floor(offset[i] * range);
}),
];
})
)
.fn(doTest);
g.test('uint_texel_data_in_shader')
.params(u =>
u
.combine('format', kEncodableTextureFormats)
.filter(({ format }) => isCopyDstColorTextureFormatOfType(format, 'uint'))
.beginSubcases()
.expand('componentData', ({ format }) => {
const max = (bitLength: number) => Math.pow(2, bitLength) - 1;
return [
// Test extrema
makeParam(format, () => 0),
makeParam(format, bitLength => max(bitLength)),
// Test a middle value
makeParam(format, bitLength => Math.floor(max(bitLength) / 2)),
// Test mixed values
makeParam(format, (bitLength, i) => {
const offset = [0.13, 0.63, 0.42, 0.89];
return Math.floor(offset[i] * max(bitLength));
}),
];
})
)
.fn(doTest);
g.test('sint_texel_data_in_shader')
.params(u =>
u
.combine('format', kEncodableTextureFormats)
.filter(({ format }) => isCopyDstColorTextureFormatOfType(format, 'sint'))
.beginSubcases()
.expand('componentData', ({ format }) => {
const max = (bitLength: number) => Math.pow(2, bitLength - 1) - 1;
return [
// Test extrema
makeParam(format, () => 0),
makeParam(format, bitLength => max(bitLength)),
makeParam(format, bitLength => -max(bitLength) - 1),
// Test a middle value
makeParam(format, bitLength => Math.floor(max(bitLength) / 2)),
// Test mixed values
makeParam(format, (bitLength, i) => {
const offset = [0.13, 0.63, 0.42, 0.89];
const range = 2 * max(bitLength);
return -max(bitLength) + Math.floor(offset[i] * range);
}),
];
})
)
.fn(doTest);
g.test('float_texel_data_in_shader')
.desc(
`
TODO: Test NaN, Infinity, -Infinity [1]`
)
.params(u =>
u
.combine('format', kEncodableTextureFormats)
.filter(({ format }) => isCopyDstColorTextureFormatOfType(format, 'float'))
.beginSubcases()
.expand('componentData', ({ format }) => {
return [
// Test extrema
makeParam(format, () => 0),
// [1]: Test NaN, Infinity, -Infinity
// Test some values
makeParam(format, () => 0.1199951171875),
makeParam(format, () => 1.4072265625),
makeParam(format, () => 24928),
makeParam(format, () => -0.1319580078125),
makeParam(format, () => -323.25),
makeParam(format, () => -7440),
// Test mixed values
makeParam(format, (bitLength, i) => {
return [24896, -0.1319580078125, -323.25, -234.375][i];
}),
];
})
)
.fn(doTest);
g.test('ufloat_texel_data_in_shader')
.desc(
`
Note: this uses values that are representable by both rg11b10ufloat and rgb9e5ufloat.
TODO: Test NaN, Infinity`
)
.params(u =>
u
.combine('format', kEncodableTextureFormats)
.filter(({ format }) => isCopyDstColorTextureFormatOfType(format, 'ufloat'))
.beginSubcases()
.expand('componentData', ({ format }) => {
return [
// Test extrema
makeParam(format, () => 0),
// Test some values
makeParam(format, () => 128),
makeParam(format, () => 1984),
makeParam(format, () => 3968),
// Test scattered mixed values
makeParam(format, (bitLength, i) => {
return [128, 1984, 3968][i];
}),
// Test mixed values that are close in magnitude.
makeParam(format, (bitLength, i) => {
return [0.05859375, 0.03125, 0.03515625][i];
}),
];
})
)
.fn(doTest);