Skip to content

Commit

Permalink
perf: Reduce allocations on the hot path (#3111)
Browse files Browse the repository at this point in the history
This PR takes a stab at a lot of the hot path allocations in excalibur improving performance significantly (numbers will be forthcoming with the new broadphase updates)

- New `RentalPool` type for sparse object pooling
- Perf improvements: Hot path allocations
  * Reduce State/Transform stack hot path allocations in graphics context
  * Reduce Transform allocations
  * Reduce AffineMatrix allocations
  • Loading branch information
eonarheim authored Jun 27, 2024
1 parent 0d8681f commit 2fbb29c
Show file tree
Hide file tree
Showing 11 changed files with 182 additions and 113 deletions.
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- `actor.oldGlobalPos` returns the globalPosition from the previous frame
- create development builds of excalibur that bundlers can use in dev mode
- show warning in development when Entity hasn't been added to a scene after a few seconds
- New `RentalPool` type for sparse object pooling

### Fixed

Expand All @@ -32,7 +33,10 @@ This project adheres to [Semantic Versioning](http://semver.org/).

### Updates

-
- Perf improvements: Hot path allocations
* Reduce State/Transform stack hot path allocations in graphics context
* Reduce Transform allocations
* Reduce AffineMatrix allocations

### Changed

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,7 @@ export class ExcaliburGraphicsContextWebGL implements ExcaliburGraphicsContext {
}

public resetTransform(): void {
this._transform.current = AffineMatrix.identity();
this._transform.reset();
}

public updateViewport(resolution: Resolution): void {
Expand Down
46 changes: 28 additions & 18 deletions src/engine/Graphics/Context/state-stack.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,45 @@
import { Color } from '../../Color';
import { RentalPool } from '../../Util/RentalPool';
import { ExcaliburGraphicsContextState } from './ExcaliburGraphicsContext';
import { Material } from './material';

export class ContextState implements ExcaliburGraphicsContextState {
opacity: number = 1;
z: number = 0;
tint: Color = Color.White;
material: Material = null;
}

export class StateStack {
public current: ExcaliburGraphicsContextState = this._getDefaultState();
private _pool = new RentalPool<ContextState>(
() => new ContextState(),
(s) => {
s.opacity = 1;
s.z = 0;
s.tint = Color.White;
s.material = null;
return s;
},
100
);
public current: ExcaliburGraphicsContextState = this._pool.rent(true);
private _states: ExcaliburGraphicsContextState[] = [];

private _getDefaultState() {
return {
opacity: 1,
z: 0,
tint: Color.White,
material: null as Material
};
}

private _cloneState() {
return {
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
};
private _cloneState(dest: ContextState) {
dest.opacity = this.current.opacity;
dest.z = this.current.z;
dest.tint = this.current.tint.clone(); // TODO remove color alloc
dest.material = this.current.material; // TODO is this going to cause problems when cloning
return dest;
}

public save(): void {
this._states.push(this.current);
this.current = this._cloneState();
this.current = this._cloneState(this._pool.rent());
}

public restore(): void {
this._pool.return(this.current);
this.current = this._states.pop();
}
}
16 changes: 14 additions & 2 deletions src/engine/Graphics/Context/transform-stack.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
import { AffineMatrix } from '../../Math/affine-matrix';
import { RentalPool } from '../../Util/RentalPool';

export class TransformStack {
private _pool = new RentalPool(
() => AffineMatrix.identity(),
(mat) => mat.reset(),
100
);
private _transforms: AffineMatrix[] = [];
private _currentTransform: AffineMatrix = AffineMatrix.identity();

private _currentTransform: AffineMatrix = this._pool.rent(true);

public save(): void {
this._transforms.push(this._currentTransform);
this._currentTransform = this._currentTransform.clone();
this._currentTransform = this._currentTransform.clone(this._pool.rent());
}

public restore(): void {
this._pool.return(this._currentTransform);
this._currentTransform = this._transforms.pop();
}

Expand All @@ -25,6 +33,10 @@ export class TransformStack {
return this._currentTransform.scale(x, y);
}

public reset(): void {
this._currentTransform.reset();
}

public set current(matrix: AffineMatrix) {
this._currentTransform = matrix;
}
Expand Down
16 changes: 6 additions & 10 deletions src/engine/Graphics/GraphicsComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,33 +112,29 @@ export class GraphicsComponent extends Component {
*/
public opacity: number = 1;

private _offset: Vector = Vector.Zero;
private _offset: Vector = new WatchVector(Vector.Zero, () => this.recalculateBounds());

/**
* Offset to apply to graphics by default
*/
public get offset(): Vector {
return new WatchVector(this._offset, () => {
this.recalculateBounds();
});
return this._offset;
}
public set offset(value: Vector) {
this._offset = value;
this._offset = new WatchVector(value, () => this.recalculateBounds());
this.recalculateBounds();
}

private _anchor: Vector = Vector.Half;
private _anchor: Vector = new WatchVector(Vector.Half, () => this.recalculateBounds());

/**
* Anchor to apply to graphics by default
*/
public get anchor(): Vector {
return new WatchVector(this._anchor, () => {
this.recalculateBounds();
});
return this._anchor;
}
public set anchor(value: Vector) {
this._anchor = value;
this._anchor = new WatchVector(value, () => this.recalculateBounds());
this.recalculateBounds();
}

Expand Down
7 changes: 6 additions & 1 deletion src/engine/Math/affine-matrix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -407,7 +407,12 @@ export class AffineMatrix {
*/
public clone(dest?: AffineMatrix): AffineMatrix {
const mat = dest || new AffineMatrix();
mat.data.set(this.data);
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._scaleSignX = this._scaleSignX;
mat._scaleSignY = this._scaleSignY;
return mat;
Expand Down
140 changes: 67 additions & 73 deletions src/engine/Math/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,20 +27,15 @@ export class Transform {
}
private _children: Transform[] = [];

private _pos: Vector = vec(0, 0);
private _pos: Vector = new WatchVector(vec(0, 0), () => {
this.flagDirty();
});
set pos(v: Vector) {
if (!v.equals(this._pos)) {
this._pos.x = v.x;
this._pos.y = v.y;
this.flagDirty();
}
this._pos.x = v.x;
this._pos.y = v.y;
}
get pos() {
return new WatchVector(this._pos, (x, y) => {
if (x !== this._pos.x || y !== this._pos.y) {
this.flagDirty();
}
});
return this._pos;
}

set globalPos(v: Vector) {
Expand All @@ -53,33 +48,34 @@ export class Transform {
this.flagDirty();
}
}
get globalPos() {
return new VectorView({
getX: () => this.matrix.data[4],
getY: () => this.matrix.data[5],
setX: (x) => {
if (this.parent) {
const { x: newX } = this.parent.inverse.multiply(vec(x, this.pos.y));
this.pos.x = newX;
} else {
this.pos.x = x;
}
if (x !== this.matrix.data[4]) {
this.flagDirty();
}
},
setY: (y) => {
if (this.parent) {
const { y: newY } = this.parent.inverse.multiply(vec(this.pos.x, y));
this.pos.y = newY;
} else {
this.pos.y = y;
}
if (y !== this.matrix.data[5]) {
this.flagDirty();
}
private _globalPos = new VectorView({
getX: () => this.matrix.data[4],
getY: () => this.matrix.data[5],
setX: (x) => {
if (this.parent) {
const { x: newX } = this.parent.inverse.multiply(vec(x, this.pos.y));
this.pos.x = newX;
} else {
this.pos.x = x;
}
if (x !== this.matrix.data[4]) {
this.flagDirty();
}
},
setY: (y) => {
if (this.parent) {
const { y: newY } = this.parent.inverse.multiply(vec(this.pos.x, y));
this.pos.y = newY;
} else {
this.pos.y = y;
}
if (y !== this.matrix.data[5]) {
this.flagDirty();
}
});
}
});
get globalPos() {
return this._globalPos;
}

private _rotation: number = 0;
Expand Down Expand Up @@ -113,20 +109,15 @@ export class Transform {
return this.rotation;
}

private _scale: Vector = vec(1, 1);
private _scale: Vector = new WatchVector(vec(1, 1), () => {
this.flagDirty();
});
set scale(v: Vector) {
if (v.x !== this._scale.x || v.y !== this._scale.y) {
this._scale.x = v.x;
this._scale.y = v.y;
this.flagDirty();
}
this._scale.x = v.x;
this._scale.y = v.y;
}
get scale() {
return new WatchVector(this._scale, (x, y) => {
if (x !== this._scale.x || y !== this._scale.y) {
this.flagDirty();
}
});
return this._scale;
}

set globalScale(v: Vector) {
Expand All @@ -137,27 +128,28 @@ export class Transform {
this.scale = v.scale(vec(1 / inverseScale.x, 1 / inverseScale.y));
}

get globalScale() {
return new VectorView({
getX: () => (this.parent ? this.matrix.getScaleX() : this.scale.x),
getY: () => (this.parent ? this.matrix.getScaleY() : this.scale.y),
setX: (x) => {
if (this.parent) {
const globalScaleX = this.parent.globalScale.x;
this.scale.x = x / globalScaleX;
} else {
this.scale.x = x;
}
},
setY: (y) => {
if (this.parent) {
const globalScaleY = this.parent.globalScale.y;
this.scale.y = y / globalScaleY;
} else {
this.scale.y = y;
}
private _globalScale = new VectorView({
getX: () => (this.parent ? this.matrix.getScaleX() : this.scale.x),
getY: () => (this.parent ? this.matrix.getScaleY() : this.scale.y),
setX: (x) => {
if (this.parent) {
const globalScaleX = this.parent.globalScale.x;
this.scale.x = x / globalScaleX;
} else {
this.scale.x = x;
}
},
setY: (y) => {
if (this.parent) {
const globalScaleY = this.parent.globalScale.y;
this.scale.y = y / globalScaleY;
} else {
this.scale.y = y;
}
});
}
});
get globalScale() {
return this._globalScale;
}

private _z: number = 0;
Expand Down Expand Up @@ -194,9 +186,9 @@ export class Transform {
public get matrix() {
if (this._isDirty) {
if (this.parent === null) {
this._matrix = this._calculateMatrix();
this._calculateMatrix().clone(this._matrix);
} else {
this._matrix = this.parent.matrix.multiply(this._calculateMatrix());
this.parent.matrix.multiply(this._calculateMatrix()).clone(this._matrix);
}
this._isDirty = false;
}
Expand All @@ -205,15 +197,17 @@ export class Transform {

public get inverse() {
if (this._isInverseDirty) {
this._inverse = this.matrix.inverse();
this.matrix.inverse(this._inverse);
this._isInverseDirty = false;
}
return this._inverse;
}

private _scratch = AffineMatrix.identity();
private _calculateMatrix(): AffineMatrix {
const matrix = AffineMatrix.identity().translate(this.pos.x, this.pos.y).rotate(this.rotation).scale(this.scale.x, this.scale.y);
return matrix;
this._scratch.reset();
this._scratch.translate(this.pos.x, this.pos.y).rotate(this.rotation).scale(this.scale.x, this.scale.y);
return this._scratch;
}

public flagDirty() {
Expand Down
Loading

0 comments on commit 2fbb29c

Please sign in to comment.