Merge branch 'catchUpMode' into self-clocked-worker-frame-skip-backlog

This commit is contained in:
scamiv
2025-11-25 17:50:27 +01:00
3 changed files with 37 additions and 90 deletions
+24 -78
View File
@@ -213,17 +213,12 @@ export class ClientGameRunner {
private lastProcessedTick: number = 0;
private backlogTurns: number = 0;
private backlogGrowing: boolean = false;
private lastRenderedTick: number = 0;
private pendingUpdates: GameUpdateViewData[] = [];
private pendingStart = 0;
private isProcessingUpdates = false;
// 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;
private readonly MAX_RENDER_EVERY_N = 5;
constructor(
private lobby: LobbyConfig,
private eventBus: EventBus,
@@ -309,39 +304,8 @@ export class ClientGameRunner {
return;
}
this.pendingUpdates.push(gu);
if (this.renderEveryN === 1) {
this.processPendingUpdates();
}
this.processPendingUpdates();
});
const keepWorkerAlive = () => {
if (this.isActive) {
const now = performance.now();
let frameDuration = 0;
if (this.lastFrameTime !== 0) {
frameDuration = now - this.lastFrameTime;
}
this.lastFrameTime = now;
// Decide whether to render (and thus process pending updates) this frame.
let shouldRender = true;
if (
this.renderEveryN > 1 &&
this.renderSkipCounter < this.renderEveryN - 1
) {
shouldRender = false;
this.renderSkipCounter++;
} else if (this.renderEveryN > 1) {
this.renderSkipCounter = 0;
}
if (shouldRender) {
this.processPendingUpdates();
}
this.adaptRenderFrequency(frameDuration);
requestAnimationFrame(keepWorkerAlive);
}
};
requestAnimationFrame(keepWorkerAlive);
const onconnect = () => {
console.log("Connected to game server!");
@@ -489,7 +453,7 @@ export class ClientGameRunner {
const MAX_SLICE_BUDGET_MS = 2000; // allow longer slices when backlog is large
const BACKLOG_FREE_TURNS = 10; // scaling starts at this many turns
const BACKLOG_MAX_TURNS = 500; // MAX_SLICE_BUDGET_MS is reached at this many turns
const MAX_PER_SLICE = 1000;
const MAX_TICKS_PER_SLICE = 1000;
const backlogOverhead = Math.max(
0,
@@ -532,7 +496,7 @@ export class ClientGameRunner {
lastTick = gu.tick;
const elapsed = performance.now() - frameStart;
if (processedCount >= MAX_PER_SLICE || elapsed >= sliceBudgetMs) {
if (processedCount >= MAX_TICKS_PER_SLICE || elapsed >= sliceBudgetMs) {
break;
}
}
@@ -553,18 +517,27 @@ export class ClientGameRunner {
this.gameView.update(combinedGu);
}
this.renderer.tick();
this.eventBus.emit(
new TickMetricsEvent(
lastTickDuration,
this.currentTickDelay,
this.backlogTurns,
this.renderEveryN,
),
);
// Only emit metrics when ALL processing is complete
if (this.pendingStart >= this.pendingUpdates.length) {
const ticksPerRender =
this.lastRenderedTick === 0
? lastTick
: lastTick - this.lastRenderedTick;
this.lastRenderedTick = lastTick;
// Reset tick delay for next measurement
this.currentTickDelay = undefined;
this.renderer.tick();
this.eventBus.emit(
new TickMetricsEvent(
lastTickDuration,
this.currentTickDelay,
this.backlogTurns,
ticksPerRender,
),
);
// Reset tick delay for next measurement
this.currentTickDelay = undefined;
}
}
if (this.pendingStart < this.pendingUpdates.length) {
@@ -577,33 +550,6 @@ export class ClientGameRunner {
requestAnimationFrame(processFrame);
}
private adaptRenderFrequency(frameDuration: number) {
// 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_FRAME_MS = 25;
const LOW_FRAME_MS = 18;
// 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.backlogGrowing &&
frameDuration > 0 &&
frameDuration < LOW_FRAME_MS
) {
if (this.renderEveryN > 1) {
this.renderEveryN--;
}
}
}
private mergeGameUpdates(
batch: GameUpdateViewData[],
): GameUpdateViewData | null {
+2 -1
View File
@@ -131,7 +131,8 @@ export class TickMetricsEvent implements GameEvent {
public readonly tickDelay?: number,
// Number of turns the client is behind the server (if known)
public readonly backlogTurns?: number,
public readonly renderEveryN?: number,
// Number of ticks applied since last render
public readonly ticksPerRender?: number,
) {}
}
@@ -233,7 +233,7 @@ export class PerformanceOverlay extends LitElement implements Layer {
event.tickExecutionDuration,
event.tickDelay,
event.backlogTurns,
event.renderEveryN,
event.ticksPerRender,
);
});
}
@@ -424,16 +424,16 @@ export class PerformanceOverlay extends LitElement implements Layer {
}
@state()
private renderEveryN: number = 1;
private backlogTurns: number = 0;
@state()
private backlogTurns: number = 0;
private ticksPerRender: number = 0;
updateTickMetrics(
tickExecutionDuration?: number,
tickDelay?: number,
backlogTurns?: number,
renderEveryN?: number,
ticksPerRender?: number,
) {
if (!this.isVisible || !this.userSettings.performanceOverlay()) return;
@@ -474,8 +474,9 @@ export class PerformanceOverlay extends LitElement implements Layer {
if (backlogTurns !== undefined) {
this.backlogTurns = backlogTurns;
}
if (renderEveryN !== undefined) {
this.renderEveryN = renderEveryN;
if (ticksPerRender !== undefined) {
this.ticksPerRender = ticksPerRender;
}
this.requestUpdate();
@@ -623,15 +624,14 @@ export class PerformanceOverlay extends LitElement implements Layer {
<span>${this.tickDelayAvg.toFixed(2)}ms</span>
(max: <span>${this.tickDelayMax}ms</span>)
</div>
<div class="performance-line">
Ticks per render:
<span>${this.ticksPerRender}</span>
</div>
<div class="performance-line">
Backlog turns:
<span>${this.backlogTurns}</span>
</div>
${this.renderEveryN > 1
? html`<div class="performance-line">
Render every <span>${this.renderEveryN}</span> frame(s)
</div>`
: html``}
${this.layerBreakdown.length
? html`<div class="layers-section">
<div class="performance-line">