import { blockedGate, capabilityIssue, readyGate, type CapabilityGateResult } from "./capability-gates"; import type { ShaderNodeType } from "./shader-graph"; export type RenderBackend = "WEBGL2" | "WEBGPU"; export type PostProcessPass = "FXAA" | "BLOOM" | "SSAO" | "SSR" | "TAA" | "DOF" | "MOTION_BLUR"; export type RenderCapabilityRequest = | { kind: "ARBITRARY_SHADER"; nodeTypes: string[] } | { kind: "VOLUME" } | { kind: "SUBSURFACE" } | { kind: "WEBGPU_BACKEND" } | { kind: "POSTPROCESS"; passes: PostProcessPass[] }; export interface RenderCapabilityContext { webgpuAvailable?: boolean; webgpuRendererBundled?: boolean; supportedShaderNodes?: ReadonlySet; supportedPostProcessPasses?: ReadonlySet; volumeRendererAvailable?: boolean; subsurfaceRendererAvailable?: boolean; } const boundedShaderNodes = new Set([ "RGB", "VALUE", "MATH", "IMAGE_TEXTURE", "NORMAL_MAP", "PRINCIPLED", "MATERIAL_OUTPUT", ]); export function gateRenderCapability( request: RenderCapabilityRequest, context: RenderCapabilityContext = {}, ): CapabilityGateResult { if (request.kind === "ARBITRARY_SHADER") { const supported = context.supportedShaderNodes ?? boundedShaderNodes; const unsupported = [...new Set(request.nodeTypes.filter((type) => type === "UNSUPPORTED" || !supported.has(type as ShaderNodeType)))].sort(); if (unsupported.length === 0) return readyGate("PBR-012", "BOUNDED_SHADER_GRAPH"); return blockedGate("PBR-012", "ARBITRARY_SHADER", [ capabilityIssue("SHADER_NODE_UNSUPPORTED", `Web shader compiler does not support: ${unsupported.join(", ")}`, "nodeTypes"), ]); } if (request.kind === "VOLUME") { return context.volumeRendererAvailable ? readyGate("PBR-012", "VOLUME") : blockedGate("PBR-012", "VOLUME", [capabilityIssue("VOLUME_SHADER_UNAVAILABLE", "Volume transport is not available in the Web renderer")]); } if (request.kind === "SUBSURFACE") { return context.subsurfaceRendererAvailable ? readyGate("PBR-012", "SUBSURFACE") : blockedGate("PBR-012", "SUBSURFACE", [capabilityIssue("SUBSURFACE_SHADER_UNAVAILABLE", "Blender-compatible subsurface scattering is not available in the Web renderer")]); } if (request.kind === "WEBGPU_BACKEND") { if (context.webgpuAvailable && context.webgpuRendererBundled) return readyGate("PBR-012", "WEBGPU_BACKEND"); const reason = context.webgpuAvailable ? "The WebGPU renderer bundle is not installed" : "WebGPU is unavailable in this browser or device"; return blockedGate("PBR-012", "WEBGPU_BACKEND", [capabilityIssue("WEBGPU_RENDERER_UNAVAILABLE", reason)]); } const supported = context.supportedPostProcessPasses ?? new Set(); const unsupported = [...new Set(request.passes.filter((pass) => !supported.has(pass)))]; return unsupported.length === 0 ? readyGate("PBR-012", "POSTPROCESS") : blockedGate("PBR-012", "POSTPROCESS", [capabilityIssue("POSTPROCESS_PASS_UNAVAILABLE", `Unsupported post-process passes: ${unsupported.join(", ")}`, "passes")]); }