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 079e82b66cc0..3cbe7f7fbe2d 100644 --- a/src/webgpu/shader/execution/expression/call/builtin/textureLoad.spec.ts +++ b/src/webgpu/shader/execution/expression/call/builtin/textureLoad.spec.ts @@ -25,6 +25,8 @@ import { isDepthTextureFormat, kCompressedTextureFormats, kEncodableTextureFormats, + kTextureFormatInfo, + textureDimensionAndFormatCompatible, } from '../../../../../format_info.js'; import { GPUTest } from '../../../../../gpu_test.js'; import { @@ -34,6 +36,7 @@ import { pack4x8unorm, pack4x8snorm, } from '../../../../../util/conversion.js'; +import { maxMipLevelCount } from '../../../../../util/texture/base.js'; import { TexelFormats } from '../../../../types.js'; import { @@ -44,14 +47,19 @@ import { doTextureCalls, appendComponentTypeForFormatToTextureType, vec2, + vec1, + vec3, } from './texture_utils.js'; import { Boundary, + LayerSpec, LevelSpec, generateCoordBoundaries, getCoordinateForBoundaries, + getLayerFromLayerSpec, getMipLevelFromLevelSpec, isBoundaryNegative, + isLayerSpecNegative, isLevelSpecNegative, } from './utils.js'; @@ -65,8 +73,14 @@ function filterOutU32WithNegativeValues(t: { C: 'i32' | 'u32'; level: LevelSpec; coordsBoundary: Boundary; + array_index?: LayerSpec; }) { - return t.C === 'i32' || (!isLevelSpecNegative(t.level) && !isBoundaryNegative(t.coordsBoundary)); + return ( + t.C === 'i32' || + (!isLevelSpecNegative(t.level) && + !isBoundaryNegative(t.coordsBoundary) && + !isLayerSpecNegative(t.array_index ?? 0)) + ); } export const g = makeTestGroup(GPUTest); @@ -87,11 +101,64 @@ Parameters: ) .params(u => u + .combine('format', kTestableColorFormats) + .filter(t => textureDimensionAndFormatCompatible('1d', t.format)) + // 1d textures can't have a height !== 1 + .filter(t => kTextureFormatInfo[t.format].blockHeight === 1) + .beginSubcases() .combine('C', ['i32', 'u32'] as const) - .combine('coords', generateCoordBoundaries(1)) - .combine('level', [-1, 0, `numlevels-1`, `numlevels`] as const) + .combine('L', ['i32', 'u32'] as const) + .combine('coordsBoundary', generateCoordBoundaries(1)) + .combine('level', [-1, 0, `numLevels-1`, `numLevels`] as const) + // Only test level out of bounds if coordBoundary is in-bounds + .filter(t => !(t.level !== 0 && t.coordsBoundary !== 'in-bounds')) + .filter(filterOutU32WithNegativeValues) ) - .unimplemented(); + .beforeAllSubcases(t => { + const { format } = t.params; + t.skipIfTextureFormatNotSupported(format); + t.selectDeviceForTextureFormatOrSkipTestCase(t.params.format); + }) + .fn(async t => { + const { format, C, L, coordsBoundary, level } = t.params; + + // We want at least 4 blocks or something wide enough for 3 mip levels. + const [width] = chooseTextureSize({ minSize: 8, minBlocks: 4, format }); + const size = [width, 1]; + + const descriptor: GPUTextureDescriptor = { + format, + dimension: '1d', + size, + usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING, + }; + const { texels, texture } = await createTextureWithRandomDataAndGetTexels(t, descriptor); + const mipLevel = getMipLevelFromLevelSpec(texture.mipLevelCount, level); + const coords = getCoordinateForBoundaries(texture, mipLevel, coordsBoundary); + + const calls: TextureCall[] = [ + { + builtin: 'textureLoad', + coordType: C === 'i32' ? 'i' : 'u', + levelType: L === 'i32' ? 'i' : 'u', + mipLevel, + coords, + }, + ]; + const textureType = appendComponentTypeForFormatToTextureType('texture_1d', texture.format); + const viewDescriptor = {}; + const sampler = undefined; + const results = await doTextureCalls(t, texture, viewDescriptor, textureType, sampler, calls); + const res = await checkCallResults( + t, + { texels, descriptor, viewDescriptor }, + textureType, + sampler, + calls, + results + ); + t.expectOK(res); + }); g.test('sampled_2d') .specURL('https://www.w3.org/TR/WGSL/#textureload') @@ -117,6 +184,8 @@ Parameters: .combine('L', ['i32', 'u32'] as const) .combine('coordsBoundary', generateCoordBoundaries(2)) .combine('level', [-1, 0, `numLevels-1`, `numLevels`] as const) + // Only test level out of bounds if coordBoundary is in-bounds + .filter(t => !(t.level !== 0 && t.coordsBoundary !== 'in-bounds')) .filter(filterOutU32WithNegativeValues) ) .beforeAllSubcases(t => { @@ -128,12 +197,13 @@ Parameters: const { format, C, L, coordsBoundary, level } = t.params; // We want at least 4 blocks or something wide enough for 3 mip levels. - const [width, height] = chooseTextureSize({ minSize: 8, minBlocks: 4, format }); + const size = chooseTextureSize({ minSize: 8, minBlocks: 4, format }); const descriptor: GPUTextureDescriptor = { format, - size: { width, height }, + size, usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING, + mipLevelCount: maxMipLevelCount({ size }), }; const { texels, texture } = await createTextureWithRandomDataAndGetTexels(t, descriptor); const mipLevel = getMipLevelFromLevelSpec(texture.mipLevelCount, level); @@ -179,11 +249,62 @@ Parameters: ) .params(u => u + .combine('format', kTestableColorFormats) + .filter(t => textureDimensionAndFormatCompatible('3d', t.format)) + .beginSubcases() .combine('C', ['i32', 'u32'] as const) - .combine('coords', generateCoordBoundaries(3)) + .combine('L', ['i32', 'u32'] as const) + .combine('coordsBoundary', generateCoordBoundaries(3)) .combine('level', [-1, 0, `numLevels-1`, `numLevels`] as const) + // Only test level out of bounds if coordBoundary is in-bounds + .filter(t => !(t.level !== 0 && t.coordsBoundary !== 'in-bounds')) + .filter(filterOutU32WithNegativeValues) ) - .unimplemented(); + .beforeAllSubcases(t => { + const { format } = t.params; + t.skipIfTextureFormatNotSupported(format); + t.selectDeviceForTextureFormatOrSkipTestCase(t.params.format); + }) + .fn(async t => { + const { format, C, L, coordsBoundary, level } = t.params; + + // 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, + dimension: '3d', + size, + usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING, + mipLevelCount: maxMipLevelCount({ size }), + }; + const { texels, texture } = await createTextureWithRandomDataAndGetTexels(t, descriptor); + const mipLevel = getMipLevelFromLevelSpec(texture.mipLevelCount, level); + const coords = getCoordinateForBoundaries(texture, mipLevel, coordsBoundary); + + const calls: TextureCall[] = [ + { + builtin: 'textureLoad', + coordType: C === 'i32' ? 'i' : 'u', + levelType: L === 'i32' ? 'i' : 'u', + mipLevel, + coords, + }, + ]; + const textureType = appendComponentTypeForFormatToTextureType('texture_3d', texture.format); + const viewDescriptor = {}; + const sampler = undefined; + const results = await doTextureCalls(t, texture, viewDescriptor, textureType, sampler, calls); + const res = await checkCallResults( + t, + { texels, descriptor, viewDescriptor }, + textureType, + sampler, + calls, + results + ); + t.expectOK(res); + }); g.test('multisampled') .specURL('https://www.w3.org/TR/WGSL/#textureload') @@ -259,8 +380,8 @@ g.test('arrayed') ` C is i32 or u32 -fn textureLoad(t: texture_2d_array, coords: vec2, array_index: C, level: C) -> vec4 -fn textureLoad(t: texture_depth_2d_array, coords: vec2, array_index: C, level: C) -> f32 +fn textureLoad(t: texture_2d_array, coords: vec2, array_index: A, level: L) -> vec4 +fn textureLoad(t: texture_depth_2d_array, coords: vec2, array_index: A, level: L) -> f32 Parameters: * t: The sampled texture to read from @@ -271,14 +392,73 @@ Parameters: ) .params(u => u + .combine('format', kTestableColorFormats) + // MAINTENANCE_TODO: Update createTextureFromTexelViews to support depth32float and remove this filter. + .filter(t => t.format !== 'depth32float' && !isCompressedFloatTextureFormat(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)) + ) .beginSubcases() .combine('C', ['i32', 'u32'] as const) - .combine('coords', generateCoordBoundaries(2)) - .combine('array_index', [-1, 0, `numlayers-1`, `numlayers`] as const) + .combine('A', ['i32', 'u32'] as const) + .combine('L', ['i32', 'u32'] as const) + .combine('coordsBoundary', generateCoordBoundaries(3)) + .combine('array_index', [-1, 0, `numLayers-1`, `numLayers`] as const) + // Only test array_index out of bounds if coordBoundary is in bounds + .filter(t => !(t.array_index !== 0 && t.coordsBoundary !== 'in-bounds')) .combine('level', [-1, 0, `numLevels-1`, `numLevels`] as const) + // Only test level out of bounds if coordBoundary and array_index are in bounds + .filter(t => !(t.level !== 0 && (t.coordsBoundary !== 'in-bounds' || t.array_index !== 0))) + .filter(filterOutU32WithNegativeValues) ) - .unimplemented(); + .beforeAllSubcases(t => { + const { format } = t.params; + t.skipIfTextureFormatNotSupported(format); + t.selectDeviceForTextureFormatOrSkipTestCase(t.params.format); + }) + .fn(async t => { + const { texture_type, format, C, A, L, coordsBoundary, level, array_index } = t.params; + + // 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, + usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING, + mipLevelCount: maxMipLevelCount({ size }), + }; + const { texels, texture } = await createTextureWithRandomDataAndGetTexels(t, descriptor); + const mipLevel = getMipLevelFromLevelSpec(texture.mipLevelCount, level); + const arrayIndex = getLayerFromLayerSpec(texture.depthOrArrayLayers, array_index); + const coords = getCoordinateForBoundaries(texture, mipLevel, coordsBoundary); + + const calls: TextureCall[] = [ + { + builtin: 'textureLoad', + coordType: C === 'i32' ? 'i' : 'u', + levelType: L === 'i32' ? 'i' : 'u', + arrayIndexType: A === 'i32' ? 'i' : 'u', + arrayIndex, + mipLevel, + coords, + }, + ]; + const textureType = appendComponentTypeForFormatToTextureType(texture_type, texture.format); + const viewDescriptor = {}; + const sampler = undefined; + const results = await doTextureCalls(t, texture, viewDescriptor, textureType, sampler, calls); + const res = await checkCallResults( + t, + { texels, descriptor, viewDescriptor }, + textureType, + sampler, + calls, + results + ); + t.expectOK(res); + }); // Returns texel values to use as inputs for textureLoad. // Values are kept simple to avoid rounding issues. 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 831a05f4d1ba..7741884bf264 100644 --- a/src/webgpu/shader/execution/expression/call/builtin/texture_utils.ts +++ b/src/webgpu/shader/execution/expression/call/builtin/texture_utils.ts @@ -166,7 +166,9 @@ function getTextureFormatTypeInfo(format: GPUTextureFormat) { * eg: `getTextureType('texture_2d', someUnsignedIntTextureFormat)` -> `texture_2d` */ export function appendComponentTypeForFormatToTextureType(base: string, format: GPUTextureFormat) { - return `${base}<${getTextureFormatTypeInfo(format).componentType}>`; + return base.includes('depth') + ? base + : `${base}<${getTextureFormatTypeInfo(format).componentType}>`; } /** @@ -217,8 +219,8 @@ export type Dimensionality = vec1 | vec2 | vec3; type TextureCallArgKeys = keyof TextureCallArgs; const kTextureCallArgNames: TextureCallArgKeys[] = [ 'coords', - 'mipLevel', 'arrayIndex', + 'mipLevel', 'ddx', 'ddy', 'offset', @@ -238,6 +240,7 @@ export interface TextureCall extends TextureCallArgs = {}; + for (const component of components) { + out[component] = 0; + } + return out; +} + /** * Returns the expect value for a WGSL builtin texture function for a single * mip level @@ -364,7 +375,7 @@ export function softwareTextureReadMipLevel( texture.texels[mipLevel].color({ x: Math.floor(at[0]), y: Math.floor(at[1] ?? 0), - z: Math.floor(at[2] ?? 0), + z: call.arrayIndex ?? Math.floor(at[2] ?? 0), }); const isCube = texture.viewDescriptor.dimension === 'cube'; @@ -513,8 +524,10 @@ export function softwareTextureReadMipLevel( return convertPerTexelComponentToResultFormat(out, format); } case 'textureLoad': { - const c = applyAddressModesToCoords(addressMode, textureSize, call.coords!); - return convertPerTexelComponentToResultFormat(load(c), format); + const out: PerTexelComponent = isOutOfBoundsCall(texture, call) + ? zeroValuePerTexelComponent(rep.componentOrder) + : load(call.coords!); + return convertPerTexelComponentToResultFormat(out, format); } } } @@ -780,7 +793,9 @@ export async function checkCallResults( const relDiff = absDiff / Math.max(Math.abs(g), Math.abs(e)); if (ulpDiff > 3 && absDiff > maxFractionalDiff) { const desc = describeTextureCall(call); + const size = reifyExtent3D(texture.descriptor.size); errs.push(`component was not as expected: + size: [${size.width}, ${size.height}, ${size.depthOrArrayLayers}] call: ${desc} // #${callIdx} component: ${component} got: ${g} @@ -2194,7 +2209,12 @@ function buildBinnedCalls(calls: TextureCall[]) { if (name === 'offset') { args.push(`/* offset */ ${wgslExpr(value)}`); } else { - const type = name === 'mipLevel' ? prototype.levelType! : prototype.coordType; + const type = + name === 'mipLevel' + ? prototype.levelType! + : name === 'arrayIndex' + ? prototype.arrayIndexType! + : prototype.coordType; args.push(`args.${name}`); fields.push(`@align(16) ${name} : ${wgslTypeFor(value, type)}`); } @@ -2263,6 +2283,8 @@ export function describeTextureCall(call: TextureCall< args.push(`${name}: ${wgslExprFor(value, call.coordType)}`); } else if (name === 'mipLevel') { args.push(`${name}: ${wgslExprFor(value, call.levelType!)}`); + } else if (name === 'arrayIndex') { + args.push(`${name}: ${wgslExprFor(value, call.arrayIndexType!)}`); } else { args.push(`${name}: ${wgslExpr(value)}`); } @@ -2325,7 +2347,10 @@ export async function doTextureCalls( }); t.device.queue.writeBuffer(dataBuffer, 0, new Uint32Array(data)); - const { resultType, resultFormat } = getTextureFormatTypeInfo(gpuTexture.format); + const { resultType, resultFormat, componentType } = textureType.includes('depth') + ? ({ resultType: 'f32', resultFormat: 'rgba32float', componentType: 'f32' } as const) + : getTextureFormatTypeInfo(gpuTexture.format); + const returnType = `vec4<${componentType}>`; const rtWidth = 256; const renderTarget = t.createTextureTracked({ @@ -2355,11 +2380,11 @@ ${sampler ? '@group(0) @binding(1) var S : sampler' : ''}; @group(0) @binding(2) var data : Data; @fragment -fn fs_main(@builtin(position) frag_pos : vec4f) -> @location(0) ${resultType} { +fn fs_main(@builtin(position) frag_pos : vec4f) -> @location(0) ${returnType} { let frag_idx = u32(frag_pos.x) + u32(frag_pos.y) * ${renderTarget.width}; var result : ${resultType}; ${body} - return result; + return ${returnType}(result); } `; diff --git a/src/webgpu/shader/execution/expression/call/builtin/utils.ts b/src/webgpu/shader/execution/expression/call/builtin/utils.ts index a13e22c0a81b..5e8756ad599a 100644 --- a/src/webgpu/shader/execution/expression/call/builtin/utils.ts +++ b/src/webgpu/shader/execution/expression/call/builtin/utils.ts @@ -67,6 +67,27 @@ export function isLevelSpecNegative(levelSpec: LevelSpec) { return levelSpec === -1; } +export type LayerSpec = -1 | 0 | 'numLayers-1' | 'numLayers'; + +export function getLayerFromLayerSpec(arrayLayerCount: number, layerSpec: LayerSpec): number { + switch (layerSpec) { + case -1: + return -1; + case 0: + return 0; + case 'numLayers': + return arrayLayerCount; + case 'numLayers-1': + return arrayLayerCount - 1; + default: + unreachable(); + } +} + +export function isLayerSpecNegative(layerSpec: LayerSpec) { + return layerSpec === -1; +} + function getCoordForSize(size: [number, number, number], boundary: Boundary) { const coord = size.map(v => Math.floor(v / 2)); switch (boundary) {