@luma.gl/core
Version:
The luma.gl core Device API
255 lines (221 loc) • 7.79 kB
text/typescript
// luma.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors
import type {CompositeUniformValue, UniformValue} from '../adapter/types/uniforms';
import {getScratchArrayBuffer} from '../utils/array-utils-flat';
import {isNumberArray} from '../utils/is-array';
import {log} from '../utils/log';
import type {
CompositeShaderType,
VariableShaderType
} from '../shadertypes/shader-types/shader-types';
import {
getLeafLayoutInfo,
isCompositeShaderTypeStruct,
type ShaderBlockLayout
} from '../shadertypes/shader-types/shader-block-layout';
/**
* Serializes nested JavaScript uniform values according to a {@link ShaderBlockLayout}.
*/
export class ShaderBlockWriter {
/** Layout metadata used to flatten and serialize values. */
readonly layout: ShaderBlockLayout;
/**
* Creates a writer for a precomputed shader-block layout.
*/
constructor(layout: ShaderBlockLayout) {
this.layout = layout;
}
/**
* Returns `true` if the flattened layout contains the given field.
*/
has(name: string): boolean {
return Boolean(this.layout.fields[name]);
}
/**
* Returns offset and size metadata for a flattened field.
*/
get(name: string): {offset: number; size: number} | undefined {
const entry = this.layout.fields[name];
return entry ? {offset: entry.offset, size: entry.size} : undefined;
}
/**
* Flattens nested composite values into leaf-path values understood by {@link UniformBlock}.
*
* Top-level values may be supplied either in nested object form matching the
* declared composite shader types or as already-flattened leaf-path values.
*/
getFlatUniformValues(
uniformValues: Readonly<Record<string, CompositeUniformValue>>
): Record<string, UniformValue> {
const flattenedUniformValues: Record<string, UniformValue> = {};
for (const [name, value] of Object.entries(uniformValues)) {
const uniformType = this.layout.uniformTypes[name];
if (uniformType) {
this._flattenCompositeValue(flattenedUniformValues, name, uniformType, value);
} else if (this.layout.fields[name]) {
flattenedUniformValues[name] = value as UniformValue;
}
}
return flattenedUniformValues;
}
/**
* Serializes the supplied values into buffer-backed binary data.
*
* The returned view length matches {@link ShaderBlockLayout.byteLength}, which
* is the exact packed size of the block.
*/
getData(uniformValues: Readonly<Record<string, CompositeUniformValue>>): Uint8Array {
const buffer = getScratchArrayBuffer(this.layout.byteLength);
new Uint8Array(buffer, 0, this.layout.byteLength).fill(0);
const typedArrays = {
i32: new Int32Array(buffer),
u32: new Uint32Array(buffer),
f32: new Float32Array(buffer),
f16: new Uint16Array(buffer)
};
const flattenedUniformValues = this.getFlatUniformValues(uniformValues);
for (const [name, value] of Object.entries(flattenedUniformValues)) {
this._writeLeafValue(typedArrays, name, value);
}
return new Uint8Array(buffer, 0, this.layout.byteLength);
}
/**
* Recursively flattens nested values using the declared composite shader type.
*/
private _flattenCompositeValue(
flattenedUniformValues: Record<string, UniformValue>,
baseName: string,
uniformType: CompositeShaderType,
value: CompositeUniformValue | undefined
): void {
if (value === undefined) {
return;
}
if (typeof uniformType === 'string' || this.layout.fields[baseName]) {
flattenedUniformValues[baseName] = value as UniformValue;
return;
}
if (Array.isArray(uniformType)) {
const elementType = uniformType[0] as CompositeShaderType;
const length = uniformType[1] as number;
if (Array.isArray(elementType)) {
throw new Error(`Nested arrays are not supported for ${baseName}`);
}
if (typeof elementType === 'string' && isNumberArray(value)) {
this._flattenPackedArray(flattenedUniformValues, baseName, elementType, length, value);
return;
}
if (!Array.isArray(value)) {
log.warn(`Unsupported uniform array value for ${baseName}:`, value)();
return;
}
for (let index = 0; index < Math.min(value.length, length); index++) {
const elementValue = value[index];
if (elementValue === undefined) {
continue;
}
this._flattenCompositeValue(
flattenedUniformValues,
`${baseName}[${index}]`,
elementType,
elementValue
);
}
return;
}
if (isCompositeShaderTypeStruct(uniformType) && isCompositeUniformObject(value)) {
for (const [key, subValue] of Object.entries(value)) {
if (subValue === undefined) {
continue;
}
const nestedName = `${baseName}.${key}`;
this._flattenCompositeValue(flattenedUniformValues, nestedName, uniformType[key], subValue);
}
return;
}
log.warn(`Unsupported uniform value for ${baseName}:`, value)();
}
/**
* Expands tightly packed numeric arrays into per-element leaf fields.
*/
private _flattenPackedArray(
flattenedUniformValues: Record<string, UniformValue>,
baseName: string,
elementType: VariableShaderType,
length: number,
value: UniformValue
): void {
const numericValue = value as Readonly<ArrayLike<number>>;
const elementLayout = getLeafLayoutInfo(elementType, this.layout.layout);
const packedElementLength = elementLayout.components;
for (let index = 0; index < length; index++) {
const start = index * packedElementLength;
if (start >= numericValue.length) {
break;
}
if (packedElementLength === 1) {
flattenedUniformValues[`${baseName}[${index}]`] = Number(numericValue[start]);
} else {
flattenedUniformValues[`${baseName}[${index}]`] = sliceNumericArray(
value,
start,
start + packedElementLength
) as UniformValue;
}
}
}
/**
* Writes one flattened leaf value into its typed-array view.
*/
private _writeLeafValue(
typedArrays: Record<string, any>,
name: string,
value: UniformValue
): void {
const entry = this.layout.fields[name];
if (!entry) {
log.warn(`Uniform ${name} not found in layout`)();
return;
}
const {type, components, columns, rows, offset, columnStride} = entry;
const array = typedArrays[type];
if (components === 1) {
array[offset] = Number(value);
return;
}
const sourceValue = value as Readonly<ArrayLike<number>>;
if (columns === 1) {
for (let componentIndex = 0; componentIndex < components; componentIndex++) {
array[offset + componentIndex] = Number(sourceValue[componentIndex] ?? 0);
}
return;
}
let sourceIndex = 0;
for (let columnIndex = 0; columnIndex < columns; columnIndex++) {
const columnOffset = offset + columnIndex * columnStride;
for (let rowIndex = 0; rowIndex < rows; rowIndex++) {
array[columnOffset + rowIndex] = Number(sourceValue[sourceIndex++] ?? 0);
}
}
}
}
/**
* Type guard for nested uniform objects.
*/
function isCompositeUniformObject(
value: CompositeUniformValue
): value is Record<string, CompositeUniformValue | undefined> {
return (
Boolean(value) &&
typeof value === 'object' &&
!Array.isArray(value) &&
!ArrayBuffer.isView(value)
);
}
/**
* Slices a numeric array-like value without changing its numeric representation.
*/
function sliceNumericArray(value: UniformValue, start: number, end: number): number[] {
return Array.prototype.slice.call(value, start, end) as number[];
}