Skip to content

Commit

Permalink
perf: 2x image draw perf - remove allocations from image-renderer (#2962
Browse files Browse the repository at this point in the history
)

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)
  • Loading branch information
eonarheim authored Mar 30, 2024
1 parent eba425f commit 3f3ac09
Show file tree
Hide file tree
Showing 9 changed files with 216 additions and 96 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

<!--------------------------------- DO NOT EDIT BELOW THIS LINE --------------------------------->
Expand Down
50 changes: 43 additions & 7 deletions src/engine/Graphics/Context/ExcaliburGraphicsContextWebGL.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -445,6 +447,26 @@ export class ExcaliburGraphicsContextWebGL implements ExcaliburGraphicsContext {
this._postProcessTargets[1].setResolution(gl.canvas.width, gl.canvas.height);
}

private _imageToWidth = new Map<HTMLImageSource, number>();
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<HTMLImageSource, number>();
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(
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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<string, number>();
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;
Expand All @@ -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;
Expand Down Expand Up @@ -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()) {
Expand Down
133 changes: 92 additions & 41 deletions src/engine/Graphics/Context/image-renderer/image-renderer.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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<WebGLTexture, number>();
private _images = new Set<HTMLImageSource>();
private _vertexIndex: number = 0;

constructor(options: ImageRendererOptions) {
Expand Down Expand Up @@ -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 ||
Expand All @@ -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);
}
}

Expand All @@ -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;
}
Expand All @@ -161,7 +168,30 @@ export class ImageRenderer implements RendererPlugin {
return false;
}

private _imageToWidth = new Map<HTMLImageSource, number>();
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<HTMLImageSource, number>();
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,
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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();
}
}
Loading

0 comments on commit 3f3ac09

Please sign in to comment.