Complete V1 performance and OOM release gates

This commit is contained in:
mes123456
2026-08-14 22:32:09 -04:00
parent 3ea9974eee
commit a3f3071c03
45 changed files with 4206 additions and 276 deletions

View File

@@ -0,0 +1,179 @@
import {
MeshGeometryStreamError,
meshGeometryStreamChunkSha256,
meshGeometryStreamManifestSha256,
parseMeshGeometryStreamRequest,
type MeshGeometryStreamChunkIR,
type MeshGeometryStreamChunkRecordIR,
type MeshGeometryStreamRequestIR,
type MeshGeometryStreamWorkerRequest,
type MeshGeometryStreamWorkerResponse,
} from "../../../protocol/geometry-stream";
const workerScope = self as unknown as {
onmessage: ((event: MessageEvent<MeshGeometryStreamWorkerRequest>) => void) | null;
postMessage: (message: MeshGeometryStreamWorkerResponse, transfer?: Transferable[]) => void;
};
type Advance = "ack" | "cancel";
interface ActiveStream {
request: MeshGeometryStreamRequestIR;
cancelled: boolean;
resolveAdvance?: (advance: Advance) => void;
}
let active: ActiveStream | null = null;
function post(message: MeshGeometryStreamWorkerResponse, transfer: Transferable[] = []): void {
workerScope.postMessage(message, transfer);
}
function createGridGeometry(
request: MeshGeometryStreamRequestIR,
chunkIndex: number,
triangleOffset: number,
triangleCount: number,
meshId = request.meshId,
): Omit<MeshGeometryStreamChunkIR, "sha256"> {
const columns = Math.ceil(Math.sqrt(triangleCount / 2));
const rows = Math.ceil(triangleCount / (columns * 2));
const positions = new Float32Array((columns + 1) * (rows + 1) * 3);
for (let y = 0; y <= rows; y++) {
for (let x = 0; x <= columns; x++) {
const offset = (y * (columns + 1) + x) * 3;
positions[offset] = x / Math.max(1, columns) * 4 - 2;
positions[offset + 1] = y / Math.max(1, rows) * 4 - 2;
positions[offset + 2] = Math.sin((triangleOffset + x) * 0.0003) * Math.cos(y * 0.03) * 0.08;
}
}
const indices = new Uint32Array(triangleCount * 3);
for (let triangle = 0; triangle < triangleCount; triangle++) {
const cell = Math.floor(triangle / 2);
const x = cell % columns;
const y = Math.floor(cell / columns);
const a = y * (columns + 1) + x;
const offset = triangle * 3;
if (triangle % 2 === 0) indices.set([a, a + 1, a + columns + 2], offset);
else indices.set([a, a + columns + 2, a + columns + 1], offset);
}
return {
schemaVersion: 1,
streamId: request.streamId,
meshId,
chunkIndex,
chunkCount: Math.ceil(request.triangleCount / request.chunkTriangleCount),
triangleOffset,
triangleCount,
vertexCount: positions.length / 3,
byteLength: positions.byteLength + indices.byteLength,
positions: positions.buffer,
indices: indices.buffer,
};
}
function waitForAdvance(stream: ActiveStream): Promise<Advance> {
if (stream.cancelled) return Promise.resolve("cancel");
return new Promise((resolve) => { stream.resolveAdvance = resolve; });
}
function releaseAdvance(stream: ActiveStream, advance: Advance): void {
const resolve = stream.resolveAdvance;
stream.resolveAdvance = undefined;
resolve?.(advance);
}
async function sendGeometry(
stream: ActiveStream,
type: "lod" | "chunk",
unsigned: Omit<MeshGeometryStreamChunkIR, "sha256">,
): Promise<Advance> {
const geometry: MeshGeometryStreamChunkIR = { ...unsigned, sha256: await meshGeometryStreamChunkSha256(unsigned) };
const advance = waitForAdvance(stream);
const positions = geometry.positions;
const indices = geometry.indices;
post(type === "lod" ? { type, streamId: stream.request.streamId, geometry } : { type, streamId: stream.request.streamId, chunk: geometry }, [positions, indices]);
post({ type: "detached", streamId: stream.request.streamId, chunkIndex: geometry.chunkIndex, positionsByteLength: positions.byteLength, indicesByteLength: indices.byteLength });
return advance;
}
async function run(value: unknown): Promise<void> {
let request: MeshGeometryStreamRequestIR | undefined;
try {
request = parseMeshGeometryStreamRequest(value);
if (active) throw new MeshGeometryStreamError("GEOMETRY_STREAM_INVALID", "another geometry stream is active");
const stream: ActiveStream = { request, cancelled: false };
active = stream;
let transferredBytes = 0;
let peakWorkingSetBytes = 0;
const chunks: MeshGeometryStreamChunkRecordIR[] = [];
const lod = createGridGeometry(request, -1, 0, request.lodTriangleCount, `${request.meshId}:lod:0`);
if (await sendGeometry(stream, "lod", lod) === "cancel") {
post({ type: "cancelled", report: { streamId: request.streamId, meshId: request.meshId, status: "CANCELLED", triangleCount: 0, chunkCount: 0, transferredBytes: 0, peakWorkingSetBytes: lod.byteLength } });
return;
}
const chunkCount = Math.ceil(request.triangleCount / request.chunkTriangleCount);
let triangleOffset = 0;
for (let chunkIndex = 0; chunkIndex < chunkCount; chunkIndex++) {
const triangleCount = Math.min(request.chunkTriangleCount, request.triangleCount - triangleOffset);
const unsigned = createGridGeometry(request, chunkIndex, triangleOffset, triangleCount);
const sha256 = await meshGeometryStreamChunkSha256(unsigned);
const record = { chunkIndex, triangleOffset, triangleCount, vertexCount: unsigned.vertexCount, byteLength: unsigned.byteLength, sha256 };
const advance = waitForAdvance(stream);
const chunk: MeshGeometryStreamChunkIR = { ...unsigned, sha256 };
const positions = chunk.positions;
const indices = chunk.indices;
post({ type: "chunk", streamId: request.streamId, chunk }, [positions, indices]);
post({ type: "detached", streamId: request.streamId, chunkIndex, positionsByteLength: positions.byteLength, indicesByteLength: indices.byteLength });
transferredBytes += chunk.byteLength;
peakWorkingSetBytes = Math.max(peakWorkingSetBytes, chunk.byteLength, lod.byteLength);
chunks.push(record);
if (await advance === "cancel") {
post({ type: "cancelled", report: { streamId: request.streamId, meshId: request.meshId, status: "CANCELLED", triangleCount: triangleOffset + triangleCount, chunkCount: chunks.length, transferredBytes, peakWorkingSetBytes } });
return;
}
triangleOffset += triangleCount;
}
post({
type: "complete",
report: {
streamId: request.streamId,
meshId: request.meshId,
status: "COMPLETED",
triangleCount: request.triangleCount,
chunkCount,
transferredBytes,
peakWorkingSetBytes,
manifestSha256: await meshGeometryStreamManifestSha256(request, chunks),
},
chunks,
});
}
catch (error) {
post({
type: "error",
streamId: request?.streamId,
code: error instanceof MeshGeometryStreamError ? error.code : "GEOMETRY_STREAM_INVALID",
message: error instanceof Error ? error.message : String(error),
});
}
finally {
active = null;
}
}
workerScope.onmessage = (event: MessageEvent<MeshGeometryStreamWorkerRequest>): void => {
const message = event.data;
if (message.type === "start") {
void run(message.request);
return;
}
if (!active || active.request.streamId !== message.streamId) return;
if (message.type === "cancel") {
active.cancelled = true;
releaseAdvance(active, "cancel");
}
else if (message.type === "ack") {
releaseAdvance(active, "ack");
}
};

