diff --git a/CHANGELOG.md b/CHANGELOG.md index bf9a460ae..21a4c718e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 @@ -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 diff --git a/src/engine/Graphics/Context/ExcaliburGraphicsContextWebGL.ts b/src/engine/Graphics/Context/ExcaliburGraphicsContextWebGL.ts index 1c1ed5741..8ba263da4 100644 --- a/src/engine/Graphics/Context/ExcaliburGraphicsContextWebGL.ts +++ b/src/engine/Graphics/Context/ExcaliburGraphicsContextWebGL.ts @@ -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 { diff --git a/src/engine/Graphics/Context/state-stack.ts b/src/engine/Graphics/Context/state-stack.ts index cd0fe0e78..fa6de9a03 100644 --- a/src/engine/Graphics/Context/state-stack.ts +++ b/src/engine/Graphics/Context/state-stack.ts @@ -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( + () => 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(); } } diff --git a/src/engine/Graphics/Context/transform-stack.ts b/src/engine/Graphics/Context/transform-stack.ts index 7137ba8d5..f63825664 100644 --- a/src/engine/Graphics/Context/transform-stack.ts +++ b/src/engine/Graphics/Context/transform-stack.ts @@ -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(); } @@ -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; } diff --git a/src/engine/Graphics/GraphicsComponent.ts b/src/engine/Graphics/GraphicsComponent.ts index e381b61b8..457306480 100644 --- a/src/engine/Graphics/GraphicsComponent.ts +++ b/src/engine/Graphics/GraphicsComponent.ts @@ -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(); } diff --git a/src/engine/Math/affine-matrix.ts b/src/engine/Math/affine-matrix.ts index e89159f46..e2687752b 100644 --- a/src/engine/Math/affine-matrix.ts +++ b/src/engine/Math/affine-matrix.ts @@ -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; diff --git a/src/engine/Math/transform.ts b/src/engine/Math/transform.ts index 91410f03f..bd4420d0f 100644 --- a/src/engine/Math/transform.ts +++ b/src/engine/Math/transform.ts @@ -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) { @@ -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; @@ -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) { @@ -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; @@ -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; } @@ -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() { diff --git a/src/engine/Math/watch-vector.ts b/src/engine/Math/watch-vector.ts index fab021429..1e43ab76a 100644 --- a/src/engine/Math/watch-vector.ts +++ b/src/engine/Math/watch-vector.ts @@ -15,8 +15,10 @@ export class WatchVector extends Vector { } public set x(newX: number) { - this.change(newX, this._y); - this._x = this.original.x = newX; + if (newX !== this._x) { + this.change(newX, this._y); + this._x = this.original.x = newX; + } } public get y() { @@ -24,7 +26,9 @@ export class WatchVector extends Vector { } public set y(newY: number) { - this.change(this._x, newY); - this._y = this.original.y = newY; + if (newY !== this._y) { + this.change(this._x, newY); + this._y = this.original.y = newY; + } } } diff --git a/src/engine/Util/RentalPool.ts b/src/engine/Util/RentalPool.ts new file mode 100644 index 000000000..e4ce7bf19 --- /dev/null +++ b/src/engine/Util/RentalPool.ts @@ -0,0 +1,46 @@ +export class RentalPool { + private _pool: T[] = []; + private _size: number = 0; + constructor( + public builder: () => T, + public cleaner: (used: T) => T, + preAllocate: number = 1 + ) { + this.grow(preAllocate); + } + + /** + * Grow the pool size by an amount + * @param amount + */ + grow(amount: number): void { + if (amount > 0) { + this._size += amount; + for (let i = 0; i < amount; i++) { + this._pool.push(this.builder()); + } + } + } + + /** + * Rent an object from the pool, optionally clean it. If not cleaned previous state may be set. + * + * The pool will automatically double if depleted + * @param clean + */ + rent(clean: boolean = false): T { + if (this._pool.length === 0) { + this.grow(this._size); + } + + return clean ? this.cleaner(this._pool.pop()) : this._pool.pop(); + } + + /** + * Return an object to the pool + * @param object + */ + return(object: T): void { + this._pool.push(object); + } +} diff --git a/src/spec/CollisionShapeSpec.ts b/src/spec/CollisionShapeSpec.ts index ef163547e..5936f55d1 100644 --- a/src/spec/CollisionShapeSpec.ts +++ b/src/spec/CollisionShapeSpec.ts @@ -595,7 +595,7 @@ describe('Collision Shape', () => { const contact = polyA.collide(polyB)[0]; // there should be a collision - expect(contact).not.toBe(null); + expect(contact).withContext('There should be a collision').not.toBeFalsy(); // normal and mtv should point away from bodyA expect(directionOfBodyB.dot(contact.mtv)).toBeGreaterThan(0); diff --git a/src/spec/ScreenElementSpec.ts b/src/spec/ScreenElementSpec.ts index 307b18785..6c74e8e81 100644 --- a/src/spec/ScreenElementSpec.ts +++ b/src/spec/ScreenElementSpec.ts @@ -67,8 +67,6 @@ describe('A ScreenElement', () => { it('can be constructed with a non-default collider', () => { const sut = new ScreenElement({ - width: 100, - height: 100, collisionType: ex.CollisionType.Active, collider: ex.Shape.Circle(50) });