From 7d7c6bd203f98b86ba60c5b3036c7a885da2989e Mon Sep 17 00:00:00 2001 From: Greggman Date: Wed, 10 Apr 2024 07:02:10 +0900 Subject: [PATCH] WGSL execution TextureSample tests (#3578) * WGSL execution tests for textureSample --- src/resources/cache/hashes.json | 218 ++--- .../call/builtin/textureSample.spec.ts | 86 +- .../expression/call/builtin/texture_utils.ts | 809 ++++++++++++++++++ src/webgpu/util/math.ts | 19 + src/webgpu/util/texture/texel_data.ts | 50 +- 5 files changed, 1056 insertions(+), 126 deletions(-) create mode 100644 src/webgpu/shader/execution/expression/call/builtin/texture_utils.ts diff --git a/src/resources/cache/hashes.json b/src/resources/cache/hashes.json index 047a322bf647..490b786dac49 100644 --- a/src/resources/cache/hashes.json +++ b/src/resources/cache/hashes.json @@ -1,111 +1,111 @@ { - "webgpu/shader/execution/binary/af_addition.bin": "481cb03", - "webgpu/shader/execution/binary/af_logical.bin": "f8ead618", - "webgpu/shader/execution/binary/af_division.bin": "6016c00b", - "webgpu/shader/execution/binary/af_matrix_addition.bin": "c6201e19", - "webgpu/shader/execution/binary/af_matrix_subtraction.bin": "5d6aa3bf", - "webgpu/shader/execution/binary/af_multiplication.bin": "45eb683d", - "webgpu/shader/execution/binary/af_remainder.bin": "3b504c08", - "webgpu/shader/execution/binary/af_subtraction.bin": "f2b402b3", - "webgpu/shader/execution/binary/f16_addition.bin": "8895dab0", - "webgpu/shader/execution/binary/f16_logical.bin": "1a539f26", - "webgpu/shader/execution/binary/f16_division.bin": "bab4816f", - "webgpu/shader/execution/binary/f16_matrix_addition.bin": "f2025010", - "webgpu/shader/execution/binary/f16_matrix_matrix_multiplication.bin": "5f943633", - "webgpu/shader/execution/binary/f16_matrix_scalar_multiplication.bin": "493f6137", - "webgpu/shader/execution/binary/f16_matrix_subtraction.bin": "9bd533c7", - "webgpu/shader/execution/binary/f16_matrix_vector_multiplication.bin": "a1038760", - "webgpu/shader/execution/binary/f16_multiplication.bin": "aec1a23a", - "webgpu/shader/execution/binary/f16_remainder.bin": "dda90153", - "webgpu/shader/execution/binary/f16_subtraction.bin": "465fd188", - "webgpu/shader/execution/binary/f32_addition.bin": "c99b2b07", - "webgpu/shader/execution/binary/f32_logical.bin": "75ae7869", - "webgpu/shader/execution/binary/f32_division.bin": "553a4148", - "webgpu/shader/execution/binary/f32_matrix_addition.bin": "a2d8d6df", - "webgpu/shader/execution/binary/f32_matrix_matrix_multiplication.bin": "488d1fd2", - "webgpu/shader/execution/binary/f32_matrix_scalar_multiplication.bin": "767b51f9", - "webgpu/shader/execution/binary/f32_matrix_subtraction.bin": "8e4547e1", - "webgpu/shader/execution/binary/f32_matrix_vector_multiplication.bin": "9a9b8822", - "webgpu/shader/execution/binary/f32_multiplication.bin": "b9326df2", - "webgpu/shader/execution/binary/f32_remainder.bin": "84f3b766", - "webgpu/shader/execution/binary/f32_subtraction.bin": "5ae94119", - "webgpu/shader/execution/binary/i32_arithmetic.bin": "539c55c", - "webgpu/shader/execution/binary/i32_comparison.bin": "65d45a34", - "webgpu/shader/execution/binary/u32_arithmetic.bin": "a0d51e10", - "webgpu/shader/execution/binary/u32_comparison.bin": "aca6d579", - "webgpu/shader/execution/abs.bin": "237ee225", - "webgpu/shader/execution/acos.bin": "14025868", - "webgpu/shader/execution/acosh.bin": "84cbd37f", - "webgpu/shader/execution/asin.bin": "6ec2067c", - "webgpu/shader/execution/asinh.bin": "f94fda84", - "webgpu/shader/execution/atan.bin": "38125983", - "webgpu/shader/execution/atan2.bin": "fa30e008", - "webgpu/shader/execution/atanh.bin": "bc041da5", - "webgpu/shader/execution/bitcast.bin": "c9dcb86a", - "webgpu/shader/execution/ceil.bin": "6afc7fb5", - "webgpu/shader/execution/clamp.bin": "b751048", - "webgpu/shader/execution/cos.bin": "afad4fc1", - "webgpu/shader/execution/cosh.bin": "66c45ea3", - "webgpu/shader/execution/cross.bin": "1d32ddcc", - "webgpu/shader/execution/degrees.bin": "8cf32625", - "webgpu/shader/execution/determinant.bin": "75c5106f", - "webgpu/shader/execution/distance.bin": "15067f84", - "webgpu/shader/execution/dot.bin": "20602e01", - "webgpu/shader/execution/exp.bin": "30e623e", - "webgpu/shader/execution/exp2.bin": "d04dcec2", - "webgpu/shader/execution/faceForward.bin": "c99c4512", - "webgpu/shader/execution/floor.bin": "bdb958a", - "webgpu/shader/execution/fma.bin": "7c752ef8", - "webgpu/shader/execution/fract.bin": "6c969be5", - "webgpu/shader/execution/frexp.bin": "1e464bde", - "webgpu/shader/execution/inverseSqrt.bin": "6a9a8da9", - "webgpu/shader/execution/ldexp.bin": "97e96a62", - "webgpu/shader/execution/length.bin": "ae84a5f5", - "webgpu/shader/execution/log.bin": "d608ad1d", - "webgpu/shader/execution/log2.bin": "e027d7bf", - "webgpu/shader/execution/max.bin": "5622c8f0", - "webgpu/shader/execution/min.bin": "eb966751", - "webgpu/shader/execution/mix.bin": "3cfe9d1f", - "webgpu/shader/execution/modf.bin": "54f20c32", - "webgpu/shader/execution/normalize.bin": "7dc4a4da", - "webgpu/shader/execution/pack2x16float.bin": "4637ca12", - "webgpu/shader/execution/pow.bin": "8bdcbe61", - "webgpu/shader/execution/quantizeToF16.bin": "fc9c1c9c", - "webgpu/shader/execution/radians.bin": "ea39b014", - "webgpu/shader/execution/reflect.bin": "e56256eb", - "webgpu/shader/execution/refract.bin": "94a2ca00", - "webgpu/shader/execution/round.bin": "3f588c9e", - "webgpu/shader/execution/saturate.bin": "39a9a2a6", - "webgpu/shader/execution/sign.bin": "9f9d7d6b", - "webgpu/shader/execution/sin.bin": "41c1f9f8", - "webgpu/shader/execution/sinh.bin": "5be15018", - "webgpu/shader/execution/smoothstep.bin": "d99e406e", - "webgpu/shader/execution/sqrt.bin": "28380636", - "webgpu/shader/execution/step.bin": "1e8b7ec6", - "webgpu/shader/execution/tan.bin": "f1555bab", - "webgpu/shader/execution/tanh.bin": "1766c34d", - "webgpu/shader/execution/transpose.bin": "45f2c2b7", - "webgpu/shader/execution/trunc.bin": "39f76667", - "webgpu/shader/execution/unpack2x16float.bin": "ab964a2a", - "webgpu/shader/execution/unpack2x16snorm.bin": "f1b862e7", - "webgpu/shader/execution/unpack2x16unorm.bin": "77bdfe6c", - "webgpu/shader/execution/unpack4x8snorm.bin": "217b1c7b", - "webgpu/shader/execution/unpack4x8unorm.bin": "dbb91ece", - "webgpu/shader/execution/unary/af_arithmetic.bin": "57d1df88", - "webgpu/shader/execution/unary/af_assignment.bin": "b07975c3", - "webgpu/shader/execution/unary/bool_conversion.bin": "435182f", - "webgpu/shader/execution/unary/f16_arithmetic.bin": "bd72157", - "webgpu/shader/execution/unary/f16_conversion.bin": "1ac28739", - "webgpu/shader/execution/unary/f32_arithmetic.bin": "94bac084", - "webgpu/shader/execution/unary/f32_conversion.bin": "78ed87a7", - "webgpu/shader/execution/unary/i32_arithmetic.bin": "573807ce", - "webgpu/shader/execution/unary/i32_conversion.bin": "78191f08", - "webgpu/shader/execution/unary/u32_conversion.bin": "1becfae5", - "webgpu/shader/execution/unary/ai_assignment.bin": "c6f0f12b", - "webgpu/shader/execution/binary/ai_arithmetic.bin": "1549fa76", - "webgpu/shader/execution/unary/ai_arithmetic.bin": "deac2a47", - "webgpu/shader/execution/binary/af_matrix_matrix_multiplication.bin": "19607838", - "webgpu/shader/execution/binary/af_matrix_scalar_multiplication.bin": "29c9bff5", - "webgpu/shader/execution/binary/af_matrix_vector_multiplication.bin": "ff5d8157", - "webgpu/shader/execution/derivatives.bin": "d0003650" + "webgpu/shader/execution/binary/af_addition.bin": "7d91a72b", + "webgpu/shader/execution/binary/af_logical.bin": "4ccda74e", + "webgpu/shader/execution/binary/af_division.bin": "40623df1", + "webgpu/shader/execution/binary/af_matrix_addition.bin": "19466e16", + "webgpu/shader/execution/binary/af_matrix_subtraction.bin": "bc55c56d", + "webgpu/shader/execution/binary/af_multiplication.bin": "453b566c", + "webgpu/shader/execution/binary/af_remainder.bin": "e95c02c3", + "webgpu/shader/execution/binary/af_subtraction.bin": "2364467", + "webgpu/shader/execution/binary/f16_addition.bin": "e2a7576d", + "webgpu/shader/execution/binary/f16_logical.bin": "e2f05790", + "webgpu/shader/execution/binary/f16_division.bin": "b564cc2f", + "webgpu/shader/execution/binary/f16_matrix_addition.bin": "fb199920", + "webgpu/shader/execution/binary/f16_matrix_matrix_multiplication.bin": "31b50646", + "webgpu/shader/execution/binary/f16_matrix_scalar_multiplication.bin": "a0e8bf21", + "webgpu/shader/execution/binary/f16_matrix_subtraction.bin": "e7e1e7ad", + "webgpu/shader/execution/binary/f16_matrix_vector_multiplication.bin": "93620e5c", + "webgpu/shader/execution/binary/f16_multiplication.bin": "dc965e96", + "webgpu/shader/execution/binary/f16_remainder.bin": "99fb9c8d", + "webgpu/shader/execution/binary/f16_subtraction.bin": "449176ee", + "webgpu/shader/execution/binary/f32_addition.bin": "4ecaa4dc", + "webgpu/shader/execution/binary/f32_logical.bin": "ccf636d6", + "webgpu/shader/execution/binary/f32_division.bin": "6222238d", + "webgpu/shader/execution/binary/f32_matrix_addition.bin": "b3191ee0", + "webgpu/shader/execution/binary/f32_matrix_matrix_multiplication.bin": "1dd44f90", + "webgpu/shader/execution/binary/f32_matrix_scalar_multiplication.bin": "83f70ea5", + "webgpu/shader/execution/binary/f32_matrix_subtraction.bin": "fa83e3cf", + "webgpu/shader/execution/binary/f32_matrix_vector_multiplication.bin": "199e9c8a", + "webgpu/shader/execution/binary/f32_multiplication.bin": "2c438421", + "webgpu/shader/execution/binary/f32_remainder.bin": "e1d627d9", + "webgpu/shader/execution/binary/f32_subtraction.bin": "6475df64", + "webgpu/shader/execution/binary/i32_arithmetic.bin": "200781bb", + "webgpu/shader/execution/binary/i32_comparison.bin": "b12b325a", + "webgpu/shader/execution/binary/u32_arithmetic.bin": "6678a8f5", + "webgpu/shader/execution/binary/u32_comparison.bin": "63eb2849", + "webgpu/shader/execution/abs.bin": "e2aa8bc0", + "webgpu/shader/execution/acos.bin": "ae9980a8", + "webgpu/shader/execution/acosh.bin": "cebc0537", + "webgpu/shader/execution/asin.bin": "aab6d966", + "webgpu/shader/execution/asinh.bin": "18ec75d3", + "webgpu/shader/execution/atan.bin": "deb7c548", + "webgpu/shader/execution/atan2.bin": "afc4fb0c", + "webgpu/shader/execution/atanh.bin": "eacee7c", + "webgpu/shader/execution/bitcast.bin": "838dd39", + "webgpu/shader/execution/ceil.bin": "92f35ff9", + "webgpu/shader/execution/clamp.bin": "1ba96a84", + "webgpu/shader/execution/cos.bin": "20598940", + "webgpu/shader/execution/cosh.bin": "dcf8ab94", + "webgpu/shader/execution/cross.bin": "1dffb636", + "webgpu/shader/execution/degrees.bin": "eb1badc7", + "webgpu/shader/execution/determinant.bin": "b811496", + "webgpu/shader/execution/distance.bin": "711d17c7", + "webgpu/shader/execution/dot.bin": "9ed893c1", + "webgpu/shader/execution/exp.bin": "929a566a", + "webgpu/shader/execution/exp2.bin": "794a6577", + "webgpu/shader/execution/faceForward.bin": "56892023", + "webgpu/shader/execution/floor.bin": "376f2eee", + "webgpu/shader/execution/fma.bin": "c2bc0bf2", + "webgpu/shader/execution/fract.bin": "37c4aa7f", + "webgpu/shader/execution/frexp.bin": "61808e98", + "webgpu/shader/execution/inverseSqrt.bin": "d7cecf73", + "webgpu/shader/execution/ldexp.bin": "21f36224", + "webgpu/shader/execution/length.bin": "dce92f51", + "webgpu/shader/execution/log.bin": "cc4f45d9", + "webgpu/shader/execution/log2.bin": "98794813", + "webgpu/shader/execution/max.bin": "d8f09e93", + "webgpu/shader/execution/min.bin": "401929fb", + "webgpu/shader/execution/mix.bin": "d4ad0c99", + "webgpu/shader/execution/modf.bin": "eb17c2d3", + "webgpu/shader/execution/normalize.bin": "118bd5b5", + "webgpu/shader/execution/pack2x16float.bin": "92f275c9", + "webgpu/shader/execution/pow.bin": "f69c78ba", + "webgpu/shader/execution/quantizeToF16.bin": "cbc504dd", + "webgpu/shader/execution/radians.bin": "63edd5eb", + "webgpu/shader/execution/reflect.bin": "e65cdeb0", + "webgpu/shader/execution/refract.bin": "90aec5f9", + "webgpu/shader/execution/round.bin": "50908a1c", + "webgpu/shader/execution/saturate.bin": "e28062ee", + "webgpu/shader/execution/sign.bin": "369cd937", + "webgpu/shader/execution/sin.bin": "73825256", + "webgpu/shader/execution/sinh.bin": "bb8220d6", + "webgpu/shader/execution/smoothstep.bin": "de551808", + "webgpu/shader/execution/sqrt.bin": "6df4c04e", + "webgpu/shader/execution/step.bin": "889e2845", + "webgpu/shader/execution/tan.bin": "298f37dc", + "webgpu/shader/execution/tanh.bin": "872a62cc", + "webgpu/shader/execution/transpose.bin": "46cc4aec", + "webgpu/shader/execution/trunc.bin": "3e3c3e01", + "webgpu/shader/execution/unpack2x16float.bin": "d1cddd57", + "webgpu/shader/execution/unpack2x16snorm.bin": "89c76a8d", + "webgpu/shader/execution/unpack2x16unorm.bin": "60f8cb10", + "webgpu/shader/execution/unpack4x8snorm.bin": "19641fdc", + "webgpu/shader/execution/unpack4x8unorm.bin": "a905109", + "webgpu/shader/execution/unary/af_arithmetic.bin": "1a5f4d56", + "webgpu/shader/execution/unary/af_assignment.bin": "29e94c74", + "webgpu/shader/execution/unary/bool_conversion.bin": "d6608993", + "webgpu/shader/execution/unary/f16_arithmetic.bin": "8745cd83", + "webgpu/shader/execution/unary/f16_conversion.bin": "928217cc", + "webgpu/shader/execution/unary/f32_arithmetic.bin": "f6eafc51", + "webgpu/shader/execution/unary/f32_conversion.bin": "3b4680ac", + "webgpu/shader/execution/unary/i32_arithmetic.bin": "c82920a1", + "webgpu/shader/execution/unary/i32_conversion.bin": "d8064b36", + "webgpu/shader/execution/unary/u32_conversion.bin": "e2e5a964", + "webgpu/shader/execution/unary/ai_assignment.bin": "7d28289c", + "webgpu/shader/execution/binary/ai_arithmetic.bin": "8da655c9", + "webgpu/shader/execution/unary/ai_arithmetic.bin": "2ccfba48", + "webgpu/shader/execution/binary/af_matrix_matrix_multiplication.bin": "ab42cf2b", + "webgpu/shader/execution/binary/af_matrix_scalar_multiplication.bin": "d91b8361", + "webgpu/shader/execution/binary/af_matrix_vector_multiplication.bin": "b94f1644", + "webgpu/shader/execution/derivatives.bin": "4d8a9451" } \ No newline at end of file diff --git a/src/webgpu/shader/execution/expression/call/builtin/textureSample.spec.ts b/src/webgpu/shader/execution/expression/call/builtin/textureSample.spec.ts index f5b01dfc6335..b56a940cef96 100644 --- a/src/webgpu/shader/execution/expression/call/builtin/textureSample.spec.ts +++ b/src/webgpu/shader/execution/expression/call/builtin/textureSample.spec.ts @@ -6,11 +6,22 @@ Must only be invoked in uniform control flow. `; import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; -import { GPUTest } from '../../../../../gpu_test.js'; +import { kEncodableTextureFormats, kTextureFormatInfo } from '../../../../../format_info.js'; +import { GPUTest, TextureTestMixin } from '../../../../../gpu_test.js'; +import { hashU32 } from '../../../../../util/math.js'; +import { kTexelRepresentationInfo } from '../../../../../util/texture/texel_data.js'; +import { + vec2, + createRandomTexelView, + TextureCall, + putDataInTextureThenDrawAndCheckResults, + generateSamplePoints, + kSamplePointMethods, +} from './texture_utils.js'; import { generateCoordBoundaries, generateOffsets } from './utils.js'; -export const g = makeTestGroup(GPUTest); +export const g = makeTestGroup(TextureTestMixin(GPUTest)); g.test('stage') .specURL('https://www.w3.org/TR/WGSL/#texturesample') @@ -70,11 +81,74 @@ Parameters: Values outside of this range will result in a shader-creation error. ` ) - .paramsSubcasesOnly(u => + .params(u => u - .combine('S', ['clamp-to-edge', 'repeat', 'mirror-repeat'] as const) - .combine('coords', generateCoordBoundaries(2)) - .combine('offset', generateOffsets(2)) + .combine('format', kEncodableTextureFormats) + .filter(t => { + const type = kTextureFormatInfo[t.format].color?.type; + return type === 'float' || type === 'unfilterable-float'; + }) + .combine('sample_points', kSamplePointMethods) + .combine('addressModeU', ['clamp-to-edge', 'repeat', 'mirror-repeat'] as const) + .combine('addressModeV', ['clamp-to-edge', 'repeat', 'mirror-repeat'] as const) + .combine('minFilter', ['nearest', 'linear'] as const) + .combine('offset', [false, true] as const) + ) + .beforeAllSubcases(t => { + const format = kTexelRepresentationInfo[t.params.format]; + t.skipIfTextureFormatNotSupported(t.params.format); + const hasFloat32 = format.componentOrder.some(c => { + const info = format.componentInfo[c]!; + return info.dataType === 'float' && info.bitLength === 32; + }); + if (hasFloat32) { + t.selectDeviceOrSkipTestCase('float32-filterable'); + } + }) + .fn(async t => { + const descriptor: GPUTextureDescriptor = { + format: t.params.format, + size: { width: 8, height: 8 }, + usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING, + }; + const texelView = createRandomTexelView(descriptor); + const calls: TextureCall[] = generateSamplePoints(50, t.params.minFilter === 'nearest', { + method: t.params.sample_points, + textureWidth: 8, + textureHeight: 8, + }).map((c, i) => { + const hash = hashU32(i) & 0xff; + return { + builtin: 'textureSample', + coordType: 'f', + coords: c, + offset: t.params.offset ? [(hash & 15) - 8, (hash >> 4) - 8] : undefined, + }; + }); + const sampler: GPUSamplerDescriptor = { + addressModeU: t.params.addressModeU, + addressModeV: t.params.addressModeV, + minFilter: t.params.minFilter, + magFilter: t.params.minFilter, + }; + const res = await putDataInTextureThenDrawAndCheckResults( + t.device, + { texels: texelView, descriptor }, + sampler, + calls + ); + t.expectOK(res); + }); + +g.test('sampled_2d_coords,derivatives') + .specURL('https://www.w3.org/TR/WGSL/#texturesample') + .desc( + ` +fn textureSample(t: texture_2d, s: sampler, coords: vec2) -> vec4 +fn textureSample(t: texture_2d, s: sampler, coords: vec2, offset: vec2) -> vec4 + +test mip level selection based on derivatives + ` ) .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 new file mode 100644 index 000000000000..fe2da53d5af5 --- /dev/null +++ b/src/webgpu/shader/execution/expression/call/builtin/texture_utils.ts @@ -0,0 +1,809 @@ +import { assert, range, unreachable } from '../../../../../../common/util/util.js'; +import { EncodableTextureFormat } from '../../../../../format_info.js'; +import { float32ToUint32 } from '../../../../../util/conversion.js'; +import { align, clamp, hashU32, lerp, quantizeToF32 } from '../../../../../util/math.js'; +import { + kTexelRepresentationInfo, + PerTexelComponent, + TexelRepresentationInfo, +} from '../../../../../util/texture/texel_data.js'; +import { TexelView } from '../../../../../util/texture/texel_view.js'; +import { createTextureFromTexelView } from '../../../../../util/texture.js'; +import { reifyExtent3D } from '../../../../../util/unions.js'; + +function getLimitValue(v: number) { + switch (v) { + case Number.POSITIVE_INFINITY: + return 1000; + case Number.NEGATIVE_INFINITY: + return -1000; + default: + return v; + } +} + +function getValueBetweenMinAndMaxTexelValueInclusive( + rep: TexelRepresentationInfo, + normalized: number +) { + return lerp( + getLimitValue(rep.numericRange!.min), + getLimitValue(rep.numericRange!.max), + normalized + ); +} + +/** + * Creates a TexelView filled with random values. + */ +export function createRandomTexelView(info: { + format: GPUTextureFormat; + size: GPUExtent3D; +}): TexelView { + const rep = kTexelRepresentationInfo[info.format as EncodableTextureFormat]; + const generator = (coords: Required): Readonly> => { + const texel: PerTexelComponent = {}; + 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); + } + return quantize(texel, rep); + }; + return TexelView.fromTexelsAsColors(info.format as EncodableTextureFormat, generator); +} + +export type vec2 = [number, number]; +export type vec3 = [number, number, number]; +export type vec4 = [number, number, number, number]; +export type Dimensionality = number | vec2 | vec3; + +type TextureCallArgKeys = keyof TextureCallArgs; +const kTextureCallArgNames: TextureCallArgKeys[] = [ + 'coords', + 'mipLevel', + 'arrayIndex', + 'ddx', + 'ddy', + 'offset', +]; + +export interface TextureCallArgs { + coords?: T; + mipLevel?: number; + arrayIndex?: number; + ddx?: T; + ddy?: T; + offset?: T; +} + +export interface TextureCall extends TextureCallArgs { + builtin: 'textureSample' | 'textureLoad'; + coordType: 'f'; +} + +function toArray(coords: Dimensionality): number[] { + if (coords instanceof Array) { + return coords; + } + return [coords]; +} + +function quantize(texel: PerTexelComponent, repl: TexelRepresentationInfo) { + return repl.bitsToNumber(repl.unpackBits(new Uint8Array(repl.pack(repl.encode(texel))))); +} + +function apply(a: number[], b: number[], op: (x: number, y: number) => number) { + assert(a.length === b.length, `apply(${a}, ${b}): arrays must have same length`); + return a.map((v, i) => op(v, b[i])); +} + +const add = (a: number[], b: number[]) => apply(a, b, (x, y) => x + y); + +export interface Texture { + texels: TexelView; + descriptor: GPUTextureDescriptor; +} + +/** + * Returns the expect value for a WGSL builtin texture function + */ +export function expected( + call: TextureCall, + texture: Texture, + sampler: GPUSamplerDescriptor +): PerTexelComponent { + const rep = kTexelRepresentationInfo[texture.texels.format]; + const textureExtent = reifyExtent3D(texture.descriptor.size); + const textureSize = [textureExtent.width, textureExtent.height, textureExtent.depthOrArrayLayers]; + const addressMode = [ + sampler.addressModeU ?? 'clamp-to-edge', + sampler.addressModeV ?? 'clamp-to-edge', + sampler.addressModeW ?? 'clamp-to-edge', + ]; + + const load = (at: number[]) => + texture.texels.color({ + x: Math.floor(at[0]), + y: Math.floor(at[1] ?? 0), + z: Math.floor(at[2] ?? 0), + }); + + switch (call.builtin) { + case 'textureSample': { + const coords = toArray(call.coords!); + + // convert normalized to absolute texel coordinate + // ┌───┬───┬───┬───┐ + // │ a │ │ │ │ norm: a = 1/8, b = 7/8 + // ├───┼───┼───┼───┤ abs: a = 0, b = 3 + // │ │ │ │ │ + // ├───┼───┼───┼───┤ + // │ │ │ │ │ + // ├───┼───┼───┼───┤ + // │ │ │ │ b │ + // └───┴───┴───┴───┘ + let at = coords.map((v, i) => v * textureSize[i] - 0.5); + + // Apply offset in whole texel units + if (call.offset !== undefined) { + at = add(at, toArray(call.offset)); + } + + const samples: { at: number[]; weight: number }[] = []; + + const filter = sampler.minFilter; + switch (filter) { + case 'linear': { + // 'p0' is the lower texel for 'at' + const p0 = at.map(v => Math.floor(v)); + // 'p1' is the higher texel for 'at' + const p1 = p0.map(v => v + 1); + + // interpolation weights for p0 and p1 + const p1W = at.map((v, i) => v - p0[i]); + const p0W = p1W.map(v => 1 - v); + + switch (coords.length) { + case 1: + samples.push({ at: p0, weight: p0W[0] }); + samples.push({ at: p1, weight: p1W[0] }); + break; + case 2: { + samples.push({ at: p0, weight: p0W[0] * p0W[1] }); + samples.push({ at: [p1[0], p0[1]], weight: p1W[0] * p0W[1] }); + samples.push({ at: [p0[0], p1[1]], weight: p0W[0] * p1W[1] }); + samples.push({ at: p1, weight: p1W[0] * p1W[1] }); + break; + } + } + break; + } + case 'nearest': { + const p = at.map(v => Math.round(quantizeToF32(v))); + samples.push({ at: p, weight: 1 }); + break; + } + default: + unreachable(); + } + + const out: PerTexelComponent = {}; + const ss = []; + for (const sample of samples) { + // Apply sampler address mode + const c = sample.at.map((v, i) => { + switch (addressMode[i]) { + case 'clamp-to-edge': + return clamp(v, { min: 0, max: textureSize[i] - 1 }); + case 'mirror-repeat': { + const n = Math.floor(v / textureSize[i]); + v = v - n * textureSize[i]; + return (n & 1) !== 0 ? textureSize[i] - v - 1 : v; + } + case 'repeat': + return v - Math.floor(v / textureSize[i]) * textureSize[i]; + default: + unreachable(); + } + }); + const v = load(c); + ss.push(v); + for (const component of rep.componentOrder) { + out[component] = (out[component] ?? 0) + v[component]! * sample.weight; + } + } + + return out; + } + case 'textureLoad': { + return load(toArray(call.coords!)); + } + } +} + +/** + * Puts random data in a texture, generates a shader that implements `calls` + * such that each call's result is written to the next consecutive texel of + * a rgba32float texture. It then checks the result of each call matches + * the expected result. + */ +export async function putDataInTextureThenDrawAndCheckResults( + device: GPUDevice, + texture: Texture, + sampler: GPUSamplerDescriptor, + calls: TextureCall[] +) { + const results = await doTextureCalls(device, texture, sampler, calls); + const errs: string[] = []; + const rep = kTexelRepresentationInfo[texture.texels.format]; + for (let callIdx = 0; callIdx < calls.length; callIdx++) { + const call = calls[callIdx]; + const got = results[callIdx]; + const expect = expected(call, texture, sampler); + + const gULP = rep.bitsToULPFromZero(rep.numberToBits(got)); + const eULP = rep.bitsToULPFromZero(rep.numberToBits(expect)); + for (const component of rep.componentOrder) { + const g = got[component]!; + const e = expect[component]!; + const absDiff = Math.abs(g - e); + const ulpDiff = Math.abs(gULP[component]! - eULP[component]!); + const relDiff = absDiff / Math.max(Math.abs(g), Math.abs(e)); + if (ulpDiff > 3 && relDiff > 0.03) { + const desc = describeTextureCall(call); + errs.push(`component was not as expected: + call: ${desc} + component: ${component} + got: ${g} + expected: ${e} + abs diff: ${absDiff.toFixed(4)} + rel diff: ${(relDiff * 100).toFixed(2)}% + ulp diff: ${ulpDiff} + sample points: +`); + const expectedSamplePoints = [ + 'expected:', + ...(await identifySamplePoints(texture.descriptor, (texels: TexelView) => { + return Promise.resolve( + expected(call, { texels, descriptor: texture.descriptor }, sampler) + ); + })), + ]; + const gotSamplePoints = [ + 'got:', + ...(await identifySamplePoints( + texture.descriptor, + async (texels: TexelView) => + ( + await doTextureCalls(device, { texels, descriptor: texture.descriptor }, sampler, [ + call, + ]) + )[0] + )), + ]; + errs.push(layoutTwoColumns(expectedSamplePoints, gotSamplePoints).join('\n')); + errs.push('', ''); + } + } + } + + return errs.length > 0 ? new Error(errs.join('\n')) : undefined; +} + +/** + * Generates a text art grid showing which texels were sampled + * followed by a list of the samples and the weights used for each + * component. + * + * Example: + * + * 0 1 2 3 4 5 6 7 + * ┌───┬───┬───┬───┬───┬───┬───┬───┐ + * 0 │ │ │ │ │ │ │ │ │ + * ├───┼───┼───┼───┼───┼───┼───┼───┤ + * 1 │ │ │ │ │ │ │ │ a │ + * ├───┼───┼───┼───┼───┼───┼───┼───┤ + * 2 │ │ │ │ │ │ │ │ b │ + * ├───┼───┼───┼───┼───┼───┼───┼───┤ + * 3 │ │ │ │ │ │ │ │ │ + * ├───┼───┼───┼───┼───┼───┼───┼───┤ + * 4 │ │ │ │ │ │ │ │ │ + * ├───┼───┼───┼───┼───┼───┼───┼───┤ + * 5 │ │ │ │ │ │ │ │ │ + * ├───┼───┼───┼───┼───┼───┼───┼───┤ + * 6 │ │ │ │ │ │ │ │ │ + * ├───┼───┼───┼───┼───┼───┼───┼───┤ + * 7 │ │ │ │ │ │ │ │ │ + * └───┴───┴───┴───┴───┴───┴───┴───┘ + * a: at: [7, 1], weights: [R: 0.75000] + * b: at: [7, 2], weights: [R: 0.25000] + */ +async function identifySamplePoints( + info: GPUTextureDescriptor, + run: (texels: TexelView) => Promise> +) { + const textureSize = reifyExtent3D(info.size); + const numTexels = textureSize.width * textureSize.height; + const rep = kTexelRepresentationInfo[info.format as EncodableTextureFormat]; + + // Identify all the texels that are sampled, and their weights. + const sampledTexelWeights = new Map>(); + const unclassifiedStack = [new Set(range(numTexels, v => v))]; + while (unclassifiedStack.length > 0) { + // Pop the an unclassified texels stack + const unclassified = unclassifiedStack.pop()!; + + // Split unclassified texels evenly into two new sets + const setA = new Set(); + const setB = new Set(); + [...unclassified.keys()].forEach((t, i) => ((i & 1) === 0 ? setA : setB).add(t)); + + // Push setB to the unclassified texels stack + if (setB.size > 0) { + unclassifiedStack.push(setB); + } + + // See if any of the texels in setA were sampled. + const results = await run( + TexelView.fromTexelsAsColors( + info.format as EncodableTextureFormat, + (coords: Required): Readonly> => { + const isCandidate = setA.has(coords.x + coords.y * textureSize.width); + const texel: PerTexelComponent = {}; + for (const component of rep.componentOrder) { + texel[component] = isCandidate ? 1 : 0; + } + return texel; + } + ) + ); + if (rep.componentOrder.some(c => results[c] !== 0)) { + // One or more texels of setA were sampled. + if (setA.size === 1) { + // We identified a specific texel was sampled. + // As there was only one texel in the set, results holds the sampling weights. + setA.forEach(texel => sampledTexelWeights.set(texel, results)); + } else { + // More than one texel in the set. Needs splitting. + unclassifiedStack.push(setA); + } + } + } + + // ┌───┬───┬───┬───┐ + // │ a │ │ │ │ + // ├───┼───┼───┼───┤ + // │ │ │ │ │ + // ├───┼───┼───┼───┤ + // │ │ │ │ │ + // ├───┼───┼───┼───┤ + // │ │ │ │ b │ + // └───┴───┴───┴───┘ + const letter = (idx: number) => String.fromCharCode(97 + idx); // 97: 'a' + const orderedTexelIndices: number[] = []; + const lines: string[] = []; + { + let line = ' '; + for (let x = 0; x < textureSize.width; x++) { + line += ` ${x} `; + } + lines.push(line); + } + { + let line = ' ┌'; + for (let x = 0; x < textureSize.width; x++) { + line += x === textureSize.width - 1 ? '───┐' : '───┬'; + } + lines.push(line); + } + for (let y = 0; y < textureSize.height; y++) { + { + let line = `${y} │`; + for (let x = 0; x < textureSize.width; x++) { + const texelIdx = x + y * textureSize.height; + const weight = sampledTexelWeights.get(texelIdx); + if (weight !== undefined) { + line += ` ${letter(orderedTexelIndices.length)} │`; + orderedTexelIndices.push(texelIdx); + } else { + line += ' │'; + } + } + lines.push(line); + } + if (y < textureSize.height - 1) { + let line = ' ├'; + for (let x = 0; x < textureSize.width; x++) { + line += x === textureSize.width - 1 ? '───┤' : '───┼'; + } + lines.push(line); + } + } + { + let line = ' └'; + for (let x = 0; x < textureSize.width; x++) { + line += x === textureSize.width - 1 ? '───┘' : '───┴'; + } + lines.push(line); + } + + orderedTexelIndices.forEach((texelIdx, i) => { + const weights = sampledTexelWeights.get(texelIdx)!; + const y = Math.floor(texelIdx / textureSize.width); + const x = texelIdx - y * textureSize.height; + const w = rep.componentOrder.map(c => `${c}: ${weights[c]?.toFixed(5)}`).join(', '); + lines.push(`${letter(i)}: at: [${x}, ${y}], weights: [${w}]`); + }); + return lines; +} + +function layoutTwoColumns(columnA: string[], columnB: string[]) { + const widthA = Math.max(...columnA.map(l => l.length)); + const lines = Math.max(columnA.length, columnB.length); + const out: string[] = new Array(lines); + for (let line = 0; line < lines; line++) { + const a = columnA[line] ?? ''; + const b = columnB[line] ?? ''; + out[line] = `${a}${' '.repeat(widthA - a.length)} | ${b}`; + } + return out; +} + +export const kSamplePointMethods = ['texel-centre', 'spiral'] as const; +export type SamplePointMethods = (typeof kSamplePointMethods)[number]; + +/** + * Generates an array of coordinates at which to sample a texture. + */ +export function generateSamplePoints( + n: number, + nearest: boolean, + args: + | { + method: 'texel-centre'; + textureWidth: number; + textureHeight: number; + } + | { + method: 'spiral'; + radius?: number; + loops?: number; + textureWidth: number; + textureHeight: number; + } +) { + const out: vec2[] = []; + switch (args.method) { + case 'texel-centre': { + for (let i = 0; i < n; i++) { + const r = hashU32(i); + const x = Math.floor(lerp(0, args.textureWidth - 1, (r & 0xffff) / 0xffff)) + 0.5; + const y = Math.floor(lerp(0, args.textureHeight - 1, (r >>> 16) / 0xffff)) + 0.5; + out.push([x / args.textureWidth, y / args.textureHeight]); + } + break; + } + case 'spiral': { + for (let i = 0; i < n; i++) { + const f = i / (Math.max(n, 2) - 1); + const r = (args.radius ?? 1.5) * f; + const a = (args.loops ?? 2) * 2 * Math.PI * f; + out.push([0.5 + r * Math.cos(a), 0.5 + r * Math.sin(a)]); + } + break; + } + } + // Samplers across devices use different methods to interpolate. + // Quantizing the texture coordinates seems to hit coords that produce + // comparable results to our computed results. + // Note: This value works with 8x8 textures. Other sizes have not been tested. + // Values that worked for reference: + // Win 11, NVidia 2070 Super: 16 + // Linux, AMD Radeon Pro WX 3200: 256 + // MacOS, M1 Mac: 256 + const kSubdivisionsPerTexel = 4; + const q = [args.textureWidth * kSubdivisionsPerTexel, args.textureHeight * kSubdivisionsPerTexel]; + return out.map( + c => + c.map((v, i) => { + // Quantize to kSubdivisionsPerPixel + const v1 = Math.floor(v * q[i]); + // If it's nearest and we're on the edge of a texel then move us off the edge + // since the edge could choose one texel or another in nearest mode + const v2 = nearest && v1 % kSubdivisionsPerTexel === 0 ? v1 + 1 : v1; + // Convert back to texture coords + return v2 / q[i]; + }) as vec2 + ); +} + +function wgslTypeFor(data: Dimensionality, type: 'f' | 'i' | 'u'): string { + if (data instanceof Array) { + switch (data.length) { + case 2: + return `vec2${type}`; + case 3: + return `vec3${type}`; + } + } + return '${type}32'; +} + +function wgslExpr(data: number | vec2 | vec3 | vec4): string { + if (data instanceof Array) { + switch (data.length) { + case 2: + return `vec2(${data.map(v => v.toString()).join(', ')})`; + case 3: + return `vec3(${data.map(v => v.toString()).join(', ')})`; + } + } + return data.toString(); +} + +function binKey(call: TextureCall): string { + const keys: string[] = []; + for (const name of kTextureCallArgNames) { + const value = call[name]; + if (value !== undefined) { + if (name === 'offset') { + // offset must be a constant expression + keys.push(`${name}: ${wgslExpr(value)}`); + } else { + keys.push(`${name}: ${wgslTypeFor(value, call.coordType)}`); + } + } + } + return `${call.builtin}(${keys.join(', ')})`; +} + +function buildBinnedCalls(calls: TextureCall[]) { + const args: string[] = ['T']; // All texture builtins take the texture as the first argument + const fields: string[] = []; + const data: number[] = []; + + const prototype = calls[0]; + if (prototype.builtin.startsWith('textureSample')) { + // textureSample*() builtins take a sampler as the second argument + args.push('S'); + } + + for (const name of kTextureCallArgNames) { + const value = prototype[name]; + if (value !== undefined) { + if (name === 'offset') { + args.push(`/* offset */ ${wgslExpr(value)}`); + } else { + args.push(`args.${name}`); + fields.push(`@align(16) ${name} : ${wgslTypeFor(value, prototype.coordType)}`); + } + } + } + + for (const call of calls) { + for (const name of kTextureCallArgNames) { + const value = call[name]; + assert( + (prototype[name] === undefined) === (value === undefined), + 'texture calls are not binned correctly' + ); + if (value !== undefined && name !== 'offset') { + const bitcastToU32 = (value: number) => { + if (calls[0].coordType === 'f') { + return float32ToUint32(value); + } + return value; + }; + if (value instanceof Array) { + for (const c of value) { + data.push(bitcastToU32(c)); + } + } else { + data.push(bitcastToU32(value)); + } + // All fields are aligned to 16 bytes. + while ((data.length & 3) !== 0) { + data.push(0); + } + } + } + } + + const expr = `${prototype.builtin}(${args.join(', ')})`; + + return { expr, fields, data }; +} + +function binCalls(calls: TextureCall[]): number[][] { + const map = new Map(); // key to bin index + const bins: number[][] = []; + calls.forEach((call, callIdx) => { + const key = binKey(call); + const binIdx = map.get(key); + if (binIdx === undefined) { + map.set(key, bins.length); + bins.push([callIdx]); + } else { + bins[binIdx].push(callIdx); + } + }); + return bins; +} + +export function describeTextureCall(call: TextureCall): string { + const args: string[] = ['texture: T']; + if (call.builtin.startsWith('textureSample')) { + args.push('sampler: S'); + } + for (const name of kTextureCallArgNames) { + const value = call[name]; + if (value !== undefined) { + args.push(`${name}: ${wgslExpr(value)}`); + } + } + return `${call.builtin}(${args.join(', ')})`; +} + +/** + * Given a list of "calls", each one of which has a texture coordinate, + * generates a fragment shader that uses the fragment position as an index + * (position.y * 256 + position.x) That index is then used to look up a + * coordinate from a storage buffer which is used to call the WGSL texture + * function to read/sample the texture, and then write to an rgba32float + * texture. We then read the rgba32float texture for the per "call" results. + * + * Calls are "binned" by call parameters. Each bin has its own structure and + * field in the storage buffer. This allows the calls to be non-homogenous and + * each have their own data type for coordinates. + */ +export async function doTextureCalls( + device: GPUDevice, + texture: Texture, + sampler: GPUSamplerDescriptor, + calls: TextureCall[] +) { + let structs = ''; + let body = ''; + let dataFields = ''; + const data: number[] = []; + let callCount = 0; + const binned = binCalls(calls); + binned.forEach((binCalls, binIdx) => { + const b = buildBinnedCalls(binCalls.map(callIdx => calls[callIdx])); + structs += `struct Args${binIdx} { + ${b.fields.join(', \n')} +} +`; + dataFields += ` args${binIdx} : array, +`; + body += ` + { + let is_active = (frag_idx >= ${callCount}) & (frag_idx < ${callCount + binCalls.length}); + let args = data.args${binIdx}[frag_idx - ${callCount}]; + let call = ${b.expr}; + result = select(result, call, is_active); + } +`; + callCount += binCalls.length; + data.push(...b.data); + }); + + const dataBuffer = device.createBuffer({ + size: data.length * 4, + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.STORAGE, + }); + device.queue.writeBuffer(dataBuffer, 0, new Uint32Array(data)); + + const rtWidth = 256; + const renderTarget = device.createTexture({ + format: 'rgba32float', + size: { width: rtWidth, height: Math.ceil(calls.length / rtWidth) }, + usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT, + }); + + const code = ` +${structs} + +struct Data { +${dataFields} +} + +@vertex +fn vs_main(@builtin(vertex_index) vertex_index : u32) -> @builtin(position) vec4f { + let positions = array( + vec4f(-1, 1, 0, 1), vec4f( 1, 1, 0, 1), + vec4f(-1, -1, 0, 1), vec4f( 1, -1, 0, 1), + ); + return positions[vertex_index]; +} + +@group(0) @binding(0) var T : texture_2d; +@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 { + let frag_idx = u32(frag_pos.x) + u32(frag_pos.y) * ${renderTarget.width}; + var result : vec4f; +${body} + return result; +} +`; + const shaderModule = device.createShaderModule({ code }); + + const pipeline = device.createRenderPipeline({ + layout: 'auto', + vertex: { module: shaderModule, entryPoint: 'vs_main' }, + fragment: { + module: shaderModule, + entryPoint: 'fs_main', + targets: [{ format: renderTarget.format }], + }, + primitive: { topology: 'triangle-strip', cullMode: 'none' }, + }); + + const gpuTexture = createTextureFromTexelView(device, texture.texels, texture.descriptor); + const gpuSampler = device.createSampler(sampler); + + const bindGroup = device.createBindGroup({ + layout: pipeline.getBindGroupLayout(0), + entries: [ + { binding: 0, resource: gpuTexture.createView() }, + { binding: 1, resource: gpuSampler }, + { binding: 2, resource: { buffer: dataBuffer } }, + ], + }); + + const bytesPerRow = align(16 * renderTarget.width, 256); + const resultBuffer = device.createBuffer({ + size: renderTarget.height * bytesPerRow, + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, + }); + const encoder = device.createCommandEncoder(); + + const renderPass = encoder.beginRenderPass({ + colorAttachments: [{ view: renderTarget.createView(), loadOp: 'clear', storeOp: 'store' }], + }); + + renderPass.setPipeline(pipeline); + renderPass.setBindGroup(0, bindGroup); + renderPass.draw(4); + renderPass.end(); + encoder.copyTextureToBuffer( + { texture: renderTarget }, + { buffer: resultBuffer, bytesPerRow }, + { width: renderTarget.width, height: renderTarget.height } + ); + device.queue.submit([encoder.finish()]); + + await resultBuffer.mapAsync(GPUMapMode.READ); + + const view = TexelView.fromTextureDataByReference( + renderTarget.format as EncodableTextureFormat, + new Uint8Array(resultBuffer.getMappedRange()), + { + bytesPerRow, + rowsPerImage: renderTarget.height, + subrectOrigin: [0, 0, 0], + subrectSize: [renderTarget.width, renderTarget.height], + } + ); + + let outIdx = 0; + const out = new Array>(calls.length); + for (const bin of binned) { + for (const callIdx of bin) { + const x = outIdx % rtWidth; + const y = Math.floor(outIdx / rtWidth); + out[callIdx] = view.color({ x, y, z: 0 }); + outIdx++; + } + } + + renderTarget.destroy(); + gpuTexture.destroy(); + resultBuffer.destroy(); + + return out; +} diff --git a/src/webgpu/util/math.ts b/src/webgpu/util/math.ts index 45ea48557b89..20d7818df65d 100644 --- a/src/webgpu/util/math.ts +++ b/src/webgpu/util/math.ts @@ -413,6 +413,25 @@ export function oneULPF32(target: number, mode: FlushMode = 'flush'): number { } } +/** + * @returns an integer value between 0..0xffffffff using a simple non-cryptographic hash function + * @param values integers to generate hash from. + */ +export function hashU32(...values: number[]) { + let n = 0x3504_f333; + for (const v of values) { + n = v + (n << 7) + (n >>> 1); + n = (n * 0x29493) & 0xffff_ffff; + } + n ^= n >>> 8; + n += n << 15; + n = n & 0xffff_ffff; + if (n < 0) { + n = ~n * 2 + 1; + } + return n; +} + /** * @returns ulp(x), the unit of least precision for a specific number as a 32-bit float * diff --git a/src/webgpu/util/texture/texel_data.ts b/src/webgpu/util/texture/texel_data.ts index 42490d800b6b..0555ac5920d8 100644 --- a/src/webgpu/util/texture/texel_data.ts +++ b/src/webgpu/util/texture/texel_data.ts @@ -1,5 +1,6 @@ import { assert, unreachable } from '../../../common/util/util.js'; import { UncompressedTextureFormat, EncodableTextureFormat } from '../../format_info.js'; +import { kValue } from '../constants.js'; import { assertInIntegerRange, float32ToFloatBits, @@ -424,6 +425,8 @@ function makeNormalizedInfo( } const dataType: ComponentDataType = opt.signed ? 'snorm' : 'unorm'; + const min = opt.signed ? -1 : 0; + const max = 1; return { componentOrder, componentInfo: makePerTexelComponent(componentOrder, { @@ -438,7 +441,7 @@ function makeNormalizedInfo( numberToBits, bitsToNumber, bitsToULPFromZero, - numericRange: { min: opt.signed ? -1 : 0, max: 1 }, + numericRange: { min, max, finiteMin: min, finiteMax: max }, }; } @@ -454,9 +457,9 @@ function makeIntegerInfo( opt: { signed: boolean } ): TexelRepresentationInfo { assert(bitLength <= 32); - const numericRange = opt.signed - ? { min: -(2 ** (bitLength - 1)), max: 2 ** (bitLength - 1) - 1 } - : { min: 0, max: 2 ** bitLength - 1 }; + const min = opt.signed ? -(2 ** (bitLength - 1)) : 0; + const max = opt.signed ? 2 ** (bitLength - 1) - 1 : 2 ** bitLength - 1; + const numericRange = { min, max, finiteMin: min, finiteMax: max }; const maxUnsignedValue = 2 ** bitLength; const encode = applyEach( (n: number) => (assertInIntegerRange(n, bitLength, opt.signed), n), @@ -576,8 +579,13 @@ function makeFloatInfo( bitsToNumber, bitsToULPFromZero, numericRange: restrictedDepth - ? { min: 0, max: 1 } - : { min: Number.NEGATIVE_INFINITY, max: Number.POSITIVE_INFINITY }, + ? { min: 0, max: 1, finiteMin: 0, finiteMax: 1 } + : { + min: Number.NEGATIVE_INFINITY, + max: Number.POSITIVE_INFINITY, + finiteMin: bitLength === 32 ? kValue.f32.negative.min : kValue.f16.negative.min, + finiteMax: bitLength === 32 ? kValue.f32.positive.max : kValue.f16.positive.max, + }, }; } @@ -592,6 +600,7 @@ const identity = (n: number) => n; const kFloat11Format = { signed: 0, exponentBits: 5, mantissaBits: 6, bias: 15 } as const; const kFloat10Format = { signed: 0, exponentBits: 5, mantissaBits: 5, bias: 15 } as const; +export type PerComponentFiniteMax = Record; export type TexelRepresentationInfo = { /** Order of components in the packed representation. */ readonly componentOrder: TexelComponent[]; @@ -619,7 +628,12 @@ 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 }; + readonly numericRange: null | { + min: number; + max: number; + finiteMin: number; + finiteMax: number | PerComponentFiniteMax; + }; // Add fields as needed }; @@ -765,7 +779,7 @@ export const kTexelRepresentationInfo: { A: normalizedIntegerAsFloat(components.A!, 2, false), }), bitsToULPFromZero: components => components, - numericRange: { min: 0, max: 1 }, + numericRange: { min: 0, max: 1, finiteMin: 0, finiteMax: 1 }, }, rg11b10ufloat: { componentOrder: kRGB, @@ -809,7 +823,16 @@ export const kTexelRepresentationInfo: { G: floatBitsToNormalULPFromZero(components.G!, kFloat11Format), B: floatBitsToNormalULPFromZero(components.B!, kFloat10Format), }), - numericRange: { min: 0, max: Number.POSITIVE_INFINITY }, + numericRange: { + min: 0, + max: Number.POSITIVE_INFINITY, + finiteMin: 0, + finiteMax: { + R: floatBitsToNumber(0b111_1011_1111, kFloat11Format), + G: floatBitsToNumber(0b111_1011_1111, kFloat11Format), + B: floatBitsToNumber(0b11_1101_1111, kFloat10Format), + } as PerComponentFiniteMax, + }, }, rgb9e5ufloat: { componentOrder: kRGB, @@ -854,7 +877,12 @@ export const kTexelRepresentationInfo: { G: floatBitsToNormalULPFromZero(components.G!, kUFloat9e5Format), B: floatBitsToNormalULPFromZero(components.B!, kUFloat9e5Format), }), - numericRange: { min: 0, max: Number.POSITIVE_INFINITY }, + numericRange: { + min: 0, + max: Number.POSITIVE_INFINITY, + finiteMin: 0, + finiteMax: ufloatM9E5BitsToNumber(0b11_1111_1111_1111, kUFloat9e5Format), + }, }, depth32float: makeFloatInfo([TexelComponent.Depth], 32, { restrictedDepth: true }), depth16unorm: makeNormalizedInfo([TexelComponent.Depth], 16, { signed: false, sRGB: false }), @@ -868,7 +896,7 @@ export const kTexelRepresentationInfo: { numberToBits: () => unreachable('depth24plus has no representation'), bitsToNumber: () => unreachable('depth24plus has no representation'), bitsToULPFromZero: () => unreachable('depth24plus has no representation'), - numericRange: { min: 0, max: 1 }, + numericRange: { min: 0, max: 1, finiteMin: 0, finiteMax: 1 }, }, stencil8: makeIntegerInfo([TexelComponent.Stencil], 8, { signed: false }), 'depth32float-stencil8': {