From e0b76d258f26806d20602d64ae41e01bb5ed6fa2 Mon Sep 17 00:00:00 2001 From: Austin Eng <2154796+austinEng@users.noreply.github.com> Date: Tue, 17 Nov 2020 16:09:00 -0800 Subject: [PATCH] Implement texelData for rgb9e5ufloat and add texelData unit tests (#352) - Adds dataType and componentType to the texture format table for encodable texture formats. dataType is the format's data representation whereas componentType is the GPUTextureComponentType - the type you get in the shader. - Implements texelData helpers for packed format rgb9e5ufloat - Adds unit tests for upload texel data as dataType, loading the componentType in the shader, and writing it out. --- src/webgpu/capability_info.ts | 88 +++--- src/webgpu/util/conversion.ts | 88 +++++- src/webgpu/util/math.ts | 7 + src/webgpu/util/texture/texelData.spec.ts | 352 ++++++++++++++++++++++ src/webgpu/util/texture/texelData.ts | 194 ++++++++++-- 5 files changed, 655 insertions(+), 74 deletions(-) create mode 100644 src/webgpu/util/texture/texelData.spec.ts diff --git a/src/webgpu/capability_info.ts b/src/webgpu/capability_info.ts index 3a70ec240229..bbf50011a367 100644 --- a/src/webgpu/capability_info.ts +++ b/src/webgpu/capability_info.ts @@ -102,56 +102,60 @@ type TextureFormatInfo = { // Add fields as needed }; +export type TextureDataType = 'uint' | 'sint' | 'unorm' | 'snorm' | 'float' | 'ufloat'; + export const kRegularTextureFormatInfo: { readonly [k in RegularTextureFormat]: { - color: true; - bytesPerBlock: number; - blockWidth: 1; - blockHeight: 1; + readonly color: true; + readonly bytesPerBlock: number; + readonly blockWidth: 1; + readonly blockHeight: 1; + readonly dataType: TextureDataType; + readonly componentType: GPUTextureComponentType; } & TextureFormatInfo; } = /* prettier-ignore */ { // 8-bit formats - 'r8unorm': { renderable: true, color: true, depth: false, stencil: false, storage: false, copySrc: true, copyDst: true, bytesPerBlock: 1, blockWidth: 1, blockHeight: 1 }, - 'r8snorm': { renderable: false, color: true, depth: false, stencil: false, storage: false, copySrc: true, copyDst: true, bytesPerBlock: 1, blockWidth: 1, blockHeight: 1 }, - 'r8uint': { renderable: true, color: true, depth: false, stencil: false, storage: false, copySrc: true, copyDst: true, bytesPerBlock: 1, blockWidth: 1, blockHeight: 1 }, - 'r8sint': { renderable: true, color: true, depth: false, stencil: false, storage: false, copySrc: true, copyDst: true, bytesPerBlock: 1, blockWidth: 1, blockHeight: 1 }, + 'r8unorm': { renderable: true, color: true, depth: false, stencil: false, storage: false, copySrc: true, copyDst: true, bytesPerBlock: 1, blockWidth: 1, blockHeight: 1, dataType: 'unorm', componentType: 'float' }, + 'r8snorm': { renderable: false, color: true, depth: false, stencil: false, storage: false, copySrc: true, copyDst: true, bytesPerBlock: 1, blockWidth: 1, blockHeight: 1, dataType: 'snorm', componentType: 'float' }, + 'r8uint': { renderable: true, color: true, depth: false, stencil: false, storage: false, copySrc: true, copyDst: true, bytesPerBlock: 1, blockWidth: 1, blockHeight: 1, dataType: 'uint', componentType: 'uint' }, + 'r8sint': { renderable: true, color: true, depth: false, stencil: false, storage: false, copySrc: true, copyDst: true, bytesPerBlock: 1, blockWidth: 1, blockHeight: 1, dataType: 'sint', componentType: 'sint' }, // 16-bit formats - 'r16uint': { renderable: true, color: true, depth: false, stencil: false, storage: false, copySrc: true, copyDst: true, bytesPerBlock: 2, blockWidth: 1, blockHeight: 1 }, - 'r16sint': { renderable: true, color: true, depth: false, stencil: false, storage: false, copySrc: true, copyDst: true, bytesPerBlock: 2, blockWidth: 1, blockHeight: 1 }, - 'r16float': { renderable: true, color: true, depth: false, stencil: false, storage: false, copySrc: true, copyDst: true, bytesPerBlock: 2, blockWidth: 1, blockHeight: 1 }, - 'rg8unorm': { renderable: true, color: true, depth: false, stencil: false, storage: false, copySrc: true, copyDst: true, bytesPerBlock: 2, blockWidth: 1, blockHeight: 1 }, - 'rg8snorm': { renderable: false, color: true, depth: false, stencil: false, storage: false, copySrc: true, copyDst: true, bytesPerBlock: 2, blockWidth: 1, blockHeight: 1 }, - 'rg8uint': { renderable: true, color: true, depth: false, stencil: false, storage: false, copySrc: true, copyDst: true, bytesPerBlock: 2, blockWidth: 1, blockHeight: 1 }, - 'rg8sint': { renderable: true, color: true, depth: false, stencil: false, storage: false, copySrc: true, copyDst: true, bytesPerBlock: 2, blockWidth: 1, blockHeight: 1 }, + 'r16uint': { renderable: true, color: true, depth: false, stencil: false, storage: false, copySrc: true, copyDst: true, bytesPerBlock: 2, blockWidth: 1, blockHeight: 1, dataType: 'uint', componentType: 'uint' }, + 'r16sint': { renderable: true, color: true, depth: false, stencil: false, storage: false, copySrc: true, copyDst: true, bytesPerBlock: 2, blockWidth: 1, blockHeight: 1, dataType: 'sint', componentType: 'sint' }, + 'r16float': { renderable: true, color: true, depth: false, stencil: false, storage: false, copySrc: true, copyDst: true, bytesPerBlock: 2, blockWidth: 1, blockHeight: 1, dataType: 'float', componentType: 'float' }, + 'rg8unorm': { renderable: true, color: true, depth: false, stencil: false, storage: false, copySrc: true, copyDst: true, bytesPerBlock: 2, blockWidth: 1, blockHeight: 1, dataType: 'unorm', componentType: 'float' }, + 'rg8snorm': { renderable: false, color: true, depth: false, stencil: false, storage: false, copySrc: true, copyDst: true, bytesPerBlock: 2, blockWidth: 1, blockHeight: 1, dataType: 'snorm', componentType: 'float' }, + 'rg8uint': { renderable: true, color: true, depth: false, stencil: false, storage: false, copySrc: true, copyDst: true, bytesPerBlock: 2, blockWidth: 1, blockHeight: 1, dataType: 'uint', componentType: 'uint' }, + 'rg8sint': { renderable: true, color: true, depth: false, stencil: false, storage: false, copySrc: true, copyDst: true, bytesPerBlock: 2, blockWidth: 1, blockHeight: 1, dataType: 'sint', componentType: 'sint' }, // 32-bit formats - 'r32uint': { renderable: true, color: true, depth: false, stencil: false, storage: true, copySrc: true, copyDst: true, bytesPerBlock: 4, blockWidth: 1, blockHeight: 1 }, - 'r32sint': { renderable: true, color: true, depth: false, stencil: false, storage: true, copySrc: true, copyDst: true, bytesPerBlock: 4, blockWidth: 1, blockHeight: 1 }, - 'r32float': { renderable: true, color: true, depth: false, stencil: false, storage: true, copySrc: true, copyDst: true, bytesPerBlock: 4, blockWidth: 1, blockHeight: 1 }, - 'rg16uint': { renderable: true, color: true, depth: false, stencil: false, storage: false, copySrc: true, copyDst: true, bytesPerBlock: 4, blockWidth: 1, blockHeight: 1 }, - 'rg16sint': { renderable: true, color: true, depth: false, stencil: false, storage: false, copySrc: true, copyDst: true, bytesPerBlock: 4, blockWidth: 1, blockHeight: 1 }, - 'rg16float': { renderable: true, color: true, depth: false, stencil: false, storage: false, copySrc: true, copyDst: true, bytesPerBlock: 4, blockWidth: 1, blockHeight: 1 }, - 'rgba8unorm': { renderable: true, color: true, depth: false, stencil: false, storage: true, copySrc: true, copyDst: true, bytesPerBlock: 4, blockWidth: 1, blockHeight: 1 }, - 'rgba8unorm-srgb': { renderable: true, color: true, depth: false, stencil: false, storage: false, copySrc: true, copyDst: true, bytesPerBlock: 4, blockWidth: 1, blockHeight: 1 }, - 'rgba8snorm': { renderable: false, color: true, depth: false, stencil: false, storage: true, copySrc: true, copyDst: true, bytesPerBlock: 4, blockWidth: 1, blockHeight: 1 }, - 'rgba8uint': { renderable: true, color: true, depth: false, stencil: false, storage: true, copySrc: true, copyDst: true, bytesPerBlock: 4, blockWidth: 1, blockHeight: 1 }, - 'rgba8sint': { renderable: true, color: true, depth: false, stencil: false, storage: true, copySrc: true, copyDst: true, bytesPerBlock: 4, blockWidth: 1, blockHeight: 1 }, - 'bgra8unorm': { renderable: true, color: true, depth: false, stencil: false, storage: false, copySrc: true, copyDst: true, bytesPerBlock: 4, blockWidth: 1, blockHeight: 1 }, - 'bgra8unorm-srgb': { renderable: true, color: true, depth: false, stencil: false, storage: false, copySrc: true, copyDst: true, bytesPerBlock: 4, blockWidth: 1, blockHeight: 1 }, + 'r32uint': { renderable: true, color: true, depth: false, stencil: false, storage: true, copySrc: true, copyDst: true, bytesPerBlock: 4, blockWidth: 1, blockHeight: 1, dataType: 'uint', componentType: 'uint' }, + 'r32sint': { renderable: true, color: true, depth: false, stencil: false, storage: true, copySrc: true, copyDst: true, bytesPerBlock: 4, blockWidth: 1, blockHeight: 1, dataType: 'sint', componentType: 'sint' }, + 'r32float': { renderable: true, color: true, depth: false, stencil: false, storage: true, copySrc: true, copyDst: true, bytesPerBlock: 4, blockWidth: 1, blockHeight: 1, dataType: 'float', componentType: 'float' }, + 'rg16uint': { renderable: true, color: true, depth: false, stencil: false, storage: false, copySrc: true, copyDst: true, bytesPerBlock: 4, blockWidth: 1, blockHeight: 1, dataType: 'uint', componentType: 'uint' }, + 'rg16sint': { renderable: true, color: true, depth: false, stencil: false, storage: false, copySrc: true, copyDst: true, bytesPerBlock: 4, blockWidth: 1, blockHeight: 1, dataType: 'sint', componentType: 'sint' }, + 'rg16float': { renderable: true, color: true, depth: false, stencil: false, storage: false, copySrc: true, copyDst: true, bytesPerBlock: 4, blockWidth: 1, blockHeight: 1, dataType: 'float', componentType: 'float' }, + 'rgba8unorm': { renderable: true, color: true, depth: false, stencil: false, storage: true, copySrc: true, copyDst: true, bytesPerBlock: 4, blockWidth: 1, blockHeight: 1, dataType: 'unorm', componentType: 'float' }, + 'rgba8unorm-srgb': { renderable: true, color: true, depth: false, stencil: false, storage: false, copySrc: true, copyDst: true, bytesPerBlock: 4, blockWidth: 1, blockHeight: 1, dataType: 'unorm', componentType: 'float' }, + 'rgba8snorm': { renderable: false, color: true, depth: false, stencil: false, storage: true, copySrc: true, copyDst: true, bytesPerBlock: 4, blockWidth: 1, blockHeight: 1, dataType: 'snorm', componentType: 'float' }, + 'rgba8uint': { renderable: true, color: true, depth: false, stencil: false, storage: true, copySrc: true, copyDst: true, bytesPerBlock: 4, blockWidth: 1, blockHeight: 1, dataType: 'uint', componentType: 'uint' }, + 'rgba8sint': { renderable: true, color: true, depth: false, stencil: false, storage: true, copySrc: true, copyDst: true, bytesPerBlock: 4, blockWidth: 1, blockHeight: 1, dataType: 'sint', componentType: 'sint' }, + 'bgra8unorm': { renderable: true, color: true, depth: false, stencil: false, storage: false, copySrc: true, copyDst: true, bytesPerBlock: 4, blockWidth: 1, blockHeight: 1, dataType: 'unorm', componentType: 'float' }, + 'bgra8unorm-srgb': { renderable: true, color: true, depth: false, stencil: false, storage: false, copySrc: true, copyDst: true, bytesPerBlock: 4, blockWidth: 1, blockHeight: 1, dataType: 'unorm', componentType: 'float' }, // Packed 32-bit formats - 'rgb10a2unorm': { renderable: true, color: true, depth: false, stencil: false, storage: false, copySrc: true, copyDst: true, bytesPerBlock: 4, blockWidth: 1, blockHeight: 1 }, - 'rg11b10ufloat': { renderable: false, color: true, depth: false, stencil: false, storage: false, copySrc: true, copyDst: true, bytesPerBlock: 4, blockWidth: 1, blockHeight: 1 }, - 'rgb9e5ufloat': { renderable: false, color: true, depth: false, stencil: false, storage: false, copySrc: true, copyDst: true, bytesPerBlock: 4, blockWidth: 1, blockHeight: 1 }, + 'rgb10a2unorm': { renderable: true, color: true, depth: false, stencil: false, storage: false, copySrc: true, copyDst: true, bytesPerBlock: 4, blockWidth: 1, blockHeight: 1, dataType: 'unorm', componentType: 'float' }, + 'rg11b10ufloat': { renderable: false, color: true, depth: false, stencil: false, storage: false, copySrc: true, copyDst: true, bytesPerBlock: 4, blockWidth: 1, blockHeight: 1, dataType: 'ufloat', componentType: 'float' }, + 'rgb9e5ufloat': { renderable: false, color: true, depth: false, stencil: false, storage: false, copySrc: true, copyDst: true, bytesPerBlock: 4, blockWidth: 1, blockHeight: 1, dataType: 'ufloat', componentType: 'float' }, // 64-bit formats - 'rg32uint': { renderable: true, color: true, depth: false, stencil: false, storage: true, copySrc: true, copyDst: true, bytesPerBlock: 8, blockWidth: 1, blockHeight: 1 }, - 'rg32sint': { renderable: true, color: true, depth: false, stencil: false, storage: true, copySrc: true, copyDst: true, bytesPerBlock: 8, blockWidth: 1, blockHeight: 1 }, - 'rg32float': { renderable: true, color: true, depth: false, stencil: false, storage: true, copySrc: true, copyDst: true, bytesPerBlock: 8, blockWidth: 1, blockHeight: 1 }, - 'rgba16uint': { renderable: true, color: true, depth: false, stencil: false, storage: true, copySrc: true, copyDst: true, bytesPerBlock: 8, blockWidth: 1, blockHeight: 1 }, - 'rgba16sint': { renderable: true, color: true, depth: false, stencil: false, storage: true, copySrc: true, copyDst: true, bytesPerBlock: 8, blockWidth: 1, blockHeight: 1 }, - 'rgba16float': { renderable: true, color: true, depth: false, stencil: false, storage: true, copySrc: true, copyDst: true, bytesPerBlock: 8, blockWidth: 1, blockHeight: 1 }, + 'rg32uint': { renderable: true, color: true, depth: false, stencil: false, storage: true, copySrc: true, copyDst: true, bytesPerBlock: 8, blockWidth: 1, blockHeight: 1, dataType: 'uint', componentType: 'uint' }, + 'rg32sint': { renderable: true, color: true, depth: false, stencil: false, storage: true, copySrc: true, copyDst: true, bytesPerBlock: 8, blockWidth: 1, blockHeight: 1, dataType: 'sint', componentType: 'sint' }, + 'rg32float': { renderable: true, color: true, depth: false, stencil: false, storage: true, copySrc: true, copyDst: true, bytesPerBlock: 8, blockWidth: 1, blockHeight: 1, dataType: 'float', componentType: 'float' }, + 'rgba16uint': { renderable: true, color: true, depth: false, stencil: false, storage: true, copySrc: true, copyDst: true, bytesPerBlock: 8, blockWidth: 1, blockHeight: 1, dataType: 'uint', componentType: 'uint' }, + 'rgba16sint': { renderable: true, color: true, depth: false, stencil: false, storage: true, copySrc: true, copyDst: true, bytesPerBlock: 8, blockWidth: 1, blockHeight: 1, dataType: 'sint', componentType: 'sint' }, + 'rgba16float': { renderable: true, color: true, depth: false, stencil: false, storage: true, copySrc: true, copyDst: true, bytesPerBlock: 8, blockWidth: 1, blockHeight: 1, dataType: 'float', componentType: 'float' }, // 128-bit formats - 'rgba32uint': { renderable: true, color: true, depth: false, stencil: false, storage: true, copySrc: true, copyDst: true, bytesPerBlock: 16, blockWidth: 1, blockHeight: 1 }, - 'rgba32sint': { renderable: true, color: true, depth: false, stencil: false, storage: true, copySrc: true, copyDst: true, bytesPerBlock: 16, blockWidth: 1, blockHeight: 1 }, - 'rgba32float': { renderable: true, color: true, depth: false, stencil: false, storage: true, copySrc: true, copyDst: true, bytesPerBlock: 16, blockWidth: 1, blockHeight: 1 }, + 'rgba32uint': { renderable: true, color: true, depth: false, stencil: false, storage: true, copySrc: true, copyDst: true, bytesPerBlock: 16, blockWidth: 1, blockHeight: 1, dataType: 'uint', componentType: 'uint' }, + 'rgba32sint': { renderable: true, color: true, depth: false, stencil: false, storage: true, copySrc: true, copyDst: true, bytesPerBlock: 16, blockWidth: 1, blockHeight: 1, dataType: 'sint', componentType: 'sint' }, + 'rgba32float': { renderable: true, color: true, depth: false, stencil: false, storage: true, copySrc: true, copyDst: true, bytesPerBlock: 16, blockWidth: 1, blockHeight: 1, dataType: 'float', componentType: 'float' }, } as const; export const kRegularTextureFormats = keysOf(kRegularTextureFormatInfo); @@ -162,9 +166,11 @@ export const kSizedDepthStencilFormatInfo: { readonly bytesPerBlock: number; readonly blockWidth: 1; readonly blockHeight: 1; + readonly dataType: TextureDataType; + readonly componentType: GPUTextureComponentType; } & TextureFormatInfo; } = /* prettier-ignore */ { - 'depth32float': { renderable: true, color: false, depth: true, stencil: false, storage: false, copySrc: true, copyDst: false, bytesPerBlock: 4, blockWidth: 1, blockHeight: 1 }, + 'depth32float': { renderable: true, color: false, depth: true, stencil: false, storage: false, copySrc: true, copyDst: false, bytesPerBlock: 4, blockWidth: 1, blockHeight: 1, dataType: 'float', componentType: 'float' }, }; export const kSizedDepthStencilFormats = keysOf(kSizedDepthStencilFormatInfo); diff --git a/src/webgpu/util/conversion.ts b/src/webgpu/util/conversion.ts index 3d62cdae3f50..a19a7f650d0e 100644 --- a/src/webgpu/util/conversion.ts +++ b/src/webgpu/util/conversion.ts @@ -1,5 +1,7 @@ import { assert } from '../../common/framework/util/util.js'; +import { clamp } from './math.js'; + export function floatAsNormalizedInteger(float: number, bits: number, signed: boolean): number { if (signed) { assert(float >= -1 && float <= 1); @@ -12,16 +14,32 @@ export function floatAsNormalizedInteger(float: number, bits: number, signed: bo } } +export function normalizedIntegerAsFloat(integer: number, bits: number, signed: boolean): number { + assert(Number.isInteger(integer)); + if (signed) { + const max = Math.pow(2, bits - 1) - 1; + assert(integer >= -max - 1 && integer <= max); + if (integer === -max - 1) { + integer = -max; + } + return integer / max; + } else { + const max = Math.pow(2, bits) - 1; + assert(integer >= 0 && integer <= max); + return integer / max; + } +} + // Does not handle clamping, underflow, overflow, denormalized numbers export function float32ToFloatBits( n: number, signBits: 0 | 1, exponentBits: number, - fractionBits: number, + mantissaBits: number, bias: number ): number { assert(exponentBits <= 8); - assert(fractionBits <= 23); + assert(mantissaBits <= 23); assert(Number.isFinite(n)); if (n === 0) { @@ -37,21 +55,68 @@ export function float32ToFloatBits( const bits = buf.getUint32(0, true); // bits (32): seeeeeeeefffffffffffffffffffffff - const fractionBitsToDiscard = 23 - fractionBits; + const mantissaBitsToDiscard = 23 - mantissaBits; // 0 or 1 const sign = (bits >> 31) & signBits; - // >> to remove fraction, & to remove sign, - 127 to remove bias. + // >> to remove mantissa, & to remove sign, - 127 to remove bias. const exp = ((bits >> 23) & 0xff) - 127; // Convert to the new biased exponent. const newBiasedExp = bias + exp; assert(newBiasedExp >= 0 && newBiasedExp < 1 << exponentBits); - // Mask only the fraction, and discard the lower bits. - const newFraction = (bits & 0x7fffff) >> fractionBitsToDiscard; - return (sign << (exponentBits + fractionBits)) | (newBiasedExp << fractionBits) | newFraction; + // Mask only the mantissa, and discard the lower bits. + const newMantissa = (bits & 0x7fffff) >> mantissaBitsToDiscard; + return (sign << (exponentBits + mantissaBits)) | (newBiasedExp << mantissaBits) | newMantissa; +} + +// Three partial-precision floating-point numbers encoded into a single 32-bit value all +// sharing the same 5-bit exponent. +// There is no sign bit, and there is a shared 5-bit biased (15) exponent and a 9-bit +// mantissa for each channel. The mantissa does NOT have an implicit leading "1.", +// and instead has an implicit leading "0.". +export function encodeRGB9E5UFloat(r: number, g: number, b: number): number { + for (const v of [r, g, b]) { + assert(v >= 0 && v < Math.pow(2, 16)); + } + + const buf = new DataView(new ArrayBuffer(Float32Array.BYTES_PER_ELEMENT)); + const extractMantissaAndExponent = (n: number) => { + const mantissaBits = 9; + buf.setFloat32(0, n, true); + const bits = buf.getUint32(0, true); + // >> to remove mantissa, & to remove sign + let biasedExponent = (bits >> 23) & 0xff; + const mantissaBitsToDiscard = 23 - mantissaBits; + let mantissa = (bits & 0x7fffff) >> mantissaBitsToDiscard; + + // RGB9E5UFloat has an implicit leading 0. instead of a leading 1., + // so we need to move the 1. into the mantissa and bump the exponent. + // For float32 encoding, the leading 1 is only present if the biased + // exponent is non-zero. + if (biasedExponent !== 0) { + mantissa = (mantissa >> 1) | 0b100000000; + biasedExponent += 1; + } + return { biasedExponent, mantissa }; + }; + + const { biasedExponent: rExp, mantissa: rOrigMantissa } = extractMantissaAndExponent(r); + const { biasedExponent: gExp, mantissa: gOrigMantissa } = extractMantissaAndExponent(g); + const { biasedExponent: bExp, mantissa: bOrigMantissa } = extractMantissaAndExponent(b); + + // Use the largest exponent, and shift the mantissa accordingly + const exp = Math.max(rExp, gExp, bExp); + const rMantissa = rOrigMantissa >> (exp - rExp); + const gMantissa = gOrigMantissa >> (exp - gExp); + const bMantissa = bOrigMantissa >> (exp - bExp); + + const bias = 15; + const biasedExp = exp === 0 ? 0 : exp - 127 + bias; + assert(biasedExp >= 0 && biasedExp <= 31); + return rMantissa | (gMantissa << 9) | (bMantissa << 18) | (biasedExp << 27); } export function assertInIntegerRange(n: number, bits: number, signed: boolean): void { @@ -66,6 +131,11 @@ export function assertInIntegerRange(n: number, bits: number, signed: boolean): } export function gammaCompress(n: number): number { - n = n <= 0.0031308 ? 12.92 * n : 1.055 * Math.pow(n, 1 / 2.4) - 0.055; - return n < 0 ? 0 : n > 1 ? 1 : n; + n = n <= 0.0031308 ? (323 * n) / 25 : (211 * Math.pow(n, 5 / 12) - 11) / 200; + return clamp(n, 0, 1); +} + +export function gammaDecompress(n: number): number { + n = n <= 0.04045 ? (n * 25) / 323 : Math.pow((200 * n + 11) / 211, 12 / 5); + return clamp(n, 0, 1); } diff --git a/src/webgpu/util/math.ts b/src/webgpu/util/math.ts index 4bd83212b2c2..fb627881b718 100644 --- a/src/webgpu/util/math.ts +++ b/src/webgpu/util/math.ts @@ -1,3 +1,5 @@ +import { assert } from '../../common/framework/util/util.js'; + export function align(n: number, alignment: number): number { return Math.ceil(n / alignment) * alignment; } @@ -7,3 +9,8 @@ export function isAligned(n: number, alignment: number): boolean { } export const kMaxSafeMultipleOf8 = Number.MAX_SAFE_INTEGER - 7; + +export function clamp(n: number, min: number, max: number): number { + assert(max >= min); + return Math.min(Math.max(n, min), max); +} diff --git a/src/webgpu/util/texture/texelData.spec.ts b/src/webgpu/util/texture/texelData.spec.ts new file mode 100644 index 000000000000..8c4149a79cab --- /dev/null +++ b/src/webgpu/util/texture/texelData.spec.ts @@ -0,0 +1,352 @@ +export const description = 'Test helpers for texel data produce the expected data in the shader'; + +import { params, poptions } from '../../../common/framework/params_builder.js'; +import { makeTestGroup } from '../../../common/framework/test_group.js'; +import { unreachable, assert } from '../../../common/framework/util/util.js'; +import { + kEncodableTextureFormats, + kEncodableTextureFormatInfo, + EncodableTextureFormat, +} from '../../capability_info.js'; +import { GPUTest } from '../../gpu_test.js'; + +import { getTexelDataRepresentation } from './texelData.js'; + +export const g = makeTestGroup(GPUTest); + +let ReadbackTypedArray: Float32ArrayConstructor | Int32ArrayConstructor | Uint32ArrayConstructor; + +function doTest( + t: GPUTest & { + params: { + format: EncodableTextureFormat; + R?: number; + G?: number; + B?: number; + A?: number; + }; + } +) { + const { format } = t.params; + const componentData = (() => { + const { R, G, B, A } = t.params; + return { R, G, B, A }; + })(); + + const rep = getTexelDataRepresentation(format); + const texelData = rep.packData(componentData); + const texture = t.device.createTexture({ + format, + size: [1, 1, 1], + usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.SAMPLED, + }); + + t.device.defaultQueue.writeTexture( + { texture }, + texelData, + { + bytesPerRow: texelData.byteLength, + }, + [1] + ); + + let shaderType: 'i32' | 'u32' | 'f32'; + switch (kEncodableTextureFormatInfo[format].componentType) { + case 'sint': + ReadbackTypedArray = Int32Array; + shaderType = 'i32'; + break; + case 'uint': + ReadbackTypedArray = Uint32Array; + shaderType = 'u32'; + break; + case 'float': + ReadbackTypedArray = Float32Array; + shaderType = 'f32'; + break; + default: + unreachable(); + } + + const shader = ` + [[set(0), binding(0)]] var tex : texture_2d<${shaderType}>; + + [[block]] struct Output { + ${rep.componentOrder + .map((C, i) => `[[offset(${i * 4})]] result${C} : ${shaderType};`) + .join('\n')} + }; + [[set(0), binding(1)]] var output : Output; + + [[stage(compute)]] + fn main() -> void { + var texel : vec4<${shaderType}> = textureLoad(tex, vec2(0, 0), 0); + ${rep.componentOrder.map(C => `output.result${C} = texel.${C.toLowerCase()};`).join('\n')} + return; + }`; + + const pipeline = t.device.createComputePipeline({ + computeStage: { + module: t.device.createShaderModule({ + code: shader, + }), + entryPoint: 'main', + }, + }); + + const outputBuffer = t.device.createBuffer({ + size: rep.componentOrder.length * 4, + usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC, + }); + + const bindGroup = t.device.createBindGroup({ + layout: pipeline.getBindGroupLayout(0), + entries: [ + { + binding: 0, + resource: texture.createView(), + }, + { + binding: 1, + resource: { + buffer: outputBuffer, + }, + }, + ], + }); + + const encoder = t.device.createCommandEncoder(); + const pass = encoder.beginComputePass(); + pass.setPipeline(pipeline); + pass.setBindGroup(0, bindGroup); + pass.dispatch(1); + pass.endPass(); + t.device.defaultQueue.submit([encoder.finish()]); + + t.expectContents( + outputBuffer, + new ReadbackTypedArray( + rep.componentOrder.map(c => { + const value = rep.decode(componentData)[c]; + assert(value !== undefined); + return value; + }) + ) + ); +} + +// Make a test parameter by mapping a format and each component to a texel component +// data value. +function makeParam( + format: EncodableTextureFormat, + fn: (bitLength: number, index: number) => number +) { + const rep = getTexelDataRepresentation(format); + return { + R: rep.componentInfo.R ? fn(rep.componentInfo.R.bitLength, 0) : undefined, + G: rep.componentInfo.G ? fn(rep.componentInfo.G.bitLength, 1) : undefined, + B: rep.componentInfo.B ? fn(rep.componentInfo.B.bitLength, 2) : undefined, + A: rep.componentInfo.A ? fn(rep.componentInfo.A.bitLength, 3) : undefined, + }; +} + +g.test('unorm_texel_data_in_shader') + .params( + params() + .combine(poptions('format', kEncodableTextureFormats)) + .filter(({ format }) => { + return ( + kEncodableTextureFormatInfo[format].copyDst && + kEncodableTextureFormatInfo[format].color && + kEncodableTextureFormatInfo[format].dataType === 'unorm' + ); + }) + .expand(({ format }) => { + const max = (bitLength: number) => Math.pow(2, bitLength) - 1; + return [ + // Test extrema + makeParam(format, () => 0), + makeParam(format, bitLength => max(bitLength)), + + // Test a middle value + makeParam(format, bitLength => Math.floor(max(bitLength) / 2)), + + // Test mixed values + makeParam(format, (bitLength, i) => { + const offset = [0.13, 0.63, 0.42, 0.89]; + return Math.floor(offset[i] * max(bitLength)); + }), + ]; + }) + ) + .fn(doTest); + +g.test('snorm_texel_data_in_shader') + .params( + params() + .combine(poptions('format', kEncodableTextureFormats)) + .filter(({ format }) => { + return ( + kEncodableTextureFormatInfo[format].copyDst && + kEncodableTextureFormatInfo[format].color && + kEncodableTextureFormatInfo[format].dataType === 'snorm' + ); + }) + .expand(({ format }) => { + const max = (bitLength: number) => Math.pow(2, bitLength - 1) - 1; + return [ + // Test extrema + makeParam(format, () => 0), + makeParam(format, bitLength => max(bitLength)), + makeParam(format, bitLength => -max(bitLength)), + makeParam(format, bitLength => -max(bitLength) - 1), + + // Test a middle value + makeParam(format, bitLength => Math.floor(max(bitLength) / 2)), + + // Test mixed values + makeParam(format, (bitLength, i) => { + const offset = [0.13, 0.63, 0.42, 0.89]; + const range = 2 * max(bitLength); + return -max(bitLength) + Math.floor(offset[i] * range); + }), + ]; + }) + ) + .fn(doTest); + +g.test('uint_texel_data_in_shader') + .params( + params() + .combine(poptions('format', kEncodableTextureFormats)) + .filter(({ format }) => { + return ( + kEncodableTextureFormatInfo[format].copyDst && + kEncodableTextureFormatInfo[format].color && + kEncodableTextureFormatInfo[format].dataType === 'uint' + ); + }) + .expand(({ format }) => { + const max = (bitLength: number) => Math.pow(2, bitLength) - 1; + return [ + // Test extrema + makeParam(format, () => 0), + makeParam(format, bitLength => max(bitLength)), + + // Test a middle value + makeParam(format, bitLength => Math.floor(max(bitLength) / 2)), + + // Test mixed values + makeParam(format, (bitLength, i) => { + const offset = [0.13, 0.63, 0.42, 0.89]; + return Math.floor(offset[i] * max(bitLength)); + }), + ]; + }) + ) + .fn(doTest); + +g.test('sint_texel_data_in_shader') + .params( + params() + .combine(poptions('format', kEncodableTextureFormats)) + .filter(({ format }) => { + return ( + kEncodableTextureFormatInfo[format].copyDst && + kEncodableTextureFormatInfo[format].color && + kEncodableTextureFormatInfo[format].dataType === 'sint' + ); + }) + .expand(({ format }) => { + const max = (bitLength: number) => Math.pow(2, bitLength - 1) - 1; + return [ + // Test extrema + makeParam(format, () => 0), + makeParam(format, bitLength => max(bitLength)), + makeParam(format, bitLength => -max(bitLength) - 1), + + // Test a middle value + makeParam(format, bitLength => Math.floor(max(bitLength) / 2)), + + // Test mixed values + makeParam(format, (bitLength, i) => { + const offset = [0.13, 0.63, 0.42, 0.89]; + const range = 2 * max(bitLength); + return -max(bitLength) + Math.floor(offset[i] * range); + }), + ]; + }) + ) + .fn(doTest); + +g.test('float_texel_data_in_shader') + .params( + params() + .combine(poptions('format', kEncodableTextureFormats)) + .filter(({ format }) => { + return ( + kEncodableTextureFormatInfo[format].copyDst && + kEncodableTextureFormatInfo[format].color && + kEncodableTextureFormatInfo[format].dataType === 'float' + ); + }) + .expand(({ format }) => { + return [ + // Test extrema + makeParam(format, () => 0), + + // TODO: Test NaN, Infinity, -Infinity + + // Test some values + makeParam(format, () => 0.1199951171875), + makeParam(format, () => 1.4072265625), + makeParam(format, () => 24928), + makeParam(format, () => -0.1319580078125), + makeParam(format, () => -323.25), + makeParam(format, () => -7440), + + // Test mixed values + makeParam(format, (bitLength, i) => { + return [24896, -0.1319580078125, -323.25, -234.375][i]; + }), + ]; + }) + ) + .fn(doTest); + +g.test('ufloat_texel_data_in_shader') + .params( + params() + .combine(poptions('format', kEncodableTextureFormats)) + .filter(({ format }) => { + return ( + kEncodableTextureFormatInfo[format].copyDst && + kEncodableTextureFormatInfo[format].color && + kEncodableTextureFormatInfo[format].dataType === 'ufloat' + ); + }) + .expand(({ format }) => { + return [ + // Test extrema + makeParam(format, () => 0), + + // TODO: Test NaN, Infinity + + // Test some values + makeParam(format, () => 0.119140625), + makeParam(format, () => 1.40625), + makeParam(format, () => 24896), + + // Test scattered mixed values + makeParam(format, (bitLength, i) => { + return [24896, 1.40625, 0.119140625, 0.23095703125][i]; + }), + + // Test mixed values that are close in magnitude. + makeParam(format, (bitLength, i) => { + return [0.1337890625, 0.17919921875, 0.119140625, 0.125][i]; + }), + ]; + }) + ) + .fn(doTest); diff --git a/src/webgpu/util/texture/texelData.ts b/src/webgpu/util/texture/texelData.ts index 90dc2733ba69..756dcc807093 100644 --- a/src/webgpu/util/texture/texelData.ts +++ b/src/webgpu/util/texture/texelData.ts @@ -1,5 +1,7 @@ import { assert, unreachable } from '../../../common/framework/util/util.js'; import { + EncodableTextureFormat, + kEncodableTextureFormatInfo, kUncompressedTextureFormatInfo, UncompressedTextureFormat, } from '../../capability_info.js'; @@ -8,6 +10,9 @@ import { float32ToFloatBits, floatAsNormalizedInteger, gammaCompress, + encodeRGB9E5UFloat, + normalizedIntegerAsFloat, + gammaDecompress, } from '../conversion.js'; export const enum TexelComponent { @@ -33,7 +38,11 @@ const enum TexelWriteType { // - floats to half floats, interpreted as uint16 bits type TexelWriteFn = (value: number) => { value: number; type: TexelWriteType }; +// Converts a data value to its representation in a shader +type DecodeFn = (value: number) => number; + interface SingleComponentInfo { + decode: DecodeFn; write: TexelWriteFn; bitLength: number; } @@ -66,21 +75,28 @@ const sint = (bitLength: number) => (n: number) => ({ type: TexelWriteType.Sint, }); -const unorm2 = { write: unorm(2), bitLength: 2 }; -const unorm8 = { write: unorm(8), bitLength: 8 }; -const unorm10 = { write: unorm(10), bitLength: 10 }; +const decodeUnorm = (bitLength: number) => (n: number) => + normalizedIntegerAsFloat(n, bitLength, false); +const decodeSnorm = (bitLength: number) => (n: number) => + normalizedIntegerAsFloat(n, bitLength, true); +const identity = (n: number) => n; + +const unorm2 = { decode: decodeUnorm(2), write: unorm(2), bitLength: 2 }; +const unorm8 = { decode: decodeUnorm(8), write: unorm(8), bitLength: 8 }; +const unorm10 = { decode: decodeUnorm(10), write: unorm(10), bitLength: 10 }; -const snorm8 = { write: snorm(8), bitLength: 8 }; +const snorm8 = { decode: decodeSnorm(8), write: snorm(8), bitLength: 8 }; -const uint8 = { write: uint(8), bitLength: 8 }; -const uint16 = { write: uint(16), bitLength: 16 }; -const uint32 = { write: uint(32), bitLength: 32 }; +const uint8 = { decode: identity, write: uint(8), bitLength: 8 }; +const uint16 = { decode: identity, write: uint(16), bitLength: 16 }; +const uint32 = { decode: identity, write: uint(32), bitLength: 32 }; -const sint8 = { write: sint(8), bitLength: 8 }; -const sint16 = { write: sint(16), bitLength: 16 }; -const sint32 = { write: sint(32), bitLength: 32 }; +const sint8 = { decode: identity, write: sint(8), bitLength: 8 }; +const sint16 = { decode: identity, write: sint(16), bitLength: 16 }; +const sint32 = { decode: identity, write: sint(32), bitLength: 32 }; const float10 = { + decode: identity, write: (n: number) => ({ value: float32ToFloatBits(n, 0, 5, 5, 15), type: TexelWriteType.Uint, @@ -89,6 +105,7 @@ const float10 = { }; const float11 = { + decode: identity, write: (n: number) => ({ value: float32ToFloatBits(n, 0, 5, 6, 15), type: TexelWriteType.Uint, @@ -97,6 +114,7 @@ const float11 = { }; const float16 = { + decode: identity, write: (n: number) => ({ value: float32ToFloatBits(n, 1, 5, 10, 15), type: TexelWriteType.Uint, @@ -105,6 +123,7 @@ const float16 = { }; const float32 = { + decode: identity, write: (n: number) => ({ value: Math.fround(n), type: TexelWriteType.Float, @@ -113,6 +132,7 @@ const float32 = { }; const componentUnimplemented = { + decode: identity, write: () => { unreachable('TexelComponentInfo not implemented for this texture format'); }, @@ -192,7 +212,18 @@ const kRepresentationInfo: { export interface TexelDataRepresentation { readonly componentOrder: TexelComponent[]; readonly componentInfo: TexelComponentInfo; + + // Gets the data representation for |components| where |components| is the expected + // values when read in a shader. i.e. Passing in 1.0 for a 8-bit unorm component will + // yield 255. getBytes(components: { [c in TexelComponent]?: number }): ArrayBuffer; + + // Pack texel components into the packed byte representation. This may round values, but + // does not do unorm <-> float conversion. + packData(components: { [c in TexelComponent]?: number }): ArrayBuffer; + + // Decode data into the shader representation + decode(components: { [c in TexelComponent]?: number }): { [c in TexelComponent]?: number }; } class TexelDataRepresentationImpl implements TexelDataRepresentation { @@ -212,20 +243,13 @@ class TexelDataRepresentationImpl implements TexelDataRepresentation { }, 0); } - private setComponent(data: ArrayBuffer, component: TexelComponent, n: number): void { - const componentIndex = this.componentOrder.indexOf(component); - assert(componentIndex !== -1); - const bitOffset = this.componentOrder.slice(0, componentIndex).reduce((acc, curr) => { - const componentInfo = this.componentInfo[curr]; - assert(!!componentInfo); - return acc + componentInfo.bitLength; - }, 0); - - const componentInfo = this.componentInfo[component]; - assert(!!componentInfo); - const { write, bitLength } = componentInfo; - - const { value, type } = write(n); + private writeTexelData( + data: ArrayBuffer, + bitOffset: number, + bitLength: number, + type: TexelWriteType, + value: number + ) { switch (type) { case TexelWriteType.Float: { const byteOffset = Math.floor(bitOffset / 8); @@ -315,6 +339,55 @@ class TexelDataRepresentationImpl implements TexelDataRepresentation { } } + private getComponentBitOffset(component: TexelComponent): number { + const componentIndex = this.componentOrder.indexOf(component); + assert(componentIndex !== -1); + return this.componentOrder.slice(0, componentIndex).reduce((acc, curr) => { + const componentInfo = this.componentInfo[curr]; + assert(!!componentInfo); + return acc + componentInfo.bitLength; + }, 0); + } + + private setComponent(data: ArrayBuffer, component: TexelComponent, n: number): void { + const bitOffset = this.getComponentBitOffset(component); + const componentInfo = this.componentInfo[component]; + assert(!!componentInfo); + const { write, bitLength } = componentInfo; + + const { value, type } = write(n); + this.writeTexelData(data, bitOffset, bitLength, type, value); + } + + private setComponentBytes(data: ArrayBuffer, component: TexelComponent, value: number): void { + assert(this.format in kEncodableTextureFormatInfo); + const format = this.format as EncodableTextureFormat; + + const componentInfo = this.componentInfo[component]; + assert(!!componentInfo); + + const bitOffset = this.getComponentBitOffset(component); + const { bitLength } = componentInfo; + + switch (kEncodableTextureFormatInfo[format].dataType) { + case 'float': + case 'ufloat': + // Use the shader encoding which can pack floats as uint data. + this.setComponent(data, component, value); + break; + case 'snorm': + case 'sint': { + this.writeTexelData(data, bitOffset, bitLength, TexelWriteType.Sint, value); + break; + } + case 'unorm': + case 'uint': { + this.writeTexelData(data, bitOffset, bitLength, TexelWriteType.Uint, value); + break; + } + } + } + getBytes(components: PerTexelComponent): ArrayBuffer { if (this.sRGB) { components = Object.assign({}, components); @@ -331,6 +404,25 @@ class TexelDataRepresentationImpl implements TexelDataRepresentation { const bytesPerBlock = kUncompressedTextureFormatInfo[this.format].bytesPerBlock; assert(!!bytesPerBlock); + if (this.format === 'rgb9e5ufloat') { + assert(this.componentOrder.length === 3); + assert(this.componentOrder[0] === TexelComponent.R); + assert(this.componentOrder[1] === TexelComponent.G); + assert(this.componentOrder[2] === TexelComponent.B); + assert(bytesPerBlock === 4); + assert(components.R !== undefined); + assert(components.G !== undefined); + assert(components.B !== undefined); + + const buf = new ArrayBuffer(bytesPerBlock); + new DataView(buf).setUint32( + 0, + encodeRGB9E5UFloat(components.R, components.G, components.B), + this.isGPULittleEndian + ); + return buf; + } + const data = new ArrayBuffer(bytesPerBlock); for (const c of this.componentOrder) { const componentValue = components[c]; @@ -339,6 +431,60 @@ class TexelDataRepresentationImpl implements TexelDataRepresentation { } return data; } + + packData(components: PerTexelComponent): ArrayBuffer { + const bytesPerBlock = kUncompressedTextureFormatInfo[this.format].bytesPerBlock; + assert(!!bytesPerBlock); + + if (this.format === 'rgb9e5ufloat') { + assert(this.componentOrder.length === 3); + assert(this.componentOrder[0] === TexelComponent.R); + assert(this.componentOrder[1] === TexelComponent.G); + assert(this.componentOrder[2] === TexelComponent.B); + assert(bytesPerBlock === 4); + assert(components.R !== undefined); + assert(components.G !== undefined); + assert(components.B !== undefined); + + const buf = new ArrayBuffer(bytesPerBlock); + new DataView(buf).setUint32( + 0, + encodeRGB9E5UFloat(components.R, components.G, components.B), + this.isGPULittleEndian + ); + return buf; + } + + const data = new ArrayBuffer(bytesPerBlock); + for (const c of this.componentOrder) { + const componentValue = components[c]; + assert(componentValue !== undefined); + this.setComponentBytes(data, c, componentValue); + } + return data; + } + + decode(components: PerTexelComponent): PerTexelComponent { + const values: PerTexelComponent = {}; + for (const c of this.componentOrder) { + const componentValue = components[c]; + const info = this.componentInfo[c]; + assert(componentValue !== undefined); + assert(!!info); + values[c] = info.decode(componentValue); + } + if (this.sRGB) { + assert('R' in values && values.R !== undefined); + assert('G' in values && values.G !== undefined); + assert('B' in values && values.B !== undefined); + [values.R, values.G, values.B] = [ + gammaDecompress(values.R), + gammaDecompress(values.G), + gammaDecompress(values.B), + ]; + } + return values; + } } const kRepresentationCache: Map = new Map();