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) {