-
-
Notifications
You must be signed in to change notification settings - Fork 195
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: WebGLTexture garbage collection (#2974)
This PR adds WebGLTexture garbage collection, it looks for textures that haven't been drawn in a minute and removes them from the GPU. If they get drawn in the future they will be reloaded --------- Co-authored-by: Yorick van Klinken <[email protected]>
- Loading branch information
1 parent
8689e72
commit 385c061
Showing
9 changed files
with
381 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
<!doctype html> | ||
<html lang="en"> | ||
<head> | ||
<meta charset="UTF-8" /> | ||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
<title>Memory Leaker</title> | ||
</head> | ||
<body> | ||
<p>This creates a new bitmap every 200 ms and uploads it to the gpu</p> | ||
<p>You should see WebGL Texture cleanup messages in the console after the configured interval (1 minute)</p> | ||
<script src="../../lib/excalibur.js"></script> | ||
<script src="index.js"></script> | ||
</body> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
ex.Logger.getInstance().defaultLevel = ex.LogLevel.Debug; | ||
var game = new ex.Engine({ | ||
width: 1400, | ||
height: 1400, | ||
pixelRatio: 4, | ||
garbageCollection: true // should not crash chrome/ff after 10 minutes | ||
}); | ||
|
||
var actor = new ex.Actor({ | ||
anchor: ex.vec(0.5, 0.5), | ||
pos: game.screen.center, | ||
color: ex.Color.Red, | ||
radius: 5 | ||
}); | ||
|
||
actor.actions.repeatForever((ctx) => { | ||
ctx.moveBy(ex.vec(100, 100), 100); | ||
ctx.moveBy(ex.vec(-100, -100), 100); | ||
}); | ||
var ran = new ex.Random(1337); | ||
// var rectangles: ex.Rectangle[] = []; | ||
// for (let i = 0; i < 4; i++) { | ||
// const rect = new ex.Rectangle({ | ||
// width: 2000, | ||
// height: 2000, | ||
// color: new ex.Color(ran.integer(0, 255), ran.integer(0, 255), ran.integer(0, 255)) | ||
// }); | ||
// rectangles.push(rect); | ||
// } | ||
|
||
actor.onInitialize = () => { | ||
ex.coroutine(function* () { | ||
let index = 0; | ||
// actor.graphics.use(rectangles[index]); | ||
while (true) { | ||
yield 500; | ||
// yield 2_000; | ||
// actor.graphics.use(rectangles[index++ % rectangles.length]); | ||
actor.graphics.use( | ||
new ex.Rectangle({ | ||
width: 2000, | ||
height: 2000, | ||
color: new ex.Color(ran.integer(0, 255), ran.integer(0, 255), ran.integer(0, 255)) | ||
}) | ||
); | ||
} | ||
}); | ||
}; | ||
|
||
game.start(); | ||
game.add(actor); | ||
|
||
actor.angularVelocity = 2; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
export interface GarbageCollectionOptions { | ||
/** | ||
* Textures that aren't drawn after a certain number of milliseconds are unloaded from the GPU | ||
* Default 60_000 ms | ||
*/ | ||
textureCollectInterval?: number; // default 60_000 ms | ||
// TODO future work to integrate the font and text configuration, refactor existing collection mechanism | ||
// /** | ||
// * Font pre-renders that aren't drawn after a certain number of milliseconds are unloaded from the GPU | ||
// * Default 60_000 ms | ||
// */ | ||
// fontCollectInterval: number; // default 60_000 ms | ||
// /** | ||
// * Text measurements that aren't used after a certain number of milliseconds are unloaded from the GPU | ||
// * Default 60_000 ms | ||
// */ | ||
// textMeasurementCollectInterval: number; // default 60_000 ms | ||
} | ||
|
||
export const DefaultGarbageCollectionOptions: GarbageCollectionOptions = { | ||
textureCollectInterval: 60_000 | ||
// TODO future work to integrate the font and text configuration, refactor existing collection mechanism | ||
// fontCollectInterval: 60_000, | ||
// textMeasurementCollectInterval: 60_000, | ||
}; | ||
|
||
export interface GarbageCollectorOptions { | ||
/** | ||
* Returns a timestamp in milliseconds representing now | ||
*/ | ||
getTimestamp: () => number; | ||
} | ||
|
||
export class GarbageCollector { | ||
private _collectHandle: number; | ||
private _running = false; | ||
private _collectionMap = new Map<any, [type: string, time: number]>(); | ||
private _collectors = new Map<string, [(resource: any) => boolean, interval: number]>(); | ||
|
||
constructor(public options: GarbageCollectorOptions) {} | ||
|
||
/** | ||
* | ||
* @param type Resource type | ||
* @param timeoutInterval If resource type exceeds interval in milliseconds collect() is called | ||
* @param collect Collection implementation, returns true if collected | ||
*/ | ||
registerCollector(type: string, timeoutInterval: number, collect: (resource: any) => boolean) { | ||
this._collectors.set(type, [collect, timeoutInterval]); | ||
} | ||
|
||
/** | ||
* Add a resource to be tracked for collection | ||
* @param type | ||
* @param resource | ||
*/ | ||
addCollectableResource(type: string, resource: any) { | ||
this._collectionMap.set(resource, [type, this.options.getTimestamp()]); | ||
} | ||
|
||
/** | ||
* Update the resource last used timestamp preventing collection | ||
* @param resource | ||
*/ | ||
touch(resource: any) { | ||
const collectionData = this._collectionMap.get(resource); | ||
if (collectionData) { | ||
this._collectionMap.set(resource, [collectionData[0], this.options.getTimestamp()]); | ||
} | ||
} | ||
|
||
/** | ||
* Runs the collection loop to cleanup any stale resources given the registered collect handlers | ||
*/ | ||
public collectStaleResources = (deadline?: IdleDeadline) => { | ||
if (!this._running) { | ||
return; | ||
} | ||
for (const [type, [collector, timeoutInterval]] of this._collectors.entries()) { | ||
const now = this.options.getTimestamp(); | ||
for (const [resource, [resourceType, time]] of this._collectionMap.entries()) { | ||
if (type !== resourceType || time + timeoutInterval >= now) { | ||
continue; | ||
} | ||
|
||
const collected = collector(resource); | ||
if (collected) { | ||
this._collectionMap.delete(resource); | ||
} | ||
} | ||
} | ||
|
||
this._collectHandle = requestIdleCallback(this.collectStaleResources); | ||
}; | ||
|
||
/** | ||
* Force collect all resources, useful for shutting down a game | ||
* or if you know that you will not use anything you've allocated before now | ||
*/ | ||
public forceCollectAll() { | ||
for (const [_, [collector]] of this._collectors.entries()) { | ||
for (const [resource] of this._collectionMap.entries()) { | ||
const collected = collector(resource); | ||
if (collected) { | ||
this._collectionMap.delete(resource); | ||
} | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Starts the garbage collection loop | ||
*/ | ||
start() { | ||
this._running = true; | ||
this.collectStaleResources(); | ||
} | ||
|
||
/** | ||
* Stops the garbage collection loop | ||
*/ | ||
stop() { | ||
this._running = false; | ||
cancelIdleCallback(this._collectHandle); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.