Source: rendering/renderers/shared/texture/RenderableGCSystem.ts

import { ExtensionType } from '../../../../extensions/Extensions';

import type { Renderer } from '../../types';
import type { InstructionSet } from '../instructions/InstructionSet';
import type { RenderPipe } from '../instructions/RenderPipe';
import type { Renderable } from '../Renderable';
import type { System } from '../system/System';

/**
 * Options for the RenderableGCSystem.
 * @memberof rendering
 * @property {boolean} [renderableGCActive=true] - If set to true, this will enable the garbage collector on the renderables.
 * @property {number} [renderableGCAMaxIdle=60000] -
 * The maximum idle frames before a texture is destroyed by garbage collection.
 * @property {number} [renderableGCCheckCountMax=60000] - time between two garbage collections.
 */
export interface RenderableGCSystemOptions
{
    /**
     * If set to true, this will enable the garbage collector on the GPU.
     * @default true
     * @memberof rendering.SharedRendererOptions
     */
    renderableGCActive: boolean;
    /**
     * The maximum idle frames before a texture is destroyed by garbage collection.
     * @default 60 * 60
     * @memberof rendering.SharedRendererOptions
     */
    renderableGCMaxUnusedTime: number;
    /**
     * Frames between two garbage collections.
     * @default 600
     * @memberof rendering.SharedRendererOptions
     */
    renderableGCFrequency: number;
}
/**
 * System plugin to the renderer to manage renderable garbage collection. When rendering
 * stuff with the renderer will assign resources to each renderable. This could be for example
 * a batchable Sprite, or a text texture. If the renderable is not used for a certain amount of time
 * its resources will be tided up by its render pipe.
 * @memberof rendering
 */
export class RenderableGCSystem implements System<RenderableGCSystemOptions>
{
    /** @ignore */
    public static extension = {
        type: [
            ExtensionType.WebGLSystem,
            ExtensionType.WebGPUSystem,
        ],
        name: 'renderableGC',
    } as const;

    /** default options for the renderableGCSystem */
    public static defaultOptions: RenderableGCSystemOptions = {
        /**
         * If set to true, this will enable the garbage collector on the GPU.
         * @default true
         */
        renderableGCActive: true,
        /**
         * The maximum idle frames before a texture is destroyed by garbage collection.
         * @default 60 * 60
         */
        renderableGCMaxUnusedTime: 60000,
        /**
         * Frames between two garbage collections.
         * @default 600
         */
        renderableGCFrequency: 30000,
    };

    /**
     * Maximum idle frames before a texture is destroyed by garbage collection.
     * @see renderableGCSystem.defaultMaxIdle
     */
    public maxUnusedTime: number;

    private _renderer: Renderer;

    private readonly _managedRenderables: Renderable[] = [];
    private _handler: number;
    private _frequency: number;
    private _now: number;

    /** @param renderer - The renderer this System works for. */
    constructor(renderer: Renderer)
    {
        this._renderer = renderer;
    }

    public init(options: RenderableGCSystemOptions): void
    {
        options = { ...RenderableGCSystem.defaultOptions, ...options };

        this.maxUnusedTime = options.renderableGCMaxUnusedTime;
        this._frequency = options.renderableGCFrequency;

        this.enabled = options.renderableGCActive;
    }

    get enabled(): boolean
    {
        return !!this._handler;
    }

    set enabled(value: boolean)
    {
        if (this.enabled === value) return;

        if (value)
        {
            this._handler = this._renderer.scheduler.repeat(
                () => this.run(),
                this._frequency
            );
        }
        else
        {
            this._renderer.scheduler.cancel(this._handler);
        }
    }

    public prerender(): void
    {
        this._now = performance.now();
    }

    public addRenderable(renderable: Renderable, instructionSet: InstructionSet): void
    {
        renderable._lastUsed = this._now;

        if (renderable._lastInstructionTick === -1)
        {
            // TODO manage index...
            this._managedRenderables.push(renderable);
        }

        renderable._lastInstructionTick = instructionSet.tick;
    }

    /** Runs the scheduled garbage collection */
    public run(): void
    {
        const now = performance.now();

        const managedRenderables = this._managedRenderables;

        const renderPipes = this._renderer.renderPipes;

        let offset = 0;

        for (let i = 0; i < managedRenderables.length; i++)
        {
            const renderable = managedRenderables[i];

            const renderGroup = renderable.renderGroup ?? renderable.parentRenderGroup;
            const currentIndex = renderGroup?.instructionSet?.tick ?? -1;

            if (renderable._lastInstructionTick !== currentIndex && now - renderable._lastUsed > this.maxUnusedTime)
            {
                if (!renderable.destroyed)
                {
                    const rp = renderPipes as unknown as Record<string, RenderPipe>;

                    rp[renderable.renderPipeId].destroyRenderable(renderable);
                }

                // remove from the array as this has been destroyed..
                renderable._lastInstructionTick = -1;
                offset++;
            }
            else
            {
                managedRenderables[i - (offset)] = renderable;
            }
        }

        managedRenderables.length = managedRenderables.length - offset;
    }

    public destroy(): void
    {
        this.enabled = false;
        this._renderer = null as any as Renderer;
        this._managedRenderables.length = 0;
    }
}