// 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 { const flattenedUniformValues: Record = {}; 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>): 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, 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, baseName: string, elementType: VariableShaderType, length: number, value: UniformValue ): void { const numericValue = value as Readonly>; 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, 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>; 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 { 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[]; }