Source: rendering/renderers/gpu/pipeline/PipelineSystem.ts

import { ExtensionType } from '../../../../extensions/Extensions';
import { warn } from '../../../../utils/logging/warn';
import { ensureAttributes } from '../../gl/shader/program/ensureAttributes';
import { STENCIL_MODES } from '../../shared/state/const';
import { createIdFromString } from '../../shared/utils/createIdFromString';
import { GpuStencilModesToPixi } from '../state/GpuStencilModesToPixi';

import type { Topology } from '../../shared/geometry/const';
import type { Geometry } from '../../shared/geometry/Geometry';
import type { State } from '../../shared/state/State';
import type { System } from '../../shared/system/System';
import type { GPU } from '../GpuDeviceSystem';
import type { GpuRenderTarget } from '../renderTarget/GpuRenderTarget';
import type { GpuProgram } from '../shader/GpuProgram';
import type { StencilState } from '../state/GpuStencilModesToPixi';
import type { WebGPURenderer } from '../WebGPURenderer';

const topologyStringToId = {
    'point-list': 0,
    'line-list': 1,
    'line-strip': 2,
    'triangle-list': 3,
    'triangle-strip': 4,
};

// geometryLayouts = 256; // 8 bits // 256 states // value 0-255;
// shaderKeys = 256; // 8 bits // 256 states // value 0-255;
// state = 64; // 6 bits // 64 states // value 0-63;
// blendMode = 32; // 5 bits // 32 states // value 0-31;
// topology = 8; // 3 bits // 8 states // value 0-7;
function getGraphicsStateKey(
    geometryLayout: number,
    shaderKey: number,
    state: number,
    blendMode: number,
    topology: number,
): number
{
    return (geometryLayout << 24) // Allocate the 8 bits for geometryLayouts at the top
         | (shaderKey << 16) // Next 8 bits for shaderKeys
         | (state << 10) // 6 bits for state
         | (blendMode << 5) // 5 bits for blendMode
         | topology; // And 3 bits for topology at the least significant position
}

// colorMask = 16;// 4 bits // 16 states // value 0-15;
// stencilState = 8; // 3 bits // 8 states // value 0-7;
// renderTarget = 1; // 2 bit // 3 states // value 0-3; // none, stencil, depth, depth-stencil
// multiSampleCount = 1; // 1 bit // 2 states // value 0-1;
function getGlobalStateKey(
    stencilStateId: number,
    multiSampleCount: number,
    colorMask: number,
    renderTarget: number,
): number
{
    return (colorMask << 6) // Allocate the 4 bits for colorMask at the top
         | (stencilStateId << 3) // Next 3 bits for stencilStateId
         | (renderTarget << 1) // 2 bits for renderTarget
         | multiSampleCount; // And 1 bit for multiSampleCount at the least significant position
}

type PipeHash = Record<number, GPURenderPipeline>;

/**
 * A system that creates and manages the GPU pipelines.
 *
 * Caching Mechanism: At its core, the system employs a two-tiered caching strategy to minimize
 * the redundant creation of GPU pipelines (or "pipes"). This strategy is based on generating unique
 * keys that represent the state of the graphics settings and the specific requirements of the
 * item being rendered. By caching these pipelines, subsequent draw calls with identical configurations
 * can reuse existing pipelines instead of generating new ones.
 *
 * State Management: The system differentiates between "global" state properties (like color masks
 * and stencil masks, which do not change frequently) and properties that may vary between draw calls
 * (such as geometry, shaders, and blend modes). Unique keys are generated for both these categories
 * using getStateKey for global state and getGraphicsStateKey for draw-specific settings. These keys are
 * then then used to caching the pipe. The next time we need a pipe we can check
 * the cache by first looking at the state cache and then the pipe cache.
 * @memberof rendering
 */
export class PipelineSystem implements System
{
    /** @ignore */
    public static extension = {
        type: [ExtensionType.WebGPUSystem],
        name: 'pipeline',
    } as const;
    private readonly _renderer: WebGPURenderer;

    protected CONTEXT_UID: number;

    private _moduleCache: Record<string, GPUShaderModule> = Object.create(null);
    private _bufferLayoutsCache: Record<number, GPUVertexBufferLayout[]> = Object.create(null);
    private readonly _bindingNamesCache: Record<string, Record<string, string>> = Object.create(null);

    private _pipeCache: PipeHash = Object.create(null);
    private readonly _pipeStateCaches: Record<number, PipeHash> = Object.create(null);

