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.
pull/1286/head
PFedak 3 years ago committed by GitHub
parent 2b4eb6771f
commit 6f56d77535
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -167,4 +167,25 @@ export class BufferMaintainer {
});
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;
}
}

@ -55,6 +55,7 @@ export const globalConfig = {
// Map
mapChunkSize: 16,
chunkAggregateSize: 4,
mapChunkOverviewMinZoom: 0.9,
mapChunkWorldSize: null, // COMPUTED

@ -3,6 +3,7 @@ import { Vector } from "../core/vector";
import { BasicSerializableObject, types } from "../savegame/serialization";
import { BaseItem } from "./base_item";
import { Entity } from "./entity";
import { MapChunkAggregate } from "./map_chunk_aggregate";
import { MapChunkView } from "./map_chunk_view";
import { GameRoot } from "./root";
@ -31,6 +32,11 @@ export class BaseMap extends BasicSerializableObject {
* Mapping of 'X|Y' to chunk
* @type {Map<string, MapChunkView>} */
this.chunksById = new Map();
/**
* Mapping of 'X|Y' to chunk aggregate
* @type {Map<string, MapChunkAggregate>} */
this.aggregatesById = new Map();
}
/**
@ -55,6 +61,39 @@ export class BaseMap extends BasicSerializableObject {
return null;
}
/**
* Returns the chunk aggregate containing a given chunk
* @param {number} chunkX
* @param {number} chunkY
*/
getAggregateForChunk(chunkX, chunkY, createIfNotExistent = false) {
const aggX = Math.floor(chunkX / globalConfig.chunkAggregateSize);
const aggY = Math.floor(chunkY / globalConfig.chunkAggregateSize);
return this.getAggregate(aggX, aggY, createIfNotExistent);
}
/**
* Returns the given chunk aggregate by index
* @param {number} aggX
* @param {number} aggY
*/
getAggregate(aggX, aggY, createIfNotExistent = false) {
const aggIdentifier = aggX + "|" + aggY;
let storedAggregate;
if ((storedAggregate = this.aggregatesById.get(aggIdentifier))) {
return storedAggregate;
}
if (createIfNotExistent) {
const instance = new MapChunkAggregate(this.root, aggX, aggY);
this.aggregatesById.set(aggIdentifier, instance);
return instance;
}
return null;
}
/**
* Gets or creates a new chunk if not existent for the given tile
* @param {number} tileX

@ -0,0 +1,146 @@
import { globalConfig } from "../core/config";
import { DrawParameters } from "../core/draw_parameters";
import { MapChunk } from "./map_chunk";
import { GameRoot } from "./root";
import { drawSpriteClipped } from "../core/draw_utils";
export const CHUNK_OVERLAY_RES = 3;
export class MapChunkAggregate {
/**
*
* @param {GameRoot} root
* @param {number} x
* @param {number} y
*/
constructor(root, x, y) {
this.root = root;
this.x = x;
this.y = y;
/**
* Whenever something changes, we increase this number - so we know we need to redraw
*/
this.renderIteration = 0;
this.dirty = false;
/** @type {Array<boolean>} */
this.dirtyList = new Array(globalConfig.chunkAggregateSize ** 2).fill(true);
this.markDirty(0, 0);
}
/**
* Marks this chunk as dirty, rerendering all caches
* @param {number} chunkX
* @param {number} chunkY
*/
markDirty(chunkX, chunkY) {
const relX = chunkX % globalConfig.chunkAggregateSize;
const relY = chunkY % globalConfig.chunkAggregateSize;
this.dirtyList[relY * globalConfig.chunkAggregateSize + relX] = true;
if (this.dirty) {
return;
}
this.dirty = true;
++this.renderIteration;
this.renderKey = this.x + "/" + this.y + "@" + this.renderIteration;
}
/**
*
* @param {HTMLCanvasElement} canvas
* @param {CanvasRenderingContext2D} context
* @param {number} w
* @param {number} h
* @param {number} dpi
*/
generateOverlayBuffer(canvas, context, w, h, dpi) {
const prevKey = this.x + "/" + this.y + "@" + (this.renderIteration - 1);
const prevBuffer = this.root.buffers.getForKeyOrNullNoUpdate({
key: "agg@" + this.root.currentLayer,
subKey: prevKey,
});
const overlaySize = globalConfig.mapChunkSize * CHUNK_OVERLAY_RES;
let onlyDirty = false;
if (prevBuffer) {
context.drawImage(prevBuffer, 0, 0);
onlyDirty = true;
}
for (let x = 0; x < globalConfig.chunkAggregateSize; x++) {
for (let y = 0; y < globalConfig.chunkAggregateSize; y++) {
if (onlyDirty && !this.dirtyList[globalConfig.chunkAggregateSize * y + x]) continue;
this.root.map
.getChunk(
this.x * globalConfig.chunkAggregateSize + x,
this.y * globalConfig.chunkAggregateSize + y,
true
)
.generateOverlayBuffer(
context,
overlaySize,
overlaySize,
x * overlaySize,
y * overlaySize
);
}
}
this.dirty = false;
this.dirtyList.fill(false);
}
/**
* Overlay
* @param {DrawParameters} parameters
*/
drawOverlay(parameters) {
const aggregateOverlaySize =
globalConfig.mapChunkSize * globalConfig.chunkAggregateSize * CHUNK_OVERLAY_RES;
const sprite = this.root.buffers.getForKey({
key: "agg@" + this.root.currentLayer,
subKey: this.renderKey,
w: aggregateOverlaySize,
h: aggregateOverlaySize,
dpi: 1,
redrawMethod: this.generateOverlayBuffer.bind(this),
});
const dims = globalConfig.mapChunkWorldSize * globalConfig.chunkAggregateSize;
const extrude = 0.05;
// Draw chunk "pixel" art
parameters.context.imageSmoothingEnabled = false;
drawSpriteClipped({
parameters,
sprite,
x: this.x * dims - extrude,
y: this.y * dims - extrude,
w: dims + 2 * extrude,
h: dims + 2 * extrude,
originalW: aggregateOverlaySize,
originalH: aggregateOverlaySize,
});
parameters.context.imageSmoothingEnabled = true;
const resourcesScale = this.root.app.settings.getAllSettings().mapResourcesScale;
// Draw patch items
if (this.root.currentLayer === "regular" && resourcesScale > 0.05) {
const diameter = (70 / Math.pow(parameters.zoomLevel, 0.35)) * (0.2 + 2 * resourcesScale);
for (let x = 0; x < globalConfig.chunkAggregateSize; x++) {
for (let y = 0; y < globalConfig.chunkAggregateSize; y++) {
this.root.map
.getChunk(this.x + x, this.y + y, true)
.drawOverlayPatches(
parameters,
this.x * dims + x * globalConfig.mapChunkSize * CHUNK_OVERLAY_RES,
this.y * dims + y * globalConfig.mapChunkSize * CHUNK_OVERLAY_RES,
diameter
);
}
}
}
}
}

