Files
workinf_Blender_Wasm/web/protocol/volume-vdb.ts
2026-08-12 04:47:48 -04:00

80 lines
4.0 KiB
TypeScript

import { normalizeProjectAssetPath } from "./asset-path";
import type { VolumeGridMetadataIR } from "./scene-ir";
export const VDB_MAX_RESOURCE_BYTES = 512 * 1024 * 1024;
export const VDB_MAX_ACTIVE_VOXELS = 64_000_000;
export const VDB_MAX_GRIDS = 64;
export interface VDBResourceManifest {
projectId: string;
sourcePath: string;
byteLength: number;
sha256: string;
grids: VolumeGridMetadataIR[];
}
export interface VDBDecodeRequest extends VDBResourceManifest {
data: ArrayBuffer;
}
export interface VDBDecodeResult {
metadata: VDBResourceManifest;
decodedByteLength: number;
}
export type VDBDecoder = (request: VDBDecodeRequest, signal: AbortSignal) => Promise<VDBDecodeResult>;
function invalid(message: string): never {
throw new Error(`NON_MESH_BINARY_INVALID: ${message}`);
}
export function validateVDBManifest(manifest: VDBResourceManifest): VDBResourceManifest {
if (!manifest.projectId || !/^[a-zA-Z0-9._-]+$/.test(manifest.projectId)) invalid("VDB projectId is invalid");
let sourcePath: string;
try {
sourcePath = normalizeProjectAssetPath(manifest.sourcePath);
}
catch {
throw new Error("NON_MESH_RESOURCE_OUTSIDE_PROJECT: VDB path is outside the project asset root");
}
if (!sourcePath.toLowerCase().endsWith(".vdb")) invalid("Volume resources must use the .vdb extension");
if (!Number.isSafeInteger(manifest.byteLength) || manifest.byteLength <= 0 || manifest.byteLength > VDB_MAX_RESOURCE_BYTES) throw new Error("NON_MESH_VDB_BUDGET_EXCEEDED: VDB resource size is outside the bounded range");
if (!/^[a-f0-9]{64}$/.test(manifest.sha256)) invalid("VDB SHA-256 is invalid");
if (!Array.isArray(manifest.grids) || manifest.grids.length === 0 || manifest.grids.length > VDB_MAX_GRIDS) throw new Error("NON_MESH_VDB_BUDGET_EXCEEDED: VDB grid count is outside the bounded range");
const names = new Set<string>();
let activeVoxels = 0;
for (const grid of manifest.grids) {
if (!grid.name || names.has(grid.name) || !grid.valueType) invalid("VDB grid identity is missing or duplicated");
names.add(grid.name);
const count = grid.activeVoxelCount ?? grid.voxelCount;
if (!Number.isSafeInteger(count) || count < 0) invalid(`VDB grid ${grid.name} has an invalid active voxel count`);
activeVoxels += count;
if (!Number.isSafeInteger(activeVoxels) || activeVoxels > VDB_MAX_ACTIVE_VOXELS) throw new Error("NON_MESH_VDB_BUDGET_EXCEEDED: VDB active voxel budget exceeded");
if (grid.bounds && grid.bounds.min.some((value, index) => !Number.isFinite(value) || value > grid.bounds!.max[index])) invalid(`VDB grid ${grid.name} bounds are invalid`);
}
return { ...manifest, sourcePath };
}
function hex(bytes: Uint8Array): string {
return Array.from(bytes, (value) => value.toString(16).padStart(2, "0")).join("");
}
export async function decodeVDBResource(
request: VDBDecodeRequest,
decoder: VDBDecoder | undefined,
signal: AbortSignal,
): Promise<VDBDecodeResult> {
const metadata = validateVDBManifest(request);
if (signal.aborted) throw new DOMException("VDB decode cancelled", "AbortError");
if (request.data.byteLength !== metadata.byteLength) invalid("VDB byte length does not match its manifest");
if (!globalThis.crypto?.subtle) invalid("SHA-256 is unavailable");
const digest = hex(new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", request.data)));
if (digest !== metadata.sha256) invalid("VDB bytes do not match the manifest SHA-256");
if (signal.aborted) throw new DOMException("VDB decode cancelled", "AbortError");
if (!decoder) throw new Error("VOLUME_SHADER_UNAVAILABLE: no bounded OpenVDB decoder is installed");
const result = await decoder({ ...request, ...metadata }, signal);
if (signal.aborted) throw new DOMException("VDB decode cancelled", "AbortError");
if (!Number.isSafeInteger(result.decodedByteLength) || result.decodedByteLength < 0 || result.decodedByteLength > VDB_MAX_RESOURCE_BYTES * 2) throw new Error("NON_MESH_VDB_BUDGET_EXCEEDED: decoded VDB memory budget exceeded");
return { ...result, metadata };
}