import type { ErrorCode } from "./error"; export const RENDER_ROUTING_SCHEMA_VERSION = 1 as const; export type RenderRoutingBackend = | "WEBGL2" | "WEBGPU" | "CYCLES" | "EEVEE_COMPLEX" | "CUDA" | "OPTIX" | "HIP" | "METAL" | "ONEAPI"; export type RenderRoutingEngine = "BLENDER_EEVEE" | "BLENDER_EEVEE_NEXT" | "BLENDER_CYCLES" | "BLENDER_WORKBENCH"; export type RenderRoutingTarget = "WEB_LOCAL_BOUNDED" | "SERVER_JOB"; export interface RenderRoutingRequestIR { schemaVersion: typeof RENDER_ROUTING_SCHEMA_VERSION; renderEngine: string; backend: RenderRoutingBackend; complexity: "BOUNDED" | "COMPLEX"; hardwareBackend?: "NONE" | "CUDA" | "OPTIX" | "HIP" | "METAL" | "ONEAPI"; } export interface RenderRoutingContextIR { webgpuAvailable?: boolean; webgpuRendererBundled?: boolean; serverRenderAvailable?: boolean; } export interface RenderRoutingResultIR { schemaVersion: typeof RENDER_ROUTING_SCHEMA_VERSION; target: RenderRoutingTarget; status: "READY" | "BLOCKED"; capability: "WEB_REALTIME_BOUNDED" | "CYCLES_SERVER_RENDER" | "COMPLEX_EEVEE_SERVER_RENDER" | "HARDWARE_SERVER_RENDER" | "WORKBENCH_SERVER_RENDER" | "UNSUPPORTED_RENDER_ENGINE"; reason: "BOUNDED_EEVEE" | "WEBGPU_UNAVAILABLE" | "CYCLES_REQUIRES_SERVER" | "COMPLEX_EEVEE_REQUIRES_SERVER" | "HARDWARE_BACKEND_REQUIRES_SERVER" | "WORKBENCH_REQUIRES_SERVER" | "SERVER_ENDPOINT_UNAVAILABLE" | "UNSUPPORTED_ENGINE"; issues: Array<{ code: ErrorCode; message: string; recoverable: boolean }>; } export type RenderCommandKind = "renderFrame" | "renderAnimation"; export interface RenderCommandRequestIR { kind: RenderCommandKind; routing: RenderRoutingRequestIR; frame?: number; frameStart?: number; frameEnd?: number; step?: number; } function blocked( target: RenderRoutingTarget, capability: RenderRoutingResultIR["capability"], reason: RenderRoutingResultIR["reason"], code: ErrorCode, message: string, ): RenderRoutingResultIR { return { schemaVersion: RENDER_ROUTING_SCHEMA_VERSION, target, status: "BLOCKED", capability, reason, issues: [{ code, message, recoverable: true }], }; } function ready(target: RenderRoutingTarget, capability: RenderRoutingResultIR["capability"], reason: RenderRoutingResultIR["reason"]): RenderRoutingResultIR { return { schemaVersion: RENDER_ROUTING_SCHEMA_VERSION, target, status: "READY", capability, reason, issues: [] }; } function validateRequest(request: RenderRoutingRequestIR): void { if (!request || request.schemaVersion !== RENDER_ROUTING_SCHEMA_VERSION) throw new Error("INVALID_ARGUMENT: render routing schema is unsupported"); if (!Object.hasOwn({ WEBGL2: true, WEBGPU: true, CYCLES: true, EEVEE_COMPLEX: true, CUDA: true, OPTIX: true, HIP: true, METAL: true, ONEAPI: true }, request.backend)) { throw new Error("INVALID_ARGUMENT: render routing backend is unsupported"); } if (typeof request.renderEngine !== "string" || !/^[A-Z0-9_]{1,64}$/.test(request.renderEngine)) throw new Error("INVALID_ARGUMENT: render routing engine identity is invalid"); if (request.complexity !== "BOUNDED" && request.complexity !== "COMPLEX") throw new Error("INVALID_ARGUMENT: render routing complexity is unsupported"); if (request.hardwareBackend !== undefined && !Object.hasOwn({ NONE: true, CUDA: true, OPTIX: true, HIP: true, METAL: true, ONEAPI: true }, request.hardwareBackend)) { throw new Error("INVALID_ARGUMENT: render routing hardware backend is unsupported"); } } /** Routes final-render-only capabilities without claiming a local approximation is equivalent. */ export function routeRenderExecution(request: RenderRoutingRequestIR, context: RenderRoutingContextIR = {}): RenderRoutingResultIR { validateRequest(request); const hardware = request.hardwareBackend && request.hardwareBackend !== "NONE" ? request.hardwareBackend : undefined; if (hardware || ["CUDA", "OPTIX", "HIP", "METAL", "ONEAPI"].includes(request.backend)) { if (!context.serverRenderAvailable) return blocked("SERVER_JOB", "HARDWARE_SERVER_RENDER", "HARDWARE_BACKEND_REQUIRES_SERVER", "SERVER_JOB_UNAVAILABLE", `Hardware backend ${hardware ?? request.backend} requires a configured server render job`); return ready("SERVER_JOB", "HARDWARE_SERVER_RENDER", "HARDWARE_BACKEND_REQUIRES_SERVER"); } if (request.renderEngine === "BLENDER_CYCLES" || request.backend === "CYCLES") { if (!context.serverRenderAvailable) return blocked("SERVER_JOB", "CYCLES_SERVER_RENDER", "CYCLES_REQUIRES_SERVER", "SERVER_JOB_UNAVAILABLE", "Cycles final rendering requires a configured server render job"); return ready("SERVER_JOB", "CYCLES_SERVER_RENDER", "CYCLES_REQUIRES_SERVER"); } if (request.renderEngine === "BLENDER_WORKBENCH") { if (!context.serverRenderAvailable) return blocked("SERVER_JOB", "WORKBENCH_SERVER_RENDER", "WORKBENCH_REQUIRES_SERVER", "SERVER_JOB_UNAVAILABLE", "Workbench final rendering is not a Web realtime equivalent and requires a configured server render job"); return ready("SERVER_JOB", "WORKBENCH_SERVER_RENDER", "WORKBENCH_REQUIRES_SERVER"); } if (request.renderEngine === "BLENDER_EEVEE" || request.renderEngine === "BLENDER_EEVEE_NEXT") { if (request.complexity === "COMPLEX" || request.backend === "EEVEE_COMPLEX") { if (!context.serverRenderAvailable) return blocked("SERVER_JOB", "COMPLEX_EEVEE_SERVER_RENDER", "COMPLEX_EEVEE_REQUIRES_SERVER", "SERVER_JOB_UNAVAILABLE", "Complex Eevee final rendering requires a configured server render job"); return ready("SERVER_JOB", "COMPLEX_EEVEE_SERVER_RENDER", "COMPLEX_EEVEE_REQUIRES_SERVER"); } if (request.backend === "WEBGPU" && !(context.webgpuAvailable && context.webgpuRendererBundled)) { return blocked("WEB_LOCAL_BOUNDED", "WEB_REALTIME_BOUNDED", "WEBGPU_UNAVAILABLE", "WEBGPU_RENDERER_UNAVAILABLE", "The requested WebGPU realtime renderer is unavailable"); } if (request.backend !== "WEBGL2" && request.backend !== "WEBGPU") throw new Error("INVALID_ARGUMENT: bounded Eevee requires WEBGL2 or WEBGPU"); return ready("WEB_LOCAL_BOUNDED", "WEB_REALTIME_BOUNDED", "BOUNDED_EEVEE"); } return blocked("SERVER_JOB", "UNSUPPORTED_RENDER_ENGINE", "UNSUPPORTED_ENGINE", "PLATFORM_CAPABILITY_UNAVAILABLE", "The requested render engine has no declared Web or server route"); } /** Routes a user-visible frame or animation render through the declared native/server capability. */ export function routeRenderCommand(request: RenderCommandRequestIR, context: RenderRoutingContextIR = {}): RenderRoutingResultIR { if (request.kind === "renderFrame" && request.frame !== undefined && (!Number.isSafeInteger(request.frame) || request.frame < -1000000 || request.frame > 1000000)) { throw new Error("INVALID_ARGUMENT: render frame is outside the bounded range"); } if (request.kind === "renderAnimation") { if (!Number.isSafeInteger(request.frameStart) || !Number.isSafeInteger(request.frameEnd) || request.frameStart! > request.frameEnd!) { throw new Error("INVALID_ARGUMENT: render animation range is invalid"); } if (request.step !== undefined && (!Number.isSafeInteger(request.step) || request.step < 1 || request.step > 1000)) { throw new Error("INVALID_ARGUMENT: render animation step is invalid"); } } return routeRenderExecution(request.routing, context); }