View File

@@ -0,0 +1,79 @@
import {
LongMediaTimelineSession,
buildLongMediaTimelineIndex,
type LongMediaSeekResultIR,
} from "../sequencer/LongMediaTimeline";
interface MediaAssetMessage {
sourceId: string;
data: ArrayBuffer;
}
type Request =
| { type: "init"; timeline: unknown; assets: MediaAssetMessage[]; cacheMaxBytes: number }
| { type: "seek"; requestId: string; frame: number; decodeDelayMs?: number }
| { type: "cancel" }
| { type: "dispose" };
type Response =
| { type: "ready"; index: { stripCount: number; bucketCount: number; referenceCount: number; estimatedBytes: number } }
| { type: "seekResult"; requestId: string; result: LongMediaSeekResultIR }
| { type: "disposed"; cacheBytes: number }
| { type: "error"; requestId?: string; message: string };
const scope = self as unknown as {
onmessage: ((event: MessageEvent<Request>) => void) | null;
postMessage: (message: Response) => void;
};
let session: LongMediaTimelineSession | null = null;
let assets = new Map<string, ArrayBuffer>();
let decodeDelayMs = 0;
function abortableDelay(delayMs: number, signal: AbortSignal): Promise<void> {
if (signal.aborted) return Promise.reject(new DOMException("Long media decode cancelled", "AbortError"));
if (delayMs <= 0) return Promise.resolve();
return new Promise((resolve, reject) => {
const timer = setTimeout(resolve, delayMs);
signal.addEventListener("abort", () => { clearTimeout(timer); reject(new DOMException("Long media decode cancelled", "AbortError")); }, { once: true });
});
}
scope.onmessage = (event): void => {
const message = event.data;
if (message.type === "init") {
void (async () => {
try {
session?.dispose();
const index = await buildLongMediaTimelineIndex(message.timeline, new AbortController().signal);
assets = new Map(message.assets.map((asset) => [asset.sourceId, asset.data]));
session = new LongMediaTimelineSession(index, {
load: async (strip, _sourceFrame, signal) => {
await abortableDelay(decodeDelayMs, signal);
const data = strip.sourceId ? assets.get(strip.sourceId) : undefined;
if (!data) throw new Error(`SEQUENCER_RESOURCE_MISSING: ${strip.id} source bytes are unavailable`);
return data.slice(0);
},
}, message.cacheMaxBytes);
scope.postMessage({ type: "ready", index: index.stats });
}
catch (error) { scope.postMessage({ type: "error", message: error instanceof Error ? error.message : String(error) }); }
})();
return;
}
if (message.type === "seek") {
const active = session;
if (!active) { scope.postMessage({ type: "error", requestId: message.requestId, message: "SEQUENCER_RESOURCE_MISSING: long media session is not initialized" }); return; }
decodeDelayMs = message.decodeDelayMs ?? 0;
void active.seek(message.frame)
.then((result) => scope.postMessage({ type: "seekResult", requestId: message.requestId, result }))
.catch((error) => scope.postMessage({ type: "error", requestId: message.requestId, message: error instanceof Error ? error.message : String(error) }));
return;
}
if (message.type === "cancel") { session?.cancel(); return; }
const cacheBytes = session?.cache.stats().bytes ?? 0;
session?.dispose();
session = null;
assets.clear();
scope.postMessage({ type: "disposed", cacheBytes });
};

