diff --git a/src/js/core/buffer_maintainer.js b/src/js/core/buffer_maintainer.js index 3d466f14..6da69b30 100644 --- a/src/js/core/buffer_maintainer.js +++ b/src/js/core/buffer_maintainer.js @@ -13,7 +13,7 @@ import { round1Digit } from "./utils"; const logger = createLogger("buffers"); -const bufferGcDurationSeconds = 5; +const bufferGcDurationSeconds = 0.5; export class BufferMaintainer { /** diff --git a/src/js/core/buffer_utils.js b/src/js/core/buffer_utils.js index 228560bc..310c315f 100644 --- a/src/js/core/buffer_utils.js +++ b/src/js/core/buffer_utils.js @@ -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} + */ +const registeredCanvas = []; + +/** + * Buckets for each width * height combination + * @type {Map>} + */ +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; } diff --git a/src/js/game/core.js b/src/js/game/core.js index 642d8d9d..6021cc66 100644 --- a/src/js/game/core.js +++ b/src/js/game/core.js @@ -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) {