80 lines
4.0 KiB
TypeScript
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 };
|
|
}
|