Newer
Older
pixi.js / packages / core / src / batch / AbstractBatchRenderer.js
import builtinAttributeDefinitions from './utils/builtinAttributeDefinitions';
import BatchDrawCall from './BatchDrawCall';
import BaseTexture from '../textures/BaseTexture';
import State from '../state/State';
import ObjectRenderer from './ObjectRenderer';
import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader';
import { settings } from '@pixi/settings';
import { premultiplyBlendMode, premultiplyTint, nextPow2, log2 } from '@pixi/utils';
import ViewableBuffer from '../geometry/ViewableBuffer';
import { ENV } from '@pixi/constants';

/**
 * @typedef {Object} AttributeDefinition
 * @memberof PIXI
 *
 * @description
 * Holds the information required to pass attributes from
 * renderable objects to the WebGL vertex shader.
 *
 * @property {string} property - the property of rendered objects
 *    that hold the attributes.
 * @property {string} name - attribute identifier in the GLSL
 *    vertex shader.
 * @property {string} type - type of the attribute. It can be
 *    any of the view types of `PIXI.ViewableBuffer`.
 * @property {number} size - number of elements in the property
 *    array that compose one attribute.
 * @property {PIXI.TYPES} glType - type of the attribute as given
 *    to the geometry.
 * @property {number} glSize - number of elements as glType which
 *    compose one attribute.
 *
 * @see PIXI.AbstractBatchRenderer#attributeDefinitions
 */

/**
 * Renderer dedicated to drawing and batching sprites.
 *
 * This is the default batch renderer. It buffers objects
 * with texture-based geometries and renders them in
 * batches. It uploads multiple textures to the GPU to
 * reduce to the number of draw calls.
 *
 * @class
 * @protected
 * @memberof PIXI
 * @extends PIXI.ObjectRenderer
 */
