diff --git a/src/webgpu/format_info.ts b/src/webgpu/format_info.ts index dcf7f6b77572..5bdd5fe4e569 100644 --- a/src/webgpu/format_info.ts +++ b/src/webgpu/format_info.ts @@ -1773,6 +1773,18 @@ export function isCompressedTextureFormat(format: GPUTextureFormat) { return format in kCompressedTextureFormatInfo; } +export function isDepthTextureFormat(format: GPUTextureFormat) { + return !!kTextureFormatInfo[format].depth; +} + +export function isStencilTextureFormat(format: GPUTextureFormat) { + return !!kTextureFormatInfo[format].stencil; +} + +export function isDepthOrStencilTextureFormat(format: GPUTextureFormat) { + return isDepthTextureFormat(format) || isStencilTextureFormat(format); +} + export const kCompatModeUnsupportedStorageTextureFormats: readonly GPUTextureFormat[] = [ 'rg32float', 'rg32sint', @@ -1796,6 +1808,13 @@ export function isRegularTextureFormat(format: GPUTextureFormat) { return format in kRegularTextureFormatInfo; } +/** + * Returns true of format is both compressed and a float format, for example 'bc6h-rgb-ufloat'. + */ +export function isCompressedFloatTextureFormat(format: GPUTextureFormat) { + return isCompressedTextureFormat(format) && format.includes('float'); +} + export const kFeaturesForFormats = getFeaturesForFormats(kAllTextureFormats); /** 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 5e7e4f8ead8a..079e82b66cc0 100644 --- a/src/webgpu/shader/execution/expression/call/builtin/textureLoad.spec.ts +++ b/src/webgpu/shader/execution/expression/call/builtin/textureLoad.spec.ts @@ -14,10 +14,18 @@ If an out of bounds access occurs, the built-in function returns one of: * The data for some texel within bounds of the texture * 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... `; import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { unreachable, iterRange } from '../../../../../../common/util/util.js'; +import { + isCompressedFloatTextureFormat, + isDepthTextureFormat, + kCompressedTextureFormats, + kEncodableTextureFormats, +} from '../../../../../format_info.js'; import { GPUTest } from '../../../../../gpu_test.js'; import { kFloat32Format, @@ -28,7 +36,38 @@ import { } from '../../../../../util/conversion.js'; import { TexelFormats } from '../../../../types.js'; -import { generateCoordBoundaries } from './utils.js'; +import { + TextureCall, + checkCallResults, + chooseTextureSize, + createTextureWithRandomDataAndGetTexels, + doTextureCalls, + appendComponentTypeForFormatToTextureType, + vec2, +} from './texture_utils.js'; +import { + Boundary, + LevelSpec, + generateCoordBoundaries, + getCoordinateForBoundaries, + getMipLevelFromLevelSpec, + isBoundaryNegative, + isLevelSpecNegative, +} from './utils.js'; + +const kTestableColorFormats = [...kEncodableTextureFormats, ...kCompressedTextureFormats] as const; + +function filterOutDepthAndCompressedFloatTextureFormats({ format }: { format: GPUTextureFormat }) { + return !isDepthTextureFormat(format) && !isCompressedFloatTextureFormat(format); +} + +function filterOutU32WithNegativeValues(t: { + C: 'i32' | 'u32'; + level: LevelSpec; + coordsBoundary: Boundary; +}) { + return t.C === 'i32' || (!isLevelSpecNegative(t.level) && !isBoundaryNegative(t.coordsBoundary)); +} export const g = makeTestGroup(GPUTest); @@ -59,8 +98,9 @@ g.test('sampled_2d') .desc( ` C is i32 or u32 +L is i32 or u32 -fn textureLoad(t: texture_2d, coords: vec2, level: C) -> vec4 +fn textureLoad(t: texture_2d, coords: vec2, level: L) -> vec4 Parameters: * t: The sampled texture to read from @@ -70,11 +110,58 @@ Parameters: ) .params(u => u + .combine('format', kTestableColorFormats) + .filter(filterOutDepthAndCompressedFloatTextureFormats) + .beginSubcases() .combine('C', ['i32', 'u32'] as const) - .combine('coords', generateCoordBoundaries(2)) - .combine('level', [-1, 0, `numlevels-1`, `numlevels`] as const) + .combine('L', ['i32', 'u32'] as const) + .combine('coordsBoundary', generateCoordBoundaries(2)) + .combine('level', [-1, 0, `numLevels-1`, `numLevels`] as const) + .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, height] = chooseTextureSize({ minSize: 8, minBlocks: 4, format }); + + const descriptor: GPUTextureDescriptor = { + format, + size: { width, height }, + 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_2d', 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_3d') .specURL('https://www.w3.org/TR/WGSL/#textureload') @@ -94,7 +181,7 @@ Parameters: u .combine('C', ['i32', 'u32'] as const) .combine('coords', generateCoordBoundaries(3)) - .combine('level', [-1, 0, `numlevels-1`, `numlevels`] as const) + .combine('level', [-1, 0, `numLevels-1`, `numLevels`] as const) ) .unimplemented(); @@ -144,7 +231,7 @@ Parameters: u .combine('C', ['i32', 'u32'] as const) .combine('coords', generateCoordBoundaries(2)) - .combine('level', [-1, 0, `numlevels-1`, `numlevels`] as const) + .combine('level', [-1, 0, `numLevels-1`, `numLevels`] as const) ) .unimplemented(); @@ -189,7 +276,7 @@ Parameters: .combine('C', ['i32', 'u32'] as const) .combine('coords', generateCoordBoundaries(2)) .combine('array_index', [-1, 0, `numlayers-1`, `numlayers`] as const) - .combine('level', [-1, 0, `numlevels-1`, `numlevels`] as const) + .combine('level', [-1, 0, `numLevels-1`, `numLevels`] as const) ) .unimplemented(); 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 f951bd8142cd..831a05f4d1ba 100644 --- a/src/webgpu/shader/execution/expression/call/builtin/texture_utils.ts +++ b/src/webgpu/shader/execution/expression/call/builtin/texture_utils.ts @@ -2,7 +2,9 @@ import { keysOf } from '../../../../../../common/util/data_tables.js'; import { assert, range, unreachable } from '../../../../../../common/util/util.js'; import { EncodableTextureFormat, + isCompressedFloatTextureFormat, isCompressedTextureFormat, + isDepthOrStencilTextureFormat, kEncodableTextureFormats, kTextureFormatInfo, } from '../../../../../format_info.js'; @@ -20,11 +22,15 @@ import { import { effectiveViewDimensionForDimension, physicalMipSizeFromTexture, + reifyTextureDescriptor, virtualMipSize, } from '../../../../../util/texture/base.js'; import { kTexelRepresentationInfo, + NumericRange, + PerComponentNumericRange, PerTexelComponent, + TexelComponent, TexelRepresentationInfo, } from '../../../../../util/texture/texel_data.js'; import { TexelView } from '../../../../../util/texture/texel_view.js'; @@ -99,13 +105,15 @@ function getLimitValue(v: number) { function getValueBetweenMinAndMaxTexelValueInclusive( rep: TexelRepresentationInfo, + component: TexelComponent, normalized: number ) { - return lerp( - getLimitValue(rep.numericRange!.min), - getLimitValue(rep.numericRange!.max), - normalized - ); + assert(!!rep.numericRange); + const perComponentRanges = rep.numericRange as PerComponentNumericRange; + const perComponentRange = perComponentRanges[component]; + const range = rep.numericRange as NumericRange; + const { min, max } = perComponentRange ? perComponentRange : range; + return lerp(getLimitValue(min), getLimitValue(max), normalized); } /** @@ -118,6 +126,49 @@ export function getTexelViewFormatForTextureFormat(format: GPUTextureFormat) { return format.endsWith('-srgb') ? 'rgba8unorm-srgb' : 'rgba32float'; } +const kTextureTypeInfo = { + depth: { + componentType: 'f32', + resultType: 'vec4f', + resultFormat: 'rgba32float', + }, + float: { + componentType: 'f32', + resultType: 'vec4f', + resultFormat: 'rgba32float', + }, + 'unfilterable-float': { + componentType: 'f32', + resultType: 'vec4f', + resultFormat: 'rgba32float', + }, + sint: { + componentType: 'i32', + resultType: 'vec4i', + resultFormat: 'rgba32sint', + }, + uint: { + componentType: 'u32', + resultType: 'vec4u', + resultFormat: 'rgba32uint', + }, +} as const; + +function getTextureFormatTypeInfo(format: GPUTextureFormat) { + const info = kTextureFormatInfo[format]; + const type = info.color?.type ?? info.depth?.type ?? info.stencil?.type; + assert(!!type); + return kTextureTypeInfo[type]; +} + +/** + * given a texture type 'base', returns the base with the correct component for the given texture format. + * eg: `getTextureType('texture_2d', someUnsignedIntTextureFormat)` -> `texture_2d` + */ +export function appendComponentTypeForFormatToTextureType(base: string, format: GPUTextureFormat) { + return `${base}<${getTextureFormatTypeInfo(format).componentType}>`; +} + /** * Creates a TexelView filled with random values. */ @@ -131,7 +182,7 @@ export function createRandomTexelView(info: { for (const component of rep.componentOrder) { const rnd = hashU32(coords.x, coords.y, coords.z, component.charCodeAt(0)); const normalized = clamp(rnd / 0xffffffff, { min: 0, max: 1 }); - texel[component] = getValueBetweenMinAndMaxTexelValueInclusive(rep, normalized); + texel[component] = getValueBetweenMinAndMaxTexelValueInclusive(rep, component, normalized); } return quantize(texel, rep); }; @@ -177,6 +228,7 @@ export interface TextureCallArgs { coords?: T; mipLevel?: number; arrayIndex?: number; + sampleIndex?: number; ddx?: T; ddy?: T; offset?: T; @@ -184,7 +236,8 @@ export interface TextureCallArgs { export interface TextureCall extends TextureCallArgs { builtin: 'textureSample' | 'textureLoad'; - coordType: 'f'; + coordType: 'f' | 'i' | 'u'; + levelType?: 'i' | 'u'; } function toArray(coords: Dimensionality): number[] { @@ -244,6 +297,46 @@ export interface Texture { viewDescriptor: GPUTextureViewDescriptor; } +/** + * Converts the src texel representation to an RGBA representation. + */ +function convertPerTexelComponentToResultFormat( + src: PerTexelComponent, + format: EncodableTextureFormat +): PerTexelComponent { + const rep = kTexelRepresentationInfo[format]; + const out: PerTexelComponent = { R: 0, G: 0, B: 0, A: 1 }; + for (const component of rep.componentOrder) { + switch (component) { + case 'Stencil': + case 'Depth': + out.R = src[component]; + break; + default: + assert(out[component] !== undefined); // checks that component = R, G, B or A + out[component] = src[component]; + } + } + return out; +} + +/** + * Convert RGBA result format to texel view format of src texture. + * Effectively this converts something like { R: 0.1, G: 0, B: 0, A: 1 } + * to { Depth: 0.1 } + */ +function convertResultFormatToTexelViewFormat( + src: PerTexelComponent, + format: EncodableTextureFormat +): PerTexelComponent { + const rep = kTexelRepresentationInfo[format]; + const out: PerTexelComponent = {}; + for (const component of rep.componentOrder) { + out[component] = src[component] ?? src.R; + } + return out; +} + /** * Returns the expect value for a WGSL builtin texture function for a single * mip level @@ -251,19 +344,20 @@ export interface Texture { export function softwareTextureReadMipLevel( call: TextureCall, texture: Texture, - sampler: GPUSamplerDescriptor, + sampler: GPUSamplerDescriptor | undefined, mipLevel: number ): PerTexelComponent { - const rep = kTexelRepresentationInfo[texture.texels[mipLevel].format]; + const { format } = texture.texels[mipLevel]; + const rep = kTexelRepresentationInfo[format]; const textureSize = virtualMipSize( texture.descriptor.dimension || '2d', texture.descriptor.size, mipLevel ); const addressMode = [ - sampler.addressModeU ?? 'clamp-to-edge', - sampler.addressModeV ?? 'clamp-to-edge', - sampler.addressModeW ?? 'clamp-to-edge', + sampler?.addressModeU ?? 'clamp-to-edge', + sampler?.addressModeV ?? 'clamp-to-edge', + sampler?.addressModeW ?? 'clamp-to-edge', ]; const load = (at: number[]) => @@ -304,7 +398,7 @@ export function softwareTextureReadMipLevel( const samples: { at: number[]; weight: number }[] = []; - const filter = sampler.minFilter; + const filter = sampler?.minFilter ?? 'nearest'; switch (filter) { case 'linear': { // 'p0' is the lower texel for 'at' @@ -416,10 +510,11 @@ export function softwareTextureReadMipLevel( } } - return out; + return convertPerTexelComponentToResultFormat(out, format); } case 'textureLoad': { - return load(toArray(call.coords!)); + const c = applyAddressModesToCoords(addressMode, textureSize, call.coords!); + return convertPerTexelComponentToResultFormat(load(c), format); } } } @@ -495,6 +590,149 @@ export type TextureTestOptions = { offset?: readonly [number, number]; // a constant offset }; +/** + * out of bounds is defined as any of the following being true + * + * * coords is outside the range [0, textureDimensions(t, level)) + * * array_index is outside the range [0, textureNumLayers(t)) + * * level is outside the range [0, textureNumLevels(t)) + * * sample_index is outside the range [0, textureNumSamples(s)) + */ +function isOutOfBoundsCall(texture: Texture, call: TextureCall) { + assert(call.mipLevel !== undefined); + assert(call.coords !== undefined); + assert(call.offset === undefined); + + const desc = reifyTextureDescriptor(texture.descriptor); + + const { coords, mipLevel, arrayIndex, sampleIndex } = call; + + if (mipLevel < 0 || mipLevel >= desc.mipLevelCount) { + return true; + } + + const size = virtualMipSize( + texture.descriptor.dimension || '2d', + texture.descriptor.size, + mipLevel + ); + + for (let i = 0; i < coords.length; ++i) { + const v = coords[i]; + if (v < 0 || v >= size[i]) { + return true; + } + } + + if (arrayIndex !== undefined) { + const size = reifyExtent3D(desc.size); + if (arrayIndex < 0 || arrayIndex >= size.depthOrArrayLayers) { + return true; + } + } + + if (sampleIndex !== undefined) { + if (sampleIndex < 0 || sampleIndex >= desc.sampleCount) { + return true; + } + } + + return false; +} + +/** + * For a texture builtin with no sampler (eg textureLoad), + * any out of bounds access is allowed to return one of: + * + * * the value of any texel in the texture + * * 0,0,0,0 or 0,0,0,1 if not a depth texture + * * 0 if a depth texture + */ +function okBecauseOutOfBounds( + texture: Texture, + call: TextureCall, + gotRGBA: PerTexelComponent, + maxFractionalDiff: number +) { + if (!isOutOfBoundsCall(texture, call)) { + return false; + } + + if (texture.descriptor.format.includes('depth')) { + if (gotRGBA.R === 0) { + return true; + } + } else { + if ( + gotRGBA.R === 0 && + gotRGBA.B === 0 && + gotRGBA.G === 0 && + (gotRGBA.A === 0 || gotRGBA.A === 1) + ) { + return true; + } + } + + for (let mipLevel = 0; mipLevel < texture.texels.length; ++mipLevel) { + const mipTexels = texture.texels[mipLevel]; + const size = virtualMipSize( + texture.descriptor.dimension || '2d', + texture.descriptor.size, + mipLevel + ); + for (let z = 0; z < size[2]; ++z) { + for (let y = 0; y < size[1]; ++y) { + for (let x = 0; x < size[0]; ++x) { + const texel = mipTexels.color({ x, y, z }); + const rgba = convertPerTexelComponentToResultFormat(texel, mipTexels.format); + if (texelsApproximatelyEqual(gotRGBA, rgba, mipTexels.format, maxFractionalDiff)) { + return true; + } + } + } + } + } + + return false; +} + +const kRGBAComponents = [ + TexelComponent.R, + TexelComponent.G, + TexelComponent.B, + TexelComponent.A, +] as const; + +const kRComponent = [TexelComponent.R] as const; + +function texelsApproximatelyEqual( + gotRGBA: PerTexelComponent, + expectRGBA: PerTexelComponent, + format: EncodableTextureFormat, + maxFractionalDiff: number +) { + const rep = kTexelRepresentationInfo[format]; + const got = convertResultFormatToTexelViewFormat(gotRGBA, format); + const expect = convertResultFormatToTexelViewFormat(expectRGBA, format); + const gULP = rep.bitsToULPFromZero(rep.numberToBits(got)); + const eULP = rep.bitsToULPFromZero(rep.numberToBits(expect)); + + const rgbaComponentsToCheck = isDepthOrStencilTextureFormat(format) + ? kRComponent + : kRGBAComponents; + + for (const component of rgbaComponentsToCheck) { + const g = gotRGBA[component]!; + const e = expectRGBA[component]!; + const absDiff = Math.abs(g - e); + const ulpDiff = Math.abs(gULP[component]! - eULP[component]!); + if (ulpDiff > 3 && absDiff > maxFractionalDiff) { + return false; + } + } + return true; +} + /** * Checks the result of each call matches the expected result. */ @@ -502,18 +740,36 @@ export async function checkCallResults( t: GPUTest, texture: Texture, textureType: string, - sampler: GPUSamplerDescriptor, + sampler: GPUSamplerDescriptor | undefined, calls: TextureCall[], results: PerTexelComponent[] ) { const errs: string[] = []; const rep = kTexelRepresentationInfo[texture.texels[0].format]; - const maxFractionalDiff = getMaxFractionalDiffForTextureFormat(texture.descriptor.format); + const maxFractionalDiff = + sampler?.minFilter === 'linear' || + sampler?.magFilter === 'linear' || + sampler?.mipmapFilter === 'linear' + ? getMaxFractionalDiffForTextureFormat(texture.descriptor.format) + : 0; + for (let callIdx = 0; callIdx < calls.length; callIdx++) { const call = calls[callIdx]; - const got = results[callIdx]; - const expect = softwareTextureReadMipLevel(call, texture, sampler, 0); + const gotRGBA = results[callIdx]; + const expectRGBA = softwareTextureReadMipLevel(call, texture, sampler, 0); + + if ( + texelsApproximatelyEqual(gotRGBA, expectRGBA, texture.texels[0].format, maxFractionalDiff) + ) { + continue; + } + + if (!sampler && okBecauseOutOfBounds(texture, call, gotRGBA, maxFractionalDiff)) { + continue; + } + const got = convertResultFormatToTexelViewFormat(gotRGBA, texture.texels[0].format); + const expect = convertResultFormatToTexelViewFormat(expectRGBA, texture.texels[0].format); const gULP = rep.bitsToULPFromZero(rep.numberToBits(got)); const eULP = rep.bitsToULPFromZero(rep.numberToBits(expect)); for (const component of rep.componentOrder) { @@ -532,40 +788,42 @@ export async function checkCallResults( abs diff: ${absDiff.toFixed(4)} rel diff: ${(relDiff * 100).toFixed(2)}% ulp diff: ${ulpDiff} - sample points: `); - const expectedSamplePoints = [ - 'expected:', - ...(await identifySamplePoints(texture, (texels: TexelView) => { - return Promise.resolve( - softwareTextureReadMipLevel( - call, - { - texels: [texels], - descriptor: texture.descriptor, - viewDescriptor: texture.viewDescriptor, - }, - sampler, - 0 - ) - ); - })), - ]; - const gotSamplePoints = [ - 'got:', - ...(await identifySamplePoints(texture, async (texels: TexelView) => { - const gpuTexture = createTextureFromTexelViews(t, [texels], texture.descriptor); - const result = ( - await doTextureCalls(t, gpuTexture, texture.viewDescriptor, textureType, sampler, [ - call, - ]) - )[0]; - gpuTexture.destroy(); - return result; - })), - ]; - errs.push(layoutTwoColumns(expectedSamplePoints, gotSamplePoints).join('\n')); - errs.push('', ''); + if (sampler) { + const expectedSamplePoints = [ + 'expected:', + ...(await identifySamplePoints(texture, (texels: TexelView) => { + return Promise.resolve( + softwareTextureReadMipLevel( + call, + { + texels: [texels], + descriptor: texture.descriptor, + viewDescriptor: texture.viewDescriptor, + }, + sampler, + 0 + ) + ); + })), + ]; + const gotSamplePoints = [ + 'got:', + ...(await identifySamplePoints(texture, async (texels: TexelView) => { + const gpuTexture = createTextureFromTexelViews(t, [texels], texture.descriptor); + const result = ( + await doTextureCalls(t, gpuTexture, texture.viewDescriptor, textureType, sampler, [ + call, + ]) + )[0]; + gpuTexture.destroy(); + return result; + })), + ]; + errs.push(' sample points:'); + errs.push(layoutTwoColumns(expectedSamplePoints, gotSamplePoints).join('\n')); + errs.push('', ''); + } } } } @@ -794,7 +1052,8 @@ function getMaxFractionalDiffForTextureFormat(format: GPUTextureFormat) { } else if (format.endsWith('float')) { return 44; } else { - unreachable(); + // It's likely an integer format. In any case, zero tolerance is passable. + return 0; } } @@ -911,6 +1170,7 @@ function getBlockFiller(format: GPUTextureFormat) { * Fills a texture with random data. */ export function fillTextureWithRandomData(device: GPUDevice, texture: GPUTexture) { + assert(!isCompressedFloatTextureFormat(texture.format)); const info = kTextureFormatInfo[texture.format]; const hashBase = sumOfCharCodesOfString(texture.format) + @@ -1354,6 +1614,20 @@ function layoutTwoColumns(columnA: string[], columnB: string[]) { return out; } +function getDepthOrArrayLayersForViewDimension(viewDimension?: GPUTextureViewDimension) { + switch (viewDimension) { + case undefined: + case '2d': + return 1; + case '3d': + return 8; + case 'cube': + return 6; + default: + unreachable(); + } +} + /** * Choose a texture size based on the given parameters. * The size will be in a multiple of blocks. If it's a cube @@ -1375,9 +1649,10 @@ export function chooseTextureSize({ const height = align(Math.max(minSize, blockHeight * minBlocks), blockHeight); if (viewDimension === 'cube') { const size = lcm(width, height); - return [size, size]; + return [size, size, 6]; } - return [width, height]; + const depthOrArrayLayers = getDepthOrArrayLayersForViewDimension(viewDimension); + return [width, height, depthOrArrayLayers]; } export const kSamplePointMethods = ['texel-centre', 'spiral'] as const; @@ -1870,6 +2145,22 @@ function wgslExpr(data: number | vec1 | vec2 | vec3 | vec4): string { return data.toString(); } +function wgslExprFor(data: number | vec1 | vec2 | vec3 | vec4, type: 'f' | 'i' | 'u'): string { + if (Array.isArray(data)) { + switch (data.length) { + case 1: + return `${type}(${data[0].toString()})`; + case 2: + return `vec2${type}(${data.map(v => v.toString()).join(', ')})`; + case 3: + return `vec3${type}(${data.map(v => v.toString()).join(', ')})`; + default: + unreachable(); + } + } + return `${type}32(${data.toString()})`; +} + function binKey(call: TextureCall): string { const keys: string[] = []; for (const name of kTextureCallArgNames) { @@ -1903,8 +2194,9 @@ function buildBinnedCalls(calls: TextureCall[]) { if (name === 'offset') { args.push(`/* offset */ ${wgslExpr(value)}`); } else { + const type = name === 'mipLevel' ? prototype.levelType! : prototype.coordType; args.push(`args.${name}`); - fields.push(`@align(16) ${name} : ${wgslTypeFor(value, prototype.coordType)}`); + fields.push(`@align(16) ${name} : ${wgslTypeFor(value, type)}`); } } } @@ -1967,7 +2259,13 @@ export function describeTextureCall(call: TextureCall< for (const name of kTextureCallArgNames) { const value = call[name]; if (value !== undefined) { - args.push(`${name}: ${wgslExpr(value)}`); + if (name === 'coords') { + args.push(`${name}: ${wgslExprFor(value, call.coordType)}`); + } else if (name === 'mipLevel') { + args.push(`${name}: ${wgslExprFor(value, call.levelType!)}`); + } else { + args.push(`${name}: ${wgslExpr(value)}`); + } } } return `${call.builtin}(${args.join(', ')})`; @@ -1992,7 +2290,7 @@ export async function doTextureCalls( gpuTexture: GPUTexture, viewDescriptor: GPUTextureViewDescriptor, textureType: string, - sampler: GPUSamplerDescriptor, + sampler: GPUSamplerDescriptor | undefined, calls: TextureCall[] ) { let structs = ''; @@ -2027,9 +2325,11 @@ export async function doTextureCalls( }); t.device.queue.writeBuffer(dataBuffer, 0, new Uint32Array(data)); + const { resultType, resultFormat } = getTextureFormatTypeInfo(gpuTexture.format); + const rtWidth = 256; const renderTarget = t.createTextureTracked({ - format: 'rgba32float', + format: resultFormat, size: { width: rtWidth, height: Math.ceil(calls.length / rtWidth) }, usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT, }); @@ -2051,13 +2351,13 @@ fn vs_main(@builtin(vertex_index) vertex_index : u32) -> @builtin(position) vec4 } @group(0) @binding(0) var T : ${textureType}; -@group(0) @binding(1) var S : sampler; +${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) vec4f { +fn fs_main(@builtin(position) frag_pos : vec4f) -> @location(0) ${resultType} { let frag_idx = u32(frag_pos.x) + u32(frag_pos.y) * ${renderTarget.width}; - var result : vec4f; + var result : ${resultType}; ${body} return result; } @@ -2083,13 +2383,13 @@ ${body} pipelines.set(code, pipeline); } - const gpuSampler = t.device.createSampler(sampler); + const gpuSampler = sampler ? t.device.createSampler(sampler) : undefined; const bindGroup = t.device.createBindGroup({ layout: pipeline.getBindGroupLayout(0), entries: [ { binding: 0, resource: gpuTexture.createView(viewDescriptor) }, - { binding: 1, resource: gpuSampler }, + ...(sampler ? [{ binding: 1, resource: gpuSampler! }] : []), { binding: 2, resource: { buffer: dataBuffer } }, ], }); diff --git a/src/webgpu/shader/execution/expression/call/builtin/utils.ts b/src/webgpu/shader/execution/expression/call/builtin/utils.ts index 9cbee0093926..a13e22c0a81b 100644 --- a/src/webgpu/shader/execution/expression/call/builtin/utils.ts +++ b/src/webgpu/shader/execution/expression/call/builtin/utils.ts @@ -1,11 +1,34 @@ +import { assert, unreachable } from '../../../../../../common/util/util.js'; +import { virtualMipSize } from '../../../../../util/texture/base.js'; + +/* Valid types of Boundaries */ +export type Boundary = + | 'in-bounds' + | 'x-min-wrap' + | 'x-min-boundary' + | 'x-max-wrap' + | 'x-max-boundary' + | 'y-min-wrap' + | 'y-min-boundary' + | 'y-max-wrap' + | 'y-max-boundary' + | 'z-min-wrap' + | 'z-min-boundary' + | 'z-max-wrap' + | 'z-max-boundary'; + +export function isBoundaryNegative(boundary: Boundary) { + return boundary.endsWith('min-wrap'); +} + /** * Generates the boundary entries for the given number of dimensions * * @param numDimensions: The number of dimensions to generate for * @returns an array of generated coord boundaries */ -export function generateCoordBoundaries(numDimensions: number) { - const ret = ['in-bounds']; +export function generateCoordBoundaries(numDimensions: number): Boundary[] { + const ret: Boundary[] = ['in-bounds']; if (numDimensions < 1 || numDimensions > 3) { throw new Error(`invalid numDimensions: ${numDimensions}`); @@ -15,7 +38,7 @@ export function generateCoordBoundaries(numDimensions: number) { for (let i = 0; i < numDimensions; ++i) { for (const j of ['min', 'max']) { for (const k of ['wrap', 'boundary']) { - ret.push(`${name[i]}-${j}-${k}`); + ret.push(`${name[i]}-${j}-${k}` as Boundary); } } } @@ -23,18 +46,91 @@ export function generateCoordBoundaries(numDimensions: number) { return ret; } +export type LevelSpec = -1 | 0 | 'numLevels-1' | 'numLevels'; + +export function getMipLevelFromLevelSpec(mipLevelCount: number, levelSpec: LevelSpec): number { + switch (levelSpec) { + case -1: + return -1; + case 0: + return 0; + case 'numLevels': + return mipLevelCount; + case 'numLevels-1': + return mipLevelCount - 1; + default: + unreachable(); + } +} + +export function isLevelSpecNegative(levelSpec: LevelSpec) { + return levelSpec === -1; +} + +function getCoordForSize(size: [number, number, number], boundary: Boundary) { + const coord = size.map(v => Math.floor(v / 2)); + switch (boundary) { + case 'in-bounds': + break; + default: { + const axis = boundary[0]; + const axisIndex = axis.charCodeAt(0) - 'x'.charCodeAt(0); + const axisSize = size[axisIndex]; + const location = boundary.substring(2); + let v = 0; + switch (location) { + case 'min-wrap': + v = -1; + break; + case 'min-boundary': + v = 0; + break; + case 'max-wrap': + v = axisSize; + break; + case 'max-boundary': + v = axisSize - 1; + break; + default: + unreachable(); + } + coord[axisIndex] = v; + } + } + return coord; +} + +function getNumDimensions(dimension: GPUTextureDimension) { + switch (dimension) { + case '1d': + return 1; + case '2d': + return 2; + case '3d': + return 3; + } +} + +export function getCoordinateForBoundaries( + texture: GPUTexture, + mipLevel: number, + boundary: Boundary +) { + const size = virtualMipSize(texture.dimension, texture, mipLevel); + const coord = getCoordForSize(size, boundary); + return coord.slice(0, getNumDimensions(texture.dimension)) as T; +} + /** - * Generates a set of offset values to attempt in the range [-9, 8]. + * Generates a set of offset values to attempt in the range [-8, 7]. * * @param numDimensions: The number of dimensions to generate for * @return an array of generated offset values */ export function generateOffsets(numDimensions: number) { - if (numDimensions < 2 || numDimensions > 3) { - throw new Error(`generateOffsets: invalid numDimensions: ${numDimensions}`); - } + assert(numDimensions >= 2 && numDimensions <= 3); const ret: Array> = [undefined]; - for (const val of [-9, -8, 0, 1, 7, 8]) { + for (const val of [-8, 0, 1, 7]) { const v = []; for (let i = 0; i < numDimensions; ++i) { v.push(val); diff --git a/src/webgpu/util/texture/texel_data.ts b/src/webgpu/util/texture/texel_data.ts index 0555ac5920d8..4c88d9c2182a 100644 --- a/src/webgpu/util/texture/texel_data.ts +++ b/src/webgpu/util/texture/texel_data.ts @@ -78,12 +78,15 @@ function makePerTexelComponent(components: TexelComponent[], value: T): PerTe * @returns {ComponentMapFn} The map function which clones the input component values, and applies * `fn` to each of component of `components`. */ -function applyEach(fn: (value: number) => number, components: TexelComponent[]): ComponentMapFn { +function applyEach( + fn: (value: number, component: TexelComponent) => number, + components: TexelComponent[] +): ComponentMapFn { return (values: PerTexelComponent) => { values = Object.assign({}, values); for (const c of components) { assert(values[c] !== undefined); - values[c] = fn(values[c]!); + values[c] = fn(values[c]!, c); } return values; }; @@ -122,7 +125,13 @@ const decodeSRGB: ComponentMapFn = components => { export function makeClampToRange(format: EncodableTextureFormat): ComponentMapFn { const repr = kTexelRepresentationInfo[format]; assert(repr.numericRange !== null, 'Format has unknown numericRange'); - return applyEach(x => clamp(x, repr.numericRange!), repr.componentOrder); + const perComponentRanges = repr.numericRange as PerComponentNumericRange; + const range = repr.numericRange as NumericRange; + + return applyEach((x, component) => { + const perComponentRange = perComponentRanges[component]; + return clamp(x, perComponentRange ? perComponentRange : range); + }, repr.componentOrder); } // MAINTENANCE_TODO: Look into exposing this map to the test fixture so that it can be GCed at the @@ -601,6 +610,23 @@ const kFloat11Format = { signed: 0, exponentBits: 5, mantissaBits: 6, bias: 15 } const kFloat10Format = { signed: 0, exponentBits: 5, mantissaBits: 5, bias: 15 } as const; export type PerComponentFiniteMax = Record; +export type NumericRange = { + min: number; + max: number; + finiteMin: number; + finiteMax: number | PerComponentFiniteMax; +}; +export type PerComponentNumericRange = Partial< + Record< + TexelComponent, + { + min: number; + max: number; + finiteMin: number; + finiteMax: number; + } + > +>; export type TexelRepresentationInfo = { /** Order of components in the packed representation. */ readonly componentOrder: TexelComponent[]; @@ -628,15 +654,11 @@ export type TexelRepresentationInfo = { /** Convert integer bit representations into ULPs-from-zero, e.g. unorm8 255 -> 255 ULPs */ readonly bitsToULPFromZero: ComponentMapFn; /** The valid range of numeric "color" values, e.g. [0, Infinity] for ufloat. */ - readonly numericRange: null | { - min: number; - max: number; - finiteMin: number; - finiteMax: number | PerComponentFiniteMax; - }; + readonly numericRange: null | NumericRange | PerComponentNumericRange; // Add fields as needed }; + export const kTexelRepresentationInfo: { readonly [k in UncompressedTextureFormat]: TexelRepresentationInfo; } = { @@ -726,7 +748,12 @@ export const kTexelRepresentationInfo: { return components; }, bitsToULPFromZero: components => components, - numericRange: null, + numericRange: { + R: { min: 0, max: 0x3ff, finiteMin: 0, finiteMax: 0x3ff }, + G: { min: 0, max: 0x3ff, finiteMin: 0, finiteMax: 0x3ff }, + B: { min: 0, max: 0x3ff, finiteMin: 0, finiteMax: 0x3ff }, + A: { min: 0, max: 0x3, finiteMin: 0, finiteMax: 0x3 }, + }, }, rgb10a2unorm: { componentOrder: kRGBA,