diff --git a/src/webgpu/shader/execution/expression/call/builtin/textureDimensions.spec.ts b/src/webgpu/shader/execution/expression/call/builtin/textureDimensions.spec.ts index b80c8334a55b..cebf3a4dafb2 100644 --- a/src/webgpu/shader/execution/expression/call/builtin/textureDimensions.spec.ts +++ b/src/webgpu/shader/execution/expression/call/builtin/textureDimensions.spec.ts @@ -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. @@ -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, diff --git a/src/webgpu/shader/execution/expression/call/builtin/textureLoad.spec.ts b/src/webgpu/shader/execution/expression/call/builtin/textureLoad.spec.ts index 62378111c0a5..1e64e23b9ad0 100644 --- a/src/webgpu/shader/execution/expression/call/builtin/textureLoad.spec.ts +++ b/src/webgpu/shader/execution/expression/call/builtin/textureLoad.spec.ts @@ -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'; @@ -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'; @@ -54,6 +50,8 @@ import { generateTextureBuiltinInputs3D, Dimensionality, createVideoFrameWithRandomDataAndGetTexels, + ShortShaderStage, + isFillable, } from './texture_utils.js'; export function normalizedCoordToTexelLoadTestCoord( @@ -69,7 +67,20 @@ export function normalizedCoordToTexelLoadTestCoord( }) 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') @@ -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)) @@ -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, @@ -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]; @@ -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 = { @@ -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 = { @@ -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 = { diff --git a/src/webgpu/shader/execution/expression/call/builtin/textureNumLayers.spec.ts b/src/webgpu/shader/execution/expression/call/builtin/textureNumLayers.spec.ts index 0cb94b798a5d..9ce7952c1272 100644 --- a/src/webgpu/shader/execution/expression/call/builtin/textureNumLayers.spec.ts +++ b/src/webgpu/shader/execution/expression/call/builtin/textureNumLayers.spec.ts @@ -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'; @@ -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') @@ -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, diff --git a/src/webgpu/shader/execution/expression/call/builtin/textureStore.spec.ts b/src/webgpu/shader/execution/expression/call/builtin/textureStore.spec.ts index 1dc7f8139d79..6500b9ac3b5f 100644 --- a/src/webgpu/shader/execution/expression/call/builtin/textureStore.spec.ts +++ b/src/webgpu/shader/execution/expression/call/builtin/textureStore.spec.ts @@ -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, @@ -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. @@ -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' : ''; diff --git a/src/webgpu/shader/execution/expression/call/builtin/texture_utils.spec.ts b/src/webgpu/shader/execution/expression/call/builtin/texture_utils.spec.ts index 483b8f36a456..3923e7d39092 100644 --- a/src/webgpu/shader/execution/expression/call/builtin/texture_utils.spec.ts +++ b/src/webgpu/shader/execution/expression/call/builtin/texture_utils.spec.ts @@ -16,6 +16,7 @@ import { kShaderStages } from '../../../../validation/decl/util.js'; import { chooseTextureSize, + convertPerTexelComponentToResultFormat, createTextureWithRandomDataAndGetTexels, graphWeights, isSupportedViewFormatCombo, @@ -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), @@ -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)) @@ -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), @@ -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}` + ); + } } } } diff --git a/src/webgpu/shader/execution/expression/call/builtin/texture_utils.ts b/src/webgpu/shader/execution/expression/call/builtin/texture_utils.ts index b9013281e2cc..11f4e3676126 100644 --- a/src/webgpu/shader/execution/expression/call/builtin/texture_utils.ts +++ b/src/webgpu/shader/execution/expression/call/builtin/texture_utils.ts @@ -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, @@ -795,6 +808,7 @@ export class WGSLTextureQueryTest extends GPUTest { expected: number[] ) { const { device } = this; + const returnType = `vec4`; const castWGSL = `${returnType}(getValue()${range(4 - expected.length, () => ', 0').join('')})`; const stageWGSL = @@ -1516,7 +1530,7 @@ export interface Texture { /** * Converts the src texel representation to an RGBA representation. */ -function convertPerTexelComponentToResultFormat( +export function convertPerTexelComponentToResultFormat( src: PerTexelComponent, format: EncodableTextureFormat ): PerTexelComponent { @@ -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) { @@ -2486,7 +2502,7 @@ export async function checkCallResults( ` : 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', @@ -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 + ); }) ); } diff --git a/src/webgpu/util/texture.ts b/src/webgpu/util/texture.ts index 508854943a5b..373b495ddd59 100644 --- a/src/webgpu/util/texture.ts +++ b/src/webgpu/util/texture.ts @@ -1,4 +1,4 @@ -import { assert } from '../../common/util/util.js'; +import { assert, unreachable } from '../../common/util/util.js'; import { isDepthOrStencilTextureFormat, isDepthTextureFormat, @@ -27,28 +27,28 @@ const kLoadValueFromStorageInfo: Partial<{ storageType: 'u32', texelType: 'vec4f', unpackWGSL: ` - return vec4f(unpack4x8unorm(src[byteOffset / 4])[byteOffset % 4], 0.123, 0.123, 0.123) + return vec4f(unpack4x8unorm(getSrc(byteOffset / 4))[byteOffset % 4], 0.123, 0.123, 0.123) `, }, r8uint: { storageType: 'u32', texelType: 'vec4u', unpackWGSL: ` - return vec4u(unpack4xU8(src[byteOffset / 4])[byteOffset % 4], 123, 123, 123) + return vec4u(unpack4xU8(getSrc(byteOffset / 4))[byteOffset % 4], 123, 123, 123) `, }, r8sint: { storageType: 'u32', texelType: 'vec4i', unpackWGSL: ` - return vec4i(unpack4xI8(src[byteOffset / 4])[byteOffset % 4], 123, 123, 123) + return vec4i(unpack4xI8(getSrc(byteOffset / 4))[byteOffset % 4], 123, 123, 123) `, }, rg8unorm: { storageType: 'u32', texelType: 'vec4f', unpackWGSL: ` - let v = unpack4x8unorm(src[byteOffset / 4]); + let v = unpack4x8unorm(getSrc(byteOffset / 4)); return vec4f(select(v.rg, v.ba, byteOffset % 4 >= 2), 0.123, 0.123) `, }, @@ -56,7 +56,7 @@ const kLoadValueFromStorageInfo: Partial<{ storageType: 'u32', texelType: 'vec4u', unpackWGSL: ` - let v = unpack4xU8(src[byteOffset / 4]); + let v = unpack4xU8(getSrc(byteOffset / 4)); return vec4u(select(v.rg, v.ba, byteOffset % 4 >= 2), 123, 123) `, }, @@ -64,20 +64,20 @@ const kLoadValueFromStorageInfo: Partial<{ storageType: 'u32', texelType: 'vec4i', unpackWGSL: ` - let v = unpack4xI8(src[byteOffset / 4]); + let v = unpack4xI8(getSrc(byteOffset / 4)); return vec4i(select(v.rg, v.ba, byteOffset % 4 >= 2), 123, 123) `, }, rgba8unorm: { storageType: 'u32', texelType: 'vec4f', - unpackWGSL: 'return unpack4x8unorm(src[byteOffset / 4])', + unpackWGSL: 'return unpack4x8unorm(getSrc(byteOffset / 4))', }, 'rgba8unorm-srgb': { storageType: 'u32', texelType: 'vec4f', unpackWGSL: ` - let v = unpack4x8unorm(src[byteOffset / 4]); + let v = unpack4x8unorm(getSrc(byteOffset / 4)); let srgb = select( v / 12.92, pow((v + 0.055) / 1.055, vec4f(2.4)), @@ -89,13 +89,13 @@ const kLoadValueFromStorageInfo: Partial<{ bgra8unorm: { storageType: 'u32', texelType: 'vec4f', - unpackWGSL: 'return unpack4x8unorm(src[byteOffset / 4]).bgra', + unpackWGSL: 'return unpack4x8unorm(getSrc(byteOffset / 4)).bgra', }, 'bgra8unorm-srgb': { storageType: 'u32', texelType: 'vec4f', unpackWGSL: ` - let v = unpack4x8unorm(src[byteOffset / 4]); + let v = unpack4x8unorm(getSrc(byteOffset / 4)); let srgb = select( v / 12.92, pow((v + 0.055) / 1.055, vec4f(2.4)), @@ -107,41 +107,41 @@ const kLoadValueFromStorageInfo: Partial<{ rgba8uint: { storageType: 'u32', texelType: 'vec4u', - unpackWGSL: 'return unpack4xU8(src[byteOffset / 4])', + unpackWGSL: 'return unpack4xU8(getSrc(byteOffset / 4))', }, rgba8sint: { storageType: 'u32', texelType: 'vec4i', - unpackWGSL: 'return unpack4xI8(src[byteOffset / 4])', + unpackWGSL: 'return unpack4xI8(getSrc(byteOffset / 4))', }, r16float: { storageType: 'u32', texelType: 'vec4f', unpackWGSL: - 'return vec4f(unpack2x16float(src[byteOffset / 4])[byteOffset % 4 / 2], 0.123, 0.123, 0.123)', + 'return vec4f(unpack2x16float(getSrc(byteOffset / 4))[byteOffset % 4 / 2], 0.123, 0.123, 0.123)', }, r16uint: { storageType: 'u32', texelType: 'vec4u', unpackWGSL: - 'return vec4u(extractBits(src[byteOffset / 4], (byteOffset % 4 / 2 * 16), 16), 123, 123, 123)', + 'return vec4u(extractBits(getSrc(byteOffset / 4), (byteOffset % 4 / 2 * 16), 16), 123, 123, 123)', }, r16sint: { storageType: 'i32', texelType: 'vec4i', unpackWGSL: - 'return vec4i(extractBits(src[byteOffset / 4], byteOffset % 4 / 2 * 16, 16), 123, 123, 123)', + 'return vec4i(extractBits(getSrc(byteOffset / 4), byteOffset % 4 / 2 * 16, 16), 123, 123, 123)', }, rg16float: { storageType: 'u32', texelType: 'vec4f', - unpackWGSL: 'return vec4f(unpack2x16float(src[byteOffset / 4]), 0.123, 0.123)', + unpackWGSL: 'return vec4f(unpack2x16float(getSrc(byteOffset / 4)), 0.123, 0.123)', }, rg16uint: { storageType: 'u32', texelType: 'vec4u', unpackWGSL: ` - let v = src[byteOffset / 4]; + let v = getSrc(byteOffset / 4); return vec4u(v & 0xFFFF, v >> 16, 123, 123) `, }, @@ -149,7 +149,7 @@ const kLoadValueFromStorageInfo: Partial<{ storageType: 'i32', texelType: 'vec4i', unpackWGSL: ` - let v = src[byteOffset / 4]; + let v = getSrc(byteOffset / 4); return vec4i( extractBits(v, 0, 16), extractBits(v, 16, 16), @@ -161,16 +161,16 @@ const kLoadValueFromStorageInfo: Partial<{ texelType: 'vec4f', unpackWGSL: ` return vec4f( - unpack2x16float(src[byteOffset / 4]), - unpack2x16float(src[byteOffset / 4 + 1])) + unpack2x16float(getSrc(byteOffset / 4)), + unpack2x16float(getSrc(byteOffset / 4 + 1))) `, }, rgba16uint: { storageType: 'u32', texelType: 'vec4u', unpackWGSL: ` - let v0 = src[byteOffset / 4]; - let v1 = src[byteOffset / 4 + 1]; + let v0 = getSrc(byteOffset / 4); + let v1 = getSrc(byteOffset / 4 + 1); return vec4u(v0 & 0xFFFF, v0 >> 16, v1 & 0xFFFF, v1 >> 16) `, }, @@ -178,8 +178,8 @@ const kLoadValueFromStorageInfo: Partial<{ storageType: 'i32', texelType: 'vec4i', unpackWGSL: ` - let v0 = src[byteOffset / 4]; - let v1 = src[byteOffset / 4 + 1]; + let v0 = getSrc(byteOffset / 4); + let v1 = getSrc(byteOffset / 4 + 1); return vec4i( extractBits(v0, 0, 16), extractBits(v0, 16, 16), @@ -191,13 +191,13 @@ const kLoadValueFromStorageInfo: Partial<{ r32float: { storageType: 'f32', texelType: 'vec4f', - unpackWGSL: 'return vec4f(src[byteOffset / 4], 0.123, 0.123, 0.123)', + unpackWGSL: 'return vec4f(getSrc(byteOffset / 4), 0.123, 0.123, 0.123)', }, rgb10a2uint: { storageType: 'u32', texelType: 'vec4u', unpackWGSL: ` - let v = src[byteOffset / 4]; + let v = getSrc(byteOffset / 4); return vec4u( extractBits(v, 0, 10), extractBits(v, 10, 10), @@ -210,7 +210,7 @@ const kLoadValueFromStorageInfo: Partial<{ storageType: 'u32', texelType: 'vec4f', unpackWGSL: ` - let v = src[byteOffset / 4]; + let v = getSrc(byteOffset / 4); return vec4f( f32(extractBits(v, 0, 10)) / f32(0x3FF), f32(extractBits(v, 10, 10)) / f32(0x3FF), @@ -223,7 +223,7 @@ const kLoadValueFromStorageInfo: Partial<{ storageType: 'u32', texelType: 'vec4f', unpackWGSL: ` - let v = unpack2x16unorm(src[byteOffset / 4])[byteOffset % 4 / 2]; + let v = unpack2x16unorm(getSrc(byteOffset / 4))[byteOffset % 4 / 2]; return vec4f(v, 0.123, 0.123, 0.123) `, }, @@ -231,7 +231,7 @@ const kLoadValueFromStorageInfo: Partial<{ storageType: 'f32', texelType: 'vec4f', unpackWGSL: ` - let v = src[byteOffset / 4]; + let v = getSrc(byteOffset / 4); return vec4f(v, 0.123, 0.123, 0.123) `, }, @@ -239,7 +239,7 @@ const kLoadValueFromStorageInfo: Partial<{ storageType: 'u32', texelType: 'vec4u', unpackWGSL: ` - return vec4u(unpack4xU8(src[byteOffset / 4])[byteOffset % 4], 123, 123, 123) + return vec4u(unpack4xU8(getSrc(byteOffset / 4))[byteOffset % 4], 123, 123, 123) `, }, }; @@ -267,12 +267,13 @@ function getCopyBufferToTextureViaRenderCode( const stencilCode = discardWithStencil ? 'if ((fs.v.r & vin.stencilMask) == 0) { discard; }' : ''; - return ` + const code = ` struct Uniforms { numTexelRows: u32, bytesPerRow: u32, bytesPerSample: u32, sampleCount: u32, + offset: u32, }; struct VSOutput { @@ -297,7 +298,15 @@ function getCopyBufferToTextureViaRenderCode( } @group(0) @binding(0) var uni: Uniforms; - @group(0) @binding(1) var src: array<${storageType}>; + @group(0) @binding(1) var src: texture_2d<${storageType}>; + + // get a u32/i32/f32 from a r32uint/r32sint/r32float as though it was 1d array + fn getSrc(offset: u32) -> ${storageType} { + let width = textureDimensions(src, 0).x; + let x = offset % width; + let y = offset / width; + return textureLoad(src, vec2u(x, y), 0).r; + } fn unpack(byteOffset: u32) -> ${texelType} { ${unpackWGSL}; @@ -311,6 +320,7 @@ function getCopyBufferToTextureViaRenderCode( @fragment fn fs(vin: VSOutput) -> FSOutput { let coord = vec2u(vin.pos.xy); let byteOffset = + uni.offset + coord.y * uni.bytesPerRow + (coord.x * uni.sampleCount + vin.sampleIndex) * uni.bytesPerSample; var fs: FSOutput; @@ -320,6 +330,22 @@ function getCopyBufferToTextureViaRenderCode( return fs; } `; + + let dataFormat: GPUTextureFormat; + switch (storageType) { + case 'f32': + dataFormat = 'r32float'; + break; + case 'i32': + dataFormat = 'r32sint'; + break; + case 'u32': + dataFormat = 'r32uint'; + break; + default: + unreachable(); + } + return { code, dataFormat }; } const s_copyBufferToTextureViaRenderPipelines = new WeakMap< @@ -327,6 +353,14 @@ const s_copyBufferToTextureViaRenderPipelines = new WeakMap< Map >(); +// This function emulates copyBufferToTexture by by rendering into the texture. +// This is for formats that can't be copied to directly. depth textures, stencil +// textures, multisampled textures. +// +// For source data it creates an r32uint/r32sint/r32float texture +// and copies the source buffer into it and then reads the texture +// as a 1d array. It does this because compat mode might not have +// storage buffers in fragment shaders. function copyBufferToTextureViaRender( t: GPUTestBase, encoder: GPUCommandEncoder, @@ -339,11 +373,15 @@ function copyBufferToTextureViaRender( const origin = reifyOrigin3D(dest.origin ?? [0]); const copySize = reifyExtent3D(size); const { useFragDepth, discardWithStencil } = getDepthStencilOptionsForFormat(dest.texture.format); + const resourcesToDestroy: (GPUTexture | GPUBuffer)[] = []; const { device } = t; const numBlits = discardWithStencil ? 8 : 1; for (let blitCount = 0; blitCount < numBlits; ++blitCount) { - const code = getCopyBufferToTextureViaRenderCode(sourceFormat, dest.texture.format); + const { code, dataFormat } = getCopyBufferToTextureViaRenderCode( + sourceFormat, + dest.texture.format + ); const stencilWriteMask = 1 << blitCount; const id = JSON.stringify({ textureFormat, @@ -406,22 +444,40 @@ function copyBufferToTextureViaRender( pipelines.set(id, pipeline); } - const info = kTextureFormatInfo[sourceFormat]; - const uniforms = new Uint32Array([ - copySize.height, // numTexelRows: u32, - source.bytesPerRow!, // bytesPerRow: u32, - info.bytesPerBlock!, // bytesPerSample: u32, - dest.texture.sampleCount, // sampleCount: u32, - ]); - const uniformBuffer = t.makeBufferWithContents( - uniforms, - GPUBufferUsage.COPY_DST | GPUBufferUsage.UNIFORM - ); - const storageBuffer = t.createBufferTracked({ - size: source.buffer.size, - usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.STORAGE, + const width = 1024; + const bytesPerRow = width * 4; + const fullRows = Math.floor(source.buffer.size / bytesPerRow); + const rows = Math.ceil(source.buffer.size / bytesPerRow); + const srcTexture = t.createTextureTracked({ + format: dataFormat, + size: [width, rows], + usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING, }); - encoder.copyBufferToBuffer(source.buffer, 0, storageBuffer, 0, storageBuffer.size); + resourcesToDestroy.push(srcTexture); + + if (fullRows > 0) { + encoder.copyBufferToTexture({ buffer: source.buffer, bytesPerRow }, { texture: srcTexture }, [ + width, + fullRows, + ]); + } + if (rows > fullRows) { + const totalPixels = source.buffer.size / 4; + const pixelsCopied = fullRows * width; + const pixelsInLastRow = totalPixels - pixelsCopied; + encoder.copyBufferToTexture( + { + buffer: source.buffer, + offset: pixelsCopied * 4, + bytesPerRow, + }, + { + texture: srcTexture, + origin: [0, fullRows], + }, + [pixelsInLastRow, 1] + ); + } const baseMipLevel = dest.mipLevel; for (let l = 0; l < copySize.depthOrArrayLayers; ++l) { const baseArrayLayer = origin.z + l; @@ -477,13 +533,27 @@ function copyBufferToTextureViaRender( pass.setViewport(origin.x, origin.y, copySize.width, copySize.height, 0, 1); pass.setPipeline(pipeline); + const info = kTextureFormatInfo[sourceFormat]; const offset = (source.offset ?? 0) + (source.bytesPerRow ?? 0) * (source.rowsPerImage ?? 0) * l; + const uniforms = new Uint32Array([ + copySize.height, // numTexelRows: u32, + source.bytesPerRow!, // bytesPerRow: u32, + info.bytesPerBlock!, // bytesPerSample: u32, + dest.texture.sampleCount, // sampleCount: u32, + offset, // offset: u32, + ]); + + const uniformBuffer = t.makeBufferWithContents( + uniforms, + GPUBufferUsage.COPY_DST | GPUBufferUsage.UNIFORM + ); + resourcesToDestroy.push(uniformBuffer); const bindGroup = device.createBindGroup({ layout: pipeline.getBindGroupLayout(0), entries: [ { binding: 0, resource: { buffer: uniformBuffer } }, - { binding: 1, resource: { buffer: storageBuffer, offset } }, + { binding: 1, resource: srcTexture.createView() }, ], }); @@ -493,6 +563,8 @@ function copyBufferToTextureViaRender( pass.end(); } } + + return resourcesToDestroy; } /** @@ -521,7 +593,7 @@ export function createTextureFromTexelViews( // Copy the texel view into each mip level layer. const commandEncoder = t.device.createCommandEncoder(); - const stagingBuffers = []; + const resourcesToDestroy: (GPUTexture | GPUBuffer)[] = []; for (let mipLevel = 0; mipLevel < texelViews.length; mipLevel++) { const { bytesPerRow, @@ -542,7 +614,7 @@ export function createTextureFromTexelViews( size: bytesPerRow * mipHeight * mipDepthOrArray, usage: GPUBufferUsage.COPY_SRC, }); - stagingBuffers.push(stagingBuffer); + resourcesToDestroy.push(stagingBuffer); // Write the texels into the staging buffer. texelViews[mipLevel].writeTextureData(new Uint8Array(stagingBuffer.getMappedRange()), { @@ -559,13 +631,15 @@ export function createTextureFromTexelViews( texture.sampleCount > 1 || isDepthOrStencilTextureFormat(textureFormat) ) { - copyBufferToTextureViaRender( - t, - commandEncoder, - { buffer: stagingBuffer, bytesPerRow, rowsPerImage }, - viewsFormat, - { texture, mipLevel }, - [mipWidth, mipHeight, mipDepthOrArray] + resourcesToDestroy.push( + ...copyBufferToTextureViaRender( + t, + commandEncoder, + { buffer: stagingBuffer, bytesPerRow, rowsPerImage }, + viewsFormat, + { texture, mipLevel }, + [mipWidth, mipHeight, mipDepthOrArray] + ) ); } else { // Copy from the staging buffer into the texture. @@ -578,8 +652,8 @@ export function createTextureFromTexelViews( } t.device.queue.submit([commandEncoder.finish()]); - // Cleanup the staging buffers. - stagingBuffers.forEach(value => value.destroy()); + // Cleanup temp buffers and textures. + resourcesToDestroy.forEach(value => value.destroy()); return texture; }