Advance M8-M11 parity workflows
This commit is contained in:
@@ -2,6 +2,10 @@ import type { NanoVDBFloat32TreeLayoutIR, NanoVDBGridIR } from "../../../protoco
|
||||
|
||||
export interface NanoVDBSampleIR { value: number; active: boolean }
|
||||
|
||||
type NanoVDBNearestLocation =
|
||||
| { sample: NanoVDBSampleIR }
|
||||
| { leaf: number; voxel: number };
|
||||
|
||||
export class NanoVDBFloat32Sampler {
|
||||
private readonly view: DataView;
|
||||
private readonly layout: NanoVDBFloat32TreeLayoutIR;
|
||||
@@ -24,6 +28,20 @@ export class NanoVDBFloat32Sampler {
|
||||
}
|
||||
|
||||
nearest(coord: readonly [number, number, number]): NanoVDBSampleIR {
|
||||
const location = this.nearestLocation(coord);
|
||||
if ("sample" in location) return location.sample;
|
||||
return {
|
||||
value: this.f32(location.leaf + this.layout.leafValuesOffset + location.voxel * 4),
|
||||
active: this.mask(location.leaf + this.layout.leafValueMaskOffset, location.voxel),
|
||||
};
|
||||
}
|
||||
|
||||
leafByteOffset(coord: readonly [number, number, number]): number | null {
|
||||
const location = this.nearestLocation(coord);
|
||||
return "leaf" in location ? location.leaf : null;
|
||||
}
|
||||
|
||||
private nearestLocation(coord: readonly [number, number, number]): NanoVDBNearestLocation {
|
||||
if (coord.some((value) => !Number.isSafeInteger(value) || value < -0x8000_0000 || value > 0x7fff_ffff)) throw new Error("NANOVDB_MANIFEST_INVALID: sample coordinate is outside int32");
|
||||
const tableSize = this.u32(this.root + this.layout.rootTableSizeOffset);
|
||||
const key = this.rootKey(coord);
|
||||
@@ -39,20 +57,20 @@ export class NanoVDBFloat32Sampler {
|
||||
if (candidate > key) low = middle + 1;
|
||||
else high = middle - 1;
|
||||
}
|
||||
if (tile < 0) return { value: this.view.getFloat32(this.root + 28, true), active: false };
|
||||
if (tile < 0) return { sample: { value: this.view.getFloat32(this.root + 28, true), active: false } };
|
||||
const child = this.i64(tile + this.layout.rootTileChildOffset);
|
||||
if (child === 0n) return { value: this.f32(tile + this.layout.rootTileValueOffset), active: this.u32(tile + this.layout.rootTileStateOffset) !== 0 };
|
||||
if (child === 0n) return { sample: { value: this.f32(tile + this.layout.rootTileValueOffset), active: this.u32(tile + this.layout.rootTileStateOffset) !== 0 } };
|
||||
const upper = this.child(this.root, child, this.layout.upperNodeBytes);
|
||||
const upperOffset = (((coord[0] >>> 0 & 4095) >>> 7) << 10) | (((coord[1] >>> 0 & 4095) >>> 7) << 5) | ((coord[2] >>> 0 & 4095) >>> 7);
|
||||
const upperSample = this.internal(upper, upperOffset, this.layout.upperValueMaskOffset, this.layout.upperChildMaskOffset, this.layout.upperTableOffset, this.layout.lowerNodeBytes);
|
||||
if ("sample" in upperSample) return upperSample.sample;
|
||||
if ("sample" in upperSample) return upperSample;
|
||||
const lower = upperSample.child;
|
||||
const lowerOffset = (((coord[0] >>> 0 & 127) >>> 3) << 8) | (((coord[1] >>> 0 & 127) >>> 3) << 4) | ((coord[2] >>> 0 & 127) >>> 3);
|
||||
const lowerSample = this.internal(lower, lowerOffset, this.layout.lowerValueMaskOffset, this.layout.lowerChildMaskOffset, this.layout.lowerTableOffset, this.layout.leafNodeBytes);
|
||||
if ("sample" in lowerSample) return lowerSample.sample;
|
||||
if ("sample" in lowerSample) return lowerSample;
|
||||
const leaf = lowerSample.child;
|
||||
const voxel = ((coord[0] >>> 0 & 7) << 6) | ((coord[1] >>> 0 & 7) << 3) | (coord[2] >>> 0 & 7);
|
||||
return { value: this.f32(leaf + this.layout.leafValuesOffset + voxel * 4), active: this.mask(leaf + this.layout.leafValueMaskOffset, voxel) };
|
||||
return { leaf, voxel };
|
||||
}
|
||||
|
||||
linear(coord: readonly [number, number, number]): NanoVDBSampleIR {
|
||||
|
||||
@@ -7,6 +7,16 @@ import {
|
||||
type NanoVDBRangeIR,
|
||||
} from "../../../protocol/volume-vdb";
|
||||
import {
|
||||
dispatchNanoVDBPageFeedbackBatch,
|
||||
type NanoVDBPageFeedbackBatch,
|
||||
type NanoVDBPageFeedbackDispatchResult,
|
||||
} from "../../../protocol/nanovdb-page-feedback";
|
||||
import {
|
||||
planNanoVDBDeviceLossReplay,
|
||||
type NanoVDBDeviceLossReplayPlanIR,
|
||||
} from "../../../protocol/nanovdb-device-recovery";
|
||||
import {
|
||||
createNanoVDBFloat32GridPaged,
|
||||
NanoVDBWebGPUDeviceSession,
|
||||
probeNanoVDBWebGPU,
|
||||
renderNanoVDBFloat32WebGPU,
|
||||
@@ -267,6 +277,177 @@ export async function loadNanoVDBGridPage(
|
||||
return output.buffer;
|
||||
}
|
||||
|
||||
export interface NanoVDBFeedbackPageIR {
|
||||
pageId: number;
|
||||
renderRevision: number;
|
||||
data: ArrayBuffer;
|
||||
}
|
||||
|
||||
export interface NanoVDBDeviceLossGridReplayIR {
|
||||
grid: NanoVDBWebGPUGrid;
|
||||
plan: NanoVDBDeviceLossReplayPlanIR;
|
||||
residentBeforeReplay: readonly number[];
|
||||
}
|
||||
|
||||
export async function rebuildNanoVDBGridAfterDeviceLoss(
|
||||
device: GPUDevice,
|
||||
manifestValue: NanoVDBBundleManifestIR,
|
||||
gridName: string,
|
||||
source: NanoVDBRangeSource,
|
||||
visiblePageIds: readonly number[],
|
||||
signal: AbortSignal,
|
||||
): Promise<NanoVDBDeviceLossGridReplayIR> {
|
||||
const manifest = validateNanoVDBBundleManifest(manifestValue);
|
||||
const gridDefinition = manifest.grids.find((candidate) => candidate.name === gridName);
|
||||
if (!gridDefinition) throw new Error(`NANOVDB_MANIFEST_INVALID: grid ${gridName} is missing`);
|
||||
const rebuilt = createNanoVDBFloat32GridPaged(
|
||||
device,
|
||||
gridDefinition.byteLength,
|
||||
manifest.gpu.pageByteLength,
|
||||
manifest.gpu.maxResidentBytes,
|
||||
);
|
||||
const residentBeforeReplay = [...rebuilt.residentVirtualPages];
|
||||
const plan = planNanoVDBDeviceLossReplay(visiblePageIds, rebuilt.pageCount, rebuilt.residentPageCapacity);
|
||||
try {
|
||||
for (const pageId of plan.replayedPageIds) {
|
||||
if (signal.aborted) throw new DOMException("NanoVDB device-loss replay cancelled", "AbortError");
|
||||
const data = await loadNanoVDBGridPage(manifest, gridName, pageId, source, signal);
|
||||
rebuilt.uploadPage(pageId, data);
|
||||
}
|
||||
return { grid: rebuilt, plan, residentBeforeReplay };
|
||||
}
|
||||
catch (error) {
|
||||
rebuilt.dispose();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
interface NanoVDBPageRequestSubscriber {
|
||||
signal: AbortSignal;
|
||||
onAbort: () => void;
|
||||
resolve: (data: ArrayBuffer) => void;
|
||||
reject: (error: unknown) => void;
|
||||
}
|
||||
|
||||
interface NanoVDBPendingPageRequest {
|
||||
controller: AbortController;
|
||||
subscribers: Map<symbol, NanoVDBPageRequestSubscriber>;
|
||||
}
|
||||
|
||||
export interface NanoVDBPageRequestCoordinatorStatsIR {
|
||||
pendingPages: number;
|
||||
subscribers: number;
|
||||
pageIds: number[];
|
||||
}
|
||||
|
||||
function pageRequestCancelled(): DOMException {
|
||||
return new DOMException("NanoVDB page request cancelled", "AbortError");
|
||||
}
|
||||
|
||||
export class NanoVDBGridPageRequestCoordinator {
|
||||
private readonly manifest: NanoVDBBundleManifestIR;
|
||||
private readonly pending = new Map<number, NanoVDBPendingPageRequest>();
|
||||
private disposed = false;
|
||||
|
||||
constructor(
|
||||
manifestValue: NanoVDBBundleManifestIR,
|
||||
private readonly gridName: string,
|
||||
private readonly source: NanoVDBRangeSource,
|
||||
) {
|
||||
this.manifest = validateNanoVDBBundleManifest(manifestValue);
|
||||
if (!this.manifest.grids.some((grid) => grid.name === gridName)) {
|
||||
throw new Error(`NANOVDB_MANIFEST_INVALID: grid ${gridName} is missing`);
|
||||
}
|
||||
}
|
||||
|
||||
request(pageId: number, signal: AbortSignal): Promise<ArrayBuffer> {
|
||||
if (this.disposed || signal.aborted) return Promise.reject(pageRequestCancelled());
|
||||
let request = this.pending.get(pageId);
|
||||
if (!request) {
|
||||
request = { controller: new AbortController(), subscribers: new Map() };
|
||||
this.pending.set(pageId, request);
|
||||
const current = request;
|
||||
void loadNanoVDBGridPage(this.manifest, this.gridName, pageId, this.source, current.controller.signal).then(
|
||||
(data) => this.resolve(pageId, current, data),
|
||||
(error) => this.reject(pageId, current, error),
|
||||
);
|
||||
}
|
||||
const current = request;
|
||||
return new Promise<ArrayBuffer>((resolve, reject) => {
|
||||
const token = Symbol("NanoVDB page subscriber");
|
||||
const onAbort = (): void => {
|
||||
const subscriber = current.subscribers.get(token);
|
||||
if (!subscriber) return;
|
||||
current.subscribers.delete(token);
|
||||
signal.removeEventListener("abort", onAbort);
|
||||
reject(pageRequestCancelled());
|
||||
if (current.subscribers.size === 0) {
|
||||
if (this.pending.get(pageId) === current) this.pending.delete(pageId);
|
||||
current.controller.abort();
|
||||
}
|
||||
};
|
||||
current.subscribers.set(token, { signal, onAbort, resolve, reject });
|
||||
signal.addEventListener("abort", onAbort, { once: true });
|
||||
if (signal.aborted) onAbort();
|
||||
});
|
||||
}
|
||||
|
||||
stats(): NanoVDBPageRequestCoordinatorStatsIR {
|
||||
return {
|
||||
pendingPages: this.pending.size,
|
||||
subscribers: [...this.pending.values()].reduce((total, request) => total + request.subscribers.size, 0),
|
||||
pageIds: [...this.pending.keys()].sort((left, right) => left - right),
|
||||
};
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this.disposed) return;
|
||||
this.disposed = true;
|
||||
for (const [pageId, request] of this.pending) {
|
||||
this.pending.delete(pageId);
|
||||
request.controller.abort();
|
||||
for (const subscriber of request.subscribers.values()) {
|
||||
subscriber.signal.removeEventListener("abort", subscriber.onAbort);
|
||||
subscriber.reject(pageRequestCancelled());
|
||||
}
|
||||
request.subscribers.clear();
|
||||
}
|
||||
}
|
||||
|
||||
private resolve(pageId: number, request: NanoVDBPendingPageRequest, data: ArrayBuffer): void {
|
||||
if (this.pending.get(pageId) === request) this.pending.delete(pageId);
|
||||
for (const subscriber of request.subscribers.values()) {
|
||||
subscriber.signal.removeEventListener("abort", subscriber.onAbort);
|
||||
subscriber.resolve(data.slice(0));
|
||||
}
|
||||
request.subscribers.clear();
|
||||
}
|
||||
|
||||
private reject(pageId: number, request: NanoVDBPendingPageRequest, error: unknown): void {
|
||||
if (this.pending.get(pageId) === request) this.pending.delete(pageId);
|
||||
for (const subscriber of request.subscribers.values()) {
|
||||
subscriber.signal.removeEventListener("abort", subscriber.onAbort);
|
||||
subscriber.reject(error);
|
||||
}
|
||||
request.subscribers.clear();
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadNanoVDBFeedbackPages(
|
||||
batch: NanoVDBPageFeedbackBatch,
|
||||
currentRenderRevision: number,
|
||||
requests: NanoVDBGridPageRequestCoordinator,
|
||||
signal: AbortSignal,
|
||||
consume: (page: NanoVDBFeedbackPageIR) => Promise<void> | void,
|
||||
): Promise<NanoVDBPageFeedbackDispatchResult> {
|
||||
return dispatchNanoVDBPageFeedbackBatch(batch, currentRenderRevision, async (pageId, renderRevision) => {
|
||||
if (signal.aborted) throw new DOMException("NanoVDB feedback page load cancelled", "AbortError");
|
||||
const data = await requests.request(pageId, signal);
|
||||
if (signal.aborted) throw new DOMException("NanoVDB feedback page load cancelled", "AbortError");
|
||||
await consume({ pageId, renderRevision, data });
|
||||
});
|
||||
}
|
||||
|
||||
export function validateNanoVDBViewportAsset(value: NanoVDBViewportAssetIR): NanoVDBViewportAssetIR {
|
||||
if (!value.dataId || value.dataId.length > 256) throw new Error("NANOVDB_MANIFEST_INVALID: viewport dataId");
|
||||
const manifest = validateNanoVDBBundleManifest(value.manifest);
|
||||
|
||||
Reference in New Issue
Block a user