mirror of
https://github.com/tobspr/shapez.io.git
synced 2025-12-17 04:01:51 +00:00
Overlay rendering performance seemed bottlenecked by drawImage calls. To reduce both the number of calls and the number of different source buffers, cache overlay buffers for squares of chunks. This adds a very small extra cost for updates (one additional drawImage) and some cost for drawing chunks outside of view, but this is more than made up for by the savings. By default, the aggregate are 4x4 squares of chunks.
192 lines
5.1 KiB
JavaScript
192 lines
5.1 KiB
JavaScript
import { GameRoot } from "../game/root";
|
|
import { clearBufferBacklog, freeCanvas, getBufferStats, makeOffscreenBuffer } from "./buffer_utils";
|
|
import { createLogger } from "./logging";
|
|
import { round1Digit } from "./utils";
|
|
|
|
/**
|
|
* @typedef {{
|
|
* canvas: HTMLCanvasElement,
|
|
* context: CanvasRenderingContext2D,
|
|
* lastUse: number,
|
|
* }} CacheEntry
|
|
*/
|
|
|
|
const logger = createLogger("buffers");
|
|
|
|
const bufferGcDurationSeconds = 0.5;
|
|
|
|
export class BufferMaintainer {
|
|
/**
|
|
* @param {GameRoot} root
|
|
*/
|
|
constructor(root) {
|
|
this.root = root;
|
|
|
|
/** @type {Map<string, Map<string, CacheEntry>>} */
|
|
this.cache = new Map();
|
|
|
|
this.iterationIndex = 1;
|
|
this.lastIteration = 0;
|
|
|
|
this.root.signals.gameFrameStarted.add(this.update, this);
|
|
}
|
|
|
|
/**
|
|
* Returns the buffer stats
|
|
*/
|
|
getStats() {
|
|
let stats = {
|
|
rootKeys: 0,
|
|
subKeys: 0,
|
|
vramBytes: 0,
|
|
};
|
|
this.cache.forEach((subCache, key) => {
|
|
++stats.rootKeys;
|
|
|
|
subCache.forEach((cacheEntry, subKey) => {
|
|
++stats.subKeys;
|
|
|
|
const canvas = cacheEntry.canvas;
|
|
stats.vramBytes += canvas.width * canvas.height * 4;
|
|
});
|
|
});
|
|
|
|
return stats;
|
|
}
|
|
|
|
/**
|
|
* Goes to the next buffer iteration, clearing all buffers which were not used
|
|
* for a few iterations
|
|
*/
|
|
garbargeCollect() {
|
|
let totalKeys = 0;
|
|
let deletedKeys = 0;
|
|
const minIteration = this.iterationIndex;
|
|
|
|
this.cache.forEach((subCache, key) => {
|
|
let unusedSubKeys = [];
|
|
|
|
// Filter sub cache
|
|
subCache.forEach((cacheEntry, subKey) => {
|
|
if (cacheEntry.lastUse < minIteration) {
|
|
unusedSubKeys.push(subKey);
|
|
freeCanvas(cacheEntry.canvas);
|
|
++deletedKeys;
|
|
} else {
|
|
++totalKeys;
|
|
}
|
|
});
|
|
|
|
// Delete unused sub keys
|
|
for (let i = 0; i < unusedSubKeys.length; ++i) {
|
|
subCache.delete(unusedSubKeys[i]);
|
|
}
|
|
});
|
|
|
|
// Make sure our backlog never gets too big
|
|
clearBufferBacklog();
|
|
|
|
// if (G_IS_DEV) {
|
|
// const bufferStats = getBufferStats();
|
|
// const mbUsed = round1Digit(bufferStats.vramUsage / (1024 * 1024));
|
|
// logger.log(
|
|
// "GC: Remove",
|
|
// (deletedKeys + "").padStart(4),
|
|
// ", Remain",
|
|
// (totalKeys + "").padStart(4),
|
|
// "(",
|
|
// (bufferStats.bufferCount + "").padStart(4),
|
|
// "total",
|
|
// ")",
|
|
|
|
// "(",
|
|
// (bufferStats.backlogSize + "").padStart(4),
|
|
// "backlog",
|
|
// ")",
|
|
|
|
// "VRAM:",
|
|
// mbUsed,
|
|
// "MB"
|
|
// );
|
|
// }
|
|
|
|
++this.iterationIndex;
|
|
}
|
|
|
|
update() {
|
|
const now = this.root.time.realtimeNow();
|
|
if (now - this.lastIteration > bufferGcDurationSeconds) {
|
|
this.lastIteration = now;
|
|
this.garbargeCollect();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {object} param0
|
|
* @param {string} param0.key
|
|
* @param {string} param0.subKey
|
|
* @param {number} param0.w
|
|
* @param {number} param0.h
|
|
* @param {number} param0.dpi
|
|
* @param {function(HTMLCanvasElement, CanvasRenderingContext2D, number, number, number, object?) : void} param0.redrawMethod
|
|
* @param {object=} param0.additionalParams
|
|
* @returns {HTMLCanvasElement}
|
|
*
|
|
*/
|
|
getForKey({ key, subKey, w, h, dpi, redrawMethod, additionalParams }) {
|
|
// First, create parent key
|
|
let parent = this.cache.get(key);
|
|
if (!parent) {
|
|
parent = new Map();
|
|
this.cache.set(key, parent);
|
|
}
|
|
|
|
// Now search for sub key
|
|
const cacheHit = parent.get(subKey);
|
|
if (cacheHit) {
|
|
cacheHit.lastUse = this.iterationIndex;
|
|
return cacheHit.canvas;
|
|
}
|
|
|
|
// Need to generate new buffer
|
|
const effectiveWidth = w * dpi;
|
|
const effectiveHeight = h * dpi;
|
|
|
|
const [canvas, context] = makeOffscreenBuffer(effectiveWidth, effectiveHeight, {
|
|
reusable: true,
|
|
label: "buffer-" + key + "/" + subKey,
|
|
smooth: true,
|
|
});
|
|
|
|
redrawMethod(canvas, context, w, h, dpi, additionalParams);
|
|
|
|
parent.set(subKey, {
|
|
canvas,
|
|
context,
|
|
lastUse: this.iterationIndex,
|
|
});
|
|
return canvas;
|
|
}
|
|
|
|
/**
|
|
* @param {object} param0
|
|
* @param {string} param0.key
|
|
* @param {string} param0.subKey
|
|
* @returns {HTMLCanvasElement?}
|
|
*
|
|
*/
|
|
getForKeyOrNullNoUpdate({ key, subKey }) {
|
|
let parent = this.cache.get(key);
|
|
if (!parent) {
|
|
return null;
|
|
}
|
|
|
|
// Now search for sub key
|
|
const cacheHit = parent.get(subKey);
|
|
if (cacheHit) {
|
|
return cacheHit.canvas;
|
|
}
|
|
return null;
|
|
}
|
|
}
|