Source: rendering/renderers/shared/renderTarget/RenderTargetSystem.ts

import { Matrix } from '../../../../maths/matrix/Matrix';
import { Rectangle } from '../../../../maths/shapes/Rectangle';
import { CLEAR } from '../../gl/const';
import { calculateProjection } from '../../gpu/renderTarget/calculateProjection';
import { SystemRunner } from '../system/SystemRunner';
import { CanvasSource } from '../texture/sources/CanvasSource';
import { TextureSource } from '../texture/sources/TextureSource';
import { Texture } from '../texture/Texture';
import { getCanvasTexture } from '../texture/utils/getCanvasTexture';
import { isRenderingToScreen } from './isRenderingToScreen';
import { RenderTarget } from './RenderTarget';

import type { RgbaArray } from '../../../../color/Color';
import type { ICanvas } from '../../../../environment/canvas/ICanvas';
import type { CLEAR_OR_BOOL } from '../../gl/const';
import type { GlRenderTarget } from '../../gl/GlRenderTarget';
import type { GpuRenderTarget } from '../../gpu/renderTarget/GpuRenderTarget';
import type { Renderer } from '../../types';
import type { System } from '../system/System';
import type { BindableTexture } from '../texture/Texture';

/**
 * A render surface is a texture, canvas, or render target
 * @memberof rendering
 * @see environment.ICanvas
 * @see rendering.Texture
 * @see rendering.RenderTarget
 */
export type RenderSurface = ICanvas | BindableTexture | RenderTarget;

/**
 * stores a render target and its frame
 * @ignore
 */
interface RenderTargetAndFrame
{
    /** the render target */
    renderTarget: RenderTarget;
    /** the frame to use when using the render target */
    frame: Rectangle
}

/**
 * An adaptor interface for RenderTargetSystem to support WebGL and WebGPU.
 * This is used internally by the renderer, and is not intended to be used directly.
 * @ignore
 */
export interface RenderTargetAdaptor<RENDER_TARGET extends GlRenderTarget | GpuRenderTarget>
{
    init(
        /** the renderer */
        renderer: Renderer,
        /** the render target system */
        renderTargetSystem: RenderTargetSystem<RENDER_TARGET>
    ): void

    /** A function copies the contents of a render surface to a texture */
    copyToTexture(
        /** the render surface to copy from  */
        sourceRenderSurfaceTexture: RenderTarget,
        /** the texture to copy to */
        destinationTexture: Texture,
        /** the origin of the copy */
        originSrc: { x: number; y: number },
        /** the size of the copy */
        size: { width: number; height: number },
        /** the destination origin (top left to paste from!) */
        originDest?: { x: number; y: number },
    ): Texture

    /** starts a render pass on the render target */
    startRenderPass(
        /** the render target to start the render pass on */
        renderTarget: RenderTarget,
        /* the clear mode to use. Can be true or a CLEAR number 'COLOR | DEPTH | STENCIL' 0b111* */
        clear: CLEAR_OR_BOOL,
        /** the color to clear to */
        clearColor?: RgbaArray,
        /** the viewport to use */
        viewport?: Rectangle
    ): void

    /** clears the current render target to the specified color */
    clear(
        /** the render target to clear */
        renderTarget: RenderTarget,
        /** the clear mode to use. Can be true or a CLEAR number 'COLOR | DEPTH | STENCIL' 0b111 */
        clear: CLEAR_OR_BOOL,
        /** the color to clear to   */
        clearColor?: RgbaArray,
        /** the viewport to use */
        viewport?: Rectangle
    ): void

    /** finishes the current render pass */
    finishRenderPass(renderTarget: RenderTarget): void

    /**
     * initializes a gpu render target. Both renderers use this function to initialize a gpu render target
     * Its different type of object depending on the renderer.
     */
    initGpuRenderTarget(
        /** the render target to initialize */
        renderTarget: RenderTarget
    ): RENDER_TARGET

    /** called when a render target is resized */
    resizeGpuRenderTarget(
        /** the render target to resize */
        renderTarget: RenderTarget
    ): void

    /** destroys the gpu render target */
    destroyGpuRenderTarget(
        /** the render target to destroy */
        gpuRenderTarget: RENDER_TARGET
    ): void
}