export default class AbstractBatchRenderer extends ObjectRenderer
{
    /**
     * This will hook onto the renderer's `contextChange`
     * and `prerender` signals.
     *
     * @param {PIXI.Renderer} renderer - The renderer this works for.
     */
    constructor(renderer)
    {
        super(renderer);

        /**
         * This is used to generate a shader that can
         * color each vertex based on a `aTextureId`
         * attribute that points to an texture in `uSampler`.
         *
         * This enables the objects with different textures
         * to be drawn in the same draw call.
         *
         * You can customize your shader by creating your
         * custom shader generator.
         *
         * @member {PIXI.BatchShaderGenerator}
         * @readonly
         */
        this.shaderGenerator = null;

        /**
         * The class that represents the geometry of objects
         * that are going to be batched with this.
         *
         * @member {object}
         * @default PIXI.BatchGeometry
         * @readonly
         */
        this.geometryClass = null;

        /**
         * Array of attribute definitions that are used to
         * pass attribute data from your objects to the vertex
         * shader. Default values are given below:
         *
         * | Index | property   | name            | type      | size | glType               | glSize |
         * |-------|------------|-----------------|-----------|------|----------------------|--------|
         * | 1     | vertexData | aVertexPosition | `float32` | 2    | TYPES.FLOAT          | 1      |
         * | 2     | uvs        | aTextureCoord   | `float32` | 2    | TYPES.FLOAT          | 1      |
         * | 3     | undefined  | aColor          | `uint32`  | 1    | TYPES.UNSIGNED_BYTE  | 4      |
         * | 4     | undefined  | aTextureId      | `float32` | 1    | TYPES.FLOAT          | 1      |
         *
         * @type {PIXI.AttributeDefinitions[]}
         * @readonly
         */
        this.attributeDefinitions = null;

        /**
         * Size of data being buffered per vertex in the
         * attribute buffers (in floats). By default, the
         * batch-renderer plugin uses 6:
         *
         * | Attribute       | Size |
         * |-----------------|------|
         * | aVertexPosition | 2    |
         * | aTextureCoords  | 2    |
         * | aColor          | 1    |
         * | aTextureId      | 1    |
         *
         * @member {number} vertexSize
         * @readonly
         */
        this.vertexSize = null;

        /**
         * The WebGL state in which this renderer will work.
         *
         * @member {PIXI.State}
         * @readonly
         */
        this.state = State.for2d();

        /**
         * The number of bufferable objects before a flush
         * occurs automatically.
         *
         * @member {number}
         * @default settings.SPRITE_MAX_TEXTURES
         */
        this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE, 2000 is a nice balance between mobile/desktop

        /**
         * Total count of all vertices used by the currently
         * buffered objects.
         *
         * @member {number}
         * @private
         */
        this._vertexCount = 0;

        /**
         * Total count of all indices used by the currently
         * buffered objects.
         *
         * @member {number}
         * @private
         */
        this._indexCount = 0;

        /**
         * Buffer of objects that are yet to be rendered.
         *
         * @member {PIXI.DisplayObject[]}
         * @private
         */
        this._bufferedElements = [];

        /**
         * Number of elements that are buffered and are
         * waiting to be flushed.
         *
         * @member {number}
         * @private
         */
        this._bufferSize = 0;

        /**
         * This shader is generated by `this.shaderGenerator`.
         *
         * It is generated specifically to handle the required
         * number of textures being batched together.
         *
         * @member {PIXI.Shader}
         * @private
         */
        this._shader = null;

        /**
         * Pool of `this.geometryClass` geometry objects
         * that store buffers. They are used to pass data
         * to the shader on each draw call.
         *
         * These are never re-allocated again, unless a
         * context change occurs; however, the pool may
         * be expanded if required.
         *
         * @member {PIXI.Geometry[]}
         * @private
         * @see PIXI.BatchRenderer.contextChange
         */
        this._packedGeometries = [];

        /**
         * Size of `this._packedGeometries`. It can be expanded
         * if more than `this._packedGeometryPoolSize` flushes
         * occur in a single frame.
         *
         * @member {number}
         * @private
         */
        this._packedGeometryPoolSize = 2;

        /**
         * A flush may occur multiple times in a single
         * frame. On iOS devices or when
         * `settings.CAN_UPLOAD_SAME_BUFFER` is false, the
         * batch renderer does not upload data to the same
         * `WebGLBuffer` for performance reasons.
         *
         * This is the index into `packedGeometries` that points to
         * geometry holding the most recent buffers.
         *
         * @member {number}
         * @private
         */
        this._flushId = 0;

        /**
         * Pool of `BatchDrawCall` objects that `flush` uses
         * to create "batches" of the objects being rendered.
         *
         * These are never re-allocated again.
         *
         * @member BatchDrawCall[]
         * @private
         */
        this._drawCalls = [];

        for (let k = 0; k < this.size / 4; k++)
        { // initialize the draw-calls pool to max size.
            this._drawCalls[k] = new BatchDrawCall();
        }

        /**
         * Pool of `ViewableBuffer` objects that are sorted in
         * order of increasing size. The flush method uses
         * the buffer with the least size above the amount
         * it requires. These are used for passing attributes.
         *
         * The first buffer has a size of 8; each subsequent
         * buffer has double capacity of its previous.
         *
         * @member {PIXI.ViewableBuffer}
         * @private
         * @see PIXI.BatchRenderer#getAttributeBuffer
         */
        this._aBuffers = {};

        /**
         * Pool of `Uint16Array` objects that are sorted in
         * order of increasing size. The flush method uses
         * the buffer with the least size above the amount
         * it requires. These are used for passing indices.
         *
         * The first buffer has a size of 12; each subsequent
         * buffer has double capacity of its previous.
         *
         * @member {Uint16Array[]}
         * @private
         * @see PIXI.BatchRenderer#getIndexBuffer
         */
        this._iBuffers = {};

        /**
         * Maximum number of textures that can be uploaded to
         * the GPU under the current context. It is initialized
         * properly in `this.contextChange`.
         *
         * @member {number}
         * @see PIXI.BatchRenderer#contextChange
         * @readonly
         */
        this.MAX_TEXTURES = 1;

        this.renderer.on('prerender', this.onPrerender, this);
        renderer.runners.contextChange.add(this);
    }

    /**
     * Handles the `contextChange` signal.
     *
     * It calculates `this.MAX_TEXTURES` and allocating the
     * packed-geometry object pool.
     */
    contextChange()
    {
        const gl = this.renderer.gl;

        if (settings.PREFER_ENV === ENV.WEBGL_LEGACY)
        {
            this.MAX_TEXTURES = 1;
        }
        else
        {
            // step 1: first check max textures the GPU can handle.
            this.MAX_TEXTURES = Math.min(
                gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS),
                settings.SPRITE_MAX_TEXTURES);

            // step 2: check the maximum number of if statements the shader can have too..
            this.MAX_TEXTURES = checkMaxIfStatementsInShader(
                this.MAX_TEXTURES, gl);
        }

