blob: 410854562822568d7537dd60d8cf8b7fba4f2474 [file] [log] [blame]
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<T> = { [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<number>} components - The input components.
* @returns {PerTexelComponent<number>} The new output components.
*/
type ComponentMapFn = (components: PerTexelComponent<number>) => PerTexelComponent<number>;
/**
* Packs component values as an ArrayBuffer
* @param {PerTexelComponent<number>} components - The input components.
* @returns {ArrayBuffer} The packed data.
*/
type ComponentPackFn = (components: PerTexelComponent<number>) => ArrayBuffer;
/** Unpacks component values from a Uint8Array */
type ComponentUnpackFn = (data: Uint8Array) => PerTexelComponent<number>;
/**
* 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<T>}
*/
function makePerTexelComponent<T>(components: TexelComponent[], value: T): PerTexelComponent<T> {
const values: PerTexelComponent<T> = {};
for (const c of components) {
values[c] = value;
}
return values;
}
/**
* Create a function which applies clones a `PerTexelComponent<number>` 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<number>) => {
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<number>} components - The input component values.
* @returns {TexelComponent<number>} 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<number>} components - The input component values.
* @returns {TexelComponent<number>} 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<number>} components - The input component values.
* @param {number | PerTexelComponent<number>} 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<ComponentDataType>} 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<number>,
bitLengths: number | PerTexelComponent<number>,
componentDataTypes: ComponentDataType | PerTexelComponent<ComponentDataType>
): 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<number>
): PerTexelComponent<number> {
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<number>) =>
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<number>) =>
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<number>) => {
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<TexelComponent, number>;
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();
}
}