Source: rendering/renderers/gl/shader/GlProgram.ts

import { createIdFromString } from '../../shared/utils/createIdFromString';
import { getMaxFragmentPrecision } from './program/getMaxFragmentPrecision';
import { addProgramDefines } from './program/preprocessors/addProgramDefines';
import { ensurePrecision } from './program/preprocessors/ensurePrecision';
import { insertVersion } from './program/preprocessors/insertVersion';
import { setProgramName } from './program/preprocessors/setProgramName';
import { stripVersion } from './program/preprocessors/stripVersion';

import type { TypedArray } from '../../shared/buffer/Buffer';
import type { ExtractedAttributeData } from './program/extractAttributesFromGlProgram';

export interface GlAttributeData
{
    type: string;
    size: number;
    location: number;
    name: string;
}

export interface GlUniformData
{
    name: string;
    index: number;
    type: string;
    size: number;
    isArray: boolean;
    value: any;
}

export interface GlUniformBlockData
{
    index: number;
    name: string;
    size: number;
    value?: TypedArray;
}

/**
 * The options for the gl program
 * @memberof rendering
 */
export interface GlProgramOptions
{
    /** The fragment glsl shader source. */
    fragment: string;
    /** The vertex glsl shader source. */
    vertex: string;
    /** the name of the program, defaults to 'pixi-program' */
    name?: string;
    /** the preferred vertex precision for the shader, this may not be used if the device does not support it  */
    preferredVertexPrecision?: string;
    /** the preferred fragment precision for the shader, this may not be used if the device does not support it  */
    preferredFragmentPrecision?: string;
}

const processes: Record<string, ((source: string, options: any, isFragment?: boolean) => string)> = {
    // strips any version headers..
    stripVersion,
    // adds precision string if not already present
    ensurePrecision,
    // add some defines if WebGL1 to make it more compatible with WebGL2 shaders
    addProgramDefines,
    // add the program name to the shader
    setProgramName,
    // add the version string to the shader header
    insertVersion,
};

const programCache: Record<string, GlProgram> = Object.create(null);

/**
 * A wrapper for a WebGL Program. You can create one and then pass it to a shader.
 * This will manage the WebGL program that is compiled and uploaded to the GPU.
 *
 * To get the most out of this class, you should be familiar with glsl shaders and how they work.
 * @see https://developer.mozilla.org/en-US/docs/Web/API/WebGLProgram
 * @example
 *
 * // Create a new program
 * const program = new GlProgram({
 *   vertex: '...',
 *   fragment: '...',
 * });
 *
 *
 * There are a few key things that pixi shader will do for you automatically:
 * <br>
 * - If no precision is provided in the shader, it will be injected into the program source for you.
 * This precision will be taken form the options provided, if none is provided,
 * then the program will default to the defaultOptions.
 * <br>
 * - It will inject the program name into the shader source if none is provided.
 * <br>
 *  - It will set the program version to 300 es.
 *
 * For optimal usage and best performance, its best to reuse programs as much as possible.
 * You should use the GlProgram.from helper function to create programs.
 * @class
 * @memberof rendering
 */
export class GlProgram
{
    /** The default options used by the program. */
    public static defaultOptions: Partial<GlProgramOptions> = {
        preferredVertexPrecision: 'highp',
        preferredFragmentPrecision: 'mediump',
    };

    /** the fragment glsl shader source. */
    public readonly fragment?: string;
    /** the vertex glsl shader source */
    public readonly vertex?: string;
    /**
     * attribute data extracted from the program once created this happens when the program is used for the first time
     * @internal
     * @ignore
     */
    public _attributeData: Record<string, ExtractedAttributeData>;
    /**
     * uniform data extracted from the program once created this happens when the program is used for the first time
     * @internal
     * @ignore
     */
    public _uniformData: Record<string, GlUniformData>;
    /**
     * uniform data extracted from the program once created this happens when the program is used for the first time
     * @internal
     * @ignore
     */
    public _uniformBlockData: Record<string, GlUniformBlockData>;
    /** details on how to use this program with transform feedback */
    public transformFeedbackVaryings?: {names: string[], bufferMode: 'separate' | 'interleaved'};
    /**
     * the key that identifies the program via its source vertex + fragment
     * @internal
     * @ignore
     */
    public readonly _key: number;

    /**
     * Creates a shiny new GlProgram. Used by WebGL renderer.
     * @param options - The options for the program.
     */
    constructor(options: GlProgramOptions)
    {
        options = { ...GlProgram.defaultOptions, ...options };

        // only need to check one as they both need to be the same or
        // errors ensue!
        const isES300 = options.fragment.indexOf('#version 300 es') !== -1;

        const preprocessorOptions = {
            stripVersion: isES300,
            ensurePrecision: {
                requestedFragmentPrecision: options.preferredFragmentPrecision,
                requestedVertexPrecision: options.preferredVertexPrecision,
                maxSupportedVertexPrecision: 'highp',
                maxSupportedFragmentPrecision: getMaxFragmentPrecision(),
            },
            setProgramName: {
                name: options.name,
            },
            addProgramDefines: isES300,
            insertVersion: isES300
        };

        let fragment = options.fragment;
        let vertex = options.vertex;

        Object.keys(processes).forEach((processKey) =>
        {
            const processOptions = preprocessorOptions[processKey as keyof typeof preprocessorOptions];

            fragment = processes[processKey](fragment, processOptions, true);
            vertex = processes[processKey](vertex, processOptions, false);
        });

        this.fragment = fragment;
        this.vertex = vertex;

        this._key = createIdFromString(`${this.vertex}:${this.fragment}`, 'gl-program');
    }

    /** destroys the program */
    public destroy(): void
    {
        (this.fragment as null) = null;
        (this.vertex as null) = null;

        this._attributeData = null;
        this._uniformData = null;
        this._uniformBlockData = null;

        this.transformFeedbackVaryings = null;
    }

    /**
     * Helper function that creates a program for a given source.
     * It will check the program cache if the program has already been created.
     * If it has that one will be returned, if not a new one will be created and cached.
     * @param options - The options for the program.
     * @returns A program using the same source
     */
    public static from(options: GlProgramOptions): GlProgram
    {
        const key = `${options.vertex}:${options.fragment}`;

        if (!programCache[key])
        {
            programCache[key] = new GlProgram(options);
        }

        return programCache[key];
    }
}