diff --git a/packages/core/src/batch/AbstractBatchRenderer.js b/packages/core/src/batch/AbstractBatchRenderer.js new file mode 100644 index 0000000..7b4d061 --- /dev/null +++ b/packages/core/src/batch/AbstractBatchRenderer.js @@ -0,0 +1,783 @@ +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} 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; + } +} diff --git a/packages/core/src/batch/AbstractBatchRenderer.js b/packages/core/src/batch/AbstractBatchRenderer.js new file mode 100644 index 0000000..7b4d061 --- /dev/null +++ b/packages/core/src/batch/AbstractBatchRenderer.js @@ -0,0 +1,783 @@ +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} 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; + } +} diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js deleted file mode 100644 index b80bc7e..0000000 --- a/packages/core/src/batch/BatchBuffer.js +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Used by the BatchRenderer - * - * @class - * @memberof PIXI - */ -export default class BatchBuffer -{ - /** - * @param {number} size - The size of the buffer in bytes. - */ - constructor(size) - { - this.vertices = new ArrayBuffer(size); - - /** - * View on the vertices as a Float32Array for positions - * - * @member {Float32Array} - */ - this.float32View = new Float32Array(this.vertices); - - /** - * View on the vertices as a Uint32Array for uvs - * - * @member {Float32Array} - */ - this.uint32View = new Uint32Array(this.vertices); - } - - /** - * Destroys the buffer. - * - */ - destroy() - { - this.vertices = null; - this.float32View = null; - this.uint32View = null; - } -} diff --git a/packages/core/src/batch/AbstractBatchRenderer.js b/packages/core/src/batch/AbstractBatchRenderer.js new file mode 100644 index 0000000..7b4d061 --- /dev/null +++ b/packages/core/src/batch/AbstractBatchRenderer.js @@ -0,0 +1,783 @@ +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} 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; + } +} diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js deleted file mode 100644 index b80bc7e..0000000 --- a/packages/core/src/batch/BatchBuffer.js +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Used by the BatchRenderer - * - * @class - * @memberof PIXI - */ -export default class BatchBuffer -{ - /** - * @param {number} size - The size of the buffer in bytes. - */ - constructor(size) - { - this.vertices = new ArrayBuffer(size); - - /** - * View on the vertices as a Float32Array for positions - * - * @member {Float32Array} - */ - this.float32View = new Float32Array(this.vertices); - - /** - * View on the vertices as a Uint32Array for uvs - * - * @member {Float32Array} - */ - this.uint32View = new Uint32Array(this.vertices); - } - - /** - * Destroys the buffer. - * - */ - destroy() - { - this.vertices = null; - this.float32View = null; - this.uint32View = null; - } -} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js index 6275f8c..ad67273 100644 --- a/packages/core/src/batch/BatchGeometry.js +++ b/packages/core/src/batch/BatchGeometry.js @@ -1,9 +1,32 @@ -import { TYPES } from '@pixi/constants'; -import Geometry from '../geometry/Geometry'; import Buffer from '../geometry/Buffer'; +import builtinAttributeDefinitions from './utils/builtinAttributeDefinitions'; +import Geometry from '../geometry/Geometry'; +import { TYPES } from '@pixi/constants'; + +const defaultAttributes = [ + { + property: 'vertexData', + name: 'aVertexPosition', + type: 'float32', + size: 2, + glType: TYPES.FLOAT, + glSize: 2, + }, + { + property: 'uvs', + name: 'aTextureCoord', + type: 'float32', + size: 2, + glType: TYPES.FLOAT, + glSize: 2, + }, + 'aColor', // built-in attribute + 'aTextureId', +]; /** - * Geometry used to batch standard PIXI content (e.g. Mesh, Sprite, Graphics objects). + * Geometry used to batch standard PIXI content (e.g. Mesh, Sprite, + * Graphics objects). * * @class * @memberof PIXI @@ -13,8 +36,9 @@ /** * @param {boolean} [_static=false] Optimization flag, where `false` * is updated every frame, `true` doesn't change frame-to-frame. + * @param {Array} attributeDefinitions - attribute definitions */ - constructor(_static = false) + constructor(_static = false, attributeDefinitions = defaultAttributes) { super(); @@ -34,10 +58,25 @@ */ this._indexBuffer = new Buffer(null, _static, true); - this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) - .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) - .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) - .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) - .addIndex(this._indexBuffer); + /* These are automatically interleaved by GeometrySystem. */ + for (let i = 0; i < attributeDefinitions.length; i++) + { + const def = attributeDefinitions[i]; + + if (def === 'aColor') + { // special + this.addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE); + continue; + } + + const isBuiltin = (typeof def === 'string'); + const identifier = isBuiltin ? def : def.name; + const size = isBuiltin ? builtinAttributeDefinitions[identifier].glSize : def.glSize; + const type = isBuiltin ? builtinAttributeDefinitions[identifier].glType : def.glType; + + this.addAttribute(identifier, this._buffer, size, def === 'aTextureId', type); + } + + this.addIndex(this._indexBuffer); } } diff --git a/packages/core/src/batch/AbstractBatchRenderer.js b/packages/core/src/batch/AbstractBatchRenderer.js new file mode 100644 index 0000000..7b4d061 --- /dev/null +++ b/packages/core/src/batch/AbstractBatchRenderer.js @@ -0,0 +1,783 @@ +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} 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; + } +} diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js deleted file mode 100644 index b80bc7e..0000000 --- a/packages/core/src/batch/BatchBuffer.js +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Used by the BatchRenderer - * - * @class - * @memberof PIXI - */ -export default class BatchBuffer -{ - /** - * @param {number} size - The size of the buffer in bytes. - */ - constructor(size) - { - this.vertices = new ArrayBuffer(size); - - /** - * View on the vertices as a Float32Array for positions - * - * @member {Float32Array} - */ - this.float32View = new Float32Array(this.vertices); - - /** - * View on the vertices as a Uint32Array for uvs - * - * @member {Float32Array} - */ - this.uint32View = new Uint32Array(this.vertices); - } - - /** - * Destroys the buffer. - * - */ - destroy() - { - this.vertices = null; - this.float32View = null; - this.uint32View = null; - } -} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js index 6275f8c..ad67273 100644 --- a/packages/core/src/batch/BatchGeometry.js +++ b/packages/core/src/batch/BatchGeometry.js @@ -1,9 +1,32 @@ -import { TYPES } from '@pixi/constants'; -import Geometry from '../geometry/Geometry'; import Buffer from '../geometry/Buffer'; +import builtinAttributeDefinitions from './utils/builtinAttributeDefinitions'; +import Geometry from '../geometry/Geometry'; +import { TYPES } from '@pixi/constants'; + +const defaultAttributes = [ + { + property: 'vertexData', + name: 'aVertexPosition', + type: 'float32', + size: 2, + glType: TYPES.FLOAT, + glSize: 2, + }, + { + property: 'uvs', + name: 'aTextureCoord', + type: 'float32', + size: 2, + glType: TYPES.FLOAT, + glSize: 2, + }, + 'aColor', // built-in attribute + 'aTextureId', +]; /** - * Geometry used to batch standard PIXI content (e.g. Mesh, Sprite, Graphics objects). + * Geometry used to batch standard PIXI content (e.g. Mesh, Sprite, + * Graphics objects). * * @class * @memberof PIXI @@ -13,8 +36,9 @@ /** * @param {boolean} [_static=false] Optimization flag, where `false` * is updated every frame, `true` doesn't change frame-to-frame. + * @param {Array} attributeDefinitions - attribute definitions */ - constructor(_static = false) + constructor(_static = false, attributeDefinitions = defaultAttributes) { super(); @@ -34,10 +58,25 @@ */ this._indexBuffer = new Buffer(null, _static, true); - this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) - .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) - .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) - .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) - .addIndex(this._indexBuffer); + /* These are automatically interleaved by GeometrySystem. */ + for (let i = 0; i < attributeDefinitions.length; i++) + { + const def = attributeDefinitions[i]; + + if (def === 'aColor') + { // special + this.addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE); + continue; + } + + const isBuiltin = (typeof def === 'string'); + const identifier = isBuiltin ? def : def.name; + const size = isBuiltin ? builtinAttributeDefinitions[identifier].glSize : def.glSize; + const type = isBuiltin ? builtinAttributeDefinitions[identifier].glType : def.glType; + + this.addAttribute(identifier, this._buffer, size, def === 'aTextureId', type); + } + + this.addIndex(this._indexBuffer); } } diff --git a/packages/core/src/batch/BatchPluginFactory.js b/packages/core/src/batch/BatchPluginFactory.js index ce5a14a..aad5e68 100644 --- a/packages/core/src/batch/BatchPluginFactory.js +++ b/packages/core/src/batch/BatchPluginFactory.js @@ -1,6 +1,10 @@ import BatchShaderGenerator from './BatchShaderGenerator'; import BatchGeometry from './BatchGeometry'; -import BaseBatchRenderer from './BatchRenderer'; +import AbstractBatchRenderer from './AbstractBatchRenderer'; +import ViewableBuffer from '../geometry/ViewableBuffer'; + +import { sizeOfType } from '@pixi/utils'; +import { TYPES } from '@pixi/constants'; import defaultVertex from './texture.vert'; import defaultFragment from './texture.frag'; @@ -35,6 +39,8 @@ * * @static * @param {object} [options] + * @param {object} [option.attributeDefinitions=Array] - + * Attribute definitions, see PIXI.AbstractBatchRenderer#attributeDefinitions * @param {string} [options.vertex=PIXI.BatchPluginFactory.defaultVertexSrc] - Vertex shader source * @param {string} [options.fragment=PIXI.BatchPluginFactory.defaultFragmentTemplate] - Fragment shader template * @param {number} [options.vertexSize=6] - Vertex size @@ -43,19 +49,48 @@ */ static create(options) { - const { vertex, fragment, vertexSize, geometryClass } = Object.assign({ + const { + attributeDefinitions, + fragment, + geometryClass, + vertex, + } = Object.assign({ + attributeDefinitions: [ + { + property: 'vertexData', + name: 'aVertexPosition', + type: 'float32', + size: 2, + glType: TYPES.FLOAT, + glSize: 2, + }, + { + property: 'uvs', + name: 'aTextureCoord', + type: 'float32', + size: 2, + glType: TYPES.FLOAT, + glSize: 2, + }, + 'aColor', // built-in attribute + 'aTextureId', + ], vertex: defaultVertex, fragment: defaultFragment, geometryClass: BatchGeometry, - vertexSize: 6, }, options); - return class BatchPlugin extends BaseBatchRenderer + BatchPluginFactory._checkAttributeDefinitionCompatibility(attributeDefinitions); + + const vertexSize = AbstractBatchRenderer.vertexSizeOf(attributeDefinitions); + + return class BatchPlugin extends AbstractBatchRenderer { constructor(renderer) { super(renderer); + this.attributeDefinitions = attributeDefinitions; this.shaderGenerator = new BatchShaderGenerator(vertex, fragment); this.geometryClass = geometryClass; this.vertexSize = vertexSize; @@ -86,6 +121,39 @@ { return defaultFragment; } + + static _checkAttributeDefinitionCompatibility(definitions) + { + definitions.forEach((def) => + { + if (typeof def === 'string') + { + return;// built-in attribute + } + + const inputSize = ViewableBuffer.sizeOf(def.type) * def.size; + + if (inputSize % 4 !== 0) + { + throw new Error('Batch rendering requires that your object ' + + 'attributes be of net size multiple of four. The attribute ' + + `${def.property}, a.k.a ${def.name}, has a source size of` + + `${inputSize}, which is not a multiple of 4. Consider padding` + + 'your elements with additional bytes.'); + } + + const outputSize = sizeOfType(def.glType) * def.glSize; + + if (outputSize !== inputSize) + { + throw new Error('Your object- and gl- types do not match in size.' + + 'The size of each attribute in the object property array is ' + + `${inputSize}, while the buffered size is ${outputSize} in bytes.`); + } + + def._wordSize = inputSize / 4; + }); + } } // Setup the default BatchRenderer plugin, this is what diff --git a/packages/core/src/batch/AbstractBatchRenderer.js b/packages/core/src/batch/AbstractBatchRenderer.js new file mode 100644 index 0000000..7b4d061 --- /dev/null +++ b/packages/core/src/batch/AbstractBatchRenderer.js @@ -0,0 +1,783 @@ +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} 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; + } +} diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js deleted file mode 100644 index b80bc7e..0000000 --- a/packages/core/src/batch/BatchBuffer.js +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Used by the BatchRenderer - * - * @class - * @memberof PIXI - */ -export default class BatchBuffer -{ - /** - * @param {number} size - The size of the buffer in bytes. - */ - constructor(size) - { - this.vertices = new ArrayBuffer(size); - - /** - * View on the vertices as a Float32Array for positions - * - * @member {Float32Array} - */ - this.float32View = new Float32Array(this.vertices); - - /** - * View on the vertices as a Uint32Array for uvs - * - * @member {Float32Array} - */ - this.uint32View = new Uint32Array(this.vertices); - } - - /** - * Destroys the buffer. - * - */ - destroy() - { - this.vertices = null; - this.float32View = null; - this.uint32View = null; - } -} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js index 6275f8c..ad67273 100644 --- a/packages/core/src/batch/BatchGeometry.js +++ b/packages/core/src/batch/BatchGeometry.js @@ -1,9 +1,32 @@ -import { TYPES } from '@pixi/constants'; -import Geometry from '../geometry/Geometry'; import Buffer from '../geometry/Buffer'; +import builtinAttributeDefinitions from './utils/builtinAttributeDefinitions'; +import Geometry from '../geometry/Geometry'; +import { TYPES } from '@pixi/constants'; + +const defaultAttributes = [ + { + property: 'vertexData', + name: 'aVertexPosition', + type: 'float32', + size: 2, + glType: TYPES.FLOAT, + glSize: 2, + }, + { + property: 'uvs', + name: 'aTextureCoord', + type: 'float32', + size: 2, + glType: TYPES.FLOAT, + glSize: 2, + }, + 'aColor', // built-in attribute + 'aTextureId', +]; /** - * Geometry used to batch standard PIXI content (e.g. Mesh, Sprite, Graphics objects). + * Geometry used to batch standard PIXI content (e.g. Mesh, Sprite, + * Graphics objects). * * @class * @memberof PIXI @@ -13,8 +36,9 @@ /** * @param {boolean} [_static=false] Optimization flag, where `false` * is updated every frame, `true` doesn't change frame-to-frame. + * @param {Array} attributeDefinitions - attribute definitions */ - constructor(_static = false) + constructor(_static = false, attributeDefinitions = defaultAttributes) { super(); @@ -34,10 +58,25 @@ */ this._indexBuffer = new Buffer(null, _static, true); - this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) - .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) - .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) - .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) - .addIndex(this._indexBuffer); + /* These are automatically interleaved by GeometrySystem. */ + for (let i = 0; i < attributeDefinitions.length; i++) + { + const def = attributeDefinitions[i]; + + if (def === 'aColor') + { // special + this.addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE); + continue; + } + + const isBuiltin = (typeof def === 'string'); + const identifier = isBuiltin ? def : def.name; + const size = isBuiltin ? builtinAttributeDefinitions[identifier].glSize : def.glSize; + const type = isBuiltin ? builtinAttributeDefinitions[identifier].glType : def.glType; + + this.addAttribute(identifier, this._buffer, size, def === 'aTextureId', type); + } + + this.addIndex(this._indexBuffer); } } diff --git a/packages/core/src/batch/BatchPluginFactory.js b/packages/core/src/batch/BatchPluginFactory.js index ce5a14a..aad5e68 100644 --- a/packages/core/src/batch/BatchPluginFactory.js +++ b/packages/core/src/batch/BatchPluginFactory.js @@ -1,6 +1,10 @@ import BatchShaderGenerator from './BatchShaderGenerator'; import BatchGeometry from './BatchGeometry'; -import BaseBatchRenderer from './BatchRenderer'; +import AbstractBatchRenderer from './AbstractBatchRenderer'; +import ViewableBuffer from '../geometry/ViewableBuffer'; + +import { sizeOfType } from '@pixi/utils'; +import { TYPES } from '@pixi/constants'; import defaultVertex from './texture.vert'; import defaultFragment from './texture.frag'; @@ -35,6 +39,8 @@ * * @static * @param {object} [options] + * @param {object} [option.attributeDefinitions=Array] - + * Attribute definitions, see PIXI.AbstractBatchRenderer#attributeDefinitions * @param {string} [options.vertex=PIXI.BatchPluginFactory.defaultVertexSrc] - Vertex shader source * @param {string} [options.fragment=PIXI.BatchPluginFactory.defaultFragmentTemplate] - Fragment shader template * @param {number} [options.vertexSize=6] - Vertex size @@ -43,19 +49,48 @@ */ static create(options) { - const { vertex, fragment, vertexSize, geometryClass } = Object.assign({ + const { + attributeDefinitions, + fragment, + geometryClass, + vertex, + } = Object.assign({ + attributeDefinitions: [ + { + property: 'vertexData', + name: 'aVertexPosition', + type: 'float32', + size: 2, + glType: TYPES.FLOAT, + glSize: 2, + }, + { + property: 'uvs', + name: 'aTextureCoord', + type: 'float32', + size: 2, + glType: TYPES.FLOAT, + glSize: 2, + }, + 'aColor', // built-in attribute + 'aTextureId', + ], vertex: defaultVertex, fragment: defaultFragment, geometryClass: BatchGeometry, - vertexSize: 6, }, options); - return class BatchPlugin extends BaseBatchRenderer + BatchPluginFactory._checkAttributeDefinitionCompatibility(attributeDefinitions); + + const vertexSize = AbstractBatchRenderer.vertexSizeOf(attributeDefinitions); + + return class BatchPlugin extends AbstractBatchRenderer { constructor(renderer) { super(renderer); + this.attributeDefinitions = attributeDefinitions; this.shaderGenerator = new BatchShaderGenerator(vertex, fragment); this.geometryClass = geometryClass; this.vertexSize = vertexSize; @@ -86,6 +121,39 @@ { return defaultFragment; } + + static _checkAttributeDefinitionCompatibility(definitions) + { + definitions.forEach((def) => + { + if (typeof def === 'string') + { + return;// built-in attribute + } + + const inputSize = ViewableBuffer.sizeOf(def.type) * def.size; + + if (inputSize % 4 !== 0) + { + throw new Error('Batch rendering requires that your object ' + + 'attributes be of net size multiple of four. The attribute ' + + `${def.property}, a.k.a ${def.name}, has a source size of` + + `${inputSize}, which is not a multiple of 4. Consider padding` + + 'your elements with additional bytes.'); + } + + const outputSize = sizeOfType(def.glType) * def.glSize; + + if (outputSize !== inputSize) + { + throw new Error('Your object- and gl- types do not match in size.' + + 'The size of each attribute in the object property array is ' + + `${inputSize}, while the buffered size is ${outputSize} in bytes.`); + } + + def._wordSize = inputSize / 4; + }); + } } // Setup the default BatchRenderer plugin, this is what diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js deleted file mode 100644 index daf5dcf..0000000 --- a/packages/core/src/batch/BatchRenderer.js +++ /dev/null @@ -1,653 +0,0 @@ -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 BatchBuffer from './BatchBuffer'; -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 BatchRenderer 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; - - /** - * 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} 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` 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 `BatchBuffer` 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.BatchBuffer} - * @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)(); - } - } - - /** - * 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 and empties the current batch. - * - */ - flush() - { - if (this._vertexCount === 0) - { - return; - } - - const gl = this.renderer.gl; - const MAX_TEXTURES = this.MAX_TEXTURES; - const vertSize = this.vertexSize; - - const buffer = this.getAttributeBuffer(this._vertexCount); - const indexBuffer = this.getIndexBuffer(this._indexCount); - - const elements = this._bufferedElements; - const _drawCalls = this._drawCalls; - - const float32View = buffer.float32View; - const uint32View = buffer.uint32View; - - const touch = this.renderer.textureGC.count; - - let index = 0; - let _indexCount = 0; - let nextTexture; - let currentTexture; - let groupCount = 0; - - let textureCount = 0; - let currentGroup = _drawCalls[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; - - for (i = 0; i < this._bufferSize; ++i) - { - // upload the sprite elements... - // they have all ready been calculated so we just need to push them into the buffer. - - 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, float32View, uint32View, indexBuffer, index, _indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, _indexCount); - - // push a graphics.. - index += (sprite.vertexData.length / 2) * vertSize; - _indexCount += sprite.indices.length; - } - - BaseTexture._globalBatch = TICK; - - currentGroup.size = _indexCount - currentGroup.start; - - if (!settings.CAN_UPLOAD_SAME_BUFFER) - { - // this is still needed for IOS performance.. - // it really does not like uploading to the same buffer in a single frame! - if (this._packedGeometryPoolSize <= this._flushId) - { - this._packedGeometryPoolSize++; - /* eslint-disable max-len */ - this._packedGeometries[this._flushId] = new (this.geometryClass)(); - } - - this._packedGeometries[this._flushId]._buffer.update(buffer.vertices, 0); - this._packedGeometries[this._flushId]._indexBuffer.update(indexBuffer, 0); - - // this.vertexBuffers[this._flushId].update(buffer.vertices, 0); - this.renderer.geometry.bind(this._packedGeometries[this._flushId]); - - this.renderer.geometry.updateBuffers(); - - this._flushId++; - } - else - { - // lets use the faster option, always use buffer number 0 - this._packedGeometries[this._flushId]._buffer.update(buffer.vertices, 0); - this._packedGeometries[this._flushId]._indexBuffer.update(indexBuffer, 0); - - // if (true)// this.spriteOnly) - // { - // this._packedGeometries[this._flushId].indexBuffer = this.defualtSpriteIndexBuffer; - // this._packedGeometries[this._flushId].buffers[1] = this.defualtSpriteIndexBuffer; - // } - - this.renderer.geometry.updateBuffers(); - } - - // this.renderer.state.set(this.state); - - const textureSystem = this.renderer.texture; - const stateSystem = this.renderer.state; - // e.log(groupCount); - // / render the _drawCalls.. - - 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; - } - - // this.state.blendMode = group.blend; - // this.state.blend = true; - - // this.renderer.state.setState(this.state); - // set the blend mode.. - 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 `BatchRenderer`. 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 {BatchBuffer} - 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 BatchBuffer(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 {FLoat32Array} float32View - float32-view of the attribute buffer - * @param {Uint32Array} uint32View - uint32-view of the 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, - float32View, uint32View, indexBuffer, aIndex, iIndex) - { - const p = 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++] = p + indicies[i]; - } - } -} diff --git a/packages/core/src/batch/AbstractBatchRenderer.js b/packages/core/src/batch/AbstractBatchRenderer.js new file mode 100644 index 0000000..7b4d061 --- /dev/null +++ b/packages/core/src/batch/AbstractBatchRenderer.js @@ -0,0 +1,783 @@ +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} 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; + } +} diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js deleted file mode 100644 index b80bc7e..0000000 --- a/packages/core/src/batch/BatchBuffer.js +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Used by the BatchRenderer - * - * @class - * @memberof PIXI - */ -export default class BatchBuffer -{ - /** - * @param {number} size - The size of the buffer in bytes. - */ - constructor(size) - { - this.vertices = new ArrayBuffer(size); - - /** - * View on the vertices as a Float32Array for positions - * - * @member {Float32Array} - */ - this.float32View = new Float32Array(this.vertices); - - /** - * View on the vertices as a Uint32Array for uvs - * - * @member {Float32Array} - */ - this.uint32View = new Uint32Array(this.vertices); - } - - /** - * Destroys the buffer. - * - */ - destroy() - { - this.vertices = null; - this.float32View = null; - this.uint32View = null; - } -} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js index 6275f8c..ad67273 100644 --- a/packages/core/src/batch/BatchGeometry.js +++ b/packages/core/src/batch/BatchGeometry.js @@ -1,9 +1,32 @@ -import { TYPES } from '@pixi/constants'; -import Geometry from '../geometry/Geometry'; import Buffer from '../geometry/Buffer'; +import builtinAttributeDefinitions from './utils/builtinAttributeDefinitions'; +import Geometry from '../geometry/Geometry'; +import { TYPES } from '@pixi/constants'; + +const defaultAttributes = [ + { + property: 'vertexData', + name: 'aVertexPosition', + type: 'float32', + size: 2, + glType: TYPES.FLOAT, + glSize: 2, + }, + { + property: 'uvs', + name: 'aTextureCoord', + type: 'float32', + size: 2, + glType: TYPES.FLOAT, + glSize: 2, + }, + 'aColor', // built-in attribute + 'aTextureId', +]; /** - * Geometry used to batch standard PIXI content (e.g. Mesh, Sprite, Graphics objects). + * Geometry used to batch standard PIXI content (e.g. Mesh, Sprite, + * Graphics objects). * * @class * @memberof PIXI @@ -13,8 +36,9 @@ /** * @param {boolean} [_static=false] Optimization flag, where `false` * is updated every frame, `true` doesn't change frame-to-frame. + * @param {Array} attributeDefinitions - attribute definitions */ - constructor(_static = false) + constructor(_static = false, attributeDefinitions = defaultAttributes) { super(); @@ -34,10 +58,25 @@ */ this._indexBuffer = new Buffer(null, _static, true); - this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) - .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) - .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) - .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) - .addIndex(this._indexBuffer); + /* These are automatically interleaved by GeometrySystem. */ + for (let i = 0; i < attributeDefinitions.length; i++) + { + const def = attributeDefinitions[i]; + + if (def === 'aColor') + { // special + this.addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE); + continue; + } + + const isBuiltin = (typeof def === 'string'); + const identifier = isBuiltin ? def : def.name; + const size = isBuiltin ? builtinAttributeDefinitions[identifier].glSize : def.glSize; + const type = isBuiltin ? builtinAttributeDefinitions[identifier].glType : def.glType; + + this.addAttribute(identifier, this._buffer, size, def === 'aTextureId', type); + } + + this.addIndex(this._indexBuffer); } } diff --git a/packages/core/src/batch/BatchPluginFactory.js b/packages/core/src/batch/BatchPluginFactory.js index ce5a14a..aad5e68 100644 --- a/packages/core/src/batch/BatchPluginFactory.js +++ b/packages/core/src/batch/BatchPluginFactory.js @@ -1,6 +1,10 @@ import BatchShaderGenerator from './BatchShaderGenerator'; import BatchGeometry from './BatchGeometry'; -import BaseBatchRenderer from './BatchRenderer'; +import AbstractBatchRenderer from './AbstractBatchRenderer'; +import ViewableBuffer from '../geometry/ViewableBuffer'; + +import { sizeOfType } from '@pixi/utils'; +import { TYPES } from '@pixi/constants'; import defaultVertex from './texture.vert'; import defaultFragment from './texture.frag'; @@ -35,6 +39,8 @@ * * @static * @param {object} [options] + * @param {object} [option.attributeDefinitions=Array] - + * Attribute definitions, see PIXI.AbstractBatchRenderer#attributeDefinitions * @param {string} [options.vertex=PIXI.BatchPluginFactory.defaultVertexSrc] - Vertex shader source * @param {string} [options.fragment=PIXI.BatchPluginFactory.defaultFragmentTemplate] - Fragment shader template * @param {number} [options.vertexSize=6] - Vertex size @@ -43,19 +49,48 @@ */ static create(options) { - const { vertex, fragment, vertexSize, geometryClass } = Object.assign({ + const { + attributeDefinitions, + fragment, + geometryClass, + vertex, + } = Object.assign({ + attributeDefinitions: [ + { + property: 'vertexData', + name: 'aVertexPosition', + type: 'float32', + size: 2, + glType: TYPES.FLOAT, + glSize: 2, + }, + { + property: 'uvs', + name: 'aTextureCoord', + type: 'float32', + size: 2, + glType: TYPES.FLOAT, + glSize: 2, + }, + 'aColor', // built-in attribute + 'aTextureId', + ], vertex: defaultVertex, fragment: defaultFragment, geometryClass: BatchGeometry, - vertexSize: 6, }, options); - return class BatchPlugin extends BaseBatchRenderer + BatchPluginFactory._checkAttributeDefinitionCompatibility(attributeDefinitions); + + const vertexSize = AbstractBatchRenderer.vertexSizeOf(attributeDefinitions); + + return class BatchPlugin extends AbstractBatchRenderer { constructor(renderer) { super(renderer); + this.attributeDefinitions = attributeDefinitions; this.shaderGenerator = new BatchShaderGenerator(vertex, fragment); this.geometryClass = geometryClass; this.vertexSize = vertexSize; @@ -86,6 +121,39 @@ { return defaultFragment; } + + static _checkAttributeDefinitionCompatibility(definitions) + { + definitions.forEach((def) => + { + if (typeof def === 'string') + { + return;// built-in attribute + } + + const inputSize = ViewableBuffer.sizeOf(def.type) * def.size; + + if (inputSize % 4 !== 0) + { + throw new Error('Batch rendering requires that your object ' + + 'attributes be of net size multiple of four. The attribute ' + + `${def.property}, a.k.a ${def.name}, has a source size of` + + `${inputSize}, which is not a multiple of 4. Consider padding` + + 'your elements with additional bytes.'); + } + + const outputSize = sizeOfType(def.glType) * def.glSize; + + if (outputSize !== inputSize) + { + throw new Error('Your object- and gl- types do not match in size.' + + 'The size of each attribute in the object property array is ' + + `${inputSize}, while the buffered size is ${outputSize} in bytes.`); + } + + def._wordSize = inputSize / 4; + }); + } } // Setup the default BatchRenderer plugin, this is what diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js deleted file mode 100644 index daf5dcf..0000000 --- a/packages/core/src/batch/BatchRenderer.js +++ /dev/null @@ -1,653 +0,0 @@ -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 BatchBuffer from './BatchBuffer'; -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 BatchRenderer 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; - - /** - * 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} 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` 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 `BatchBuffer` 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.BatchBuffer} - * @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)(); - } - } - - /** - * 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 and empties the current batch. - * - */ - flush() - { - if (this._vertexCount === 0) - { - return; - } - - const gl = this.renderer.gl; - const MAX_TEXTURES = this.MAX_TEXTURES; - const vertSize = this.vertexSize; - - const buffer = this.getAttributeBuffer(this._vertexCount); - const indexBuffer = this.getIndexBuffer(this._indexCount); - - const elements = this._bufferedElements; - const _drawCalls = this._drawCalls; - - const float32View = buffer.float32View; - const uint32View = buffer.uint32View; - - const touch = this.renderer.textureGC.count; - - let index = 0; - let _indexCount = 0; - let nextTexture; - let currentTexture; - let groupCount = 0; - - let textureCount = 0; - let currentGroup = _drawCalls[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; - - for (i = 0; i < this._bufferSize; ++i) - { - // upload the sprite elements... - // they have all ready been calculated so we just need to push them into the buffer. - - 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, float32View, uint32View, indexBuffer, index, _indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, _indexCount); - - // push a graphics.. - index += (sprite.vertexData.length / 2) * vertSize; - _indexCount += sprite.indices.length; - } - - BaseTexture._globalBatch = TICK; - - currentGroup.size = _indexCount - currentGroup.start; - - if (!settings.CAN_UPLOAD_SAME_BUFFER) - { - // this is still needed for IOS performance.. - // it really does not like uploading to the same buffer in a single frame! - if (this._packedGeometryPoolSize <= this._flushId) - { - this._packedGeometryPoolSize++; - /* eslint-disable max-len */ - this._packedGeometries[this._flushId] = new (this.geometryClass)(); - } - - this._packedGeometries[this._flushId]._buffer.update(buffer.vertices, 0); - this._packedGeometries[this._flushId]._indexBuffer.update(indexBuffer, 0); - - // this.vertexBuffers[this._flushId].update(buffer.vertices, 0); - this.renderer.geometry.bind(this._packedGeometries[this._flushId]); - - this.renderer.geometry.updateBuffers(); - - this._flushId++; - } - else - { - // lets use the faster option, always use buffer number 0 - this._packedGeometries[this._flushId]._buffer.update(buffer.vertices, 0); - this._packedGeometries[this._flushId]._indexBuffer.update(indexBuffer, 0); - - // if (true)// this.spriteOnly) - // { - // this._packedGeometries[this._flushId].indexBuffer = this.defualtSpriteIndexBuffer; - // this._packedGeometries[this._flushId].buffers[1] = this.defualtSpriteIndexBuffer; - // } - - this.renderer.geometry.updateBuffers(); - } - - // this.renderer.state.set(this.state); - - const textureSystem = this.renderer.texture; - const stateSystem = this.renderer.state; - // e.log(groupCount); - // / render the _drawCalls.. - - 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; - } - - // this.state.blendMode = group.blend; - // this.state.blend = true; - - // this.renderer.state.setState(this.state); - // set the blend mode.. - 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 `BatchRenderer`. 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 {BatchBuffer} - 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 BatchBuffer(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 {FLoat32Array} float32View - float32-view of the attribute buffer - * @param {Uint32Array} uint32View - uint32-view of the 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, - float32View, uint32View, indexBuffer, aIndex, iIndex) - { - const p = 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++] = p + indicies[i]; - } - } -} diff --git a/packages/core/src/batch/BatchSystem.js b/packages/core/src/batch/BatchSystem.js index 2d1a8bf..5b75137 100644 --- a/packages/core/src/batch/BatchSystem.js +++ b/packages/core/src/batch/BatchSystem.js @@ -46,7 +46,6 @@ this.currentRenderer.stop(); this.currentRenderer = objectRenderer; - this.currentRenderer.start(); } diff --git a/packages/core/src/batch/AbstractBatchRenderer.js b/packages/core/src/batch/AbstractBatchRenderer.js new file mode 100644 index 0000000..7b4d061 --- /dev/null +++ b/packages/core/src/batch/AbstractBatchRenderer.js @@ -0,0 +1,783 @@ +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} 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; + } +} diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js deleted file mode 100644 index b80bc7e..0000000 --- a/packages/core/src/batch/BatchBuffer.js +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Used by the BatchRenderer - * - * @class - * @memberof PIXI - */ -export default class BatchBuffer -{ - /** - * @param {number} size - The size of the buffer in bytes. - */ - constructor(size) - { - this.vertices = new ArrayBuffer(size); - - /** - * View on the vertices as a Float32Array for positions - * - * @member {Float32Array} - */ - this.float32View = new Float32Array(this.vertices); - - /** - * View on the vertices as a Uint32Array for uvs - * - * @member {Float32Array} - */ - this.uint32View = new Uint32Array(this.vertices); - } - - /** - * Destroys the buffer. - * - */ - destroy() - { - this.vertices = null; - this.float32View = null; - this.uint32View = null; - } -} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js index 6275f8c..ad67273 100644 --- a/packages/core/src/batch/BatchGeometry.js +++ b/packages/core/src/batch/BatchGeometry.js @@ -1,9 +1,32 @@ -import { TYPES } from '@pixi/constants'; -import Geometry from '../geometry/Geometry'; import Buffer from '../geometry/Buffer'; +import builtinAttributeDefinitions from './utils/builtinAttributeDefinitions'; +import Geometry from '../geometry/Geometry'; +import { TYPES } from '@pixi/constants'; + +const defaultAttributes = [ + { + property: 'vertexData', + name: 'aVertexPosition', + type: 'float32', + size: 2, + glType: TYPES.FLOAT, + glSize: 2, + }, + { + property: 'uvs', + name: 'aTextureCoord', + type: 'float32', + size: 2, + glType: TYPES.FLOAT, + glSize: 2, + }, + 'aColor', // built-in attribute + 'aTextureId', +]; /** - * Geometry used to batch standard PIXI content (e.g. Mesh, Sprite, Graphics objects). + * Geometry used to batch standard PIXI content (e.g. Mesh, Sprite, + * Graphics objects). * * @class * @memberof PIXI @@ -13,8 +36,9 @@ /** * @param {boolean} [_static=false] Optimization flag, where `false` * is updated every frame, `true` doesn't change frame-to-frame. + * @param {Array} attributeDefinitions - attribute definitions */ - constructor(_static = false) + constructor(_static = false, attributeDefinitions = defaultAttributes) { super(); @@ -34,10 +58,25 @@ */ this._indexBuffer = new Buffer(null, _static, true); - this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) - .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) - .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) - .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) - .addIndex(this._indexBuffer); + /* These are automatically interleaved by GeometrySystem. */ + for (let i = 0; i < attributeDefinitions.length; i++) + { + const def = attributeDefinitions[i]; + + if (def === 'aColor') + { // special + this.addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE); + continue; + } + + const isBuiltin = (typeof def === 'string'); + const identifier = isBuiltin ? def : def.name; + const size = isBuiltin ? builtinAttributeDefinitions[identifier].glSize : def.glSize; + const type = isBuiltin ? builtinAttributeDefinitions[identifier].glType : def.glType; + + this.addAttribute(identifier, this._buffer, size, def === 'aTextureId', type); + } + + this.addIndex(this._indexBuffer); } } diff --git a/packages/core/src/batch/BatchPluginFactory.js b/packages/core/src/batch/BatchPluginFactory.js index ce5a14a..aad5e68 100644 --- a/packages/core/src/batch/BatchPluginFactory.js +++ b/packages/core/src/batch/BatchPluginFactory.js @@ -1,6 +1,10 @@ import BatchShaderGenerator from './BatchShaderGenerator'; import BatchGeometry from './BatchGeometry'; -import BaseBatchRenderer from './BatchRenderer'; +import AbstractBatchRenderer from './AbstractBatchRenderer'; +import ViewableBuffer from '../geometry/ViewableBuffer'; + +import { sizeOfType } from '@pixi/utils'; +import { TYPES } from '@pixi/constants'; import defaultVertex from './texture.vert'; import defaultFragment from './texture.frag'; @@ -35,6 +39,8 @@ * * @static * @param {object} [options] + * @param {object} [option.attributeDefinitions=Array] - + * Attribute definitions, see PIXI.AbstractBatchRenderer#attributeDefinitions * @param {string} [options.vertex=PIXI.BatchPluginFactory.defaultVertexSrc] - Vertex shader source * @param {string} [options.fragment=PIXI.BatchPluginFactory.defaultFragmentTemplate] - Fragment shader template * @param {number} [options.vertexSize=6] - Vertex size @@ -43,19 +49,48 @@ */ static create(options) { - const { vertex, fragment, vertexSize, geometryClass } = Object.assign({ + const { + attributeDefinitions, + fragment, + geometryClass, + vertex, + } = Object.assign({ + attributeDefinitions: [ + { + property: 'vertexData', + name: 'aVertexPosition', + type: 'float32', + size: 2, + glType: TYPES.FLOAT, + glSize: 2, + }, + { + property: 'uvs', + name: 'aTextureCoord', + type: 'float32', + size: 2, + glType: TYPES.FLOAT, + glSize: 2, + }, + 'aColor', // built-in attribute + 'aTextureId', + ], vertex: defaultVertex, fragment: defaultFragment, geometryClass: BatchGeometry, - vertexSize: 6, }, options); - return class BatchPlugin extends BaseBatchRenderer + BatchPluginFactory._checkAttributeDefinitionCompatibility(attributeDefinitions); + + const vertexSize = AbstractBatchRenderer.vertexSizeOf(attributeDefinitions); + + return class BatchPlugin extends AbstractBatchRenderer { constructor(renderer) { super(renderer); + this.attributeDefinitions = attributeDefinitions; this.shaderGenerator = new BatchShaderGenerator(vertex, fragment); this.geometryClass = geometryClass; this.vertexSize = vertexSize; @@ -86,6 +121,39 @@ { return defaultFragment; } + + static _checkAttributeDefinitionCompatibility(definitions) + { + definitions.forEach((def) => + { + if (typeof def === 'string') + { + return;// built-in attribute + } + + const inputSize = ViewableBuffer.sizeOf(def.type) * def.size; + + if (inputSize % 4 !== 0) + { + throw new Error('Batch rendering requires that your object ' + + 'attributes be of net size multiple of four. The attribute ' + + `${def.property}, a.k.a ${def.name}, has a source size of` + + `${inputSize}, which is not a multiple of 4. Consider padding` + + 'your elements with additional bytes.'); + } + + const outputSize = sizeOfType(def.glType) * def.glSize; + + if (outputSize !== inputSize) + { + throw new Error('Your object- and gl- types do not match in size.' + + 'The size of each attribute in the object property array is ' + + `${inputSize}, while the buffered size is ${outputSize} in bytes.`); + } + + def._wordSize = inputSize / 4; + }); + } } // Setup the default BatchRenderer plugin, this is what diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js deleted file mode 100644 index daf5dcf..0000000 --- a/packages/core/src/batch/BatchRenderer.js +++ /dev/null @@ -1,653 +0,0 @@ -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 BatchBuffer from './BatchBuffer'; -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 BatchRenderer 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; - - /** - * 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} 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` 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 `BatchBuffer` 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.BatchBuffer} - * @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)(); - } - } - - /** - * 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 and empties the current batch. - * - */ - flush() - { - if (this._vertexCount === 0) - { - return; - } - - const gl = this.renderer.gl; - const MAX_TEXTURES = this.MAX_TEXTURES; - const vertSize = this.vertexSize; - - const buffer = this.getAttributeBuffer(this._vertexCount); - const indexBuffer = this.getIndexBuffer(this._indexCount); - - const elements = this._bufferedElements; - const _drawCalls = this._drawCalls; - - const float32View = buffer.float32View; - const uint32View = buffer.uint32View; - - const touch = this.renderer.textureGC.count; - - let index = 0; - let _indexCount = 0; - let nextTexture; - let currentTexture; - let groupCount = 0; - - let textureCount = 0; - let currentGroup = _drawCalls[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; - - for (i = 0; i < this._bufferSize; ++i) - { - // upload the sprite elements... - // they have all ready been calculated so we just need to push them into the buffer. - - 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, float32View, uint32View, indexBuffer, index, _indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, _indexCount); - - // push a graphics.. - index += (sprite.vertexData.length / 2) * vertSize; - _indexCount += sprite.indices.length; - } - - BaseTexture._globalBatch = TICK; - - currentGroup.size = _indexCount - currentGroup.start; - - if (!settings.CAN_UPLOAD_SAME_BUFFER) - { - // this is still needed for IOS performance.. - // it really does not like uploading to the same buffer in a single frame! - if (this._packedGeometryPoolSize <= this._flushId) - { - this._packedGeometryPoolSize++; - /* eslint-disable max-len */ - this._packedGeometries[this._flushId] = new (this.geometryClass)(); - } - - this._packedGeometries[this._flushId]._buffer.update(buffer.vertices, 0); - this._packedGeometries[this._flushId]._indexBuffer.update(indexBuffer, 0); - - // this.vertexBuffers[this._flushId].update(buffer.vertices, 0); - this.renderer.geometry.bind(this._packedGeometries[this._flushId]); - - this.renderer.geometry.updateBuffers(); - - this._flushId++; - } - else - { - // lets use the faster option, always use buffer number 0 - this._packedGeometries[this._flushId]._buffer.update(buffer.vertices, 0); - this._packedGeometries[this._flushId]._indexBuffer.update(indexBuffer, 0); - - // if (true)// this.spriteOnly) - // { - // this._packedGeometries[this._flushId].indexBuffer = this.defualtSpriteIndexBuffer; - // this._packedGeometries[this._flushId].buffers[1] = this.defualtSpriteIndexBuffer; - // } - - this.renderer.geometry.updateBuffers(); - } - - // this.renderer.state.set(this.state); - - const textureSystem = this.renderer.texture; - const stateSystem = this.renderer.state; - // e.log(groupCount); - // / render the _drawCalls.. - - 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; - } - - // this.state.blendMode = group.blend; - // this.state.blend = true; - - // this.renderer.state.setState(this.state); - // set the blend mode.. - 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 `BatchRenderer`. 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 {BatchBuffer} - 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 BatchBuffer(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 {FLoat32Array} float32View - float32-view of the attribute buffer - * @param {Uint32Array} uint32View - uint32-view of the 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, - float32View, uint32View, indexBuffer, aIndex, iIndex) - { - const p = 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++] = p + indicies[i]; - } - } -} diff --git a/packages/core/src/batch/BatchSystem.js b/packages/core/src/batch/BatchSystem.js index 2d1a8bf..5b75137 100644 --- a/packages/core/src/batch/BatchSystem.js +++ b/packages/core/src/batch/BatchSystem.js @@ -46,7 +46,6 @@ this.currentRenderer.stop(); this.currentRenderer = objectRenderer; - this.currentRenderer.start(); } diff --git a/packages/core/src/batch/ObjectRenderer.js b/packages/core/src/batch/ObjectRenderer.js index 0c851a9..2788895 100644 --- a/packages/core/src/batch/ObjectRenderer.js +++ b/packages/core/src/batch/ObjectRenderer.js @@ -1,5 +1,3 @@ -import System from '../System'; - /** * Base for a common object renderer that can be used as a system renderer plugin. * @@ -7,29 +5,24 @@ * @extends PIXI.System * @memberof PIXI */ -export default class ObjectRenderer extends System +export default class ObjectRenderer { /** - * Starts the renderer and sets the shader - * + * @param {PIXI.Renderer} renderer - The renderer this manager works for. */ - start() + constructor(renderer) { - // set the shader.. + /** + * The renderer this manager works for. + * + * @member {PIXI.Renderer} + */ + this.renderer = renderer; } /** - * Stops the renderer - * - */ - stop() - { - this.flush(); - } - - /** - * Stub method for rendering content and emptying the current batch. - * + * Stub method that should be used to empty the current + * batch by rendering objects now. */ flush() { @@ -37,7 +30,37 @@ } /** - * Renders an object + * Generic destruction method that frees all resources. This + * should be called by subclasses. + */ + destroy() + { + this.renderer = null; + } + + /** + * Stub method that initializes any state required before + * rendering starts. It is different from the `prerender` + * signal, which occurs every frame, in that it is called + * whenever an object requests _this_ renderer specifically. + */ + start() + { + // set the shader.. + } + + /** + * Stops the renderer. It should free up any state and + * become dormant. + */ + stop() + { + this.flush(); + } + + /** + * Keeps the object to render. It doesn't have to be + * rendered immediately. * * @param {PIXI.DisplayObject} object - The object to render. */ diff --git a/packages/core/src/batch/AbstractBatchRenderer.js b/packages/core/src/batch/AbstractBatchRenderer.js new file mode 100644 index 0000000..7b4d061 --- /dev/null +++ b/packages/core/src/batch/AbstractBatchRenderer.js @@ -0,0 +1,783 @@ +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} 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; + } +} diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js deleted file mode 100644 index b80bc7e..0000000 --- a/packages/core/src/batch/BatchBuffer.js +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Used by the BatchRenderer - * - * @class - * @memberof PIXI - */ -export default class BatchBuffer -{ - /** - * @param {number} size - The size of the buffer in bytes. - */ - constructor(size) - { - this.vertices = new ArrayBuffer(size); - - /** - * View on the vertices as a Float32Array for positions - * - * @member {Float32Array} - */ - this.float32View = new Float32Array(this.vertices); - - /** - * View on the vertices as a Uint32Array for uvs - * - * @member {Float32Array} - */ - this.uint32View = new Uint32Array(this.vertices); - } - - /** - * Destroys the buffer. - * - */ - destroy() - { - this.vertices = null; - this.float32View = null; - this.uint32View = null; - } -} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js index 6275f8c..ad67273 100644 --- a/packages/core/src/batch/BatchGeometry.js +++ b/packages/core/src/batch/BatchGeometry.js @@ -1,9 +1,32 @@ -import { TYPES } from '@pixi/constants'; -import Geometry from '../geometry/Geometry'; import Buffer from '../geometry/Buffer'; +import builtinAttributeDefinitions from './utils/builtinAttributeDefinitions'; +import Geometry from '../geometry/Geometry'; +import { TYPES } from '@pixi/constants'; + +const defaultAttributes = [ + { + property: 'vertexData', + name: 'aVertexPosition', + type: 'float32', + size: 2, + glType: TYPES.FLOAT, + glSize: 2, + }, + { + property: 'uvs', + name: 'aTextureCoord', + type: 'float32', + size: 2, + glType: TYPES.FLOAT, + glSize: 2, + }, + 'aColor', // built-in attribute + 'aTextureId', +]; /** - * Geometry used to batch standard PIXI content (e.g. Mesh, Sprite, Graphics objects). + * Geometry used to batch standard PIXI content (e.g. Mesh, Sprite, + * Graphics objects). * * @class * @memberof PIXI @@ -13,8 +36,9 @@ /** * @param {boolean} [_static=false] Optimization flag, where `false` * is updated every frame, `true` doesn't change frame-to-frame. + * @param {Array} attributeDefinitions - attribute definitions */ - constructor(_static = false) + constructor(_static = false, attributeDefinitions = defaultAttributes) { super(); @@ -34,10 +58,25 @@ */ this._indexBuffer = new Buffer(null, _static, true); - this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) - .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) - .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) - .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) - .addIndex(this._indexBuffer); + /* These are automatically interleaved by GeometrySystem. */ + for (let i = 0; i < attributeDefinitions.length; i++) + { + const def = attributeDefinitions[i]; + + if (def === 'aColor') + { // special + this.addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE); + continue; + } + + const isBuiltin = (typeof def === 'string'); + const identifier = isBuiltin ? def : def.name; + const size = isBuiltin ? builtinAttributeDefinitions[identifier].glSize : def.glSize; + const type = isBuiltin ? builtinAttributeDefinitions[identifier].glType : def.glType; + + this.addAttribute(identifier, this._buffer, size, def === 'aTextureId', type); + } + + this.addIndex(this._indexBuffer); } } diff --git a/packages/core/src/batch/BatchPluginFactory.js b/packages/core/src/batch/BatchPluginFactory.js index ce5a14a..aad5e68 100644 --- a/packages/core/src/batch/BatchPluginFactory.js +++ b/packages/core/src/batch/BatchPluginFactory.js @@ -1,6 +1,10 @@ import BatchShaderGenerator from './BatchShaderGenerator'; import BatchGeometry from './BatchGeometry'; -import BaseBatchRenderer from './BatchRenderer'; +import AbstractBatchRenderer from './AbstractBatchRenderer'; +import ViewableBuffer from '../geometry/ViewableBuffer'; + +import { sizeOfType } from '@pixi/utils'; +import { TYPES } from '@pixi/constants'; import defaultVertex from './texture.vert'; import defaultFragment from './texture.frag'; @@ -35,6 +39,8 @@ * * @static * @param {object} [options] + * @param {object} [option.attributeDefinitions=Array] - + * Attribute definitions, see PIXI.AbstractBatchRenderer#attributeDefinitions * @param {string} [options.vertex=PIXI.BatchPluginFactory.defaultVertexSrc] - Vertex shader source * @param {string} [options.fragment=PIXI.BatchPluginFactory.defaultFragmentTemplate] - Fragment shader template * @param {number} [options.vertexSize=6] - Vertex size @@ -43,19 +49,48 @@ */ static create(options) { - const { vertex, fragment, vertexSize, geometryClass } = Object.assign({ + const { + attributeDefinitions, + fragment, + geometryClass, + vertex, + } = Object.assign({ + attributeDefinitions: [ + { + property: 'vertexData', + name: 'aVertexPosition', + type: 'float32', + size: 2, + glType: TYPES.FLOAT, + glSize: 2, + }, + { + property: 'uvs', + name: 'aTextureCoord', + type: 'float32', + size: 2, + glType: TYPES.FLOAT, + glSize: 2, + }, + 'aColor', // built-in attribute + 'aTextureId', + ], vertex: defaultVertex, fragment: defaultFragment, geometryClass: BatchGeometry, - vertexSize: 6, }, options); - return class BatchPlugin extends BaseBatchRenderer + BatchPluginFactory._checkAttributeDefinitionCompatibility(attributeDefinitions); + + const vertexSize = AbstractBatchRenderer.vertexSizeOf(attributeDefinitions); + + return class BatchPlugin extends AbstractBatchRenderer { constructor(renderer) { super(renderer); + this.attributeDefinitions = attributeDefinitions; this.shaderGenerator = new BatchShaderGenerator(vertex, fragment); this.geometryClass = geometryClass; this.vertexSize = vertexSize; @@ -86,6 +121,39 @@ { return defaultFragment; } + + static _checkAttributeDefinitionCompatibility(definitions) + { + definitions.forEach((def) => + { + if (typeof def === 'string') + { + return;// built-in attribute + } + + const inputSize = ViewableBuffer.sizeOf(def.type) * def.size; + + if (inputSize % 4 !== 0) + { + throw new Error('Batch rendering requires that your object ' + + 'attributes be of net size multiple of four. The attribute ' + + `${def.property}, a.k.a ${def.name}, has a source size of` + + `${inputSize}, which is not a multiple of 4. Consider padding` + + 'your elements with additional bytes.'); + } + + const outputSize = sizeOfType(def.glType) * def.glSize; + + if (outputSize !== inputSize) + { + throw new Error('Your object- and gl- types do not match in size.' + + 'The size of each attribute in the object property array is ' + + `${inputSize}, while the buffered size is ${outputSize} in bytes.`); + } + + def._wordSize = inputSize / 4; + }); + } } // Setup the default BatchRenderer plugin, this is what diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js deleted file mode 100644 index daf5dcf..0000000 --- a/packages/core/src/batch/BatchRenderer.js +++ /dev/null @@ -1,653 +0,0 @@ -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 BatchBuffer from './BatchBuffer'; -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 BatchRenderer 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; - - /** - * 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} 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` 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 `BatchBuffer` 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.BatchBuffer} - * @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)(); - } - } - - /** - * 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 and empties the current batch. - * - */ - flush() - { - if (this._vertexCount === 0) - { - return; - } - - const gl = this.renderer.gl; - const MAX_TEXTURES = this.MAX_TEXTURES; - const vertSize = this.vertexSize; - - const buffer = this.getAttributeBuffer(this._vertexCount); - const indexBuffer = this.getIndexBuffer(this._indexCount); - - const elements = this._bufferedElements; - const _drawCalls = this._drawCalls; - - const float32View = buffer.float32View; - const uint32View = buffer.uint32View; - - const touch = this.renderer.textureGC.count; - - let index = 0; - let _indexCount = 0; - let nextTexture; - let currentTexture; - let groupCount = 0; - - let textureCount = 0; - let currentGroup = _drawCalls[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; - - for (i = 0; i < this._bufferSize; ++i) - { - // upload the sprite elements... - // they have all ready been calculated so we just need to push them into the buffer. - - 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, float32View, uint32View, indexBuffer, index, _indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, _indexCount); - - // push a graphics.. - index += (sprite.vertexData.length / 2) * vertSize; - _indexCount += sprite.indices.length; - } - - BaseTexture._globalBatch = TICK; - - currentGroup.size = _indexCount - currentGroup.start; - - if (!settings.CAN_UPLOAD_SAME_BUFFER) - { - // this is still needed for IOS performance.. - // it really does not like uploading to the same buffer in a single frame! - if (this._packedGeometryPoolSize <= this._flushId) - { - this._packedGeometryPoolSize++; - /* eslint-disable max-len */ - this._packedGeometries[this._flushId] = new (this.geometryClass)(); - } - - this._packedGeometries[this._flushId]._buffer.update(buffer.vertices, 0); - this._packedGeometries[this._flushId]._indexBuffer.update(indexBuffer, 0); - - // this.vertexBuffers[this._flushId].update(buffer.vertices, 0); - this.renderer.geometry.bind(this._packedGeometries[this._flushId]); - - this.renderer.geometry.updateBuffers(); - - this._flushId++; - } - else - { - // lets use the faster option, always use buffer number 0 - this._packedGeometries[this._flushId]._buffer.update(buffer.vertices, 0); - this._packedGeometries[this._flushId]._indexBuffer.update(indexBuffer, 0); - - // if (true)// this.spriteOnly) - // { - // this._packedGeometries[this._flushId].indexBuffer = this.defualtSpriteIndexBuffer; - // this._packedGeometries[this._flushId].buffers[1] = this.defualtSpriteIndexBuffer; - // } - - this.renderer.geometry.updateBuffers(); - } - - // this.renderer.state.set(this.state); - - const textureSystem = this.renderer.texture; - const stateSystem = this.renderer.state; - // e.log(groupCount); - // / render the _drawCalls.. - - 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; - } - - // this.state.blendMode = group.blend; - // this.state.blend = true; - - // this.renderer.state.setState(this.state); - // set the blend mode.. - 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 `BatchRenderer`. 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 {BatchBuffer} - 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 BatchBuffer(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 {FLoat32Array} float32View - float32-view of the attribute buffer - * @param {Uint32Array} uint32View - uint32-view of the 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, - float32View, uint32View, indexBuffer, aIndex, iIndex) - { - const p = 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++] = p + indicies[i]; - } - } -} diff --git a/packages/core/src/batch/BatchSystem.js b/packages/core/src/batch/BatchSystem.js index 2d1a8bf..5b75137 100644 --- a/packages/core/src/batch/BatchSystem.js +++ b/packages/core/src/batch/BatchSystem.js @@ -46,7 +46,6 @@ this.currentRenderer.stop(); this.currentRenderer = objectRenderer; - this.currentRenderer.start(); } diff --git a/packages/core/src/batch/ObjectRenderer.js b/packages/core/src/batch/ObjectRenderer.js index 0c851a9..2788895 100644 --- a/packages/core/src/batch/ObjectRenderer.js +++ b/packages/core/src/batch/ObjectRenderer.js @@ -1,5 +1,3 @@ -import System from '../System'; - /** * Base for a common object renderer that can be used as a system renderer plugin. * @@ -7,29 +5,24 @@ * @extends PIXI.System * @memberof PIXI */ -export default class ObjectRenderer extends System +export default class ObjectRenderer { /** - * Starts the renderer and sets the shader - * + * @param {PIXI.Renderer} renderer - The renderer this manager works for. */ - start() + constructor(renderer) { - // set the shader.. + /** + * The renderer this manager works for. + * + * @member {PIXI.Renderer} + */ + this.renderer = renderer; } /** - * Stops the renderer - * - */ - stop() - { - this.flush(); - } - - /** - * Stub method for rendering content and emptying the current batch. - * + * Stub method that should be used to empty the current + * batch by rendering objects now. */ flush() { @@ -37,7 +30,37 @@ } /** - * Renders an object + * Generic destruction method that frees all resources. This + * should be called by subclasses. + */ + destroy() + { + this.renderer = null; + } + + /** + * Stub method that initializes any state required before + * rendering starts. It is different from the `prerender` + * signal, which occurs every frame, in that it is called + * whenever an object requests _this_ renderer specifically. + */ + start() + { + // set the shader.. + } + + /** + * Stops the renderer. It should free up any state and + * become dormant. + */ + stop() + { + this.flush(); + } + + /** + * Keeps the object to render. It doesn't have to be + * rendered immediately. * * @param {PIXI.DisplayObject} object - The object to render. */ diff --git a/packages/core/src/batch/utils/builtinAttributeDefinitions.js b/packages/core/src/batch/utils/builtinAttributeDefinitions.js new file mode 100644 index 0000000..edbf531 --- /dev/null +++ b/packages/core/src/batch/utils/builtinAttributeDefinitions.js @@ -0,0 +1,18 @@ +import { TYPES } from '@pixi/constants'; + +export default { + aColor: { + type: 'uint32', + size: 1, + glType: TYPES.UNSIGNED_BYTE, + glSize: 4, + _wordSize: 1, + }, + aTextureId: { + type: 'float32', + size: 1, + glType: TYPES.FLOAT, + glSize: 1, + _wordSize: 1, + }, +}; diff --git a/packages/core/src/batch/AbstractBatchRenderer.js b/packages/core/src/batch/AbstractBatchRenderer.js new file mode 100644 index 0000000..7b4d061 --- /dev/null +++ b/packages/core/src/batch/AbstractBatchRenderer.js @@ -0,0 +1,783 @@ +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} 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; + } +} diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js deleted file mode 100644 index b80bc7e..0000000 --- a/packages/core/src/batch/BatchBuffer.js +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Used by the BatchRenderer - * - * @class - * @memberof PIXI - */ -export default class BatchBuffer -{ - /** - * @param {number} size - The size of the buffer in bytes. - */ - constructor(size) - { - this.vertices = new ArrayBuffer(size); - - /** - * View on the vertices as a Float32Array for positions - * - * @member {Float32Array} - */ - this.float32View = new Float32Array(this.vertices); - - /** - * View on the vertices as a Uint32Array for uvs - * - * @member {Float32Array} - */ - this.uint32View = new Uint32Array(this.vertices); - } - - /** - * Destroys the buffer. - * - */ - destroy() - { - this.vertices = null; - this.float32View = null; - this.uint32View = null; - } -} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js index 6275f8c..ad67273 100644 --- a/packages/core/src/batch/BatchGeometry.js +++ b/packages/core/src/batch/BatchGeometry.js @@ -1,9 +1,32 @@ -import { TYPES } from '@pixi/constants'; -import Geometry from '../geometry/Geometry'; import Buffer from '../geometry/Buffer'; +import builtinAttributeDefinitions from './utils/builtinAttributeDefinitions'; +import Geometry from '../geometry/Geometry'; +import { TYPES } from '@pixi/constants'; + +const defaultAttributes = [ + { + property: 'vertexData', + name: 'aVertexPosition', + type: 'float32', + size: 2, + glType: TYPES.FLOAT, + glSize: 2, + }, + { + property: 'uvs', + name: 'aTextureCoord', + type: 'float32', + size: 2, + glType: TYPES.FLOAT, + glSize: 2, + }, + 'aColor', // built-in attribute + 'aTextureId', +]; /** - * Geometry used to batch standard PIXI content (e.g. Mesh, Sprite, Graphics objects). + * Geometry used to batch standard PIXI content (e.g. Mesh, Sprite, + * Graphics objects). * * @class * @memberof PIXI @@ -13,8 +36,9 @@ /** * @param {boolean} [_static=false] Optimization flag, where `false` * is updated every frame, `true` doesn't change frame-to-frame. + * @param {Array} attributeDefinitions - attribute definitions */ - constructor(_static = false) + constructor(_static = false, attributeDefinitions = defaultAttributes) { super(); @@ -34,10 +58,25 @@ */ this._indexBuffer = new Buffer(null, _static, true); - this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) - .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) - .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) - .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) - .addIndex(this._indexBuffer); + /* These are automatically interleaved by GeometrySystem. */ + for (let i = 0; i < attributeDefinitions.length; i++) + { + const def = attributeDefinitions[i]; + + if (def === 'aColor') + { // special + this.addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE); + continue; + } + + const isBuiltin = (typeof def === 'string'); + const identifier = isBuiltin ? def : def.name; + const size = isBuiltin ? builtinAttributeDefinitions[identifier].glSize : def.glSize; + const type = isBuiltin ? builtinAttributeDefinitions[identifier].glType : def.glType; + + this.addAttribute(identifier, this._buffer, size, def === 'aTextureId', type); + } + + this.addIndex(this._indexBuffer); } } diff --git a/packages/core/src/batch/BatchPluginFactory.js b/packages/core/src/batch/BatchPluginFactory.js index ce5a14a..aad5e68 100644 --- a/packages/core/src/batch/BatchPluginFactory.js +++ b/packages/core/src/batch/BatchPluginFactory.js @@ -1,6 +1,10 @@ import BatchShaderGenerator from './BatchShaderGenerator'; import BatchGeometry from './BatchGeometry'; -import BaseBatchRenderer from './BatchRenderer'; +import AbstractBatchRenderer from './AbstractBatchRenderer'; +import ViewableBuffer from '../geometry/ViewableBuffer'; + +import { sizeOfType } from '@pixi/utils'; +import { TYPES } from '@pixi/constants'; import defaultVertex from './texture.vert'; import defaultFragment from './texture.frag'; @@ -35,6 +39,8 @@ * * @static * @param {object} [options] + * @param {object} [option.attributeDefinitions=Array] - + * Attribute definitions, see PIXI.AbstractBatchRenderer#attributeDefinitions * @param {string} [options.vertex=PIXI.BatchPluginFactory.defaultVertexSrc] - Vertex shader source * @param {string} [options.fragment=PIXI.BatchPluginFactory.defaultFragmentTemplate] - Fragment shader template * @param {number} [options.vertexSize=6] - Vertex size @@ -43,19 +49,48 @@ */ static create(options) { - const { vertex, fragment, vertexSize, geometryClass } = Object.assign({ + const { + attributeDefinitions, + fragment, + geometryClass, + vertex, + } = Object.assign({ + attributeDefinitions: [ + { + property: 'vertexData', + name: 'aVertexPosition', + type: 'float32', + size: 2, + glType: TYPES.FLOAT, + glSize: 2, + }, + { + property: 'uvs', + name: 'aTextureCoord', + type: 'float32', + size: 2, + glType: TYPES.FLOAT, + glSize: 2, + }, + 'aColor', // built-in attribute + 'aTextureId', + ], vertex: defaultVertex, fragment: defaultFragment, geometryClass: BatchGeometry, - vertexSize: 6, }, options); - return class BatchPlugin extends BaseBatchRenderer + BatchPluginFactory._checkAttributeDefinitionCompatibility(attributeDefinitions); + + const vertexSize = AbstractBatchRenderer.vertexSizeOf(attributeDefinitions); + + return class BatchPlugin extends AbstractBatchRenderer { constructor(renderer) { super(renderer); + this.attributeDefinitions = attributeDefinitions; this.shaderGenerator = new BatchShaderGenerator(vertex, fragment); this.geometryClass = geometryClass; this.vertexSize = vertexSize; @@ -86,6 +121,39 @@ { return defaultFragment; } + + static _checkAttributeDefinitionCompatibility(definitions) + { + definitions.forEach((def) => + { + if (typeof def === 'string') + { + return;// built-in attribute + } + + const inputSize = ViewableBuffer.sizeOf(def.type) * def.size; + + if (inputSize % 4 !== 0) + { + throw new Error('Batch rendering requires that your object ' + + 'attributes be of net size multiple of four. The attribute ' + + `${def.property}, a.k.a ${def.name}, has a source size of` + + `${inputSize}, which is not a multiple of 4. Consider padding` + + 'your elements with additional bytes.'); + } + + const outputSize = sizeOfType(def.glType) * def.glSize; + + if (outputSize !== inputSize) + { + throw new Error('Your object- and gl- types do not match in size.' + + 'The size of each attribute in the object property array is ' + + `${inputSize}, while the buffered size is ${outputSize} in bytes.`); + } + + def._wordSize = inputSize / 4; + }); + } } // Setup the default BatchRenderer plugin, this is what diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js deleted file mode 100644 index daf5dcf..0000000 --- a/packages/core/src/batch/BatchRenderer.js +++ /dev/null @@ -1,653 +0,0 @@ -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 BatchBuffer from './BatchBuffer'; -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 BatchRenderer 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; - - /** - * 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} 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` 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 `BatchBuffer` 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.BatchBuffer} - * @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)(); - } - } - - /** - * 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 and empties the current batch. - * - */ - flush() - { - if (this._vertexCount === 0) - { - return; - } - - const gl = this.renderer.gl; - const MAX_TEXTURES = this.MAX_TEXTURES; - const vertSize = this.vertexSize; - - const buffer = this.getAttributeBuffer(this._vertexCount); - const indexBuffer = this.getIndexBuffer(this._indexCount); - - const elements = this._bufferedElements; - const _drawCalls = this._drawCalls; - - const float32View = buffer.float32View; - const uint32View = buffer.uint32View; - - const touch = this.renderer.textureGC.count; - - let index = 0; - let _indexCount = 0; - let nextTexture; - let currentTexture; - let groupCount = 0; - - let textureCount = 0; - let currentGroup = _drawCalls[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; - - for (i = 0; i < this._bufferSize; ++i) - { - // upload the sprite elements... - // they have all ready been calculated so we just need to push them into the buffer. - - 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, float32View, uint32View, indexBuffer, index, _indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, _indexCount); - - // push a graphics.. - index += (sprite.vertexData.length / 2) * vertSize; - _indexCount += sprite.indices.length; - } - - BaseTexture._globalBatch = TICK; - - currentGroup.size = _indexCount - currentGroup.start; - - if (!settings.CAN_UPLOAD_SAME_BUFFER) - { - // this is still needed for IOS performance.. - // it really does not like uploading to the same buffer in a single frame! - if (this._packedGeometryPoolSize <= this._flushId) - { - this._packedGeometryPoolSize++; - /* eslint-disable max-len */ - this._packedGeometries[this._flushId] = new (this.geometryClass)(); - } - - this._packedGeometries[this._flushId]._buffer.update(buffer.vertices, 0); - this._packedGeometries[this._flushId]._indexBuffer.update(indexBuffer, 0); - - // this.vertexBuffers[this._flushId].update(buffer.vertices, 0); - this.renderer.geometry.bind(this._packedGeometries[this._flushId]); - - this.renderer.geometry.updateBuffers(); - - this._flushId++; - } - else - { - // lets use the faster option, always use buffer number 0 - this._packedGeometries[this._flushId]._buffer.update(buffer.vertices, 0); - this._packedGeometries[this._flushId]._indexBuffer.update(indexBuffer, 0); - - // if (true)// this.spriteOnly) - // { - // this._packedGeometries[this._flushId].indexBuffer = this.defualtSpriteIndexBuffer; - // this._packedGeometries[this._flushId].buffers[1] = this.defualtSpriteIndexBuffer; - // } - - this.renderer.geometry.updateBuffers(); - } - - // this.renderer.state.set(this.state); - - const textureSystem = this.renderer.texture; - const stateSystem = this.renderer.state; - // e.log(groupCount); - // / render the _drawCalls.. - - 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; - } - - // this.state.blendMode = group.blend; - // this.state.blend = true; - - // this.renderer.state.setState(this.state); - // set the blend mode.. - 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 `BatchRenderer`. 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 {BatchBuffer} - 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 BatchBuffer(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 {FLoat32Array} float32View - float32-view of the attribute buffer - * @param {Uint32Array} uint32View - uint32-view of the 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, - float32View, uint32View, indexBuffer, aIndex, iIndex) - { - const p = 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++] = p + indicies[i]; - } - } -} diff --git a/packages/core/src/batch/BatchSystem.js b/packages/core/src/batch/BatchSystem.js index 2d1a8bf..5b75137 100644 --- a/packages/core/src/batch/BatchSystem.js +++ b/packages/core/src/batch/BatchSystem.js @@ -46,7 +46,6 @@ this.currentRenderer.stop(); this.currentRenderer = objectRenderer; - this.currentRenderer.start(); } diff --git a/packages/core/src/batch/ObjectRenderer.js b/packages/core/src/batch/ObjectRenderer.js index 0c851a9..2788895 100644 --- a/packages/core/src/batch/ObjectRenderer.js +++ b/packages/core/src/batch/ObjectRenderer.js @@ -1,5 +1,3 @@ -import System from '../System'; - /** * Base for a common object renderer that can be used as a system renderer plugin. * @@ -7,29 +5,24 @@ * @extends PIXI.System * @memberof PIXI */ -export default class ObjectRenderer extends System +export default class ObjectRenderer { /** - * Starts the renderer and sets the shader - * + * @param {PIXI.Renderer} renderer - The renderer this manager works for. */ - start() + constructor(renderer) { - // set the shader.. + /** + * The renderer this manager works for. + * + * @member {PIXI.Renderer} + */ + this.renderer = renderer; } /** - * Stops the renderer - * - */ - stop() - { - this.flush(); - } - - /** - * Stub method for rendering content and emptying the current batch. - * + * Stub method that should be used to empty the current + * batch by rendering objects now. */ flush() { @@ -37,7 +30,37 @@ } /** - * Renders an object + * Generic destruction method that frees all resources. This + * should be called by subclasses. + */ + destroy() + { + this.renderer = null; + } + + /** + * Stub method that initializes any state required before + * rendering starts. It is different from the `prerender` + * signal, which occurs every frame, in that it is called + * whenever an object requests _this_ renderer specifically. + */ + start() + { + // set the shader.. + } + + /** + * Stops the renderer. It should free up any state and + * become dormant. + */ + stop() + { + this.flush(); + } + + /** + * Keeps the object to render. It doesn't have to be + * rendered immediately. * * @param {PIXI.DisplayObject} object - The object to render. */ diff --git a/packages/core/src/batch/utils/builtinAttributeDefinitions.js b/packages/core/src/batch/utils/builtinAttributeDefinitions.js new file mode 100644 index 0000000..edbf531 --- /dev/null +++ b/packages/core/src/batch/utils/builtinAttributeDefinitions.js @@ -0,0 +1,18 @@ +import { TYPES } from '@pixi/constants'; + +export default { + aColor: { + type: 'uint32', + size: 1, + glType: TYPES.UNSIGNED_BYTE, + glSize: 4, + _wordSize: 1, + }, + aTextureId: { + type: 'float32', + size: 1, + glType: TYPES.FLOAT, + glSize: 1, + _wordSize: 1, + }, +}; diff --git a/packages/core/src/geometry/ViewableBuffer.js b/packages/core/src/geometry/ViewableBuffer.js new file mode 100644 index 0000000..793a425 --- /dev/null +++ b/packages/core/src/geometry/ViewableBuffer.js @@ -0,0 +1,168 @@ +/** + * Flexible wrapper around `ArrayBuffer` that also provides + * typed array views on demand. + * + * @class + * @memberof PIXI + */ +export default class ViewableBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + /** + * Underlying `ArrayBuffer` that holds all the data + * and is of capacity `size`. + * + * @member {ArrayBuffer} + */ + this.rawBinaryData = new ArrayBuffer(size); + } + + /** + * View on the raw binary data as a `Int8Array`. + * + * @member {Int8Array} + */ + get int8View() + { + if (!this._int8View) + { + this._int8View = new Int8Array(this.rawBinaryData); + } + + return this._int8View; + } + + /** + * View on the raw binary data as a `Uint8Array`. + * + * @member {Uint8Array} + */ + get uint8View() + { + if (!this._uint8View) + { + this._uint8View = new Uint8Array(this.rawBinaryData); + } + + return this._uint8View; + } + + /** + * View on the raw binary data as a `Int16Array`. + * + * @member {Int16Array} + */ + get int16View() + { + if (!this._int16View) + { + this._int16View = new Int16Array(this.rawBinaryData); + } + + return this._int16View; + } + + /** + * View on the raw binary data as a `Uint16Array`. + * + * @member {Uint16Array} + */ + get uint16View() + { + if (!this._uint16View) + { + this._uint16View = new Uint16Array(this.rawBinaryData); + } + + return this._uint16View; + } + + /** + * View on the raw binary data as a `Int32Array`. + * + * @member {Int32Array} + */ + get int32View() + { + if (!this._int32View) + { + this._int32View = new Int32Array(this.rawBinaryData); + } + + return this._int32View; + } + + /** + * View on the raw binary data as a `Uint32Array`. + * + * @member {Float32Array} + */ + get uint32View() + { + if (!this._uint32View) + { + this._uint32View = new Uint32Array(this.rawBinaryData); + } + + return this._uint32View; + } + + /** + * View on the raw binary data as a `Float32Array`. + * + * @member {Float32Array} + */ + get float32View() + { + if (!this._float32View) + { + this._float32View = new Float32Array(this.rawBinaryData); + } + + return this._float32View; + } + + view(type) + { + return this[`${type}View`]; + } + + /** + * Destroys all buffer references. Do not use after calling + * this. + */ + destroy() + { + this.rawBinaryData = null; + this._int8View = null; + this._uint8View = null; + this._int16View = null; + this._uint16View = null; + this._int32View = null; + this._uint32View = null; + this._float32View = null; + } + + static sizeOf(type) + { + switch (type) + { + case 'int8': + case 'uint8': + return 1; + case 'int16': + case 'uint16': + return 2; + case 'int32': + case 'uint32': + case 'float32': + return 4; + default: + throw new Error(`${type} isn't a valid view type`); + } + } +} diff --git a/packages/core/src/batch/AbstractBatchRenderer.js b/packages/core/src/batch/AbstractBatchRenderer.js new file mode 100644 index 0000000..7b4d061 --- /dev/null +++ b/packages/core/src/batch/AbstractBatchRenderer.js @@ -0,0 +1,783 @@ +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} 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; + } +} diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js deleted file mode 100644 index b80bc7e..0000000 --- a/packages/core/src/batch/BatchBuffer.js +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Used by the BatchRenderer - * - * @class - * @memberof PIXI - */ -export default class BatchBuffer -{ - /** - * @param {number} size - The size of the buffer in bytes. - */ - constructor(size) - { - this.vertices = new ArrayBuffer(size); - - /** - * View on the vertices as a Float32Array for positions - * - * @member {Float32Array} - */ - this.float32View = new Float32Array(this.vertices); - - /** - * View on the vertices as a Uint32Array for uvs - * - * @member {Float32Array} - */ - this.uint32View = new Uint32Array(this.vertices); - } - - /** - * Destroys the buffer. - * - */ - destroy() - { - this.vertices = null; - this.float32View = null; - this.uint32View = null; - } -} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js index 6275f8c..ad67273 100644 --- a/packages/core/src/batch/BatchGeometry.js +++ b/packages/core/src/batch/BatchGeometry.js @@ -1,9 +1,32 @@ -import { TYPES } from '@pixi/constants'; -import Geometry from '../geometry/Geometry'; import Buffer from '../geometry/Buffer'; +import builtinAttributeDefinitions from './utils/builtinAttributeDefinitions'; +import Geometry from '../geometry/Geometry'; +import { TYPES } from '@pixi/constants'; + +const defaultAttributes = [ + { + property: 'vertexData', + name: 'aVertexPosition', + type: 'float32', + size: 2, + glType: TYPES.FLOAT, + glSize: 2, + }, + { + property: 'uvs', + name: 'aTextureCoord', + type: 'float32', + size: 2, + glType: TYPES.FLOAT, + glSize: 2, + }, + 'aColor', // built-in attribute + 'aTextureId', +]; /** - * Geometry used to batch standard PIXI content (e.g. Mesh, Sprite, Graphics objects). + * Geometry used to batch standard PIXI content (e.g. Mesh, Sprite, + * Graphics objects). * * @class * @memberof PIXI @@ -13,8 +36,9 @@ /** * @param {boolean} [_static=false] Optimization flag, where `false` * is updated every frame, `true` doesn't change frame-to-frame. + * @param {Array} attributeDefinitions - attribute definitions */ - constructor(_static = false) + constructor(_static = false, attributeDefinitions = defaultAttributes) { super(); @@ -34,10 +58,25 @@ */ this._indexBuffer = new Buffer(null, _static, true); - this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) - .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) - .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) - .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) - .addIndex(this._indexBuffer); + /* These are automatically interleaved by GeometrySystem. */ + for (let i = 0; i < attributeDefinitions.length; i++) + { + const def = attributeDefinitions[i]; + + if (def === 'aColor') + { // special + this.addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE); + continue; + } + + const isBuiltin = (typeof def === 'string'); + const identifier = isBuiltin ? def : def.name; + const size = isBuiltin ? builtinAttributeDefinitions[identifier].glSize : def.glSize; + const type = isBuiltin ? builtinAttributeDefinitions[identifier].glType : def.glType; + + this.addAttribute(identifier, this._buffer, size, def === 'aTextureId', type); + } + + this.addIndex(this._indexBuffer); } } diff --git a/packages/core/src/batch/BatchPluginFactory.js b/packages/core/src/batch/BatchPluginFactory.js index ce5a14a..aad5e68 100644 --- a/packages/core/src/batch/BatchPluginFactory.js +++ b/packages/core/src/batch/BatchPluginFactory.js @@ -1,6 +1,10 @@ import BatchShaderGenerator from './BatchShaderGenerator'; import BatchGeometry from './BatchGeometry'; -import BaseBatchRenderer from './BatchRenderer'; +import AbstractBatchRenderer from './AbstractBatchRenderer'; +import ViewableBuffer from '../geometry/ViewableBuffer'; + +import { sizeOfType } from '@pixi/utils'; +import { TYPES } from '@pixi/constants'; import defaultVertex from './texture.vert'; import defaultFragment from './texture.frag'; @@ -35,6 +39,8 @@ * * @static * @param {object} [options] + * @param {object} [option.attributeDefinitions=Array] - + * Attribute definitions, see PIXI.AbstractBatchRenderer#attributeDefinitions * @param {string} [options.vertex=PIXI.BatchPluginFactory.defaultVertexSrc] - Vertex shader source * @param {string} [options.fragment=PIXI.BatchPluginFactory.defaultFragmentTemplate] - Fragment shader template * @param {number} [options.vertexSize=6] - Vertex size @@ -43,19 +49,48 @@ */ static create(options) { - const { vertex, fragment, vertexSize, geometryClass } = Object.assign({ + const { + attributeDefinitions, + fragment, + geometryClass, + vertex, + } = Object.assign({ + attributeDefinitions: [ + { + property: 'vertexData', + name: 'aVertexPosition', + type: 'float32', + size: 2, + glType: TYPES.FLOAT, + glSize: 2, + }, + { + property: 'uvs', + name: 'aTextureCoord', + type: 'float32', + size: 2, + glType: TYPES.FLOAT, + glSize: 2, + }, + 'aColor', // built-in attribute + 'aTextureId', + ], vertex: defaultVertex, fragment: defaultFragment, geometryClass: BatchGeometry, - vertexSize: 6, }, options); - return class BatchPlugin extends BaseBatchRenderer + BatchPluginFactory._checkAttributeDefinitionCompatibility(attributeDefinitions); + + const vertexSize = AbstractBatchRenderer.vertexSizeOf(attributeDefinitions); + + return class BatchPlugin extends AbstractBatchRenderer { constructor(renderer) { super(renderer); + this.attributeDefinitions = attributeDefinitions; this.shaderGenerator = new BatchShaderGenerator(vertex, fragment); this.geometryClass = geometryClass; this.vertexSize = vertexSize; @@ -86,6 +121,39 @@ { return defaultFragment; } + + static _checkAttributeDefinitionCompatibility(definitions) + { + definitions.forEach((def) => + { + if (typeof def === 'string') + { + return;// built-in attribute + } + + const inputSize = ViewableBuffer.sizeOf(def.type) * def.size; + + if (inputSize % 4 !== 0) + { + throw new Error('Batch rendering requires that your object ' + + 'attributes be of net size multiple of four. The attribute ' + + `${def.property}, a.k.a ${def.name}, has a source size of` + + `${inputSize}, which is not a multiple of 4. Consider padding` + + 'your elements with additional bytes.'); + } + + const outputSize = sizeOfType(def.glType) * def.glSize; + + if (outputSize !== inputSize) + { + throw new Error('Your object- and gl- types do not match in size.' + + 'The size of each attribute in the object property array is ' + + `${inputSize}, while the buffered size is ${outputSize} in bytes.`); + } + + def._wordSize = inputSize / 4; + }); + } } // Setup the default BatchRenderer plugin, this is what diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js deleted file mode 100644 index daf5dcf..0000000 --- a/packages/core/src/batch/BatchRenderer.js +++ /dev/null @@ -1,653 +0,0 @@ -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 BatchBuffer from './BatchBuffer'; -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 BatchRenderer 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; - - /** - * 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} 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` 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 `BatchBuffer` 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.BatchBuffer} - * @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)(); - } - } - - /** - * 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 and empties the current batch. - * - */ - flush() - { - if (this._vertexCount === 0) - { - return; - } - - const gl = this.renderer.gl; - const MAX_TEXTURES = this.MAX_TEXTURES; - const vertSize = this.vertexSize; - - const buffer = this.getAttributeBuffer(this._vertexCount); - const indexBuffer = this.getIndexBuffer(this._indexCount); - - const elements = this._bufferedElements; - const _drawCalls = this._drawCalls; - - const float32View = buffer.float32View; - const uint32View = buffer.uint32View; - - const touch = this.renderer.textureGC.count; - - let index = 0; - let _indexCount = 0; - let nextTexture; - let currentTexture; - let groupCount = 0; - - let textureCount = 0; - let currentGroup = _drawCalls[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; - - for (i = 0; i < this._bufferSize; ++i) - { - // upload the sprite elements... - // they have all ready been calculated so we just need to push them into the buffer. - - 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, float32View, uint32View, indexBuffer, index, _indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, _indexCount); - - // push a graphics.. - index += (sprite.vertexData.length / 2) * vertSize; - _indexCount += sprite.indices.length; - } - - BaseTexture._globalBatch = TICK; - - currentGroup.size = _indexCount - currentGroup.start; - - if (!settings.CAN_UPLOAD_SAME_BUFFER) - { - // this is still needed for IOS performance.. - // it really does not like uploading to the same buffer in a single frame! - if (this._packedGeometryPoolSize <= this._flushId) - { - this._packedGeometryPoolSize++; - /* eslint-disable max-len */ - this._packedGeometries[this._flushId] = new (this.geometryClass)(); - } - - this._packedGeometries[this._flushId]._buffer.update(buffer.vertices, 0); - this._packedGeometries[this._flushId]._indexBuffer.update(indexBuffer, 0); - - // this.vertexBuffers[this._flushId].update(buffer.vertices, 0); - this.renderer.geometry.bind(this._packedGeometries[this._flushId]); - - this.renderer.geometry.updateBuffers(); - - this._flushId++; - } - else - { - // lets use the faster option, always use buffer number 0 - this._packedGeometries[this._flushId]._buffer.update(buffer.vertices, 0); - this._packedGeometries[this._flushId]._indexBuffer.update(indexBuffer, 0); - - // if (true)// this.spriteOnly) - // { - // this._packedGeometries[this._flushId].indexBuffer = this.defualtSpriteIndexBuffer; - // this._packedGeometries[this._flushId].buffers[1] = this.defualtSpriteIndexBuffer; - // } - - this.renderer.geometry.updateBuffers(); - } - - // this.renderer.state.set(this.state); - - const textureSystem = this.renderer.texture; - const stateSystem = this.renderer.state; - // e.log(groupCount); - // / render the _drawCalls.. - - 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; - } - - // this.state.blendMode = group.blend; - // this.state.blend = true; - - // this.renderer.state.setState(this.state); - // set the blend mode.. - 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 `BatchRenderer`. 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 {BatchBuffer} - 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 BatchBuffer(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 {FLoat32Array} float32View - float32-view of the attribute buffer - * @param {Uint32Array} uint32View - uint32-view of the 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, - float32View, uint32View, indexBuffer, aIndex, iIndex) - { - const p = 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++] = p + indicies[i]; - } - } -} diff --git a/packages/core/src/batch/BatchSystem.js b/packages/core/src/batch/BatchSystem.js index 2d1a8bf..5b75137 100644 --- a/packages/core/src/batch/BatchSystem.js +++ b/packages/core/src/batch/BatchSystem.js @@ -46,7 +46,6 @@ this.currentRenderer.stop(); this.currentRenderer = objectRenderer; - this.currentRenderer.start(); } diff --git a/packages/core/src/batch/ObjectRenderer.js b/packages/core/src/batch/ObjectRenderer.js index 0c851a9..2788895 100644 --- a/packages/core/src/batch/ObjectRenderer.js +++ b/packages/core/src/batch/ObjectRenderer.js @@ -1,5 +1,3 @@ -import System from '../System'; - /** * Base for a common object renderer that can be used as a system renderer plugin. * @@ -7,29 +5,24 @@ * @extends PIXI.System * @memberof PIXI */ -export default class ObjectRenderer extends System +export default class ObjectRenderer { /** - * Starts the renderer and sets the shader - * + * @param {PIXI.Renderer} renderer - The renderer this manager works for. */ - start() + constructor(renderer) { - // set the shader.. + /** + * The renderer this manager works for. + * + * @member {PIXI.Renderer} + */ + this.renderer = renderer; } /** - * Stops the renderer - * - */ - stop() - { - this.flush(); - } - - /** - * Stub method for rendering content and emptying the current batch. - * + * Stub method that should be used to empty the current + * batch by rendering objects now. */ flush() { @@ -37,7 +30,37 @@ } /** - * Renders an object + * Generic destruction method that frees all resources. This + * should be called by subclasses. + */ + destroy() + { + this.renderer = null; + } + + /** + * Stub method that initializes any state required before + * rendering starts. It is different from the `prerender` + * signal, which occurs every frame, in that it is called + * whenever an object requests _this_ renderer specifically. + */ + start() + { + // set the shader.. + } + + /** + * Stops the renderer. It should free up any state and + * become dormant. + */ + stop() + { + this.flush(); + } + + /** + * Keeps the object to render. It doesn't have to be + * rendered immediately. * * @param {PIXI.DisplayObject} object - The object to render. */ diff --git a/packages/core/src/batch/utils/builtinAttributeDefinitions.js b/packages/core/src/batch/utils/builtinAttributeDefinitions.js new file mode 100644 index 0000000..edbf531 --- /dev/null +++ b/packages/core/src/batch/utils/builtinAttributeDefinitions.js @@ -0,0 +1,18 @@ +import { TYPES } from '@pixi/constants'; + +export default { + aColor: { + type: 'uint32', + size: 1, + glType: TYPES.UNSIGNED_BYTE, + glSize: 4, + _wordSize: 1, + }, + aTextureId: { + type: 'float32', + size: 1, + glType: TYPES.FLOAT, + glSize: 1, + _wordSize: 1, + }, +}; diff --git a/packages/core/src/geometry/ViewableBuffer.js b/packages/core/src/geometry/ViewableBuffer.js new file mode 100644 index 0000000..793a425 --- /dev/null +++ b/packages/core/src/geometry/ViewableBuffer.js @@ -0,0 +1,168 @@ +/** + * Flexible wrapper around `ArrayBuffer` that also provides + * typed array views on demand. + * + * @class + * @memberof PIXI + */ +export default class ViewableBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + /** + * Underlying `ArrayBuffer` that holds all the data + * and is of capacity `size`. + * + * @member {ArrayBuffer} + */ + this.rawBinaryData = new ArrayBuffer(size); + } + + /** + * View on the raw binary data as a `Int8Array`. + * + * @member {Int8Array} + */ + get int8View() + { + if (!this._int8View) + { + this._int8View = new Int8Array(this.rawBinaryData); + } + + return this._int8View; + } + + /** + * View on the raw binary data as a `Uint8Array`. + * + * @member {Uint8Array} + */ + get uint8View() + { + if (!this._uint8View) + { + this._uint8View = new Uint8Array(this.rawBinaryData); + } + + return this._uint8View; + } + + /** + * View on the raw binary data as a `Int16Array`. + * + * @member {Int16Array} + */ + get int16View() + { + if (!this._int16View) + { + this._int16View = new Int16Array(this.rawBinaryData); + } + + return this._int16View; + } + + /** + * View on the raw binary data as a `Uint16Array`. + * + * @member {Uint16Array} + */ + get uint16View() + { + if (!this._uint16View) + { + this._uint16View = new Uint16Array(this.rawBinaryData); + } + + return this._uint16View; + } + + /** + * View on the raw binary data as a `Int32Array`. + * + * @member {Int32Array} + */ + get int32View() + { + if (!this._int32View) + { + this._int32View = new Int32Array(this.rawBinaryData); + } + + return this._int32View; + } + + /** + * View on the raw binary data as a `Uint32Array`. + * + * @member {Float32Array} + */ + get uint32View() + { + if (!this._uint32View) + { + this._uint32View = new Uint32Array(this.rawBinaryData); + } + + return this._uint32View; + } + + /** + * View on the raw binary data as a `Float32Array`. + * + * @member {Float32Array} + */ + get float32View() + { + if (!this._float32View) + { + this._float32View = new Float32Array(this.rawBinaryData); + } + + return this._float32View; + } + + view(type) + { + return this[`${type}View`]; + } + + /** + * Destroys all buffer references. Do not use after calling + * this. + */ + destroy() + { + this.rawBinaryData = null; + this._int8View = null; + this._uint8View = null; + this._int16View = null; + this._uint16View = null; + this._int32View = null; + this._uint32View = null; + this._float32View = null; + } + + static sizeOf(type) + { + switch (type) + { + case 'int8': + case 'uint8': + return 1; + case 'int16': + case 'uint16': + return 2; + case 'int32': + case 'uint32': + case 'float32': + return 4; + default: + throw new Error(`${type} isn't a valid view type`); + } + } +} diff --git a/packages/core/src/index.js b/packages/core/src/index.js index d4cb1fa..08ba1c1 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -38,3 +38,4 @@ export { default as Attribute } from './geometry/Attribute'; export { default as Buffer } from './geometry/Buffer'; export { default as Geometry } from './geometry/Geometry'; +export { default as ViewableBuffer } from './geometry/ViewableBuffer'; diff --git a/packages/core/src/batch/AbstractBatchRenderer.js b/packages/core/src/batch/AbstractBatchRenderer.js new file mode 100644 index 0000000..7b4d061 --- /dev/null +++ b/packages/core/src/batch/AbstractBatchRenderer.js @@ -0,0 +1,783 @@ +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} 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; + } +} diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js deleted file mode 100644 index b80bc7e..0000000 --- a/packages/core/src/batch/BatchBuffer.js +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Used by the BatchRenderer - * - * @class - * @memberof PIXI - */ -export default class BatchBuffer -{ - /** - * @param {number} size - The size of the buffer in bytes. - */ - constructor(size) - { - this.vertices = new ArrayBuffer(size); - - /** - * View on the vertices as a Float32Array for positions - * - * @member {Float32Array} - */ - this.float32View = new Float32Array(this.vertices); - - /** - * View on the vertices as a Uint32Array for uvs - * - * @member {Float32Array} - */ - this.uint32View = new Uint32Array(this.vertices); - } - - /** - * Destroys the buffer. - * - */ - destroy() - { - this.vertices = null; - this.float32View = null; - this.uint32View = null; - } -} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js index 6275f8c..ad67273 100644 --- a/packages/core/src/batch/BatchGeometry.js +++ b/packages/core/src/batch/BatchGeometry.js @@ -1,9 +1,32 @@ -import { TYPES } from '@pixi/constants'; -import Geometry from '../geometry/Geometry'; import Buffer from '../geometry/Buffer'; +import builtinAttributeDefinitions from './utils/builtinAttributeDefinitions'; +import Geometry from '../geometry/Geometry'; +import { TYPES } from '@pixi/constants'; + +const defaultAttributes = [ + { + property: 'vertexData', + name: 'aVertexPosition', + type: 'float32', + size: 2, + glType: TYPES.FLOAT, + glSize: 2, + }, + { + property: 'uvs', + name: 'aTextureCoord', + type: 'float32', + size: 2, + glType: TYPES.FLOAT, + glSize: 2, + }, + 'aColor', // built-in attribute + 'aTextureId', +]; /** - * Geometry used to batch standard PIXI content (e.g. Mesh, Sprite, Graphics objects). + * Geometry used to batch standard PIXI content (e.g. Mesh, Sprite, + * Graphics objects). * * @class * @memberof PIXI @@ -13,8 +36,9 @@ /** * @param {boolean} [_static=false] Optimization flag, where `false` * is updated every frame, `true` doesn't change frame-to-frame. + * @param {Array} attributeDefinitions - attribute definitions */ - constructor(_static = false) + constructor(_static = false, attributeDefinitions = defaultAttributes) { super(); @@ -34,10 +58,25 @@ */ this._indexBuffer = new Buffer(null, _static, true); - this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) - .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) - .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) - .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) - .addIndex(this._indexBuffer); + /* These are automatically interleaved by GeometrySystem. */ + for (let i = 0; i < attributeDefinitions.length; i++) + { + const def = attributeDefinitions[i]; + + if (def === 'aColor') + { // special + this.addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE); + continue; + } + + const isBuiltin = (typeof def === 'string'); + const identifier = isBuiltin ? def : def.name; + const size = isBuiltin ? builtinAttributeDefinitions[identifier].glSize : def.glSize; + const type = isBuiltin ? builtinAttributeDefinitions[identifier].glType : def.glType; + + this.addAttribute(identifier, this._buffer, size, def === 'aTextureId', type); + } + + this.addIndex(this._indexBuffer); } } diff --git a/packages/core/src/batch/BatchPluginFactory.js b/packages/core/src/batch/BatchPluginFactory.js index ce5a14a..aad5e68 100644 --- a/packages/core/src/batch/BatchPluginFactory.js +++ b/packages/core/src/batch/BatchPluginFactory.js @@ -1,6 +1,10 @@ import BatchShaderGenerator from './BatchShaderGenerator'; import BatchGeometry from './BatchGeometry'; -import BaseBatchRenderer from './BatchRenderer'; +import AbstractBatchRenderer from './AbstractBatchRenderer'; +import ViewableBuffer from '../geometry/ViewableBuffer'; + +import { sizeOfType } from '@pixi/utils'; +import { TYPES } from '@pixi/constants'; import defaultVertex from './texture.vert'; import defaultFragment from './texture.frag'; @@ -35,6 +39,8 @@ * * @static * @param {object} [options] + * @param {object} [option.attributeDefinitions=Array] - + * Attribute definitions, see PIXI.AbstractBatchRenderer#attributeDefinitions * @param {string} [options.vertex=PIXI.BatchPluginFactory.defaultVertexSrc] - Vertex shader source * @param {string} [options.fragment=PIXI.BatchPluginFactory.defaultFragmentTemplate] - Fragment shader template * @param {number} [options.vertexSize=6] - Vertex size @@ -43,19 +49,48 @@ */ static create(options) { - const { vertex, fragment, vertexSize, geometryClass } = Object.assign({ + const { + attributeDefinitions, + fragment, + geometryClass, + vertex, + } = Object.assign({ + attributeDefinitions: [ + { + property: 'vertexData', + name: 'aVertexPosition', + type: 'float32', + size: 2, + glType: TYPES.FLOAT, + glSize: 2, + }, + { + property: 'uvs', + name: 'aTextureCoord', + type: 'float32', + size: 2, + glType: TYPES.FLOAT, + glSize: 2, + }, + 'aColor', // built-in attribute + 'aTextureId', + ], vertex: defaultVertex, fragment: defaultFragment, geometryClass: BatchGeometry, - vertexSize: 6, }, options); - return class BatchPlugin extends BaseBatchRenderer + BatchPluginFactory._checkAttributeDefinitionCompatibility(attributeDefinitions); + + const vertexSize = AbstractBatchRenderer.vertexSizeOf(attributeDefinitions); + + return class BatchPlugin extends AbstractBatchRenderer { constructor(renderer) { super(renderer); + this.attributeDefinitions = attributeDefinitions; this.shaderGenerator = new BatchShaderGenerator(vertex, fragment); this.geometryClass = geometryClass; this.vertexSize = vertexSize; @@ -86,6 +121,39 @@ { return defaultFragment; } + + static _checkAttributeDefinitionCompatibility(definitions) + { + definitions.forEach((def) => + { + if (typeof def === 'string') + { + return;// built-in attribute + } + + const inputSize = ViewableBuffer.sizeOf(def.type) * def.size; + + if (inputSize % 4 !== 0) + { + throw new Error('Batch rendering requires that your object ' + + 'attributes be of net size multiple of four. The attribute ' + + `${def.property}, a.k.a ${def.name}, has a source size of` + + `${inputSize}, which is not a multiple of 4. Consider padding` + + 'your elements with additional bytes.'); + } + + const outputSize = sizeOfType(def.glType) * def.glSize; + + if (outputSize !== inputSize) + { + throw new Error('Your object- and gl- types do not match in size.' + + 'The size of each attribute in the object property array is ' + + `${inputSize}, while the buffered size is ${outputSize} in bytes.`); + } + + def._wordSize = inputSize / 4; + }); + } } // Setup the default BatchRenderer plugin, this is what diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js deleted file mode 100644 index daf5dcf..0000000 --- a/packages/core/src/batch/BatchRenderer.js +++ /dev/null @@ -1,653 +0,0 @@ -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 BatchBuffer from './BatchBuffer'; -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 BatchRenderer 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; - - /** - * 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} 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` 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 `BatchBuffer` 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.BatchBuffer} - * @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)(); - } - } - - /** - * 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 and empties the current batch. - * - */ - flush() - { - if (this._vertexCount === 0) - { - return; - } - - const gl = this.renderer.gl; - const MAX_TEXTURES = this.MAX_TEXTURES; - const vertSize = this.vertexSize; - - const buffer = this.getAttributeBuffer(this._vertexCount); - const indexBuffer = this.getIndexBuffer(this._indexCount); - - const elements = this._bufferedElements; - const _drawCalls = this._drawCalls; - - const float32View = buffer.float32View; - const uint32View = buffer.uint32View; - - const touch = this.renderer.textureGC.count; - - let index = 0; - let _indexCount = 0; - let nextTexture; - let currentTexture; - let groupCount = 0; - - let textureCount = 0; - let currentGroup = _drawCalls[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; - - for (i = 0; i < this._bufferSize; ++i) - { - // upload the sprite elements... - // they have all ready been calculated so we just need to push them into the buffer. - - 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, float32View, uint32View, indexBuffer, index, _indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, _indexCount); - - // push a graphics.. - index += (sprite.vertexData.length / 2) * vertSize; - _indexCount += sprite.indices.length; - } - - BaseTexture._globalBatch = TICK; - - currentGroup.size = _indexCount - currentGroup.start; - - if (!settings.CAN_UPLOAD_SAME_BUFFER) - { - // this is still needed for IOS performance.. - // it really does not like uploading to the same buffer in a single frame! - if (this._packedGeometryPoolSize <= this._flushId) - { - this._packedGeometryPoolSize++; - /* eslint-disable max-len */ - this._packedGeometries[this._flushId] = new (this.geometryClass)(); - } - - this._packedGeometries[this._flushId]._buffer.update(buffer.vertices, 0); - this._packedGeometries[this._flushId]._indexBuffer.update(indexBuffer, 0); - - // this.vertexBuffers[this._flushId].update(buffer.vertices, 0); - this.renderer.geometry.bind(this._packedGeometries[this._flushId]); - - this.renderer.geometry.updateBuffers(); - - this._flushId++; - } - else - { - // lets use the faster option, always use buffer number 0 - this._packedGeometries[this._flushId]._buffer.update(buffer.vertices, 0); - this._packedGeometries[this._flushId]._indexBuffer.update(indexBuffer, 0); - - // if (true)// this.spriteOnly) - // { - // this._packedGeometries[this._flushId].indexBuffer = this.defualtSpriteIndexBuffer; - // this._packedGeometries[this._flushId].buffers[1] = this.defualtSpriteIndexBuffer; - // } - - this.renderer.geometry.updateBuffers(); - } - - // this.renderer.state.set(this.state); - - const textureSystem = this.renderer.texture; - const stateSystem = this.renderer.state; - // e.log(groupCount); - // / render the _drawCalls.. - - 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; - } - - // this.state.blendMode = group.blend; - // this.state.blend = true; - - // this.renderer.state.setState(this.state); - // set the blend mode.. - 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 `BatchRenderer`. 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 {BatchBuffer} - 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 BatchBuffer(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 {FLoat32Array} float32View - float32-view of the attribute buffer - * @param {Uint32Array} uint32View - uint32-view of the 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, - float32View, uint32View, indexBuffer, aIndex, iIndex) - { - const p = 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++] = p + indicies[i]; - } - } -} diff --git a/packages/core/src/batch/BatchSystem.js b/packages/core/src/batch/BatchSystem.js index 2d1a8bf..5b75137 100644 --- a/packages/core/src/batch/BatchSystem.js +++ b/packages/core/src/batch/BatchSystem.js @@ -46,7 +46,6 @@ this.currentRenderer.stop(); this.currentRenderer = objectRenderer; - this.currentRenderer.start(); } diff --git a/packages/core/src/batch/ObjectRenderer.js b/packages/core/src/batch/ObjectRenderer.js index 0c851a9..2788895 100644 --- a/packages/core/src/batch/ObjectRenderer.js +++ b/packages/core/src/batch/ObjectRenderer.js @@ -1,5 +1,3 @@ -import System from '../System'; - /** * Base for a common object renderer that can be used as a system renderer plugin. * @@ -7,29 +5,24 @@ * @extends PIXI.System * @memberof PIXI */ -export default class ObjectRenderer extends System +export default class ObjectRenderer { /** - * Starts the renderer and sets the shader - * + * @param {PIXI.Renderer} renderer - The renderer this manager works for. */ - start() + constructor(renderer) { - // set the shader.. + /** + * The renderer this manager works for. + * + * @member {PIXI.Renderer} + */ + this.renderer = renderer; } /** - * Stops the renderer - * - */ - stop() - { - this.flush(); - } - - /** - * Stub method for rendering content and emptying the current batch. - * + * Stub method that should be used to empty the current + * batch by rendering objects now. */ flush() { @@ -37,7 +30,37 @@ } /** - * Renders an object + * Generic destruction method that frees all resources. This + * should be called by subclasses. + */ + destroy() + { + this.renderer = null; + } + + /** + * Stub method that initializes any state required before + * rendering starts. It is different from the `prerender` + * signal, which occurs every frame, in that it is called + * whenever an object requests _this_ renderer specifically. + */ + start() + { + // set the shader.. + } + + /** + * Stops the renderer. It should free up any state and + * become dormant. + */ + stop() + { + this.flush(); + } + + /** + * Keeps the object to render. It doesn't have to be + * rendered immediately. * * @param {PIXI.DisplayObject} object - The object to render. */ diff --git a/packages/core/src/batch/utils/builtinAttributeDefinitions.js b/packages/core/src/batch/utils/builtinAttributeDefinitions.js new file mode 100644 index 0000000..edbf531 --- /dev/null +++ b/packages/core/src/batch/utils/builtinAttributeDefinitions.js @@ -0,0 +1,18 @@ +import { TYPES } from '@pixi/constants'; + +export default { + aColor: { + type: 'uint32', + size: 1, + glType: TYPES.UNSIGNED_BYTE, + glSize: 4, + _wordSize: 1, + }, + aTextureId: { + type: 'float32', + size: 1, + glType: TYPES.FLOAT, + glSize: 1, + _wordSize: 1, + }, +}; diff --git a/packages/core/src/geometry/ViewableBuffer.js b/packages/core/src/geometry/ViewableBuffer.js new file mode 100644 index 0000000..793a425 --- /dev/null +++ b/packages/core/src/geometry/ViewableBuffer.js @@ -0,0 +1,168 @@ +/** + * Flexible wrapper around `ArrayBuffer` that also provides + * typed array views on demand. + * + * @class + * @memberof PIXI + */ +export default class ViewableBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + /** + * Underlying `ArrayBuffer` that holds all the data + * and is of capacity `size`. + * + * @member {ArrayBuffer} + */ + this.rawBinaryData = new ArrayBuffer(size); + } + + /** + * View on the raw binary data as a `Int8Array`. + * + * @member {Int8Array} + */ + get int8View() + { + if (!this._int8View) + { + this._int8View = new Int8Array(this.rawBinaryData); + } + + return this._int8View; + } + + /** + * View on the raw binary data as a `Uint8Array`. + * + * @member {Uint8Array} + */ + get uint8View() + { + if (!this._uint8View) + { + this._uint8View = new Uint8Array(this.rawBinaryData); + } + + return this._uint8View; + } + + /** + * View on the raw binary data as a `Int16Array`. + * + * @member {Int16Array} + */ + get int16View() + { + if (!this._int16View) + { + this._int16View = new Int16Array(this.rawBinaryData); + } + + return this._int16View; + } + + /** + * View on the raw binary data as a `Uint16Array`. + * + * @member {Uint16Array} + */ + get uint16View() + { + if (!this._uint16View) + { + this._uint16View = new Uint16Array(this.rawBinaryData); + } + + return this._uint16View; + } + + /** + * View on the raw binary data as a `Int32Array`. + * + * @member {Int32Array} + */ + get int32View() + { + if (!this._int32View) + { + this._int32View = new Int32Array(this.rawBinaryData); + } + + return this._int32View; + } + + /** + * View on the raw binary data as a `Uint32Array`. + * + * @member {Float32Array} + */ + get uint32View() + { + if (!this._uint32View) + { + this._uint32View = new Uint32Array(this.rawBinaryData); + } + + return this._uint32View; + } + + /** + * View on the raw binary data as a `Float32Array`. + * + * @member {Float32Array} + */ + get float32View() + { + if (!this._float32View) + { + this._float32View = new Float32Array(this.rawBinaryData); + } + + return this._float32View; + } + + view(type) + { + return this[`${type}View`]; + } + + /** + * Destroys all buffer references. Do not use after calling + * this. + */ + destroy() + { + this.rawBinaryData = null; + this._int8View = null; + this._uint8View = null; + this._int16View = null; + this._uint16View = null; + this._int32View = null; + this._uint32View = null; + this._float32View = null; + } + + static sizeOf(type) + { + switch (type) + { + case 'int8': + case 'uint8': + return 1; + case 'int16': + case 'uint16': + return 2; + case 'int32': + case 'uint32': + case 'float32': + return 4; + default: + throw new Error(`${type} isn't a valid view type`); + } + } +} diff --git a/packages/core/src/index.js b/packages/core/src/index.js index d4cb1fa..08ba1c1 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -38,3 +38,4 @@ export { default as Attribute } from './geometry/Attribute'; export { default as Buffer } from './geometry/Buffer'; export { default as Geometry } from './geometry/Geometry'; +export { default as ViewableBuffer } from './geometry/ViewableBuffer'; diff --git a/packages/graphics/src/Graphics.js b/packages/graphics/src/Graphics.js index e807104..6299d32 100644 --- a/packages/graphics/src/Graphics.js +++ b/packages/graphics/src/Graphics.js @@ -158,6 +158,16 @@ this._transformID = -1; this.batchDirty = -1; + /** + * Plugin that is responsible for rendering this element. + * Allows to customize the rendering process without overriding the + * '_render' & '_renderCanvas' methods. + * + * @member {string} + * @default 'batch' + */ + this.pluginName = 'batch'; + // Set default this.tint = 0xFFFFFF; this.blendMode = BLEND_MODES.NORMAL; @@ -864,7 +874,7 @@ } } - renderer.batch.setObjectRenderer(renderer.plugins.batch); + renderer.batch.setObjectRenderer(renderer.plugins[this.pluginName]); if (this.batches.length) { @@ -877,7 +887,7 @@ batch.worldAlpha = this.worldAlpha * batch.alpha; - renderer.plugins.batch.render(batch); + renderer.plugins[this.pluginName].render(batch); } } } diff --git a/packages/core/src/batch/AbstractBatchRenderer.js b/packages/core/src/batch/AbstractBatchRenderer.js new file mode 100644 index 0000000..7b4d061 --- /dev/null +++ b/packages/core/src/batch/AbstractBatchRenderer.js @@ -0,0 +1,783 @@ +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} 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; + } +} diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js deleted file mode 100644 index b80bc7e..0000000 --- a/packages/core/src/batch/BatchBuffer.js +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Used by the BatchRenderer - * - * @class - * @memberof PIXI - */ -export default class BatchBuffer -{ - /** - * @param {number} size - The size of the buffer in bytes. - */ - constructor(size) - { - this.vertices = new ArrayBuffer(size); - - /** - * View on the vertices as a Float32Array for positions - * - * @member {Float32Array} - */ - this.float32View = new Float32Array(this.vertices); - - /** - * View on the vertices as a Uint32Array for uvs - * - * @member {Float32Array} - */ - this.uint32View = new Uint32Array(this.vertices); - } - - /** - * Destroys the buffer. - * - */ - destroy() - { - this.vertices = null; - this.float32View = null; - this.uint32View = null; - } -} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js index 6275f8c..ad67273 100644 --- a/packages/core/src/batch/BatchGeometry.js +++ b/packages/core/src/batch/BatchGeometry.js @@ -1,9 +1,32 @@ -import { TYPES } from '@pixi/constants'; -import Geometry from '../geometry/Geometry'; import Buffer from '../geometry/Buffer'; +import builtinAttributeDefinitions from './utils/builtinAttributeDefinitions'; +import Geometry from '../geometry/Geometry'; +import { TYPES } from '@pixi/constants'; + +const defaultAttributes = [ + { + property: 'vertexData', + name: 'aVertexPosition', + type: 'float32', + size: 2, + glType: TYPES.FLOAT, + glSize: 2, + }, + { + property: 'uvs', + name: 'aTextureCoord', + type: 'float32', + size: 2, + glType: TYPES.FLOAT, + glSize: 2, + }, + 'aColor', // built-in attribute + 'aTextureId', +]; /** - * Geometry used to batch standard PIXI content (e.g. Mesh, Sprite, Graphics objects). + * Geometry used to batch standard PIXI content (e.g. Mesh, Sprite, + * Graphics objects). * * @class * @memberof PIXI @@ -13,8 +36,9 @@ /** * @param {boolean} [_static=false] Optimization flag, where `false` * is updated every frame, `true` doesn't change frame-to-frame. + * @param {Array} attributeDefinitions - attribute definitions */ - constructor(_static = false) + constructor(_static = false, attributeDefinitions = defaultAttributes) { super(); @@ -34,10 +58,25 @@ */ this._indexBuffer = new Buffer(null, _static, true); - this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) - .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) - .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) - .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) - .addIndex(this._indexBuffer); + /* These are automatically interleaved by GeometrySystem. */ + for (let i = 0; i < attributeDefinitions.length; i++) + { + const def = attributeDefinitions[i]; + + if (def === 'aColor') + { // special + this.addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE); + continue; + } + + const isBuiltin = (typeof def === 'string'); + const identifier = isBuiltin ? def : def.name; + const size = isBuiltin ? builtinAttributeDefinitions[identifier].glSize : def.glSize; + const type = isBuiltin ? builtinAttributeDefinitions[identifier].glType : def.glType; + + this.addAttribute(identifier, this._buffer, size, def === 'aTextureId', type); + } + + this.addIndex(this._indexBuffer); } } diff --git a/packages/core/src/batch/BatchPluginFactory.js b/packages/core/src/batch/BatchPluginFactory.js index ce5a14a..aad5e68 100644 --- a/packages/core/src/batch/BatchPluginFactory.js +++ b/packages/core/src/batch/BatchPluginFactory.js @@ -1,6 +1,10 @@ import BatchShaderGenerator from './BatchShaderGenerator'; import BatchGeometry from './BatchGeometry'; -import BaseBatchRenderer from './BatchRenderer'; +import AbstractBatchRenderer from './AbstractBatchRenderer'; +import ViewableBuffer from '../geometry/ViewableBuffer'; + +import { sizeOfType } from '@pixi/utils'; +import { TYPES } from '@pixi/constants'; import defaultVertex from './texture.vert'; import defaultFragment from './texture.frag'; @@ -35,6 +39,8 @@ * * @static * @param {object} [options] + * @param {object} [option.attributeDefinitions=Array] - + * Attribute definitions, see PIXI.AbstractBatchRenderer#attributeDefinitions * @param {string} [options.vertex=PIXI.BatchPluginFactory.defaultVertexSrc] - Vertex shader source * @param {string} [options.fragment=PIXI.BatchPluginFactory.defaultFragmentTemplate] - Fragment shader template * @param {number} [options.vertexSize=6] - Vertex size @@ -43,19 +49,48 @@ */ static create(options) { - const { vertex, fragment, vertexSize, geometryClass } = Object.assign({ + const { + attributeDefinitions, + fragment, + geometryClass, + vertex, + } = Object.assign({ + attributeDefinitions: [ + { + property: 'vertexData', + name: 'aVertexPosition', + type: 'float32', + size: 2, + glType: TYPES.FLOAT, + glSize: 2, + }, + { + property: 'uvs', + name: 'aTextureCoord', + type: 'float32', + size: 2, + glType: TYPES.FLOAT, + glSize: 2, + }, + 'aColor', // built-in attribute + 'aTextureId', + ], vertex: defaultVertex, fragment: defaultFragment, geometryClass: BatchGeometry, - vertexSize: 6, }, options); - return class BatchPlugin extends BaseBatchRenderer + BatchPluginFactory._checkAttributeDefinitionCompatibility(attributeDefinitions); + + const vertexSize = AbstractBatchRenderer.vertexSizeOf(attributeDefinitions); + + return class BatchPlugin extends AbstractBatchRenderer { constructor(renderer) { super(renderer); + this.attributeDefinitions = attributeDefinitions; this.shaderGenerator = new BatchShaderGenerator(vertex, fragment); this.geometryClass = geometryClass; this.vertexSize = vertexSize; @@ -86,6 +121,39 @@ { return defaultFragment; } + + static _checkAttributeDefinitionCompatibility(definitions) + { + definitions.forEach((def) => + { + if (typeof def === 'string') + { + return;// built-in attribute + } + + const inputSize = ViewableBuffer.sizeOf(def.type) * def.size; + + if (inputSize % 4 !== 0) + { + throw new Error('Batch rendering requires that your object ' + + 'attributes be of net size multiple of four. The attribute ' + + `${def.property}, a.k.a ${def.name}, has a source size of` + + `${inputSize}, which is not a multiple of 4. Consider padding` + + 'your elements with additional bytes.'); + } + + const outputSize = sizeOfType(def.glType) * def.glSize; + + if (outputSize !== inputSize) + { + throw new Error('Your object- and gl- types do not match in size.' + + 'The size of each attribute in the object property array is ' + + `${inputSize}, while the buffered size is ${outputSize} in bytes.`); + } + + def._wordSize = inputSize / 4; + }); + } } // Setup the default BatchRenderer plugin, this is what diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js deleted file mode 100644 index daf5dcf..0000000 --- a/packages/core/src/batch/BatchRenderer.js +++ /dev/null @@ -1,653 +0,0 @@ -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 BatchBuffer from './BatchBuffer'; -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 BatchRenderer 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; - - /** - * 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} 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` 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 `BatchBuffer` 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.BatchBuffer} - * @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)(); - } - } - - /** - * 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 and empties the current batch. - * - */ - flush() - { - if (this._vertexCount === 0) - { - return; - } - - const gl = this.renderer.gl; - const MAX_TEXTURES = this.MAX_TEXTURES; - const vertSize = this.vertexSize; - - const buffer = this.getAttributeBuffer(this._vertexCount); - const indexBuffer = this.getIndexBuffer(this._indexCount); - - const elements = this._bufferedElements; - const _drawCalls = this._drawCalls; - - const float32View = buffer.float32View; - const uint32View = buffer.uint32View; - - const touch = this.renderer.textureGC.count; - - let index = 0; - let _indexCount = 0; - let nextTexture; - let currentTexture; - let groupCount = 0; - - let textureCount = 0; - let currentGroup = _drawCalls[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; - - for (i = 0; i < this._bufferSize; ++i) - { - // upload the sprite elements... - // they have all ready been calculated so we just need to push them into the buffer. - - 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, float32View, uint32View, indexBuffer, index, _indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, _indexCount); - - // push a graphics.. - index += (sprite.vertexData.length / 2) * vertSize; - _indexCount += sprite.indices.length; - } - - BaseTexture._globalBatch = TICK; - - currentGroup.size = _indexCount - currentGroup.start; - - if (!settings.CAN_UPLOAD_SAME_BUFFER) - { - // this is still needed for IOS performance.. - // it really does not like uploading to the same buffer in a single frame! - if (this._packedGeometryPoolSize <= this._flushId) - { - this._packedGeometryPoolSize++; - /* eslint-disable max-len */ - this._packedGeometries[this._flushId] = new (this.geometryClass)(); - } - - this._packedGeometries[this._flushId]._buffer.update(buffer.vertices, 0); - this._packedGeometries[this._flushId]._indexBuffer.update(indexBuffer, 0); - - // this.vertexBuffers[this._flushId].update(buffer.vertices, 0); - this.renderer.geometry.bind(this._packedGeometries[this._flushId]); - - this.renderer.geometry.updateBuffers(); - - this._flushId++; - } - else - { - // lets use the faster option, always use buffer number 0 - this._packedGeometries[this._flushId]._buffer.update(buffer.vertices, 0); - this._packedGeometries[this._flushId]._indexBuffer.update(indexBuffer, 0); - - // if (true)// this.spriteOnly) - // { - // this._packedGeometries[this._flushId].indexBuffer = this.defualtSpriteIndexBuffer; - // this._packedGeometries[this._flushId].buffers[1] = this.defualtSpriteIndexBuffer; - // } - - this.renderer.geometry.updateBuffers(); - } - - // this.renderer.state.set(this.state); - - const textureSystem = this.renderer.texture; - const stateSystem = this.renderer.state; - // e.log(groupCount); - // / render the _drawCalls.. - - 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; - } - - // this.state.blendMode = group.blend; - // this.state.blend = true; - - // this.renderer.state.setState(this.state); - // set the blend mode.. - 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 `BatchRenderer`. 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 {BatchBuffer} - 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 BatchBuffer(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 {FLoat32Array} float32View - float32-view of the attribute buffer - * @param {Uint32Array} uint32View - uint32-view of the 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, - float32View, uint32View, indexBuffer, aIndex, iIndex) - { - const p = 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++] = p + indicies[i]; - } - } -} diff --git a/packages/core/src/batch/BatchSystem.js b/packages/core/src/batch/BatchSystem.js index 2d1a8bf..5b75137 100644 --- a/packages/core/src/batch/BatchSystem.js +++ b/packages/core/src/batch/BatchSystem.js @@ -46,7 +46,6 @@ this.currentRenderer.stop(); this.currentRenderer = objectRenderer; - this.currentRenderer.start(); } diff --git a/packages/core/src/batch/ObjectRenderer.js b/packages/core/src/batch/ObjectRenderer.js index 0c851a9..2788895 100644 --- a/packages/core/src/batch/ObjectRenderer.js +++ b/packages/core/src/batch/ObjectRenderer.js @@ -1,5 +1,3 @@ -import System from '../System'; - /** * Base for a common object renderer that can be used as a system renderer plugin. * @@ -7,29 +5,24 @@ * @extends PIXI.System * @memberof PIXI */ -export default class ObjectRenderer extends System +export default class ObjectRenderer { /** - * Starts the renderer and sets the shader - * + * @param {PIXI.Renderer} renderer - The renderer this manager works for. */ - start() + constructor(renderer) { - // set the shader.. + /** + * The renderer this manager works for. + * + * @member {PIXI.Renderer} + */ + this.renderer = renderer; } /** - * Stops the renderer - * - */ - stop() - { - this.flush(); - } - - /** - * Stub method for rendering content and emptying the current batch. - * + * Stub method that should be used to empty the current + * batch by rendering objects now. */ flush() { @@ -37,7 +30,37 @@ } /** - * Renders an object + * Generic destruction method that frees all resources. This + * should be called by subclasses. + */ + destroy() + { + this.renderer = null; + } + + /** + * Stub method that initializes any state required before + * rendering starts. It is different from the `prerender` + * signal, which occurs every frame, in that it is called + * whenever an object requests _this_ renderer specifically. + */ + start() + { + // set the shader.. + } + + /** + * Stops the renderer. It should free up any state and + * become dormant. + */ + stop() + { + this.flush(); + } + + /** + * Keeps the object to render. It doesn't have to be + * rendered immediately. * * @param {PIXI.DisplayObject} object - The object to render. */ diff --git a/packages/core/src/batch/utils/builtinAttributeDefinitions.js b/packages/core/src/batch/utils/builtinAttributeDefinitions.js new file mode 100644 index 0000000..edbf531 --- /dev/null +++ b/packages/core/src/batch/utils/builtinAttributeDefinitions.js @@ -0,0 +1,18 @@ +import { TYPES } from '@pixi/constants'; + +export default { + aColor: { + type: 'uint32', + size: 1, + glType: TYPES.UNSIGNED_BYTE, + glSize: 4, + _wordSize: 1, + }, + aTextureId: { + type: 'float32', + size: 1, + glType: TYPES.FLOAT, + glSize: 1, + _wordSize: 1, + }, +}; diff --git a/packages/core/src/geometry/ViewableBuffer.js b/packages/core/src/geometry/ViewableBuffer.js new file mode 100644 index 0000000..793a425 --- /dev/null +++ b/packages/core/src/geometry/ViewableBuffer.js @@ -0,0 +1,168 @@ +/** + * Flexible wrapper around `ArrayBuffer` that also provides + * typed array views on demand. + * + * @class + * @memberof PIXI + */ +export default class ViewableBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + /** + * Underlying `ArrayBuffer` that holds all the data + * and is of capacity `size`. + * + * @member {ArrayBuffer} + */ + this.rawBinaryData = new ArrayBuffer(size); + } + + /** + * View on the raw binary data as a `Int8Array`. + * + * @member {Int8Array} + */ + get int8View() + { + if (!this._int8View) + { + this._int8View = new Int8Array(this.rawBinaryData); + } + + return this._int8View; + } + + /** + * View on the raw binary data as a `Uint8Array`. + * + * @member {Uint8Array} + */ + get uint8View() + { + if (!this._uint8View) + { + this._uint8View = new Uint8Array(this.rawBinaryData); + } + + return this._uint8View; + } + + /** + * View on the raw binary data as a `Int16Array`. + * + * @member {Int16Array} + */ + get int16View() + { + if (!this._int16View) + { + this._int16View = new Int16Array(this.rawBinaryData); + } + + return this._int16View; + } + + /** + * View on the raw binary data as a `Uint16Array`. + * + * @member {Uint16Array} + */ + get uint16View() + { + if (!this._uint16View) + { + this._uint16View = new Uint16Array(this.rawBinaryData); + } + + return this._uint16View; + } + + /** + * View on the raw binary data as a `Int32Array`. + * + * @member {Int32Array} + */ + get int32View() + { + if (!this._int32View) + { + this._int32View = new Int32Array(this.rawBinaryData); + } + + return this._int32View; + } + + /** + * View on the raw binary data as a `Uint32Array`. + * + * @member {Float32Array} + */ + get uint32View() + { + if (!this._uint32View) + { + this._uint32View = new Uint32Array(this.rawBinaryData); + } + + return this._uint32View; + } + + /** + * View on the raw binary data as a `Float32Array`. + * + * @member {Float32Array} + */ + get float32View() + { + if (!this._float32View) + { + this._float32View = new Float32Array(this.rawBinaryData); + } + + return this._float32View; + } + + view(type) + { + return this[`${type}View`]; + } + + /** + * Destroys all buffer references. Do not use after calling + * this. + */ + destroy() + { + this.rawBinaryData = null; + this._int8View = null; + this._uint8View = null; + this._int16View = null; + this._uint16View = null; + this._int32View = null; + this._uint32View = null; + this._float32View = null; + } + + static sizeOf(type) + { + switch (type) + { + case 'int8': + case 'uint8': + return 1; + case 'int16': + case 'uint16': + return 2; + case 'int32': + case 'uint32': + case 'float32': + return 4; + default: + throw new Error(`${type} isn't a valid view type`); + } + } +} diff --git a/packages/core/src/index.js b/packages/core/src/index.js index d4cb1fa..08ba1c1 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -38,3 +38,4 @@ export { default as Attribute } from './geometry/Attribute'; export { default as Buffer } from './geometry/Buffer'; export { default as Geometry } from './geometry/Geometry'; +export { default as ViewableBuffer } from './geometry/ViewableBuffer'; diff --git a/packages/graphics/src/Graphics.js b/packages/graphics/src/Graphics.js index e807104..6299d32 100644 --- a/packages/graphics/src/Graphics.js +++ b/packages/graphics/src/Graphics.js @@ -158,6 +158,16 @@ this._transformID = -1; this.batchDirty = -1; + /** + * Plugin that is responsible for rendering this element. + * Allows to customize the rendering process without overriding the + * '_render' & '_renderCanvas' methods. + * + * @member {string} + * @default 'batch' + */ + this.pluginName = 'batch'; + // Set default this.tint = 0xFFFFFF; this.blendMode = BLEND_MODES.NORMAL; @@ -864,7 +874,7 @@ } } - renderer.batch.setObjectRenderer(renderer.plugins.batch); + renderer.batch.setObjectRenderer(renderer.plugins[this.pluginName]); if (this.batches.length) { @@ -877,7 +887,7 @@ batch.worldAlpha = this.worldAlpha * batch.alpha; - renderer.plugins.batch.render(batch); + renderer.plugins[this.pluginName].render(batch); } } } diff --git a/packages/sprite/src/Sprite.js b/packages/sprite/src/Sprite.js index efbc6dd..6572004 100644 --- a/packages/sprite/src/Sprite.js +++ b/packages/sprite/src/Sprite.js @@ -365,7 +365,6 @@ _render(renderer) { this.calculateVertices(); - renderer.batch.setObjectRenderer(renderer.plugins[this.pluginName]); renderer.plugins[this.pluginName].render(this); } diff --git a/packages/core/src/batch/AbstractBatchRenderer.js b/packages/core/src/batch/AbstractBatchRenderer.js new file mode 100644 index 0000000..7b4d061 --- /dev/null +++ b/packages/core/src/batch/AbstractBatchRenderer.js @@ -0,0 +1,783 @@ +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} 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; + } +} diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js deleted file mode 100644 index b80bc7e..0000000 --- a/packages/core/src/batch/BatchBuffer.js +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Used by the BatchRenderer - * - * @class - * @memberof PIXI - */ -export default class BatchBuffer -{ - /** - * @param {number} size - The size of the buffer in bytes. - */ - constructor(size) - { - this.vertices = new ArrayBuffer(size); - - /** - * View on the vertices as a Float32Array for positions - * - * @member {Float32Array} - */ - this.float32View = new Float32Array(this.vertices); - - /** - * View on the vertices as a Uint32Array for uvs - * - * @member {Float32Array} - */ - this.uint32View = new Uint32Array(this.vertices); - } - - /** - * Destroys the buffer. - * - */ - destroy() - { - this.vertices = null; - this.float32View = null; - this.uint32View = null; - } -} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js index 6275f8c..ad67273 100644 --- a/packages/core/src/batch/BatchGeometry.js +++ b/packages/core/src/batch/BatchGeometry.js @@ -1,9 +1,32 @@ -import { TYPES } from '@pixi/constants'; -import Geometry from '../geometry/Geometry'; import Buffer from '../geometry/Buffer'; +import builtinAttributeDefinitions from './utils/builtinAttributeDefinitions'; +import Geometry from '../geometry/Geometry'; +import { TYPES } from '@pixi/constants'; + +const defaultAttributes = [ + { + property: 'vertexData', + name: 'aVertexPosition', + type: 'float32', + size: 2, + glType: TYPES.FLOAT, + glSize: 2, + }, + { + property: 'uvs', + name: 'aTextureCoord', + type: 'float32', + size: 2, + glType: TYPES.FLOAT, + glSize: 2, + }, + 'aColor', // built-in attribute + 'aTextureId', +]; /** - * Geometry used to batch standard PIXI content (e.g. Mesh, Sprite, Graphics objects). + * Geometry used to batch standard PIXI content (e.g. Mesh, Sprite, + * Graphics objects). * * @class * @memberof PIXI @@ -13,8 +36,9 @@ /** * @param {boolean} [_static=false] Optimization flag, where `false` * is updated every frame, `true` doesn't change frame-to-frame. + * @param {Array} attributeDefinitions - attribute definitions */ - constructor(_static = false) + constructor(_static = false, attributeDefinitions = defaultAttributes) { super(); @@ -34,10 +58,25 @@ */ this._indexBuffer = new Buffer(null, _static, true); - this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) - .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) - .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) - .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) - .addIndex(this._indexBuffer); + /* These are automatically interleaved by GeometrySystem. */ + for (let i = 0; i < attributeDefinitions.length; i++) + { + const def = attributeDefinitions[i]; + + if (def === 'aColor') + { // special + this.addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE); + continue; + } + + const isBuiltin = (typeof def === 'string'); + const identifier = isBuiltin ? def : def.name; + const size = isBuiltin ? builtinAttributeDefinitions[identifier].glSize : def.glSize; + const type = isBuiltin ? builtinAttributeDefinitions[identifier].glType : def.glType; + + this.addAttribute(identifier, this._buffer, size, def === 'aTextureId', type); + } + + this.addIndex(this._indexBuffer); } } diff --git a/packages/core/src/batch/BatchPluginFactory.js b/packages/core/src/batch/BatchPluginFactory.js index ce5a14a..aad5e68 100644 --- a/packages/core/src/batch/BatchPluginFactory.js +++ b/packages/core/src/batch/BatchPluginFactory.js @@ -1,6 +1,10 @@ import BatchShaderGenerator from './BatchShaderGenerator'; import BatchGeometry from './BatchGeometry'; -import BaseBatchRenderer from './BatchRenderer'; +import AbstractBatchRenderer from './AbstractBatchRenderer'; +import ViewableBuffer from '../geometry/ViewableBuffer'; + +import { sizeOfType } from '@pixi/utils'; +import { TYPES } from '@pixi/constants'; import defaultVertex from './texture.vert'; import defaultFragment from './texture.frag'; @@ -35,6 +39,8 @@ * * @static * @param {object} [options] + * @param {object} [option.attributeDefinitions=Array] - + * Attribute definitions, see PIXI.AbstractBatchRenderer#attributeDefinitions * @param {string} [options.vertex=PIXI.BatchPluginFactory.defaultVertexSrc] - Vertex shader source * @param {string} [options.fragment=PIXI.BatchPluginFactory.defaultFragmentTemplate] - Fragment shader template * @param {number} [options.vertexSize=6] - Vertex size @@ -43,19 +49,48 @@ */ static create(options) { - const { vertex, fragment, vertexSize, geometryClass } = Object.assign({ + const { + attributeDefinitions, + fragment, + geometryClass, + vertex, + } = Object.assign({ + attributeDefinitions: [ + { + property: 'vertexData', + name: 'aVertexPosition', + type: 'float32', + size: 2, + glType: TYPES.FLOAT, + glSize: 2, + }, + { + property: 'uvs', + name: 'aTextureCoord', + type: 'float32', + size: 2, + glType: TYPES.FLOAT, + glSize: 2, + }, + 'aColor', // built-in attribute + 'aTextureId', + ], vertex: defaultVertex, fragment: defaultFragment, geometryClass: BatchGeometry, - vertexSize: 6, }, options); - return class BatchPlugin extends BaseBatchRenderer + BatchPluginFactory._checkAttributeDefinitionCompatibility(attributeDefinitions); + + const vertexSize = AbstractBatchRenderer.vertexSizeOf(attributeDefinitions); + + return class BatchPlugin extends AbstractBatchRenderer { constructor(renderer) { super(renderer); + this.attributeDefinitions = attributeDefinitions; this.shaderGenerator = new BatchShaderGenerator(vertex, fragment); this.geometryClass = geometryClass; this.vertexSize = vertexSize; @@ -86,6 +121,39 @@ { return defaultFragment; } + + static _checkAttributeDefinitionCompatibility(definitions) + { + definitions.forEach((def) => + { + if (typeof def === 'string') + { + return;// built-in attribute + } + + const inputSize = ViewableBuffer.sizeOf(def.type) * def.size; + + if (inputSize % 4 !== 0) + { + throw new Error('Batch rendering requires that your object ' + + 'attributes be of net size multiple of four. The attribute ' + + `${def.property}, a.k.a ${def.name}, has a source size of` + + `${inputSize}, which is not a multiple of 4. Consider padding` + + 'your elements with additional bytes.'); + } + + const outputSize = sizeOfType(def.glType) * def.glSize; + + if (outputSize !== inputSize) + { + throw new Error('Your object- and gl- types do not match in size.' + + 'The size of each attribute in the object property array is ' + + `${inputSize}, while the buffered size is ${outputSize} in bytes.`); + } + + def._wordSize = inputSize / 4; + }); + } } // Setup the default BatchRenderer plugin, this is what diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js deleted file mode 100644 index daf5dcf..0000000 --- a/packages/core/src/batch/BatchRenderer.js +++ /dev/null @@ -1,653 +0,0 @@ -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 BatchBuffer from './BatchBuffer'; -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 BatchRenderer 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; - - /** - * 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} 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` 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 `BatchBuffer` 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.BatchBuffer} - * @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)(); - } - } - - /** - * 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 and empties the current batch. - * - */ - flush() - { - if (this._vertexCount === 0) - { - return; - } - - const gl = this.renderer.gl; - const MAX_TEXTURES = this.MAX_TEXTURES; - const vertSize = this.vertexSize; - - const buffer = this.getAttributeBuffer(this._vertexCount); - const indexBuffer = this.getIndexBuffer(this._indexCount); - - const elements = this._bufferedElements; - const _drawCalls = this._drawCalls; - - const float32View = buffer.float32View; - const uint32View = buffer.uint32View; - - const touch = this.renderer.textureGC.count; - - let index = 0; - let _indexCount = 0; - let nextTexture; - let currentTexture; - let groupCount = 0; - - let textureCount = 0; - let currentGroup = _drawCalls[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; - - for (i = 0; i < this._bufferSize; ++i) - { - // upload the sprite elements... - // they have all ready been calculated so we just need to push them into the buffer. - - 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, float32View, uint32View, indexBuffer, index, _indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, _indexCount); - - // push a graphics.. - index += (sprite.vertexData.length / 2) * vertSize; - _indexCount += sprite.indices.length; - } - - BaseTexture._globalBatch = TICK; - - currentGroup.size = _indexCount - currentGroup.start; - - if (!settings.CAN_UPLOAD_SAME_BUFFER) - { - // this is still needed for IOS performance.. - // it really does not like uploading to the same buffer in a single frame! - if (this._packedGeometryPoolSize <= this._flushId) - { - this._packedGeometryPoolSize++; - /* eslint-disable max-len */ - this._packedGeometries[this._flushId] = new (this.geometryClass)(); - } - - this._packedGeometries[this._flushId]._buffer.update(buffer.vertices, 0); - this._packedGeometries[this._flushId]._indexBuffer.update(indexBuffer, 0); - - // this.vertexBuffers[this._flushId].update(buffer.vertices, 0); - this.renderer.geometry.bind(this._packedGeometries[this._flushId]); - - this.renderer.geometry.updateBuffers(); - - this._flushId++; - } - else - { - // lets use the faster option, always use buffer number 0 - this._packedGeometries[this._flushId]._buffer.update(buffer.vertices, 0); - this._packedGeometries[this._flushId]._indexBuffer.update(indexBuffer, 0); - - // if (true)// this.spriteOnly) - // { - // this._packedGeometries[this._flushId].indexBuffer = this.defualtSpriteIndexBuffer; - // this._packedGeometries[this._flushId].buffers[1] = this.defualtSpriteIndexBuffer; - // } - - this.renderer.geometry.updateBuffers(); - } - - // this.renderer.state.set(this.state); - - const textureSystem = this.renderer.texture; - const stateSystem = this.renderer.state; - // e.log(groupCount); - // / render the _drawCalls.. - - 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; - } - - // this.state.blendMode = group.blend; - // this.state.blend = true; - - // this.renderer.state.setState(this.state); - // set the blend mode.. - 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 `BatchRenderer`. 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 {BatchBuffer} - 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 BatchBuffer(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 {FLoat32Array} float32View - float32-view of the attribute buffer - * @param {Uint32Array} uint32View - uint32-view of the 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, - float32View, uint32View, indexBuffer, aIndex, iIndex) - { - const p = 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++] = p + indicies[i]; - } - } -} diff --git a/packages/core/src/batch/BatchSystem.js b/packages/core/src/batch/BatchSystem.js index 2d1a8bf..5b75137 100644 --- a/packages/core/src/batch/BatchSystem.js +++ b/packages/core/src/batch/BatchSystem.js @@ -46,7 +46,6 @@ this.currentRenderer.stop(); this.currentRenderer = objectRenderer; - this.currentRenderer.start(); } diff --git a/packages/core/src/batch/ObjectRenderer.js b/packages/core/src/batch/ObjectRenderer.js index 0c851a9..2788895 100644 --- a/packages/core/src/batch/ObjectRenderer.js +++ b/packages/core/src/batch/ObjectRenderer.js @@ -1,5 +1,3 @@ -import System from '../System'; - /** * Base for a common object renderer that can be used as a system renderer plugin. * @@ -7,29 +5,24 @@ * @extends PIXI.System * @memberof PIXI */ -export default class ObjectRenderer extends System +export default class ObjectRenderer { /** - * Starts the renderer and sets the shader - * + * @param {PIXI.Renderer} renderer - The renderer this manager works for. */ - start() + constructor(renderer) { - // set the shader.. + /** + * The renderer this manager works for. + * + * @member {PIXI.Renderer} + */ + this.renderer = renderer; } /** - * Stops the renderer - * - */ - stop() - { - this.flush(); - } - - /** - * Stub method for rendering content and emptying the current batch. - * + * Stub method that should be used to empty the current + * batch by rendering objects now. */ flush() { @@ -37,7 +30,37 @@ } /** - * Renders an object + * Generic destruction method that frees all resources. This + * should be called by subclasses. + */ + destroy() + { + this.renderer = null; + } + + /** + * Stub method that initializes any state required before + * rendering starts. It is different from the `prerender` + * signal, which occurs every frame, in that it is called + * whenever an object requests _this_ renderer specifically. + */ + start() + { + // set the shader.. + } + + /** + * Stops the renderer. It should free up any state and + * become dormant. + */ + stop() + { + this.flush(); + } + + /** + * Keeps the object to render. It doesn't have to be + * rendered immediately. * * @param {PIXI.DisplayObject} object - The object to render. */ diff --git a/packages/core/src/batch/utils/builtinAttributeDefinitions.js b/packages/core/src/batch/utils/builtinAttributeDefinitions.js new file mode 100644 index 0000000..edbf531 --- /dev/null +++ b/packages/core/src/batch/utils/builtinAttributeDefinitions.js @@ -0,0 +1,18 @@ +import { TYPES } from '@pixi/constants'; + +export default { + aColor: { + type: 'uint32', + size: 1, + glType: TYPES.UNSIGNED_BYTE, + glSize: 4, + _wordSize: 1, + }, + aTextureId: { + type: 'float32', + size: 1, + glType: TYPES.FLOAT, + glSize: 1, + _wordSize: 1, + }, +}; diff --git a/packages/core/src/geometry/ViewableBuffer.js b/packages/core/src/geometry/ViewableBuffer.js new file mode 100644 index 0000000..793a425 --- /dev/null +++ b/packages/core/src/geometry/ViewableBuffer.js @@ -0,0 +1,168 @@ +/** + * Flexible wrapper around `ArrayBuffer` that also provides + * typed array views on demand. + * + * @class + * @memberof PIXI + */ +export default class ViewableBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + /** + * Underlying `ArrayBuffer` that holds all the data + * and is of capacity `size`. + * + * @member {ArrayBuffer} + */ + this.rawBinaryData = new ArrayBuffer(size); + } + + /** + * View on the raw binary data as a `Int8Array`. + * + * @member {Int8Array} + */ + get int8View() + { + if (!this._int8View) + { + this._int8View = new Int8Array(this.rawBinaryData); + } + + return this._int8View; + } + + /** + * View on the raw binary data as a `Uint8Array`. + * + * @member {Uint8Array} + */ + get uint8View() + { + if (!this._uint8View) + { + this._uint8View = new Uint8Array(this.rawBinaryData); + } + + return this._uint8View; + } + + /** + * View on the raw binary data as a `Int16Array`. + * + * @member {Int16Array} + */ + get int16View() + { + if (!this._int16View) + { + this._int16View = new Int16Array(this.rawBinaryData); + } + + return this._int16View; + } + + /** + * View on the raw binary data as a `Uint16Array`. + * + * @member {Uint16Array} + */ + get uint16View() + { + if (!this._uint16View) + { + this._uint16View = new Uint16Array(this.rawBinaryData); + } + + return this._uint16View; + } + + /** + * View on the raw binary data as a `Int32Array`. + * + * @member {Int32Array} + */ + get int32View() + { + if (!this._int32View) + { + this._int32View = new Int32Array(this.rawBinaryData); + } + + return this._int32View; + } + + /** + * View on the raw binary data as a `Uint32Array`. + * + * @member {Float32Array} + */ + get uint32View() + { + if (!this._uint32View) + { + this._uint32View = new Uint32Array(this.rawBinaryData); + } + + return this._uint32View; + } + + /** + * View on the raw binary data as a `Float32Array`. + * + * @member {Float32Array} + */ + get float32View() + { + if (!this._float32View) + { + this._float32View = new Float32Array(this.rawBinaryData); + } + + return this._float32View; + } + + view(type) + { + return this[`${type}View`]; + } + + /** + * Destroys all buffer references. Do not use after calling + * this. + */ + destroy() + { + this.rawBinaryData = null; + this._int8View = null; + this._uint8View = null; + this._int16View = null; + this._uint16View = null; + this._int32View = null; + this._uint32View = null; + this._float32View = null; + } + + static sizeOf(type) + { + switch (type) + { + case 'int8': + case 'uint8': + return 1; + case 'int16': + case 'uint16': + return 2; + case 'int32': + case 'uint32': + case 'float32': + return 4; + default: + throw new Error(`${type} isn't a valid view type`); + } + } +} diff --git a/packages/core/src/index.js b/packages/core/src/index.js index d4cb1fa..08ba1c1 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -38,3 +38,4 @@ export { default as Attribute } from './geometry/Attribute'; export { default as Buffer } from './geometry/Buffer'; export { default as Geometry } from './geometry/Geometry'; +export { default as ViewableBuffer } from './geometry/ViewableBuffer'; diff --git a/packages/graphics/src/Graphics.js b/packages/graphics/src/Graphics.js index e807104..6299d32 100644 --- a/packages/graphics/src/Graphics.js +++ b/packages/graphics/src/Graphics.js @@ -158,6 +158,16 @@ this._transformID = -1; this.batchDirty = -1; + /** + * Plugin that is responsible for rendering this element. + * Allows to customize the rendering process without overriding the + * '_render' & '_renderCanvas' methods. + * + * @member {string} + * @default 'batch' + */ + this.pluginName = 'batch'; + // Set default this.tint = 0xFFFFFF; this.blendMode = BLEND_MODES.NORMAL; @@ -864,7 +874,7 @@ } } - renderer.batch.setObjectRenderer(renderer.plugins.batch); + renderer.batch.setObjectRenderer(renderer.plugins[this.pluginName]); if (this.batches.length) { @@ -877,7 +887,7 @@ batch.worldAlpha = this.worldAlpha * batch.alpha; - renderer.plugins.batch.render(batch); + renderer.plugins[this.pluginName].render(batch); } } } diff --git a/packages/sprite/src/Sprite.js b/packages/sprite/src/Sprite.js index efbc6dd..6572004 100644 --- a/packages/sprite/src/Sprite.js +++ b/packages/sprite/src/Sprite.js @@ -365,7 +365,6 @@ _render(renderer) { this.calculateVertices(); - renderer.batch.setObjectRenderer(renderer.plugins[this.pluginName]); renderer.plugins[this.pluginName].render(this); } diff --git a/packages/utils/src/data/index.js b/packages/utils/src/data/index.js index e77949c..6e78c02 100644 --- a/packages/utils/src/data/index.js +++ b/packages/utils/src/data/index.js @@ -2,4 +2,5 @@ export * from './removeItems'; export * from './uid'; export * from './sign'; +export * from './sizeOfType'; export * from './pow2'; diff --git a/packages/core/src/batch/AbstractBatchRenderer.js b/packages/core/src/batch/AbstractBatchRenderer.js new file mode 100644 index 0000000..7b4d061 --- /dev/null +++ b/packages/core/src/batch/AbstractBatchRenderer.js @@ -0,0 +1,783 @@ +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} 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; + } +} diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js deleted file mode 100644 index b80bc7e..0000000 --- a/packages/core/src/batch/BatchBuffer.js +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Used by the BatchRenderer - * - * @class - * @memberof PIXI - */ -export default class BatchBuffer -{ - /** - * @param {number} size - The size of the buffer in bytes. - */ - constructor(size) - { - this.vertices = new ArrayBuffer(size); - - /** - * View on the vertices as a Float32Array for positions - * - * @member {Float32Array} - */ - this.float32View = new Float32Array(this.vertices); - - /** - * View on the vertices as a Uint32Array for uvs - * - * @member {Float32Array} - */ - this.uint32View = new Uint32Array(this.vertices); - } - - /** - * Destroys the buffer. - * - */ - destroy() - { - this.vertices = null; - this.float32View = null; - this.uint32View = null; - } -} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js index 6275f8c..ad67273 100644 --- a/packages/core/src/batch/BatchGeometry.js +++ b/packages/core/src/batch/BatchGeometry.js @@ -1,9 +1,32 @@ -import { TYPES } from '@pixi/constants'; -import Geometry from '../geometry/Geometry'; import Buffer from '../geometry/Buffer'; +import builtinAttributeDefinitions from './utils/builtinAttributeDefinitions'; +import Geometry from '../geometry/Geometry'; +import { TYPES } from '@pixi/constants'; + +const defaultAttributes = [ + { + property: 'vertexData', + name: 'aVertexPosition', + type: 'float32', + size: 2, + glType: TYPES.FLOAT, + glSize: 2, + }, + { + property: 'uvs', + name: 'aTextureCoord', + type: 'float32', + size: 2, + glType: TYPES.FLOAT, + glSize: 2, + }, + 'aColor', // built-in attribute + 'aTextureId', +]; /** - * Geometry used to batch standard PIXI content (e.g. Mesh, Sprite, Graphics objects). + * Geometry used to batch standard PIXI content (e.g. Mesh, Sprite, + * Graphics objects). * * @class * @memberof PIXI @@ -13,8 +36,9 @@ /** * @param {boolean} [_static=false] Optimization flag, where `false` * is updated every frame, `true` doesn't change frame-to-frame. + * @param {Array} attributeDefinitions - attribute definitions */ - constructor(_static = false) + constructor(_static = false, attributeDefinitions = defaultAttributes) { super(); @@ -34,10 +58,25 @@ */ this._indexBuffer = new Buffer(null, _static, true); - this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) - .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) - .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) - .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) - .addIndex(this._indexBuffer); + /* These are automatically interleaved by GeometrySystem. */ + for (let i = 0; i < attributeDefinitions.length; i++) + { + const def = attributeDefinitions[i]; + + if (def === 'aColor') + { // special + this.addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE); + continue; + } + + const isBuiltin = (typeof def === 'string'); + const identifier = isBuiltin ? def : def.name; + const size = isBuiltin ? builtinAttributeDefinitions[identifier].glSize : def.glSize; + const type = isBuiltin ? builtinAttributeDefinitions[identifier].glType : def.glType; + + this.addAttribute(identifier, this._buffer, size, def === 'aTextureId', type); + } + + this.addIndex(this._indexBuffer); } } diff --git a/packages/core/src/batch/BatchPluginFactory.js b/packages/core/src/batch/BatchPluginFactory.js index ce5a14a..aad5e68 100644 --- a/packages/core/src/batch/BatchPluginFactory.js +++ b/packages/core/src/batch/BatchPluginFactory.js @@ -1,6 +1,10 @@ import BatchShaderGenerator from './BatchShaderGenerator'; import BatchGeometry from './BatchGeometry'; -import BaseBatchRenderer from './BatchRenderer'; +import AbstractBatchRenderer from './AbstractBatchRenderer'; +import ViewableBuffer from '../geometry/ViewableBuffer'; + +import { sizeOfType } from '@pixi/utils'; +import { TYPES } from '@pixi/constants'; import defaultVertex from './texture.vert'; import defaultFragment from './texture.frag'; @@ -35,6 +39,8 @@ * * @static * @param {object} [options] + * @param {object} [option.attributeDefinitions=Array] - + * Attribute definitions, see PIXI.AbstractBatchRenderer#attributeDefinitions * @param {string} [options.vertex=PIXI.BatchPluginFactory.defaultVertexSrc] - Vertex shader source * @param {string} [options.fragment=PIXI.BatchPluginFactory.defaultFragmentTemplate] - Fragment shader template * @param {number} [options.vertexSize=6] - Vertex size @@ -43,19 +49,48 @@ */ static create(options) { - const { vertex, fragment, vertexSize, geometryClass } = Object.assign({ + const { + attributeDefinitions, + fragment, + geometryClass, + vertex, + } = Object.assign({ + attributeDefinitions: [ + { + property: 'vertexData', + name: 'aVertexPosition', + type: 'float32', + size: 2, + glType: TYPES.FLOAT, + glSize: 2, + }, + { + property: 'uvs', + name: 'aTextureCoord', + type: 'float32', + size: 2, + glType: TYPES.FLOAT, + glSize: 2, + }, + 'aColor', // built-in attribute + 'aTextureId', + ], vertex: defaultVertex, fragment: defaultFragment, geometryClass: BatchGeometry, - vertexSize: 6, }, options); - return class BatchPlugin extends BaseBatchRenderer + BatchPluginFactory._checkAttributeDefinitionCompatibility(attributeDefinitions); + + const vertexSize = AbstractBatchRenderer.vertexSizeOf(attributeDefinitions); + + return class BatchPlugin extends AbstractBatchRenderer { constructor(renderer) { super(renderer); + this.attributeDefinitions = attributeDefinitions; this.shaderGenerator = new BatchShaderGenerator(vertex, fragment); this.geometryClass = geometryClass; this.vertexSize = vertexSize; @@ -86,6 +121,39 @@ { return defaultFragment; } + + static _checkAttributeDefinitionCompatibility(definitions) + { + definitions.forEach((def) => + { + if (typeof def === 'string') + { + return;// built-in attribute + } + + const inputSize = ViewableBuffer.sizeOf(def.type) * def.size; + + if (inputSize % 4 !== 0) + { + throw new Error('Batch rendering requires that your object ' + + 'attributes be of net size multiple of four. The attribute ' + + `${def.property}, a.k.a ${def.name}, has a source size of` + + `${inputSize}, which is not a multiple of 4. Consider padding` + + 'your elements with additional bytes.'); + } + + const outputSize = sizeOfType(def.glType) * def.glSize; + + if (outputSize !== inputSize) + { + throw new Error('Your object- and gl- types do not match in size.' + + 'The size of each attribute in the object property array is ' + + `${inputSize}, while the buffered size is ${outputSize} in bytes.`); + } + + def._wordSize = inputSize / 4; + }); + } } // Setup the default BatchRenderer plugin, this is what diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js deleted file mode 100644 index daf5dcf..0000000 --- a/packages/core/src/batch/BatchRenderer.js +++ /dev/null @@ -1,653 +0,0 @@ -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 BatchBuffer from './BatchBuffer'; -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 BatchRenderer 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; - - /** - * 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} 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` 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 `BatchBuffer` 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.BatchBuffer} - * @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)(); - } - } - - /** - * 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 and empties the current batch. - * - */ - flush() - { - if (this._vertexCount === 0) - { - return; - } - - const gl = this.renderer.gl; - const MAX_TEXTURES = this.MAX_TEXTURES; - const vertSize = this.vertexSize; - - const buffer = this.getAttributeBuffer(this._vertexCount); - const indexBuffer = this.getIndexBuffer(this._indexCount); - - const elements = this._bufferedElements; - const _drawCalls = this._drawCalls; - - const float32View = buffer.float32View; - const uint32View = buffer.uint32View; - - const touch = this.renderer.textureGC.count; - - let index = 0; - let _indexCount = 0; - let nextTexture; - let currentTexture; - let groupCount = 0; - - let textureCount = 0; - let currentGroup = _drawCalls[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; - - for (i = 0; i < this._bufferSize; ++i) - { - // upload the sprite elements... - // they have all ready been calculated so we just need to push them into the buffer. - - 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, float32View, uint32View, indexBuffer, index, _indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, _indexCount); - - // push a graphics.. - index += (sprite.vertexData.length / 2) * vertSize; - _indexCount += sprite.indices.length; - } - - BaseTexture._globalBatch = TICK; - - currentGroup.size = _indexCount - currentGroup.start; - - if (!settings.CAN_UPLOAD_SAME_BUFFER) - { - // this is still needed for IOS performance.. - // it really does not like uploading to the same buffer in a single frame! - if (this._packedGeometryPoolSize <= this._flushId) - { - this._packedGeometryPoolSize++; - /* eslint-disable max-len */ - this._packedGeometries[this._flushId] = new (this.geometryClass)(); - } - - this._packedGeometries[this._flushId]._buffer.update(buffer.vertices, 0); - this._packedGeometries[this._flushId]._indexBuffer.update(indexBuffer, 0); - - // this.vertexBuffers[this._flushId].update(buffer.vertices, 0); - this.renderer.geometry.bind(this._packedGeometries[this._flushId]); - - this.renderer.geometry.updateBuffers(); - - this._flushId++; - } - else - { - // lets use the faster option, always use buffer number 0 - this._packedGeometries[this._flushId]._buffer.update(buffer.vertices, 0); - this._packedGeometries[this._flushId]._indexBuffer.update(indexBuffer, 0); - - // if (true)// this.spriteOnly) - // { - // this._packedGeometries[this._flushId].indexBuffer = this.defualtSpriteIndexBuffer; - // this._packedGeometries[this._flushId].buffers[1] = this.defualtSpriteIndexBuffer; - // } - - this.renderer.geometry.updateBuffers(); - } - - // this.renderer.state.set(this.state); - - const textureSystem = this.renderer.texture; - const stateSystem = this.renderer.state; - // e.log(groupCount); - // / render the _drawCalls.. - - 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; - } - - // this.state.blendMode = group.blend; - // this.state.blend = true; - - // this.renderer.state.setState(this.state); - // set the blend mode.. - 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 `BatchRenderer`. 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 {BatchBuffer} - 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 BatchBuffer(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 {FLoat32Array} float32View - float32-view of the attribute buffer - * @param {Uint32Array} uint32View - uint32-view of the 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, - float32View, uint32View, indexBuffer, aIndex, iIndex) - { - const p = 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++] = p + indicies[i]; - } - } -} diff --git a/packages/core/src/batch/BatchSystem.js b/packages/core/src/batch/BatchSystem.js index 2d1a8bf..5b75137 100644 --- a/packages/core/src/batch/BatchSystem.js +++ b/packages/core/src/batch/BatchSystem.js @@ -46,7 +46,6 @@ this.currentRenderer.stop(); this.currentRenderer = objectRenderer; - this.currentRenderer.start(); } diff --git a/packages/core/src/batch/ObjectRenderer.js b/packages/core/src/batch/ObjectRenderer.js index 0c851a9..2788895 100644 --- a/packages/core/src/batch/ObjectRenderer.js +++ b/packages/core/src/batch/ObjectRenderer.js @@ -1,5 +1,3 @@ -import System from '../System'; - /** * Base for a common object renderer that can be used as a system renderer plugin. * @@ -7,29 +5,24 @@ * @extends PIXI.System * @memberof PIXI */ -export default class ObjectRenderer extends System +export default class ObjectRenderer { /** - * Starts the renderer and sets the shader - * + * @param {PIXI.Renderer} renderer - The renderer this manager works for. */ - start() + constructor(renderer) { - // set the shader.. + /** + * The renderer this manager works for. + * + * @member {PIXI.Renderer} + */ + this.renderer = renderer; } /** - * Stops the renderer - * - */ - stop() - { - this.flush(); - } - - /** - * Stub method for rendering content and emptying the current batch. - * + * Stub method that should be used to empty the current + * batch by rendering objects now. */ flush() { @@ -37,7 +30,37 @@ } /** - * Renders an object + * Generic destruction method that frees all resources. This + * should be called by subclasses. + */ + destroy() + { + this.renderer = null; + } + + /** + * Stub method that initializes any state required before + * rendering starts. It is different from the `prerender` + * signal, which occurs every frame, in that it is called + * whenever an object requests _this_ renderer specifically. + */ + start() + { + // set the shader.. + } + + /** + * Stops the renderer. It should free up any state and + * become dormant. + */ + stop() + { + this.flush(); + } + + /** + * Keeps the object to render. It doesn't have to be + * rendered immediately. * * @param {PIXI.DisplayObject} object - The object to render. */ diff --git a/packages/core/src/batch/utils/builtinAttributeDefinitions.js b/packages/core/src/batch/utils/builtinAttributeDefinitions.js new file mode 100644 index 0000000..edbf531 --- /dev/null +++ b/packages/core/src/batch/utils/builtinAttributeDefinitions.js @@ -0,0 +1,18 @@ +import { TYPES } from '@pixi/constants'; + +export default { + aColor: { + type: 'uint32', + size: 1, + glType: TYPES.UNSIGNED_BYTE, + glSize: 4, + _wordSize: 1, + }, + aTextureId: { + type: 'float32', + size: 1, + glType: TYPES.FLOAT, + glSize: 1, + _wordSize: 1, + }, +}; diff --git a/packages/core/src/geometry/ViewableBuffer.js b/packages/core/src/geometry/ViewableBuffer.js new file mode 100644 index 0000000..793a425 --- /dev/null +++ b/packages/core/src/geometry/ViewableBuffer.js @@ -0,0 +1,168 @@ +/** + * Flexible wrapper around `ArrayBuffer` that also provides + * typed array views on demand. + * + * @class + * @memberof PIXI + */ +export default class ViewableBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + /** + * Underlying `ArrayBuffer` that holds all the data + * and is of capacity `size`. + * + * @member {ArrayBuffer} + */ + this.rawBinaryData = new ArrayBuffer(size); + } + + /** + * View on the raw binary data as a `Int8Array`. + * + * @member {Int8Array} + */ + get int8View() + { + if (!this._int8View) + { + this._int8View = new Int8Array(this.rawBinaryData); + } + + return this._int8View; + } + + /** + * View on the raw binary data as a `Uint8Array`. + * + * @member {Uint8Array} + */ + get uint8View() + { + if (!this._uint8View) + { + this._uint8View = new Uint8Array(this.rawBinaryData); + } + + return this._uint8View; + } + + /** + * View on the raw binary data as a `Int16Array`. + * + * @member {Int16Array} + */ + get int16View() + { + if (!this._int16View) + { + this._int16View = new Int16Array(this.rawBinaryData); + } + + return this._int16View; + } + + /** + * View on the raw binary data as a `Uint16Array`. + * + * @member {Uint16Array} + */ + get uint16View() + { + if (!this._uint16View) + { + this._uint16View = new Uint16Array(this.rawBinaryData); + } + + return this._uint16View; + } + + /** + * View on the raw binary data as a `Int32Array`. + * + * @member {Int32Array} + */ + get int32View() + { + if (!this._int32View) + { + this._int32View = new Int32Array(this.rawBinaryData); + } + + return this._int32View; + } + + /** + * View on the raw binary data as a `Uint32Array`. + * + * @member {Float32Array} + */ + get uint32View() + { + if (!this._uint32View) + { + this._uint32View = new Uint32Array(this.rawBinaryData); + } + + return this._uint32View; + } + + /** + * View on the raw binary data as a `Float32Array`. + * + * @member {Float32Array} + */ + get float32View() + { + if (!this._float32View) + { + this._float32View = new Float32Array(this.rawBinaryData); + } + + return this._float32View; + } + + view(type) + { + return this[`${type}View`]; + } + + /** + * Destroys all buffer references. Do not use after calling + * this. + */ + destroy() + { + this.rawBinaryData = null; + this._int8View = null; + this._uint8View = null; + this._int16View = null; + this._uint16View = null; + this._int32View = null; + this._uint32View = null; + this._float32View = null; + } + + static sizeOf(type) + { + switch (type) + { + case 'int8': + case 'uint8': + return 1; + case 'int16': + case 'uint16': + return 2; + case 'int32': + case 'uint32': + case 'float32': + return 4; + default: + throw new Error(`${type} isn't a valid view type`); + } + } +} diff --git a/packages/core/src/index.js b/packages/core/src/index.js index d4cb1fa..08ba1c1 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -38,3 +38,4 @@ export { default as Attribute } from './geometry/Attribute'; export { default as Buffer } from './geometry/Buffer'; export { default as Geometry } from './geometry/Geometry'; +export { default as ViewableBuffer } from './geometry/ViewableBuffer'; diff --git a/packages/graphics/src/Graphics.js b/packages/graphics/src/Graphics.js index e807104..6299d32 100644 --- a/packages/graphics/src/Graphics.js +++ b/packages/graphics/src/Graphics.js @@ -158,6 +158,16 @@ this._transformID = -1; this.batchDirty = -1; + /** + * Plugin that is responsible for rendering this element. + * Allows to customize the rendering process without overriding the + * '_render' & '_renderCanvas' methods. + * + * @member {string} + * @default 'batch' + */ + this.pluginName = 'batch'; + // Set default this.tint = 0xFFFFFF; this.blendMode = BLEND_MODES.NORMAL; @@ -864,7 +874,7 @@ } } - renderer.batch.setObjectRenderer(renderer.plugins.batch); + renderer.batch.setObjectRenderer(renderer.plugins[this.pluginName]); if (this.batches.length) { @@ -877,7 +887,7 @@ batch.worldAlpha = this.worldAlpha * batch.alpha; - renderer.plugins.batch.render(batch); + renderer.plugins[this.pluginName].render(batch); } } } diff --git a/packages/sprite/src/Sprite.js b/packages/sprite/src/Sprite.js index efbc6dd..6572004 100644 --- a/packages/sprite/src/Sprite.js +++ b/packages/sprite/src/Sprite.js @@ -365,7 +365,6 @@ _render(renderer) { this.calculateVertices(); - renderer.batch.setObjectRenderer(renderer.plugins[this.pluginName]); renderer.plugins[this.pluginName].render(this); } diff --git a/packages/utils/src/data/index.js b/packages/utils/src/data/index.js index e77949c..6e78c02 100644 --- a/packages/utils/src/data/index.js +++ b/packages/utils/src/data/index.js @@ -2,4 +2,5 @@ export * from './removeItems'; export * from './uid'; export * from './sign'; +export * from './sizeOfType'; export * from './pow2'; diff --git a/packages/utils/src/data/sizeOfType.js b/packages/utils/src/data/sizeOfType.js new file mode 100644 index 0000000..f647b1f --- /dev/null +++ b/packages/utils/src/data/sizeOfType.js @@ -0,0 +1,27 @@ +import { TYPES } from '@pixi/constants'; + +/** + * Returns the size of any type in the `TYPES` enum. + * + * @function sizeOfType + * @param {number} glint - TYPES enum constant + * @return {number} size of `glint` in bytes + */ +export function sizeOfType(glint) +{ + switch (glint) + { + case TYPES.FLOAT: + return 4; + case TYPES.HALF_FLOAT: + case TYPES.UNSIGNED_SHORT_5_5_5_1: + case TYPES.UNSIGNED_SHORT_4_4_4_4: + case TYPES.UNSIGNED_SHORT_5_6_5: + case TYPES.UNSIGNED_SHORT: + return 2; + case TYPES.UNSIGNED_BYTE: + return 1; + default: + throw new Error(`{$glint} isn't a TYPES enum!`); + } +}