Clean up previous implementations

removed:
- catchUpMode and its CATCH_UP_ENTER/EXIT thresholds in ClientGameRunner
- tick metrics fields and overlay UI for inCatchUpMode and beatsPerFrame
- leftover worker heartbeat plumbing (message type + WorkerClient.sendHeartbeat) that was no longer used after self-clocking

changed:
- backlog tracking: keep serverTurnHighWater / lastProcessedTick / backlogTurns, but simplify it to just compute backlog and a backlogGrowing flag instead of driving a dedicated catch-up mode
- frame skip: adaptRenderFrequency now only increases renderEveryN when backlog > 0 and still growing; when backlog is stable/shrinking or zero, it decays renderEveryN back toward 1
- render loop: uses the backlog-aware renderEveryN unconditionally (no catch-up flag), and resets skipping completely when backlog reaches 0
- metrics/overlay: TickMetricsEvent now carries backlogTurns and renderEveryN; the performance overlay displays backlog and current “render every N frames” but no longer mentions catch-up or heartbeats

Learnings during branch development leading to this

Once the worker self-clocks, a separate “catch-up mode” and beats-per-frame knob don’t add real control; they just complicate the model.
Backlog is still a valuable signal, but it’s more effective as a quantitative input (backlog size and whether it’s growing) than as a boolean mode toggle.
Frame skipping should be driven by actual backlog pressure plus frame cost: throttle only while backlog is growing and frames are heavy, and automatically relax back to full-rate rendering once the simulation catches up.
This commit is contained in:
scamiv
2025-11-24 15:01:17 +01:00
parent b458d00157
commit 8508baee84
5 changed files with 23 additions and 77 deletions
+20 -40
View File
@@ -212,15 +212,12 @@ export class ClientGameRunner {
private serverTurnHighWater: number = 0;
private lastProcessedTick: number = 0;
private backlogTurns: number = 0;
private catchUpMode: boolean = false;
private readonly CATCH_UP_ENTER_BACKLOG = 120; // turns behind to enter catch-up
private readonly CATCH_UP_EXIT_BACKLOG = 20; // turns behind to exit catch-up
private backlogGrowing: boolean = false;
private pendingUpdates: GameUpdateViewData[] = [];
private isProcessingUpdates = false;
// Adaptive rendering during catch-up: render at most once every N frames.
// Adaptive rendering when frames are heavy: render at most once every N frames.
private renderEveryN: number = 1;
private renderSkipCounter: number = 0;
private lastFrameTime: number = 0;
@@ -311,7 +308,7 @@ export class ClientGameRunner {
return;
}
this.pendingUpdates.push(gu);
if (!this.catchUpMode) {
if (this.renderEveryN === 1) {
this.processPendingUpdates();
}
});
@@ -326,13 +323,14 @@ export class ClientGameRunner {
// Decide whether to render (and thus process pending updates) this frame.
let shouldRender = true;
if (this.catchUpMode && this.renderEveryN > 1) {
if (this.renderSkipCounter < this.renderEveryN - 1) {
shouldRender = false;
this.renderSkipCounter++;
} else {
this.renderSkipCounter = 0;
}
if (
this.renderEveryN > 1 &&
this.renderSkipCounter < this.renderEveryN - 1
) {
shouldRender = false;
this.renderSkipCounter++;
} else if (this.renderEveryN > 1) {
this.renderSkipCounter = 0;
}
if (shouldRender) {
@@ -528,7 +526,6 @@ export class ClientGameRunner {
lastTickDuration,
this.currentTickDelay,
this.backlogTurns,
this.catchUpMode,
this.renderEveryN,
),
);
@@ -539,28 +536,26 @@ export class ClientGameRunner {
}
private adaptRenderFrequency(frameDuration: number) {
if (!this.catchUpMode) {
// Back to normal rendering.
// Frameskip only matters while we have a backlog; otherwise stay at 1.
if (this.backlogTurns === 0) {
this.renderEveryN = 1;
this.renderSkipCounter = 0;
return;
}
const HIGH_BACKLOG = 200;
const LOW_BACKLOG = 50;
const HIGH_FRAME_MS = 25;
const LOW_FRAME_MS = 18;
if (this.backlogTurns > HIGH_BACKLOG && frameDuration > HIGH_FRAME_MS) {
// We are far behind and frames are heavy → render less often.
// Only throttle rendering if backlog is still growing; otherwise drift back toward 1.
if (this.backlogGrowing && frameDuration > HIGH_FRAME_MS) {
if (this.renderEveryN < this.MAX_RENDER_EVERY_N) {
this.renderEveryN++;
}
} else if (
this.backlogTurns < LOW_BACKLOG ||
(frameDuration > 0 && frameDuration < LOW_FRAME_MS)
!this.backlogGrowing &&
frameDuration > 0 &&
frameDuration < LOW_FRAME_MS
) {
// Either mostly caught up or frames are cheap again → move back toward normal.
if (this.renderEveryN > 1) {
this.renderEveryN--;
}
@@ -609,27 +604,12 @@ export class ClientGameRunner {
private updateBacklogMetrics(processedTick: number) {
this.lastProcessedTick = processedTick;
const previousBacklog = this.backlogTurns;
this.backlogTurns = Math.max(
0,
this.serverTurnHighWater - this.lastProcessedTick,
);
const wasCatchUp = this.catchUpMode;
if (!this.catchUpMode && this.backlogTurns >= this.CATCH_UP_ENTER_BACKLOG) {
this.catchUpMode = true;
} else if (
this.catchUpMode &&
this.backlogTurns <= this.CATCH_UP_EXIT_BACKLOG
) {
this.catchUpMode = false;
}
if (wasCatchUp !== this.catchUpMode) {
console.log(
`Catch-up mode ${this.catchUpMode ? "enabled" : "disabled"} (backlog: ${
this.backlogTurns
} turns)`,
);
}
this.backlogGrowing = this.backlogTurns > previousBacklog;
}
private inputEvent(event: MouseUpEvent) {
-3
View File
@@ -131,10 +131,7 @@ export class TickMetricsEvent implements GameEvent {
public readonly tickDelay?: number,
// Number of turns the client is behind the server (if known)
public readonly backlogTurns?: number,
// Whether the client is currently in catch-up mode
public readonly inCatchUpMode?: boolean,
public readonly renderEveryN?: number,
public readonly beatsPerFrame?: number,
) {}
}
@@ -233,9 +233,7 @@ export class PerformanceOverlay extends LitElement implements Layer {
event.tickExecutionDuration,
event.tickDelay,
event.backlogTurns,
event.inCatchUpMode,
event.renderEveryN,
event.beatsPerFrame,
);
});
}
@@ -425,25 +423,17 @@ export class PerformanceOverlay extends LitElement implements Layer {
this.layerBreakdown = breakdown;
}
@state()
private backlogTurns: number = 0;
@state()
private inCatchUpMode: boolean = false;
@state()
private renderEveryN: number = 1;
@state()
private beatsPerFrame: number | null = null;
private backlogTurns: number = 0;
updateTickMetrics(
tickExecutionDuration?: number,
tickDelay?: number,
backlogTurns?: number,
inCatchUpMode?: boolean,
renderEveryN?: number,
beatsPerFrame?: number,
) {
if (!this.isVisible || !this.userSettings.performanceOverlay()) return;
@@ -484,15 +474,9 @@ export class PerformanceOverlay extends LitElement implements Layer {
if (backlogTurns !== undefined) {
this.backlogTurns = backlogTurns;
}
if (inCatchUpMode !== undefined) {
this.inCatchUpMode = inCatchUpMode;
}
if (renderEveryN !== undefined) {
this.renderEveryN = renderEveryN;
}
if (beatsPerFrame !== undefined) {
this.beatsPerFrame = beatsPerFrame ?? null;
}
this.requestUpdate();
}
@@ -642,13 +626,10 @@ export class PerformanceOverlay extends LitElement implements Layer {
<div class="performance-line">
Backlog turns:
<span>${this.backlogTurns}</span>
${this.inCatchUpMode ? html`<span> (catch-up)</span>` : html``}
</div>
${this.inCatchUpMode
${this.renderEveryN > 1
? html`<div class="performance-line">
Render every <span>${this.renderEveryN}</span> frame(s),
heartbeats per frame:
<span>${this.beatsPerFrame ?? "auto"}</span>
Render every <span>${this.renderEveryN}</span> frame(s)
</div>`
: html``}
${this.layerBreakdown.length
-6
View File
@@ -100,12 +100,6 @@ export class WorkerClient {
});
}
sendHeartbeat() {
this.worker.postMessage({
type: "heartbeat",
});
}
playerProfile(playerID: number): Promise<PlayerProfile> {
return new Promise((resolve, reject) => {
if (!this.isInitialized) {
-6
View File
@@ -9,7 +9,6 @@ import { GameUpdateViewData } from "../game/GameUpdates";
import { ClientID, GameStartInfo, Turn } from "../Schemas";
export type WorkerMessageType =
| "heartbeat"
| "init"
| "initialized"
| "turn"
@@ -31,10 +30,6 @@ interface BaseWorkerMessage {
id?: string;
}
export interface HeartbeatMessage extends BaseWorkerMessage {
type: "heartbeat";
}
// Messages from main thread to worker
export interface InitMessage extends BaseWorkerMessage {
type: "init";
@@ -114,7 +109,6 @@ export interface TransportShipSpawnResultMessage extends BaseWorkerMessage {
// Union types for type safety
export type MainThreadMessage =
| HeartbeatMessage
| InitMessage
| TurnMessage
| PlayerActionsMessage