Source: rendering/renderers/gl/geometry/GlGeometrySystem.ts

import { ExtensionType } from '../../../../extensions/Extensions';
import { getAttributeInfoFromFormat } from '../../shared/geometry/utils/getAttributeInfoFromFormat';
import { ensureAttributes } from '../shader/program/ensureAttributes';
import { getGlTypeFromFormat } from './utils/getGlTypeFromFormat';

import type { Topology } from '../../shared/geometry/const';
import type { Geometry } from '../../shared/geometry/Geometry';
import type { System } from '../../shared/system/System';
import type { GlRenderingContext } from '../context/GlRenderingContext';
import type { GlProgram } from '../shader/GlProgram';
import type { WebGLRenderer } from '../WebGLRenderer';

const topologyToGlMap = {
    'point-list': 0x0000,
    'line-list': 0x0001,
    'line-strip': 0x0003,
    'triangle-list': 0x0004,
    'triangle-strip': 0x0005
};

/**
 * System plugin to the renderer to manage geometry.
 * @memberof rendering
 */
export class GlGeometrySystem implements System
{
    /** @ignore */
    public static extension = {
        type: [
            ExtensionType.WebGLSystem,
        ],
        name: 'geometry',
    } as const;

    /**
     * `true` if we has `*_vertex_array_object` extension.
     * @readonly
     */
    public hasVao: boolean;

    /**
     * `true` if has `ANGLE_instanced_arrays` extension.
     * @readonly
     */
    public hasInstance: boolean;

    protected gl: GlRenderingContext;
    protected _activeGeometry: Geometry;
    protected _activeVao: WebGLVertexArrayObject;

    protected _geometryVaoHash: Record<number, Record<string, WebGLVertexArrayObject>> = Object.create(null);

    /** Renderer that owns this GeometrySystem. */
    private _renderer: WebGLRenderer;

    /** @param renderer - The renderer this System works for. */
    constructor(renderer: WebGLRenderer)
    {
        this._renderer = renderer;
        this._activeGeometry = null;
        this._activeVao = null;

        this.hasVao = true;
        this.hasInstance = true;
    }

    /** Sets up the renderer context and necessary buffers. */
    protected contextChange(): void
    {
        const gl = this.gl = this._renderer.gl;

        if (!this._renderer.context.supports.vertexArrayObject)
        {
            throw new Error('[PixiJS] Vertex Array Objects are not supported on this device');
        }

        const nativeVaoExtension = this._renderer.context.extensions.vertexArrayObject;

        if (nativeVaoExtension)
        {
            gl.createVertexArray = (): WebGLVertexArrayObject =>
                nativeVaoExtension.createVertexArrayOES();

            gl.bindVertexArray = (vao): void =>
                nativeVaoExtension.bindVertexArrayOES(vao);

            gl.deleteVertexArray = (vao): void =>
                nativeVaoExtension.deleteVertexArrayOES(vao);
        }

        const nativeInstancedExtension = this._renderer.context.extensions.vertexAttribDivisorANGLE;

        if (nativeInstancedExtension)
        {
            gl.drawArraysInstanced = (a, b, c, d): void =>
            {
                nativeInstancedExtension.drawArraysInstancedANGLE(a, b, c, d);
            };

            gl.drawElementsInstanced = (a, b, c, d, e): void =>
            {
                nativeInstancedExtension.drawElementsInstancedANGLE(a, b, c, d, e);
            };

            gl.vertexAttribDivisor = (a, b): void =>
                nativeInstancedExtension.vertexAttribDivisorANGLE(a, b);
        }

        this._activeGeometry = null;
        this._activeVao = null;
        this._geometryVaoHash = Object.create(null);
    }

    /**
     * Binds geometry so that is can be drawn. Creating a Vao if required
     * @param geometry - Instance of geometry to bind.
     * @param program - Instance of program to use vao for.
     */
    public bind(geometry?: Geometry, program?: GlProgram): void
    {
        // shader = shader || this.renderer.shader.shader;

        const gl = this.gl;

        this._activeGeometry = geometry;

        const vao = this.getVao(geometry, program);

        if (this._activeVao !== vao)
        {
            this._activeVao = vao;

            gl.bindVertexArray(vao);
        }

        this.updateBuffers();
    }

    /** Reset and unbind any active VAO and geometry. */
    public reset(): void
    {
        this.unbind();
    }

    /** Update buffers of the currently bound geometry. */
    public updateBuffers(): void
    {
        const geometry = this._activeGeometry;

        const bufferSystem = this._renderer.buffer;

        for (let i = 0; i < geometry.buffers.length; i++)
        {
            const buffer = geometry.buffers[i];

            bufferSystem.updateBuffer(buffer);
        }
    }