View File

@@ -1,19 +1,33 @@
import { gateRelease, parseReleaseManifest, serializeReleaseManifest } from "../../../protocol/release-gate";
const family = (id: string, dependencies: string[] = []) => ({ id, name: id, status: "BLOCKED", roadmapStatus: "planned", completedSlices: ["schema"], blockedSlices: ["A", "B"], excludedSlices: [], acceptance: [], dependencies });
const family = (id: string, dependencies: string[] = []) => ({
id,
name: id,
parityStatus: "BLOCKED",
releaseClass: "LOCAL_BOUNDED",
releaseStatus: "BLOCKED",
roadmapStatus: "planned",
completedSlices: ["schema"],
blockedSlices: ["A", "B"],
excludedSlices: [],
v1RequiredSlices: ["A"],
v1ExcludedSlices: ["B"],
acceptance: [],
dependencies,
});
const evidenceRecord = { id: "fixture", fields: ["runtime.offline", "performance.geometry1M", "faults.malformedBlend", "faults.zipBomb", "provenance.license"], command: "fixture", exitCode: 0, durationMs: 1, output: "fixture passed", artifactSha256: ["a".repeat(64)] };
const evidence = { browser: { chromium: false }, runtime: { offline: true, workerRestart: false, opfsRecovery: false }, performance: { geometry1M: true, geometry10M: false, texture4K: false, texture8K: false, longMedia: false, simulationCache: false }, faults: { oom: false, deviceLoss: false, networkInterrupt: false, malformedBlend: true, zipBomb: true }, provenance: { license: true, sbom: false, sourceOffer: false, deterministicPackage: false }, records: [evidenceRecord] };
const base = { schemaVersion: 3, source: "docs/status/parity-ledger.json", sourceSha256: "b".repeat(64), generatedAt: "2026-08-11T00:00:00.000Z", families: [family("N-015"), family("N-016", ["N-015"])], evidence };
const base = { schemaVersion: 4, source: "docs/status/parity-ledger.json", sourceSha256: "b".repeat(64), generatedAt: "2026-08-11T00:00:00.000Z", families: [family("N-015"), family("N-016", ["N-015"])], evidence };
self.onmessage = () => {
const result: Record<string, unknown> = {};
try { const parsed = parseReleaseManifest(base); result.valid = [parsed.families.length, serializeReleaseManifest(base) === serializeReleaseManifest({ ...base, families: [...base.families].reverse() })]; } catch (error) { result.valid = error instanceof Error ? error.message : String(error); }
const gate = gateRelease(base); result.gate = [gate.status, gate.issues.map((issue) => issue.code)];
try { parseReleaseManifest({ ...base, schemaVersion: 2 }); } catch (error) { result.oldSchema = error instanceof Error ? error.message : String(error); }
try { parseReleaseManifest({ ...base, schemaVersion: 3 }); } catch (error) { result.oldSchema = error instanceof Error ? error.message : String(error); }
try { parseReleaseManifest({ ...base, families: [family("N-015", ["N-016"]), family("N-016", ["N-015"])] }); } catch (error) { result.cycle = error instanceof Error ? error.message : String(error); }
try { parseReleaseManifest({ ...base, families: [{ ...family("N-015"), status: "LOCAL_EXACT", completedSlices: [] }] }); } catch (error) { result.status = error instanceof Error ? error.message : String(error); }
try { parseReleaseManifest({ ...base, families: [{ ...family("N-015"), releaseStatus: "READY" }] }); } catch (error) { result.status = error instanceof Error ? error.message : String(error); }
try { parseReleaseManifest({ ...base, evidence: { ...evidence, browser: { chromium: true } } }); } catch (error) { result.unbound = error instanceof Error ? error.message : String(error); }
try { parseReleaseManifest({ ...base, families: [{ ...family("N-015"), excludedSlices: ["A"], blockedSlices: ["A", "B"] }, family("N-016", ["N-015"])] }); } catch (error) { result.excludedOverlap = error instanceof Error ? error.message : String(error); }
try { parseReleaseManifest({ ...base, families: [{ ...family("N-015"), v1ExcludedSlices: ["A"] }, family("N-016", ["N-015"])] }); } catch (error) { result.excludedOverlap = error instanceof Error ? error.message : String(error); }
try { parseReleaseManifest({ ...base, evidence: { ...evidence, records: [{ ...evidenceRecord, fields: ["performance.geometry10M"] }] } }); } catch (error) { result.disabledEvidence = error instanceof Error ? error.message : String(error); }
try { parseReleaseManifest({ ...base, evidence: { ...evidence, records: [{ ...evidenceRecord, artifactSha256: [] }] } }); } catch (error) { result.emptyArtifact = error instanceof Error ? error.message : String(error); }
try { parseReleaseManifest({ ...base, generatedAt: "2026-02-31T00:00:00.000Z" }); } catch (error) { result.generatedAt = error instanceof Error ? error.message : String(error); }

View File

@@ -1,12 +1,14 @@
import { verifyNanoVDBChunk, type NanoVDBBundleManifestIR } from "../../../protocol/volume-vdb";
import {
createNanoVDBFloat32GridPaged,
NanoVDBGpuPageAllocator,
NanoVDBWebGPUDeviceSession,
readNanoVDBWordsWebGPU,
sampleNanoVDBFloat32WebGPU,
uploadNanoVDBFloat32GridPaged,
} from "../render/nanovdb-volume-renderer";
import { createResumableHttpNanoVDBRangeSource } from "../volume/nanovdb-stream";
import { loadNanoVDBViewportAsset } from "../volume/nanovdb-viewport";
import { loadNanoVDBGridPage, loadNanoVDBViewportAsset, planNanoVDBGridResidency } from "../volume/nanovdb-viewport";
const scope = self as unknown as { onmessage: (() => void) | null; postMessage: (value: unknown) => void };
@@ -128,12 +130,64 @@ scope.onmessage = (): void => {
const lru = allocator.stats();
allocator.dispose();
const pageByteLength = 256 * 1024;
const pagingManifest: NanoVDBBundleManifestIR = {
...manifest,
gpu: { ...manifest.gpu, pageByteLength, maxResidentBytes: 2 * pageByteLength },
};
const globalResidency = planNanoVDBGridResidency({
...pagingManifest,
gpu: { ...pagingManifest.gpu, maxResidentBytes: 3 * pageByteLength },
});
const pageRanges: string[] = [];
const pageSource = createResumableHttpNanoVDBRangeSource("/__vdb_fixture__/bundle", manifest.bundleByteLength, {
fetcher: async (input, init) => {
pageRanges.push(new Headers(init?.headers).get("Range") ?? "");
return fetch(input, init);
},
retries: 0,
requireStableEtag: true,
});
const constrained = createNanoVDBFloat32GridPaged(device, payload.byteLength, pageByteLength, 2 * pageByteLength);
const initialVirtualPages = [...constrained.residentVirtualPages];
const page0 = await loadNanoVDBGridPage(pagingManifest, density.name, 0, pageSource, new AbortController().signal);
const page1 = await loadNanoVDBGridPage(pagingManifest, density.name, 1, pageSource, new AbortController().signal);
const lastPageIndex = constrained.pageCount - 1;
const lastPage = await loadNanoVDBGridPage(pagingManifest, density.name, lastPageIndex, pageSource, new AbortController().signal);
constrained.uploadPage(0, page0);
constrained.uploadPage(1, page1);
constrained.touchPage(0);
constrained.uploadPage(lastPageIndex, lastPage);
const lastWords = new Uint32Array(lastPage);
const lastWordIndex = lastWords.findIndex((value) => value !== 0);
if (lastWordIndex < 0) throw new Error("VDB demand page fixture has no observable word");
const wordOffsets = [0, lastPageIndex * pageByteLength + lastWordIndex * 4, pageByteLength];
const expectedWords = [new Uint32Array(page0)[0], lastWords[lastWordIndex], 0];
const demandWords = await readNanoVDBWordsWebGPU(device, constrained, wordOffsets);
let incompletePage = "";
try { constrained.uploadPage(2, new ArrayBuffer(4)); }
catch (error) { incompletePage = error instanceof Error ? error.message : String(error); }
const demandPaging = {
pageCount: constrained.pageCount,
residentPageCapacity: constrained.residentPageCapacity,
residentBytes: constrained.residentBytes,
maxResidentBytes: constrained.maxResidentBytes,
evictions: constrained.evictionCount,
initialVirtualPages,
residentVirtualPages: [...constrained.residentVirtualPages],
requestedPageResident: constrained.hasResidentPage(lastPageIndex),
touchedPageResident: constrained.hasResidentPage(0),
evictedPageResident: constrained.hasResidentPage(1),
pageRanges,
wordOffsets,
expectedWords,
demandWords,
incompletePage,
};
constrained.dispose();
let oom = "";
try {
const constrained = uploadNanoVDBFloat32GridPaged(device, payload, 256 * 1024, 256 * 1024);
if (constrained.residentPageCount < constrained.pageCount) oom = "NANOVDB_GPU_BUDGET_EXCEEDED: resident paging active";
constrained.dispose();
}
try { uploadNanoVDBFloat32GridPaged(device, payload, 256 * 1024, 128 * 1024); }
catch (error) { oom = error instanceof Error ? error.message : String(error); }
const residentBudget = Math.ceil(payload.byteLength / (256 * 1024)) * 256 * 1024;
@@ -145,6 +199,11 @@ scope.onmessage = (): void => {
device.destroy();
const loss = await session.waitForLoss();
const recoveredDevice = await session.recover(payload.byteLength + 256 * 1024);
const recoveredDemand = createNanoVDBFloat32GridPaged(recoveredDevice, payload.byteLength, pageByteLength, 2 * pageByteLength);
recoveredDemand.uploadPage(0, await loadNanoVDBGridPage(pagingManifest, density.name, 0, pageSource, new AbortController().signal));
recoveredDemand.uploadPage(lastPageIndex, await loadNanoVDBGridPage(pagingManifest, density.name, lastPageIndex, pageSource, new AbortController().signal));
const recoveredDemandWords = await readNanoVDBWordsWebGPU(recoveredDevice, recoveredDemand, wordOffsets.slice(0, 2));
recoveredDemand.dispose();
const recovered = uploadNanoVDBFloat32GridPaged(recoveredDevice, payload, 256 * 1024, residentBudget);
const after = await sampleNanoVDBFloat32WebGPU(recoveredDevice, recovered, native.map((sample) => sample.coord));
recovered.dispose();
@@ -165,8 +224,10 @@ scope.onmessage = (): void => {
},
lru,
oom,
demandPaging,
globalResidency,
paging,
deviceLoss: { reason: loss.reason, firstGeneration, recoveredGeneration },
deviceLoss: { reason: loss.reason, firstGeneration, recoveredGeneration, recoveredDemandWords },
samplesStable: JSON.stringify(before) === JSON.stringify(after),
};
})().then((result) => scope.postMessage(result)).catch((error) => scope.postMessage({ error: error instanceof Error ? error.message : String(error) }));

