1
0
mirror of https://github.com/tobspr/shapez.io.git synced 2025-12-17 04:01:51 +00:00
tobspr_shapez.io/src/js/core/buffer_maintainer.js
PFedak 6f56d77535
Aggregate map chunks in overlay. (#1247)
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.
2021-08-25 13:04:52 +02:00

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;
}
}