Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
bcd1412
refactor: restructure WebGPU territory renderer into extensible pass-…
scamiv Jan 16, 2026
c666a84
move terrain color computation to GPU compute shader
scamiv Jan 16, 2026
a16ef67
refactor: optimize terrain recomputation in TerritoryRenderer
scamiv Jan 16, 2026
b1898c7
refactor: update workgroup size in compute shader and dispatch logic
scamiv Jan 16, 2026
4463f1a
refactor: optimize terrain color extraction in GroundTruthData
scamiv Jan 16, 2026
efb99cc
simplify defended territory rendering logic
scamiv Jan 17, 2026
f90b3e4
replace defended epoch stamping with defended-strength field
scamiv Jan 17, 2026
6e43d1b
Switched loadShader() to a Vite-bundled static shader map using impor…
scamiv Jan 17, 2026
d2559dc
border test 9000
scamiv Jan 18, 2026
34886ba
fix border mode selection
scamiv Jan 18, 2026
6118a77
Add WebGPU Debug Overlay and Shader Management
scamiv Jan 18, 2026
87d4838
Add WebGPU Debug Overlay to prod index.html
scamiv Jan 18, 2026
ac0a7b6
add temporal smoothing for territory rendering
scamiv Jan 19, 2026
20e3a91
adjusted defaults
scamiv Jan 19, 2026
b543171
Add improved terrain compute shaders with lite and heavy variants
scamiv Jan 20, 2026
db11112
Update WebGPUDebugOverlay section title and adjust terrain shader def…
scamiv Jan 20, 2026
7f799d0
Update terrain shader parameters
scamiv Jan 20, 2026
a03e09a
flawed
scamiv Jan 25, 2026
0b9f67c
flawed but "working"
scamiv Feb 1, 2026
848697e
this isnt getting good soon
scamiv Feb 2, 2026
9ef4f18
Enhance WorkerCanvas2DRenderer with terrain handling improvements
scamiv Feb 2, 2026
f109a3e
Add relations management to GroundTruthData and update Worker components
scamiv Feb 2, 2026
3d31cd8
fix close
scamiv Feb 2, 2026
6f96cab
Worker renderers: decouple from Game/TerrainMap, coalesce view and sim
scamiv Feb 3, 2026
ee90da8
Worker rendering: backpressure render_frame + reduce relations rebuilds
scamiv Feb 3, 2026
45a8e33
Implement worker metrics and debugging events
scamiv Feb 3, 2026
aa25785
Optimize turn processing and enhance performance metrics handling
scamiv Feb 4, 2026
f134e5b
Enhance WorkerTerritoryRenderer with view management and frame dirty …
scamiv Feb 4, 2026
dbf2d34
Enhance performance metrics in rendering layers
scamiv Feb 4, 2026
978b7fc
Add offset retrieval methods and enhance TerritoryLayer canvas manage…
scamiv Feb 4, 2026
f6df2cc
Implement tile state change handling in Game and Worker components
scamiv Feb 4, 2026
3137d8b
fix: remove stray method stub from GameRunner after rebase
scamiv Feb 4, 2026
813c502
extract WorkerProfiler + remove dead helpers
scamiv Feb 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@
<multi-tab-modal></multi-tab-modal>
<game-left-sidebar></game-left-sidebar>
<performance-overlay></performance-overlay>
<webgpu-debug-overlay></webgpu-debug-overlay>
<player-info-overlay></player-info-overlay>
<leader-board></leader-board>
<team-stats></team-stats>
Expand Down
2 changes: 2 additions & 0 deletions resources/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,8 @@
"attack_ratio_desc": "What percentage of your troops to send in an attack (1–100%)",
"territory_patterns_label": "🏳️ Territory Skins",
"territory_patterns_desc": "Choose whether to display territory skin designs in game",
"territory_border_mode_label": "Territory Borders",
"territory_border_mode_desc": "Select border rendering style (visual only)",
"performance_overlay_label": "Performance Overlay",
"performance_overlay_desc": "Toggle the performance overlay. When enabled, the performance overlay will be displayed. Press shift-D during game to toggle.",
"easter_writing_speed_label": "Writing Speed Multiplier",
Expand Down
103 changes: 77 additions & 26 deletions src/client/ClientGameRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
PlayerCosmeticRefs,
PlayerRecord,
ServerMessage,
Turn,
} from "../core/Schemas";
import { createPartialGameRecord, replacer } from "../core/Util";
import { ServerConfig } from "../core/configuration/Config";
Expand All @@ -35,7 +36,9 @@ import {
InputHandler,
MouseMoveEvent,
MouseUpEvent,
SetWorkerDebugEvent,
TickMetricsEvent,
WorkerMetricsEvent,
} from "./InputHandler";
import { endGame, startGame, startTime } from "./LocalPersistantStats";
import { terrainMapFileLoader } from "./TerrainMapFileLoader";
Expand Down Expand Up @@ -226,6 +229,12 @@ async function createClientGame(
lobbyConfig.clientID,
);
await worker.initialize();
worker.onWorkerMetrics((metrics) => {
eventBus.emit(new WorkerMetricsEvent(metrics));
});
eventBus.on(SetWorkerDebugEvent, (event: SetWorkerDebugEvent) => {
worker.setWorkerDebug(event.config);
});
const gameView = new GameView(
worker,
config,
Expand Down Expand Up @@ -386,15 +395,6 @@ export class ClientGameRunner {
}
});

