Advance M8-M11 parity workflows
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled

This commit is contained in:
mes123456
2026-08-17 04:37:07 -04:00
parent 7c16b279ae
commit 0fe8d2bb56
324 changed files with 31920 additions and 863 deletions

View File

@@ -1,4 +1,18 @@
import type { NanoVDBGridIR, NanoVDBMaterialIR } from "../../../protocol/volume-vdb";
import {
consumeNanoVDBProgressiveRedrawBudget,
NANOVDB_PROGRESSIVE_REDRAW_LIMIT_CODE,
NANOVDB_PROGRESSIVE_REDRAW_MAX_FRAMES,
validateNanoVDBProgressiveRedrawLimit,
} from "../../../protocol/nanovdb-progressive-redraw";
import {
createNanoVDBPageFeedbackBuffer,
nanoVDBPageFeedbackByteLength,
NANOVDB_PAGE_FEEDBACK_RECORD_WGSL,
NANOVDB_PAGE_FEEDBACK_WGSL,
parseNanoVDBPageFeedbackBatch,
type NanoVDBPageFeedbackBatch,
} from "../../../protocol/nanovdb-page-feedback";
export interface NanoVDBWebGPUCapabilityIR {
available: boolean;
@@ -23,6 +37,9 @@ export interface NanoVDBWebGPUGrid {
residentVirtualPages: readonly number[];
uploadPage(pageIndex: number, data?: ArrayBuffer): void;
touchPage(pageIndex: number): boolean;
beginFrame(): void;
pinPage(pageIndex: number): boolean;
endFrame(): void;
evictPage(pageIndex: number): void;
hasResidentPage(pageIndex: number): boolean;
dispose(): void;
@@ -44,6 +61,104 @@ export interface NanoVDBGpuPageAllocatorStatsIR {
keys: string[];
}
export interface NanoVDBProgressiveRedrawStatsIR {
pending: boolean;
scheduledCount: number;
redrawCount: number;
maxRedraws: number;
capped: boolean;
errorCode: "NANOVDB_PROGRESSIVE_REDRAW_LIMIT" | null;
}
export interface NanoVDBProgressiveRedrawOptionsIR {
maxRedraws?: number;
}
export class NanoVDBProgressiveRedrawScheduler {
private pending = false;
private scheduledCount = 0;
private redrawCount = 0;
private capped = false;
private disposed = false;
private generation = 0;
private readonly maxRedraws: number;
constructor(
private readonly requestFrame: (callback: () => void) => void,
private readonly redraw: () => void,
options: NanoVDBProgressiveRedrawOptionsIR = {},
) {
const maxRedraws = options.maxRedraws ?? NANOVDB_PROGRESSIVE_REDRAW_MAX_FRAMES;
this.maxRedraws = validateNanoVDBProgressiveRedrawLimit(maxRedraws);
}
schedule(): boolean {
if (this.disposed || this.pending || this.capped) return false;
const budget = consumeNanoVDBProgressiveRedrawBudget(this.redrawCount, this.maxRedraws);
if (!budget.allowed) {
this.capped = budget.capped;
return false;
}
this.pending = true;
const generation = this.generation;
try {
this.requestFrame(() => {
if (this.disposed || generation !== this.generation || !this.pending) return;
this.pending = false;
const consumed = consumeNanoVDBProgressiveRedrawBudget(this.redrawCount, this.maxRedraws);
this.redrawCount = consumed.redrawCount;
this.capped = consumed.capped;
this.redraw();
});
this.scheduledCount++;
return true;
}
catch (error) {
this.pending = false;
throw error;
}
}
stats(): NanoVDBProgressiveRedrawStatsIR {
return {
pending: this.pending,
scheduledCount: this.scheduledCount,
redrawCount: this.redrawCount,
maxRedraws: this.maxRedraws,
capped: this.capped,
errorCode: this.capped ? NANOVDB_PROGRESSIVE_REDRAW_LIMIT_CODE : null,
};
}
/** Starts a new render epoch and invalidates callbacks queued by the old epoch. */
beginRender(): void {
if (this.disposed) return;
this.generation++;
this.pending = false;
this.scheduledCount = 0;
this.redrawCount = 0;
this.capped = false;
}
dispose(): void {
this.disposed = true;
this.generation++;
this.pending = false;
}
}
export class NanoVDBProgressivePageUploader {
constructor(
private readonly grid: NanoVDBWebGPUGrid,
private readonly redraw: NanoVDBProgressiveRedrawScheduler,
) {}
upload(pageId: number, data: ArrayBuffer): { pageId: number; redrawScheduled: boolean } {
this.grid.uploadPage(pageId, data);
return { pageId, redrawScheduled: this.redraw.schedule() };
}
}
export interface NanoVDBDeviceLossIR { reason?: string; message: string }
const traversalWGSL = /* wgsl */`
@@ -57,7 +172,7 @@ fn word(byte_offset: u32) -> u32 {
let page = byte_offset / params.page_bytes;
if (page >= params.page_count) { return 0u; }
let slot = page_table[page];
if (slot == 0xffffffffu || slot >= params.resident_pages) { return 0u; }
if (slot == 0xffffffffu || slot >= params.resident_pages) { /* NANOVDB_PAGE_FAULT */ return 0u; }
let physical = slot * params.page_bytes + (byte_offset % params.page_bytes);
if (physical > params.resident_pages * params.page_bytes - 4u) { return 0u; }
return grid[physical >> 2u];
@@ -148,6 +263,11 @@ fn sample_density_linear(position: vec3<f32>) -> vec2<f32> {
}
`;
const pageFeedbackTraversalWGSL = traversalWGSL.replaceAll(
"/* NANOVDB_PAGE_FAULT */",
"nanovdb_record_page_fault(page);",
);
function specializeFloatTraversal(prefix: string, gridName: string, pageTableName: string, parameterPrefix: string): string {
let source = traversalWGSL
.replaceAll("grid[", `${gridName}[`)
@@ -255,9 +375,10 @@ export function uploadNanoVDBFloat32Grid(device: GPUDevice, payload: ArrayBuffer
const buffer = device.createBuffer({ label: "NanoVDB Float32 grid", size: payload.byteLength, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, mappedAtCreation: true });
new Uint8Array(buffer.getMappedRange()).set(new Uint8Array(payload));
buffer.unmap();
const pageTable = device.createBuffer({ label: "NanoVDB direct page table", size: 4, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, mappedAtCreation: true });
const pageTable = device.createBuffer({ label: "NanoVDB direct page table", size: 4, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC, mappedAtCreation: true });
new Uint32Array(pageTable.getMappedRange())[0] = 0;
pageTable.unmap();
let disposed = false;
return {
buffer,
pageTable,
@@ -275,9 +396,17 @@ export function uploadNanoVDBFloat32Grid(device: GPUDevice, payload: ArrayBuffer
if (pageIndex !== 0 || (data && data.byteLength !== payload.byteLength)) throw new Error("NANOVDB_STREAM_INCOMPLETE: direct NanoVDB grid has one immutable page");
},
touchPage: (pageIndex) => pageIndex === 0,
beginFrame: () => undefined,
pinPage: (pageIndex) => pageIndex === 0,
endFrame: () => undefined,
evictPage: () => { throw new Error("NANOVDB_GPU_BUDGET_EXCEEDED: direct NanoVDB grid cannot evict its only page"); },
hasResidentPage: (pageIndex) => pageIndex === 0,
dispose: () => { buffer.destroy(); pageTable.destroy(); },
dispose: () => {
if (disposed) return;
disposed = true;
buffer.destroy();
pageTable.destroy();
},
};
}
@@ -322,7 +451,7 @@ export function createNanoVDBFloat32GridPaged(
const pageTableBytes = Math.max(4, pageCount * 4);
try {
buffer = device.createBuffer({ label: "NanoVDB paged Float32 grid", size: physicalBytes, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
pageTable = device.createBuffer({ label: "NanoVDB page table", size: pageTableBytes, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, mappedAtCreation: true });
pageTable = device.createBuffer({ label: "NanoVDB page table", size: pageTableBytes, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC, mappedAtCreation: true });
new Uint32Array(pageTable.getMappedRange()).fill(0xffffffff);
pageTable.unmap();
}
@@ -335,6 +464,9 @@ export function createNanoVDBFloat32GridPaged(
const lastUsed = new Map<number, number>();
let clock = 0;
let evictions = 0;
let frameActive = false;
let disposed = false;
const framePins = new Set<number>();
const writePageTable = (pageIndex: number, slot: number): void => {
device.queue.writeBuffer(pageTable, pageIndex * 4, new Uint32Array([slot]));
};
@@ -356,8 +488,12 @@ export function createNanoVDBFloat32GridPaged(
const existingSlot = resident.get(pageIndex);
let slot = existingSlot ?? [...Array(residentPageCount).keys()].find((candidate) => !residentHasSlot(candidate));
if (slot === undefined) {
const oldest = [...lastUsed].sort((left, right) => left[1] - right[1] || left[0] - right[0])[0];
if (!oldest) throw new Error("NANOVDB_GPU_BUDGET_EXCEEDED: no resident NanoVDB page slot is available");
const oldest = [...lastUsed]
.filter(([candidate]) => !framePins.has(candidate))
.sort((left, right) => left[1] - right[1] || left[0] - right[0])[0];
if (!oldest) throw new Error(frameActive
? "NANOVDB_GPU_BUDGET_EXCEEDED: all resident NanoVDB pages are pinned"
: "NANOVDB_GPU_BUDGET_EXCEEDED: no resident NanoVDB page slot is available");
slot = resident.get(oldest[0]);
evict(oldest[0], true);
}
@@ -386,9 +522,25 @@ export function createNanoVDBFloat32GridPaged(
lastUsed.set(pageIndex, ++clock);
return true;
},
beginFrame: () => { frameActive = true; framePins.clear(); },
pinPage: (pageIndex) => {
if (!frameActive || !resident.has(pageIndex)) return false;
framePins.add(pageIndex);
return true;
},
endFrame: () => { frameActive = false; framePins.clear(); },
evictPage: evict,
hasResidentPage: (pageIndex) => resident.has(pageIndex),
dispose: () => { resident.clear(); lastUsed.clear(); buffer.destroy(); pageTable.destroy(); },
dispose: () => {
if (disposed) return;
disposed = true;
frameActive = false;
framePins.clear();
resident.clear();
lastUsed.clear();
buffer.destroy();
pageTable.destroy();
},
};
}
@@ -499,7 +651,97 @@ function paramsBuffer(device: GPUDevice, values: Uint32Array): GPUBuffer {
return buffer;
}
export async function readNanoVDBWordsWebGPU(device: GPUDevice, uploaded: NanoVDBWebGPUGrid, byteOffsets: readonly number[]): Promise<number[]> {
export function createNanoVDBPageFeedbackGPUBuffer(device: GPUDevice, capacity = 1024): GPUBuffer {
const initial = createNanoVDBPageFeedbackBuffer(capacity);
let buffer: GPUBuffer | undefined;
try {
buffer = device.createBuffer({
label: "NanoVDB page feedback",
size: initial.byteLength,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC,
});
device.queue.writeBuffer(buffer, 0, initial);
return buffer;
}
catch (error) {
buffer?.destroy();
throw error;
}
}
export interface NanoVDBPagedRenderResources {
grid: NanoVDBWebGPUGrid;
feedbackBuffer: GPUBuffer;
feedbackCapacity: number;
dispose(): void;
}
export function createNanoVDBPagedRenderResources(
device: GPUDevice,
byteLength: number,
pageByteLength: number,
maxResidentBytes: number,
feedbackCapacity = 1024,
): NanoVDBPagedRenderResources {
let grid: NanoVDBWebGPUGrid | undefined;
let feedbackBuffer: GPUBuffer | undefined;
try {
grid = createNanoVDBFloat32GridPaged(device, byteLength, pageByteLength, maxResidentBytes);
feedbackBuffer = createNanoVDBPageFeedbackGPUBuffer(device, feedbackCapacity);
}
catch (error) {
feedbackBuffer?.destroy();
grid?.dispose();
throw error;
}
let disposed = false;
return {
grid,
feedbackBuffer,
feedbackCapacity,
dispose: () => {
if (disposed) return;
disposed = true;
feedbackBuffer.destroy();
grid.dispose();
},
};
}
export async function readNanoVDBPageFeedbackGPUBuffer(
device: GPUDevice,
feedback: GPUBuffer,
capacity: number,
pageCount: number,
renderRevision: number,
): Promise<NanoVDBPageFeedbackBatch> {
const byteLength = nanoVDBPageFeedbackByteLength(capacity);
const readback = device.createBuffer({
label: `NanoVDB page feedback readback revision ${renderRevision}`,
size: byteLength,
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
});
const encoder = device.createCommandEncoder();
encoder.copyBufferToBuffer(feedback, 0, readback, 0, byteLength);
device.queue.submit([encoder.finish()]);
await readback.mapAsync(GPUMapMode.READ);
const buffer = readback.getMappedRange().slice(0);
readback.unmap();
readback.destroy();
return parseNanoVDBPageFeedbackBatch(buffer, pageCount, renderRevision);
}
function resolveNanoVDBPageFeedbackGPUBuffer(device: GPUDevice, value: GPUBuffer | undefined): { buffer: GPUBuffer; owned: boolean } {
if (value) return { buffer: value, owned: false };
return { buffer: createNanoVDBPageFeedbackGPUBuffer(device, 1), owned: true };
}
export async function readNanoVDBWordsWebGPU(
device: GPUDevice,
uploaded: NanoVDBWebGPUGrid,
byteOffsets: readonly number[],
pageFeedback?: GPUBuffer,
): Promise<number[]> {
if (byteOffsets.length < 1 || byteOffsets.length > 4096 || byteOffsets.some((offset) => !Number.isSafeInteger(offset) || offset < 0 || offset > uploaded.byteLength - 4 || offset % 4 !== 0)) {
throw new Error("NANOVDB_GPU_BUDGET_EXCEEDED: paged word read offsets");
}
@@ -510,6 +752,7 @@ export async function readNanoVDBWordsWebGPU(device: GPUDevice, uploaded: NanoVD
const resultBuffer = device.createBuffer({ size: resultBytes, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC });
const readback = device.createBuffer({ size: resultBytes, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ });
const params = paramsBuffer(device, new Uint32Array([uploaded.byteLength, byteOffsets.length, 0, 0, uploaded.pageByteLength, uploaded.pageCount, uploaded.residentPageCapacity, uploaded.paged ? 1 : 0]));
const feedback = resolveNanoVDBPageFeedbackGPUBuffer(device, pageFeedback);
const module = device.createShaderModule({ label: "NanoVDB paged word reader", code: /* wgsl */`
struct Params { data_bytes: u32, count: u32, width: u32, height: u32, page_bytes: u32, page_count: u32, resident_pages: u32, paged: u32 }
@group(0) @binding(0) var<storage, read> grid: array<u32>;
@@ -517,7 +760,10 @@ struct Params { data_bytes: u32, count: u32, width: u32, height: u32, page_bytes
@group(0) @binding(2) var<storage, read_write> results: array<u32>;
@group(0) @binding(3) var<uniform> params: Params;
@group(0) @binding(4) var<storage, read> page_table: array<u32>;
${traversalWGSL}
${NANOVDB_PAGE_FEEDBACK_WGSL}
@group(0) @binding(5) var<storage, read_write> nanovdb_page_feedback: NanoVDBPageFeedback;
${NANOVDB_PAGE_FEEDBACK_RECORD_WGSL}
${pageFeedbackTraversalWGSL}
@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) id: vec3<u32>) {
if (id.x < params.count) { results[id.x] = word(offsets[id.x]); }
@@ -527,6 +773,7 @@ fn main(@builtin(global_invocation_id) id: vec3<u32>) {
{ binding: 0, resource: { buffer: uploaded.buffer } }, { binding: 1, resource: { buffer: offsetBuffer } },
{ binding: 2, resource: { buffer: resultBuffer } }, { binding: 3, resource: { buffer: params } },
{ binding: 4, resource: { buffer: uploaded.pageTable } },
{ binding: 5, resource: { buffer: feedback.buffer } },
] });
const encoder = device.createCommandEncoder();
const pass = encoder.beginComputePass();
@@ -537,10 +784,16 @@ fn main(@builtin(global_invocation_id) id: vec3<u32>) {
const result = [...new Uint32Array(readback.getMappedRange().slice(0))];
readback.unmap();
offsetBuffer.destroy(); resultBuffer.destroy(); readback.destroy(); params.destroy();
if (feedback.owned) feedback.buffer.destroy();
return result;
}
export async function sampleNanoVDBFloat32WebGPU(device: GPUDevice, uploaded: NanoVDBWebGPUGrid, coordinates: Array<readonly [number, number, number]>): Promise<Array<{ value: number; active: boolean; valid: boolean }>> {
export async function sampleNanoVDBFloat32WebGPU(
device: GPUDevice,
uploaded: NanoVDBWebGPUGrid,
coordinates: Array<readonly [number, number, number]>,
pageFeedback?: GPUBuffer,
): Promise<Array<{ value: number; active: boolean; valid: boolean }>> {
if (coordinates.length < 1 || coordinates.length > 4096) throw new Error("NANOVDB_GPU_BUDGET_EXCEEDED: sample count");
const coordinateData = new Int32Array(coordinates.length * 4);
coordinates.forEach((coord, index) => coordinateData.set(coord, index * 4));
@@ -550,6 +803,7 @@ export async function sampleNanoVDBFloat32WebGPU(device: GPUDevice, uploaded: Na
const resultBuffer = device.createBuffer({ size: resultBytes, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC });
const readback = device.createBuffer({ size: resultBytes, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ });
const params = paramsBuffer(device, new Uint32Array([uploaded.byteLength, coordinates.length, 0, 0, uploaded.pageByteLength, uploaded.pageCount, uploaded.residentPageCapacity, uploaded.paged ? 1 : 0]));
const feedback = resolveNanoVDBPageFeedbackGPUBuffer(device, pageFeedback);
const module = device.createShaderModule({ label: "NanoVDB Float32 sampler", code: /* wgsl */`
struct Params { data_bytes: u32, count: u32, width: u32, height: u32, page_bytes: u32, page_count: u32, resident_pages: u32, paged: u32 }
@group(0) @binding(0) var<storage, read> grid: array<u32>;
@@ -557,7 +811,10 @@ struct Params { data_bytes: u32, count: u32, width: u32, height: u32, page_bytes
@group(0) @binding(2) var<storage, read_write> results: array<vec4<f32>>;
@group(0) @binding(3) var<uniform> params: Params;
@group(0) @binding(4) var<storage, read> page_table: array<u32>;
${traversalWGSL}
${NANOVDB_PAGE_FEEDBACK_WGSL}
@group(0) @binding(5) var<storage, read_write> nanovdb_page_feedback: NanoVDBPageFeedback;
${NANOVDB_PAGE_FEEDBACK_RECORD_WGSL}
${pageFeedbackTraversalWGSL}
@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) id: vec3<u32>) {
if (id.x >= params.count) { return; }
@@ -569,6 +826,7 @@ fn main(@builtin(global_invocation_id) id: vec3<u32>) {
{ binding: 0, resource: { buffer: uploaded.buffer } }, { binding: 1, resource: { buffer: coordinateBuffer } },
{ binding: 2, resource: { buffer: resultBuffer } }, { binding: 3, resource: { buffer: params } },
{ binding: 4, resource: { buffer: uploaded.pageTable } },
{ binding: 5, resource: { buffer: feedback.buffer } },
] });
const encoder = device.createCommandEncoder();
const pass = encoder.beginComputePass();
@@ -579,6 +837,7 @@ fn main(@builtin(global_invocation_id) id: vec3<u32>) {
const values = new Float32Array(readback.getMappedRange().slice(0));
readback.unmap();
coordinateBuffer.destroy(); resultBuffer.destroy(); readback.destroy(); params.destroy();
if (feedback.owned) feedback.buffer.destroy();
return coordinates.map((_coord, index) => ({ value: values[index * 4], active: values[index * 4 + 1] > 0.5, valid: values[index * 4 + 1] >= 0 }));
}