@ -33,6 +33,7 @@ export class MapChunkView extends MapChunk {
markDirty() {
++this.renderIteration;
this.renderKey = this.x + "/" + this.y + "@" + this.renderIteration;
this.root.map.getAggregateForChunk(this.x, this.y, true).markDirty(this.x, this.y);
}
/**
@ -82,73 +83,41 @@ export class MapChunkView extends MapChunk {
}
/**
* Overlay
* @param {DrawParameters} parameters
* @param {number} xoffs
* @param {number} yoffs
* @param {number} diameter
*/
drawOverlay(parameters) {
const overlaySize = globalConfig.mapChunkSize * CHUNK_OVERLAY_RES;
const sprite = this.root.buffers.getForKey({
key: "chunk@" + this.root.currentLayer,
subKey: this.renderKey,
w: overlaySize,
h: overlaySize,
dpi: 1,
redrawMethod: this.generateOverlayBuffer.bind(this),
});
const dims = globalConfig.mapChunkWorldSize;
const extrude = 0.05;
// Draw chunk "pixel" art
parameters.context.imageSmoothingEnabled = false;
drawSpriteClipped({
parameters,
sprite,
x: this.x * dims - extrude,
y: this.y * dims - extrude,
w: dims + 2 * extrude,
h: dims + 2 * extrude,
originalW: overlaySize,
originalH: overlaySize,
});
parameters.context.imageSmoothingEnabled = true;
const resourcesScale = this.root.app.settings.getAllSettings().mapResourcesScale;
// Draw patch items
if (this.root.currentLayer === "regular" && resourcesScale > 0.05) {
const diameter = (70 / Math.pow(parameters.zoomLevel, 0.35)) * (0.2 + 2 * resourcesScale);
for (let i = 0; i < this.patches.length; ++i) {
const patch = this.patches[i];
if (patch.item.getItemType() === "shape") {
const destX = this.x * dims + patch.pos.x * globalConfig.tileSize;
const destY = this.y * dims + patch.pos.y * globalConfig.tileSize;
patch.item.drawItemCenteredClipped(destX, destY, parameters, diameter);
}
drawOverlayPatches(parameters, xoffs, yoffs, diameter) {
for (let i = 0; i < this.patches.length; ++i) {
const patch = this.patches[i];
if (patch.item.getItemType() === "shape") {
const destX = xoffs + patch.pos.x * globalConfig.tileSize;
const destY = yoffs + patch.pos.y * globalConfig.tileSize;
patch.item.drawItemCenteredClipped(destX, destY, parameters, diameter);
}
}
}
/**
*
* @param {HTMLCanvasElement} canvas
* @param {CanvasRenderingContext2D} context
* @param {number} w
* @param {number} h
* @param {number} dpi
* @param {number=} xoffs
* @param {number=} yoffs
*/
generateOverlayBuffer(canvas, context, w, h, dpi) {
generateOverlayBuffer(context, w, h, xoffs, yoffs) {
context.fillStyle =
this.containedEntities.length > 0
? THEME.map.chunkOverview.filled
: THEME.map.chunkOverview.empty;
context.fillRect(0, 0, w, h);
context.fillRect(xoffs, yoffs, w, h);
if (this.root.app.settings.getAllSettings().displayChunkBorders) {
context.fillStyle = THEME.map.chunkBorders;
context.fillRect(0, 0, w, 1);
context.fillRect(0, 1, 1, h);
context.fillRect(xoffs, yoffs, w, 1);
context.fillRect(xoffs, yoffs + 1, 1, h);
}
for (let x = 0; x < globalConfig.mapChunkSize; ++x) {
@ -174,8 +143,8 @@ export class MapChunkView extends MapChunk {
if (lowerContent) {
context.fillStyle = lowerContent.getBackgroundColorAsResource();
context.fillRect(
x * CHUNK_OVERLAY_RES,
y * CHUNK_OVERLAY_RES,
xoffs + x * CHUNK_OVERLAY_RES,
yoffs + y * CHUNK_OVERLAY_RES,
CHUNK_OVERLAY_RES,
CHUNK_OVERLAY_RES
);
@ -190,8 +159,8 @@ export class MapChunkView extends MapChunk {
const isFilled = overlayMatrix[dx + dy * 3];
if (isFilled) {
context.fillRect(
x * CHUNK_OVERLAY_RES + dx,
y * CHUNK_OVERLAY_RES + dy,
xoffs + x * CHUNK_OVERLAY_RES + dx,
yoffs + y * CHUNK_OVERLAY_RES + dy,
1,
1
);
@ -206,8 +175,8 @@ export class MapChunkView extends MapChunk {
data.rotationVariant
);
context.fillRect(
x * CHUNK_OVERLAY_RES,
y * CHUNK_OVERLAY_RES,
xoffs + x * CHUNK_OVERLAY_RES,
yoffs + y * CHUNK_OVERLAY_RES,
CHUNK_OVERLAY_RES,
CHUNK_OVERLAY_RES
);
@ -220,8 +189,8 @@ export class MapChunkView extends MapChunk {
if (lowerContent) {
context.fillStyle = lowerContent.getBackgroundColorAsResource();
context.fillRect(
x * CHUNK_OVERLAY_RES,
y * CHUNK_OVERLAY_RES,
xoffs + x * CHUNK_OVERLAY_RES,
yoffs + y * CHUNK_OVERLAY_RES,
CHUNK_OVERLAY_RES,
CHUNK_OVERLAY_RES
);
@ -233,7 +202,7 @@ export class MapChunkView extends MapChunk {
// Draw wires overlay
context.fillStyle = THEME.map.wires.overlayColor;
context.fillRect(0, 0, w, h);
context.fillRect(xoffs, yoffs, w, h);
for (let x = 0; x < globalConfig.mapChunkSize; ++x) {
const wiresArray = this.wireContents[x];
@ -244,8 +213,8 @@ export class MapChunkView extends MapChunk {
}
MapChunkView.drawSingleWiresOverviewTile({
context,
x: x * CHUNK_OVERLAY_RES,
y: y * CHUNK_OVERLAY_RES,
x: xoffs + x * CHUNK_OVERLAY_RES,
y: yoffs + y * CHUNK_OVERLAY_RES,
entity: content,
tileSizePixels: CHUNK_OVERLAY_RES,
});

@ -5,6 +5,7 @@ import { freeCanvas, makeOffscreenBuffer } from "../core/buffer_utils";
import { Entity } from "./entity";
import { THEME } from "./theme";
import { MapChunkView } from "./map_chunk_view";
import { MapChunkAggregate } from "./map_chunk_aggregate";
/**
* This is the view of the map, it extends the map which is the raw model and allows
@ -164,6 +165,40 @@ export class MapView extends BaseMap {
}
}
/**
* Calls a given method on all given chunks
* @param {DrawParameters} parameters
* @param {function} method
*/
drawVisibleAggregates(parameters, method) {
const cullRange = parameters.visibleRect.allScaled(1 / globalConfig.tileSize);
const top = cullRange.top();
const right = cullRange.right();
const bottom = cullRange.bottom();
const left = cullRange.left();
const border = 0;
const minY = top - border;
const maxY = bottom + border;
const minX = left - border;
const maxX = right + border;
const aggregateTiles = globalConfig.chunkAggregateSize * globalConfig.mapChunkSize;
const aggStartX = Math.floor(minX / aggregateTiles);
const aggStartY = Math.floor(minY / aggregateTiles);
const aggEndX = Math.floor(maxX / aggregateTiles);
const aggEndY = Math.floor(maxY / aggregateTiles);
// Render y from top down for proper blending
for (let aggX = aggStartX; aggX <= aggEndX; ++aggX) {
for (let aggY = aggStartY; aggY <= aggEndY; ++aggY) {
const aggregate = this.root.map.getAggregate(aggX, aggY, true);
method.call(aggregate, parameters);
}
}
}
/**
* Draws the wires foreground
* @param {DrawParameters} parameters
@ -177,7 +212,7 @@ export class MapView extends BaseMap {
* @param {DrawParameters} parameters
*/
drawOverlay(parameters) {
this.drawVisibleChunks(parameters, MapChunkView.prototype.drawOverlay);
this.drawVisibleAggregates(parameters, MapChunkAggregate.prototype.drawOverlay);
}
/**

Loading…
Cancel
Save