const worker = this.worker;
const keepWorkerAlive = () => {
if (this.isActive) {
worker.sendHeartbeat();
requestAnimationFrame(keepWorkerAlive);
}
};
requestAnimationFrame(keepWorkerAlive);

const onconnect = () => {
console.log("Connected to game server!");
this.transport.rejoinGame(this.turnsSeen);
Expand Down Expand Up @@ -436,20 +436,43 @@ export class ClientGameRunner {
goToPlayer();
}

for (const turn of message.turns) {
const normalizeTurn = (turn: Turn): Turn =>
this.gameView.config().isReplay()
? {
...turn,
intents: turn.intents.filter((i) => i.type !== "toggle_pause"),
}
: turn;

// Firefox in particular suffers from a storm of thousands of tiny
// postMessage() calls on reconnect. Batch turns to keep the worker
// event loop responsive for render_frame and sim scheduling.
const batchSize = 256;
let batch: Turn[] = [];
const flush = () => {
if (batch.length === 0) return;
this.worker.sendTurnBatch(batch);
batch = [];
};

for (const rawTurn of message.turns as Turn[]) {
const turn = normalizeTurn(rawTurn);
if (turn.turnNumber < this.turnsSeen) {
continue;
}
while (turn.turnNumber - 1 > this.turnsSeen) {
this.worker.sendTurn({
batch.push({
turnNumber: this.turnsSeen,
intents: [],
});
this.turnsSeen++;
if (batch.length >= batchSize) flush();
}
this.worker.sendTurn(turn);
batch.push(turn);
this.turnsSeen++;
if (batch.length >= batchSize) flush();
}
flush();
}
if (message.type === "desync") {
if (this.lobby.gameStartInfo === undefined) {
Expand Down Expand Up @@ -543,11 +566,19 @@ export class ClientGameRunner {
const tile = this.gameView.ref(cell.x, cell.y);
if (
this.gameView.isLand(tile) &&
!this.gameView.hasOwner(tile) &&
this.gameView.inSpawnPhase() &&
!this.gameView.config().isRandomSpawn()
) {
this.eventBus.emit(new SendSpawnIntentEvent(tile));
// Main thread no longer maintains authoritative tile ownership. Query the
// worker for spawn validation.
this.worker
.tileContext(tile)
.then((ctx) => {
if (!ctx.hasOwner) {
this.eventBus.emit(new SendSpawnIntentEvent(tile));
}
})
.catch((err) => console.warn("tileContext spawn lookup failed:", err));
return;
}
if (this.gameView.inSpawnPhase()) {
Expand All @@ -561,12 +592,22 @@ export class ClientGameRunner {
this.myPlayer.actions(tile).then((actions) => {
if (this.myPlayer === null) return;
if (actions.canAttack) {
this.eventBus.emit(
new SendAttackIntentEvent(
this.gameView.owner(tile).id(),
this.myPlayer.troops() * this.renderer.uiState.attackRatio,
),
);
this.worker
.tileContext(tile)
.then((ctx) => {
if (!this.myPlayer) {
return;
}
this.eventBus.emit(
new SendAttackIntentEvent(
ctx.ownerId ?? null,
this.myPlayer.troops() * this.renderer.uiState.attackRatio,
),
);
})
.catch((err) =>
console.warn("tileContext attack lookup failed:", err),
);
} else if (this.canAutoBoat(actions, tile)) {
this.sendBoatAttackIntent(tile);
}
Expand Down Expand Up @@ -677,12 +718,22 @@ export class ClientGameRunner {
this.myPlayer.actions(tile).then((actions) => {
if (this.myPlayer === null) return;
if (actions.canAttack) {
this.eventBus.emit(
new SendAttackIntentEvent(
this.gameView.owner(tile).id(),
this.myPlayer.troops() * this.renderer.uiState.attackRatio,
),
);
this.worker
.tileContext(tile)
.then((ctx) => {
if (!this.myPlayer) {
return;
}
this.eventBus.emit(
new SendAttackIntentEvent(
ctx.ownerId ?? null,
this.myPlayer.troops() * this.renderer.uiState.attackRatio,
),
);
})
.catch((err) =>
console.warn("tileContext attack lookup failed:", err),
);
}
});
}
Expand Down
19 changes: 19 additions & 0 deletions src/client/InputHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { EventBus, GameEvent } from "../core/EventBus";
import { UnitType } from "../core/game/Game";
import { UnitView } from "../core/game/GameView";
import { UserSettings } from "../core/game/UserSettings";
import type { WorkerMetricsMessage } from "../core/worker/WorkerMessages";
import { UIState } from "./graphics/UIState";
import { ReplaySpeedMultiplier } from "./utilities/ReplaySpeedMultiplier";

Expand Down Expand Up @@ -81,6 +82,20 @@ export class RefreshGraphicsEvent implements GameEvent {}

export class TogglePerformanceOverlayEvent implements GameEvent {}

export class SetWorkerDebugEvent implements GameEvent {
constructor(
public readonly config: {
enabled: boolean;
intervalMs?: number;
includeTrace?: boolean;
},
) {}
}

export class WorkerMetricsEvent implements GameEvent {
constructor(public readonly metrics: WorkerMetricsMessage) {}
}

export class ToggleStructureEvent implements GameEvent {
constructor(public readonly structureTypes: UnitType[] | null) {}
}
Expand Down Expand Up @@ -136,6 +151,10 @@ export class TickMetricsEvent implements GameEvent {
) {}
}

export class WebGPUComputeMetricsEvent implements GameEvent {
constructor(public readonly computeMs: number) {}
}

export class InputHandler {
private lastPointerX: number = 0;
private lastPointerY: number = 0;
Expand Down
1 change: 1 addition & 0 deletions src/client/UserSettingModal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { UserSettings } from "../core/game/UserSettings";
import "./components/baseComponents/setting/SettingKeybind";
import { SettingKeybind } from "./components/baseComponents/setting/SettingKeybind";
import "./components/baseComponents/setting/SettingNumber";
import "./components/baseComponents/setting/SettingSelect";
import "./components/baseComponents/setting/SettingSlider";
import "./components/baseComponents/setting/SettingToggle";
import { BaseModal } from "./components/BaseModal";
Expand Down
62 changes: 62 additions & 0 deletions src/client/components/baseComponents/setting/SettingSelect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { LitElement, html } from "lit";
import { customElement, property } from "lit/decorators.js";

export type SettingSelectOption = { value: string; label: string };

@customElement("setting-select")
export class SettingSelect extends LitElement {
@property() label = "Setting";
@property() description = "";
@property() id = "";
@property() value = "";
@property({ attribute: false }) options: SettingSelectOption[] = [];
@property({ type: Boolean }) easter = false;
Comment on lines +8 to +13
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n src/client/components/baseComponents/setting/SettingSelect.ts | head -60

Repository: openfrontio/OpenFrontIO

Length of output: 2517


🏁 Script executed:

rg "setting-select" --type html --type ts -A 2 -B 2 | head -50

Repository: openfrontio/OpenFrontIO

Length of output: 559


🏁 Script executed:

rg "<setting-select" -A 1 -B 1

Repository: openfrontio/OpenFrontIO

Length of output: 49


🏁 Script executed:

rg "SettingSelect" --type ts -l

Repository: openfrontio/OpenFrontIO

Length of output: 159


🏁 Script executed:

cat -n src/client/UserSettingModal.ts | head -100

Repository: openfrontio/OpenFrontIO

Length of output: 3791


🏁 Script executed:

cat -n src/client/UserSettingModal.ts | grep -A 10 -B 2 "setting-select"

Repository: openfrontio/OpenFrontIO

Length of output: 49


🏁 Script executed:

cat -n src/client/UserSettingModal.ts | tail -100

Repository: openfrontio/OpenFrontIO

Length of output: 4296


🏁 Script executed:

rg "setting-select" src/client/UserSettingModal.ts -A 5 -B 2

Repository: openfrontio/OpenFrontIO

Length of output: 49


🏁 Script executed:

wc -l src/client/UserSettingModal.ts

Repository: openfrontio/OpenFrontIO

Length of output: 101


🏁 Script executed:

cat -n src/client/UserSettingModal.ts | grep -i select

Repository: openfrontio/OpenFrontIO

Length of output: 448


🏁 Script executed:

find . -name "*.ts" -o -name "*.html" | xargs rg "setting-select" 2>/dev/null

Repository: openfrontio/OpenFrontIO

Length of output: 163


Use <label> with for attribute and separate the select ID from the host element ID.

The label is currently a <div>, so screen readers won't associate it with the <select>. Additionally, if the host element receives an id attribute, it duplicates on the inner <select> because @property() id mirrors to the select element, creating two elements with the same ID. Use a dedicated selectId property and properly labeled <label> element instead.

Suggested structure
-export class SettingSelect extends LitElement {
+export class SettingSelect extends LitElement {
   `@property`() label = "Setting";
   `@property`() description = "";
-  `@property`() id = "";
+  `@property`({ attribute: "select-id" }) selectId = "";
   `@property`() value = "";
   `@property`({ attribute: false }) options: SettingSelectOption[] = [];
   `@property`({ type: Boolean }) easter = false;
@@
-        <div class="text-white font-bold text-base block mb-1">
-          ${this.label}
-        </div>
+        <label class="text-white font-bold text-base block mb-1" for=${this.selectId}>
+          ${this.label}
+        </label>
@@
-        <select
-          id=${this.id}
+        <select
+          id=${this.selectId}
           class="shrink-0 bg-black/60 border border-white/10 text-white/90 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-600"

This also applies to lines 40–52 in the render method.

🤖 Prompt for AI Agents
In `@src/client/components/baseComponents/setting/SettingSelect.ts` around lines 8
- 13, The label is a DIV and the component mirrors the host id onto the inner
<select>, causing duplicate IDs and poor a11y; update the SettingSelect
component by adding a new property selectId (e.g., `@property`() selectId = "")
and stop using the host-level id for the select element, keep the existing id
property for the host only, then change the render method (around the current
render lines ~40–52) to render a proper <label
for={this.selectId}>{this.label}</label> and set the <select id={this.selectId}>
(remove id binding to the host id), ensuring the selectId is generated or set by
consumers when not provided so the label correctly associates with the select
without duplicating IDs.


createRenderRoot() {
return this;
}

private handleChange(e: Event) {
const select = e.target as HTMLSelectElement;
this.value = select.value;
this.dispatchEvent(
new CustomEvent("change", {
detail: { value: this.value },
bubbles: true,
composed: true,
}),
);
Comment on lines +19 to +28
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n src/client/components/baseComponents/setting/SettingSelect.ts

Repository: openfrontio/OpenFrontIO

Length of output: 2537


🌐 Web query:

LitElement light DOM createRenderRoot native event bubbling double dispatch custom element

💡 Result:

  • Light DOM in LitElement / Lit: override createRenderRoot() and return this to render your template into the element’s own children (no shadow root). This is the “light DOM” pattern. It’s supported but generally not recommended because you lose DOM/style scoping and other Shadow DOM composition benefits. [1]

    import {LitElement, html} from 'lit';
    
    class MyEl extends LitElement {
      createRenderRoot() { return this; } // light DOM
      render() { return html`<button `@click`=${this._onClick}>Click</button>`; }
    }
    customElements.define('my-el', MyEl);
  • Native event bubbling & “double dispatch”:

    • With light DOM, events behave like normal DOM: bubbling goes up through your rendered children → your custom element → ancestors.
    • With shadow DOM (Lit’s default), bubbling events coming from inside the shadow root are retargeted so outside listeners see event.target as the host element, not the internal node—this can look like “double dispatch” if you log from both inside and outside, but it’s the same event observed at different points with retargeting. Lit explicitly calls out this retargeting behavior and recommends using event.composedPath() to find the original source node. [2]
  • Custom events crossing the shadow boundary:

    • By default, a bubbling CustomEvent created inside shadow DOM stops at the shadow root.
    • To let it escape to the outside DOM, set both bubbles: true and composed: true. [2]
    • composed is specifically the flag that allows an event to cross the Shadow DOM boundary (and it only propagates outward if it also bubbles). [3]

Sources: Lit “Working with Shadow DOM” (render root / createRenderRoot) [1], Lit “Events” (retargeting, composedPath(), bubbles+composed for custom events) [2], MDN Event.composed (crossing Shadow DOM boundary) [3].


Stop the native change event from propagating.

This component uses light DOM (see createRenderRoot()). When the native <select> element fires its change event, it will bubble up through the custom element to listeners above. But you also dispatch your own change event. This means listeners receive two separate events with the same name.

Use e.stopPropagation() to prevent the native event from bubbling, leaving only your custom event:

Fix: prevent native event from bubbling
  private handleChange(e: Event) {
+   e.stopPropagation();
    const select = e.target as HTMLSelectElement;
    this.value = select.value;
    this.dispatchEvent(
      new CustomEvent("change", {
        detail: { value: this.value },
        bubbles: true,
        composed: true,
      }),
    );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private handleChange(e: Event) {
const select = e.target as HTMLSelectElement;
this.value = select.value;
this.dispatchEvent(
new CustomEvent("change", {
detail: { value: this.value },
bubbles: true,
composed: true,
}),
);
private handleChange(e: Event) {
e.stopPropagation();
const select = e.target as HTMLSelectElement;
this.value = select.value;
this.dispatchEvent(
new CustomEvent("change", {
detail: { value: this.value },
bubbles: true,
composed: true,
}),
);
🤖 Prompt for AI Agents
In `@src/client/components/baseComponents/setting/SettingSelect.ts` around lines
19 - 28, The native change event from the inner <select> is currently allowed to
bubble in handleChange, causing consumers to receive both the native event and
the component's custom change event; inside the handleChange method (in
SettingSelect), call e.stopPropagation() at the start (before dispatching your
CustomEvent) to prevent the native event from propagating so only your composed
CustomEvent("change", { detail: { value: this.value }, bubbles: true, composed:
true }) is seen by outer listeners.

}

render() {
const rainbowClass = this.easter
? "bg-[linear-gradient(270deg,#990033,#996600,#336600,#008080,#1c3f99,#5e0099,#990033)] bg-[length:1400%_1400%] animate-rainbow-bg text-white hover:bg-[linear-gradient(270deg,#990033,#996600,#336600,#008080,#1c3f99,#5e0099,#990033)]"
: "";

return html`
<div
class="flex flex-row items-center justify-between w-full p-4 bg-white/5 border border-white/10 rounded-xl hover:bg-white/10 transition-all gap-4 ${rainbowClass}"
>
<div class="flex flex-col flex-1 min-w-0 mr-4">
<div class="text-white font-bold text-base block mb-1">
${this.label}
</div>
<div class="text-white/50 text-sm leading-snug">
${this.description}
</div>
</div>

<select
id=${this.id}
class="shrink-0 bg-black/60 border border-white/10 text-white/90 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-600"
.value=${this.value}
@change=${this.handleChange}
>
${this.options.map(
(o) => html`<option value=${o.value}>${o.label}</option>`,
)}
</select>
</div>
`;
}
}
29 changes: 19 additions & 10 deletions src/client/graphics/GameRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,11 @@ import { SpawnVideoAd } from "./layers/SpawnVideoReward";
import { StructureIconsLayer } from "./layers/StructureIconsLayer";
import { StructureLayer } from "./layers/StructureLayer";
import { TeamStats } from "./layers/TeamStats";
import { TerrainLayer } from "./layers/TerrainLayer";
import { TerritoryLayer } from "./layers/TerritoryLayer";
import { UILayer } from "./layers/UILayer";
import { UnitDisplay } from "./layers/UnitDisplay";
import { UnitLayer } from "./layers/UnitLayer";
import { WebGPUDebugOverlay } from "./layers/WebGPUDebugOverlay";
import { WinModal } from "./layers/WinModal";

export function createRenderer(
Expand Down Expand Up @@ -224,6 +224,16 @@ export function createRenderer(
performanceOverlay.eventBus = eventBus;
performanceOverlay.userSettings = userSettings;

const webgpuDebugOverlay = document.querySelector(
"webgpu-debug-overlay",
) as WebGPUDebugOverlay;
if (!(webgpuDebugOverlay instanceof WebGPUDebugOverlay)) {
console.error("webgpu debug overlay not found");
}
webgpuDebugOverlay.eventBus = eventBus;
webgpuDebugOverlay.userSettings = userSettings;
webgpuDebugOverlay.requestUpdate();

const alertFrame = document.querySelector("alert-frame") as AlertFrame;
if (!(alertFrame instanceof AlertFrame)) {
console.error("alert frame not found");
Expand Down Expand Up @@ -263,7 +273,6 @@ export function createRenderer(
// Try to group layers by the return value of shouldTransform.
// Not grouping the layers may cause excessive calls to context.save() and context.restore().
const layers: Layer[] = [
new TerrainLayer(game, transformHandler),
new TerritoryLayer(game, eventBus, transformHandler, userSettings),
new RailroadLayer(game, eventBus, transformHandler),
structureLayer,
Expand Down Expand Up @@ -306,6 +315,7 @@ export function createRenderer(
spawnVideoAd,
alertFrame,
performanceOverlay,
webgpuDebugOverlay,
];

return new GameRenderer(
Expand All @@ -316,6 +326,7 @@ export function createRenderer(
uiState,
layers,
performanceOverlay,
webgpuDebugOverlay,
);
}

Expand All @@ -331,8 +342,10 @@ export class GameRenderer {
public uiState: UIState,
private layers: Layer[],
private performanceOverlay: PerformanceOverlay,
private webgpuDebugOverlay: WebGPUDebugOverlay,
) {
const context = canvas.getContext("2d", { alpha: false });
// Keep the main canvas transparent; the WebGPU territory canvas renders the background.
const context = canvas.getContext("2d", { alpha: true });
if (context === null) throw new Error("2d context not supported");
this.context = context;
}
Expand Down Expand Up @@ -380,13 +393,8 @@ export class GameRenderer {
renderGame() {
FrameProfiler.clear();
const start = performance.now();
// Set background
this.context.fillStyle = this.game
.config()
.theme()
.backgroundColor()
.toHex();
this.context.fillRect(0, 0, this.canvas.width, this.canvas.height);
// Clear overlay canvas to transparent; the territory WebGPU canvas draws the base.
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);

const handleTransformState = (
needsTransform: boolean,
Expand Down Expand Up @@ -424,6 +432,7 @@ export class GameRenderer {

const layerDurations = FrameProfiler.consume();
this.performanceOverlay.updateFrameMetrics(duration, layerDurations);
this.webgpuDebugOverlay.updateFrameMetrics(duration);

if (duration > 50) {
console.warn(
Expand Down
Loading
Loading