import BatchDrawCall from './BatchDrawCall';
import BaseTexture from '../textures/BaseTexture';
import ObjectRenderer from './ObjectRenderer';
import State from '../state/State';
import ViewableBuffer from '../geometry/ViewableBuffer';
import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader';
import { settings } from '@pixi/settings';
import { premultiplyBlendMode, premultiplyTint, nextPow2, log2 } from '@pixi/utils';
import { ENV } from '@pixi/constants';
/**
* 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}
* @protected
*/
this.shaderGenerator = null;
/**
* The class that represents the geometry of objects
* that are going to be batched with this.
*
* @member {object}
* @default PIXI.BatchGeometry
* @protected
*/
this.geometryClass = null;
/**
* Size of data being buffered per vertex in the
* attribute buffers (in floats). By default, the
* batch-renderer plugin uses 6:
*
* | aVertexPosition | 2 |
* |-----------------|---|
* | aTextureCoords | 2 |
* | aColor | 1 |
* | aTextureId | 1 |
*
* @member {number}
* @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}
* @protected
*/
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.AbstractBatchRenderer.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` used
* 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.AbstractBatchRenderer#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.AbstractBatchRenderer#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.AbstractBatchRenderer#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)();
}
}
/**
* 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
*/
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 _now_ and empties the current batch.
*/
flush()
{
if (this._vertexCount === 0)
{
return;
}
const attributeBuffer = this.getAttributeBuffer(this._vertexCount);
const indexBuffer = this.getIndexBuffer(this._indexCount);
const gl = this.renderer.gl;
const {
_bufferedElements: elements,
_drawCalls: drawCalls,
MAX_TEXTURES,
_packedGeometries: packedGeometries,
vertexSize,
} = this;
const touch = this.renderer.textureGC.count;
let index = 0;
let _indexCount = 0;
let nextTexture;
let currentTexture;
let textureCount = 0;
let currentGroup = drawCalls[0];
let groupCount = 0;
let blendMode = -1;// blend-mode of previous element/sprite/object!
currentGroup.textureCount = 0;
currentGroup.start = 0;
currentGroup.blend = blendMode;
let TICK = ++BaseTexture._globalBatch;
let i;
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)
{
blendMode = spriteBlendMode;
// force the batch to break!
currentTexture = null;
textureCount = MAX_TEXTURES;
TICK++;
}
if (currentTexture !== nextTexture)
{
currentTexture = nextTexture;
if (nextTexture._batchEnabled !== TICK)
{
if (textureCount === MAX_TEXTURES)
{
TICK++;
textureCount = 0;
currentGroup.size = _indexCount - currentGroup.start;
currentGroup = drawCalls[groupCount++];
currentGroup.textureCount = 0;
currentGroup.blend = blendMode;
currentGroup.start = _indexCount;
}
nextTexture.touched = touch;
nextTexture._batchEnabled = TICK;
nextTexture._id = textureCount;
currentGroup.textures[currentGroup.textureCount++] = nextTexture;
textureCount++;
}
}
this.packInterleavedGeometry(sprite, attributeBuffer,
indexBuffer, index, _indexCount);
// push a graphics..
index += (sprite.vertexData.length / 2) * vertexSize;
_indexCount += sprite.indices.length;
}
BaseTexture._globalBatch = TICK;
currentGroup.size = _indexCount - currentGroup.start;
if (!settings.CAN_UPLOAD_SAME_BUFFER)
{ /* Usually on iOS devices, where the browser doesn't
like uploads to the same buffer in a single frame. */
if (this._packedGeometryPoolSize <= this._flushId)
{
this._packedGeometryPoolSize++;
packedGeometries[this._flushId] = new (this.geometryClass)();
}
packedGeometries[this._flushId]._buffer.update(attributeBuffer.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(attributeBuffer.rawBinaryData, 0);
packedGeometries[this._flushId]._indexBuffer.update(indexBuffer, 0);
this.renderer.geometry.updateBuffers();
}
const textureSystem = this.renderer.texture;
const stateSystem = this.renderer.state;
// Upload textures and do the draw calls
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.
*/
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.
*/
stop()
{
this.flush();
}
/**
* Destroys this `AbstractBatchRenderer`. It cannot be used again.
*/
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;
}
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 the four batching parameters of `element`, interleaves
* and pushes them into the batching attribute/index buffers given.
*
* It uses these properties: `vertexData` `uvs`, `textureId` and
* `indicies`. It also uses the "tint" of the base-texture, if
* present.
*
* @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 {
uint32View,
float32View,
} = attributeBuffer;
const packedVertices = aIndex / this.vertexSize;
const uvs = element.uvs;
const indicies = element.indices;
const vertexData = element.vertexData;
const textureId = element._texture.baseTexture._id;
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);
// lets not worry about tint! for now..
for (let i = 0; i < vertexData.length; i += 2)
{
float32View[aIndex++] = vertexData[i];
float32View[aIndex++] = vertexData[i + 1];
float32View[aIndex++] = uvs[i];
float32View[aIndex++] = uvs[i + 1];
uint32View[aIndex++] = argb;
float32View[aIndex++] = textureId;
}
for (let i = 0; i < indicies.length; i++)
{
indexBuffer[iIndex++] = packedVertices + indicies[i];
}
}
}