import { Bounds3 as SceneBounds3, Camera, DirectionalLight, Light, PointLight, Scene, SpotLight, identityMat4, invertMat4, multiplyMat4, orthographicMat4, perspectiveMat4, transformPoint } from "../scene/index.js";
import { createRenderDevice } from "./RenderBackend.js";
import { RenderDeviceError } from "./RenderDevice.js";
import { ENVIRONMENT_BACKGROUND_COLOR_RESOURCE, EnvironmentBackgroundPass } from "./EnvironmentBackgroundPass.js";
import { ForwardPass } from "./ForwardPass.js";
import { LightCollector } from "./LightCollector.js";
import { Geometry } from "./Geometry.js";
import {} from "./MorphTarget.js";
import { computeSkinnedMorphTargetWeightedBounds } from "./SkinningBounds.js";
import { RenderGraph } from "./RenderGraph.js";
import { BloomPass, FXAAPass, ToneMappingPass, bloomFloatPixels, bloomPixels, chromaticAberrationPixels, colorGradePixels, contactShadowPixels, createDepthTextureBinding, depthOfFieldPixels, filmGrainPixels, fusedLdrPostprocessPixels, fxaaPixels, motionBlurPixels, outlinePixels, ssaoPixels, ssrPixels, taaPixels, toneMapFloatPixels, toneMapPixels, writePostProcessPixels } from "./PostProcessPass.js";
import { createRendererPostprocessPasses, createRendererPostprocessPlanDiagnostics } from "./RendererPostprocessPlan.js";
import { createDefaultShaderLibrary } from "./ShaderLibrary.js";
import { ShadowMap } from "./ShadowMap.js";
import { ShadowPass } from "./ShadowPass.js";
import { Sampler } from "./Sampler.js";
import { TextureBinding } from "./TextureBinding.js";
import { computePerspectiveCameraFrame } from "./CameraFraming.js";
import { assertRendererFeatures, createRendererFeatureReport } from "./RendererFeatureGates.js";
import { batchStaticRenderItems } from "./SceneOptimization.js";
export const DEFAULT_RENDERER_AUTO_FRAME_OPTIONS = {
    paddingRatio: 0.14,
    yawRadians: -0.38,
    pitchRadians: -0.18,
    nearPadding: 0.2,
    farPadding: 2.4
};
export const DEFAULT_RENDERER_ENVIRONMENT_LIGHTING = {
    color: [0.78, 0.8, 0.84],
    intensity: 0.42,
    proceduralMap: {
        skyColor: [0.64, 0.76, 0.94],
        horizonColor: [0.94, 0.82, 0.62],
        groundColor: [0.12, 0.13, 0.15],
        specularColor: [1, 0.94, 0.78],
        intensity: 0.5,
        specularIntensity: 0.82
    }
};
const DISABLED_RENDERER_ENVIRONMENT_LIGHTING = {
    color: [0, 0, 0],
    intensity: 0,
    proceduralMap: {
        skyColor: [0, 0, 0],
        horizonColor: [0, 0, 0],
        groundColor: [0, 0, 0],
        specularColor: [0, 0, 0],
        intensity: 0,
        specularIntensity: 0
    },
    environmentMapIntensity: 0,
    environmentMapSpecularIntensity: 0
};
export const DEFAULT_RENDERER_DIRECT_LIGHTING = {
    key: {
        color: [1, 0.92, 0.78],
        intensity: 2.25,
        direction: [0.42, -0.58, -0.7]
    },
    fill: {
        color: [0.48, 0.62, 0.9],
        intensity: 0.55,
        direction: [-0.35, -0.22, -0.91]
    }
};
export class Renderer {
    device;
    graph = new RenderGraph();
    shaderLibrary;
    canvas;
    width;
    height;
    clearColor;
    disposed = false;
    animationLoop = null;
    fusedLdrPostprocessScratch = {};
    constructor(device, options) {
        this.device = device;
        this.canvas = options.canvas;
        this.width = options.width ?? inferInitialCanvasDimension(options.canvas, "width");
        this.height = options.height ?? inferInitialCanvasDimension(options.canvas, "height");
        this.clearColor = options.clearColor ?? [0, 0, 0, 1];
        this.shaderLibrary = options.shaderLibrary ?? createDefaultShaderLibrary();
        this.resizeCanvas(this.width, this.height);
    }
    static async create(options = {}) {
        const device = await createRenderDevice(options);
        if (options.requiredFeatures && options.requiredFeatures.length > 0) {
            assertRendererFeatures(device, options.requiredFeatures);
        }
        return new Renderer(device, options);
    }
    getFeatureReport() {
        return createRendererFeatureReport(this.device);
    }
    resize(width, height) {
        this.assertAlive();
        if (width <= 0 || height <= 0 || !Number.isInteger(width) || !Number.isInteger(height)) {
            throw new RenderDeviceError("Renderer dimensions must be positive integers", "INVALID_FRAME_SIZE", { width, height });
        }
        this.width = width;
        this.height = height;
        this.resizeCanvas(width, height);
    }
    resizeToDisplay(options = {}) {
        this.assertAlive();
        if (!this.canvas) {
            throw new RenderDeviceError("resizeToDisplay requires a canvas-backed renderer", "CANVAS_REQUIRED");
        }
        const cssWidth = options.cssWidth ?? readCanvasCssSize(this.canvas, "width");
        const cssHeight = options.cssHeight ?? readCanvasCssSize(this.canvas, "height");
        const devicePixelRatio = options.devicePixelRatio ?? globalThis.devicePixelRatio ?? 1;
        if (![cssWidth, cssHeight, devicePixelRatio].every(Number.isFinite) || cssWidth <= 0 || cssHeight <= 0 || devicePixelRatio <= 0) {
            throw new RenderDeviceError("Display size and DPR must be finite positive values", "INVALID_DISPLAY_SIZE", {
                cssWidth,
                cssHeight,
                devicePixelRatio
            });
        }
        const width = Math.max(1, Math.round(cssWidth * devicePixelRatio));
        const height = Math.max(1, Math.round(cssHeight * devicePixelRatio));
        const resized = width !== this.width || height !== this.height;
        if (resized) {
            this.resize(width, height);
        }
        return { resized, cssWidth, cssHeight, devicePixelRatio, width, height };
    }
    startAnimationLoop(callback) {
        this.assertAlive();
        this.animationLoop?.stop();
        const loop = new RendererAnimationLoopImpl(this, callback);
        this.animationLoop = loop;
        loop.start();
        return loop;
    }
    render(sourceOrInput, camera) {
        this.assertAlive();
        const { source, camera: inputCamera } = normalizeRendererInput(sourceOrInput, camera);
        sceneFromSource(source)?.updateWorldTransforms();
        const cameraPolicy = collectCameraPolicy(source);
        const identityPolicyIgnoresSceneCamera = inputCamera === undefined && cameraPolicy === "identity";
        const autoFrameOverridesSceneCamera = inputCamera === undefined && hasExplicitAutoFrameCameraPolicy(source);
        const resolvedCamera = resolveCamera(source, inputCamera, { width: this.width, height: this.height }, {
            allowSceneCamera: !identityPolicyIgnoresSceneCamera && !autoFrameOverridesSceneCamera
        });
        if (!resolvedCamera && cameraPolicy === "require") {
            throw new RenderDeviceError("RenderSource requires an explicit camera but none was supplied or found in the scene.", "CAMERA_REQUIRED");
        }
        let cameraViewProjection = resolvedCamera?.viewProjectionMatrix;
        const collectedItems = collectRenderItemsWithDiagnostics(source, cameraViewProjection, resolvedCamera?.camera);
        const collectionDiagnostics = collectedItems.diagnostics;
        let items = collectedItems.items;
        const lights = collectRenderLights(source);
        const environmentBackground = collectEnvironmentBackground(source);
        const environmentLighting = collectEnvironmentLighting(source);
        const environmentFog = collectEnvironmentFog(source);
        const explicitShadowMap = collectForwardShadowMap(source);
        const shadowOptions = collectRendererShadowOptions(source);
        const sourceCameraPosition = collectSourceCameraPosition(source);
        const explicitRenderTarget = collectRenderTarget(source);
        validateExplicitRenderTarget(explicitRenderTarget, this.width, this.height);
        let cameraPosition = sourceCameraPosition ?? resolvedCamera?.cameraPosition;
        if (!resolvedCamera && cameraPolicy === "auto-frame") {
            const autoFrame = createAutoFrameCamera(source, items, this.width, this.height);
            if (autoFrame) {
                cameraViewProjection = autoFrame.viewProjectionMatrix;
                items = applyViewProjection(items, autoFrame.viewProjectionMatrix);
                cameraPosition = sourceCameraPosition ?? autoFrame.cameraPosition;
            }
        }
        const postprocess = collectPostprocess(source);
        const ownedTargets = [];
        const ownedShadowPasses = [];
        this.graph.clear();
        let postprocessTargetFormat;
        if (postprocess) {
            const format = postprocess.targetFormat ?? defaultPostprocessTargetFormat(this.device, postprocess);
            postprocessTargetFormat = format;
            if ((format === "rgba16f" || format === "rgba32f") && !this.device.info.capabilities?.includes("hdr-render-targets")) {
                throw new RenderDeviceError("Renderer HDR postprocess requires an HDR-capable backend", "HDR_POSTPROCESS_UNSUPPORTED", {
                    backend: this.device.kind,
                    format
                });
            }
            if ((format === "rgba16f" || format === "rgba32f") && postprocess.toneMapping === false) {
                throw new RenderDeviceError("Renderer HDR postprocess requires tone mapping before presentation or LDR postprocess passes", "HDR_POSTPROCESS_TONEMAPPING_REQUIRED", {
                    backend: this.device.kind,
                    format
                });
            }
            if (!this.device.presentRenderTarget) {
                throw new RenderDeviceError("Renderer postprocess requires a backend presentation path", "POSTPROCESS_PRESENT_UNSUPPORTED", {
                    backend: this.device.kind
                });
            }
            const requiresDepthTexture = postprocessRequiresDepthTexture(postprocess);
            if (requiresDepthTexture && !this.device.info.capabilities?.includes("depth-textures")) {
                throw new RenderDeviceError("Depth-aware renderer postprocess requires a backend with sampleable depth texture render targets.", "DEPTH_POSTPROCESS_UNSUPPORTED", {
                    backend: this.device.kind
                });
            }
            const forwardTarget = this.device.createRenderTarget({
                width: this.width,
                height: this.height,
                label: "renderer-forward-color",
                format,
                depth: requiresDepthTexture ? "texture" : true
            });
            ownedTargets.push(forwardTarget);
            this.device.setRenderTarget(forwardTarget);
        }
        this.device.beginFrame(this.width, this.height);
        try {
            const rendererShadowMap = explicitShadowMap ?? this.executeRendererShadowMap({
                shadowOptions,
                source,
                items,
                lights,
                ownedTargets,
                ownedShadowPasses
            });
            if (postprocess) {
                const forwardTarget = ownedTargets[0];
                if (!forwardTarget) {
                    throw new RenderDeviceError("Renderer postprocess missing forward render target", "POSTPROCESS_TARGET_MISSING");
                }
                this.device.setRenderTarget(forwardTarget);
            }
            else {
                this.device.setRenderTarget(explicitRenderTarget ?? null);
            }
            this.device.clear(this.clearColor);
            if (environmentBackground) {
                this.graph.addPass(new EnvironmentBackgroundPass({
                    ...environmentBackground,
                    outputColorSpace: postprocess ? "linear" : environmentBackground.outputColorSpace,
                    inverseViewProjectionMatrix: environmentBackground.inverseViewProjectionMatrix ?? invertMat4(cameraViewProjection ?? identityMat4()),
                    shaderLibrary: this.shaderLibrary
                }));
            }
            this.graph.addPass(new ForwardPass({
                items,
                lights,
                environmentLighting,
                environmentFog,
                inputColorResource: environmentBackground ? ENVIRONMENT_BACKGROUND_COLOR_RESOURCE : undefined,
                shadowMap: rendererShadowMap,
                cameraPosition,
                outputColorSpace: postprocess ? "linear" : "srgb",
                shaderLibrary: this.shaderLibrary
            }));
            this.graph.execute({ device: this.device, width: this.width, height: this.height });
            if (postprocess) {
                this.executePostprocess(postprocess, ownedTargets, explicitRenderTarget);
            }
        }
        finally {
            this.device.endFrame();
            for (const shadowPass of ownedShadowPasses) {
                shadowPass.dispose();
            }
            for (const target of ownedTargets) {
                target.dispose();
            }
        }
        return withRendererFrameDiagnostics(this.device.getDiagnostics(), collectionDiagnostics, createPostprocessDiagnostics(postprocess, ownedTargets, this.width, this.height, {
            targetFormat: postprocessTargetFormat,
            nativeLdrPostprocess: Boolean(this.device.presentLdrPostprocess),
            rendererDepthAvailable: Boolean(postprocess && postprocessRequiresDepthTexture(postprocess) && this.device.info.capabilities?.includes("depth-textures"))
        }));
    }
    async renderAsync(sourceOrInput, camera) {
        this.assertAlive();
        const { source, camera: inputCamera } = normalizeRendererInput(sourceOrInput, camera);
        sceneFromSource(source)?.updateWorldTransforms();
        const cameraPolicy = collectCameraPolicy(source);
        const identityPolicyIgnoresSceneCamera = inputCamera === undefined && cameraPolicy === "identity";
        const autoFrameOverridesSceneCamera = inputCamera === undefined && hasExplicitAutoFrameCameraPolicy(source);
        const resolvedCamera = resolveCamera(source, inputCamera, { width: this.width, height: this.height }, {
            allowSceneCamera: !identityPolicyIgnoresSceneCamera && !autoFrameOverridesSceneCamera
        });
        if (!resolvedCamera && cameraPolicy === "require") {
            throw new RenderDeviceError("RenderSource requires an explicit camera but none was supplied or found in the scene.", "CAMERA_REQUIRED");
        }
        let cameraViewProjection = resolvedCamera?.viewProjectionMatrix;
        const collectedItems = collectRenderItemsWithDiagnostics(source, cameraViewProjection, resolvedCamera?.camera);
        const collectionDiagnostics = collectedItems.diagnostics;
        let items = collectedItems.items;
        const lights = collectRenderLights(source);
        const environmentBackground = collectEnvironmentBackground(source);
        const environmentLighting = collectEnvironmentLighting(source);
        const environmentFog = collectEnvironmentFog(source);
        const explicitShadowMap = collectForwardShadowMap(source);
        const shadowOptions = collectRendererShadowOptions(source);
        const sourceCameraPosition = collectSourceCameraPosition(source);
        const explicitRenderTarget = collectRenderTarget(source);
        validateExplicitRenderTarget(explicitRenderTarget, this.width, this.height);
        let cameraPosition = sourceCameraPosition ?? resolvedCamera?.cameraPosition;
        if (!resolvedCamera && cameraPolicy === "auto-frame") {
            const autoFrame = createAutoFrameCamera(source, items, this.width, this.height);
            if (autoFrame) {
                cameraViewProjection = autoFrame.viewProjectionMatrix;
                items = applyViewProjection(items, autoFrame.viewProjectionMatrix);
                cameraPosition = sourceCameraPosition ?? autoFrame.cameraPosition;
            }
        }
        const postprocess = collectPostprocess(source);
        const ownedTargets = [];
        const ownedShadowPasses = [];
        this.graph.clear();
        let postprocessTargetFormat;
        if (postprocess) {
            const format = postprocess.targetFormat ?? defaultPostprocessTargetFormat(this.device, postprocess);
            postprocessTargetFormat = format;
            if ((format === "rgba16f" || format === "rgba32f") && !this.device.info.capabilities?.includes("hdr-render-targets")) {
                throw new RenderDeviceError("Renderer HDR postprocess requires an HDR-capable backend", "HDR_POSTPROCESS_UNSUPPORTED", {
                    backend: this.device.kind,
                    format
                });
            }
            if ((format === "rgba16f" || format === "rgba32f") && postprocess.toneMapping === false) {
                throw new RenderDeviceError("Renderer HDR postprocess requires tone mapping before presentation or LDR postprocess passes", "HDR_POSTPROCESS_TONEMAPPING_REQUIRED", {
                    backend: this.device.kind,
                    format
                });
            }
            if (!this.device.presentRenderTarget) {
                throw new RenderDeviceError("Renderer postprocess requires a backend presentation path", "POSTPROCESS_PRESENT_UNSUPPORTED", {
                    backend: this.device.kind
                });
            }
            const requiresDepthTexture = postprocessRequiresDepthTexture(postprocess);
            if (requiresDepthTexture && !this.device.info.capabilities?.includes("depth-textures")) {
                throw new RenderDeviceError("Depth-aware renderer postprocess requires a backend with sampleable depth texture render targets.", "DEPTH_POSTPROCESS_UNSUPPORTED", {
                    backend: this.device.kind
                });
            }
            const forwardTarget = this.device.createRenderTarget({
                width: this.width,
                height: this.height,
                label: "renderer-forward-color",
                format,
                depth: requiresDepthTexture ? "texture" : true
            });
            ownedTargets.push(forwardTarget);
            this.device.setRenderTarget(forwardTarget);
        }
        this.device.beginFrame(this.width, this.height);
        try {
            const rendererShadowMap = explicitShadowMap ?? this.executeRendererShadowMap({
                shadowOptions,
                source,
                items,
                lights,
                ownedTargets,
                ownedShadowPasses
            });
            if (postprocess) {
                const forwardTarget = ownedTargets[0];
                if (!forwardTarget) {
                    throw new RenderDeviceError("Renderer postprocess missing forward render target", "POSTPROCESS_TARGET_MISSING");
                }
                this.device.setRenderTarget(forwardTarget);
            }
            else {
                this.device.setRenderTarget(explicitRenderTarget ?? null);
            }
            this.device.clear(this.clearColor);
            if (environmentBackground) {
                this.graph.addPass(new EnvironmentBackgroundPass({
                    ...environmentBackground,
                    outputColorSpace: postprocess ? "linear" : environmentBackground.outputColorSpace,
                    inverseViewProjectionMatrix: environmentBackground.inverseViewProjectionMatrix ?? invertMat4(cameraViewProjection ?? identityMat4()),
                    shaderLibrary: this.shaderLibrary
                }));
            }
            this.graph.addPass(new ForwardPass({
                items,
                lights,
                environmentLighting,
                environmentFog,
                inputColorResource: environmentBackground ? ENVIRONMENT_BACKGROUND_COLOR_RESOURCE : undefined,
                shadowMap: rendererShadowMap,
                cameraPosition,
                outputColorSpace: postprocess ? "linear" : "srgb",
                shaderLibrary: this.shaderLibrary
            }));
            this.graph.execute({ device: this.device, width: this.width, height: this.height });
            if (postprocess) {
                await this.executePostprocessAsync(postprocess, ownedTargets, explicitRenderTarget);
            }
        }
        finally {
            this.device.endFrame();
            for (const shadowPass of ownedShadowPasses) {
                shadowPass.dispose();
            }
            for (const target of ownedTargets) {
                target.dispose();
            }
        }
        return withRendererFrameDiagnostics(this.device.getDiagnostics(), collectionDiagnostics, createPostprocessDiagnostics(postprocess, ownedTargets, this.width, this.height, {
            targetFormat: postprocessTargetFormat,
            nativeLdrPostprocess: Boolean(this.device.presentLdrPostprocess),
            rendererDepthAvailable: Boolean(postprocess && postprocessRequiresDepthTexture(postprocess) && this.device.info.capabilities?.includes("depth-textures"))
        }));
    }
    renderScene(scene, camera) {
        return this.render(scene, camera);
    }
    renderItems(items, camera, options = {}) {
        return this.render({ ...options, renderItems: items }, camera);
    }
    captureFrame(source, camera) {
        const diagnostics = source ? this.render(source, camera) : this.device.getDiagnostics();
        const pixels = this.device.readPixels(0, 0, this.width, this.height);
        const metadata = createRendererFrameCaptureMetadata(this.device, this.width, this.height, pixels, diagnostics);
        return {
            width: this.width,
            height: this.height,
            pixels,
            diagnostics,
            capturedAt: metadata.capturedAt,
            renderSize: metadata.renderSize,
            pixelHash: metadata.pixelHash,
            pixelDigest: metadata.pixelDigest,
            pixelStats: metadata.pixelStats,
            diagnosticsSummary: metadata.diagnosticsSummary,
            metadata
        };
    }
    getDiagnostics() {
        return this.device.getDiagnostics();
    }
    dispose() {
        this.animationLoop?.stop();
        this.animationLoop = null;
        this.device.dispose();
        this.disposed = true;
    }
    assertAlive() {
        if (this.disposed || this.device.disposed) {
            throw new RenderDeviceError("Renderer is disposed", "DISPOSED_DEVICE");
        }
    }
    resizeCanvas(width, height) {
        if (!this.canvas)
            return;
        if (this.canvas.width !== width) {
            this.canvas.width = width;
        }
        if (this.canvas.height !== height) {
            this.canvas.height = height;
        }
    }
    executePostprocess(postprocess, ownedTargets, outputTarget) {
        const forwardTarget = ownedTargets[0];
        let current = forwardTarget;
        if (!current) {
            throw new RenderDeviceError("Renderer postprocess missing forward render target", "POSTPROCESS_TARGET_MISSING");
        }
        const passes = createRendererPostprocessPasses(postprocess);
        if (passes.length === 0) {
            if (outputTarget) {
                this.device.setRenderTarget(current);
                writePostProcessPixels(this.device, current, outputTarget, this.device.readPixels(0, 0, current.width, current.height));
            }
            else {
                this.device.presentRenderTarget?.(current);
            }
            return;
        }
        if (this.executeFusedLdrPostprocess(current, passes, outputTarget))
            return;
        for (let index = 0; index < passes.length; index += 1) {
            const pass = passes[index];
            const nextPass = passes[index + 1];
            if (pass.name === "bloom" && isHdrRenderTarget(current)) {
                if (nextPass?.name !== "tone-mapping") {
                    throw new RenderDeviceError("Renderer HDR bloom requires tone mapping immediately after the float bloom pass.", "HDR_BLOOM_TONEMAPPING_REQUIRED", {
                        source: current.label
                    });
                }
                const isCombinedLast = index + 1 === passes.length - 1;
                const target = isCombinedLast ? outputTarget : this.device.createRenderTarget({
                    width: this.width,
                    height: this.height,
                    label: "renderer-postprocess-tone-mapping",
                    format: "rgba8",
                    depth: false
                });
                if (target)
                    ownedTargets.push(target);
                this.device.setRenderTarget(current);
                const bloomed = bloomFloatPixels(this.device.readFloatPixels(0, 0, current.width, current.height), current.width, current.height, pass.options);
                const mapped = toneMapFloatPixels(bloomed.pixels, current.width, current.height, {
                    outputColorSpace: "srgb",
                    ...nextPass.options
                });
                writePostProcessPixels(this.device, current, target, mapped.pixels);
                if (target)
                    current = target;
                index += 1;
                continue;
            }
            const isLast = index === passes.length - 1;
            const target = isLast ? outputTarget : this.device.createRenderTarget({
                width: this.width,
                height: this.height,
                label: `renderer-postprocess-${pass.name}`,
                format: "rgba8",
                depth: false
            });
            if (target)
                ownedTargets.push(target);
            if (pass.name === "tone-mapping") {
                new ToneMappingPass({
                    source: current,
                    target,
                    outputColorSpace: "srgb",
                    ...pass.options
                }).execute({ device: this.device, width: this.width, height: this.height });
            }
            else if (pass.name === "bloom") {
                new BloomPass({
                    source: current,
                    target,
                    ...pass.options
                }).execute({ device: this.device, width: this.width, height: this.height });
            }
            else if (pass.name === "fxaa") {
                new FXAAPass({
                    source: current,
                    target,
                    ...pass.options
                }).execute({ device: this.device, width: this.width, height: this.height });
            }
            else {
                this.executePixelPostprocessPass(pass, current, target, forwardTarget);
            }
            if (target)
                current = target;
        }
    }
    executeFusedLdrPostprocess(current, passes, outputTarget) {
        if (!canFuseLdrPostprocess(current, passes))
            return false;
        if (this.device.presentLdrPostprocess) {
            this.device.presentLdrPostprocess(current, {
                passes: passes.map((pass) => ({
                    name: pass.name,
                    options: pass.options
                })),
                ...(outputTarget ? { outputTarget } : {}),
                toneMappingDefaults: { outputColorSpace: "srgb" }
            });
            return true;
        }
        this.device.setRenderTarget(current);
        const pixels = fusedLdrPostprocessPixels(this.device.readPixels(0, 0, current.width, current.height), current.width, current.height, passes, {
            mutateInput: true,
            scratch: this.fusedLdrPostprocessScratch,
            toneMappingDefaults: { outputColorSpace: "srgb" }
        });
        writePostProcessPixels(this.device, current, outputTarget, pixels);
        return true;
    }
    async executePostprocessAsync(postprocess, ownedTargets, outputTarget) {
        const forwardTarget = ownedTargets[0];
        let current = forwardTarget;
        if (!current) {
            throw new RenderDeviceError("Renderer postprocess missing forward render target", "POSTPROCESS_TARGET_MISSING");
        }
        const passes = createRendererPostprocessPasses(postprocess);
        if (passes.length === 0) {
            if (outputTarget) {
                this.device.setRenderTarget(current);
                writePostProcessPixels(this.device, current, outputTarget, await this.readRenderTargetPixelsAsync(current));
            }
            else {
                this.device.presentRenderTarget?.(current);
            }
            return;
        }
        if (await this.executeFusedLdrPostprocessAsync(current, passes, outputTarget))
            return;
        for (let index = 0; index < passes.length; index += 1) {
            const pass = passes[index];
            const nextPass = passes[index + 1];
            if (pass.name === "bloom" && isHdrRenderTarget(current)) {
                if (nextPass?.name !== "tone-mapping") {
                    throw new RenderDeviceError("Renderer HDR bloom requires tone mapping immediately after the float bloom pass.", "HDR_BLOOM_TONEMAPPING_REQUIRED", {
                        source: current.label
                    });
                }
                const isCombinedLast = index + 1 === passes.length - 1;
                const target = isCombinedLast ? outputTarget : this.device.createRenderTarget({
                    width: this.width,
                    height: this.height,
                    label: "renderer-postprocess-tone-mapping",
                    format: "rgba8",
                    depth: false
                });
                if (target)
                    ownedTargets.push(target);
                const bloomed = bloomFloatPixels(await this.readRenderTargetFloatPixelsAsync(current), current.width, current.height, pass.options);
                const mapped = toneMapFloatPixels(bloomed.pixels, current.width, current.height, {
                    outputColorSpace: "srgb",
                    ...nextPass.options
                });
                writePostProcessPixels(this.device, current, target, mapped.pixels);
                if (target)
                    current = target;
                index += 1;
                continue;
            }
            const isLast = index === passes.length - 1;
            const target = isLast ? outputTarget : this.device.createRenderTarget({
                width: this.width,
                height: this.height,
                label: `renderer-postprocess-${pass.name}`,
                format: "rgba8",
                depth: false
            });
            if (target)
                ownedTargets.push(target);
            if (pass.name === "tone-mapping") {
                const mapped = isHdrRenderTarget(current)
                    ? toneMapFloatPixels(await this.readRenderTargetFloatPixelsAsync(current), current.width, current.height, {
                        outputColorSpace: "srgb",
                        ...pass.options
                    })
                    : toneMapPixels(await this.readRenderTargetPixelsAsync(current), current.width, current.height, {
                        outputColorSpace: "srgb",
                        ...pass.options
                    });
                writePostProcessPixels(this.device, current, target, mapped.pixels);
            }
            else if (pass.name === "bloom") {
                const bloomed = bloomPixels(await this.readRenderTargetPixelsAsync(current), current.width, current.height, pass.options);
                writePostProcessPixels(this.device, current, target, bloomed.pixels);
            }
            else if (pass.name === "fxaa") {
                const smoothed = fxaaPixels(await this.readRenderTargetPixelsAsync(current), current.width, current.height, pass.options);
                writePostProcessPixels(this.device, current, target, smoothed.pixels);
            }
            else {
                await this.executePixelPostprocessPassAsync(pass, current, target, forwardTarget);
            }
            if (target)
                current = target;
        }
    }
    async executeFusedLdrPostprocessAsync(current, passes, outputTarget) {
        if (!canFuseLdrPostprocess(current, passes))
            return false;
        if (this.device.presentLdrPostprocess) {
            this.device.presentLdrPostprocess(current, {
                passes: passes.map((pass) => ({
                    name: pass.name,
                    options: pass.options
                })),
                ...(outputTarget ? { outputTarget } : {}),
                toneMappingDefaults: { outputColorSpace: "srgb" }
            });
            return true;
        }
        const pixels = fusedLdrPostprocessPixels(await this.readRenderTargetPixelsAsync(current), current.width, current.height, passes, {
            mutateInput: true,
            scratch: this.fusedLdrPostprocessScratch,
            toneMappingDefaults: { outputColorSpace: "srgb" }
        });
        writePostProcessPixels(this.device, current, outputTarget, pixels);
        return true;
    }
    executePixelPostprocessPass(pass, source, target, forwardTarget) {
        this.device.setRenderTarget(source);
        const input = this.device.readPixels(0, 0, source.width, source.height);
        const rendererDepth = isDepthPostprocessPass(pass.name) && !postprocessPassHasDepth(pass.options)
            ? this.readRendererOwnedDepthTexture(forwardTarget)
            : undefined;
        const result = pass.name === "color-grade"
            ? colorGradePixels(input, source.width, source.height, pass.options).pixels
            : pass.name === "chromatic-aberration"
                ? chromaticAberrationPixels(input, source.width, source.height, pass.options).pixels
                : pass.name === "film-grain"
                    ? filmGrainPixels(input, source.width, source.height, pass.options).pixels
                    : pass.name === "depth-of-field"
                        ? depthOfFieldPixels(input, source.width, source.height, withRendererDepth(pass.options, rendererDepth)).pixels
                        : pass.name === "motion-blur"
                            ? motionBlurPixels(input, source.width, source.height, pass.options).pixels
                            : pass.name === "contact-shadow"
                                ? contactShadowPixels(input, source.width, source.height, withRendererDepth(pass.options, rendererDepth)).pixels
                                : pass.name === "ssao"
                                    ? ssaoPixels(input, source.width, source.height, withRendererDepth(pass.options, rendererDepth)).pixels
                                    : pass.name === "ssr"
                                        ? ssrPixels(input, source.width, source.height, withRendererDepth(pass.options, rendererDepth)).pixels
                                        : pass.name === "taa"
                                            ? taaPixels(input, source.width, source.height, pass.options).pixels
                                            : pass.name === "outline"
                                                ? outlinePixels(input, source.width, source.height, pass.options).pixels
                                                : undefined;
        if (!result) {
            throw new RenderDeviceError("Renderer postprocess pass is outside the supported renderer pass catalog", "POSTPROCESS_PASS_UNKNOWN", {
                pass: pass.name
            });
        }
        writePostProcessPixels(this.device, source, target, result);
    }
    async executePixelPostprocessPassAsync(pass, source, target, forwardTarget) {
        const input = await this.readRenderTargetPixelsAsync(source);
        const rendererDepth = isDepthPostprocessPass(pass.name) && !postprocessPassHasDepth(pass.options)
            ? this.readRendererOwnedDepthTexture(forwardTarget)
            : undefined;
        const result = pass.name === "color-grade"
            ? colorGradePixels(input, source.width, source.height, pass.options).pixels
            : pass.name === "chromatic-aberration"
                ? chromaticAberrationPixels(input, source.width, source.height, pass.options).pixels
                : pass.name === "film-grain"
                    ? filmGrainPixels(input, source.width, source.height, pass.options).pixels
                    : pass.name === "depth-of-field"
                        ? depthOfFieldPixels(input, source.width, source.height, withRendererDepth(pass.options, rendererDepth)).pixels
                        : pass.name === "motion-blur"
                            ? motionBlurPixels(input, source.width, source.height, pass.options).pixels
                            : pass.name === "contact-shadow"
                                ? contactShadowPixels(input, source.width, source.height, withRendererDepth(pass.options, rendererDepth)).pixels
                                : pass.name === "ssao"
                                    ? ssaoPixels(input, source.width, source.height, withRendererDepth(pass.options, rendererDepth)).pixels
                                    : pass.name === "ssr"
                                        ? ssrPixels(input, source.width, source.height, withRendererDepth(pass.options, rendererDepth)).pixels
                                        : pass.name === "taa"
                                            ? taaPixels(input, source.width, source.height, pass.options).pixels
                                            : pass.name === "outline"
                                                ? outlinePixels(input, source.width, source.height, pass.options).pixels
                                                : undefined;
        if (!result) {
            throw new RenderDeviceError("Renderer postprocess pass is outside the supported renderer pass catalog", "POSTPROCESS_PASS_UNKNOWN", {
                pass: pass.name
            });
        }
        writePostProcessPixels(this.device, source, target, result);
    }
    async readRenderTargetPixelsAsync(target) {
        this.device.setRenderTarget(target);
        if (this.device.readPixelsAsync && target.colorTexture.format === "rgba8") {
            return this.device.readPixelsAsync(0, 0, target.width, target.height);
        }
        return this.device.readPixels(0, 0, target.width, target.height);
    }
    async readRenderTargetFloatPixelsAsync(target) {
        this.device.setRenderTarget(target);
        if (this.device.readFloatPixelsAsync && isHdrRenderTarget(target)) {
            return this.device.readFloatPixelsAsync(0, 0, target.width, target.height);
        }
        return this.device.readFloatPixels(0, 0, target.width, target.height);
    }
    readRendererOwnedDepthTexture(forwardTarget) {
        if (!forwardTarget.depthTexture) {
            throw new RenderDeviceError("Renderer-owned depth postprocess requires the forward target to expose a depth texture.", "POSTPROCESS_DEPTH_TARGET_MISSING", {
                renderTarget: forwardTarget.label
            });
        }
        if (!this.device.readDepthPixels) {
            throw new RenderDeviceError("Renderer-owned depth postprocess requires backend depth readback.", "DEPTH_READBACK_UNSUPPORTED", {
                backend: this.device.kind
            });
        }
        this.device.setRenderTarget(forwardTarget);
        return createDepthTextureBinding({
            label: forwardTarget.depthTexture.label,
            width: forwardTarget.width,
            height: forwardTarget.height,
            data: this.device.readDepthPixels(0, 0, forwardTarget.width, forwardTarget.height)
        });
    }
    executeRendererShadowMap(options) {
        if (!options.shadowOptions || options.shadowOptions.enabled === false) {
            return undefined;
        }
        if (!this.device.info.capabilities?.includes("render-targets")) {
            throw new RenderDeviceError("Renderer-owned shadows require render targets", "SHADOW_RENDER_TARGET_UNSUPPORTED", {
                backend: this.device.kind
            });
        }
        const light = options.shadowOptions.light ?? firstShadowCastingLight(options.source, options.lights);
        if (light instanceof PointLight) {
            return this.executeRendererPointShadowMap({
                shadowOptions: options.shadowOptions,
                items: options.items,
                ownedTargets: options.ownedTargets,
                ownedShadowPasses: options.ownedShadowPasses,
                light
            });
        }
        if (light && !(light instanceof DirectionalLight) && !(light instanceof SpotLight)) {
            throw new RenderDeviceError("Renderer-owned shadow maps require directional, spot, or point lights.", "SHADOW_LIGHT_TYPE_UNSUPPORTED", {
                lightName: light.name,
                lightType: light.constructor.name
            });
        }
        const lightMatrix = options.shadowOptions.lightMatrix
            ? toMat4(options.shadowOptions.lightMatrix, "shadow.lightMatrix")
            : createRendererOwnedShadowMatrix(light, options.items, firstShadowCastingCollectedLight(options.lights));
        const shadowMap = new ShadowMap({
            size: options.shadowOptions.size,
            bias: options.shadowOptions.bias,
            filter: options.shadowOptions.filter,
            pcfRadius: options.shadowOptions.pcfRadius,
            pcfSamples: options.shadowOptions.pcfSamples,
            pcfDistribution: options.shadowOptions.pcfDistribution,
            label: options.shadowOptions.label ?? "renderer-shadow-map"
        });
        const shadowPass = new ShadowPass({
            light,
            casters: options.items,
            shadowMap,
            viewProjectionMatrix: lightMatrix,
            shaderLibrary: this.shaderLibrary
        });
        options.ownedShadowPasses.push(shadowPass);
        const result = shadowPass.execute({ device: this.device, width: this.width, height: this.height });
        if (!result.rendered) {
            if (result.reason === "no-light" || result.reason === "light-disabled" || result.reason === "not-shadow-casting") {
                throw new RenderDeviceError("Renderer-owned shadows require an enabled shadow-casting light.", "SHADOW_LIGHT_REQUIRED", {
                    reason: result.reason
                });
            }
            return undefined;
        }
        return shadowPass.getForwardShadowMap({
            lightMatrix,
            strength: options.shadowOptions.strength,
            slopeBias: options.shadowOptions.slopeBias,
            texelSize: options.shadowOptions.texelSize,
            bias: options.shadowOptions.bias,
            filterKernel: options.shadowOptions.filterKernel
        }) ?? undefined;
    }
    executeRendererPointShadowMap(options) {
        if (!this.device.writeRenderTargetPixels) {
            throw new RenderDeviceError("Renderer-owned point shadows require render-target pixel upload for the point-light atlas.", "POINT_SHADOW_ATLAS_UPLOAD_UNSUPPORTED", {
                backend: this.device.kind
            });
        }
        options.light.transform.updateWorld(undefined, true);
        const size = options.shadowOptions.size ?? 512;
        const faceMatrices = createPointShadowFaceMatrices(options.light);
        const faceRects = createPointShadowFaceRects();
        const shadowMapOptions = {
            size,
            bias: options.shadowOptions.bias,
            filter: options.shadowOptions.filter,
            pcfRadius: options.shadowOptions.pcfRadius,
            pcfSamples: options.shadowOptions.pcfSamples,
            pcfDistribution: options.shadowOptions.pcfDistribution
        };
        const atlasPixels = new Uint8Array(size * 3 * size * 2 * 4);
        for (let face = 0; face < 6; face += 1) {
            const faceMatrix = faceMatrices.slice(face * 16, face * 16 + 16);
            const shadowPass = new ShadowPass({
                light: options.light,
                casters: options.items,
                shadowMap: new ShadowMap({ ...shadowMapOptions, label: `${options.shadowOptions.label ?? "renderer-point-shadow"}-face-${face}` }),
                viewProjectionMatrix: faceMatrix,
                shaderLibrary: this.shaderLibrary
            });
            options.ownedShadowPasses.push(shadowPass);
            const result = shadowPass.execute({ device: this.device, width: this.width, height: this.height });
            if (!result.rendered) {
                return undefined;
            }
            const target = shadowPass.getRenderTarget();
            if (!target) {
                throw new RenderDeviceError("Renderer-owned point shadow face did not expose a render target.", "POINT_SHADOW_FACE_TARGET_MISSING", { face });
            }
            const facePixels = readShadowFacePixels(this.device, target);
            blitPointShadowFace(atlasPixels, size * 3, size * 2, facePixels, size, faceRects, face);
        }
        const atlasTarget = this.device.createRenderTarget({
            width: size * 3,
            height: size * 2,
            label: `${options.shadowOptions.label ?? "renderer-point-shadow"}-atlas`,
            format: "rgba8",
            depth: false
        });
        options.ownedTargets.push(atlasTarget);
        this.device.writeRenderTargetPixels(atlasTarget, atlasPixels);
        const texture = new TextureBinding({
            name: "u_pointShadowMapTexture",
            texture: atlasTarget.colorTexture,
            sampler: new Sampler({ minFilter: "nearest", magFilter: "nearest", addressU: "clamp-to-edge", addressV: "clamp-to-edge" }),
            required: true
        });
        return {
            texture: new TextureBinding({
                name: "u_shadowMapTexture",
                texture: atlasTarget.colorTexture,
                sampler: new Sampler({ minFilter: "nearest", magFilter: "nearest", addressU: "clamp-to-edge", addressV: "clamp-to-edge" }),
                required: true
            }),
            lightMatrix: identityMat4(),
            strength: options.shadowOptions.strength,
            slopeBias: options.shadowOptions.slopeBias,
            texelSize: options.shadowOptions.texelSize,
            bias: options.shadowOptions.bias,
            filterKernel: options.shadowOptions.filterKernel,
            pointLight: {
                texture,
                lightPosition: [options.light.transform.worldMatrix[12], options.light.transform.worldMatrix[13], options.light.transform.worldMatrix[14]],
                range: options.light.range,
                faceMatrices,
                faceRects,
                strength: options.shadowOptions.strength,
                slopeBias: options.shadowOptions.slopeBias,
                texelSize: [1 / Math.max(1, size * 3), 1 / Math.max(1, size * 2)],
                bias: options.shadowOptions.bias,
                filterKernel: options.shadowOptions.filterKernel
            }
        };
    }
}
export function pickSceneRenderables(source, ray, options = {}) {
    return pickSceneRenderableHits(source, ray, options)[0];
}
export function pickSceneRenderableHits(source, ray, options = {}) {
    const pointRadius = options.pointRadius;
    const lineRadius = options.lineRadius;
    if (pointRadius !== undefined && (!Number.isFinite(pointRadius) || pointRadius <= 0)) {
        throw new RenderDeviceError("Scene point picking radius must be a finite positive number", "SCENE_PICK_RADIUS_INVALID", { pointRadius });
    }
    if (lineRadius !== undefined && (!Number.isFinite(lineRadius) || lineRadius <= 0)) {
        throw new RenderDeviceError("Scene line picking radius must be a finite positive number", "SCENE_PICK_RADIUS_INVALID", { lineRadius });
    }
    const scene = source.scene;
    if (!scene) {
        throw new RenderDeviceError("Scene picking requires a scene", "SCENE_PICKING_SCENE_MISSING");
    }
    if (!source.geometryLibrary) {
        throw new RenderDeviceError("Scene picking requires a geometryLibrary resource lookup", "SCENE_PICKING_RESOURCES_MISSING");
    }
    scene.updateWorldTransforms();
    const hits = [];
    for (const { node, renderable } of scene.collectRenderables()) {
        const geometry = lookupRenderResource(source.geometryLibrary, renderable.geometry);
        if (!geometry) {
            throw new RenderDeviceError("Scene pick target references missing geometry", "SCENE_PICK_GEOMETRY_MISSING", {
                node: node.name,
                geometry: renderable.geometry
            });
        }
        const morphTargets = source.morphTargetLibrary ? lookupRenderResource(source.morphTargetLibrary, renderable.geometry) : undefined;
        if (renderable.morphWeights.length > 0 && !morphTargets) {
            throw new RenderDeviceError("Scene pick target has morph weights but no morph target resource entry", "SCENE_PICK_MORPH_TARGETS_MISSING", {
                node: node.name,
                geometry: renderable.geometry,
                morphWeights: renderable.morphWeights.length
            });
        }
        const bounds = renderableWorldBounds(geometry, node.transform.worldMatrix, renderable.instanceTransforms, morphTargets, renderable.morphWeights, renderable.skinning);
        if (geometry.topology === "points" && options.pointRadius !== undefined) {
            hits.push(...pickPoints(node, geometry, ray, node.transform.worldMatrix, renderable.instanceTransforms, options.pointRadius));
            continue;
        }
        const pickBounds = geometry.topology === "lines" && options.lineRadius !== undefined
            ? expandSceneBounds(bounds, options.lineRadius)
            : bounds;
        const hitPoint = ray.intersectBox(pickBounds.toMathBox());
        if (hitPoint) {
            hits.push({ node, geometry, bounds: pickBounds, distance: hitPoint.distanceTo(ray.origin), hitPoint: hitPoint.toArray() });
        }
    }
    return hits.sort((left, right) => left.distance - right.distance);
}
function pickPoints(node, geometry, ray, modelMatrix, instanceTransforms, pointRadius) {
    const hits = [];
    const radiusSquared = pointRadius * pointRadius;
    const transforms = instanceTransforms ? collectInstanceWorldMatrices(modelMatrix, instanceTransforms) : [modelMatrix];
    for (let instanceIndex = 0; instanceIndex < transforms.length; instanceIndex += 1) {
        const transform = transforms[instanceIndex];
        for (let pointIndex = 0; pointIndex < geometry.vertexBuffer.vertexCount; pointIndex += 1) {
            const position = geometry.vertexBuffer.getAttribute(pointIndex, "position");
            const worldPoint = transformPoint(transform, [position[0] ?? 0, position[1] ?? 0, position[2] ?? 0]);
            const distance = distanceAlongRay(ray, worldPoint);
            if (distance < 0)
                continue;
            const radialDistanceSquared = squaredDistanceToRayAt(ray, worldPoint, distance);
            if (radialDistanceSquared <= radiusSquared) {
                hits.push({
                    node,
                    geometry,
                    bounds: expandPointBounds(worldPoint, pointRadius),
                    distance,
                    hitPoint: worldPoint,
                    pointIndex,
                    instanceIndex: instanceTransforms ? instanceIndex : undefined
                });
            }
        }
    }
    return hits;
}
function collectInstanceWorldMatrices(modelMatrix, instanceTransforms) {
    const matrices = [];
    for (let offset = 0; offset < instanceTransforms.length; offset += 16) {
        matrices.push(multiplyMat4(modelMatrix, toMat4(instanceTransforms.slice(offset, offset + 16), "instanceTransforms")));
    }
    return matrices;
}
function distanceAlongRay(ray, point) {
    return ((point[0] - ray.origin.x) * ray.direction.x +
        (point[1] - ray.origin.y) * ray.direction.y +
        (point[2] - ray.origin.z) * ray.direction.z);
}
function squaredDistanceToRayAt(ray, point, distance) {
    const closestX = ray.origin.x + ray.direction.x * distance;
    const closestY = ray.origin.y + ray.direction.y * distance;
    const closestZ = ray.origin.z + ray.direction.z * distance;
    const dx = point[0] - closestX;
    const dy = point[1] - closestY;
    const dz = point[2] - closestZ;
    return dx * dx + dy * dy + dz * dz;
}
function expandPointBounds(center, radius) {
    return new SceneBounds3([center[0] - radius, center[1] - radius, center[2] - radius], [center[0] + radius, center[1] + radius, center[2] + radius]);
}
function expandSceneBounds(bounds, radius) {
    return new SceneBounds3([bounds.min[0] - radius, bounds.min[1] - radius, bounds.min[2] - radius], [bounds.max[0] + radius, bounds.max[1] + radius, bounds.max[2] + radius]);
}
class RendererAnimationLoopImpl {
    renderer;
    callback;
    requestId = null;
    running = false;
    constructor(renderer, callback) {
        this.renderer = renderer;
        this.callback = callback;
    }
    start() {
        if (typeof requestAnimationFrame !== "function" || typeof cancelAnimationFrame !== "function") {
            throw new RenderDeviceError("Renderer animation loops require requestAnimationFrame", "ANIMATION_LOOP_UNAVAILABLE");
        }
        this.running = true;
        this.requestId = requestAnimationFrame((timeMs) => this.tick(timeMs));
    }
    stop() {
        this.running = false;
        if (this.requestId !== null) {
            cancelAnimationFrame(this.requestId);
            this.requestId = null;
        }
    }
    tick(timeMs) {
        if (!this.running) {
            return;
        }
        this.callback(timeMs, this.renderer);
        if (this.running) {
            this.requestId = requestAnimationFrame((nextTimeMs) => this.tick(nextTimeMs));
        }
    }
}
function readCanvasCssSize(canvas, axis) {
    if ("getBoundingClientRect" in canvas) {
        const bounds = canvas.getBoundingClientRect();
        const value = axis === "width" ? bounds.width : bounds.height;
        if (value > 0) {
            return value;
        }
    }
    return axis === "width" ? canvas.width : canvas.height;
}
function inferInitialCanvasDimension(canvas, axis) {
    if (!canvas)
        return 1;
    const cssSize = readCanvasCssSize(canvas, axis);
    const dpr = Number.isFinite(globalThis.devicePixelRatio) && globalThis.devicePixelRatio > 0 ? globalThis.devicePixelRatio : 1;
    const displaySize = Math.round(cssSize * dpr);
    return Math.max(1, displaySize || (axis === "width" ? canvas.width : canvas.height) || 1);
}
function collectEnvironmentLighting(source) {
    if (source instanceof Scene || isIterable(source))
        return undefined;
    if (source.environmentLighting === false)
        return cloneEnvironmentLighting(DISABLED_RENDERER_ENVIRONMENT_LIGHTING);
    if (source.environmentLighting)
        return source.environmentLighting;
    return cloneEnvironmentLighting(DEFAULT_RENDERER_ENVIRONMENT_LIGHTING);
}
function collectEnvironmentBackground(source) {
    if (source instanceof Scene || isIterable(source) || source.environmentBackground === false)
        return undefined;
    return source.environmentBackground;
}
function collectEnvironmentFog(source) {
    if (source instanceof Scene || isIterable(source))
        return undefined;
    return source.environmentFog;
}
function collectForwardShadowMap(source) {
    return source instanceof Scene || isIterable(source) ? undefined : source.shadowMap;
}
function collectRenderTarget(source) {
    return source instanceof Scene || isIterable(source) ? undefined : source.renderTarget;
}
function validateExplicitRenderTarget(target, width, height) {
    if (!target)
        return;
    if (target.disposed) {
        throw new RenderDeviceError("Renderer renderTarget must be a live render target.", "RENDER_TARGET_DISPOSED", {
            renderTarget: target.label
        });
    }
    if (target.width !== width || target.height !== height) {
        throw new RenderDeviceError("Renderer renderTarget dimensions must match the renderer viewport.", "RENDER_TARGET_VIEWPORT_MISMATCH", {
            renderTarget: target.label,
            targetWidth: target.width,
            targetHeight: target.height,
            rendererWidth: width,
            rendererHeight: height
        });
    }
}
function collectRendererShadowOptions(source) {
    if (source instanceof Scene || isIterable(source))
        return undefined;
    if (source.shadow === true)
        return {};
    if (!source.shadow)
        return undefined;
    return source.shadow;
}
function firstShadowCastingLight(source, lights) {
    const explicit = lights.find((light) => light.castsShadow && light.source.visible)?.source;
    if (explicit)
        return explicit;
    const scene = sceneFromSource(source);
    return scene?.collectLights().find((light) => light.visible && light.castsShadow) ?? null;
}
function firstShadowCastingCollectedLight(lights) {
    return lights.find((light) => light.castsShadow && light.source.visible);
}
function lightDirectionFromLight(light) {
    if (!light)
        return [0, -1, -1];
    const direction = light instanceof DirectionalLight ? light.getDirection() : undefined;
    return normalizeVec3([
        Number(direction?.[0] ?? 0),
        Number(direction?.[1] ?? -1),
        Number(direction?.[2] ?? -1)
    ]);
}
function createRendererOwnedShadowMatrix(light, items, collectedLight) {
    if (light instanceof SpotLight) {
        light.transform.updateWorld(undefined, true);
        const near = 0.01;
        const far = Math.max(near + 0.01, light.range);
        const projectionMatrix = perspectiveMat4(light.angle * 2, 1, near, far);
        return multiplyMat4(projectionMatrix, light.transform.inverseWorldMatrix);
    }
    return createDirectionalShadowMatrix(items, collectedLight?.direction ?? lightDirectionFromLight(light));
}
function createPointShadowFaceMatrices(light) {
    light.transform.updateWorld(undefined, true);
    const position = [
        light.transform.worldMatrix[12],
        light.transform.worldMatrix[13],
        light.transform.worldMatrix[14]
    ];
    const projection = perspectiveMat4(Math.PI / 2, 1, 0.01, Math.max(0.02, light.range));
    const faces = [
        { direction: [1, 0, 0], up: [0, -1, 0] },
        { direction: [-1, 0, 0], up: [0, -1, 0] },
        { direction: [0, 1, 0], up: [0, 0, 1] },
        { direction: [0, -1, 0], up: [0, 0, -1] },
        { direction: [0, 0, 1], up: [0, -1, 0] },
        { direction: [0, 0, -1], up: [0, -1, 0] }
    ];
    const matrices = new Float32Array(6 * 16);
    for (const [index, face] of faces.entries()) {
        const view = lookAtMatrix(position, addVec3(position, face.direction), face.up);
        matrices.set(multiplyMat4(projection, view), index * 16);
    }
    return matrices;
}
function createPointShadowFaceRects() {
    const rects = new Float32Array(6 * 4);
    for (let face = 0; face < 6; face += 1) {
        const column = face % 3;
        const row = Math.floor(face / 3);
        rects.set([column / 3, row / 2, 1 / 3, 1 / 2], face * 4);
    }
    return rects;
}
function readShadowFacePixels(device, target) {
    device.setRenderTarget(target);
    if (target.depthTexture && device.readDepthPixels) {
        const depth = device.readDepthPixels(0, 0, target.width, target.height);
        const pixels = new Uint8Array(target.width * target.height * 4);
        for (let index = 0; index < depth.length; index += 1) {
            const byte = Math.max(0, Math.min(255, Math.round((depth[index] ?? 1) * 255)));
            const offset = index * 4;
            pixels[offset] = byte;
            pixels[offset + 1] = byte;
            pixels[offset + 2] = byte;
            pixels[offset + 3] = 255;
        }
        return pixels;
    }
    return device.readPixels(0, 0, target.width, target.height);
}
function blitPointShadowFace(atlasPixels, atlasWidth, atlasHeight, facePixels, faceSize, faceRects, face) {
    const rectOffset = face * 4;
    const destX = Math.round((faceRects[rectOffset] ?? 0) * atlasWidth);
    const destY = Math.round((faceRects[rectOffset + 1] ?? 0) * atlasHeight);
    for (let row = 0; row < faceSize; row += 1) {
        const sourceOffset = row * faceSize * 4;
        const targetOffset = ((destY + row) * atlasWidth + destX) * 4;
        atlasPixels.set(facePixels.subarray(sourceOffset, sourceOffset + faceSize * 4), targetOffset);
    }
}
function lookAtMatrix(eye, target, up) {
    const forward = normalizeVec3(subtractVec3(target, eye));
    const right = normalizeVec3(crossVec3(forward, up));
    const correctedUp = crossVec3(right, forward);
    return [
        right[0], correctedUp[0], -forward[0], 0,
        right[1], correctedUp[1], -forward[1], 0,
        right[2], correctedUp[2], -forward[2], 0,
        -dotVec3(right, eye), -dotVec3(correctedUp, eye), dotVec3(forward, eye), 1
    ];
}
function createDirectionalShadowMatrix(items, lightDirection) {
    const bounds = collectRenderItemBounds(items);
    if (!bounds || bounds.isEmpty()) {
        return identityMat4();
    }
    const basis = directionalLightBasis(lightDirection);
    const points = boundsCorners(bounds).map((point) => projectPointToLightSpace(point, basis));
    const lightBounds = boundsFromVec3(points);
    const width = Math.max(0.01, lightBounds.max[0] - lightBounds.min[0]);
    const height = Math.max(0.01, lightBounds.max[1] - lightBounds.min[1]);
    const depth = Math.max(0.01, lightBounds.max[2] - lightBounds.min[2]);
    const padding = Math.max(width, height, depth) * 0.08 + 0.05;
    const viewMatrix = lightViewMatrix(basis);
    const projectionMatrix = orthographicMat4(lightBounds.min[0] - padding, lightBounds.max[0] + padding, lightBounds.min[1] - padding, lightBounds.max[1] + padding, lightBounds.min[2] - padding, lightBounds.max[2] + padding);
    return multiplyMat4(projectionMatrix, viewMatrix);
}
function directionalLightBasis(lightDirection) {
    const forward = normalizeVec3(scaleVec3(normalizeVec3(lightDirection), -1));
    const fallbackUp = Math.abs(forward[1]) > 0.94 ? [0, 0, 1] : [0, 1, 0];
    const right = normalizeVec3(crossVec3(fallbackUp, forward));
    const up = normalizeVec3(crossVec3(forward, right));
    return { right, up, forward };
}
function lightViewMatrix(basis) {
    return [
        basis.right[0], basis.up[0], basis.forward[0], 0,
        basis.right[1], basis.up[1], basis.forward[1], 0,
        basis.right[2], basis.up[2], basis.forward[2], 0,
        0, 0, 0, 1
    ];
}
function projectPointToLightSpace(point, basis) {
    return [dotVec3(point, basis.right), dotVec3(point, basis.up), dotVec3(point, basis.forward)];
}
function boundsCorners(bounds) {
    return [
        [bounds.min[0], bounds.min[1], bounds.min[2]],
        [bounds.max[0], bounds.min[1], bounds.min[2]],
        [bounds.min[0], bounds.max[1], bounds.min[2]],
        [bounds.min[0], bounds.min[1], bounds.max[2]],
        [bounds.max[0], bounds.max[1], bounds.min[2]],
        [bounds.max[0], bounds.min[1], bounds.max[2]],
        [bounds.min[0], bounds.max[1], bounds.max[2]],
        [bounds.max[0], bounds.max[1], bounds.max[2]]
    ];
}
function boundsFromVec3(points) {
    const min = [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY];
    const max = [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY];
    for (const point of points) {
        for (let axis = 0; axis < 3; axis += 1) {
            min[axis] = Math.min(min[axis], point[axis]);
            max[axis] = Math.max(max[axis], point[axis]);
        }
    }
    return { min, max };
}
function normalizeVec3(value) {
    const length = Math.hypot(value[0], value[1], value[2]);
    if (!Number.isFinite(length) || length <= 1e-8)
        return [0, -1 / Math.SQRT2, -1 / Math.SQRT2];
    return [value[0] / length, value[1] / length, value[2] / length];
}
function scaleVec3(value, amount) {
    return [value[0] * amount, value[1] * amount, value[2] * amount];
}
function addVec3(left, right) {
    return [left[0] + right[0], left[1] + right[1], left[2] + right[2]];
}
function subtractVec3(left, right) {
    return [left[0] - right[0], left[1] - right[1], left[2] - right[2]];
}
function crossVec3(left, right) {
    return [
        left[1] * right[2] - left[2] * right[1],
        left[2] * right[0] - left[0] * right[2],
        left[0] * right[1] - left[1] * right[0]
    ];
}
function dotVec3(left, right) {
    return left[0] * right[0] + left[1] * right[1] + left[2] * right[2];
}
function collectPostprocess(source) {
    if (source instanceof Scene || isIterable(source))
        return undefined;
    if (source.postprocess === true)
        return {};
    if (!source.postprocess)
        return undefined;
    return source.postprocess;
}
function normalizeRendererInput(sourceOrInput, camera) {
    if (isRendererInput(sourceOrInput)) {
        return { source: sourceOrInput.source, camera: camera ?? sourceOrInput.camera };
    }
    return { source: sourceOrInput, ...(camera ? { camera } : {}) };
}
function isRendererInput(value) {
    return !(value instanceof Scene) && !isIterable(value) && isRecord(value) && "source" in value;
}
function isRecord(value) {
    return typeof value === "object" && value !== null;
}
function collectCameraPolicy(source) {
    if (source instanceof Scene || isIterable(source))
        return "identity";
    if (source.cameraPolicy)
        return source.cameraPolicy;
    if (source.scene && !source.renderItems && !source.collectRenderItems)
        return "auto-frame";
    if ((source.renderItems || source.collectRenderItems) && !source.renderTarget)
        return "auto-frame";
    return "identity";
}
function postprocessRequiresDepthTexture(postprocess) {
    return Boolean((postprocess.depthOfField && !postprocess.depthOfField.depth) ||
        (postprocess.contactShadow && !postprocess.contactShadow.depth) ||
        (postprocess.ssao && !postprocess.ssao.depth) ||
        (postprocess.ssr && !postprocess.ssr.depth));
}
function canFuseLdrPostprocess(source, passes) {
    const sourceIsHdr = isHdrRenderTarget(source);
    return passes.length > 1
        && (!sourceIsHdr || passes[0]?.name === "tone-mapping")
        && passes.every((pass) => pass.name === "tone-mapping" || pass.name === "color-grade" || pass.name === "fxaa")
        && passes.every((pass, index) => {
            const previousRank = index === 0 ? -1 : ldrFusionPassRank(passes[index - 1].name);
            return ldrFusionPassRank(pass.name) >= previousRank;
        });
}
function ldrFusionPassRank(name) {
    if (name === "tone-mapping")
        return 0;
    if (name === "color-grade")
        return 1;
    if (name === "fxaa")
        return 2;
    return Number.POSITIVE_INFINITY;
}
function isDepthPostprocessPass(name) {
    return name === "depth-of-field" || name === "contact-shadow" || name === "ssao" || name === "ssr";
}
function postprocessPassHasDepth(options) {
    return typeof options === "object" && options !== null && "depth" in options && Boolean(options.depth);
}
function withRendererDepth(options, depth) {
    return depth && !options.depth ? { ...options, depth } : options;
}
function defaultPostprocessTargetFormat(device, postprocess) {
    if (postprocess.toneMapping === false)
        return "rgba8";
    return device.info.capabilities?.includes("hdr-render-targets") ? "rgba16f" : "rgba8";
}
function isHdrRenderTarget(target) {
    return target.colorTexture.format === "rgba16f" || target.colorTexture.format === "rgba32f";
}
function collectSourceCameraPosition(source) {
    if (source instanceof Scene || isIterable(source))
        return undefined;
    const position = source.cameraPosition;
    if (position === undefined)
        return undefined;
    if (position.length !== 3 || position.some((value) => !Number.isFinite(value))) {
        throw new RenderDeviceError("RenderSource cameraPosition must contain three finite numbers", "CAMERA_POSITION_INVALID", {
            cameraPosition: [...position]
        });
    }
    return position;
}
function collectRenderItems(source, cameraViewProjection, camera) {
    return collectRenderItemsWithDiagnostics(source, cameraViewProjection, camera).items;
}
function collectRenderItemsWithDiagnostics(source, cameraViewProjection, camera) {
    const diagnostics = createRenderCollectionDiagnostics();
    if (isIterable(source)) {
        const items = applyViewProjection([...source], cameraViewProjection);
        diagnostics.submittedObjects += items.length;
        diagnostics.visibleObjects += items.length;
        return { items, diagnostics };
    }
    if (source instanceof Scene) {
        const items = collectSceneRenderItems(source, {}, cameraViewProjection, camera, diagnostics);
        return { items, diagnostics };
    }
    const items = [];
    if (source.scene) {
        items.push(...collectSceneRenderItems(source.scene, source, cameraViewProjection, source.frustumCulling === false ? undefined : camera, diagnostics));
    }
    const explicitItems = [];
    if (source.collectRenderItems) {
        explicitItems.push(...source.collectRenderItems());
    }
    if (source.renderItems) {
        explicitItems.push(...source.renderItems);
    }
    if (explicitItems.length > 0) {
        const optimizedItems = applyRendererOwnedStaticBatching(source, explicitItems);
        const projectedItems = applyViewProjection(optimizedItems, cameraViewProjection);
        diagnostics.submittedObjects += explicitItems.length;
        diagnostics.visibleObjects += explicitItems.length;
        items.push(...projectedItems);
    }
    return { items, diagnostics };
}
const staticBatchGeometryIds = new WeakMap();
const staticBatchMaterialIds = new WeakMap();
let nextStaticBatchResourceId = 1;
function applyRendererOwnedStaticBatching(source, items) {
    if (!source.staticBatching)
        return items;
    const batchable = [];
    const passthrough = [];
    for (const item of items) {
        if (isStaticBatchCandidate(item)) {
            batchable.push({
                geometry: item.geometry,
                material: item.material,
                modelMatrix: item.modelMatrix ?? identityMat4(),
                batchKey: staticBatchKey(item),
                label: item.label
            });
        }
        else {
            passthrough.push(item);
        }
    }
    if (batchable.length === 0)
        return items;
    const options = source.staticBatching === true ? {} : source.staticBatching;
    const batched = batchStaticRenderItems(batchable, {
        labelPrefix: "renderer-static-batch",
        ...options
    });
    return [...passthrough, ...batched.renderItems];
}
function isStaticBatchCandidate(item) {
    return item.material !== undefined &&
        item.drawRange === undefined &&
        item.skinning === undefined &&
        item.morphTargets === undefined &&
        item.morphWeights === undefined &&
        item.instanceTransforms === undefined &&
        item.instanceColors === undefined &&
        item.instanceAttributes === undefined;
}
function staticBatchKey(item) {
    return `${resourceId(staticBatchGeometryIds, item.geometry)}:${resourceId(staticBatchMaterialIds, item.material)}`;
}
function resourceId(ids, resource) {
    const existing = ids.get(resource);
    if (existing !== undefined)
        return existing;
    const next = nextStaticBatchResourceId++;
    ids.set(resource, next);
    return next;
}
function collectSceneRenderItems(scene, source, cameraViewProjection, camera, diagnostics) {
    scene.updateWorldTransforms();
    const renderables = scene.collectRenderables();
    if (renderables.length === 0) {
        return [];
    }
    if (!source.geometryLibrary || !source.materialLibrary) {
        throw new RenderDeviceError("Scene rendering requires geometryLibrary and materialLibrary resource lookups", "SCENE_RENDER_RESOURCES_MISSING", {
            renderables: renderables.length
        });
    }
    const items = [];
    for (const { node, renderable } of renderables) {
        const geometry = lookupRenderResource(source.geometryLibrary, renderable.geometry);
        const material = lookupRenderResource(source.materialLibrary, renderable.material);
        if (!geometry) {
            throw new RenderDeviceError("Scene renderable references missing geometry", "SCENE_GEOMETRY_MISSING", {
                node: node.name,
                geometry: renderable.geometry
            });
        }
        if (!material) {
            throw new RenderDeviceError("Scene renderable references missing material", "SCENE_MATERIAL_MISSING", {
                node: node.name,
                material: renderable.material
            });
        }
        const morphTargets = source.morphTargetLibrary ? lookupRenderResource(source.morphTargetLibrary, renderable.geometry) : undefined;
        if (renderable.morphWeights.length > 0 && !morphTargets) {
            throw new RenderDeviceError("Scene renderable has morph weights but no morph target resource entry", "SCENE_MORPH_TARGETS_MISSING", {
                node: node.name,
                geometry: renderable.geometry,
                morphWeights: renderable.morphWeights.length
            });
        }
        const modelMatrix = node.transform.worldMatrix;
        const bounds = renderableWorldBounds(geometry, modelMatrix, renderable.instanceTransforms, morphTargets, renderable.morphWeights, renderable.skinning);
        if (diagnostics) {
            diagnostics.submittedObjects += 1;
            if (camera)
                diagnostics.frustumTestedObjects += 1;
        }
        if (camera && !camera.frustum.intersectsBox(bounds.toMathBox())) {
            if (diagnostics)
                diagnostics.culledObjects += 1;
            continue;
        }
        if (diagnostics)
            diagnostics.visibleObjects += 1;
        items.push({
            geometry,
            material,
            label: node.name,
            modelMatrix,
            normalMatrix: normalMatrixFromModel(modelMatrix),
            modelViewProjectionMatrix: multiplyMat4(cameraViewProjection ?? identityMat4(), modelMatrix),
            ...(renderable.skinning ? { skinning: renderable.skinning } : {}),
            ...(renderable.instanceTransforms ? { instanceTransforms: renderable.instanceTransforms } : {}),
            ...(renderable.instanceColors ? { instanceColors: renderable.instanceColors } : {}),
            ...(morphTargets && renderable.morphWeights.length > 0 ? { morphTargets, morphWeights: renderable.morphWeights } : {})
        });
    }
    return items;
}
function createRenderCollectionDiagnostics() {
    return {
        submittedObjects: 0,
        visibleObjects: 0,
        culledObjects: 0,
        frustumTestedObjects: 0
    };
}
function createPostprocessDiagnostics(postprocess, ownedTargets, width, height, context = {}) {
    if (!postprocess)
        return undefined;
    const passes = createRendererPostprocessPasses(postprocess);
    const targetFormat = context.targetFormat ?? postprocess.targetFormat ?? "rgba8";
    return {
        postprocessPasses: passes.length,
        postprocessPassNames: passes.map((pass) => pass.name),
        postprocessTargetFormat: targetFormat,
        postprocessRenderTargets: ownedTargets.length,
        postprocessTextures: ownedTargets.reduce((total, target) => total + 1 + (target.depthTexture ? 1 : 0), 0),
        postprocessTargetWidth: width,
        postprocessTargetHeight: height,
        postprocessPlan: createRendererPostprocessPlanDiagnostics(postprocess, {
            sourceTargetFormat: targetFormat,
            targetFormat,
            nativeLdrPostprocess: context.nativeLdrPostprocess,
            rendererDepthAvailable: context.rendererDepthAvailable
        })
    };
}
const CAPTURE_NON_BLANK_LUMINANCE_THRESHOLD = 1 / 255;
const CAPTURE_UNDEREXPOSED_LUMINANCE_THRESHOLD = 5 / 255;
const CAPTURE_OVEREXPOSED_LUMINANCE_THRESHOLD = 250 / 255;
const CAPTURE_CLIPPED_CHANNEL_THRESHOLD = 254;
function createRendererFrameCaptureMetadata(device, width, height, pixels, diagnostics) {
    const pixelDigest = createRendererFramePixelDigest(pixels);
    return {
        capturedAt: new Date().toISOString(),
        renderSize: {
            width,
            height,
            pixelCount: width * height,
            byteLength: pixels.byteLength,
            aspectRatio: roundCaptureMetric(height > 0 ? width / height : 0)
        },
        pixelHash: pixelDigest.value,
        pixelDigest,
        pixelStats: createRendererFramePixelStats(pixels, width, height),
        diagnosticsSummary: createRendererFrameDiagnosticsSummary(device, diagnostics)
    };
}
function createRendererFramePixelDigest(pixels) {
    return {
        algorithm: "sha256",
        encoding: "hex",
        source: "rgba8",
        value: sha256Hex(pixels),
        byteLength: pixels.byteLength
    };
}
function createRendererFramePixelStats(pixels, width, height) {
    const totalPixels = Math.max(0, width * height);
    const divisor = totalPixels > 0 ? totalPixels : 1;
    let opaquePixels = 0;
    let transparentPixels = 0;
    let nonBlankPixels = 0;
    let underexposedPixels = 0;
    let overexposedPixels = 0;
    let clippedChannelPixels = 0;
    let luminanceSum = 0;
    let alphaSum = 0;
    let redSum = 0;
    let greenSum = 0;
    let blueSum = 0;
    let minLuminance = totalPixels > 0 ? Number.POSITIVE_INFINITY : 0;
    let maxLuminance = 0;
    for (let pixel = 0; pixel < totalPixels; pixel += 1) {
        const offset = pixel * 4;
        const red = pixels[offset] ?? 0;
        const green = pixels[offset + 1] ?? 0;
        const blue = pixels[offset + 2] ?? 0;
        const alpha = pixels[offset + 3] ?? 0;
        const luminance = ((0.2126 * red) + (0.7152 * green) + (0.0722 * blue)) / 255;
        redSum += red;
        greenSum += green;
        blueSum += blue;
        alphaSum += alpha;
        luminanceSum += luminance;
        minLuminance = Math.min(minLuminance, luminance);
        maxLuminance = Math.max(maxLuminance, luminance);
        if (alpha === 255)
            opaquePixels += 1;
        if (alpha === 0)
            transparentPixels += 1;
        if (alpha > 0 && luminance > CAPTURE_NON_BLANK_LUMINANCE_THRESHOLD)
            nonBlankPixels += 1;
        if (alpha > 0 && luminance <= CAPTURE_UNDEREXPOSED_LUMINANCE_THRESHOLD)
            underexposedPixels += 1;
        if (alpha > 0 && luminance >= CAPTURE_OVEREXPOSED_LUMINANCE_THRESHOLD)
            overexposedPixels += 1;
        if (alpha > 0 &&
            (red >= CAPTURE_CLIPPED_CHANNEL_THRESHOLD ||
                green >= CAPTURE_CLIPPED_CHANNEL_THRESHOLD ||
                blue >= CAPTURE_CLIPPED_CHANNEL_THRESHOLD)) {
            clippedChannelPixels += 1;
        }
    }
    return {
        totalPixels,
        opaquePixels,
        opaqueRatio: captureRatio(opaquePixels, totalPixels),
        transparentPixels,
        transparentRatio: captureRatio(transparentPixels, totalPixels),
        nonBlankPixels,
        nonBlankRatio: captureRatio(nonBlankPixels, totalPixels),
        underexposedPixels,
        underexposedRatio: captureRatio(underexposedPixels, totalPixels),
        overexposedPixels,
        overexposedRatio: captureRatio(overexposedPixels, totalPixels),
        clippedChannelPixels,
        clippedChannelRatio: captureRatio(clippedChannelPixels, totalPixels),
        averageLuminance: roundCaptureMetric(luminanceSum / divisor),
        minLuminance: roundCaptureMetric(minLuminance),
        maxLuminance: roundCaptureMetric(maxLuminance),
        averageAlpha: roundCaptureMetric((alphaSum / divisor) / 255),
        averageRgb: [
            roundCaptureMetric((redSum / divisor) / 255),
            roundCaptureMetric((greenSum / divisor) / 255),
            roundCaptureMetric((blueSum / divisor) / 255)
        ],
        thresholds: {
            nonBlankLuminance: roundCaptureMetric(CAPTURE_NON_BLANK_LUMINANCE_THRESHOLD),
            underexposedLuminance: roundCaptureMetric(CAPTURE_UNDEREXPOSED_LUMINANCE_THRESHOLD),
            overexposedLuminance: roundCaptureMetric(CAPTURE_OVEREXPOSED_LUMINANCE_THRESHOLD),
            clippedChannel: roundCaptureMetric(CAPTURE_CLIPPED_CHANNEL_THRESHOLD / 255)
        }
    };
}
function createRendererFrameDiagnosticsSummary(device, diagnostics) {
    const resources = {
        buffers: diagnostics.buffers,
        shaders: diagnostics.shaders,
        ...(diagnostics.renderTargets !== undefined ? { renderTargets: diagnostics.renderTargets } : {}),
        ...(diagnostics.textures !== undefined ? { textures: diagnostics.textures } : {})
    };
    const memoryBytes = {
        ...(diagnostics.bufferBytes !== undefined ? { buffers: diagnostics.bufferBytes } : {}),
        ...(diagnostics.textureBytes !== undefined ? { textures: diagnostics.textureBytes } : {}),
        ...(diagnostics.approximateGpuMemoryBytes !== undefined ? { approximateGpu: diagnostics.approximateGpuMemoryBytes } : {})
    };
    const scene = {
        ...(diagnostics.submittedObjects !== undefined ? { submittedObjects: diagnostics.submittedObjects } : {}),
        ...(diagnostics.visibleObjects !== undefined ? { visibleObjects: diagnostics.visibleObjects } : {}),
        ...(diagnostics.culledObjects !== undefined ? { culledObjects: diagnostics.culledObjects } : {}),
        ...(diagnostics.frustumTestedObjects !== undefined ? { frustumTestedObjects: diagnostics.frustumTestedObjects } : {})
    };
    const postprocess = {
        ...(diagnostics.postprocessPasses !== undefined ? { passes: diagnostics.postprocessPasses } : {}),
        ...(diagnostics.postprocessPassNames !== undefined ? { passNames: diagnostics.postprocessPassNames } : {}),
        ...(diagnostics.postprocessTargetFormat !== undefined ? { targetFormat: diagnostics.postprocessTargetFormat } : {}),
        ...(diagnostics.postprocessRenderTargets !== undefined ? { renderTargets: diagnostics.postprocessRenderTargets } : {}),
        ...(diagnostics.postprocessTextures !== undefined ? { textures: diagnostics.postprocessTextures } : {}),
        ...(diagnostics.postprocessTargetWidth !== undefined ? { targetWidth: diagnostics.postprocessTargetWidth } : {}),
        ...(diagnostics.postprocessTargetHeight !== undefined ? { targetHeight: diagnostics.postprocessTargetHeight } : {})
    };
    return {
        backend: device.kind,
        renderer: device.info.renderer,
        vendor: device.info.vendor,
        capabilities: device.info.capabilities ?? [],
        limitations: device.info.limitations ?? [],
        drawCalls: diagnostics.drawCalls,
        resources,
        ...(Object.keys(memoryBytes).length > 0 ? { memoryBytes } : {}),
        ...(Object.keys(scene).length > 0 ? { scene } : {}),
        ...(Object.keys(postprocess).length > 0 ? { postprocess } : {}),
        contextLost: diagnostics.contextLost,
        lastError: diagnostics.lastError
    };
}
function captureRatio(count, total) {
    return roundCaptureMetric(total > 0 ? count / total : 0);
}
function roundCaptureMetric(value) {
    if (!Number.isFinite(value))
        return 0;
    return Math.round(value * 1000000) / 1000000;
}
const SHA256_INITIAL_HASH = [
    0x6a09e667,
    0xbb67ae85,
    0x3c6ef372,
    0xa54ff53a,
    0x510e527f,
    0x9b05688c,
    0x1f83d9ab,
    0x5be0cd19
];
const SHA256_ROUND_CONSTANTS = [
    0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
    0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
    0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
    0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
    0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
    0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
    0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
    0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
];
function sha256Hex(bytes) {
    const paddedLength = Math.ceil((bytes.length + 9) / 64) * 64;
    const bitLengthHigh = Math.floor(bytes.length / 0x20000000);
    const bitLengthLow = (bytes.length % 0x20000000) * 8;
    const hash = [...SHA256_INITIAL_HASH];
    const words = new Uint32Array(64);
    for (let block = 0; block < paddedLength; block += 64) {
        for (let index = 0; index < 16; index += 1) {
            const offset = block + index * 4;
            words[index] = ((sha256PaddedByte(bytes, offset, paddedLength, bitLengthHigh, bitLengthLow) << 24) |
                (sha256PaddedByte(bytes, offset + 1, paddedLength, bitLengthHigh, bitLengthLow) << 16) |
                (sha256PaddedByte(bytes, offset + 2, paddedLength, bitLengthHigh, bitLengthLow) << 8) |
                sha256PaddedByte(bytes, offset + 3, paddedLength, bitLengthHigh, bitLengthLow)) >>> 0;
        }
        for (let index = 16; index < 64; index += 1) {
            const first = words[index - 15];
            const second = words[index - 2];
            const sigma0 = rightRotate32(first, 7) ^ rightRotate32(first, 18) ^ (first >>> 3);
            const sigma1 = rightRotate32(second, 17) ^ rightRotate32(second, 19) ^ (second >>> 10);
            words[index] = (words[index - 16] + sigma0 + words[index - 7] + sigma1) >>> 0;
        }
        let a = hash[0];
        let b = hash[1];
        let c = hash[2];
        let d = hash[3];
        let e = hash[4];
        let f = hash[5];
        let g = hash[6];
        let h = hash[7];
        for (let index = 0; index < 64; index += 1) {
            const sigma1 = rightRotate32(e, 6) ^ rightRotate32(e, 11) ^ rightRotate32(e, 25);
            const choice = (e & f) ^ (~e & g);
            const temp1 = (h + sigma1 + choice + SHA256_ROUND_CONSTANTS[index] + words[index]) >>> 0;
            const sigma0 = rightRotate32(a, 2) ^ rightRotate32(a, 13) ^ rightRotate32(a, 22);
            const majority = (a & b) ^ (a & c) ^ (b & c);
            const temp2 = (sigma0 + majority) >>> 0;
            h = g;
            g = f;
            f = e;
            e = (d + temp1) >>> 0;
            d = c;
            c = b;
            b = a;
            a = (temp1 + temp2) >>> 0;
        }
        hash[0] = (hash[0] + a) >>> 0;
        hash[1] = (hash[1] + b) >>> 0;
        hash[2] = (hash[2] + c) >>> 0;
        hash[3] = (hash[3] + d) >>> 0;
        hash[4] = (hash[4] + e) >>> 0;
        hash[5] = (hash[5] + f) >>> 0;
        hash[6] = (hash[6] + g) >>> 0;
        hash[7] = (hash[7] + h) >>> 0;
    }
    return hash.map((word) => word.toString(16).padStart(8, "0")).join("");
}
function sha256PaddedByte(bytes, index, paddedLength, bitLengthHigh, bitLengthLow) {
    if (index < bytes.length)
        return bytes[index];
    if (index === bytes.length)
        return 0x80;
    const lengthOffset = index - (paddedLength - 8);
    if (lengthOffset < 0)
        return 0;
    if (lengthOffset < 4)
        return (bitLengthHigh >>> ((3 - lengthOffset) * 8)) & 0xff;
    return (bitLengthLow >>> ((7 - lengthOffset) * 8)) & 0xff;
}
function rightRotate32(value, bits) {
    return ((value >>> bits) | (value << (32 - bits))) >>> 0;
}
function withRendererFrameDiagnostics(diagnostics, collection, postprocess) {
    return {
        ...diagnostics,
        submittedObjects: collection.submittedObjects,
        visibleObjects: collection.visibleObjects,
        culledObjects: collection.culledObjects,
        frustumTestedObjects: collection.frustumTestedObjects,
        ...(postprocess ?? {})
    };
}
function renderableWorldBounds(geometry, modelMatrix, instanceTransforms, morphTargets, morphWeights, skinning) {
    const envelope = computeSkinnedMorphTargetWeightedBounds(geometry, skinning, morphTargets, morphWeights);
    const local = new SceneBounds3([envelope.min[0], envelope.min[1], envelope.min[2]], [envelope.max[0], envelope.max[1], envelope.max[2]]);
    if (!instanceTransforms) {
        return local.transform(modelMatrix);
    }
    let bounds = new SceneBounds3();
    for (let offset = 0; offset < instanceTransforms.length; offset += 16) {
        const instanceMatrix = toMat4(instanceTransforms.slice(offset, offset + 16), "instanceTransforms");
        bounds = bounds.union(local.transform(multiplyMat4(modelMatrix, instanceMatrix)));
    }
    return bounds;
}
function applyViewProjection(items, cameraViewProjection) {
    const hasCameraViewProjection = cameraViewProjection !== undefined;
    const viewProjection = cameraViewProjection ?? identityMat4();
    return items.map((item) => {
        const modelMatrix = toMat4(item.modelMatrix ?? identityMat4(), "modelMatrix", item.label);
        const explicitModelViewProjection = item.modelViewProjectionMatrix
            ? toMat4(item.modelViewProjectionMatrix, "modelViewProjectionMatrix", item.label)
            : undefined;
        return {
            ...item,
            modelMatrix,
            normalMatrix: item.normalMatrix ? toMat4(item.normalMatrix, "normalMatrix", item.label) : normalMatrixFromModel(modelMatrix),
            modelViewProjectionMatrix: hasCameraViewProjection || !explicitModelViewProjection
                ? multiplyMat4(viewProjection, modelMatrix)
                : explicitModelViewProjection
        };
    });
}
function createAutoFrameCamera(source, items, width, height) {
    const bounds = collectCameraFrameBounds(source) ?? collectRenderItemBounds(items);
    if (!bounds || bounds.isEmpty()) {
        return undefined;
    }
    const frame = computePerspectiveCameraFrame(bounds, { width, height }, {
        ...DEFAULT_RENDERER_AUTO_FRAME_OPTIONS,
        ...collectCameraFrameOptions(source)
    });
    return {
        viewProjectionMatrix: frame.viewProjectionMatrix,
        cameraPosition: frame.cameraPosition
    };
}
function collectRenderItemBounds(items) {
    let bounds;
    for (const item of items) {
        if (item.includeInAutoFrame === false)
            continue;
        const itemBounds = renderableWorldBounds(item.geometry, toMat4(item.modelMatrix ?? identityMat4(), "modelMatrix", item.label), item.instanceTransforms, item.morphTargets, item.morphWeights, item.skinning);
        bounds = bounds ? bounds.union(itemBounds) : itemBounds;
    }
    return bounds;
}
function collectCameraFrameOptions(source) {
    if (source instanceof Scene || isIterable(source) || !source.cameraFrameOptions)
        return {};
    return source.cameraFrameOptions;
}
function collectCameraFrameBounds(source) {
    if (source instanceof Scene || isIterable(source) || !source.cameraFrameBounds)
        return undefined;
    const bounds = source.cameraFrameBounds;
    const min = bounds.min;
    const max = bounds.max;
    const values = [...min, ...max];
    if (values.length !== 6 || values.some((value) => !Number.isFinite(value))) {
        throw new RenderDeviceError("RenderSource cameraFrameBounds must contain finite min/max vectors.", "CAMERA_FRAME_BOUNDS_INVALID", {
            min: [...min],
            max: [...max]
        });
    }
    if (max[0] < min[0] || max[1] < min[1] || max[2] < min[2]) {
        throw new RenderDeviceError("RenderSource cameraFrameBounds max must be greater than or equal to min.", "CAMERA_FRAME_BOUNDS_INVALID", {
            min: [...min],
            max: [...max]
        });
    }
    return new SceneBounds3([min[0], min[1], min[2]], [max[0], max[1], max[2]]);
}
function resolveCamera(source, camera, viewport, options = {}) {
    const resolved = camera ?? (options.allowSceneCamera === false ? undefined : resolveSceneCamera(source));
    if (!resolved) {
        return undefined;
    }
    const resize = maybeCameraResize(resolved);
    if (viewport && resize) {
        resize(viewport.width, viewport.height);
    }
    resolved.updateCameraMatrices?.();
    if (resolved.viewProjectionMatrix) {
        const viewMatrix = resolved.viewMatrix ? toMat4(resolved.viewMatrix, "viewMatrix") : undefined;
        return {
            viewProjectionMatrix: toMat4(resolved.viewProjectionMatrix, "viewProjectionMatrix"),
            ...(resolved instanceof Camera
                ? { camera: resolved, cameraPosition: cameraWorldPosition(resolved) }
                : viewMatrix
                    ? { cameraPosition: cameraPositionFromViewMatrix(viewMatrix) }
                    : {})
        };
    }
    if (resolved.projectionMatrix && resolved.viewMatrix) {
        const viewMatrix = toMat4(resolved.viewMatrix, "viewMatrix");
        return {
            viewProjectionMatrix: multiplyMat4(toMat4(resolved.projectionMatrix, "projectionMatrix"), viewMatrix),
            ...(resolved instanceof Camera
                ? { camera: resolved, cameraPosition: cameraWorldPosition(resolved) }
                : { cameraPosition: cameraPositionFromViewMatrix(viewMatrix) })
        };
    }
    throw new RenderDeviceError("Renderer camera must expose a viewProjectionMatrix or projectionMatrix plus viewMatrix", "CAMERA_VIEW_PROJECTION_MISSING");
}
function hasExplicitAutoFrameCameraPolicy(source) {
    return !(source instanceof Scene) && !isIterable(source) && source.cameraPolicy === "auto-frame";
}
function cameraWorldPosition(camera) {
    const matrix = camera.transform.worldMatrix;
    return [matrix[12] ?? 0, matrix[13] ?? 0, matrix[14] ?? 0];
}
function cameraPositionFromViewMatrix(viewMatrix) {
    const inverseView = invertMat4(viewMatrix);
    return [inverseView[12] ?? 0, inverseView[13] ?? 0, inverseView[14] ?? 0];
}
function maybeCameraResize(camera) {
    const candidate = camera.resize;
    return typeof candidate === "function" ? candidate.bind(camera) : undefined;
}
function resolveSceneCamera(source) {
    return sceneFromSource(source)?.collectCameras()[0];
}
function sceneFromSource(source) {
    return source instanceof Scene ? source : isIterable(source) ? undefined : source.scene;
}
function normalMatrixFromModel(modelMatrix) {
    const matrix = transposeMat4(invertMat4(modelMatrix));
    return [
        matrix[0], matrix[1], matrix[2], 0,
        matrix[4], matrix[5], matrix[6], 0,
        matrix[8], matrix[9], matrix[10], 0,
        0, 0, 0, 1
    ];
}
function transposeMat4(matrix) {
    return [
        matrix[0], matrix[4], matrix[8], matrix[12],
        matrix[1], matrix[5], matrix[9], matrix[13],
        matrix[2], matrix[6], matrix[10], matrix[14],
        matrix[3], matrix[7], matrix[11], matrix[15]
    ];
}
function toMat4(value, field, label) {
    const values = Array.from(value);
    if (values.length !== 16 || !values.every(Number.isFinite)) {
        throw new RenderDeviceError("Renderer matrix inputs must be finite mat4 values", "RENDERER_MATRIX_CONTRACT", {
            field,
            label,
            scalars: values.length
        });
    }
    return values;
}
function lookupRenderResource(lookup, key) {
    if (isReadonlyMap(lookup)) {
        return lookup.get(key);
    }
    return lookup[key];
}
function isReadonlyMap(lookup) {
    return typeof lookup.get === "function";
}
function collectRenderLights(source) {
    if (source instanceof Scene) {
        return new LightCollector().collect(source);
    }
    if (isIterable(source)) {
        return [];
    }
    if (source.collectedLights !== undefined) {
        return [...source.collectedLights];
    }
    if (source.scene) {
        const collected = new LightCollector().collect(source.scene);
        if (collected.length > 0 || source.environmentLighting === false) {
            return collected;
        }
        return createDefaultRendererDirectLights();
    }
    if (source.environmentLighting !== false && (source.renderItems || source.collectRenderItems)) {
        return createDefaultRendererDirectLights();
    }
    return [];
}
function createDefaultRendererDirectLights() {
    const key = new DirectionalLight("default-renderer-key-light");
    const fill = new DirectionalLight("default-renderer-fill-light");
    const lights = [
        { source: key, ...DEFAULT_RENDERER_DIRECT_LIGHTING.key },
        { source: fill, ...DEFAULT_RENDERER_DIRECT_LIGHTING.fill }
    ];
    return lights.map((light) => {
        light.source.color = [...light.color];
        light.source.intensity = light.intensity;
        return {
            kind: "directional",
            color: [...light.color],
            intensity: light.intensity,
            position: [0, 0, 0],
            direction: normalizeDefaultLightDirection(light.direction),
            range: 0,
            spotAngle: 0,
            penumbra: 0,
            castsShadow: false,
            layerMask: 0xffffffff,
            source: light.source
        };
    });
}
function normalizeDefaultLightDirection(direction) {
    const length = Math.hypot(direction[0], direction[1], direction[2]);
    if (length <= 0 || !Number.isFinite(length)) {
        return [0, 0, -1];
    }
    return [direction[0] / length, direction[1] / length, direction[2] / length];
}
function cloneEnvironmentLighting(environment) {
    return {
        color: [...environment.color],
        intensity: environment.intensity,
        ...(environment.proceduralMap
            ? {
                proceduralMap: {
                    skyColor: [...environment.proceduralMap.skyColor],
                    horizonColor: [...environment.proceduralMap.horizonColor],
                    groundColor: [...environment.proceduralMap.groundColor],
                    specularColor: [...environment.proceduralMap.specularColor],
                    intensity: environment.proceduralMap.intensity,
                    specularIntensity: environment.proceduralMap.specularIntensity
                }
            }
            : {}),
        ...(environment.environmentMapTexture ? { environmentMapTexture: environment.environmentMapTexture } : {}),
        ...(environment.environmentCubeMapTexture ? { environmentCubeMapTexture: environment.environmentCubeMapTexture } : {}),
        ...(environment.environmentMapIntensity !== undefined ? { environmentMapIntensity: environment.environmentMapIntensity } : {}),
        ...(environment.environmentMapSpecularIntensity !== undefined ? { environmentMapSpecularIntensity: environment.environmentMapSpecularIntensity } : {}),
        ...(environment.environmentMapRotation !== undefined ? { environmentMapRotation: environment.environmentMapRotation } : {}),
        ...(environment.environmentMapMipCount !== undefined ? { environmentMapMipCount: environment.environmentMapMipCount } : {}),
        ...(environment.environmentMapEncoding ? { environmentMapEncoding: environment.environmentMapEncoding } : {}),
        ...(environment.environmentBrdfLutTexture ? { environmentBrdfLutTexture: environment.environmentBrdfLutTexture } : {})
    };
}
function isIterable(value) {
    return typeof value[Symbol.iterator] === "function";
}
//# sourceMappingURL=Renderer.js.map