View File

@@ -1,7 +1,15 @@
import { validateNanoVDBBundleManifest, type NanoVDBBundleManifestIR } from "../../../protocol/volume-vdb";
import { NanoVDBFloat32Sampler } from "../volume/nanovdb-float32";
import { mapPrincipledVolumeToNanoVDB } from "../volume/volume-material-mapping";
import { probeNanoVDBWebGPU, renderNanoVDBFloat32WebGPU, sampleNanoVDBFloat32WebGPU, uploadNanoVDBFloat32Grid } from "../render/nanovdb-volume-renderer";
import {
probeNanoVDBWebGPU,
renderNanoVDBFloat32WebGPU,
sampleNanoVDBFloat32WebGPU,
uploadNanoVDBFloat32Grid,
uploadNanoVDBFloat32GridPaged,
type NanoVDBMaterialGridUploadsIR,
type NanoVDBViewAxis,
} from "../render/nanovdb-volume-renderer";
const scope = self as unknown as { onmessage: (() => void) | null; postMessage: (value: unknown) => void };
@@ -12,12 +20,17 @@ scope.onmessage = (): void => {
const density = manifest.grids.find((grid) => grid.name === manifest.material.densityGrid);
const nativeDensity = report.grids.find((grid) => grid.name === manifest.material.densityGrid);
if (!density || !nativeDensity?.scalarSamples || !manifest.gpu.float32TreeLayout) throw new Error("VDB WebGPU fixture is incomplete");
const response = await fetch("/__vdb_fixture__/bundle", { headers: { Range: `bytes=${density.byteOffset}-${density.byteOffset + density.byteLength - 1}` }, cache: "no-store" });
if (response.status !== 206) throw new Error(`VDB payload range returned ${response.status}`);
const payload = await response.arrayBuffer();
const fetchGrid = async (name: string): Promise<ArrayBuffer> => {
const grid = manifest.grids.find((candidate) => candidate.name === name);
if (!grid) throw new Error(`VDB grid ${name} is missing`);
const response = await fetch("/__vdb_fixture__/bundle", { headers: { Range: `bytes=${grid.byteOffset}-${grid.byteOffset + grid.byteLength - 1}` }, cache: "no-store" });
if (response.status !== 206) throw new Error(`VDB payload range returned ${response.status}`);
return response.arrayBuffer();
};
const payload = await fetchGrid(density.name);
const cpu = new NanoVDBFloat32Sampler(payload, density, manifest.gpu.float32TreeLayout);
const cpuSamples = nativeDensity.scalarSamples.map((sample) => ({ coord: sample.coord, ...cpu.nearest(sample.coord), expectedValue: sample.value, expectedActive: sample.active }));
const probe = await probeNanoVDBWebGPU(payload.byteLength);
const probe = await probeNanoVDBWebGPU(payload.byteLength, 9);
if (!probe.capability.available || !probe.device) throw new Error(probe.capability.reason ?? "WebGPU adapter is unavailable");
const device = probe.device;
device.pushErrorScope("validation");
@@ -37,7 +50,30 @@ scope.onmessage = (): void => {
anisotropy: 0.2,
interpolation: "LINEAR",
});
const pixels = await renderNanoVDBFloat32WebGPU(device, uploaded, density, materialMapping.material, 96, 96);
const materialUploads: NanoVDBMaterialGridUploadsIR = {};
if (materialMapping.material.temperatureGrid) {
materialUploads.temperature = uploadNanoVDBFloat32GridPaged(device, await fetchGrid(materialMapping.material.temperatureGrid), 256 * 1024, 16 * 1024 * 1024);
}
if (materialMapping.material.colorGrid) {
materialUploads.color = uploadNanoVDBFloat32GridPaged(device, await fetchGrid(materialMapping.material.colorGrid), 256 * 1024, 16 * 1024 * 1024);
}
const emissionManifest = validateNanoVDBBundleManifest({
...manifest,
grids: manifest.grids.map((grid) => grid.name === manifest.material.temperatureGrid ? { ...grid, semantic: "EMISSION" as const } : grid),
material: { ...manifest.material, temperatureGrid: undefined, emissionGrid: manifest.material.temperatureGrid },
});
const emissionMapping = mapPrincipledVolumeToNanoVDB(emissionManifest, {
densityGrid: emissionManifest.material.densityGrid,
densityScale: 1,
emissionGrid: emissionManifest.material.emissionGrid,
emissionColor: [1, 0.2, 0.05],
emissionScale: 0.0005,
});
materialUploads.emission = materialUploads.temperature;
const combinedMaterial = { ...materialMapping.material, emissionGrid: emissionMapping.material.emissionGrid };
const viewAxis: NanoVDBViewAxis = "X";
const pixels = await renderNanoVDBFloat32WebGPU(device, uploaded, density, combinedMaterial, 64, 64, materialUploads, viewAxis);
const emissionVisiblePixels = Array.from({ length: pixels.length / 4 }, (_, index) => pixels[index * 4 + 3] > 0).filter(Boolean).length;
const validationError = await device.popErrorScope();
if (validationError) throw new Error(`WebGPU validation failed: ${validationError.message}`);
let visiblePixels = 0;
@@ -47,6 +83,8 @@ scope.onmessage = (): void => {
if (pixels[index] > 0) visiblePixels++;
}
const imageHash = await crypto.subtle.digest("SHA-256", new Uint8Array(pixels).buffer);
materialUploads.temperature?.dispose();
materialUploads.color?.dispose();
uploaded.dispose();
device.destroy();
return {
@@ -58,6 +96,9 @@ scope.onmessage = (): void => {
visiblePixels,
alphaSum,
imageSha256: Array.from(new Uint8Array(imageHash), (byte) => byte.toString(16).padStart(2, "0")).join(""),
viewAxis,
emissionVisiblePixels,
emissionMapping,
materialMapping,
};
})().then((result) => scope.postMessage(result)).catch((error) => scope.postMessage({ error: error instanceof Error ? error.message : String(error) }));