/**
 * A system that manages render targets. A render target is essentially a place where the shaders can color in the pixels.
 * The render target system is responsible for binding the render target to the renderer, and managing the viewport.
 * Render targets can be pushed and popped.
 *
 * To make it easier, you can also bind textures and canvases too. This will automatically create a render target for you.
 * The render target itself is a lot more powerful than just a texture or canvas,
 * as it can have multiple textures attached to it.
 * It will also give ou fine grain control over the stencil buffer / depth texture.
 * @example
 *
 * ```js
 *
 * // create a render target
 * const renderTarget = new RenderTarget({
 *   colorTextures: [new TextureSource({ width: 100, height: 100 })],
 * });
 *
 * // bind the render target
 * renderer.renderTarget.bind(renderTarget);
 *
 * // draw something!
 * ```
 * @memberof rendering
 */
export class RenderTargetSystem<RENDER_TARGET extends GlRenderTarget | GpuRenderTarget> implements System
{
    /** When rendering of a scene begins, this is where the root render surface is stored */
    public rootRenderTarget: RenderTarget;
    /** This is the root viewport for the render pass*/
    public rootViewPort = new Rectangle();
    /** A boolean that lets the dev know if the current render pass is rendering to the screen. Used by some plugins */
    public renderingToScreen: boolean;
    /** the current active render target */
    public renderTarget: RenderTarget;
    /** the current active render surface that the render target is created from */
    public renderSurface: RenderSurface;
    /** the current viewport that the gpu is using */
    public readonly viewport = new Rectangle();
    /**
     * a runner that lets systems know if the active render target has changed.
     * Eg the Stencil System needs to know so it can manage the stencil buffer
     */
    public readonly onRenderTargetChange = new SystemRunner('onRenderTargetChange');
    /** the projection matrix that is used by the shaders based on the active render target and the viewport */
    public readonly projectionMatrix = new Matrix();
    /** the default clear color for render targets */
    public readonly defaultClearColor: RgbaArray = [0, 0, 0, 0];
    /** a reference to the adaptor that interfaces with WebGL / WebGP */
    public readonly adaptor: RenderTargetAdaptor<RENDER_TARGET>;
    /**
     * a hash that stores the render target for a given render surface. When you pass in a texture source,
     * a render target is created for it. This map stores and makes it easy to retrieve the render target
     */
    private readonly _renderSurfaceToRenderTargetHash: Map<RenderSurface, RenderTarget>
        = new Map();
    /** A hash that stores a gpu render target for a given render target. */
    private _gpuRenderTargetHash: Record<number, RENDER_TARGET> = Object.create(null);
    /**
     * A stack that stores the render target and frame that is currently being rendered to.
     * When push is called, the current render target is stored in this stack.
     * When pop is called, the previous render target is restored.
     */
    private readonly _renderTargetStack: RenderTargetAndFrame[] = [];
    /** A reference to the renderer */
    private readonly _renderer: Renderer;

    constructor(renderer: Renderer)
    {
        this._renderer = renderer;
    }

    /** called when dev wants to finish a render pass */
    public finishRenderPass()
    {
        this.adaptor.finishRenderPass(this.renderTarget);
    }

    /**
     * called when the renderer starts to render a scene.
     * @param options
     * @param options.target - the render target to render to
     * @param options.clear - the clear mode to use. Can be true or a CLEAR number 'COLOR | DEPTH | STENCIL' 0b111
     * @param options.clearColor - the color to clear to
     * @param options.frame - the frame to render to
     */
    public renderStart({
        target,
        clear,
        clearColor,
        frame
    }: {
        target: RenderSurface;
        clear: CLEAR_OR_BOOL;
        clearColor: RgbaArray;
        frame?: Rectangle
    }): void
    {
        // TODO no need to reset this - use optimised index instead
        this._renderTargetStack.length = 0;

        this.push(
            target,
            clear,
            clearColor,
            frame
        );

        this.rootViewPort.copyFrom(this.viewport);
        this.rootRenderTarget = this.renderTarget;
        this.renderingToScreen = isRenderingToScreen(this.rootRenderTarget);
    }