    /**
     * Check compatibility between a geometry and a program
     * @param geometry - Geometry instance.
     * @param program - Program instance.
     */
    protected checkCompatibility(geometry: Geometry, program: GlProgram): void
    {
        // geometry must have at least all the attributes that the shader requires.
        const geometryAttributes = geometry.attributes;
        const shaderAttributes = program._attributeData;

        for (const j in shaderAttributes)
        {
            if (!geometryAttributes[j])
            {
                throw new Error(`shader and geometry incompatible, geometry missing the "${j}" attribute`);
            }
        }
    }

    /**
     * Takes a geometry and program and generates a unique signature for them.
     * @param geometry - To get signature from.
     * @param program - To test geometry against.
     * @returns - Unique signature of the geometry and program
     */
    protected getSignature(geometry: Geometry, program: GlProgram): string
    {
        const attribs = geometry.attributes;
        const shaderAttributes = program._attributeData;

        const strings = ['g', geometry.uid];

        for (const i in attribs)
        {
            if (shaderAttributes[i])
            {
                strings.push(i, shaderAttributes[i].location);
            }
        }

        return strings.join('-');
    }

    protected getVao(geometry: Geometry, program: GlProgram): WebGLVertexArrayObject
    {
        return this._geometryVaoHash[geometry.uid]?.[program._key] || this.initGeometryVao(geometry, program);
    }

    /**
     * Creates or gets Vao with the same structure as the geometry and stores it on the geometry.
     * If vao is created, it is bound automatically. We use a shader to infer what and how to set up the
     * attribute locations.
     * @param geometry - Instance of geometry to to generate Vao for.
     * @param program
     * @param _incRefCount - Increment refCount of all geometry buffers.
     */
    protected initGeometryVao(geometry: Geometry, program: GlProgram, _incRefCount = true): WebGLVertexArrayObject
    {
        const gl = this._renderer.gl;
        // const CONTEXT_UID = this.CONTEXT_UID;
        const bufferSystem = this._renderer.buffer;

        this._renderer.shader._getProgramData(program);

        this.checkCompatibility(geometry, program);

        const signature = this.getSignature(geometry, program);

        if (!this._geometryVaoHash[geometry.uid])
        {
            this._geometryVaoHash[geometry.uid] = Object.create(null);

            geometry.on('destroy', this.onGeometryDestroy, this);
        }

        const vaoObjectHash = this._geometryVaoHash[geometry.uid];

        let vao = vaoObjectHash[signature];

        if (vao)
        {
            // this will give us easy access to the vao
            vaoObjectHash[program._key] = vao;

            return vao;
        }

        ensureAttributes(geometry, program._attributeData);

        const buffers = geometry.buffers;

        // @TODO: We don't know if VAO is supported.
        vao = gl.createVertexArray();

        gl.bindVertexArray(vao);

        // first update - and create the buffers!
        // only create a gl buffer if it actually gets
        for (let i = 0; i < buffers.length; i++)
        {
            const buffer = buffers[i];

            bufferSystem.bind(buffer);
        }

        // TODO - maybe make this a data object?
        // lets wait to see if we need to first!

        this.activateVao(geometry, program);

        // add it to the cache!
        vaoObjectHash[program._key] = vao;
        vaoObjectHash[signature] = vao;

        gl.bindVertexArray(null);

        return vao;
    }

    /**
     * Disposes geometry.
     * @param geometry - Geometry with buffers. Only VAO will be disposed
     * @param [contextLost=false] - If context was lost, we suppress deleteVertexArray
     */
    protected onGeometryDestroy(geometry: Geometry, contextLost?: boolean): void
    {
        const vaoObjectHash = this._geometryVaoHash[geometry.uid];

        const gl = this.gl;

        if (vaoObjectHash)
        {
            if (contextLost)
            {
                for (const i in vaoObjectHash)
                {
                    if (this._activeVao !== vaoObjectHash[i])
                    {
                        this.unbind();
                    }

                    gl.deleteVertexArray(vaoObjectHash[i]);
                }
            }

            this._geometryVaoHash[geometry.uid] = null;
        }
    }