    private _gpu: GPU;
    private _stencilState: StencilState;

    private _stencilMode: STENCIL_MODES;
    private _colorMask = 0b1111;
    private _multisampleCount = 1;
    private _depthStencilAttachment: 0 | 1;

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

    protected contextChange(gpu: GPU): void
    {
        this._gpu = gpu;
        this.setStencilMode(STENCIL_MODES.DISABLED);

        this._updatePipeHash();
    }

    public setMultisampleCount(multisampleCount: number): void
    {
        if (this._multisampleCount === multisampleCount) return;

        this._multisampleCount = multisampleCount;

        this._updatePipeHash();
    }

    public setRenderTarget(renderTarget: GpuRenderTarget)
    {
        this._multisampleCount = renderTarget.msaaSamples;
        this._depthStencilAttachment = renderTarget.descriptor.depthStencilAttachment ? 1 : 0;

        this._updatePipeHash();
    }

    public setColorMask(colorMask: number): void
    {
        if (this._colorMask === colorMask) return;

        this._colorMask = colorMask;

        this._updatePipeHash();
    }

    public setStencilMode(stencilMode: STENCIL_MODES): void
    {
        if (this._stencilMode === stencilMode) return;

        this._stencilMode = stencilMode;
        this._stencilState = GpuStencilModesToPixi[stencilMode];

        this._updatePipeHash();
    }

    public setPipeline(geometry: Geometry, program: GpuProgram, state: State, passEncoder: GPURenderPassEncoder): void
    {
        const pipeline = this.getPipeline(geometry, program, state);

        passEncoder.setPipeline(pipeline);
    }

    public getPipeline(
        geometry: Geometry,
        program: GpuProgram,
        state: State,
        topology?: Topology,
    ): GPURenderPipeline
    {
        if (!geometry._layoutKey)
        {
            ensureAttributes(geometry, program.attributeData);

            // prepare the geometry for the pipeline
            this._generateBufferKey(geometry);
        }

        topology ||= geometry.topology;

        // now we have set the Ids - the key is different...
        const key = getGraphicsStateKey(
            geometry._layoutKey,
            program._layoutKey,
            state.data,
            state._blendModeId,
            topologyStringToId[topology],
        );

        if (this._pipeCache[key]) return this._pipeCache[key];

        this._pipeCache[key] = this._createPipeline(geometry, program, state, topology);

        return this._pipeCache[key];
    }

    private _createPipeline(geometry: Geometry, program: GpuProgram, state: State, topology: Topology): GPURenderPipeline
    {
        const device = this._gpu.device;

        const buffers = this._createVertexBufferLayouts(geometry, program);

        const blendModes = this._renderer.state.getColorTargets(state);

        blendModes[0].writeMask = this._stencilMode === STENCIL_MODES.RENDERING_MASK_ADD ? 0 : this._colorMask;

        const layout = this._renderer.shader.getProgramData(program).pipeline;

        const descriptor: GPURenderPipelineDescriptor = {
            // TODO later check if its helpful to create..
            // layout,
            vertex: {
                module: this._getModule(program.vertex.source),
                entryPoint: program.vertex.entryPoint,
                // geometry..
                buffers,
            },
            fragment: {
                module: this._getModule(program.fragment.source),
                entryPoint: program.fragment.entryPoint,
                targets: blendModes,
            },
            primitive: {
                topology,
                cullMode: state.cullMode,
            },
            layout,
            multisample: {
                count: this._multisampleCount,
            },
            // depthStencil,
            label: `PIXI Pipeline`,
        };

        // only apply if the texture has stencil or depth
        if (this._depthStencilAttachment)
        {
            // mask states..
            descriptor.depthStencil = {
                ...this._stencilState,
                format: 'depth24plus-stencil8',
                depthWriteEnabled: state.depthTest,
                depthCompare: state.depthTest ? 'less' : 'always',
            };
        }

        const pipeline = device.createRenderPipeline(descriptor);

        return pipeline;
    }

    private _getModule(code: string): GPUShaderModule
    {
        return this._moduleCache[code] || this._createModule(code);
    }

    private _createModule(code: string): GPUShaderModule
    {
        const device = this._gpu.device;

        this._moduleCache[code] = device.createShaderModule({
            code,
        });

        return this._moduleCache[code];
    }