View File

@@ -30,7 +30,7 @@ import type { OffscreenViewportRequest, OffscreenViewportResponse } from "../thr
import type { NonMeshElementKind } from "../three-adapter/nonmesh";
import type { CurveGizmoFrameIR, CurveGizmoScreenFrameIR } from "../../../protocol/nonmesh-interaction";
import type { NanoVDBViewportAssetIR, NanoVDBViewportRenderResultIR } from "../volume/nanovdb-viewport";
import { NanoVDBViewportRenderSession, renderNanoVDBViewportAsset } from "../volume/nanovdb-viewport";
import { NANOVDB_VIEWPORT_PREVIEW_SIZE, NanoVDBViewportRenderSession, renderNanoVDBViewportAsset } from "../volume/nanovdb-viewport";
import { createNanoVDBViewportObject } from "../three-adapter/volume";
import {
configurePBRLight,
@@ -74,6 +74,7 @@ raycaster.params.Points.threshold = 0.14;
const objectById = new Map<string, Object3D>();
let curveGizmoFrame: { dataId: string; frame: CurveGizmoFrameIR } | null = null;
let volumeAssets: NanoVDBViewportAssetIR[] = [];
let volumeAssetsInitialized = false;
const volumeRenderCache = new Map<string, NanoVDBViewportRenderResultIR>();
let volumeRenderGeneration = 0;
const volumeRenderSession = new NanoVDBViewportRenderSession(() => {
@@ -249,6 +250,7 @@ async function refreshVolumes(): Promise<void> {
return;
}
post({ type: "volumeStatus", status: "loading", count: 0 });
if (!volumeAssetsInitialized) return;
try {
let count = 0;
for (const node of nodes) {
@@ -257,7 +259,7 @@ async function refreshVolumes(): Promise<void> {
const cacheKey = `${asset.dataId}:${asset.manifest.bundleSha256}:${JSON.stringify(asset.material ?? asset.manifest.material)}`;
let result = volumeRenderCache.get(cacheKey);
if (!result) {
result = await renderNanoVDBViewportAsset(asset, 128, 128, volumeRenderSession);
result = await renderNanoVDBViewportAsset(asset, NANOVDB_VIEWPORT_PREVIEW_SIZE, NANOVDB_VIEWPORT_PREVIEW_SIZE, volumeRenderSession);
volumeRenderCache.set(cacheKey, result);
}
if (generation !== volumeRenderGeneration || currentSnapshot !== snapshot || !root) return;
@@ -546,6 +548,7 @@ workerScope.onmessage = (event): void => {
else if (message.type === "textureAssets") applyTextureAssets(message.assets);
else if (message.type === "volumeAssets") {
volumeAssets = message.assets;
volumeAssetsInitialized = true;
void refreshVolumes();
}
else if (message.type === "resize") resize(message.width, message.height, message.pixelRatio);