import { assert, unreachable } from '../../../common/util/util.js'; import { UncompressedTextureFormat, EncodableTextureFormat } from '../../format_info.js'; import { kValue } from '../constants.js'; import { assertInIntegerRange, float32ToFloatBits, float32ToFloat16Bits, floatAsNormalizedInteger, gammaCompress, gammaDecompress, normalizedIntegerAsFloat, packRGB9E5UFloat, floatBitsToNumber, float16BitsToFloat32, floatBitsToNormalULPFromZero, kFloat32Format, kFloat16Format, kUFloat9e5Format, numberToFloat32Bits, float32BitsToNumber, numberToFloatBits, ufloatM9E5BitsToNumber, } from '../conversion.js'; import { clamp, signExtend } from '../math.js'; /** A component of a texture format: R, G, B, A, Depth, or Stencil. */ export const enum TexelComponent { R = 'R', G = 'G', B = 'B', A = 'A', Depth = 'Depth', Stencil = 'Stencil', } /** Arbitrary data, per component of a texel format. */ export type PerTexelComponent = { [c in TexelComponent]?: T }; /** How a component is encoded in its bit range of a texel format. */ export type ComponentDataType = 'uint' | 'sint' | 'unorm' | 'snorm' | 'float' | 'ufloat' | null; /** * Maps component values to component values * @param {PerTexelComponent} components - The input components. * @returns {PerTexelComponent} The new output components. */ type ComponentMapFn = (components: PerTexelComponent) => PerTexelComponent; /** * Packs component values as an ArrayBuffer * @param {PerTexelComponent} components - The input components. * @returns {ArrayBuffer} The packed data. */ type ComponentPackFn = (components: PerTexelComponent) => ArrayBuffer; /** Unpacks component values from a Uint8Array */ type ComponentUnpackFn = (data: Uint8Array) => PerTexelComponent; /** * Create a PerTexelComponent object filled with the same value for all components. * @param {TexelComponent[]} components - The component names. * @param {T} value - The value to assign to each component. * @returns {PerTexelComponent} */ function makePerTexelComponent(components: TexelComponent[], value: T): PerTexelComponent { const values: PerTexelComponent = {}; for (const c of components) { values[c] = value; } return values; } /** * Create a function which applies clones a `PerTexelComponent` and then applies the * function `fn` to each component of `components`. * @param {(value: number) => number} fn - The mapping function to apply to component values. * @param {TexelComponent[]} components - The component names. * @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, 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]!, c); } return values; }; } /** * A `ComponentMapFn` for encoding sRGB. * @param {PerTexelComponent} components - The input component values. * @returns {TexelComponent} Gamma-compressed copy of `components`. */ const encodeSRGB: ComponentMapFn = components => { assert( components.R !== undefined && components.G !== undefined && components.B !== undefined, 'sRGB requires all of R, G, and B components' ); return applyEach(gammaCompress, kRGB)(components); }; /** * A `ComponentMapFn` for decoding sRGB. * @param {PerTexelComponent} components - The input component values. * @returns {TexelComponent} Gamma-decompressed copy of `components`. */ const decodeSRGB: ComponentMapFn = components => { components = Object.assign({}, components); assert( components.R !== undefined && components.G !== undefined && components.B !== undefined, 'sRGB requires all of R, G, and B components' ); return applyEach(gammaDecompress, kRGB)(components); }; /** * Makes a `ComponentMapFn` for clamping values to the specified range. */ export function makeClampToRange(format: EncodableTextureFormat): ComponentMapFn { const repr = kTexelRepresentationInfo[format]; assert(repr.numericRange !== null, 'Format has unknown numericRange'); 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 // end of each test group. That would allow for caching of larger buffers (though it's unclear how // ofter larger buffers are used by packComponents.) const smallComponentDataViews = new Map(); function getComponentDataView(byteLength: number): DataView { if (byteLength > 32) { const buffer = new ArrayBuffer(byteLength); return new DataView(buffer); } let dataView = smallComponentDataViews.get(byteLength); if (!dataView) { const buffer = new ArrayBuffer(byteLength); dataView = new DataView(buffer); smallComponentDataViews.set(byteLength, dataView); } return dataView; } /** * Helper function to pack components as an ArrayBuffer. * @param {TexelComponent[]} componentOrder - The order of the component data. * @param {PerTexelComponent} components - The input component values. * @param {number | PerTexelComponent} bitLengths - The length in bits of each component. * If a single number, all components are the same length, otherwise this is a dictionary of * per-component bit lengths. * @param {ComponentDataType | PerTexelComponent} componentDataTypes - * The type of the data in `components`. If a single value, all components have the same value. * Otherwise, this is a dictionary of per-component data types. * @returns {ArrayBuffer} The packed component data. */ function packComponents( componentOrder: TexelComponent[], components: PerTexelComponent, bitLengths: number | PerTexelComponent, componentDataTypes: ComponentDataType | PerTexelComponent ): ArrayBuffer { let bitLengthMap; let totalBitLength; if (typeof bitLengths === 'number') { bitLengthMap = makePerTexelComponent(componentOrder, bitLengths); totalBitLength = bitLengths * componentOrder.length; } else { bitLengthMap = bitLengths; totalBitLength = Object.entries(bitLengthMap).reduce((acc, [, value]) => { assert(value !== undefined); return acc + value; }, 0); } assert(totalBitLength % 8 === 0); const componentDataTypeMap = typeof componentDataTypes === 'string' || componentDataTypes === null ? makePerTexelComponent(componentOrder, componentDataTypes) : componentDataTypes; const dataView = getComponentDataView(totalBitLength / 8); let bitOffset = 0; for (const c of componentOrder) { const value = components[c]; const type = componentDataTypeMap[c]; const bitLength = bitLengthMap[c]; assert(value !== undefined); assert(type !== undefined); assert(bitLength !== undefined); const byteOffset = Math.floor(bitOffset / 8); const byteLength = Math.ceil(bitLength / 8); switch (type) { case 'uint': case 'unorm': if (byteOffset === bitOffset / 8 && byteLength === bitLength / 8) { switch (byteLength) { case 1: dataView.setUint8(byteOffset, value); break; case 2: dataView.setUint16(byteOffset, value, true); break; case 4: dataView.setUint32(byteOffset, value, true); break; default: unreachable(); } } else { // Packed representations are all 32-bit and use Uint as the data type. // ex.) rg10b11float, rgb10a2unorm switch (dataView.byteLength) { case 4: { const currentValue = dataView.getUint32(0, true); let mask = 0xffffffff; const bitsToClearRight = bitOffset; const bitsToClearLeft = 32 - (bitLength + bitOffset); mask = (mask >>> bitsToClearRight) << bitsToClearRight; mask = (mask << bitsToClearLeft) >>> bitsToClearLeft; const newValue = (currentValue & ~mask) | (value << bitOffset); dataView.setUint32(0, newValue, true); break; } default: unreachable(); } } break; case 'sint': case 'snorm': assert(byteOffset === bitOffset / 8 && byteLength === bitLength / 8); switch (byteLength) { case 1: dataView.setInt8(byteOffset, value); break; case 2: dataView.setInt16(byteOffset, value, true); break; case 4: dataView.setInt32(byteOffset, value, true); break; default: unreachable(); } break; case 'float': assert(byteOffset === bitOffset / 8 && byteLength === bitLength / 8); switch (byteLength) { case 4: dataView.setFloat32(byteOffset, value, true); break; default: unreachable(); } break; case 'ufloat': case null: unreachable(); } bitOffset += bitLength; } return dataView.buffer; } /** * Unpack substrings of bits from a Uint8Array, e.g. [8,8,8,8] or [9,9,9,5]. */ function unpackComponentsBits( componentOrder: TexelComponent[], byteView: Uint8Array, bitLengths: number | PerTexelComponent ): PerTexelComponent { const components = makePerTexelComponent(componentOrder, 0); let bitLengthMap; let totalBitLength; if (typeof bitLengths === 'number') { let index = 0; // Optimized cases for when the bit lengths are all a well aligned value. switch (bitLengths) { case 8: for (const c of componentOrder) { components[c] = byteView[index++]; } return components; case 16: { const shortView = new Uint16Array(byteView.buffer, byteView.byteOffset); for (const c of componentOrder) { components[c] = shortView[index++]; } return components; } case 32: { const longView = new Uint32Array(byteView.buffer, byteView.byteOffset); for (const c of componentOrder) { components[c] = longView[index++]; } return components; } } bitLengthMap = makePerTexelComponent(componentOrder, bitLengths); totalBitLength = bitLengths * componentOrder.length; } else { bitLengthMap = bitLengths; totalBitLength = Object.entries(bitLengthMap).reduce((acc, [, value]) => { assert(value !== undefined); return acc + value; }, 0); } assert(totalBitLength % 8 === 0); const dataView = new DataView(byteView.buffer, byteView.byteOffset, byteView.byteLength); let bitOffset = 0; for (const c of componentOrder) { const bitLength = bitLengthMap[c]; assert(bitLength !== undefined); let value: number; const byteOffset = Math.floor(bitOffset / 8); const byteLength = Math.ceil(bitLength / 8); if (byteOffset === bitOffset / 8 && byteLength === bitLength / 8) { switch (byteLength) { case 1: value = dataView.getUint8(byteOffset); break; case 2: value = dataView.getUint16(byteOffset, true); break; case 4: value = dataView.getUint32(byteOffset, true); break; default: unreachable(); } } else { // Packed representations are all 32-bit and use Uint as the data type. // ex.) rg10b11float, rgb10a2unorm assert(dataView.byteLength === 4); const word = dataView.getUint32(0, true); value = (word >>> bitOffset) & ((1 << bitLength) - 1); } bitOffset += bitLength; components[c] = value; } return components; } /** * Create an entry in `kTexelRepresentationInfo` for normalized integer texel data with constant * bitlength. * @param {TexelComponent[]} componentOrder - The order of the component data. * @param {number} bitLength - The number of bits in each component. * @param {{signed: boolean; sRGB: boolean}} opt - Boolean flags for `signed` and `sRGB`. */ function makeNormalizedInfo( componentOrder: TexelComponent[], bitLength: number, opt: { signed: boolean; sRGB: boolean } ): TexelRepresentationInfo { const encodeNonSRGB = applyEach( (n: number) => floatAsNormalizedInteger(n, bitLength, opt.signed), componentOrder ); const decodeNonSRGB = applyEach( (n: number) => normalizedIntegerAsFloat(n, bitLength, opt.signed), componentOrder ); const numberToBitsNonSRGB = applyEach( n => floatAsNormalizedInteger(n, bitLength, opt.signed), componentOrder ); let bitsToNumberNonSRGB: ComponentMapFn; if (opt.signed) { bitsToNumberNonSRGB = applyEach( n => normalizedIntegerAsFloat(signExtend(n, bitLength), bitLength, opt.signed), componentOrder ); } else { bitsToNumberNonSRGB = applyEach( n => normalizedIntegerAsFloat(n, bitLength, opt.signed), componentOrder ); } let encode: ComponentMapFn; let decode: ComponentMapFn; let numberToBits: ComponentMapFn; let bitsToNumber: ComponentMapFn; if (opt.sRGB) { encode = components => encodeNonSRGB(encodeSRGB(components)); decode = components => decodeSRGB(decodeNonSRGB(components)); numberToBits = components => numberToBitsNonSRGB(encodeSRGB(components)); bitsToNumber = components => decodeSRGB(bitsToNumberNonSRGB(components)); } else { encode = encodeNonSRGB; decode = decodeNonSRGB; numberToBits = numberToBitsNonSRGB; bitsToNumber = bitsToNumberNonSRGB; } let bitsToULPFromZero: ComponentMapFn; if (opt.signed) { const maxValue = (1 << (bitLength - 1)) - 1; // e.g. 127 for snorm8 bitsToULPFromZero = applyEach( n => Math.max(-maxValue, signExtend(n, bitLength)), componentOrder ); } else { bitsToULPFromZero = components => components; } const dataType: ComponentDataType = opt.signed ? 'snorm' : 'unorm'; const min = opt.signed ? -1 : 0; const max = 1; return { componentOrder, componentInfo: makePerTexelComponent(componentOrder, { dataType, bitLength, }), encode, decode, pack: (components: PerTexelComponent) => packComponents(componentOrder, components, bitLength, dataType), unpackBits: (data: Uint8Array) => unpackComponentsBits(componentOrder, data, bitLength), numberToBits, bitsToNumber, bitsToULPFromZero, numericRange: { min, max, finiteMin: min, finiteMax: max }, }; } /** * Create an entry in `kTexelRepresentationInfo` for integer texel data with constant bitlength. * @param {TexelComponent[]} componentOrder - The order of the component data. * @param {number} bitLength - The number of bits in each component. * @param {{signed: boolean}} opt - Boolean flag for `signed`. */ function makeIntegerInfo( componentOrder: TexelComponent[], bitLength: number, opt: { signed: boolean } ): TexelRepresentationInfo { assert(bitLength <= 32); 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), componentOrder ); const decode = applyEach( (n: number) => (assertInIntegerRange(n, bitLength, opt.signed), n), componentOrder ); const bitsToNumber = applyEach((n: number) => { const decodedN = opt.signed ? (n > numericRange.max ? n - maxUnsignedValue : n) : n; assertInIntegerRange(decodedN, bitLength, opt.signed); return decodedN; }, componentOrder); let bitsToULPFromZero: ComponentMapFn; if (opt.signed) { bitsToULPFromZero = applyEach(n => signExtend(n, bitLength), componentOrder); } else { bitsToULPFromZero = components => components; } const dataType: ComponentDataType = opt.signed ? 'sint' : 'uint'; const bitMask = (1 << bitLength) - 1; return { componentOrder, componentInfo: makePerTexelComponent(componentOrder, { dataType, bitLength, }), encode, decode, pack: (components: PerTexelComponent) => packComponents(componentOrder, components, bitLength, dataType), unpackBits: (data: Uint8Array) => unpackComponentsBits(componentOrder, data, bitLength), numberToBits: applyEach(v => v & bitMask, componentOrder), bitsToNumber, bitsToULPFromZero, numericRange, }; } /** * Create an entry in `kTexelRepresentationInfo` for floating point texel data with constant * bitlength. * @param {TexelComponent[]} componentOrder - The order of the component data. * @param {number} bitLength - The number of bits in each component. */ function makeFloatInfo( componentOrder: TexelComponent[], bitLength: number, { restrictedDepth = false }: { restrictedDepth?: boolean } = {} ): TexelRepresentationInfo { let encode: ComponentMapFn; let numberToBits; let bitsToNumber; let bitsToULPFromZero; switch (bitLength) { case 32: if (restrictedDepth) { encode = applyEach(v => { assert(v >= 0.0 && v <= 1.0, 'depth out of range'); return new Float32Array([v])[0]; }, componentOrder); } else { encode = applyEach(v => new Float32Array([v])[0], componentOrder); } numberToBits = applyEach(numberToFloat32Bits, componentOrder); bitsToNumber = applyEach(float32BitsToNumber, componentOrder); bitsToULPFromZero = applyEach( v => floatBitsToNormalULPFromZero(v, kFloat32Format), componentOrder ); break; case 16: if (restrictedDepth) { encode = applyEach(v => { assert(v >= 0.0 && v <= 1.0, 'depth out of range'); return float16BitsToFloat32(float32ToFloat16Bits(v)); }, componentOrder); } else { encode = applyEach(v => float16BitsToFloat32(float32ToFloat16Bits(v)), componentOrder); } numberToBits = applyEach(float32ToFloat16Bits, componentOrder); bitsToNumber = applyEach(float16BitsToFloat32, componentOrder); bitsToULPFromZero = applyEach( v => floatBitsToNormalULPFromZero(v, kFloat16Format), componentOrder ); break; default: unreachable(); } const decode = applyEach(identity, componentOrder); return { componentOrder, componentInfo: makePerTexelComponent(componentOrder, { dataType: 'float' as const, bitLength, }), encode, decode, pack: (components: PerTexelComponent) => { switch (bitLength) { case 16: components = applyEach(float32ToFloat16Bits, componentOrder)(components); return packComponents(componentOrder, components, 16, 'uint'); case 32: return packComponents(componentOrder, components, bitLength, 'float'); default: unreachable(); } }, unpackBits: (data: Uint8Array) => unpackComponentsBits(componentOrder, data, bitLength), numberToBits, bitsToNumber, bitsToULPFromZero, numericRange: restrictedDepth ? { 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, }, }; } const kR = [TexelComponent.R]; const kRG = [TexelComponent.R, TexelComponent.G]; const kRGB = [TexelComponent.R, TexelComponent.G, TexelComponent.B]; const kRGBA = [TexelComponent.R, TexelComponent.G, TexelComponent.B, TexelComponent.A]; const kBGRA = [TexelComponent.B, TexelComponent.G, TexelComponent.R, TexelComponent.A]; 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 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[]; /** Data type and bit length of each component in the format. */ readonly componentInfo: PerTexelComponent<{ dataType: ComponentDataType; bitLength: number; }>; /** Encode shader values into their data representation. ex.) float 1.0 -> unorm8 255 */ // MAINTENANCE_TODO: Replace with numberToBits? readonly encode: ComponentMapFn; /** Decode the data representation into the shader values. ex.) unorm8 255 -> float 1.0 */ // MAINTENANCE_TODO: Replace with bitsToNumber? readonly decode: ComponentMapFn; /** Pack texel component values into an ArrayBuffer. ex.) rg8unorm `{r: 0, g: 255}` -> 0xFF00 */ // MAINTENANCE_TODO: Replace with packBits? readonly pack: ComponentPackFn; /** Convert integer bit representations into numeric values, e.g. unorm8 255 -> numeric 1.0 */ readonly bitsToNumber: ComponentMapFn; /** Convert numeric values into integer bit representations, e.g. numeric 1.0 -> unorm8 255 */ readonly numberToBits: ComponentMapFn; /** Unpack integer bit representations from an ArrayBuffer, e.g. 0xFF00 -> rg8unorm [0,255] */ readonly unpackBits: ComponentUnpackFn; /** 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 | NumericRange | PerComponentNumericRange; // Add fields as needed }; export const kTexelRepresentationInfo: { readonly [k in UncompressedTextureFormat]: TexelRepresentationInfo; } = { .../* prettier-ignore */ { 'r8unorm': makeNormalizedInfo( kR, 8, { signed: false, sRGB: false }), 'r8snorm': makeNormalizedInfo( kR, 8, { signed: true, sRGB: false }), 'r8uint': makeIntegerInfo( kR, 8, { signed: false }), 'r8sint': makeIntegerInfo( kR, 8, { signed: true }), 'r16unorm': makeNormalizedInfo( kR, 16, { signed: false, sRGB: false }), 'r16snorm': makeNormalizedInfo( kR, 16, { signed: true, sRGB: false }), 'r16uint': makeIntegerInfo( kR, 16, { signed: false }), 'r16sint': makeIntegerInfo( kR, 16, { signed: true }), 'r16float': makeFloatInfo( kR, 16), 'rg8unorm': makeNormalizedInfo( kRG, 8, { signed: false, sRGB: false }), 'rg8snorm': makeNormalizedInfo( kRG, 8, { signed: true, sRGB: false }), 'rg8uint': makeIntegerInfo( kRG, 8, { signed: false }), 'rg8sint': makeIntegerInfo( kRG, 8, { signed: true }), 'r32uint': makeIntegerInfo( kR, 32, { signed: false }), 'r32sint': makeIntegerInfo( kR, 32, { signed: true }), 'r32float': makeFloatInfo( kR, 32), 'rg16unorm': makeNormalizedInfo( kRG, 16, { signed: false, sRGB: false }), 'rg16snorm': makeNormalizedInfo( kRG, 16, { signed: true, sRGB: false }), 'rg16uint': makeIntegerInfo( kRG, 16, { signed: false }), 'rg16sint': makeIntegerInfo( kRG, 16, { signed: true }), 'rg16float': makeFloatInfo( kRG, 16), 'rgba8unorm': makeNormalizedInfo(kRGBA, 8, { signed: false, sRGB: false }), 'rgba8unorm-srgb': makeNormalizedInfo(kRGBA, 8, { signed: false, sRGB: true }), 'rgba8snorm': makeNormalizedInfo(kRGBA, 8, { signed: true, sRGB: false }), 'rgba8uint': makeIntegerInfo( kRGBA, 8, { signed: false }), 'rgba8sint': makeIntegerInfo( kRGBA, 8, { signed: true }), 'bgra8unorm': makeNormalizedInfo(kBGRA, 8, { signed: false, sRGB: false }), 'bgra8unorm-srgb': makeNormalizedInfo(kBGRA, 8, { signed: false, sRGB: true }), 'rg32uint': makeIntegerInfo( kRG, 32, { signed: false }), 'rg32sint': makeIntegerInfo( kRG, 32, { signed: true }), 'rg32float': makeFloatInfo( kRG, 32), 'rgba16unorm': makeNormalizedInfo(kRGBA, 16, { signed: false, sRGB: false }), 'rgba16snorm': makeNormalizedInfo(kRGBA, 16, { signed: true, sRGB: false }), 'rgba16uint': makeIntegerInfo( kRGBA, 16, { signed: false }), 'rgba16sint': makeIntegerInfo( kRGBA, 16, { signed: true }), 'rgba16float': makeFloatInfo( kRGBA, 16), 'rgba32uint': makeIntegerInfo( kRGBA, 32, { signed: false }), 'rgba32sint': makeIntegerInfo( kRGBA, 32, { signed: true }), 'rgba32float': makeFloatInfo( kRGBA, 32), }, ...{ rgb10a2uint: { componentOrder: kRGBA, componentInfo: { R: { dataType: 'uint', bitLength: 10 }, G: { dataType: 'uint', bitLength: 10 }, B: { dataType: 'uint', bitLength: 10 }, A: { dataType: 'uint', bitLength: 2 }, }, encode: components => { assertInIntegerRange(components.R!, 10, false); assertInIntegerRange(components.G!, 10, false); assertInIntegerRange(components.B!, 10, false); assertInIntegerRange(components.A!, 2, false); return components; }, decode: components => { assertInIntegerRange(components.R!, 10, false); assertInIntegerRange(components.G!, 10, false); assertInIntegerRange(components.B!, 10, false); assertInIntegerRange(components.A!, 2, false); return components; }, pack: components => packComponents( kRGBA, components, { R: 10, G: 10, B: 10, A: 2, }, 'uint' ), unpackBits: (data: Uint8Array) => unpackComponentsBits(kRGBA, data, { R: 10, G: 10, B: 10, A: 2 }), numberToBits: components => ({ R: components.R! & 0x3ff, G: components.G! & 0x3ff, B: components.B! & 0x3ff, A: components.A! & 0x3, }), bitsToNumber: components => { assertInIntegerRange(components.R!, 10, false); assertInIntegerRange(components.G!, 10, false); assertInIntegerRange(components.B!, 10, false); assertInIntegerRange(components.A!, 2, false); return components; }, bitsToULPFromZero: components => components, 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, componentInfo: { R: { dataType: 'unorm', bitLength: 10 }, G: { dataType: 'unorm', bitLength: 10 }, B: { dataType: 'unorm', bitLength: 10 }, A: { dataType: 'unorm', bitLength: 2 }, }, encode: components => { return { R: floatAsNormalizedInteger(components.R ?? unreachable(), 10, false), G: floatAsNormalizedInteger(components.G ?? unreachable(), 10, false), B: floatAsNormalizedInteger(components.B ?? unreachable(), 10, false), A: floatAsNormalizedInteger(components.A ?? unreachable(), 2, false), }; }, decode: components => { return { R: normalizedIntegerAsFloat(components.R ?? unreachable(), 10, false), G: normalizedIntegerAsFloat(components.G ?? unreachable(), 10, false), B: normalizedIntegerAsFloat(components.B ?? unreachable(), 10, false), A: normalizedIntegerAsFloat(components.A ?? unreachable(), 2, false), }; }, pack: components => packComponents( kRGBA, components, { R: 10, G: 10, B: 10, A: 2, }, 'uint' ), unpackBits: (data: Uint8Array) => unpackComponentsBits(kRGBA, data, { R: 10, G: 10, B: 10, A: 2 }), numberToBits: components => ({ R: floatAsNormalizedInteger(components.R ?? unreachable(), 10, false), G: floatAsNormalizedInteger(components.G ?? unreachable(), 10, false), B: floatAsNormalizedInteger(components.B ?? unreachable(), 10, false), A: floatAsNormalizedInteger(components.A ?? unreachable(), 2, false), }), bitsToNumber: components => ({ R: normalizedIntegerAsFloat(components.R!, 10, false), G: normalizedIntegerAsFloat(components.G!, 10, false), B: normalizedIntegerAsFloat(components.B!, 10, false), A: normalizedIntegerAsFloat(components.A!, 2, false), }), bitsToULPFromZero: components => components, numericRange: { min: 0, max: 1, finiteMin: 0, finiteMax: 1 }, }, rg11b10ufloat: { componentOrder: kRGB, encode: applyEach(identity, kRGB), decode: applyEach(identity, kRGB), componentInfo: { R: { dataType: 'ufloat', bitLength: 11 }, G: { dataType: 'ufloat', bitLength: 11 }, B: { dataType: 'ufloat', bitLength: 10 }, }, pack: components => { const componentsBits = { R: float32ToFloatBits(components.R ?? unreachable(), 0, 5, 6, 15), G: float32ToFloatBits(components.G ?? unreachable(), 0, 5, 6, 15), B: float32ToFloatBits(components.B ?? unreachable(), 0, 5, 5, 15), }; return packComponents( kRGB, componentsBits, { R: 11, G: 11, B: 10, }, 'uint' ); }, unpackBits: (data: Uint8Array) => unpackComponentsBits(kRGB, data, { R: 11, G: 11, B: 10 }), numberToBits: components => ({ R: numberToFloatBits(components.R ?? unreachable(), kFloat11Format), G: numberToFloatBits(components.G ?? unreachable(), kFloat11Format), B: numberToFloatBits(components.B ?? unreachable(), kFloat10Format), }), bitsToNumber: components => ({ R: floatBitsToNumber(components.R!, kFloat11Format), G: floatBitsToNumber(components.G!, kFloat11Format), B: floatBitsToNumber(components.B!, kFloat10Format), }), bitsToULPFromZero: components => ({ R: floatBitsToNormalULPFromZero(components.R!, kFloat11Format), G: floatBitsToNormalULPFromZero(components.G!, kFloat11Format), B: floatBitsToNormalULPFromZero(components.B!, kFloat10Format), }), 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, componentInfo: makePerTexelComponent(kRGB, { dataType: 'ufloat', bitLength: -1, // Components don't really have a bitLength since the format is packed. }), encode: applyEach(identity, kRGB), decode: applyEach(identity, kRGB), pack: components => new Uint32Array([ packRGB9E5UFloat( components.R ?? unreachable(), components.G ?? unreachable(), components.B ?? unreachable() ), ]).buffer, unpackBits: (data: Uint8Array) => { const encoded = (data[3] << 24) | (data[2] << 16) | (data[1] << 8) | data[0]; const redMantissa = (encoded >>> 0) & 0b111111111; const greenMantissa = (encoded >>> 9) & 0b111111111; const blueMantissa = (encoded >>> 18) & 0b111111111; const exponentSharedBits = ((encoded >>> 27) & 0b11111) << 9; return { R: exponentSharedBits | redMantissa, G: exponentSharedBits | greenMantissa, B: exponentSharedBits | blueMantissa, }; }, numberToBits: components => ({ R: float32ToFloatBits(components.R ?? unreachable(), 0, 5, 9, 15), G: float32ToFloatBits(components.G ?? unreachable(), 0, 5, 9, 15), B: float32ToFloatBits(components.B ?? unreachable(), 0, 5, 9, 15), }), bitsToNumber: components => ({ R: ufloatM9E5BitsToNumber(components.R!, kUFloat9e5Format), G: ufloatM9E5BitsToNumber(components.G!, kUFloat9e5Format), B: ufloatM9E5BitsToNumber(components.B!, kUFloat9e5Format), }), bitsToULPFromZero: components => ({ R: floatBitsToNormalULPFromZero(components.R!, kUFloat9e5Format), G: floatBitsToNormalULPFromZero(components.G!, kUFloat9e5Format), B: floatBitsToNormalULPFromZero(components.B!, kUFloat9e5Format), }), 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 }), depth24plus: { componentOrder: [TexelComponent.Depth], componentInfo: { Depth: { dataType: null, bitLength: 24 } }, encode: applyEach(() => unreachable('depth24plus cannot be encoded'), [TexelComponent.Depth]), decode: applyEach(() => unreachable('depth24plus cannot be decoded'), [TexelComponent.Depth]), pack: () => unreachable('depth24plus data cannot be packed'), unpackBits: () => unreachable('depth24plus data cannot be unpacked'), numberToBits: () => unreachable('depth24plus has no representation'), bitsToNumber: () => unreachable('depth24plus has no representation'), bitsToULPFromZero: () => unreachable('depth24plus has no representation'), numericRange: { min: 0, max: 1, finiteMin: 0, finiteMax: 1 }, }, stencil8: makeIntegerInfo([TexelComponent.Stencil], 8, { signed: false }), 'depth32float-stencil8': { componentOrder: [TexelComponent.Depth, TexelComponent.Stencil], componentInfo: { Depth: { dataType: 'float', bitLength: 32, }, Stencil: { dataType: 'uint', bitLength: 8, }, }, encode: components => { assert(components.Stencil !== undefined); assertInIntegerRange(components.Stencil, 8, false); return components; }, decode: components => { assert(components.Stencil !== undefined); assertInIntegerRange(components.Stencil, 8, false); return components; }, pack: () => unreachable('depth32float-stencil8 data cannot be packed'), unpackBits: () => unreachable('depth32float-stencil8 data cannot be unpacked'), numberToBits: () => unreachable('not implemented'), bitsToNumber: () => unreachable('not implemented'), bitsToULPFromZero: () => unreachable('not implemented'), numericRange: null, }, 'depth24plus-stencil8': { componentOrder: [TexelComponent.Depth, TexelComponent.Stencil], componentInfo: { Depth: { dataType: null, bitLength: 24, }, Stencil: { dataType: 'uint', bitLength: 8, }, }, encode: components => { assert(components.Depth === undefined, 'depth24plus cannot be encoded'); assert(components.Stencil !== undefined); assertInIntegerRange(components.Stencil, 8, false); return components; }, decode: components => { assert(components.Depth === undefined, 'depth24plus cannot be decoded'); assert(components.Stencil !== undefined); assertInIntegerRange(components.Stencil, 8, false); return components; }, pack: () => unreachable('depth24plus-stencil8 data cannot be packed'), unpackBits: () => unreachable('depth24plus-stencil8 data cannot be unpacked'), numberToBits: () => unreachable('depth24plus-stencil8 has no representation'), bitsToNumber: () => unreachable('depth24plus-stencil8 has no representation'), bitsToULPFromZero: () => unreachable('depth24plus-stencil8 has no representation'), numericRange: null, }, }, }; /** * Get the `ComponentDataType` for a format. All components must have the same type. * @param {UncompressedTextureFormat} format - The input format. * @returns {ComponentDataType} The data of the components. */ export function getSingleDataType(format: UncompressedTextureFormat): ComponentDataType { const infos = Object.values(kTexelRepresentationInfo[format].componentInfo); assert(infos.length > 0); return infos.reduce((acc, cur) => { assert(cur !== undefined); assert(acc === undefined || acc === cur.dataType); return cur.dataType; }, infos[0].dataType); } /** * Get traits for generating code to readback data from a component. * @param {ComponentDataType} dataType - The input component data type. * @returns A dictionary containing the respective `ReadbackTypedArray` and `shaderType`. */ export function getComponentReadbackTraits(dataType: ComponentDataType) { switch (dataType) { case 'ufloat': case 'float': case 'unorm': case 'snorm': return { ReadbackTypedArray: Float32Array, shaderType: 'f32' as const, }; case 'uint': return { ReadbackTypedArray: Uint32Array, shaderType: 'u32' as const, }; case 'sint': return { ReadbackTypedArray: Int32Array, shaderType: 'i32' as const, }; default: unreachable(); } }