Advance M8-M11 parity workflows
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
export {
|
||||
COMPOSITOR_WEBGPU_NODE_ALLOWLIST,
|
||||
CompositorFrameCache,
|
||||
CompositorValidationError,
|
||||
compileCompositorWebGPUPlan,
|
||||
compositorFrameCacheKey,
|
||||
executeCompositorGraph,
|
||||
executeCompositorGraphCached,
|
||||
@@ -12,4 +14,6 @@ export type {
|
||||
CompositorExecutionResult,
|
||||
CompositorGraphIR,
|
||||
CompositorImageBuffer,
|
||||
CompositorWebGPUInstructionIR,
|
||||
CompositorWebGPUPlanIR,
|
||||
} from "../../../protocol/compositor";
|
||||
|
||||
83
web/app/src/compositor/CompositorWebGPU.ts
Normal file
83
web/app/src/compositor/CompositorWebGPU.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import {
|
||||
COMPOSITOR_BUDGET,
|
||||
CompositorValidationError,
|
||||
compileCompositorWebGPUPlan,
|
||||
type CompositorImageBuffer,
|
||||
type CompositorWebGPUInstructionIR,
|
||||
} from "../../../protocol/compositor";
|
||||
|
||||
function wgslFloat(value: number): string {
|
||||
if (!Number.isFinite(value)) throw new CompositorValidationError("COMPOSITOR_GRAPH_INVALID", "WebGPU compositor constant is not finite");
|
||||
const text = String(Math.fround(value));
|
||||
return text.includes(".") || /e/i.test(text) ? text : `${text}.0`;
|
||||
}
|
||||
|
||||
function instructionWGSL(instruction: CompositorWebGPUInstructionIR): string {
|
||||
if (instruction.type === "CONSTANT_COLOR") {
|
||||
return `color = vec4<f32>(${instruction.color.map(wgslFloat).join(", ")});`;
|
||||
}
|
||||
if (instruction.type === "EXPOSURE") {
|
||||
return `color = vec4<f32>(color.rgb * ${wgslFloat(2 ** instruction.exposure)}, color.a);`;
|
||||
}
|
||||
if (instruction.type === "INVERT") return "color = vec4<f32>(vec3<f32>(1.0) - color.rgb, color.a);";
|
||||
return "";
|
||||
}
|
||||
|
||||
export async function requestCompositorWebGPUDevice(): Promise<GPUDevice> {
|
||||
if (!navigator.gpu) throw new CompositorValidationError("WEBGPU_RENDERER_UNAVAILABLE", "WebGPU is unavailable for the compositor allowlist");
|
||||
const adapter = await navigator.gpu.requestAdapter({ powerPreference: "high-performance" });
|
||||
if (!adapter) throw new CompositorValidationError("WEBGPU_RENDERER_UNAVAILABLE", "No WebGPU adapter is available for the compositor allowlist");
|
||||
return adapter.requestDevice();
|
||||
}
|
||||
|
||||
export async function executeCompositorGraphWebGPU(
|
||||
device: GPUDevice,
|
||||
value: unknown,
|
||||
width: number,
|
||||
height: number,
|
||||
): Promise<CompositorImageBuffer> {
|
||||
const plan = compileCompositorWebGPUPlan(value);
|
||||
const pixelCount = width * height;
|
||||
const byteLength = pixelCount * 16;
|
||||
if (!Number.isSafeInteger(width) || !Number.isSafeInteger(height) || width < 1 || height < 1 ||
|
||||
width > COMPOSITOR_BUDGET.maxDimension || height > COMPOSITOR_BUDGET.maxDimension ||
|
||||
!Number.isSafeInteger(pixelCount) || pixelCount > COMPOSITOR_BUDGET.maxPixels ||
|
||||
byteLength > COMPOSITOR_BUDGET.maxImageBytes || byteLength > device.limits.maxStorageBufferBindingSize ||
|
||||
byteLength > device.limits.maxBufferSize) {
|
||||
throw new CompositorValidationError("COMPOSITOR_BUDGET_EXCEEDED", "WebGPU compositor output exceeds the image or device budget");
|
||||
}
|
||||
const output = device.createBuffer({ label: "Compositor WebGPU output", size: byteLength, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC });
|
||||
const readback = device.createBuffer({ label: "Compositor WebGPU readback", size: byteLength, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ });
|
||||
try {
|
||||
device.pushErrorScope("validation");
|
||||
const module = device.createShaderModule({ label: `Compositor ${plan.graphId}`, code: /* wgsl */`
|
||||
@group(0) @binding(0) var<storage, read_write> pixels: array<vec4<f32>>;
|
||||
@compute @workgroup_size(64)
|
||||
fn main(@builtin(global_invocation_id) id: vec3<u32>) {
|
||||
if (id.x >= ${pixelCount}u) { return; }
|
||||
var color = vec4<f32>(0.0);
|
||||
${plan.instructions.map(instructionWGSL).filter(Boolean).join("\n ")}
|
||||
pixels[id.x] = color;
|
||||
}` });
|
||||
const pipeline = device.createComputePipeline({ layout: "auto", compute: { module, entryPoint: "main" } });
|
||||
const bindGroup = device.createBindGroup({ layout: pipeline.getBindGroupLayout(0), entries: [{ binding: 0, resource: { buffer: output } }] });
|
||||
const encoder = device.createCommandEncoder();
|
||||
const pass = encoder.beginComputePass();
|
||||
pass.setPipeline(pipeline);
|
||||
pass.setBindGroup(0, bindGroup);
|
||||
pass.dispatchWorkgroups(Math.ceil(pixelCount / 64));
|
||||
pass.end();
|
||||
encoder.copyBufferToBuffer(output, 0, readback, 0, byteLength);
|
||||
device.queue.submit([encoder.finish()]);
|
||||
await readback.mapAsync(GPUMapMode.READ);
|
||||
const data = new Float32Array(readback.getMappedRange().slice(0));
|
||||
readback.unmap();
|
||||
const validationError = await device.popErrorScope();
|
||||
if (validationError) throw new CompositorValidationError("COMPOSITOR_NODE_UNSUPPORTED", `WebGPU compositor shader validation failed: ${validationError.message ?? "unknown error"}`);
|
||||
return { width, height, data, colorSpace: "LINEAR_SRGB" };
|
||||
}
|
||||
finally {
|
||||
output.destroy();
|
||||
readback.destroy();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user