    /**
     * Binding a render surface! This is the main function of the render target system.
     * It will take the RenderSurface (which can be a texture, canvas, or render target) and bind it to the renderer.
     * Once bound all draw calls will be rendered to the render surface.
     *
     * If a frame is not provide and the render surface is a texture, the frame of the texture will be used.
     * @param renderSurface - the render surface to bind
     * @param clear - the clear mode to use. Can be true or a CLEAR number 'COLOR | DEPTH | STENCIL' 0b111
     * @param clearColor - the color to clear to
     * @param frame - the frame to render to
     * @returns the render target that was bound
     */
    public bind(
        renderSurface: RenderSurface,
        clear: CLEAR_OR_BOOL = true,
        clearColor?: RgbaArray,
        frame?: Rectangle
    ): RenderTarget
    {
        const renderTarget = this.getRenderTarget(renderSurface);

        const didChange = this.renderTarget !== renderTarget;

        this.renderTarget = renderTarget;
        this.renderSurface = renderSurface;

        const gpuRenderTarget = this.getGpuRenderTarget(renderTarget);

        if (renderTarget.pixelWidth !== gpuRenderTarget.width
            || renderTarget.pixelHeight !== gpuRenderTarget.height)
        {
            this.adaptor.resizeGpuRenderTarget(renderTarget);

            gpuRenderTarget.width = renderTarget.pixelWidth;
            gpuRenderTarget.height = renderTarget.pixelHeight;
        }

        const source = renderTarget.colorTexture;
        const viewport = this.viewport;

        const pixelWidth = source.pixelWidth;
        const pixelHeight = source.pixelHeight;

        if (!frame && renderSurface instanceof Texture)
        {
            frame = renderSurface.frame;
        }

        if (frame)
        {
            const resolution = source._resolution;

            viewport.x = ((frame.x * resolution) + 0.5) | 0;
            viewport.y = ((frame.y * resolution) + 0.5) | 0;
            viewport.width = ((frame.width * resolution) + 0.5) | 0;
            viewport.height = ((frame.height * resolution) + 0.5) | 0;
        }
        else
        {
            viewport.x = 0;
            viewport.y = 0;
            viewport.width = pixelWidth;
            viewport.height = pixelHeight;
        }

        calculateProjection(
            this.projectionMatrix,
            0, 0,
            viewport.width / source.resolution,
            viewport.height / source.resolution,
            !renderTarget.isRoot
        );

        this.adaptor.startRenderPass(renderTarget, clear, clearColor, viewport);

        if (didChange)
        {
            this.onRenderTargetChange.emit(renderTarget);
        }

        return renderTarget;
    }

    public clear(
        target?: RenderSurface,
        clear: CLEAR_OR_BOOL = CLEAR.ALL,
        clearColor?: RgbaArray,
    )
    {
        if (!clear) return;

        if (target)
        {
            target = this.getRenderTarget(target);
        }

        this.adaptor.clear(
            (target as RenderTarget) || this.renderTarget,
            clear,
            clearColor,
            this.viewport
        );
    }

    protected contextChange(): void
    {
        this._gpuRenderTargetHash = Object.create(null);
    }

    /**
     * Push a render surface to the renderer. This will bind the render surface to the renderer,
     * @param renderSurface - the render surface to push
     * @param clear - the clear mode to use. Can be true or a CLEAR number 'COLOR | DEPTH | STENCIL' 0b111
     * @param clearColor - the color to clear to
     * @param frame - the frame to use when rendering to the render surface
     */
    public push(
        renderSurface: RenderSurface,
        clear: CLEAR | boolean = CLEAR.ALL,
        clearColor?: RgbaArray,
        frame?: Rectangle
    )
    {
        const renderTarget = this.bind(renderSurface, clear, clearColor, frame);

        this._renderTargetStack.push({
            renderTarget,
            frame,
        });

        return renderTarget;
    }

    /** Pops the current render target from the renderer and restores the previous render target. */
    public pop()
    {
        this._renderTargetStack.pop();

        const currentRenderTargetData = this._renderTargetStack[this._renderTargetStack.length - 1];

        this.bind(currentRenderTargetData.renderTarget, false, null, currentRenderTargetData.frame);
    }

