Improve buffer backlog performance, should speed up whole game

This commit is contained in:
tobspr 2020-09-19 09:28:29 +02:00
parent 14350721eb
commit 7bc45d8959
3 changed files with 117 additions and 65 deletions

View File

@ -13,7 +13,7 @@ import { round1Digit } from "./utils";
const logger = createLogger("buffers");
const bufferGcDurationSeconds = 5;
const bufferGcDurationSeconds = 0.5;
export class BufferMaintainer {
/**

View File

@ -25,17 +25,43 @@ export function disableImageSmoothing(context) {
context.webkitImageSmoothingEnabled = false;
}
const registeredCanvas = [];
const freeCanvasList = [];
/**
* @typedef {{
* canvas: HTMLCanvasElement,
* context: CanvasRenderingContext2D
* }} CanvasCacheEntry
*/
let vramUsage = 0;
let bufferCount = 0;
/**
* @type {Array<CanvasCacheEntry>}
*/
const registeredCanvas = [];
/**
* Buckets for each width * height combination
* @type {Map<number, Array<CanvasCacheEntry>>}
*/
const freeCanvasBuckets = new Map();
/**
* Track statistics
*/
const stats = {
vramUsage: 0,
backlogVramUsage: 0,
bufferCount: 0,
numReused: 0,
numCreated: 0,
};
/**
*
* @param {HTMLCanvasElement} canvas
*/
export function getBufferVramUsageBytes(canvas) {
assert(canvas, "no canvas given");
assert(Number.isFinite(canvas.width), "bad canvas width: " + canvas.width);
assert(Number.isFinite(canvas.height), "bad canvas height" + canvas.height);
return canvas.width * canvas.height * 4;
}
@ -43,17 +69,31 @@ export function getBufferVramUsageBytes(canvas) {
* Returns stats on the allocated buffers
*/
export function getBufferStats() {
let numBuffersFree = 0;
freeCanvasBuckets.forEach(bucket => {
numBuffersFree += bucket.length;
});
return {
vramUsage,
bufferCount,
backlog: freeCanvasList.length,
...stats,
backlogKeys: freeCanvasBuckets.size,
backlogSize: numBuffersFree,
};
}
/**
* Clears the backlog buffers if they grew too much
*/
export function clearBufferBacklog() {
while (freeCanvasList.length > 50) {
freeCanvasList.pop();
}
freeCanvasBuckets.forEach(bucket => {
while (bucket.length > 500) {
const entry = bucket[bucket.length - 1];
stats.backlogVramUsage -= getBufferVramUsageBytes(entry.canvas);
delete entry.canvas;
delete entry.context;
bucket.pop();
}
});
}
/**
@ -84,53 +124,29 @@ export function makeOffscreenBuffer(w, h, { smooth = true, reusable = true, labe
let canvas = null;
let context = null;
let bestMatchingOne = null;
let bestMatchingPixelsDiff = 1e50;
const currentPixels = w * h;
// Ok, search in cache first
for (let i = 0; i < freeCanvasList.length; ++i) {
const { canvas: useableCanvas, context: useableContext } = freeCanvasList[i];
const bucket = freeCanvasBuckets.get(w * h) || [];
for (let i = 0; i < bucket.length; ++i) {
const { canvas: useableCanvas, context: useableContext } = bucket[i];
if (useableCanvas.width === w && useableCanvas.height === h) {
// Ok we found one
canvas = useableCanvas;
context = useableContext;
fastArrayDelete(freeCanvasList, i);
// Restore past state
context.restore();
context.save();
context.clearRect(0, 0, canvas.width, canvas.height);
delete canvas.style.width;
delete canvas.style.height;
stats.numReused++;
stats.backlogVramUsage -= getBufferVramUsageBytes(canvas);
fastArrayDelete(bucket, i);
break;
}
const otherPixels = useableCanvas.width * useableCanvas.height;
const diff = Math.abs(otherPixels - currentPixels);
if (diff < bestMatchingPixelsDiff) {
bestMatchingPixelsDiff = diff;
bestMatchingOne = {
canvas: useableCanvas,
context: useableContext,
index: i,
};
}
}
// Ok none matching, reuse one though
if (!canvas && bestMatchingOne) {
canvas = bestMatchingOne.canvas;
context = bestMatchingOne.context;
canvas.width = w;
canvas.height = h;
fastArrayDelete(freeCanvasList, bestMatchingOne.index);
}
// Reset context
if (context) {
// Restore past state
context.restore();
context.save();
context.clearRect(0, 0, canvas.width, canvas.height);
delete canvas.style.width;
delete canvas.style.height;
}
// None found , create new one
@ -138,6 +154,8 @@ export function makeOffscreenBuffer(w, h, { smooth = true, reusable = true, labe
canvas = document.createElement("canvas");
context = canvas.getContext("2d" /*, { alpha } */);
stats.numCreated++;
canvas.width = w;
canvas.height = h;
@ -145,6 +163,7 @@ export function makeOffscreenBuffer(w, h, { smooth = true, reusable = true, labe
context.save();
}
// @ts-ignore
canvas.label = label;
if (smooth) {
@ -167,8 +186,9 @@ export function makeOffscreenBuffer(w, h, { smooth = true, reusable = true, labe
export function registerCanvas(canvas, context) {
registeredCanvas.push({ canvas, context });
bufferCount += 1;
vramUsage += getBufferVramUsageBytes(canvas);
stats.bufferCount += 1;
const bytesUsed = getBufferVramUsageBytes(canvas);
stats.vramUsage += bytesUsed;
}
/**
@ -180,6 +200,7 @@ export function freeCanvas(canvas) {
let index = -1;
let data = null;
for (let i = 0; i < registeredCanvas.length; ++i) {
if (registeredCanvas[i].canvas === canvas) {
index = i;
@ -193,8 +214,18 @@ export function freeCanvas(canvas) {
return;
}
fastArrayDelete(registeredCanvas, index);
freeCanvasList.push(data);
bufferCount -= 1;
vramUsage -= getBufferVramUsageBytes(canvas);
const key = canvas.width * canvas.height;
const bucket = freeCanvasBuckets.get(key);
if (bucket) {
bucket.push(data);
} else {
freeCanvasBuckets.set(key, [data]);
}
stats.bufferCount -= 1;
const bytesUsed = getBufferVramUsageBytes(canvas);
stats.vramUsage -= bytesUsed;
stats.backlogVramUsage += bytesUsed;
}

View File

@ -2,7 +2,12 @@
import { Application } from "../application";
/* typehints:end */
import { BufferMaintainer } from "../core/buffer_maintainer";
import { disableImageSmoothing, enableImageSmoothing, registerCanvas } from "../core/buffer_utils";
import {
disableImageSmoothing,
enableImageSmoothing,
getBufferStats,
registerCanvas,
} from "../core/buffer_utils";
import { globalConfig } from "../core/config";
import { getDeviceDPI, resizeHighDPICanvas } from "../core/dpi_manager";
import { DrawParameters } from "../core/draw_parameters";
@ -219,9 +224,6 @@ export class GameCore {
lastContext.clearRect(0, 0, lastCanvas.width, lastCanvas.height);
}
// globalConfig.smoothing.smoothMainCanvas = getDeviceDPI() < 1.5;
// globalConfig.smoothing.smoothMainCanvas = true;
canvas.classList.toggle("smoothed", globalConfig.smoothing.smoothMainCanvas);
// Oof, use :not() instead
@ -374,9 +376,9 @@ export class GameCore {
(zoomLevel / globalConfig.assetsDpi) * getDeviceDPI() * globalConfig.assetsSharpness;
let desiredAtlasScale = "0.25";
if (effectiveZoomLevel > 0.8 && !lowQuality) {
if (effectiveZoomLevel > 0.5 && !lowQuality) {
desiredAtlasScale = ORIGINAL_SPRITE_SCALE;
} else if (effectiveZoomLevel > 0.4 && !lowQuality) {
} else if (effectiveZoomLevel > 0.35 && !lowQuality) {
desiredAtlasScale = "0.5";
}
@ -500,18 +502,37 @@ export class GameCore {
);
const stats = this.root.buffers.getStats();
context.fillText(
"Buffers: " +
"Maintained Buffers: " +
stats.rootKeys +
" root keys, " +
" root keys / " +
stats.subKeys +
" sub keys / buffers / VRAM: " +
" buffers / VRAM: " +
round2Digits(stats.vramBytes / (1024 * 1024)) +
" MB",
20,
620
);
const internalStats = getBufferStats();
context.fillText(
"Total Buffers: " +
internalStats.bufferCount +
" buffers / " +
internalStats.backlogSize +
" backlog / " +
internalStats.backlogKeys +
" keys in backlog / VRAM " +
round2Digits(internalStats.vramUsage / (1024 * 1024)) +
" MB / Backlog " +
round2Digits(internalStats.backlogVramUsage / (1024 * 1024)) +
" MB / Created " +
internalStats.numCreated +
" / Reused " +
internalStats.numReused,
20,
640
);
}
if (G_IS_DEV && globalConfig.debug.testClipping) {