Advance WebGPU volume and bounded workflows

This commit is contained in:
mes123456
2026-08-14 18:08:29 -04:00
parent 3da1dfc804
commit 68d50f810f
119 changed files with 9028 additions and 430 deletions

View File

@@ -1,21 +1,31 @@
import { ASSET_LIBRARY_BUDGET, assetStorageCapabilities, gateIORequest, gateLibraryMutation, libraryLoadOrder, parseAssetLibraryManifest, parseIORequest, verifyAssetSource } from "../../../protocol/asset-library-io";
import { ASSET_LIBRARY_BUDGET, assetStorageCapabilities, gateIORequest, gateLibraryMutation, libraryLoadOrder, parseAssetLibraryManifest, parseIORequest, planIOArchiveRanges, verifyAssetPreview, verifyAssetSource } from "../../../protocol/asset-library-io";
const shaA = "a".repeat(64); const shaB = "b".repeat(64);
const asset = { id: "asset:1", name: "Cube", kind: "OBJECT", catalogId: "catalog:models", tags: ["model"], author: "Web", license: "CC0-1.0", sourceSha256: shaA, sourcePath: "assets/cube.blend" };
const base = { schemaVersion: 1, revision: 2, catalogs: [{ id: "catalog:root", name: "Root", parentId: null }, { id: "catalog:models", name: "Models", parentId: "catalog:root" }], assets: [asset], libraries: [{ id: "library:a", name: "A", sourcePath: "libraries/a.blend", sourceSha256: shaA, dependencyIds: ["library:b"], readOnly: true }, { id: "library:b", name: "B", sourcePath: "libraries/b.blend", sourceSha256: shaB, dependencyIds: [], readOnly: true }] };
const glbRequest = { format: "GLB", operation: "EXPORT", externalUris: [], archiveEntries: [] };
self.onmessage = () => {
self.onmessage = async () => {
const result: Record<string, unknown> = {};
try { const manifest = parseAssetLibraryManifest(base); verifyAssetSource(manifest.assets[0], shaA); result.valid = [manifest.assets[0].license, libraryLoadOrder(manifest)]; } catch (error) { result.valid = error instanceof Error ? error.message : String(error); }
try { verifyAssetSource(parseAssetLibraryManifest(base).assets[0], shaB); } catch (error) { result.hash = error instanceof Error ? error.message : String(error); }
try { parseAssetLibraryManifest({ ...base, assets: [{ ...asset, license: "" }] }); } catch (error) { result.license = error instanceof Error ? error.message : String(error); }
try { parseAssetLibraryManifest({ ...base, libraries: [{ ...base.libraries[0], dependencyIds: ["library:a"] }, base.libraries[1]] }); } catch (error) { result.cycle = error instanceof Error ? error.message : String(error); }
try { parseIORequest({ ...glbRequest, archiveEntries: [{ path: "safe.bin", compressedBytes: 1, uncompressedBytes: ASSET_LIBRARY_BUDGET.maxCompressionRatio + 1 }] }); } catch (error) { result.archive = error instanceof Error ? error.message : String(error); }
try { parseIORequest({ ...glbRequest, archiveEntries: [{ path: "mesh", compressedBytes: 1, uncompressedBytes: 1 }, { path: "mesh/data.bin", compressedBytes: 1, uncompressedBytes: 1 }] }); } catch (error) { result.archivePath = error instanceof Error ? error.message : String(error); }
try { parseIORequest({ ...glbRequest, byteLength: 1, archiveEntries: [{ path: "a.bin", compressedBytes: 2, uncompressedBytes: 2 }] }); } catch (error) { result.archiveLength = error instanceof Error ? error.message : String(error); }
result.archivePlan = planIOArchiveRanges({ ...glbRequest, byteLength: 10, archiveEntries: [{ path: "z.bin", compressedBytes: 3, uncompressedBytes: 4 }, { path: "a.bin", compressedBytes: 2, uncompressedBytes: 2 }] });
try { parseIORequest({ ...glbRequest, externalUris: ["https://example.com/file.bin"] }); } catch (error) { result.uri = error instanceof Error ? error.message : String(error); }
result.glb = gateIORequest(glbRequest).status;
result.obj = gateIORequest({ ...glbRequest, format: "OBJ", operation: "IMPORT" }).issues[0]?.code;
result.library = gateLibraryMutation("APPEND").status;
result.storage = assetStorageCapabilities();
try {
const png = new ArrayBuffer(24); const bytes = new Uint8Array(png); bytes.set([137, 80, 78, 71, 13, 10, 26, 10]); const view = new DataView(png); view.setUint32(16, 2, false); view.setUint32(20, 3, false);
const digest = Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", png)), (value) => value.toString(16).padStart(2, "0")).join("");
await verifyAssetPreview({ assetId: "preview:1", sha256: digest, mimeType: "image/png", width: 2, height: 3, byteLength: 24 }, png);
result.preview = digest;
try { await verifyAssetPreview({ assetId: "preview:1", sha256: digest, mimeType: "image/png", width: 3, height: 2, byteLength: 24 }, png); } catch (error) { result.previewSize = error instanceof Error ? error.message : String(error); }
} catch (error) { result.preview = error instanceof Error ? error.message : String(error); }
self.postMessage(result);
};

View File

@@ -1,4 +1,4 @@
import { executeCompositorGraph, gateCompositorGraph, parseCompositorGraph } from "../../../protocol/compositor";
import { CompositorFrameCache, executeCompositorGraph, executeCompositorGraphCached, gateCompositorGraph, parseCompositorGraph } from "../../../protocol/compositor";
const node = (id: string, type: string, properties: Record<string, unknown> = {}) => ({ id, type, name: id, properties });
const valid = {
@@ -26,7 +26,7 @@ const valid = {
],
};
self.onmessage = () => {
self.onmessage = async () => {
const result: Record<string, unknown> = {};
try {
const parsed = parseCompositorGraph(valid);
@@ -54,5 +54,13 @@ self.onmessage = () => {
catch (error) { result.budget = error instanceof Error ? error.message : String(error); }
try { executeCompositorGraph(valid, new Map(), { width: 2, height: 2, cancelled: () => true }); }
catch (error) { result.cancelled = error instanceof Error ? error.message : String(error); }
try {
const cache = new CompositorFrameCache(512);
const first = await executeCompositorGraphCached(valid, new Map(), cache, { frame: 1, width: 2, height: 2 });
first.composite.data[0] = 99;
const second = await executeCompositorGraphCached(valid, new Map(), cache, { frame: 1, width: 2, height: 2 });
const third = await executeCompositorGraphCached(valid, new Map(), cache, { frame: 2, width: 2, height: 2 });
result.cache = [first.cacheHit, second.cacheHit, third.cacheHit, first.cacheKey === second.cacheKey, first.cacheKey !== third.cacheKey, second.composite.data[0], cache.size, cache.byteLength];
} catch (error) { result.cache = error instanceof Error ? error.message : String(error); }
self.postMessage(result);
};

View File

@@ -1,4 +1,4 @@
import { applyEditorWorkflowEdit, EDITOR_WORKFLOW_BUDGET, gateEditorOperation, parseEditorWorkflow } from "../../../protocol/editor-workflow";
import { applyEditorWorkflowEdit, EDITOR_WORKFLOW_BUDGET, gateEditorOperation, keyChordFromKeyboardEvent, parseEditorWorkflow, resolveKeymapCommand } from "../../../protocol/editor-workflow";
const regions = [{ id: "header", kind: "HEADER", visible: true }, { id: "main", kind: "MAIN", visible: true }];
const area = (id: string, editor: string, x: number, y: number, width: number, height: number) => ({ id, editor, regions, rect: { x, y, width, height }, maximized: false });
@@ -6,10 +6,21 @@ const base = { schemaVersion: 1, workspaces: [{ id: "Layout", name: "Layout", ac
self.onmessage = () => {
const result: Record<string, unknown> = {};
try { const parsed = parseEditorWorkflow(base); const switched = applyEditorWorkflowEdit(parsed, { type: "SWITCH_WORKSPACE", revision: 0, workspaceId: "Animation" }); const selected = applyEditorWorkflowEdit(switched, { type: "SET_SELECTION", revision: 1, selectedIds: ["object:2"], activeObjectId: "object:2" }); result.edit = [selected.context.revision, selected.context.activeEditor, selected.context.selection]; } catch (error) { result.edit = error instanceof Error ? error.message : String(error); }
try { const parsed = parseEditorWorkflow(base); const switched = applyEditorWorkflowEdit(parsed, { type: "SWITCH_WORKSPACE", revision: 0, workspaceId: "Animation" }); const selected = applyEditorWorkflowEdit(switched, { type: "SET_SELECTION", revision: 1, selectedIds: ["object:2"], activeObjectId: "object:2" }); result.edit = [selected.context.revision, selected.context.activeEditor, selected.context.selection]; result.keymap = resolveKeymapCommand(parsed, keyChordFromKeyboardEvent({ key: "x", altKey: false, ctrlKey: false, metaKey: false, shiftKey: false })); } catch (error) { result.edit = error instanceof Error ? error.message : String(error); }
try { applyEditorWorkflowEdit(base, { type: "SET_ACTIVE_AREA", revision: 8, areaId: "viewport" }); } catch (error) { result.revision = error instanceof Error ? error.message : String(error); }
try { parseEditorWorkflow({ ...base, workspaces: [{ ...base.workspaces[0], areas: [area("a", "VIEW_3D", 0, 0, 0.6, 1), area("b", "OUTLINER", 0.5, 0, 0.5, 1)] }] }); } catch (error) { result.overlap = error instanceof Error ? error.message : String(error); }
try { parseEditorWorkflow({ ...base, context: { ...base.context, selection: new Array(EDITOR_WORKFLOW_BUDGET.maxSelection + 1).fill("x") } }); } catch (error) { result.budget = error instanceof Error ? error.message : String(error); }
try { parseEditorWorkflow({ ...base, keymaps: [...base.keymaps, { id: "key:delete-duplicate", key: "x", modifiers: [], command: "object.delete.other", enabled: true }] }); } catch (error) { result.keymapConflict = error instanceof Error ? error.message : String(error); }
try {
const scoped = parseEditorWorkflow({ ...base, keymaps: [
{ id: "key:view", key: "Q", modifiers: [], command: "view.command", enabled: true, editors: ["VIEW_3D"] },
{ id: "key:timeline", key: "Q", modifiers: [], command: "timeline.command", enabled: true, editors: ["TIMELINE"] },
] });
const chord = { key: "q", altKey: false, ctrlKey: false, metaKey: false, shiftKey: false };
const viewCommand = resolveKeymapCommand(scoped, keyChordFromKeyboardEvent(chord));
const timeline = applyEditorWorkflowEdit(scoped, { type: "SWITCH_WORKSPACE", revision: 0, workspaceId: "Animation" });
result.scopedKeymap = [viewCommand, resolveKeymapCommand(timeline, keyChordFromKeyboardEvent(chord))];
} catch (error) { result.scopedKeymap = error instanceof Error ? error.message : String(error); }
result.view = gateEditorOperation("READ_ONLY_VIEW").status; result.writer = gateEditorOperation("WRITER").issues[0]?.code; result.gizmo = gateEditorOperation("GIZMO").issues[0]?.code;
self.postMessage(result);
};

View File

@@ -0,0 +1,71 @@
import {
Line,
PerspectiveCamera,
Points,
Raycaster,
Vector2,
Vector3,
} from "../vendor/three/three.module.js";
import { applyGreasePencilPointPreview, applyGreasePencilPointSelection, createGreasePencilObject, greasePencilPointRef } from "../three-adapter/grease-pencil";
import type { GreasePencilDataIR } from "../../../protocol/grease-pencil";
self.onmessage = () => {
const result: Record<string, unknown> = {};
const data: GreasePencilDataIR = {
id: "grease-pencil:Viewport",
name: "Viewport",
geometryStatus: "available",
layerCount: 1,
frameCount: 1,
strokeCount: 1,
pointCount: 3,
layers: [{ id: "grease-pencil-layer:Viewport", name: "Layer", visible: true, locked: false, opacity: 1, frames: [{ frame: 1, drawing: {
id: "grease-pencil-drawing:Viewport",
strokeCount: 1,
pointCount: 3,
strokes: [{ id: "grease-pencil-stroke:Viewport", cyclic: false, pointCount: 3, points: [
{ position: [-1, 0, 0], radius: 0.1, opacity: 1 },
{ position: [0, 0, 0], radius: 0.1, opacity: 1 },
{ position: [1, 0, 0], radius: 0.1, opacity: 1 },
] }],
} }] }],
};
const object = createGreasePencilObject(data, 1);
if (!object) throw new Error("Grease Pencil viewport object was not created");
object.updateMatrixWorld(true);
let points: Points | undefined;
let line: Line | undefined;
object.traverse((child) => {
if (!points && child instanceof Points && typeof child.userData.greasePencilPointDataId === "string") points = child;
if (!line && child instanceof Line && typeof child.userData.greasePencilPreviewDataId === "string") line = child;
});
if (!points || !line) throw new Error("Grease Pencil viewport proxies were not created");
points.visible = true;
const camera = new PerspectiveCamera(45, 1, 0.01, 100);
camera.position.set(0, 0, 5);
camera.lookAt(0, 0, 0);
camera.updateMatrixWorld(true);
camera.updateProjectionMatrix();
const ndc = new Vector3(0, 0, 0).project(camera);
const raycaster = new Raycaster();
raycaster.params.Points.threshold = 0.14;
raycaster.setFromCamera(new Vector2(ndc.x, ndc.y), camera);
const hit = raycaster.intersectObject(points, true)[0];
if (!hit || hit.index === undefined) throw new Error("Grease Pencil point raycast did not hit");
const ref = greasePencilPointRef(hit.object, hit.index);
result.hit = ref;
if (!ref) throw new Error("Grease Pencil point reference was not stable");
applyGreasePencilPointSelection(object, [ref]);
const colors = points.geometry.getAttribute("color");
result.selectedColor = colors ? [colors.getX(hit.index), colors.getY(hit.index), colors.getZ(hit.index)] : null;
applyGreasePencilPointPreview(object, data.id, data.layers[0].id, 1, [{ ...ref, position: [0.5, 1, 2] }]);
const previewLine = line.geometry.getAttribute("position");
const previewPoint = points.geometry.getAttribute("position");
result.preview = [[previewLine.getX(1), previewLine.getY(1), previewLine.getZ(1)], [previewPoint.getX(1), previewPoint.getY(1), previewPoint.getZ(1)]];
applyGreasePencilPointPreview(object, data.id, data.layers[0].id, 1, null);
result.restored = [[previewLine.getX(1), previewLine.getY(1), previewLine.getZ(1)], [previewPoint.getX(1), previewPoint.getY(1), previewPoint.getZ(1)]];
let proxyCount = 0;
object.traverse((child) => { if (typeof child.userData.greasePencilPointDataId === "string") proxyCount++; });
result.proxyCount = proxyCount;
self.postMessage(result);
};

View File

@@ -1,11 +1,34 @@
import { applyCurveGizmoDelta } from "../../../protocol/nonmesh-interaction";
import { applyCurveGizmoDelta, curveGizmoAxisDelta, deriveCurveHandleGizmoFrame } from "../../../protocol/nonmesh-interaction";
import { applyCurveHandlePreview, createNonMeshObject } from "../three-adapter/nonmesh";
import { LineSegments, Points } from "../vendor/three/three.module.js";
self.onmessage = () => {
const result: Record<string, unknown> = {};
const base = { schemaVersion: 1, dataId: "curve:1", baseRevision: 3, phase: "COMMIT", axis: 0, delta: [0.25, 0, 0], handles: [{ pointIndex: 2, side: "LEFT", position: [1, 2, 3] }] };
try { const applied = applyCurveGizmoDelta({ ...base, phase: "PREVIEW" }, 3); result.preview = [applied.revision, applied.handles[0].position]; } catch (error) { result.preview = error instanceof Error ? error.message : String(error); }
try { const applied = applyCurveGizmoDelta(base, 3); result.commit = [applied.revision, applied.handles[0].position]; } catch (error) { result.commit = error instanceof Error ? error.message : String(error); }
try { applyCurveGizmoDelta(base, 4); } catch (error) { result.stale = error instanceof Error ? error.message : String(error); }
try { applyCurveGizmoDelta({ ...base, handles: [{ ...base.handles[0] }, { ...base.handles[0] }] }, 3); } catch (error) { result.duplicate = error instanceof Error ? error.message : String(error); }
try { applyCurveGizmoDelta({ ...base, delta: [0.25, 0.25, 0] }, 3); } catch (error) { result.axis = error instanceof Error ? error.message : String(error); }
try {
const frame = deriveCurveHandleGizmoFrame([0, 0, 0, 1, 0, 0], [{ pointIndex: 0, side: "LEFT", position: [0, 1, 0] }]);
const delta = curveGizmoAxisDelta(frame, 0, 0.25);
const local = applyCurveGizmoDelta({ ...base, axisVector: frame.axes[0], delta, handles: [{ pointIndex: 0, side: "LEFT", position: [0, 1, 0] }] }, 3);
result.localFrame = [frame.origin, frame.axes, local.handles[0].position];
} catch (error) { result.localFrame = error instanceof Error ? error.message : String(error); }
try {
const object = createNonMeshObject({ id: "curve:1", name: "Curve", type: "CURVE", geometryStatus: "available", pointCount: 2, splineCount: 1, controlPoints: [0, 0, 0, 1, 0, 0], splineOffsets: [0, 2], handlePointIndices: [0, 1], handlePoints: [-0.5, 0, 0, 0.5, 0, 0, 0.5, 0, 0, 1.5, 0, 0] });
if (!object) throw new Error("Curve preview object was not created");
let points: Points | undefined; let lines: LineSegments | undefined;
object.traverse((child) => { if (child instanceof Points && child.userData.nonMeshHandleBasePositions) points = child; if (child instanceof LineSegments && child.userData.nonMeshHandleBasePositions) lines = child; });
if (!points || !lines) throw new Error("Curve handle proxies were not created");
const preview = [{ pointIndex: 0, side: "LEFT" as const, position: [-0.25, 0, 0] as [number, number, number] }];
applyCurveHandlePreview(object, "curve:1", preview);
const first = [points.geometry.getAttribute("position").getX(0), lines.geometry.getAttribute("position").getX(1)];
applyCurveHandlePreview(object, "curve:1", preview);
const repeated = points.geometry.getAttribute("position").getX(0);
applyCurveHandlePreview(object, "curve:1", null);
result.rendererPreview = [first, repeated, points.geometry.getAttribute("position").getX(0)];
} catch (error) { result.rendererPreview = error instanceof Error ? error.message : String(error); }
self.postMessage(result);
};

View File

@@ -1,4 +1,4 @@
import { PAINT_BUDGET, applyUdimTilePatch, computePaintBrushWeights, parsePaintStroke, parseUdimTilePatch, parseWeightPatch } from "../../../protocol/paint";
import { PAINT_BUDGET, applyUdimTilePatch, buildPaintBrushSpatialIndex, composePaintColorPatch, composePaintWeightPatch, computePaintBrushWeights, parsePaintStroke, parseUdimTilePatch, parseWeightPatch, queryPaintBrushSpatialIndex } from "../../../protocol/paint";
const base = {
schemaVersion: 1,
@@ -25,6 +25,27 @@ self.onmessage = async () => {
{ index: 4, position: [3, 0, 0], normal: [0, 0, 1] },
], [0, 0, 0], 2, 0.8, { frontFaceOnly: true, viewDirection: [0, 0, -1] });
} catch (error) { result.brush = error instanceof Error ? error.message : String(error); }
try {
const index = buildPaintBrushSpatialIndex(Array.from({ length: 1_000 }, (_, vertex) => ({ index: vertex, position: [vertex % 100, Math.floor(vertex / 100), 0], normal: [0, 0, 1] })), 1);
const query = queryPaintBrushSpatialIndex(index, [50, 5, 0], 1.1, 1, { frontFaceOnly: true, viewDirection: [0, 0, -1], visibleVertexIndices: [449, 450, 451, 549, 550, 551, 649, 650, 651], requireVisibility: true });
result.spatialBrush = [query.candidateCount, query.visitedCellCount, query.weights.map((entry) => entry.index)];
result.selectedMasked = queryPaintBrushSpatialIndex(index, [50, 5, 0], 1.1, 1, { selectedVertexIndices: [450, 550, 650], requireSelection: true, maskWeights: [{ index: 450, weight: 0.25 }, { index: 550, weight: 0.5 }, { index: 650, weight: 0 }] }).weights;
try { queryPaintBrushSpatialIndex(index, [50, 5, 0], 1, 1, { requireVisibility: true }); } catch (error) { result.spatialVisibility = error instanceof Error ? error.message : String(error); }
try { queryPaintBrushSpatialIndex(index, [50, 5, 0], 1, 1, { requireSelection: true }); } catch (error) { result.spatialSelection = error instanceof Error ? error.message : String(error); }
try { queryPaintBrushSpatialIndex(index, [50, 5, 0], 1, 1, { selectedVertexIndices: [450, 450] }); } catch (error) { result.selectionDuplicate = error instanceof Error ? error.message : String(error); }
try { queryPaintBrushSpatialIndex(index, [50, 5, 0], 1, 1, { maskWeights: [{ index: 450, weight: 1.1 }] }); } catch (error) { result.maskInvalid = error instanceof Error ? error.message : String(error); }
try { queryPaintBrushSpatialIndex(index, [50, 5, 0], 1, 1, { selectedVertexIndices: [1001] }); } catch (error) { result.selectionUnknown = error instanceof Error ? error.message : String(error); }
try { queryPaintBrushSpatialIndex({ schemaVersion: 1, cellSize: 1, vertices: [], cells: new Map() }, [0, 0, 0], 1, 1); } catch (error) { result.spatialForgery = error instanceof Error ? error.message : String(error); }
(index.cells as Map<string, readonly number[]>).clear();
result.spatialMutation = queryPaintBrushSpatialIndex(index, [50, 5, 0], 1.1, 1, { visibleVertexIndices: [450, 550], requireVisibility: true }).candidateCount;
} catch (error) { result.spatialBrush = error instanceof Error ? error.message : String(error); }
try {
const brush = [{ index: 2, weight: 0.25 }, { index: 0, weight: 1 }];
result.weightPatch = composePaintWeightPatch("object:Paint", "Group", 4, 4, [0.2, 0.4, 0.6], brush, 1);
result.colorPatch = composePaintColorPatch(4, 4, [0, 0, 0, 1, 0.5, 0.5, 0.5, 1, 1, 0, 0, 1], brush, [0, 1, 0, 0.5]);
try { composePaintWeightPatch("object:Paint", "Group", 3, 4, [0], [{ index: 0, weight: 1 }], 1); } catch (error) { result.patchRevision = error instanceof Error ? error.message : String(error); }
try { composePaintColorPatch(4, 4, [0, 0, 0, 1], [{ index: 4, weight: 1 }], [1, 1, 1, 1]); } catch (error) { result.patchIdentity = error instanceof Error ? error.message : String(error); }
} catch (error) { result.weightPatch = error instanceof Error ? error.message : String(error); }
try {
const baseTile = new Uint8Array(16);
const hash = async (bytes: Uint8Array) => Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", Uint8Array.from(bytes).buffer)), (value) => value.toString(16).padStart(2, "0")).join("");

View File

@@ -7,6 +7,7 @@ import {
physicsCapabilityInventory,
selectPhysicsCacheFrame,
} from "../../../protocol/physics-simulation";
import { applyBrowserTransformCachePreview, BrowserTransformCachePlaybackSession } from "../../../protocol/physics-cache-playback";
const hash = "a".repeat(64);
const base = {
@@ -30,7 +31,19 @@ const base = {
},
};
self.onmessage = () => {
function browserFrame(frame: number, translation: [number, number, number]): ArrayBuffer {
const bytes = new ArrayBuffer(16 + 72);
const view = new DataView(bytes);
view.setUint32(0, 0x31465442, true); view.setUint16(4, 1, true); view.setUint16(6, 16, true); view.setInt32(8, frame, true); view.setUint32(12, 1, true);
const id = new TextEncoder().encode("object:Cloth"); view.setUint8(16, id.length); new Uint8Array(bytes, 17, id.length).set(id);
[...translation, 0, 0, 0, 1, 1, 1, 1].forEach((value, index) => view.setFloat32(48 + index * 4, value, true));
return bytes;
}
const identity = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
const scene = { schemaVersion: 1 as const, revision: 3, sceneId: "scene:Physics", source: { kind: "mock" as const }, coordinateSystem: { upAxis: "Z" as const, forwardAxis: "-Y" as const, handedness: "RIGHT" as const, unitSystem: 0, unitScale: 1 }, activeObjectId: "object:Cloth", frame: { current: 1, start: 1, end: 10 }, nodes: [{ id: "object:Cloth", name: "Cloth", type: "MESH" as const, parentId: null, dataId: "mesh:Cloth", visible: true, selectable: true, localMatrix: identity, worldMatrix: identity, transform: { translation: [0, 0, 0] as [number, number, number], rotationEuler: [0, 0, 0] as [number, number, number], scale: [1, 1, 1] as [number, number, number], rotationMode: 1 } }], meshes: [], materials: [], cameras: [], lights: [], worlds: [], images: [], animations: [], collections: [], scenes: [] };
self.onmessage = async () => {
const result: Record<string, unknown> = {};
try {
const parsed = parsePhysicsSimulationManifest({ schemaVersion: 1, systems: [base] });
@@ -63,16 +76,40 @@ self.onmessage = () => {
result.manifest = gatePhysicsExecution("RIGID_BODY", "CACHE_MANIFEST").status;
result.familyCount = PHYSICS_FAMILIES.length;
try {
const bytes = new ArrayBuffer(16 + 72);
const bytes = browserFrame(7, [1, 2, 3]);
const view = new DataView(bytes);
view.setUint32(0, 0x31465442, true); view.setUint16(4, 1, true); view.setUint16(6, 16, true); view.setInt32(8, 7, true); view.setUint32(12, 1, true);
const id = new TextEncoder().encode("object:Cloth"); view.setUint8(16, id.length); new Uint8Array(bytes, 17, id.length).set(id);
[1, 2, 3, 0, 0, 0, 1, 1, 1, 1].forEach((value, index) => view.setFloat32(48 + index * 4, value, true));
const decoded = decodeBrowserTransformCacheFrame(bytes);
result.browserPlayback = [decoded.frame, decoded.objects[0].objectId, decoded.objects[0].translation];
const preview = applyBrowserTransformCachePreview(scene, bytes.slice(0), 7);
result.browserPreview = [preview.frame.current, preview.nodes[0].transform.translation, preview.nodes[0].worldMatrix.slice(12, 15)];
try { applyBrowserTransformCachePreview(preview, bytes.slice(0), 8); } catch (error) { result.browserFrameMismatch = error instanceof Error ? error.message : String(error); }
view.setFloat32(48 + 3 * 4, 2, true);
try { decodeBrowserTransformCacheFrame(bytes); } catch (error) { result.browserRotation = error instanceof Error ? error.message : String(error); }
}
catch (error) { result.browserPlayback = error instanceof Error ? error.message : String(error); }
try {
const frames = new Map([[7, browserFrame(7, [1, 0, 0])], [8, browserFrame(8, [2, 0, 0])]]);
const published: number[] = [];
const session = new BrowserTransformCachePlaybackSession(scene, { frameStart: 7, frameEnd: 8, readFrame: async (frame) => frames.get(frame)!.slice(0) }, (preview) => published.push(preview.frame.current));
const playback = await session.play();
result.browserSession = [playback.status, playback.appliedFrames, playback.lastFrame, published];
let releaseSlow: ((data: ArrayBuffer) => void) | undefined;
const latePublished: number[] = [];
const late = new BrowserTransformCachePlaybackSession(scene, { frameStart: 7, frameEnd: 8, readFrame: (frame) => frame === 7 ? new Promise<ArrayBuffer>((resolve) => { releaseSlow = resolve; }) : Promise.resolve(frames.get(frame)!.slice(0)) }, (preview) => latePublished.push(preview.frame.current));
const superseded = late.seek(7);
const current = late.seek(8);
releaseSlow?.(frames.get(7)!.slice(0));
result.browserSupersede = [await superseded === null, (await current)?.frame.current, latePublished];
let releaseCancelled: ((data: ArrayBuffer) => void) | undefined;
const cancelledPublished: number[] = [];
const cancelled = new BrowserTransformCachePlaybackSession(scene, { frameStart: 7, frameEnd: 7, readFrame: () => new Promise<ArrayBuffer>((resolve) => { releaseCancelled = resolve; }) }, (preview) => cancelledPublished.push(preview.frame.current));
const cancelledRead = cancelled.seek(7);
cancelled.cancel();
releaseCancelled?.(frames.get(7)!.slice(0));
result.browserCancel = [await cancelledRead === null, cancelledPublished];
}
catch (error) { result.browserSession = error instanceof Error ? error.message : String(error); }
self.postMessage(result);
};

View File

@@ -1,9 +1,9 @@
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"], acceptance: [], dependencies });
const family = (id: string, dependencies: string[] = []) => ({ id, name: id, status: "BLOCKED", roadmapStatus: "planned", completedSlices: ["schema"], blockedSlices: ["A", "B"], excludedSlices: [], 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:00Z", families: [family("N-015"), family("N-016", ["N-015"])], evidence };
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 };
self.onmessage = () => {
const result: Record<string, unknown> = {};
@@ -13,5 +13,9 @@ self.onmessage = () => {
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, 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, 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); }
self.postMessage(result);
};

View File

@@ -0,0 +1,34 @@
import { applySceneDelta, diffSceneSnapshots, parseSceneDelta, sceneDeltaRequiresRendererRebuild } from "../../../protocol/scene-delta";
import type { SceneSnapshotIR } from "../../../protocol/scene-ir";
self.onmessage = () => {
const identity = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
const before: SceneSnapshotIR = {
schemaVersion: 1,
revision: 3,
sceneId: "scene:Render",
source: { kind: "mock" },
coordinateSystem: { upAxis: "Z", forwardAxis: "-Y", handedness: "RIGHT", unitSystem: 0, unitScale: 1 },
nodes: [{ id: "object:Camera", name: "Camera", type: "CAMERA", parentId: null, dataId: "camera:Main", visible: true, selectable: true, localMatrix: identity, worldMatrix: identity, transform: { translation: [0, 0, 0], rotationEuler: [0, 0, 0], scale: [1, 1, 1], rotationMode: 1 } }],
meshes: [], materials: [], cameras: [], lights: [], images: [], animations: [], collections: [],
worlds: [{ id: "world:Render", name: "World", color: [0.1, 0.2, 0.3], exposure: 0 }],
scenes: [{ id: "scene:Render", name: "Render", worldId: "world:Render", colorManagement: { displayDevice: "sRGB", viewTransform: "AgX", look: "None", exposure: 0, gamma: 1 } }],
activeObjectId: "object:Camera",
frame: { current: 1, start: 1, end: 10 },
};
const after: SceneSnapshotIR = {
...before,
revision: 4,
worlds: [{ ...before.worlds[0], color: [0.8, 0.4, 0.2], exposure: 2 }],
scenes: [{ ...before.scenes[0], colorManagement: { ...before.scenes[0].colorManagement!, viewTransform: "Standard", exposure: 1 } }],
};
const delta = parseSceneDelta(diffSceneSnapshots(before, after));
const applied = applySceneDelta(before, delta);
const result: Record<string, unknown> = {
collections: [delta.worlds?.updated.length, delta.scenes?.updated.length],
applied: [applied.worlds[0].color, applied.worlds[0].exposure, applied.scenes[0].colorManagement?.viewTransform, applied.scenes[0].colorManagement?.exposure],
rebuild: sceneDeltaRequiresRendererRebuild(delta),
};
try { parseSceneDelta({ ...delta, worlds: { updated: "not-an-array" } }); } catch (error) { result.invalid = error instanceof Error ? error.message : String(error); }
self.postMessage(result);
};

View File

@@ -1,13 +1,23 @@
import { gateScriptExecution, gateServerScriptJob, parseScriptingManifest, platformCapabilities, SCRIPTING_BUDGET } from "../../../protocol/scripting-platform";
import { appendScriptExecutionAudit, createScriptExecutionAudit, gateScriptExecution, gateServerScriptJob, parseScriptExecutionAuditLog, parseScriptingManifest, platformCapabilities, SCRIPTING_BUDGET } from "../../../protocol/scripting-platform";
const sha = "a".repeat(64); const signature = "b".repeat(128);
const script = { id: "script:clean", name: "Clean", entryPath: "scripts/clean.py", sourceSha256: sha, publisher: "Team", signature, keyId: "key:trusted", permissions: ["READ_MAIN"], dependencies: [], cpuMs: 1000, memoryBytes: 1024 * 1024, wallMs: 5000, network: false, autorun: false, driverExpressions: false, addonInstall: false };
const base = { schemaVersion: 1, scripts: [script] };
self.onmessage = () => {
self.onmessage = async () => {
const result: Record<string, unknown> = {};
try { result.valid = parseScriptingManifest(base).scripts[0].entryPath; } catch (error) { result.valid = error instanceof Error ? error.message : String(error); }
result.exec = gateScriptExecution(base, "script:clean", new Set(["key:trusted"])).issues[0]?.code;
const audit = await createScriptExecutionAudit(base, "script:clean", new Set(["key:trusted"]), { requestId: "e2e-script-audit", requestedAt: "2026-08-12T12:00:00.000Z" });
result.audit = [audit.decision, audit.reason, audit.approvedKey, audit.permissions, audit.budget.cpuMs, audit.manifestSha256, audit.requestSha256];
const secondAudit = await createScriptExecutionAudit(base, "script:clean", new Set(), { requestId: "e2e-script-audit-2", requestedAt: "2026-08-12T12:00:01.000Z" });
const firstLog = await appendScriptExecutionAudit({ schemaVersion: 1, entries: [] }, audit);
const auditLog = await appendScriptExecutionAudit(firstLog, secondAudit);
const checkedLog = await parseScriptExecutionAuditLog(auditLog);
result.auditLog = [checkedLog.entries.length, checkedLog.entries[0].previousEntrySha256, checkedLog.entries[1].previousEntrySha256, checkedLog.entries[1].entrySha256];
try { await appendScriptExecutionAudit(auditLog, secondAudit); } catch (error) { result.auditReplay = error instanceof Error ? error.message : String(error); }
try { await parseScriptExecutionAuditLog({ ...auditLog, entries: auditLog.entries.map((entry, index) => index === 0 ? { ...entry, audit: { ...entry.audit, scriptId: "script:tampered" } } : entry) }); } catch (error) { result.auditTamper = error instanceof Error ? error.message : String(error); }
try { await createScriptExecutionAudit(base, "script:clean", new Set(["key:trusted"]), { requestId: "bad-date", requestedAt: "2026-02-31T12:00:00.000Z" }); } catch (error) { result.auditDate = error instanceof Error ? error.message : String(error); }
result.server = gateServerScriptJob({ scriptId: "script:clean", sourceSha256: sha, inputBlendSha256: "c".repeat(64), status: "QUEUED" }, base, "c".repeat(64)).issues[0]?.code;
try { parseScriptingManifest({ ...base, scripts: [{ ...script, entryPath: "../escape.py" }] }); } catch (error) { result.path = error instanceof Error ? error.message : String(error); }
try { parseScriptingManifest({ ...base, scripts: [{ ...script, autorun: true }] }); } catch (error) { result.policy = error instanceof Error ? error.message : String(error); }

View File

@@ -3,6 +3,8 @@ import {
applySequencerEdit,
gateSequencerCodec,
parseSequencerTimeline,
resolveSequencerFrame,
resolveSequencerTransitionFrame,
sequencerRuntimeCapabilities,
sequencerSourceFrame,
} from "../../../protocol/sequencer";
@@ -23,6 +25,7 @@ self.onmessage = () => {
const moved = applySequencerEdit(timeline, { type: "MOVE", revision: 3, stripId: movie.id, frameDelta: 5, channel: 3 });
const split = applySequencerEdit(moved, { type: "SPLIT", revision: 4, stripId: movie.id, frame: 20, rightStripId: "strip:MovieRight" });
result.edit = [split.revision, split.strips.map((strip) => [strip.id, strip.frameStart, strip.frameEnd, strip.sourceStart, strip.sourceEnd])];
result.frame = resolveSequencerFrame(split, 20).map((strip) => [strip.stripId, strip.channel, strip.sourceFrame]);
try { applySequencerEdit(timeline, { type: "MOVE", revision: 2, stripId: movie.id, frameDelta: 1 }); }
catch (error) { result.revision = error instanceof Error ? error.message : String(error); }
}
@@ -40,6 +43,15 @@ self.onmessage = () => {
}
catch (error) { result.budget = error instanceof Error ? error.message : String(error); }
result.codec = gateSequencerCodec("video/mp4", new Set()).issues[0]?.code;
const transitionTimeline = { ...base, strips: [
{ id: "strip:From", name: "From", type: "SCENE", channel: 1, frameStart: 10, frameEnd: 20, sourceStart: 100, sourceEnd: 110, speed: 1, muted: false, locked: false },
{ id: "strip:To", name: "To", type: "SCENE", channel: 2, frameStart: 10, frameEnd: 20, sourceStart: 200, sourceEnd: 210, speed: 1, muted: false, locked: false },
{ id: "strip:Cross", name: "Cross", type: "EFFECT", effectType: "CROSS", inputStripIds: ["strip:From", "strip:To"], channel: 3, frameStart: 10, frameEnd: 20, sourceStart: 0, sourceEnd: 10, speed: 1, muted: false, locked: false },
] };
const transition = resolveSequencerTransitionFrame(transitionTimeline, "strip:Cross", 15);
result.transition = [transition.effectType, transition.factor, transition.from.stripId, transition.from.sourceFrame, transition.to.stripId, transition.to.sourceFrame];
try { resolveSequencerTransitionFrame(transitionTimeline, "strip:Cross", 20); }
catch (error) { result.transitionBoundary = error instanceof Error ? error.message : String(error); }
result.runtime = sequencerRuntimeCapabilities();
self.postMessage(result);
};

View File

@@ -3,6 +3,8 @@ import {
applyTrackingMaskEdit,
gateTrackingOperation,
parseTrackingMaskProject,
raycastMaskProject,
selectMaskPointsInBounds,
type MaskPointIR,
type TrackingMarkerIR,
} from "../../../protocol/tracking-mask";
@@ -18,6 +20,10 @@ self.onmessage = () => {
const markerEdit = applyTrackingMaskEdit(parsed, { type: "SET_MARKER", revision: 4, clipId: "clip:1", trackId: "track:1", marker: { ...marker, frame: 10, position: [0.6, 0.4], keyframe: false } });
const pointEdit = applyTrackingMaskEdit(markerEdit, { type: "SET_MASK_POINT", revision: 5, maskId: "mask:1", layerId: "layer:1", splineId: "spline:1", point: { ...point, co: [0.4, 0.6], feather: 0.25 } });
result.edit = [pointEdit.revision, pointEdit.clips[0].tracks[0].markers.map((item) => item.frame), pointEdit.masks[0].layers[0].splines[0].points[0].co];
result.raycast = raycastMaskProject(pointEdit, [0.4, 0.6], 0.03);
const selection = selectMaskPointsInBounds(pointEdit, [0.1, 0.2], [0.5, 0.7]);
const toggled = selectMaskPointsInBounds(pointEdit, [0.1, 0.2], [0.5, 0.7], selection, "TOGGLE");
result.marquee = [selection.map((item) => item.pointId), toggled.length];
try { applyTrackingMaskEdit(parsed, { type: "SET_TRACK_SELECTION", revision: 3, clipId: "clip:1", trackId: "track:1", selected: true }); } catch (error) { result.revision = error instanceof Error ? error.message : String(error); }
} catch (error) { result.edit = error instanceof Error ? error.message : String(error); }
try { parseTrackingMaskProject({ ...base, clips: [{ ...base.clips[0], sourcePath: "../shot.mp4" }] }); } catch (error) { result.path = error instanceof Error ? error.message : String(error); }

View File

@@ -0,0 +1,173 @@
import { verifyNanoVDBChunk, type NanoVDBBundleManifestIR } from "../../../protocol/volume-vdb";
import {
NanoVDBGpuPageAllocator,
NanoVDBWebGPUDeviceSession,
sampleNanoVDBFloat32WebGPU,
uploadNanoVDBFloat32GridPaged,
} from "../render/nanovdb-volume-renderer";
import { createResumableHttpNanoVDBRangeSource } from "../volume/nanovdb-stream";
import { loadNanoVDBViewportAsset } from "../volume/nanovdb-viewport";
const scope = self as unknown as { onmessage: (() => void) | null; postMessage: (value: unknown) => void };
scope.onmessage = (): void => {
void (async () => {
const manifest = await (await fetch("/__vdb_fixture__/manifest", { cache: "no-store" })).json() as NanoVDBBundleManifestIR;
const report = await (await fetch("/__vdb_fixture__/report", { cache: "no-store" })).json() as { grids: Array<{ name: string; scalarSamples?: Array<{ coord: [number, number, number]; value: number; active: boolean }> }> };
let attempts = 0;
let retryResponses = 0;
const ifRanges: string[] = [];
const interruptedFetcher: typeof fetch = async (input, init) => {
attempts++;
const headers = new Headers(init?.headers);
ifRanges.push(headers.get("If-Range") ?? "");
if (retryResponses++ === 0) return new Response("temporary interruption", { status: 503 });
return fetch(input, init);
};
const rangeSource = createResumableHttpNanoVDBRangeSource("/__vdb_fixture__/bundle", manifest.bundleByteLength, {
fetcher: interruptedFetcher,
retries: 2,
retryDelayMs: 0,
requireStableEtag: true,
});
const first = await rangeSource({ chunkIndex: 0, start: manifest.chunks[0].byteOffset, endExclusive: manifest.chunks[0].byteOffset + manifest.chunks[0].byteLength, sha256: manifest.chunks[0].sha256 }, new AbortController().signal);
await verifyNanoVDBChunk(manifest.chunks[0], first);
const second = await rangeSource({ chunkIndex: 1, start: manifest.chunks[1].byteOffset, endExclusive: manifest.chunks[1].byteOffset + manifest.chunks[1].byteLength, sha256: manifest.chunks[1].sha256 }, new AbortController().signal);
await verifyNanoVDBChunk(manifest.chunks[1], second);
const resumeRanges: string[] = [];
const resumeIfRanges: string[] = [];
let interruptBody = true;
const interruptedBodyFetcher: typeof fetch = async (input, init) => {
const headers = new Headers(init?.headers);
resumeRanges.push(headers.get("Range") ?? "");
resumeIfRanges.push(headers.get("If-Range") ?? "");
const response = await fetch(input, init);
if (!interruptBody) return response;
interruptBody = false;
const bytes = new Uint8Array(await response.arrayBuffer());
const partial = bytes.slice(0, Math.min(4096, bytes.byteLength - 1));
let emitted = false;
const body = new ReadableStream<Uint8Array>({
pull(controller) {
if (!emitted) {
emitted = true;
controller.enqueue(partial);
return;
}
controller.error(new TypeError("injected response-body interruption"));
},
});
return new Response(body, { status: response.status, headers: response.headers });
};
const resumable = createResumableHttpNanoVDBRangeSource("/__vdb_fixture__/bundle", manifest.bundleByteLength, {
fetcher: interruptedBodyFetcher,
retries: 2,
retryDelayMs: 0,
requireStableEtag: true,
});
const resumed = await resumable({ chunkIndex: 0, start: manifest.chunks[0].byteOffset, endExclusive: manifest.chunks[0].byteOffset + manifest.chunks[0].byteLength, sha256: manifest.chunks[0].sha256 }, new AbortController().signal);
await verifyNanoVDBChunk(manifest.chunks[0], resumed);
let shortResponse = "";
try {
const shortFetcher: typeof fetch = async (input, init) => {
const response = await fetch(input, init);
const bytes = new Uint8Array(await response.arrayBuffer());
return new Response(bytes.slice(0, Math.max(1, Math.floor(bytes.byteLength / 2))), { status: response.status, headers: response.headers });
};
const shortSource = createResumableHttpNanoVDBRangeSource("/__vdb_fixture__/bundle", manifest.bundleByteLength, { fetcher: shortFetcher, retries: 2, retryDelayMs: 0, requireStableEtag: true });
await shortSource({ chunkIndex: 0, start: manifest.chunks[0].byteOffset, endExclusive: manifest.chunks[0].byteOffset + manifest.chunks[0].byteLength, sha256: manifest.chunks[0].sha256 }, new AbortController().signal);
}
catch (error) { shortResponse = error instanceof Error ? error.message : String(error); }
let changedEtag = "";
try {
let etagResponses = 0;
const changedEtagFetcher: typeof fetch = async (input, init) => {
const response = await fetch(input, init);
if (etagResponses++ === 0) return response;
const headers = new Headers(response.headers);
headers.set("ETag", '"changed-vdb-etag"');
return new Response(await response.arrayBuffer(), { status: response.status, headers });
};
const changedEtagSource = createResumableHttpNanoVDBRangeSource("/__vdb_fixture__/bundle", manifest.bundleByteLength, { fetcher: changedEtagFetcher, retries: 0, requireStableEtag: true });
await changedEtagSource({ chunkIndex: 0, start: manifest.chunks[0].byteOffset, endExclusive: manifest.chunks[0].byteOffset + manifest.chunks[0].byteLength, sha256: manifest.chunks[0].sha256 }, new AbortController().signal);
await changedEtagSource({ chunkIndex: 1, start: manifest.chunks[1].byteOffset, endExclusive: manifest.chunks[1].byteOffset + manifest.chunks[1].byteLength, sha256: manifest.chunks[1].sha256 }, new AbortController().signal);
}
catch (error) { changedEtag = error instanceof Error ? error.message : String(error); }
let outOfOrderResponse = "";
try {
const outOfOrderFetcher: typeof fetch = async (input, init) => {
const response = await fetch(input, init);
const bytes = await response.arrayBuffer();
const headers = new Headers(response.headers);
const requested = new Headers(init?.headers).get("Range")?.match(/^bytes=(\d+)-(\d+)$/);
if (requested) headers.set("Content-Range", `bytes ${Number(requested[1]) + 32}-${requested[2]}/${manifest.bundleByteLength}`);
return new Response(bytes, { status: response.status, headers });
};
const outOfOrderSource = createResumableHttpNanoVDBRangeSource("/__vdb_fixture__/bundle", manifest.bundleByteLength, { fetcher: outOfOrderFetcher, retries: 0, requireStableEtag: true });
await outOfOrderSource({ chunkIndex: 0, start: manifest.chunks[0].byteOffset, endExclusive: manifest.chunks[0].byteOffset + manifest.chunks[0].byteLength, sha256: manifest.chunks[0].sha256 }, new AbortController().signal);
}
catch (error) { outOfOrderResponse = error instanceof Error ? error.message : String(error); }
const asset = await loadNanoVDBViewportAsset("volume:FaultGate", "/__vdb_fixture__/manifest", "/__vdb_fixture__/bundle", new AbortController().signal);
const density = asset.manifest.grids.find((grid) => grid.name === asset.manifest.material.densityGrid)!;
const native = report.grids.find((grid) => grid.name === density.name)?.scalarSamples ?? [];
const payload = asset.grids.find((grid) => grid.name === density.name)!.data;
const session = new NanoVDBWebGPUDeviceSession();
const device = await session.open(payload.byteLength + 256 * 1024);
const allocator = new NanoVDBGpuPageAllocator(device, 64 * 1024, 128 * 1024);
const page = new Uint8Array(64 * 1024).buffer;
allocator.upload("page-a", page);
allocator.upload("page-b", page);
allocator.touch("page-a");
allocator.upload("page-c", page);
const lru = allocator.stats();
allocator.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();
}
catch (error) { oom = error instanceof Error ? error.message : String(error); }
const residentBudget = Math.ceil(payload.byteLength / (256 * 1024)) * 256 * 1024;
const uploaded = uploadNanoVDBFloat32GridPaged(device, payload, 256 * 1024, residentBudget);
const before = await sampleNanoVDBFloat32WebGPU(device, uploaded, native.map((sample) => sample.coord));
const paging = { pageCount: uploaded.pageCount, residentPageCount: uploaded.residentPageCount, byteLength: uploaded.byteLength };
uploaded.dispose();
const firstGeneration = session.generation;
device.destroy();
const loss = await session.waitForLoss();
const recoveredDevice = await session.recover(payload.byteLength + 256 * 1024);
const recovered = uploadNanoVDBFloat32GridPaged(recoveredDevice, payload, 256 * 1024, residentBudget);
const after = await sampleNanoVDBFloat32WebGPU(recoveredDevice, recovered, native.map((sample) => sample.coord));
recovered.dispose();
const recoveredGeneration = session.generation;
session.dispose();
return {
network: {
attempts,
ifRanges,
firstBytes: first.byteLength,
secondBytes: second.byteLength,
resumeRanges,
resumeIfRanges,
resumedBytes: resumed.byteLength,
shortResponse,
changedEtag,
outOfOrderResponse,
},
lru,
oom,
paging,
deviceLoss: { reason: loss.reason, firstGeneration, recoveredGeneration },
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

@@ -0,0 +1,213 @@
import { validateNanoVDBBundleManifest, type NanoVDBBundleManifestIR } from "../../../protocol/volume-vdb";
import { IncrementalSha256 } from "../volume/incremental-sha256";
import {
commitNanoVDBToOPFS,
createVDBProjectBinding,
openNanoVDBFromOPFS,
pruneNanoVDBOPFS,
recoverNanoVDBOPFS,
} from "../volume/nanovdb-opfs";
const scope = self as unknown as { onmessage: ((event: MessageEvent<{ action: "commit" | "reopen" | "interrupt" | "recoverInterrupted" | "prepareQuota" | "quota" | "verifyQuota"; state?: TestState }>) => void) | null; postMessage: (value: unknown) => void };
const projectId = "vdb-fixtures";
const sourceBlendSha256 = "b".repeat(64);
const cancelConverter = { target: "SERVER" as const, blenderVersion: "5.2.0", openVDBVersion: "13.0.0", nanoVDBVersion: "32.9.0", executableSha256: "c".repeat(64) };
interface TestState { bundleSha256: string; conversionRequestSha256: string; sourceSha256: string }
function bytes(seed: number): Uint8Array {
return Uint8Array.from({ length: 128 * 1024 }, (_value, index) => (index * 17 + seed) & 0xff);
}
function buffer(value: Uint8Array): ArrayBuffer {
return value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength) as ArrayBuffer;
}
async function hash(value: Uint8Array): Promise<string> {
const digest = await crypto.subtle.digest("SHA-256", buffer(value));
return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
}
async function bundleDirectory(bundleSha256: string): Promise<FileSystemDirectoryHandle> {
let directory = await navigator.storage.getDirectory();
for (const name of ["projects", projectId, "cache", "vdb", bundleSha256]) directory = await directory.getDirectoryHandle(name);
return directory;
}
async function overwriteFile(directory: FileSystemDirectoryHandle, name: string, value: ArrayBuffer | string): Promise<void> {
const writer = await (await directory.getFileHandle(name)).createWritable();
await writer.write(value);
await writer.close();
}
async function manifestFor(data: Uint8Array, requestSeed: string): Promise<NanoVDBBundleManifestIR> {
const first = data.subarray(0, 64 * 1024);
const second = data.subarray(64 * 1024);
return validateNanoVDBBundleManifest({
schemaVersion: 1,
projectId,
sourcePath: "//volumes/smoke.vdb",
sourceSha256: "a".repeat(64),
conversionRequestSha256: requestSeed.repeat(64),
bundlePath: "//volumes/smoke.nvdb",
bundleByteLength: data.byteLength,
bundleSha256: await hash(data),
converter: cancelConverter,
grids: [{
name: "density", valueType: "FLOAT32", gridClass: "FOG_VOLUME", semantic: "DENSITY", activeVoxelCount: 8,
segmentByteOffset: 0, segmentByteLength: data.byteLength, byteOffset: 0, byteLength: data.byteLength,
indexBounds: { min: [0, 0, 0], max: [1, 1, 1] }, worldBounds: { min: [0, 0, 0], max: [1, 1, 1] }, voxelSize: [0.5, 0.5, 0.5],
indexToWorld: [1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1],
}],
chunks: [
{ index: 0, byteOffset: 0, byteLength: first.byteLength, sha256: await hash(first) },
{ index: 1, byteOffset: first.byteLength, byteLength: second.byteLength, sha256: await hash(second) },
],
material: { densityGrid: "density", densityScale: 1, emissionScale: 0, temperatureScale: 1, anisotropy: 0, interpolation: "LINEAR" },
gpu: { representation: "NANOVDB_STORAGE_BUFFER", byteAlignment: 32, pageByteLength: 64 * 1024, maxResidentBytes: 64 * 1024 * 1024, shaderSemanticVersion: "volume-wgsl-v1" },
});
}
async function commit(): Promise<unknown> {
await pruneNanoVDBOPFS(projectId, 0);
const manifest = validateNanoVDBBundleManifest(await (await fetch("/__vdb_fixture__/manifest", { cache: "no-store" })).json() as NanoVDBBundleManifestIR);
const rangeSource = async (range: { start: number; endExclusive: number }): Promise<ArrayBuffer> => {
const response = await fetch("/__vdb_fixture__/bundle", {
cache: "no-store",
headers: { Range: `bytes=${range.start}-${range.endExclusive - 1}` },
});
if (response.status !== 206) throw new Error(`NANOVDB_STREAM_INCOMPLETE: fixture range returned ${response.status}`);
return response.arrayBuffer();
};
const binding = await createVDBProjectBinding(manifest, sourceBlendSha256);
const committed = await commitNanoVDBToOPFS(manifest, rangeSource, new AbortController().signal, binding);
const deduplicated = await commitNanoVDBToOPFS(manifest, rangeSource, new AbortController().signal, binding);
const cancelledData = bytes(71);
const cancelledManifest = await manifestFor(cancelledData, "e");
const controller = new AbortController();
let cancelled = false;
try {
await commitNanoVDBToOPFS(cancelledManifest, async (range) => {
if (range.chunkIndex === 1) controller.abort();
return buffer(cancelledData.slice(range.start, range.endExclusive));
}, controller.signal);
}
catch (error) { cancelled = error instanceof DOMException && error.name === "AbortError"; }
const recovered = await recoverNanoVDBOPFS(projectId);
return {
committed,
deduplicated: deduplicated.deduplicated,
cancelled,
recovered,
realBundleBytes: manifest.bundleByteLength,
realChunkCount: manifest.chunks.length,
state: { bundleSha256: manifest.bundleSha256, conversionRequestSha256: manifest.conversionRequestSha256, sourceSha256: manifest.sourceSha256 },
};
}
async function reopen(state: TestState): Promise<unknown> {
const fixture = validateNanoVDBBundleManifest(await (await fetch("/__vdb_fixture__/manifest", { cache: "no-store" })).json() as NanoVDBBundleManifestIR);
const reopenContext = {
projectId, sourceBlendSha256, sourcePath: fixture.sourcePath, sourceSha256: state.sourceSha256, converter: fixture.converter, shaderSemanticVersion: "volume-wgsl-v1",
} as const;
const opened = await openNanoVDBFromOPFS(projectId, state.bundleSha256, state.conversionRequestSha256, reopenContext);
const hasher = new IncrementalSha256();
for (let index = 0; index < opened.manifest.chunks.length; index++) {
const chunk = opened.manifest.chunks[index];
const data = await opened.source({ chunkIndex: index, start: chunk.byteOffset, endExclusive: chunk.byteOffset + chunk.byteLength, sha256: chunk.sha256 }, new AbortController().signal);
hasher.update(data);
}
const stale = await openNanoVDBFromOPFS(projectId, state.bundleSha256, state.conversionRequestSha256, {
projectId, sourceBlendSha256, sourcePath: fixture.sourcePath, sourceSha256: "f".repeat(64), converter: fixture.converter, shaderSemanticVersion: "volume-wgsl-v1",
});
const bundle = await bundleDirectory(state.bundleSha256);
const firstChunkHandle = await bundle.getFileHandle("00000.chunk");
const firstChunk = new Uint8Array(await (await firstChunkHandle.getFile()).arrayBuffer());
const tampered = firstChunk.slice();
tampered[0] ^= 0xff;
await overwriteFile(bundle, "00000.chunk", tampered.buffer);
let tamperedChunk = "";
try {
const chunk = opened.manifest.chunks[0];
await opened.source({ chunkIndex: 0, start: chunk.byteOffset, endExclusive: chunk.byteOffset + chunk.byteLength, sha256: chunk.sha256 }, new AbortController().signal);
}
catch (error) { tamperedChunk = error instanceof Error ? error.message : String(error); }
await overwriteFile(bundle, "00000.chunk", firstChunk.buffer);
const manifestHandle = await bundle.getFileHandle("manifest.json");
const manifestText = await (await manifestHandle.getFile()).text();
const rolledBackManifest = { ...opened.manifest, material: { ...opened.manifest.material, densityScale: opened.manifest.material.densityScale + 0.25 } };
await overwriteFile(bundle, "manifest.json", JSON.stringify(rolledBackManifest));
let manifestRollback = "";
try { await openNanoVDBFromOPFS(projectId, state.bundleSha256, state.conversionRequestSha256, reopenContext); }
catch (error) { manifestRollback = error instanceof Error ? error.message : String(error); }
await overwriteFile(bundle, "manifest.json", manifestText);
const pruned = await pruneNanoVDBOPFS(projectId, 0);
return { bindingStatus: opened.bindingStatus, staleStatus: stale.bindingStatus, bundleHash: hasher.hex(), expectedHash: state.bundleSha256, tamperedChunk, manifestRollback, pruned };
}
async function interrupt(): Promise<never> {
await pruneNanoVDBOPFS(projectId, 0);
const manifest = validateNanoVDBBundleManifest(await (await fetch("/__vdb_fixture__/manifest", { cache: "no-store" })).json() as NanoVDBBundleManifestIR);
await commitNanoVDBToOPFS(manifest, async (range) => {
if (range.chunkIndex === 1) {
scope.postMessage({ staged: true, bundleSha256: manifest.bundleSha256 });
await new Promise<never>(() => undefined);
}
const response = await fetch("/__vdb_fixture__/bundle", { cache: "no-store", headers: { Range: `bytes=${range.start}-${range.endExclusive - 1}` } });
if (response.status !== 206) throw new Error(`NANOVDB_STREAM_INCOMPLETE: fixture range returned ${response.status}`);
return response.arrayBuffer();
}, new AbortController().signal);
throw new Error("interrupted commit unexpectedly completed");
}
async function recoverInterrupted(): Promise<unknown> {
const recovered = await recoverNanoVDBOPFS(projectId);
await pruneNanoVDBOPFS(projectId, 0);
return recovered;
}
async function prepareQuota(): Promise<unknown> {
await pruneNanoVDBOPFS(projectId, 0);
const data = bytes(81);
const manifest = await manifestFor(data, "1");
await commitNanoVDBToOPFS(manifest, async (range) => buffer(data.slice(range.start, range.endExclusive)), new AbortController().signal);
return { state: { bundleSha256: manifest.bundleSha256, conversionRequestSha256: manifest.conversionRequestSha256, sourceSha256: manifest.sourceSha256 } };
}
async function quota(state: TestState): Promise<unknown> {
let error = "";
try {
const data = bytes(82);
const manifest = await manifestFor(data, "2");
await commitNanoVDBToOPFS(manifest, async (range) => buffer(data.slice(range.start, range.endExclusive)), new AbortController().signal);
}
catch (caught) { error = caught instanceof Error ? `${caught.name}: ${caught.message}` : String(caught); }
let recoveryWhileQuotaLimited = "";
let recoveredWhileQuotaLimited: unknown;
try { recoveredWhileQuotaLimited = await recoverNanoVDBOPFS(projectId); }
catch (caught) { recoveryWhileQuotaLimited = caught instanceof Error ? `${caught.name}: ${caught.message}` : String(caught); }
return { quotaError: error, recoveryWhileQuotaLimited, recoveredWhileQuotaLimited, previousBundleSha256: state.bundleSha256 };
}
async function verifyQuota(state: TestState): Promise<unknown> {
const recovered = await recoverNanoVDBOPFS(projectId);
const opened = await openNanoVDBFromOPFS(projectId, state.bundleSha256);
const first = opened.manifest.chunks[0];
await opened.source({ chunkIndex: 0, start: first.byteOffset, endExclusive: first.byteOffset + first.byteLength, sha256: first.sha256 }, new AbortController().signal);
await pruneNanoVDBOPFS(projectId, 0);
return { recovered, previousBundleReadable: true };
}
scope.onmessage = (event): void => {
const action = event.data.action;
void (action === "commit" ? commit() :
action === "reopen" ? reopen(event.data.state!) :
action === "interrupt" ? interrupt() :
action === "recoverInterrupted" ? recoverInterrupted() :
action === "prepareQuota" ? prepareQuota() :
action === "quota" ? quota(event.data.state!) : verifyQuota(event.data.state!))
.then((result) => scope.postMessage(result))
.catch((error) => scope.postMessage({ error: error instanceof Error ? error.stack ?? error.message : String(error) }));
};

View File

@@ -1,27 +1,206 @@
import { decodeVDBResource, type VDBResourceManifest } from "../../../protocol/volume-vdb";
import {
VDB_PIPELINE_SCHEMA,
gateNanoVDBPipeline,
hashVDBConversionRequest,
planNanoVDBRanges,
prepareVDBConversionInput,
validateNanoVDBBundleManifest,
validateVDBConversionRequest,
verifyNanoVDBBundle,
verifyNanoVDBChunk,
type NanoVDBBundleManifestIR,
type NanoVDBGridIR,
type VDBConverterIdentityIR,
type VDBResourceManifest,
} from "../../../protocol/volume-vdb";
import { createHttpNanoVDBRangeSource, streamNanoVDBChunks } from "../volume/nanovdb-stream";
const scope = self as unknown as { onmessage: (() => void) | null; postMessage: (value: unknown) => void };
const identityTransform: NanoVDBGridIR["indexToWorld"] = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
function bytes(length: number, seed: number): Uint8Array {
return Uint8Array.from({ length }, (_value, index) => (index * 37 + seed) & 0xff);
}
function arrayBuffer(value: Uint8Array): ArrayBuffer {
return value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength) as ArrayBuffer;
}
async function digest(value: Uint8Array): Promise<string> {
const hash = await crypto.subtle.digest("SHA-256", arrayBuffer(value));
return Array.from(new Uint8Array(hash), (byte) => byte.toString(16).padStart(2, "0")).join("");
}
function grid(name: string, semantic: NanoVDBGridIR["semantic"], byteOffset: number): NanoVDBGridIR {
return {
name,
valueType: "FLOAT32",
gridClass: "FOG_VOLUME",
semantic,
activeVoxelCount: 8,
segmentByteOffset: byteOffset,
segmentByteLength: 32,
byteOffset,
byteLength: 32,
indexBounds: { min: [0, 0, 0], max: [1, 1, 1] },
worldBounds: { min: [0, 0, 0], max: [1, 1, 1] },
voxelSize: [0.5, 0.5, 0.5],
indexToWorld: [...identityTransform],
};
}
scope.onmessage = (): void => {
void (async () => {
const data = Uint8Array.from([0x76, 0x64, 0x62, 0x01]);
const digest = Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", data)), (value) => value.toString(16).padStart(2, "0")).join("");
const manifest: VDBResourceManifest = { projectId: "vdb-test", sourcePath: "//cache/smoke.vdb", byteLength: data.byteLength, sha256: digest, grids: [{ name: "density", valueType: "FLOAT", voxelCount: 16, activeVoxelCount: 8 }] };
const decoded = await decodeVDBResource({ ...manifest, data: data.buffer }, async (request, signal) => {
if (signal.aborted) throw new DOMException("cancelled", "AbortError");
return { metadata: request, decodedByteLength: request.data.byteLength };
}, new AbortController().signal);
// These deterministic bytes exercise the transport contract only. They are not
// presented as an OpenVDB or NanoVDB decoder fixture.
const sourceBytes = bytes(64, 11);
const source: VDBResourceManifest = {
projectId: "vdb-protocol-test",
sourcePath: "//volumes/smoke.vdb",
byteLength: sourceBytes.byteLength,
sha256: await digest(sourceBytes),
grids: [
{ name: "density", valueType: "FLOAT", voxelCount: 16, activeVoxelCount: 8 },
{ name: "temperature", valueType: "FLOAT", voxelCount: 16, activeVoxelCount: 8 },
],
};
const prepared = await prepareVDBConversionInput({ ...source, data: arrayBuffer(sourceBytes) }, new AbortController().signal);
const converter: VDBConverterIdentityIR = {
target: "SERVER",
blenderVersion: "5.2.0",
openVDBVersion: "contract-fixture",
nanoVDBVersion: "contract-fixture",
executableSha256: "1".repeat(64),
};
const conversion = validateVDBConversionRequest({
schemaVersion: VDB_PIPELINE_SCHEMA,
jobId: "vdb-protocol-job",
source,
outputPath: "//volumes/smoke.nvdb",
selectedGrids: ["density", "temperature"],
quantization: "LOSSLESS",
chunkByteLength: 64 * 1024,
converter,
});
const conversionRequestSha256 = await hashVDBConversionRequest(conversion);
const relocatedConversionSha256 = await hashVDBConversionRequest({
...conversion,
jobId: "vdb-relocated-job",
outputPath: "//cache/relocated.nvdb",
});
const bundleBytes = bytes(64, 23);
const first = bundleBytes.slice(0, 32);
const second = bundleBytes.slice(32);
const manifest: NanoVDBBundleManifestIR = {
schemaVersion: VDB_PIPELINE_SCHEMA,
projectId: source.projectId,
sourcePath: source.sourcePath,
sourceSha256: source.sha256,
conversionRequestSha256,
bundlePath: "//volumes/smoke.nvdb",
bundleByteLength: bundleBytes.byteLength,
bundleSha256: await digest(bundleBytes),
converter,
grids: [grid("density", "DENSITY", 0), grid("temperature", "TEMPERATURE", 32)],
chunks: [
{ index: 0, byteOffset: 0, byteLength: 32, sha256: await digest(first) },
{ index: 1, byteOffset: 32, byteLength: 32, sha256: await digest(second) },
],
material: {
densityGrid: "density",
temperatureGrid: "temperature",
densityScale: 1,
emissionScale: 0,
temperatureScale: 1,
anisotropy: 0,
interpolation: "LINEAR",
},
gpu: {
representation: "NANOVDB_STORAGE_BUFFER",
byteAlignment: 32,
pageByteLength: 64 * 1024,
maxResidentBytes: 64 * 1024,
shaderSemanticVersion: "volume-wgsl-v1",
},
};
const validated = validateNanoVDBBundleManifest(manifest);
const ranges = planNanoVDBRanges(validated);
await verifyNanoVDBChunk(validated.chunks[0], arrayBuffer(first));
await verifyNanoVDBChunk(validated.chunks[1], arrayBuffer(second));
await verifyNanoVDBBundle(validated, arrayBuffer(bundleBytes));
const consumed: number[] = [];
const progress: number[] = [];
const stream = await streamNanoVDBChunks(
validated,
async (range) => arrayBuffer(bundleBytes.slice(range.start, range.endExclusive)),
(range) => { consumed.push(range.chunkIndex); },
new AbortController().signal,
(state) => { progress.push(state.completedBytes); },
);
const exactFetch: typeof fetch = async (_input, init) => {
const rangeHeader = new Headers(init?.headers).get("Range");
const match = rangeHeader?.match(/^bytes=(\d+)-(\d+)$/);
if (!match) return new Response(null, { status: 416 });
const start = Number(match[1]);
const endInclusive = Number(match[2]);
return new Response(arrayBuffer(bundleBytes.slice(start, endInclusive + 1)), {
status: 206,
headers: { "Content-Range": `bytes ${start}-${endInclusive}/${bundleBytes.byteLength}` },
});
};
const httpRange = await createHttpNanoVDBRangeSource("/assets/smoke.nvdb", bundleBytes.byteLength, exactFetch)(ranges[0], new AbortController().signal);
let invalidHttpRange = "";
try {
const fullResponse: typeof fetch = async () => new Response(arrayBuffer(bundleBytes), { status: 200 });
await createHttpNanoVDBRangeSource("/assets/smoke.nvdb", bundleBytes.byteLength, fullResponse)(ranges[0], new AbortController().signal);
}
catch (error) { invalidHttpRange = error instanceof Error ? error.message : String(error); }
let outsideProject = "";
try {
await decodeVDBResource({ ...manifest, sourcePath: "../../outside.vdb", data: data.buffer }, undefined, new AbortController().signal);
await prepareVDBConversionInput({ ...source, sourcePath: "../../outside.vdb", data: arrayBuffer(sourceBytes) }, new AbortController().signal);
}
catch (error) { outsideProject = error instanceof Error ? error.message : String(error); }
let tamperedChunk = "";
try {
const tampered = first.slice();
tampered[0] ^= 0xff;
await verifyNanoVDBChunk(validated.chunks[0], arrayBuffer(tampered));
}
catch (error) { tamperedChunk = error instanceof Error ? error.message : String(error); }
let incompleteStream = "";
try {
validateNanoVDBBundleManifest({ ...manifest, chunks: manifest.chunks.map((chunk, index) => index === 1 ? { ...chunk, byteOffset: 64 } : chunk) });
}
catch (error) { incompleteStream = error instanceof Error ? error.message : String(error); }
const controller = new AbortController();
const cancelled = decodeVDBResource({ ...manifest, data: data.buffer }, async (_request, signal) => new Promise((resolve, reject) => {
const timer = setTimeout(() => resolve({ metadata: manifest, decodedByteLength: data.byteLength }), 20);
signal.addEventListener("abort", () => { clearTimeout(timer); reject(new DOMException("cancelled", "AbortError")); }, { once: true });
}), controller.signal).then(() => false).catch((error) => error instanceof DOMException && error.name === "AbortError");
setTimeout(() => controller.abort(), 1);
scope.postMessage({ decodedByteLength: decoded.decodedByteLength, outsideProject, cancelled: await cancelled });
controller.abort();
const cancelled = prepareVDBConversionInput({ ...source, data: arrayBuffer(sourceBytes) }, controller.signal)
.then(() => false)
.catch((error) => error instanceof DOMException && error.name === "AbortError");
const rawBrowserGate = gateNanoVDBPipeline("RAW_VDB_BROWSER_DECODE");
const streamGate = gateNanoVDBPipeline("NANOVDB_STREAM", { manifestValidated: true, rangeReaderAvailable: true });
const renderGate = gateNanoVDBPipeline("WEBGPU_VOLUME_RENDER", { manifestValidated: true, rangeReaderAvailable: true, webgpuAvailable: true });
scope.postMessage({
preparedByteLength: prepared.data.byteLength,
conversionTarget: conversion.converter.target,
conversionRequestSha256: manifest.conversionRequestSha256,
relocationKeepsContentKey: conversionRequestSha256 === relocatedConversionSha256,
ranges,
consumed,
progress,
stream,
httpRangeByteLength: httpRange.byteLength,
invalidHttpRange,
outsideProject,
tamperedChunk,
incompleteStream,
cancelled: await cancelled,
rawBrowserGate,
streamGate,
renderGate,
});
})().catch((error) => scope.postMessage({ error: error instanceof Error ? error.message : String(error) }));
};

View File

@@ -0,0 +1,64 @@
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";
const scope = self as unknown as { onmessage: (() => void) | null; postMessage: (value: unknown) => void };
scope.onmessage = (): void => {
void (async () => {
const manifest = validateNanoVDBBundleManifest(await (await fetch("/__vdb_fixture__/manifest", { cache: "no-store" })).json() as NanoVDBBundleManifestIR);
const report = await (await fetch("/__vdb_fixture__/report", { cache: "no-store" })).json() as { grids: Array<{ name: string; scalarSamples?: Array<{ coord: [number, number, number]; value: number; active: boolean }> }> };
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 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);
if (!probe.capability.available || !probe.device) throw new Error(probe.capability.reason ?? "WebGPU adapter is unavailable");
const device = probe.device;
device.pushErrorScope("validation");
const uploaded = uploadNanoVDBFloat32Grid(device, payload);
const gpuSamples = await sampleNanoVDBFloat32WebGPU(device, uploaded, nativeDensity.scalarSamples.map((sample) => sample.coord));
const materialMapping = mapPrincipledVolumeToNanoVDB(manifest, {
densityGrid: manifest.material.densityGrid,
densityScale: 1,
colorGrid: manifest.material.colorGrid,
color: [0.7, 0.8, 0.95],
temperatureGrid: manifest.material.temperatureGrid,
temperatureScale: 1,
blackbodyEnabled: true,
emissionColor: [1, 0.35, 0.1],
emissionScale: 0.08,
velocityGrid: manifest.material.velocityGrid,
anisotropy: 0.2,
interpolation: "LINEAR",
});
const pixels = await renderNanoVDBFloat32WebGPU(device, uploaded, density, materialMapping.material, 96, 96);
const validationError = await device.popErrorScope();
if (validationError) throw new Error(`WebGPU validation failed: ${validationError.message}`);
let visiblePixels = 0;
let alphaSum = 0;
for (let index = 3; index < pixels.length; index += 4) {
alphaSum += pixels[index];
if (pixels[index] > 0) visiblePixels++;
}
const imageHash = await crypto.subtle.digest("SHA-256", new Uint8Array(pixels).buffer);
uploaded.dispose();
device.destroy();
return {
capability: probe.capability,
payloadBytes: payload.byteLength,
nativeSamples: nativeDensity.scalarSamples,
cpuSamples,
gpuSamples,
visiblePixels,
alphaSum,
imageSha256: Array.from(new Uint8Array(imageHash), (byte) => byte.toString(16).padStart(2, "0")).join(""),
materialMapping,
};
})().then((result) => scope.postMessage(result)).catch((error) => scope.postMessage({ error: error instanceof Error ? error.message : String(error) }));
};

View File

@@ -10,6 +10,7 @@ import {
InstancedMesh,
Matrix4,
Mesh,
MeshBasicMaterial,
MeshPhysicalMaterial,
PerspectiveCamera,
Quaternion,
@@ -27,6 +28,10 @@ import type { MeshElementMode, MeshGeometryBuffer } from "../../../protocol/web-
import type { NonMeshGeometryChunk } from "../../../protocol/nonmesh-binary";
import type { OffscreenViewportRequest, OffscreenViewportResponse } from "../three-adapter/offscreen-viewport-protocol";
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 { createNanoVDBViewportObject } from "../three-adapter/volume";
import {
configurePBRLight,
configurePBRRenderer,
@@ -35,8 +40,15 @@ import {
setPBRMaterialSelected,
} from "../three-adapter/pbr";
import { GPUTextureStore } from "../three-adapter/texture-assets";
import { applyNonMeshElementSelection, applyNonMeshTransform, createNonMeshObject } from "../three-adapter/nonmesh";
import { applyGreasePencilTransform, createGreasePencilObject } from "../three-adapter/grease-pencil";
import { applyCurveHandlePreview, applyNonMeshElementSelection, applyNonMeshTransform, createNonMeshObject } from "../three-adapter/nonmesh";
import {
applyGreasePencilPointSelection,
applyGreasePencilPointPreview,
applyGreasePencilTransform,
createGreasePencilObject,
greasePencilPointRef,
type GreasePencilPointRef,
} from "../three-adapter/grease-pencil";
const workerScope = self as unknown as {
onmessage: ((event: MessageEvent<OffscreenViewportRequest>) => void) | null;
@@ -47,6 +59,7 @@ let scene: Scene | null = null;
let camera: PerspectiveCamera | null = null;
let root: Group | null = null;
let importedLights: Group | null = null;
let contextLost = false;
let width = 1;
let height = 1;
let yaw = -Math.PI / 4;
@@ -57,17 +70,27 @@ let selectionMode: MeshElementMode = "FACE";
let currentSnapshot: SceneSnapshotIR | null = null;
const textureStore = new GPUTextureStore();
const raycaster = new Raycaster();
raycaster.params.Points.threshold = 0.14;
const objectById = new Map<string, Object3D>();
let curveGizmoFrame: { dataId: string; frame: CurveGizmoFrameIR } | null = null;
let volumeAssets: NanoVDBViewportAssetIR[] = [];
const volumeRenderCache = new Map<string, NanoVDBViewportRenderResultIR>();
let volumeRenderGeneration = 0;
const volumeRenderSession = new NanoVDBViewportRenderSession(() => {
volumeRenderCache.clear();
void refreshVolumes();
});
function post(message: OffscreenViewportResponse): void {
workerScope.postMessage(message);
}
function render(): void {
if (!renderer || !scene || !camera) return;
if (contextLost || !renderer || !scene || !camera) return;
camera.position.set(distance * Math.cos(pitch) * Math.cos(yaw), distance * Math.cos(pitch) * Math.sin(yaw), distance * Math.sin(pitch));
camera.lookAt(0, 0, 0);
renderer.render(scene, camera);
publishCurveGizmoFrame();
const gl = renderer.getContext();
const sampleWidth = Math.min(16, gl.drawingBufferWidth);
const sampleHeight = Math.min(16, gl.drawingBufferHeight);
@@ -88,6 +111,28 @@ function render(): void {
post({ type: "frame", visiblePixels });
}
function publishCurveGizmoFrame(): void {
const active = curveGizmoFrame;
let frame: CurveGizmoScreenFrameIR | null = null;
const node = active ? currentSnapshot?.nodes.find((candidate) => candidate.dataId === active.dataId && candidate.id === currentSnapshot?.activeObjectId) : undefined;
const object = node ? objectById.get(node.id) : undefined;
if (active && object && camera) {
object.updateWorldMatrix(true, false);
camera.updateMatrixWorld(true);
const project = (value: readonly number[]): Vector3 => new Vector3(value[0], value[2], -value[1]).applyMatrix4(object.matrixWorld).project(camera!);
const origin = project(active.frame.origin);
const axes = active.frame.axes.map((axis) => {
const endpoint = project([active.frame.origin[0] + axis[0], active.frame.origin[1] + axis[1], active.frame.origin[2] + axis[2]]);
const x = endpoint.x - origin.x;
const y = origin.y - endpoint.y;
const magnitude = Math.hypot(x, y);
return magnitude > 1e-8 ? [x / magnitude, y / magnitude] as [number, number] : [0, 0] as [number, number];
}) as CurveGizmoScreenFrameIR["axes"];
frame = { origin: [(origin.x + 1) / 2, (1 - origin.y) / 2], axes };
}
post({ type: "curveGizmoScreenFrame", frame });
}
function resize(nextWidth: number, nextHeight: number, pixelRatio: number): void {
width = Math.max(1, nextWidth);
height = Math.max(1, nextHeight);
@@ -172,12 +217,65 @@ function clearRoot(): void {
const mesh = object as Mesh;
mesh.geometry?.dispose();
const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
for (const material of materials) material?.dispose();
for (const material of materials) {
if (mesh.userData.nanoVDBVolume && material instanceof MeshBasicMaterial) material.map?.dispose();
material?.dispose();
}
});
}
objectById.clear();
}
async function refreshVolumes(): Promise<void> {
const generation = ++volumeRenderGeneration;
if (!root || !currentSnapshot) return;
for (const child of [...root.children]) {
if (!child.userData.nanoVDBVolume) continue;
root.remove(child);
child.traverse((object) => {
const mesh = object as Mesh;
mesh.geometry?.dispose();
const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
for (const material of materials) {
if (material instanceof MeshBasicMaterial) material.map?.dispose();
material?.dispose();
}
});
}
const snapshot = currentSnapshot;
const nodes = snapshot.nodes.filter((node) => node.visible && node.type === "VOLUME" && node.dataId);
if (nodes.length === 0) {
post({ type: "volumeStatus", status: "none", count: 0 });
return;
}
post({ type: "volumeStatus", status: "loading", count: 0 });
try {
let count = 0;
for (const node of nodes) {
const asset = volumeAssets.find((candidate) => candidate.dataId === node.dataId);
if (!asset) continue;
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);
volumeRenderCache.set(cacheKey, result);
}
if (generation !== volumeRenderGeneration || currentSnapshot !== snapshot || !root) return;
const object = createNanoVDBViewportObject(result, node);
root.add(object);
objectById.set(node.id, object);
count++;
}
if (generation !== volumeRenderGeneration) return;
post({ type: "volumeStatus", status: count === nodes.length ? "ready" : "blocked", count, ...(count === nodes.length ? {} : { errorCode: "NON_MESH_RESOURCE_MISSING" }) });
render();
}
catch (error) {
if (generation !== volumeRenderGeneration) return;
post({ type: "volumeStatus", status: "blocked", count: 0, errorCode: error instanceof Error ? error.message.split(":", 1)[0] : "VOLUME_SHADER_UNAVAILABLE" });
}
}
function applyTextureAssets(assets: readonly GPUTextureAsset[]): void {
void textureStore.upload(assets).then((status) => {
if (currentSnapshot && root) textureStore.applySnapshotMaterials(root, currentSnapshot);
@@ -258,6 +356,7 @@ function setSnapshot(snapshot: SceneSnapshotIR, buffers: MeshGeometryBuffer[], n
if (node.type === "MESH" || node.type === "LIGHT" || node.type === "CAMERA" || !node.visible || !node.dataId) continue;
const data = dataById.get(node.dataId);
if (!data) continue;
if (data.type === "VOLUME") continue;
const object = createNonMeshObject(data, nonMeshGeometryBuffers);
if (!object) {
nonMeshBlockedCount++;
@@ -310,10 +409,11 @@ function setSnapshot(snapshot: SceneSnapshotIR, buffers: MeshGeometryBuffer[], n
objectById.set(node.id, light);
}
}
void refreshVolumes();
render();
}
function setSelection(ids: string[], elements: Array<{ dataId: string; kind: NonMeshElementKind; index: number }>): void {
function setSelection(ids: string[], elements: Array<{ dataId: string; kind: NonMeshElementKind; index: number }>, greasePencilPoints: GreasePencilPointRef[]): void {
const selected = new Set(ids);
const visited = new Set<Object3D>();
for (const [id, object] of objectById) {
@@ -340,13 +440,24 @@ function setSelection(ids: string[], elements: Array<{ dataId: string; kind: Non
selection.set(element.dataId, kinds);
}
if (root) applyNonMeshElementSelection(root, selection);
if (root) applyGreasePencilPointSelection(root, greasePencilPoints);
render();
}
function pick(x: number, y: number, additive: boolean): void {
if (!root || !camera) return;
raycaster.setFromCamera(new Vector2(x, y), camera);
const hit = raycaster.intersectObjects(root.children, true)[0];
const hits = raycaster.intersectObjects(root.children, true);
const greasePencilHit = editMode
? hits.find((intersection) => intersection.index !== undefined && greasePencilPointRef(intersection.object, intersection.index) !== null)
: undefined;
if (greasePencilHit?.index !== undefined) {
const point = greasePencilPointRef(greasePencilHit.object, greasePencilHit.index);
if (point) post({ type: "greasePencilPointSelected", point, additive });
return;
}
const preferredNonMeshHit = editMode ? hits.find((intersection) => intersection.index !== undefined && Array.isArray(intersection.object.userData.nonMeshPointKindMap)) : undefined;
const hit = preferredNonMeshHit ?? hits[0];
if (!hit) return;
const nonMeshDataId = hit.object.userData.nonMeshDataId;
if (typeof nonMeshDataId === "string" && hit.index !== undefined) {
@@ -403,6 +514,18 @@ workerScope.onmessage = (event): void => {
const message = event.data;
if (message.type === "init") {
renderer = new WebGLRenderer({ canvas: message.canvas, antialias: true, preserveDrawingBuffer: true });
message.canvas.addEventListener("webglcontextlost", (event) => {
event.preventDefault();
contextLost = true;
post({ type: "volumeStatus", status: "loading", count: 0, errorCode: "WEBGL_CONTEXT_LOST" });
});
message.canvas.addEventListener("webglcontextrestored", () => {
contextLost = false;
configurePBRRenderer(renderer!);
volumeRenderCache.clear();
void refreshVolumes();
render();
});
configurePBRRenderer(renderer);
scene = new Scene();
camera = new PerspectiveCamera(45, 1, 0.01, 1000);
@@ -421,11 +544,31 @@ workerScope.onmessage = (event): void => {
}
else if (message.type === "snapshot") setSnapshot(message.snapshot, message.geometryBuffers, message.nonMeshGeometryBuffers);
else if (message.type === "textureAssets") applyTextureAssets(message.assets);
else if (message.type === "volumeAssets") {
volumeAssets = message.assets;
void refreshVolumes();
}
else if (message.type === "resize") resize(message.width, message.height, message.pixelRatio);
else if (message.type === "selection") setSelection(message.objectIds, message.elements);
else if (message.type === "selection") setSelection(message.objectIds, message.elements, message.greasePencilPoints);
else if (message.type === "interaction") {
editMode = message.editMode;
selectionMode = message.selectionMode;
root?.traverse((object) => {
if (typeof object.userData.greasePencilPointDataId === "string") object.visible = editMode;
});
render();
}
else if (message.type === "curveHandlePreview") {
if (root) applyCurveHandlePreview(root, message.dataId, message.handles);
render();
}
else if (message.type === "greasePencilPointPreview") {
if (root) applyGreasePencilPointPreview(root, message.dataId, message.layerId, message.frame, message.points);
render();
}
else if (message.type === "curveGizmoFrame") {
curveGizmoFrame = message.dataId && message.frame ? { dataId: message.dataId, frame: message.frame } : null;
render();
}
else if (message.type === "orbit") {
yaw -= message.deltaX * 0.008;
@@ -436,6 +579,9 @@ workerScope.onmessage = (event): void => {
else if (message.type === "pick") pick(message.x, message.y, message.additive);
else if (message.type === "dispose") {
clearRoot();
volumeRenderGeneration++;
volumeRenderCache.clear();
volumeRenderSession.dispose();
clearLights();
renderer?.dispose();
textureStore.dispose();
@@ -444,6 +590,7 @@ workerScope.onmessage = (event): void => {
camera = null;
root = null;
importedLights = null;
contextLost = false;
}
}
catch (error) {

View File

@@ -90,6 +90,7 @@ let module: WasmModule | null = null;
let wasmFactory: WasmFactory | null = null;
let wasmBinary: ArrayBuffer | null = null;
let handle = 0;
let initializationPromise: Promise<WebEngineStatus> | null = null;
let currentSnapshot: ReturnType<typeof parseSceneSnapshotIR> | null = null;
let currentGeometryBuffers: MeshGeometryBuffer[] = [];
let sourceBlendBuffer: ArrayBuffer | null = null;
@@ -300,6 +301,17 @@ function assertFutureCapability(payload: Extract<WebEngineRequest["command"], {
if (!payload.links || [payload.links.regular, payload.links.bold, payload.links.italic, payload.links.boldItalic].some((id) => typeof id !== "string" || !available.has(id))) throw report("NON_MESH_RESOURCE_MISSING", "Font style links must reference VFonts already present in the current Main");
return;
}
case "setVolumeProperties": {
const data = currentSnapshot?.nonMeshData?.find((candidate) => candidate.id === payload.dataId);
if (!data || data.type !== "VOLUME") throw report("NON_MESH_DATA_UNSUPPORTED", `N-015 Volume data block is unavailable: ${payload.dataId}`);
if (typeof payload.sourcePath !== "string" || !payload.sourcePath.startsWith("//") || !payload.sourcePath.endsWith(".vdb") || payload.sourcePath.includes("\\") || payload.sourcePath.slice(2).split("/").includes("..") || new TextEncoder().encode(payload.sourcePath).byteLength >= 1024) throw report("NON_MESH_RESOURCE_OUTSIDE_PROJECT", "Volume source must be a bounded project-relative // path ending in .vdb");
if (!Number.isFinite(payload.displayDensity) || payload.displayDensity < 0 || payload.displayDensity > 1_000_000) throw report("NON_MESH_PROPERTY_INVALID", "Volume display density is outside the bounded range");
if (payload.interpolation !== "NEAREST" && payload.interpolation !== "LINEAR") throw report("NON_MESH_PROPERTY_INVALID", "Volume interpolation is unsupported");
if (!Number.isFinite(payload.stepSize) || payload.stepSize < 0 || payload.stepSize > 1_000_000) throw report("NON_MESH_PROPERTY_INVALID", "Volume render step is outside the bounded range");
if (payload.velocityGrid !== undefined && (typeof payload.velocityGrid !== "string" || new TextEncoder().encode(payload.velocityGrid).byteLength >= 64)) throw report("NON_MESH_PROPERTY_INVALID", "Volume velocity grid name exceeds the Blender field limit");
if (payload.velocityScale !== undefined && (!Number.isFinite(payload.velocityScale) || payload.velocityScale < -1_000_000 || payload.velocityScale > 1_000_000)) throw report("NON_MESH_PROPERTY_INVALID", "Volume velocity scale is outside the bounded range");
return;
}
case "setMetaballElements": {
const data = currentSnapshot?.nonMeshData?.find((candidate) => candidate.id === payload.dataId);
if (!data || data.type !== "METABALL") throw report("NON_MESH_DATA_UNSUPPORTED", `N-015 Metaball data block is unavailable: ${payload.dataId}`);
@@ -459,20 +471,32 @@ function nativeError(fallbackCode: ErrorReport["code"]): ErrorReport {
}
async function initialize(): Promise<WebEngineStatus> {
if (!module) {
const imported = await import("../vendor/blender/web_engine.js") as unknown as { default: WasmFactory };
const factory = imported.default;
const binary = await fetch("/vendor/blender/web_engine.wasm?v=2").then((response) => {
if (!response.ok) throw new Error(`web_engine.wasm request failed: ${response.status}`);
return response.arrayBuffer();
if (module) return status();
if (!initializationPromise) {
initializationPromise = (async () => {
const imported = await import("../vendor/blender/web_engine.js") as unknown as { default: WasmFactory };
const factory = imported.default;
const binary = await fetch("/vendor/blender/web_engine.wasm?v=2").then((response) => {
if (!response.ok) throw new Error(`web_engine.wasm request failed: ${response.status}`);
return response.arrayBuffer();
});
const initializedModule = await factory({ wasmBinary: binary });
const initializedHandle = initializedModule._web_engine_create();
if (initializedHandle <= 0) throw new Error("WebEngine handle creation failed");
wasmFactory = factory;
wasmBinary = binary;
module = initializedModule;
handle = initializedHandle;
return status();
})().catch((error: unknown) => {
module = null;
handle = 0;
throw error;
}).finally(() => {
initializationPromise = null;
});
wasmFactory = factory;
wasmBinary = binary;
module = await factory({ wasmBinary: binary });
handle = module._web_engine_create();
if (handle <= 0) throw new Error("WebEngine handle creation failed");
}
return status();
return initializationPromise;
}
function copyIntoWasm(buffer: ArrayBuffer): { pointer: number; length: number } {