    /**
     * Gets the render target from the provide render surface. Eg if its a texture,
     * it will return the render target for the texture.
     * If its a render target, it will return the same render target.
     * @param renderSurface - the render surface to get the render target for
     * @returns the render target for the render surface
     */
    public getRenderTarget(renderSurface: RenderSurface): RenderTarget
    {
        if (((renderSurface as Texture).isTexture))
        {
            renderSurface = (renderSurface as Texture).source;
        }

        return this._renderSurfaceToRenderTargetHash.get(renderSurface)
        ?? this._initRenderTarget(renderSurface);
    }

    /**
     * Copies a render surface to another texture
     * @param sourceRenderSurfaceTexture - the render surface to copy from
     * @param destinationTexture - the texture to copy to
     * @param originSrc - the origin of the copy
     * @param originSrc.x - the x origin of the copy
     * @param originSrc.y - the y origin of the copy
     * @param size - the size of the copy
     * @param size.width - the width of the copy
     * @param size.height - the height of the copy
     * @param originDest - the destination origin (top left to paste from!)
     * @param originDest.x - the x origin of the paste
     * @param originDest.y - the y origin of the paste
     */
    public copyToTexture(
        sourceRenderSurfaceTexture: RenderTarget,
        destinationTexture: Texture,
        originSrc: { x: number; y: number },
        size: { width: number; height: number },
        originDest: { x: number; y: number; },
    )
    {
        // fit the size to the source we don't want to go out of bounds

        if (originSrc.x < 0)
        {
            size.width += originSrc.x;
            originDest.x -= originSrc.x;
            originSrc.x = 0;
        }

        if (originSrc.y < 0)
        {
            size.height += originSrc.y;
            originDest.y -= originSrc.y;
            originSrc.y = 0;
        }

        const { pixelWidth, pixelHeight } = sourceRenderSurfaceTexture;

        size.width = Math.min(size.width, pixelWidth - originSrc.x);
        size.height = Math.min(size.height, pixelHeight - originSrc.y);

        return this.adaptor.copyToTexture(
            sourceRenderSurfaceTexture,
            destinationTexture,
            originSrc,
            size,
            originDest
        );
    }

    /**
     * ensures that we have a depth stencil buffer available to render to
     * This is used by the mask system to make sure we have a stencil buffer.
     */
    public ensureDepthStencil()
    {
        if (!this.renderTarget.stencil)
        {
            this.renderTarget.stencil = true;

            this.adaptor.startRenderPass(this.renderTarget, false, null, this.viewport);
        }
    }

    /** nukes the render target system */
    public destroy()
    {
        (this._renderer as null) = null;

        this._renderSurfaceToRenderTargetHash.forEach((renderTarget, key) =>
        {
            if (renderTarget !== key)
            {
                renderTarget.destroy();
            }
        });

        this._renderSurfaceToRenderTargetHash.clear();

        this._gpuRenderTargetHash = Object.create(null);
    }

    private _initRenderTarget(renderSurface: RenderSurface): RenderTarget
    {
        let renderTarget: RenderTarget = null;

        if (CanvasSource.test(renderSurface))
        {
            renderSurface = getCanvasTexture(renderSurface as ICanvas).source;
        }

        if (renderSurface instanceof RenderTarget)
        {
            renderTarget = renderSurface;
        }
        else if (renderSurface instanceof TextureSource)
        {
            renderTarget = new RenderTarget({
                colorTextures: [renderSurface],
            });

            if (CanvasSource.test(renderSurface.source.resource))
            {
                renderTarget.isRoot = true;
            }

            // TODO add a test for this
            renderSurface.once('destroy', () =>
            {
                renderTarget.destroy();

                const gpuRenderTarget = this._gpuRenderTargetHash[renderTarget.uid];

                if (gpuRenderTarget)
                {
                    this._gpuRenderTargetHash[renderTarget.uid] = null;
                    this.adaptor.destroyGpuRenderTarget(gpuRenderTarget);
                }
            });
        }

        this._renderSurfaceToRenderTargetHash.set(renderSurface, renderTarget);

        return renderTarget;
    }

    public getGpuRenderTarget(renderTarget: RenderTarget)
    {
        return this._gpuRenderTargetHash[renderTarget.uid]
        || (this._gpuRenderTargetHash[renderTarget.uid] = this.adaptor.initGpuRenderTarget(renderTarget));
    }
}