    private _generateBufferKey(geometry: Geometry): number
    {
        const keyGen = [];
        let index = 0;
        // generate a key..

        const attributeKeys = Object.keys(geometry.attributes).sort();

        for (let i = 0; i < attributeKeys.length; i++)
        {
            const attribute = geometry.attributes[attributeKeys[i]];

            keyGen[index++] = attribute.offset;
            keyGen[index++] = attribute.format;
            keyGen[index++] = attribute.stride;
            keyGen[index++] = attribute.instance;
        }

        const stringKey = keyGen.join('|');

        geometry._layoutKey = createIdFromString(stringKey, 'geometry');

        return geometry._layoutKey;
    }

    private _generateAttributeLocationsKey(program: GpuProgram): number
    {
        const keyGen = [];
        let index = 0;
        // generate a key..

        const attributeKeys = Object.keys(program.attributeData).sort();

        for (let i = 0; i < attributeKeys.length; i++)
        {
            const attribute = program.attributeData[attributeKeys[i]];

            keyGen[index++] = attribute.location;
        }

        const stringKey = keyGen.join('|');

        program._attributeLocationsKey = createIdFromString(stringKey, 'programAttributes');

        return program._attributeLocationsKey;
    }

    /**
     * Returns a hash of buffer names mapped to bind locations.
     * This is used to bind the correct buffer to the correct location in the shader.
     * @param geometry - The geometry where to get the buffer names
     * @param program - The program where to get the buffer names
     * @returns An object of buffer names mapped to the bind location.
     */
    public getBufferNamesToBind(geometry: Geometry, program: GpuProgram): Record<string, string>
    {
        const key = (geometry._layoutKey << 16) | program._attributeLocationsKey;

        if (this._bindingNamesCache[key]) return this._bindingNamesCache[key];

        const data = this._createVertexBufferLayouts(geometry, program);

        // now map the data to the buffers..
        const bufferNamesToBind: Record<string, string> = Object.create(null);

        const attributeData = program.attributeData;

        for (let i = 0; i < data.length; i++)
        {
            const attributes = Object.values(data[i].attributes);

            const shaderLocation = attributes[0].shaderLocation;

            for (const j in attributeData)
            {
                if (attributeData[j].location === shaderLocation)
                {
                    bufferNamesToBind[i] = j;
                    break;
                }
            }
        }

        this._bindingNamesCache[key] = bufferNamesToBind;

        return bufferNamesToBind;
    }

    private _createVertexBufferLayouts(geometry: Geometry, program: GpuProgram): GPUVertexBufferLayout[]
    {
        if (!program._attributeLocationsKey) this._generateAttributeLocationsKey(program);

        const key = (geometry._layoutKey << 16) | program._attributeLocationsKey;

        if (this._bufferLayoutsCache[key])
        {
            return this._bufferLayoutsCache[key];
        }

        const vertexBuffersLayout: GPUVertexBufferLayout[] = [];

        geometry.buffers.forEach((buffer) =>
        {
            const bufferEntry: GPUVertexBufferLayout = {
                arrayStride: 0,
                stepMode: 'vertex',
                attributes: [],
            };

            const bufferEntryAttributes = bufferEntry.attributes as GPUVertexAttribute[];

            for (const i in program.attributeData)
            {
                const attribute = geometry.attributes[i];

                if ((attribute.divisor ?? 1) !== 1)
                {
                    // TODO: Maybe emulate divisor with storage_buffers/float_textures?
                    // For now just issue a warning
                    warn(`Attribute ${i} has an invalid divisor value of '${attribute.divisor}'. `
                        + 'WebGPU only supports a divisor value of 1');
                }

                if (attribute.buffer === buffer)
                {
                    bufferEntry.arrayStride = attribute.stride;
                    bufferEntry.stepMode = attribute.instance ? 'instance' : 'vertex';

                    bufferEntryAttributes.push({
                        shaderLocation: program.attributeData[i].location,
                        offset: attribute.offset,
                        format: attribute.format,
                    });
                }
            }

            if (bufferEntryAttributes.length)
            {
                vertexBuffersLayout.push(bufferEntry);
            }
        });

        this._bufferLayoutsCache[key] = vertexBuffersLayout;

        return vertexBuffersLayout;
    }

    private _updatePipeHash(): void
    {
        const key = getGlobalStateKey(
            this._stencilMode,
            this._multisampleCount,
            this._colorMask,
            this._depthStencilAttachment
        );

        if (!this._pipeStateCaches[key])
        {
            this._pipeStateCaches[key] = Object.create(null);
        }

        this._pipeCache = this._pipeStateCaches[key];
    }

    public destroy(): void
    {
        (this._renderer as null) = null;
        this._bufferLayoutsCache = null;
    }
}