From 3f3ac09f0eac25adfe8243533fe95771a8d85220 Mon Sep 17 00:00:00 2001 From: Erik Onarheim Date: Fri, 29 Mar 2024 23:39:36 -0500 Subject: [PATCH] perf: 2x image draw perf - remove allocations from image-renderer (#2962) See excalibur bunnymark https://github.com/excaliburjs/excalibur-bunnymark This PR removes allocations and makes several perf improvements that DOUBLES our performance on the bunnymark * Removes all allocations from the draw image hot path * Removes allocations from object pool * Removes unecessary getters * Speed up AffineMatrix clone * Switches webgl to VAOs * Speed up texture id lookup * Caches native thunks to width/height ## New Perf Results ~50fps @ 20,000 sprites. ![image](https://github.com/excaliburjs/Excalibur/assets/612071/292a5ff5-4e03-4d19-9118-a2f3455960c1) ~30fps @ 35000 sprites ![image](https://github.com/excaliburjs/Excalibur/assets/612071/8abbb13b-cfcf-4b38-9d89-dc5def385d11) ## Previous Perf Results on Excalibur main ~25fps @ 20000 sprites ![image](https://github.com/excaliburjs/Excalibur/assets/612071/afe0bab7-b2ee-4edf-a3c0-d9ede0325cb5) ~15fps @ 35000 sprites ![image](https://github.com/excaliburjs/Excalibur/assets/612071/391bb767-e712-494a-b442-fd7425af8637) --- CHANGELOG.md | 1 + .../Context/ExcaliburGraphicsContextWebGL.ts | 50 ++++++- .../Context/image-renderer/image-renderer.ts | 133 ++++++++++++------ src/engine/Graphics/Context/state-stack.ts | 24 ++-- src/engine/Graphics/Context/vertex-buffer.ts | 4 + src/engine/Graphics/Context/vertex-layout.ts | 31 ++-- src/engine/Math/affine-matrix.ts | 38 +++-- src/engine/Util/Pool.ts | 13 +- src/spec/VertexLayoutSpec.ts | 18 ++- 9 files changed, 216 insertions(+), 96 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00d882615..6288a9ac9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -92,6 +92,7 @@ will be positioned with the top left of the graphic at the actor's position. ### Changed +- Significant 2x performance improvement to image drawing in Excalibur - Simplified `ex.Loader` viewport/resolution internal configuration diff --git a/src/engine/Graphics/Context/ExcaliburGraphicsContextWebGL.ts b/src/engine/Graphics/Context/ExcaliburGraphicsContextWebGL.ts index a3e1f3149..e0fd68274 100644 --- a/src/engine/Graphics/Context/ExcaliburGraphicsContextWebGL.ts +++ b/src/engine/Graphics/Context/ExcaliburGraphicsContextWebGL.ts @@ -107,7 +107,9 @@ export class ExcaliburGraphicsContextWebGL implements ExcaliburGraphicsContext { instance.args = undefined; return instance; }, 4000); - private _drawCalls: DrawCall[] = []; + + private _drawCallIndex = 0; + private _drawCalls: DrawCall[] = (new Array(4000)).fill(null); // Main render target private _renderTarget: RenderTarget; @@ -409,7 +411,7 @@ export class ExcaliburGraphicsContextWebGL implements ExcaliburGraphicsContext { drawCall.state.tint = this._state.current.tint; drawCall.state.material = this._state.current.material; drawCall.args = args; - this._drawCalls.push(drawCall); + this._drawCalls[this._drawCallIndex++] = drawCall; } else { // Set the current renderer if not defined if (!this._currentRenderer) { @@ -445,6 +447,26 @@ export class ExcaliburGraphicsContextWebGL implements ExcaliburGraphicsContext { this._postProcessTargets[1].setResolution(gl.canvas.width, gl.canvas.height); } + private _imageToWidth = new Map(); + private _getImageWidth(image: HTMLImageSource) { + let maybeWidth = this._imageToWidth.get(image); + if (maybeWidth === undefined) { + maybeWidth = image.width; + this._imageToWidth.set(image, maybeWidth); + } + return maybeWidth; + } + + private _imageToHeight = new Map(); + private _getImageHeight(image: HTMLImageSource) { + let maybeHeight = this._imageToHeight.get(image); + if (maybeHeight === undefined) { + maybeHeight = image.height; + this._imageToHeight.set(image, maybeHeight); + } + return maybeHeight; + } + drawImage(image: HTMLImageSource, x: number, y: number): void; drawImage(image: HTMLImageSource, x: number, y: number, width: number, height: number): void; drawImage( @@ -473,7 +495,7 @@ export class ExcaliburGraphicsContextWebGL implements ExcaliburGraphicsContext { return; // zero dimension dest exit early } else if (dwidth === 0 || dheight === 0) { return; // zero dimension dest exit early - } else if (image.width === 0 || image.height === 0) { + } else if (this._getImageWidth(image) === 0 || this._getImageHeight(image) === 0) { return; // zero dimension source exit early } @@ -636,15 +658,27 @@ export class ExcaliburGraphicsContextWebGL implements ExcaliburGraphicsContext { currentTarget.use(); if (this.useDrawSorting) { + // null out unused draw calls + for (let i = this._drawCallIndex; i < this._drawCalls.length; i++) { + this._drawCalls[i] = null; + } // sort draw calls // Find the original order of the first instance of the draw call const originalSort = new Map(); for (const [name] of this._renderers) { - const firstIndex = this._drawCalls.findIndex(dc => dc.renderer === name); + let firstIndex = 0; + for (firstIndex = 0; firstIndex < this._drawCallIndex; firstIndex++) { + if (this._drawCalls[firstIndex].renderer === name) { + break; + } + } originalSort.set(name, firstIndex); } this._drawCalls.sort((a, b) => { + if (a === null || b === null) { + return 0; + } const zIndex = a.z - b.z; const originalSortOrder = originalSort.get(a.renderer) - originalSort.get(b.renderer); const priority = a.priority - b.priority; @@ -660,10 +694,10 @@ export class ExcaliburGraphicsContextWebGL implements ExcaliburGraphicsContext { const oldTransform = this._transform.current; const oldState = this._state.current; - if (this._drawCalls.length) { + if (this._drawCalls.length && this._drawCallIndex) { let currentRendererName = this._drawCalls[0].renderer; let currentRenderer = this._renderers.get(currentRendererName); - for (let i = 0; i < this._drawCalls.length; i++) { + for (let i = 0; i < this._drawCallIndex; i++) { // hydrate the state for renderers this._transform.current = this._drawCalls[i].transform; this._state.current = this._drawCalls[i].state; @@ -694,7 +728,9 @@ export class ExcaliburGraphicsContextWebGL implements ExcaliburGraphicsContext { // reclaim draw calls this._drawCallPool.done(); - this._drawCalls.length = 0; + this._drawCallIndex = 0; + this._imageToHeight.clear(); + this._imageToWidth.clear(); } else { // This is the final flush at the moment to draw any leftover pending draw for (const renderer of this._renderers.values()) { diff --git a/src/engine/Graphics/Context/image-renderer/image-renderer.ts b/src/engine/Graphics/Context/image-renderer/image-renderer.ts index a520d79c7..ca4b9f078 100644 --- a/src/engine/Graphics/Context/image-renderer/image-renderer.ts +++ b/src/engine/Graphics/Context/image-renderer/image-renderer.ts @@ -1,5 +1,4 @@ import { sign } from '../../../Math/util'; -import { vec } from '../../../Math/vector'; import { ImageFiltering } from '../../Filtering'; import { GraphicsDiagnostics } from '../../GraphicsDiagnostics'; import { HTMLImageSource } from '../ExcaliburGraphicsContext'; @@ -37,6 +36,9 @@ export class ImageRenderer implements RendererPlugin { // Per flush vars private _imageCount: number = 0; private _textures: WebGLTexture[] = []; + private _textureIndex = 0; + private _textureToIndex = new Map(); + private _images = new Set(); private _vertexIndex: number = 0; constructor(options: ImageRendererOptions) { @@ -119,6 +121,9 @@ export class ImageRenderer implements RendererPlugin { } private _addImageAsTexture(image: HTMLImageSource) { + if (this._images.has(image)) { + return; + } const maybeFiltering = image.getAttribute('filtering'); let filtering: ImageFiltering = null; if (maybeFiltering === ImageFiltering.Blended || @@ -132,6 +137,8 @@ export class ImageRenderer implements RendererPlugin { image.removeAttribute('forceUpload'); if (this._textures.indexOf(texture) === -1) { this._textures.push(texture); + this._textureToIndex.set(texture, this._textureIndex++); + this._images.add(image); } } @@ -146,7 +153,7 @@ export class ImageRenderer implements RendererPlugin { private _getTextureIdForImage(image: HTMLImageSource) { if (image) { const maybeTexture = this._context.textureLoader.get(image); - return this._textures.indexOf(maybeTexture); + return this._textureToIndex.get(maybeTexture) ?? -1; //this._textures.indexOf(maybeTexture); } return -1; } @@ -161,7 +168,30 @@ export class ImageRenderer implements RendererPlugin { return false; } + private _imageToWidth = new Map(); + private _getImageWidth(image: HTMLImageSource) { + let maybeWidth = this._imageToWidth.get(image); + if (maybeWidth === undefined) { + maybeWidth = image.width; + this._imageToWidth.set(image, maybeWidth); + } + return maybeWidth; + } + + private _imageToHeight = new Map(); + private _getImageHeight(image: HTMLImageSource) { + let maybeHeight = this._imageToHeight.get(image); + if (maybeHeight === undefined) { + maybeHeight = image.height; + this._imageToHeight.set(image, maybeHeight); + } + return maybeHeight; + } + + private _view = [0, 0, 0, 0]; + private _dest = [0, 0]; + private _quad = [0, 0, 0, 0, 0, 0, 0, 0]; draw(image: HTMLImageSource, sx: number, sy: number, @@ -180,73 +210,89 @@ export class ImageRenderer implements RendererPlugin { this._imageCount++; // This creates and uploads the texture if not already done this._addImageAsTexture(image); - - let width = image?.width || swidth || 0; - let height = image?.height || sheight || 0; - let view = [0, 0, swidth ?? image?.width ?? 0, sheight ?? image?.height ?? 0]; - let dest = [sx ?? 1, sy ?? 1]; + const maybeImageWidth = this._getImageWidth(image); + const maybeImageHeight = this._getImageHeight(image); + + let width = maybeImageWidth || swidth || 0; + let height = maybeImageHeight || sheight || 0; + this._view[2] = swidth ?? maybeImageWidth ?? 0; + this._view[3] = sheight ?? maybeImageHeight ?? 0; + this._dest[0] = sx ?? 1; + this._dest[1] = sy ?? 1; // If destination is specified, update view and dest if (dx !== undefined && dy !== undefined && dwidth !== undefined && dheight !== undefined) { - view = [sx ?? 1, sy ?? 1, swidth ?? image?.width ?? 0, sheight ?? image?.height ?? 0]; - dest = [dx, dy]; + this._view[0] = sx ?? 1; + this._view[1] = sy ?? 1; + this._view[2] = swidth ?? maybeImageWidth ?? 0; + this._view[3] = sheight ?? maybeImageHeight ?? 0; + this._dest[0] = dx; + this._dest[1] = dy; width = dwidth; height = dheight; } - sx = view[0]; - sy = view[1]; - const sw = view[2]; - const sh = view[3]; + sx = this._view[0]; + sy = this._view[1]; + const sw = this._view[2]; + const sh = this._view[3]; // transform based on current context const transform = this._context.getTransform(); const opacity = this._context.opacity; const snapToPixel = this._context.snapToPixel; - let topLeft = vec(dest[0], dest[1]); - let topRight = vec(dest[0] + width, dest[1]); - let bottomLeft = vec(dest[0], dest[1] + height); - let bottomRight = vec(dest[0] + width, dest[1] + height); + // top left + this._quad[0] = this._dest[0]; + this._quad[1] = this._dest[1]; + + // top right + this._quad[2] = this._dest[0] + width; + this._quad[3] = this._dest[1]; + + // bottom left + this._quad[4] = this._dest[0]; + this._quad[5] = this._dest[1] + height; + + // bottom right + this._quad[6] = this._dest[0] + width; + this._quad[7] = this._dest[1] + height; - topLeft = transform.multiply(topLeft); - topRight = transform.multiply(topRight); - bottomLeft = transform.multiply(bottomLeft); - bottomRight = transform.multiply(bottomRight); + transform.multiplyQuadInPlace(this._quad); if (snapToPixel) { - topLeft.x = ~~(topLeft.x + sign(topLeft.x) * pixelSnapEpsilon); - topLeft.y = ~~(topLeft.y + sign(topLeft.y) * pixelSnapEpsilon); + this._quad[0] = ~~(this._quad[0] + sign(this._quad[0]) * pixelSnapEpsilon); + this._quad[1] = ~~(this._quad[1] + sign(this._quad[1]) * pixelSnapEpsilon); - topRight.x = ~~(topRight.x + sign(topRight.x) * pixelSnapEpsilon); - topRight.y = ~~(topRight.y + sign(topRight.y) * pixelSnapEpsilon); + this._quad[2] = ~~(this._quad[2] + sign(this._quad[2]) * pixelSnapEpsilon); + this._quad[3] = ~~(this._quad[3] + sign(this._quad[3]) * pixelSnapEpsilon); - bottomLeft.x = ~~(bottomLeft.x + sign(bottomLeft.x) * pixelSnapEpsilon); - bottomLeft.y = ~~(bottomLeft.y + sign(bottomLeft.y) * pixelSnapEpsilon); + this._quad[4] = ~~(this._quad[4] + sign(this._quad[4]) * pixelSnapEpsilon); + this._quad[5] = ~~(this._quad[5] + sign(this._quad[5]) * pixelSnapEpsilon); - bottomRight.x = ~~(bottomRight.x + sign(bottomRight.x) * pixelSnapEpsilon); - bottomRight.y = ~~(bottomRight.y + sign(bottomRight.y) * pixelSnapEpsilon); + this._quad[6] = ~~(this._quad[6] + sign(this._quad[6]) * pixelSnapEpsilon); + this._quad[7] = ~~(this._quad[7] + sign(this._quad[7]) * pixelSnapEpsilon); } const tint = this._context.tint; const textureId = this._getTextureIdForImage(image); - const imageWidth = image.width || width; - const imageHeight = image.height || height; + const imageWidth = maybeImageWidth || width; + const imageHeight = maybeImageHeight || height; const uvx0 = (sx + this.uvPadding) / imageWidth; const uvy0 = (sy + this.uvPadding) / imageHeight; const uvx1 = (sx + sw - this.uvPadding) / imageWidth; const uvy1 = (sy + sh - this.uvPadding) / imageHeight; - const txWidth = image.width; - const txHeight = image.height; + const txWidth = maybeImageWidth; + const txHeight = maybeImageHeight; // update data const vertexBuffer = this._layout.vertexBuffer.bufferData; // (0, 0) - 0 - vertexBuffer[this._vertexIndex++] = topLeft.x; - vertexBuffer[this._vertexIndex++] = topLeft.y; + vertexBuffer[this._vertexIndex++] = this._quad[0]; + vertexBuffer[this._vertexIndex++] = this._quad[1]; vertexBuffer[this._vertexIndex++] = opacity; vertexBuffer[this._vertexIndex++] = txWidth; vertexBuffer[this._vertexIndex++] = txHeight; @@ -259,8 +305,8 @@ export class ImageRenderer implements RendererPlugin { vertexBuffer[this._vertexIndex++] = tint.a; // (0, 1) - 1 - vertexBuffer[this._vertexIndex++] = bottomLeft.x; - vertexBuffer[this._vertexIndex++] = bottomLeft.y; + vertexBuffer[this._vertexIndex++] = this._quad[4]; + vertexBuffer[this._vertexIndex++] = this._quad[5]; vertexBuffer[this._vertexIndex++] = opacity; vertexBuffer[this._vertexIndex++] = txWidth; vertexBuffer[this._vertexIndex++] = txHeight; @@ -273,8 +319,8 @@ export class ImageRenderer implements RendererPlugin { vertexBuffer[this._vertexIndex++] = tint.a; // (1, 0) - 2 - vertexBuffer[this._vertexIndex++] = topRight.x; - vertexBuffer[this._vertexIndex++] = topRight.y; + vertexBuffer[this._vertexIndex++] = this._quad[2]; + vertexBuffer[this._vertexIndex++] = this._quad[3]; vertexBuffer[this._vertexIndex++] = opacity; vertexBuffer[this._vertexIndex++] = txWidth; vertexBuffer[this._vertexIndex++] = txHeight; @@ -287,8 +333,8 @@ export class ImageRenderer implements RendererPlugin { vertexBuffer[this._vertexIndex++] = tint.a; // (1, 1) - 3 - vertexBuffer[this._vertexIndex++] = bottomRight.x; - vertexBuffer[this._vertexIndex++] = bottomRight.y; + vertexBuffer[this._vertexIndex++] = this._quad[6]; + vertexBuffer[this._vertexIndex++] = this._quad[7]; vertexBuffer[this._vertexIndex++] = opacity; vertexBuffer[this._vertexIndex++] = txWidth; vertexBuffer[this._vertexIndex++] = txHeight; @@ -340,5 +386,10 @@ export class ImageRenderer implements RendererPlugin { this._imageCount = 0; this._vertexIndex = 0; this._textures.length = 0; + this._textureIndex = 0; + this._textureToIndex.clear(); + this._images.clear(); + this._imageToWidth.clear(); + this._imageToHeight.clear(); } } \ No newline at end of file diff --git a/src/engine/Graphics/Context/state-stack.ts b/src/engine/Graphics/Context/state-stack.ts index 2cb14387b..cd0fe0e78 100644 --- a/src/engine/Graphics/Context/state-stack.ts +++ b/src/engine/Graphics/Context/state-stack.ts @@ -3,8 +3,8 @@ import { ExcaliburGraphicsContextState } from './ExcaliburGraphicsContext'; import { Material } from './material'; export class StateStack { + public current: ExcaliburGraphicsContextState = this._getDefaultState(); private _states: ExcaliburGraphicsContextState[] = []; - private _currentState: ExcaliburGraphicsContextState = this._getDefaultState(); private _getDefaultState() { return { @@ -17,27 +17,19 @@ export class StateStack { private _cloneState() { return { - opacity: this._currentState.opacity, - z: this._currentState.z, - tint: this._currentState.tint.clone(), - material: this._currentState.material // TODO is this going to cause problems when cloning + opacity: this.current.opacity, + z: this.current.z, + tint: this.current.tint.clone(), + material: this.current.material // TODO is this going to cause problems when cloning }; } public save(): void { - this._states.push(this._currentState); - this._currentState = this._cloneState(); + this._states.push(this.current); + this.current = this._cloneState(); } public restore(): void { - this._currentState = this._states.pop(); - } - - public get current(): ExcaliburGraphicsContextState { - return this._currentState; - } - - public set current(val: ExcaliburGraphicsContextState) { - this._currentState = val; + this.current = this._states.pop(); } } diff --git a/src/engine/Graphics/Context/vertex-buffer.ts b/src/engine/Graphics/Context/vertex-buffer.ts index eaf6ac8ed..eb2349e5a 100644 --- a/src/engine/Graphics/Context/vertex-buffer.ts +++ b/src/engine/Graphics/Context/vertex-buffer.ts @@ -71,7 +71,11 @@ export class VertexBuffer { bind() { const gl = this._gl; gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer); + } + unbind() { + const gl = this._gl; + gl.bindBuffer(gl.ARRAY_BUFFER, null); } /** diff --git a/src/engine/Graphics/Context/vertex-layout.ts b/src/engine/Graphics/Context/vertex-layout.ts index 7c67e1ff1..f8137d840 100644 --- a/src/engine/Graphics/Context/vertex-layout.ts +++ b/src/engine/Graphics/Context/vertex-layout.ts @@ -47,6 +47,8 @@ export class VertexLayout { private _layout: VertexAttributeDefinition[] = []; private _attributes: [name: string, numberOfComponents: number][] = []; private _vertexBuffer: VertexBuffer; + private _vao: WebGLVertexArrayObject; + public get vertexBuffer() { return this._vertexBuffer; } @@ -155,8 +157,25 @@ export class VertexLayout { +` (${this._vertexBuffer.bufferData.length})`); } + + // create VAO + const gl = this._gl; + this._vao = gl.createVertexArray(); + gl.bindVertexArray(this._vao); + this._vertexBuffer.bind(); + + let offset = 0; + for (const vert of this._layout) { + if (vert.location !== -1) { // skip unused attributes + gl.vertexAttribPointer(vert.location, vert.size, vert.glType, vert.normalized, this.totalVertexSizeBytes, offset); + gl.enableVertexAttribArray(vert.location); + } + offset += getGlTypeSizeBytes(gl, vert.glType) * vert.size; + } + gl.bindVertexArray(null); + this._vertexBuffer.unbind(); + this._initialized = true; - // TODO Use VAO here instead } /** @@ -176,14 +195,6 @@ export class VertexLayout { if (uploadBuffer) { this._vertexBuffer.upload(count); } - let offset = 0; - // TODO switch to VAOs if the extension is - for (const vert of this._layout) { - if (vert.location !== -1) { // skip unused attributes - gl.vertexAttribPointer(vert.location, vert.size, vert.glType, vert.normalized, this.totalVertexSizeBytes, offset); - gl.enableVertexAttribArray(vert.location); - } - offset += getGlTypeSizeBytes(gl, vert.glType) * vert.size; - } + gl.bindVertexArray(this._vao); } } \ No newline at end of file diff --git a/src/engine/Math/affine-matrix.ts b/src/engine/Math/affine-matrix.ts index 8cc8475d8..f6d2bd0df 100644 --- a/src/engine/Math/affine-matrix.ts +++ b/src/engine/Math/affine-matrix.ts @@ -263,6 +263,33 @@ export class AffineMatrix { } } + /** + * Packed array of length 8, that contains 4 vertices, with 2 components each + * So: [x0, y0, x1, y1, x2, y2, x3, y3] + * @param quad + */ + multiplyQuadInPlace(quad: number[]) { + const resultTopLeftX = quad[0] * this.data[0] + quad[1] * this.data[2] + this.data[4]; + const resultTopLeftY = quad[0] * this.data[1] + quad[1] * this.data[3] + this.data[5]; + quad[0] = resultTopLeftX; + quad[1] = resultTopLeftY; + + const resultTopRightX = quad[2] * this.data[0] + quad[3] * this.data[2] + this.data[4]; + const resultTopRightY = quad[2] * this.data[1] + quad[3] * this.data[3] + this.data[5]; + quad[2] = resultTopRightX; + quad[3] = resultTopRightY; + + const resultBottomLeftX = quad[4] * this.data[0] + quad[5] * this.data[2] + this.data[4]; + const resultBottomLeftY = quad[4] * this.data[1] + quad[5] * this.data[3] + this.data[5]; + quad[4] = resultBottomLeftX; + quad[5] = resultBottomLeftY; + + const resultBottomRightX = quad[6] * this.data[0] + quad[7] * this.data[2] + this.data[4]; + const resultBottomRightY = quad[6] * this.data[1] + quad[7] * this.data[3] + this.data[5]; + quad[6] = resultBottomRightX; + quad[7] = resultBottomRightY; + } + to4x4() { const mat = new Matrix(); mat.data[0] = this.data[0]; @@ -383,18 +410,11 @@ export class AffineMatrix { } /** - * Creates a new Matrix with the same data as the current 4x4 + * Creates a new Matrix with the same data as the current [[AffineMatrix]] */ public clone(dest?: AffineMatrix): AffineMatrix { const mat = dest || new AffineMatrix(); - mat.data[0] = this.data[0]; - mat.data[1] = this.data[1]; - - mat.data[2] = this.data[2]; - mat.data[3] = this.data[3]; - - mat.data[4] = this.data[4]; - mat.data[5] = this.data[5]; + mat.data.set(this.data); return mat; } diff --git a/src/engine/Util/Pool.ts b/src/engine/Util/Pool.ts index 46b734bfb..805497872 100644 --- a/src/engine/Util/Pool.ts +++ b/src/engine/Util/Pool.ts @@ -7,8 +7,8 @@ export class Pool { private _logger = Logger.getInstance(); constructor( - public builder: (...args: any[]) => Type, - public recycler: (instance: Type, ...args: any[]) => Type, + public builder: () => Type, + public recycler: (instance: Type) => Type, public maxObjects: number = 100 ) {} @@ -48,9 +48,8 @@ export class Pool { /** * Retrieve a value from the pool, will allocate a new instance if necessary or recycle from the pool - * @param args */ - get(...args: any[]): Type { + get(): Type { if (this.index === this.maxObjects) { if (!this.disableWarnings) { this._logger.warn('Max pooled objects reached, possible memory leak? Doubling'); @@ -60,11 +59,11 @@ export class Pool { if (this.objects[this.index]) { // Pool has an available object already constructed - return this.recycler(this.objects[this.index++], ...args); + return this.recycler(this.objects[this.index++]); } else { // New allocation this.totalAllocations++; - const object = (this.objects[this.index++] = this.builder(...args)); + const object = (this.objects[this.index++] = this.builder()); return object; } } @@ -84,7 +83,7 @@ export class Pool { for (const object of objects) { const poolIndex = this.objects.indexOf(object); // Build a new object to take the pool place - this.objects[poolIndex] = (this as any).builder(); // TODO problematic 0-arg only support + this.objects[poolIndex] = (this as any).builder(); this.totalAllocations++; } return objects; diff --git a/src/spec/VertexLayoutSpec.ts b/src/spec/VertexLayoutSpec.ts index 2793731d3..896aee664 100644 --- a/src/spec/VertexLayoutSpec.ts +++ b/src/spec/VertexLayoutSpec.ts @@ -211,6 +211,15 @@ describe('A VertexLayout', () => { }); it('will calculate vertex size and webgl vbo correctly', () => { + spyOn(gl, 'createVertexArray').and.callThrough(); + const createVertexArray = gl.createVertexArray as jasmine.Spy; + spyOn(gl, 'bindVertexArray').and.callThrough(); + const bindVertexArray = gl.bindVertexArray as jasmine.Spy; + spyOn(gl, 'vertexAttribPointer').and.callThrough(); + const vertexAttribPointerSpy = gl.vertexAttribPointer as jasmine.Spy; + spyOn(gl, 'enableVertexAttribArray').and.callThrough(); + const enableVertexAttribArraySpy = gl.enableVertexAttribArray as jasmine.Spy; + const shader = new ex.Shader({ gl, vertexSource: ` @@ -245,16 +254,13 @@ describe('A VertexLayout', () => { ] }); expect(sut.totalVertexSizeBytes).withContext('pos is 2x4 + uv is 2x4').toBe(16); - spyOn(gl, 'vertexAttribPointer').and.callThrough(); - const vertexAttribPointerSpy = gl.vertexAttribPointer as jasmine.Spy; - spyOn(gl, 'enableVertexAttribArray').and.callThrough(); - const enableVertexAttribArraySpy = gl.enableVertexAttribArray as jasmine.Spy; - shader.use(); - sut.use(); + sut.initialize(); const pos = shader.attributes.a_position; const uv = shader.attributes.a_uv; + expect(createVertexArray).toHaveBeenCalledTimes(1); + expect(bindVertexArray).toHaveBeenCalledTimes(2); expect(vertexAttribPointerSpy.calls.argsFor(0)).toEqual([ pos.location, 2,