    /**
     * Dispose all WebGL resources of all managed geometries.
     * @param [contextLost=false] - If context was lost, we suppress `gl.delete` calls
     */
    public destroyAll(contextLost = false): void
    {
        const gl = this.gl;

        for (const i in this._geometryVaoHash)
        {
            if (contextLost)
            {
                for (const j in this._geometryVaoHash[i])
                {
                    const vaoObjectHash = this._geometryVaoHash[i];

                    if (this._activeVao !== vaoObjectHash)
                    {
                        this.unbind();
                    }

                    gl.deleteVertexArray(vaoObjectHash[j]);
                }
            }

            this._geometryVaoHash[i] = null;
        }
    }

    /**
     * Activate vertex array object.
     * @param geometry - Geometry instance.
     * @param program - Shader program instance.
     */
    protected activateVao(geometry: Geometry, program: GlProgram): void
    {
        const gl = this._renderer.gl;

        const bufferSystem = this._renderer.buffer;
        const attributes = geometry.attributes;

        if (geometry.indexBuffer)
        {
            // first update the index buffer if we have one..
            bufferSystem.bind(geometry.indexBuffer);
        }

        let lastBuffer = null;

        // add a new one!
        for (const j in attributes)
        {
            const attribute = attributes[j];
            const buffer = attribute.buffer;
            const glBuffer = bufferSystem.getGlBuffer(buffer);
            const programAttrib = program._attributeData[j];

            if (programAttrib)
            {
                if (lastBuffer !== glBuffer)
                {
                    bufferSystem.bind(buffer);

                    lastBuffer = glBuffer;
                }

                const location = attribute.location;

                // TODO introduce state again
                // we can optimise this for older devices that have no VAOs
                gl.enableVertexAttribArray(location);

                const attributeInfo = getAttributeInfoFromFormat(attribute.format);

                const type = getGlTypeFromFormat(attribute.format);

                if (programAttrib.format?.substring(1, 4) === 'int')
                {
                    gl.vertexAttribIPointer(location,
                        attributeInfo.size,
                        type,
                        attribute.stride,
                        attribute.offset);
                }
                else
                {
                    gl.vertexAttribPointer(location,
                        attributeInfo.size,
                        type,
                        attributeInfo.normalised,
                        attribute.stride,
                        attribute.offset);
                }

                if (attribute.instance)
                {
                    // TODO calculate instance count based of this...
                    if (this.hasInstance)
                    {
                        // Can't use truthiness check to determine if divisor is set,
                        // since 0 is a valid value for divisor
                        const divisor = attribute.divisor ?? 1;

                        gl.vertexAttribDivisor(location, divisor);
                    }
                    else
                    {
                        throw new Error('geometry error, GPU Instancing is not supported on this device');
                    }
                }
            }
        }
    }

    /**
     * Draws the currently bound geometry.
     * @param topology - The type primitive to render.
     * @param size - The number of elements to be rendered. If not specified, all vertices after the
     *  starting vertex will be drawn.
     * @param start - The starting vertex in the geometry to start drawing from. If not specified,
     *  drawing will start from the first vertex.
     * @param instanceCount - The number of instances of the set of elements to execute. If not specified,
     *  all instances will be drawn.
     */
    public draw(topology?: Topology, size?: number, start?: number, instanceCount?: number): this
    {
        const { gl } = this._renderer;
        const geometry = this._activeGeometry;

        const glTopology = topologyToGlMap[geometry.topology || topology];

        instanceCount ||= geometry.instanceCount;

        if (geometry.indexBuffer)
        {
            const byteSize = geometry.indexBuffer.data.BYTES_PER_ELEMENT;
            const glType = byteSize === 2 ? gl.UNSIGNED_SHORT : gl.UNSIGNED_INT;

            if (instanceCount > 1)
            {
                /* eslint-disable max-len */
                gl.drawElementsInstanced(glTopology, size || geometry.indexBuffer.data.length, glType, (start || 0) * byteSize, instanceCount);
                /* eslint-enable max-len */
            }
            else
            {
                /* eslint-disable max-len */
                gl.drawElements(glTopology, size || geometry.indexBuffer.data.length, glType, (start || 0) * byteSize);
                /* eslint-enable max-len */
            }
        }
        else if (instanceCount > 1)
        {
            // TODO need a better way to calculate size..
            gl.drawArraysInstanced(glTopology, start || 0, size || geometry.getSize(), instanceCount);
        }
        else
        {
            gl.drawArrays(glTopology, start || 0, size || geometry.getSize());
        }

        return this;
    }

    /** Unbind/reset everything. */
    protected unbind(): void
    {
        this.gl.bindVertexArray(null);
        this._activeVao = null;
        this._activeGeometry = null;
    }

    public destroy(): void
    {
        this._renderer = null;
        this.gl = null;
        this._activeVao = null;
        this._activeGeometry = null;
    }
}