        this._shader = this.shaderGenerator.generateShader(this.MAX_TEXTURES);

        // we use the second shader as the first one depending on your browser
        // may omit aTextureId as it is not used by the shader so is optimized out.
        for (let i = 0; i < this._packedGeometryPoolSize; i++)
        {
            /* eslint-disable max-len */
            this._packedGeometries[i] = new (this.geometryClass)(false, this.attributeDefinitions);
        }
    }

    /**
     * Handles the `prerender` signal.
     *
     * It ensures that flushes start from the first geometry
     * object again.
     */
    onPrerender()
    {
        this._flushId = 0;
    }

    /**
     * Buffers the "batchable" object. It need not be rendered
     * immediately.
     *
     * @param {PIXI.Sprite} sprite - the sprite to render when
     *    using this spritebatch
     * @override
     */
    render(element)
    {
        if (!element._texture.valid)
        {
            return;
        }

        if (this._vertexCount + (element.vertexData.length / 2) > this.size)
        {
            this.flush();
        }

        this._vertexCount += element.vertexData.length / 2;
        this._indexCount += element.indices.length;
        this._bufferedElements[this._bufferSize++] = element;
    }

    /**
     * Renders the content and empties the current batch.
     *
     * @override
     */
    flush()
    {
        if (this._vertexCount === 0)
        {
            return;
        }

        const gl = this.renderer.gl;
        const attrBuffer = this.getAttributeBuffer(this._vertexCount);
        const indexBuffer = this.getIndexBuffer(this._indexCount);
        const primaryAttribute = this.attributeDefinitions[0];

        const {
            _bufferedElements: elements,
            _drawCalls: drawCalls,
            MAX_TEXTURES,
            _packedGeometries: packedGeometries,
            vertexSize,
        } = this;

        const {
            property: primaryProperty,
            size: primaryAttributeSize,
        } = primaryAttribute;

        const touch = this.renderer.textureGC.count;
        let attrIndex = 0;
        let iIndex = 0;

        let nextTexture;
        let currentTexture;
        let textureCount = 0;

        let currentGroup = drawCalls[0];
        let groupCount = 0;

        let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode];

        currentGroup.textureCount = 0;
        currentGroup.start = 0;
        currentGroup.blend = blendMode;

        let TICK = ++BaseTexture._globalBatch;
        let i;

        /* Interleaves and appends each object's geometry into the
           attribute buffer (`buffer`) and indices into `indexBuffer`. It
           also groups them into homogenous draw-calls. */
        for (i = 0; i < this._bufferSize; ++i)
        {
            const sprite = elements[i];

            elements[i] = null;
            nextTexture = sprite._texture.baseTexture;

            const spriteBlendMode = premultiplyBlendMode[
                nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode];

            if (blendMode !== spriteBlendMode)
            { /* Must finish this group, since blend modes conflict. */
                blendMode = spriteBlendMode;
                currentTexture = null;
                textureCount = MAX_TEXTURES;
                TICK++;
            }

            if (currentTexture !== nextTexture)
            {
                currentTexture = nextTexture;

                if (nextTexture._batchEnabled !== TICK)
                {
                    if (textureCount === MAX_TEXTURES)
                    {
                        TICK++;
                        textureCount = 0;
                        currentGroup.size = iIndex - currentGroup.start;

                        currentGroup = drawCalls[groupCount++];
                        currentGroup.textureCount = 0;
                        currentGroup.blend = blendMode;
                        currentGroup.start = iIndex;
                    }

                    nextTexture.touched = touch;
                    nextTexture._batchEnabled = TICK;
                    nextTexture._id = textureCount;

                    currentGroup.textures[currentGroup.textureCount++] = nextTexture;
                    textureCount++;
                }
            }

            this.packInterleavedGeometry(sprite, attrBuffer,
                indexBuffer, attrIndex, iIndex);

            // push a graphics..
            attrIndex += (sprite[primaryProperty].length / primaryAttributeSize) * vertexSize;
            iIndex += sprite.indices.length;
        }

        BaseTexture._globalBatch = TICK;
        currentGroup.size = iIndex - currentGroup.start;

        if (!settings.CAN_UPLOAD_SAME_BUFFER)// we must use new buffers
        {
            if (this._packedGeometryPoolSize <= this._flushId)
            {
                this._packedGeometryPoolSize++;// expand geometry pool
                this._packedGeometries[this._flushId]
                  = new (this.geometryClass)(false, this.attributeDefinitions);
            }

            packedGeometries[this._flushId]._buffer.update(
                attrBuffer.rawBinaryData, 0);
            packedGeometries[this._flushId]._indexBuffer.update(
                indexBuffer, 0);

            this.renderer.geometry.bind(packedGeometries[this._flushId]);
            this.renderer.geometry.updateBuffers();
            this._flushId++;
        }
        else
        {
            // lets use the faster option, always use buffer number 0
            packedGeometries[this._flushId]._buffer.update(
                attrBuffer.rawBinaryData, 0);
            packedGeometries[this._flushId]._indexBuffer.update(
                indexBuffer, 0);

            this.renderer.geometry.updateBuffers();
        }

        const textureSystem = this.renderer.texture;
        const stateSystem = this.renderer.state;

        for (i = 0; i < groupCount; i++)
        {
            const group = drawCalls[i];
            const groupTextureCount = group.textureCount;

            for (let j = 0; j < groupTextureCount; j++)
            {
                textureSystem.bind(group.textures[j], j);
                group.textures[j] = null;
            }

            stateSystem.setBlendMode(group.blend);
            gl.drawElements(group.type, group.size, gl.UNSIGNED_SHORT, group.start * 2);
        }

        // reset elements for the next flush
        this._bufferSize = 0;
        this._vertexCount = 0;
        this._indexCount = 0;
    }

    /**
     * Starts a new sprite batch.
     *
     * @override
     */
    start()
    {
        this.renderer.state.set(this.state);

        this.renderer.shader.bind(this._shader);

        if (settings.CAN_UPLOAD_SAME_BUFFER)
        {
            // bind buffer #0, we don't need others
            this.renderer.geometry.bind(this._packedGeometries[this._flushId]);
        }
    }

    /**
     * Stops and flushes the current batch.
     *
     * @override
     */
    stop()
    {
        this.flush();
    }

    /**
     * Destroys this `BatchRenderer`. It cannot be used again.
     *
     * @override
     */
    destroy()
    {
        for (let i = 0; i < this._packedGeometryPoolSize; i++)
        {
            if (this._packedGeometries[i])
            {
                this._packedGeometries[i].destroy();
            }
        }

        this.renderer.off('prerender', this.onPrerender, this);

        this._aBuffers = null;
        this._iBuffers = null;
        this._packedGeometries = null;
        this._drawCalls = null;

        if (this._shader)
        {
            this._shader.destroy();
            this._shader = null;
        }

        this.state = null;
        super.destroy();
    }

    /**
     * Fetches an attribute buffer from `this._aBuffers` that
     * can hold atleast `size` floats.
     *
     * @param {number} size - minimum capacity required
     * @return {ViewableBuffer} - buffer than can hold atleast `size` floats
     * @private
     */
    getAttributeBuffer(size)
    {
        // 8 vertices is enough for 2 quads
        const roundedP2 = nextPow2(Math.ceil(size / 8));
        const roundedSizeIndex = log2(roundedP2);
        const roundedSize = roundedP2 * 8;

        if (this._aBuffers.length <= roundedSizeIndex)
        {
            this._iBuffers.length = roundedSizeIndex + 1;
        }

        let buffer = this._aBuffers[roundedSize];

        if (!buffer)
        {
            this._aBuffers[roundedSize] = buffer = new ViewableBuffer(roundedSize * this.vertexSize * 4);
        }

        return buffer;
    }

    /**
     * Fetches an index buffer from `this._iBuffers` that can
     * has atleast `size` capacity.
     *
     * @param {number} size - minimum required capacity
     * @return {Uint16Array} - buffer that can fit `size`
     *    indices.
     * @private
     */
    getIndexBuffer(size)
    {
        // 12 indices is enough for 2 quads
        const roundedP2 = nextPow2(Math.ceil(size / 12));
        const roundedSizeIndex = log2(roundedP2);
        const roundedSize = roundedP2 * 12;

        if (this._iBuffers.length <= roundedSizeIndex)
        {
            this._iBuffers.length = roundedSizeIndex + 1;
        }

        let buffer = this._iBuffers[roundedSizeIndex];

        if (!buffer)
        {
            this._iBuffers[roundedSizeIndex] = buffer = new Uint16Array(roundedSize);
        }

        return buffer;
    }

    /**
     * Takes all the attributes sources of the element being
     * drawn, interleaves them, and appends them to the
     * attribute buffer. It also appends the indices of the
     * element into the index buffer.
     *
     * @param {PIXI.Sprite} element - element being rendered
     * @param {PIXI.ViewableBuffer} attributeBuffer - attribute buffer
     * @param {Uint16Array} indexBuffer - index buffer
     * @param {number} aIndex - number of floats already in the attribute buffer
     * @param {number} iIndex - number of indices already in `indexBuffer`
     */
    packInterleavedGeometry(element, attributeBuffer, indexBuffer, aIndex, iIndex)
    {
        const packedVertices = aIndex / this.vertexSize;
        const indicies = element.indices;
        const textureId = element._texture.baseTexture._id;

        const attributeDefinitions = this.attributeDefinitions;
        const attributeSources = [];
        const sourceOffsets = [];
        let highestAttributeLength = 0;

        for (let i = 0; i < attributeDefinitions.length; i++)
        {
            sourceOffsets.push(0);
            const attribute = attributeDefinitions[i];

            if (typeof attributeDefinitions[i] !== 'string')
            {
                const source = element[attributeDefinitions[i].property];

                attributeSources.push(source);
                highestAttributeLength = Math.max(
                    highestAttributeLength, source.length / attribute.size);
            }
            else
            {
                switch (attributeDefinitions[i])
                {
                    case 'aColor':
                    {
                        const alpha = Math.min(element.worldAlpha, 1.0);
                        const argb = (alpha < 1.0
                        && element._texture.baseTexture.premultiplyAlpha)
                            ? premultiplyTint(element._tintRGB, alpha)
                            : element._tintRGB + (alpha * 255 << 24);

                        attributeSources.push([Math.round(argb)]);
                        highestAttributeLength
                            = Math.max(highestAttributeLength, 1);
                        break;
                    }
                    case 'aTextureId':
                    {
                        attributeSources.push(null);
                        break;
                    }
                    default:
                    {
                        throw new Error(`Unknown built-in attribute `
                          + `given to AbstractBatchRenderer: `
                          + `${attributeDefinitions[i]}`);
                    }
                }
            }
        }

        for (let i = 0; i < highestAttributeLength; i++)
        {
            for (let s = 0; s < attributeSources.length; s++)
            {
                const attribute = attributeDefinitions[s];
                const source = attributeSources[s];

                if (!source)// Only aTextureId has no source!
                {
                    attributeBuffer.float32View[aIndex++] = textureId;
                    continue;
                }

                const isBuiltin = (typeof attribute === 'string');
                const type = (isBuiltin) ? builtinAttributeDefinitions[attribute].type
                    : attribute.type;
                const size = (isBuiltin) ? builtinAttributeDefinitions[attribute].size
                    : attribute.size;
                const wordSize = (isBuiltin) ? builtinAttributeDefinitions[attribute]._wordSize
                    : attribute._wordSize;// size of each attribute in words
                const typeWordSize = wordSize / size;// size of type in words

                let offset = sourceOffsets[s];
                let globalOffset = aIndex / typeWordSize;

                for (let localOffset = 0; localOffset < size; localOffset++)
                {
                    attributeBuffer.view(type)[globalOffset++] = source[offset++ % source.length];
                }

                sourceOffsets[s] = offset;
                aIndex = globalOffset * typeWordSize;
            }
        }

        for (let i = 0; i < indicies.length; i++)
        {
            indexBuffer[iIndex++] = packedVertices + indicies[i];
        }
    }

    /**
     * Calculates the vertex size for the given attribute
     * definitions. It also accounts for built-in attributes.
     *
     * @param {Array<Object>} attributeDefinitions - attribute definitions
     * @return {number} sum of all attribute sizes
     * @static
     */
    static vertexSizeOf(attributeDefinitions)
    {
        let vertexSize = 0;

        for (let d = 0; d < attributeDefinitions.length; d++)
        {
            const definition = attributeDefinitions[d];

            if (typeof definition !== 'string')
            {
                vertexSize += attributeDefinitions[d]._wordSize;
            }
            else
            {
                if (!builtinAttributeDefinitions[definition])
                {
                    throw new Error(`${definition} is not a builtin attribute!`);
                }

                vertexSize += builtinAttributeDefinitions[definition]._wordSize;
            }
        }

        return vertexSize;
    }
}