Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf: Remove allocations from image-renderer #2962

Merged
merged 15 commits into from
Mar 30, 2024
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
Loading