Skip to content

Commit

Permalink
Refactor texture builtin for 0 vert/frag storage bufs/tex. (#4090)
Browse files Browse the repository at this point in the history
Update textureLoad and textureStore to handle the case where
the device supports 0 storage buffers in fragment and vertex
stages.

Refactor texture.ts copyBufferToTextureViaRender so it doesn't
use a storage buffer. Instead we use a r32float/r32sint/r32uint
1024x??? texture and treat it as a 1d array.
  • Loading branch information
greggman authored Dec 17, 2024
1 parent dedb476 commit b1d51e4
Show file tree
Hide file tree
Showing 7 changed files with 243 additions and 104 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@ import {
sampleTypeForFormatAndAspect,
textureDimensionAndFormatCompatible,
} from '../../../../../format_info.js';
import { MaxLimitsTestMixin } from '../../../../../gpu_test.js';
import { align } from '../../../../../util/math.js';
import { kShaderStages, ShaderStage } from '../../../../validation/decl/util.js';

import { WGSLTextureQueryTest } from './texture_utils.js';

export const g = makeTestGroup(WGSLTextureQueryTest);
export const g = makeTestGroup(MaxLimitsTestMixin(WGSLTextureQueryTest));

/// The maximum number of texture mipmap levels to test.
/// Keep this small to reduce memory and test permutations.
Expand Down Expand Up @@ -485,6 +486,8 @@ Parameters:
t.selectDeviceOrSkipTestCase(info.feature);
})
.fn(t => {
t.skipIfNoStorageTexturesInStage(t.params.stage);

const values = testValues(t.params);
const texture = t.createTextureTracked({
size: values.size,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,6 @@ If an out of bounds access occurs, the built-in function returns one of:
* A vector (0,0,0,0) or (0,0,0,1) of the appropriate type for non-depth textures
* 0.0 for depth textures
TODO: Test textureLoad with depth textures as texture_2d, etc...
TODO: Test textureLoad with multisampled stencil8 format
TODO: Test un-encodable formats.
TODO: Test stencil8 format.
`;

import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
Expand All @@ -33,7 +29,7 @@ import {
kTextureFormatInfo,
textureDimensionAndFormatCompatible,
} from '../../../../../format_info.js';
import { GPUTest } from '../../../../../gpu_test.js';
import { GPUTest, MaxLimitsTestMixin } from '../../../../../gpu_test.js';
import { maxMipLevelCount, virtualMipSize } from '../../../../../util/texture/base.js';
import { TexelFormats } from '../../../../types.js';

Expand All @@ -54,6 +50,8 @@ import {
generateTextureBuiltinInputs3D,
Dimensionality,
createVideoFrameWithRandomDataAndGetTexels,
ShortShaderStage,
isFillable,
} from './texture_utils.js';

export function normalizedCoordToTexelLoadTestCoord<T extends Dimensionality>(
Expand All @@ -69,7 +67,20 @@ export function normalizedCoordToTexelLoadTestCoord<T extends Dimensionality>(
}) as T;
}

export const g = makeTestGroup(GPUTest);
function skipIfStorageTexturesNotSupportedInStage(t: GPUTest, stage: ShortShaderStage) {
if (t.isCompatibility) {
t.skipIf(
stage === 'f' && !(t.device.limits.maxStorageTexturesInFragmentStage! > 0),
'device does not support storage textures in fragment shaders'
);
t.skipIf(
stage === 'v' && !(t.device.limits.maxStorageTexturesInVertexStage! > 0),
'device does not support storage textures in vertex shaders'
);
}
}

export const g = makeTestGroup(MaxLimitsTestMixin(GPUTest));

g.test('sampled_1d')
.specURL('https://www.w3.org/TR/WGSL/#textureload')
Expand Down Expand Up @@ -600,8 +611,7 @@ Parameters:
u
.combine('stage', kShortShaderStages)
.combine('format', kAllTextureFormats)
// MAINTENANCE_TODO: Update createTextureFromTexelViews to support stencil8 and remove this filter.
.filter(t => t.format !== 'stencil8' && !isCompressedFloatTextureFormat(t.format))
.filter(t => isFillable(t.format))
.combine('texture_type', ['texture_2d_array', 'texture_depth_2d_array'] as const)
.filter(
t => !(t.texture_type === 'texture_depth_2d_array' && !isDepthTextureFormat(t.format))
Expand All @@ -626,7 +636,6 @@ Parameters:

// We want at least 4 blocks or something wide enough for 3 mip levels.
const size = chooseTextureSize({ minSize: 8, minBlocks: 4, format, viewDimension: '3d' });

const descriptor: GPUTextureDescriptor = {
format,
size,
Expand Down Expand Up @@ -712,6 +721,8 @@ Parameters:
.fn(async t => {
const { format, stage, samplePoints, C } = t.params;

skipIfStorageTexturesNotSupportedInStage(t, stage);

// We want at least 3 blocks or something wide enough for 3 mip levels.
const [width] = chooseTextureSize({ minSize: 8, minBlocks: 4, format });
const size = [width, 1];
Expand Down Expand Up @@ -791,6 +802,8 @@ Parameters:
.fn(async t => {
const { format, stage, samplePoints, C } = t.params;

skipIfStorageTexturesNotSupportedInStage(t, stage);

// We want at least 3 blocks or something wide enough for 3 mip levels.
const size = chooseTextureSize({ minSize: 8, minBlocks: 3, format });
const descriptor: GPUTextureDescriptor = {
Expand Down Expand Up @@ -871,6 +884,8 @@ Parameters:
.fn(async t => {
const { format, stage, samplePoints, C, A } = t.params;

skipIfStorageTexturesNotSupportedInStage(t, stage);

// We want at least 3 blocks or something wide enough for 3 mip levels.
const size = chooseTextureSize({ minSize: 8, minBlocks: 4, format, viewDimension: '3d' });
const descriptor: GPUTextureDescriptor = {
Expand Down Expand Up @@ -953,6 +968,8 @@ Parameters:
.fn(async t => {
const { format, stage, samplePoints, C } = t.params;

skipIfStorageTexturesNotSupportedInStage(t, stage);

// We want at least 3 blocks or something wide enough for 3 mip levels.
const size = chooseTextureSize({ minSize: 8, minBlocks: 4, format, viewDimension: '3d' });
const descriptor: GPUTextureDescriptor = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Returns the number of layers (elements) of an array texture.

import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
import { kTextureFormatInfo } from '../../../../../format_info.js';
import { MaxLimitsTestMixin } from '../../../../../gpu_test.js';
import { TexelFormats } from '../../../../types.js';
import { kShaderStages } from '../../../../validation/decl/util.js';

Expand Down Expand Up @@ -34,7 +35,7 @@ function getLayerSettingsAndExpected({
};
}

export const g = makeTestGroup(WGSLTextureQueryTest);
export const g = makeTestGroup(MaxLimitsTestMixin(WGSLTextureQueryTest));

g.test('sampled')
.specURL('https://www.w3.org/TR/WGSL/#texturenumlayers')
Expand Down Expand Up @@ -206,6 +207,8 @@ Parameters
.fn(t => {
const { stage, format, access_mode, view_type } = t.params;

t.skipIfNoStorageTexturesInStage(stage);

const texture = t.createTextureTracked({
format,
usage: GPUTextureUsage.STORAGE_BINDING,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ If an out-of-bounds access occurs, the built-in function should not be executed.
import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
import { unreachable, iterRange, range } from '../../../../../../common/util/util.js';
import { kTextureFormatInfo } from '../../../../../format_info.js';
import { GPUTest, TextureTestMixin } from '../../../../../gpu_test.js';
import { GPUTest, MaxLimitsTestMixin, TextureTestMixin } from '../../../../../gpu_test.js';
import {
kFloat32Format,
kFloat16Format,
Expand All @@ -29,7 +29,7 @@ import { TexelFormats } from '../../../../types.js';
const kDims = ['1d', '2d', '3d'] as const;
const kViewDimensions = ['1d', '2d', '2d-array', '3d'] as const;

export const g = makeTestGroup(TextureTestMixin(GPUTest));
export const g = makeTestGroup(TextureTestMixin(MaxLimitsTestMixin(GPUTest)));

// We require a few values that are out of range for a given type
// so we can check clamping behavior.
Expand Down Expand Up @@ -104,6 +104,13 @@ g.test('texel_formats')
const { format, stage, access, viewDimension, _shaderType } = t.params;
const values = inputArray(format);

t.skipIf(
t.isCompatibility &&
stage === 'fragment' &&
t.device.limits.maxStorageTexturesInFragmentStage! < 1,
'device does not support storage textures in fragment shaders'
);

const suffix = format.endsWith('sint') ? 'i' : format.endsWith('uint') ? 'u' : 'f';
const swizzleWGSL = viewDimension === '1d' ? 'x' : viewDimension === '3d' ? 'xyz' : 'xy';
const layerWGSL = viewDimension === '2d-array' ? ', gid.z' : '';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { kShaderStages } from '../../../../validation/decl/util.js';

import {
chooseTextureSize,
convertPerTexelComponentToResultFormat,
createTextureWithRandomDataAndGetTexels,
graphWeights,
isSupportedViewFormatCombo,
Expand Down Expand Up @@ -51,7 +52,9 @@ g.test('createTextureWithRandomDataAndGetTexels_with_generator')
})
.fn(async t => {
const { format, viewDimension } = t.params;
const size = chooseTextureSize({ minSize: 8, minBlocks: 4, format, viewDimension });
// choose an odd size (9) so we're more likely to test alignment issue.
const size = chooseTextureSize({ minSize: 9, minBlocks: 4, format, viewDimension });
t.debug(`size: ${size.map(v => v.toString()).join(', ')}`);
const descriptor: GPUTextureDescriptor = {
format,
dimension: getTextureDimensionFromView(viewDimension),
Expand All @@ -78,9 +81,8 @@ g.test('readTextureToTexelViews')
{ srcFormat: 'rgba32uint', texelViewFormat: 'rgba32uint' },
{ srcFormat: 'rgba32sint', texelViewFormat: 'rgba32sint' },
{ srcFormat: 'depth24plus', texelViewFormat: 'rgba32float' },
{ srcFormat: 'depth24plus', texelViewFormat: 'r32float' },
{ srcFormat: 'depth24plus-stencil8', texelViewFormat: 'r32float' },
{ srcFormat: 'stencil8', texelViewFormat: 'rgba32sint' },
{ srcFormat: 'depth24plus-stencil8', texelViewFormat: 'rgba32float' },
{ srcFormat: 'stencil8', texelViewFormat: 'stencil8' },
] as const)
.combine('viewDimension', ['1d', '2d', '2d-array', '3d', 'cube', 'cube-array'] as const)
.filter(t => isSupportedViewFormatCombo(t.srcFormat, t.viewDimension))
Expand All @@ -96,7 +98,9 @@ g.test('readTextureToTexelViews')
})
.fn(async t => {
const { srcFormat, texelViewFormat, viewDimension, sampleCount } = t.params;
const size = chooseTextureSize({ minSize: 8, minBlocks: 4, format: srcFormat, viewDimension });
// choose an odd size (9) so we're more likely to test alignment issue.
const size = chooseTextureSize({ minSize: 9, minBlocks: 4, format: srcFormat, viewDimension });
t.debug(`size: ${size.map(v => v.toString()).join(', ')}`);
const descriptor: GPUTextureDescriptor = {
format: srcFormat,
dimension: getTextureDimensionFromView(viewDimension),
Expand Down Expand Up @@ -126,26 +130,38 @@ g.test('readTextureToTexelViews')
for (let z = 0; z < mipLevelSize[2]; ++z) {
for (let y = 0; y < mipLevelSize[1]; ++y) {
for (let x = 0; x < mipLevelSize[0]; ++x) {
const actual = actualMipLevelTexelView.color({ x, y, z });
const expected = expectedMipLevelTexelView.color({ x, y, z });
// This currently expects the exact same values in actual vs expected.
// It's possible this needs to be relaxed slightly but only for non-integer formats.
// For now, if the tests pass everywhere, we'll keep it at 0 tolerance.
const maxFractionalDiff = 0;
if (
!texelsApproximatelyEqual(
for (let sampleIndex = 0; sampleIndex < sampleCount; ++sampleIndex) {
const actual = actualMipLevelTexelView.color({ x, y, z, sampleIndex });
const expected = expectedMipLevelTexelView.color({ x, y, z, sampleIndex });

const actualRGBA = convertPerTexelComponentToResultFormat(
actual,
actualMipLevelTexelView.format,
actualMipLevelTexelView.format
);
const expectedRGBA = convertPerTexelComponentToResultFormat(
expected,
expectedMipLevelTexelView.format,
maxFractionalDiff
)
) {
const actualStr = texelFormat(actual, actualRep);
const expectedStr = texelFormat(expected, expectedRep);
errors.push(
`texel at ${x}, ${y}, ${z}, expected: ${expectedStr}, actual: ${actualStr}`
expectedMipLevelTexelView.format
);

// This currently expects the exact same values in actual vs expected.
// It's possible this needs to be relaxed slightly but only for non-integer formats.
// For now, if the tests pass everywhere, we'll keep it at 0 tolerance.
const maxFractionalDiff = 0;
if (
!texelsApproximatelyEqual(
actualRGBA,
actualMipLevelTexelView.format,
expectedRGBA,
expectedMipLevelTexelView.format,
maxFractionalDiff
)
) {
const actualStr = texelFormat(actual, actualRep);
const expectedStr = texelFormat(expected, expectedRep);
errors.push(
`texel at ${x}, ${y}, ${z}, sampleIndex: ${sampleIndex} expected: ${expectedStr}, actual: ${actualStr}`
);
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -787,6 +787,19 @@ function getWeightForMipLevel(
* Used for textureNumSamples, textureNumLevels, textureNumLayers, textureDimension
*/
export class WGSLTextureQueryTest extends GPUTest {
skipIfNoStorageTexturesInStage(stage: ShaderStage) {
if (this.isCompatibility) {
this.skipIf(
stage === 'fragment' && !(this.device.limits.maxStorageTexturesInFragmentStage! > 0),
'device does not support storage textures in fragment shaders'
);
this.skipIf(
stage === 'vertex' && !(this.device.limits.maxStorageTexturesInVertexStage! > 0),
'device does not support storage textures in vertex shaders'
);
}
}

executeAndExpectResult(
stage: ShaderStage,
code: string,
Expand All @@ -795,6 +808,7 @@ export class WGSLTextureQueryTest extends GPUTest {
expected: number[]
) {
const { device } = this;

const returnType = `vec4<u32>`;
const castWGSL = `${returnType}(getValue()${range(4 - expected.length, () => ', 0').join('')})`;
const stageWGSL =
Expand Down Expand Up @@ -1516,7 +1530,7 @@ export interface Texture {
/**
* Converts the src texel representation to an RGBA representation.
*/
function convertPerTexelComponentToResultFormat(
export function convertPerTexelComponentToResultFormat(
src: PerTexelComponent<number>,
format: EncodableTextureFormat
): PerTexelComponent<number> {
Expand Down Expand Up @@ -2223,6 +2237,8 @@ export function texelsApproximatelyEqual(
for (const component of rgbaComponentsToCheck) {
const g = gotRGBA[component]!;
const e = expectRGBA[component]!;
assert(!isNaN(g), () => `got component is NaN: ${g}`);
assert(!isNaN(e), () => `expected component is NaN: ${e}`);
const absDiff = Math.abs(g - e);
const ulpDiff = Math.abs(gULP[component]! - eULP[component]!);
if (ulpDiff > 3 && absDiff > maxFractionalDiff) {
Expand Down Expand Up @@ -2486,7 +2502,7 @@ export async function checkCallResults<T extends Dimensionality>(
` : as texel coord mip level[${mipLevel}]: (${t[0]}, ${t[1]}), face: ${faceNdx}(${kFaceNames[faceNdx]})`
);
}
} else {
} else if (call.coordType === 'f') {
for (let mipLevel = 0; mipLevel < (texture.descriptor.mipLevelCount ?? 1); ++mipLevel) {
const mipSize = virtualMipSize(
texture.descriptor.dimension ?? '2d',
Expand Down Expand Up @@ -3109,12 +3125,15 @@ export async function readTextureToTexelViews(
((coord.z * size[0] * size[1] + coord.y * size[0] + coord.x) * sampleCount +
(coord.sampleIndex ?? 0)) *
4;
return {
R: data[offset + 0],
G: data[offset + 1],
B: data[offset + 2],
A: data[offset + 3],
};
return convertResultFormatToTexelViewFormat(
{
R: data[offset + 0],
G: data[offset + 1],
B: data[offset + 2],
A: data[offset + 3],
},
format
);
})
);
}
Expand Down
Loading

0 comments on commit b1d51e4

Please sign in to comment.