From d14150e1d9017476f3c0034869fb89d6105b43d2 Mon Sep 17 00:00:00 2001
From: scamiv <6170744+scamiv@users.noreply.github.com>
Date: Fri, 27 Feb 2026 23:05:06 +0100
Subject: [PATCH] Split mover budgets for on-screen and off-screen passes
---
.../graphics/layers/PerformanceOverlay.ts | 28 +++++---
src/client/graphics/layers/UnitLayer.ts | 70 ++++++++++++-------
2 files changed, 60 insertions(+), 38 deletions(-)
diff --git a/src/client/graphics/layers/PerformanceOverlay.ts b/src/client/graphics/layers/PerformanceOverlay.ts
index 72111f999..ec7f34430 100644
--- a/src/client/graphics/layers/PerformanceOverlay.ts
+++ b/src/client/graphics/layers/PerformanceOverlay.ts
@@ -1308,8 +1308,8 @@ export class PerformanceOverlay extends LitElement implements Layer {
tracked: ${Number(unitLayerCounters.moversTrackedTotal ?? 0)}
sampled: ${Number(unitLayerCounters.moversSampled ?? 0)}
- drawn: ${Number(unitLayerCounters.moversDrawn ?? 0)} skipped:
- ${Number(unitLayerCounters.moversSkipped ?? 0)}
+ drawn: ${Number(unitLayerCounters.moversDrawn ?? 0)}
+ skipped: ${Number(unitLayerCounters.moversSkipped ?? 0)}
moverCanvasScale:
@@ -1323,15 +1323,21 @@ export class PerformanceOverlay extends LitElement implements Layer {
draw:
- ${Number(unitLayerCounters.drawTimeMs ?? 0).toFixed(2)}ms /
- ${Number(unitLayerCounters.budgetTargetMs ?? 0).toFixed(1)}ms
- (+${Number(
- unitLayerCounters.budgetSoftOverrunMs ?? 0,
- ).toFixed(1)}ms
- on-screen) avgOnDebt:
- ${Number(unitLayerCounters.avgOnScreenDebt ?? 0).toFixed(2)}
- maxOnDebt:
- ${Number(unitLayerCounters.maxOnScreenDebt ?? 0).toFixed(0)}
+ ${Number(unitLayerCounters.drawTimeMs ?? 0).toFixed(2)}ms
+
+
+ on:
+ ${Number(unitLayerCounters.onScreenDrawTimeMs ?? 0).toFixed(2)}ms
+ /
+ ${Number(unitLayerCounters.onScreenBudgetTargetMs ?? 0).toFixed(1)}ms
+ off:
+ ${Number(unitLayerCounters.offScreenVerifyTimeMs ?? 0).toFixed(2)}ms
+ /
+ ${Number(unitLayerCounters.offScreenVerifyBudgetMs ?? 0).toFixed(2)}ms
+
+
+ avgOnDebt: ${Number(unitLayerCounters.avgOnScreenDebt ?? 0).toFixed(2)}
+ maxOnDebt: ${Number(unitLayerCounters.maxOnScreenDebt ?? 0).toFixed(0)}
`
: html``}
diff --git a/src/client/graphics/layers/UnitLayer.ts b/src/client/graphics/layers/UnitLayer.ts
index 0bd708137..c72a310ac 100644
--- a/src/client/graphics/layers/UnitLayer.ts
+++ b/src/client/graphics/layers/UnitLayer.ts
@@ -40,11 +40,10 @@ enum Relationship {
Enemy,
}
-const UNIT_DRAW_BUDGET_MS = 2;
-const UNIT_DRAW_SOFT_OVERRUN_MS = 1;
-const OFFSCREEN_REFRESH_EVERY_N_FRAMES = 60;
+const ONSCREEN_DRAW_BUDGET_MS = 2;
+const OFFSCREEN_VERIFY_BUDGET_MS = 0.1;
+const OFFSCREEN_REFRESH_EVERY_N_FRAMES = 30;
const ONSCREEN_HYSTERESIS_FRAMES = 2;
-const OFFSCREEN_VERIFY_MAX_PER_FRAME = 12;
const VIEW_PADDING_PX = 12;
const MOVER_SPATIAL_HASH_CELL_PX = 24;
const DYNAMIC_MOVER_SCALE_STEPS = [1, 2, 3, 4];
@@ -157,8 +156,10 @@ export class UnitLayer implements Layer {
moversDrawn: 0,
moversSkipped: 0,
drawTimeMs: 0,
- budgetTargetMs: UNIT_DRAW_BUDGET_MS,
- budgetSoftOverrunMs: UNIT_DRAW_SOFT_OVERRUN_MS,
+ onScreenDrawTimeMs: 0,
+ offScreenVerifyTimeMs: 0,
+ onScreenBudgetTargetMs: ONSCREEN_DRAW_BUDGET_MS,
+ offScreenVerifyBudgetMs: OFFSCREEN_VERIFY_BUDGET_MS,
avgOnScreenDebt: 0,
maxOnScreenDebt: 0,
moverCanvasScale: 1,
@@ -580,8 +581,10 @@ export class UnitLayer implements Layer {
moversDrawn: moverPerf.drawn,
moversSkipped: moverPerf.skipped,
drawTimeMs: moverPerf.budgetUsedMs,
- budgetTargetMs: UNIT_DRAW_BUDGET_MS,
- budgetSoftOverrunMs: UNIT_DRAW_SOFT_OVERRUN_MS,
+ onScreenDrawTimeMs: moverPerf.onScreenBudgetUsedMs,
+ offScreenVerifyTimeMs: moverPerf.offScreenBudgetUsedMs,
+ onScreenBudgetTargetMs: ONSCREEN_DRAW_BUDGET_MS,
+ offScreenVerifyBudgetMs: OFFSCREEN_VERIFY_BUDGET_MS,
avgOnScreenDebt:
onScreenDebtCount > 0 ? totalOnScreenDebt / onScreenDebtCount : 0,
maxOnScreenDebt,
@@ -605,6 +608,8 @@ export class UnitLayer implements Layer {
drawn: number;
skipped: number;
budgetUsedMs: number;
+ onScreenBudgetUsedMs: number;
+ offScreenBudgetUsedMs: number;
} {
const frameStartMs = performance.now();
const drawnIds = new Set();
@@ -620,9 +625,9 @@ export class UnitLayer implements Layer {
tickFloat,
activeMoverIds,
drawnIds,
- frameStartMs,
+ performance.now(),
+ ONSCREEN_DRAW_BUDGET_MS,
viewBounds,
- Number.MAX_SAFE_INTEGER,
sampledCache,
spatial,
);
@@ -630,27 +635,28 @@ export class UnitLayer implements Layer {
drawn += onScreenPass.drawn;
skipped += onScreenPass.skipped;
- const budgetExceeded = !onScreenPass.budgetRemaining;
const shouldVerifyOffscreen =
- !budgetExceeded &&
this.offScreenMoverIds.length > 0 &&
this.renderFrame % OFFSCREEN_REFRESH_EVERY_N_FRAMES === 0;
+ let offScreenBudgetUsedMs = 0;
+
if (shouldVerifyOffscreen) {
const offscreenPass = this.drawBucketPass(
"off",
tickFloat,
activeMoverIds,
drawnIds,
- frameStartMs,
+ performance.now(),
+ OFFSCREEN_VERIFY_BUDGET_MS,
viewBounds,
- OFFSCREEN_VERIFY_MAX_PER_FRAME,
sampledCache,
spatial,
);
sampled += offscreenPass.sampled;
drawn += offscreenPass.drawn;
skipped += offscreenPass.skipped;
+ offScreenBudgetUsedMs = offscreenPass.budgetUsedMs;
}
for (const unitId of activeMoverIds) {
@@ -668,6 +674,8 @@ export class UnitLayer implements Layer {
drawn,
skipped,
budgetUsedMs: performance.now() - frameStartMs,
+ onScreenBudgetUsedMs: onScreenPass.budgetUsedMs,
+ offScreenBudgetUsedMs,
};
}
@@ -676,9 +684,9 @@ export class UnitLayer implements Layer {
tickFloat: number,
activeMoverIds: Set,
drawnIds: Set,
- frameStartMs: number,
+ passStartMs: number,
+ budgetMs: number,
viewBounds: { left: number; top: number; right: number; bottom: number },
- maxItems: number,
sampledCache: Map,
spatial: MoverSpatialIndex,
): {
@@ -686,16 +694,22 @@ export class UnitLayer implements Layer {
drawn: number;
skipped: number;
budgetRemaining: boolean;
+ budgetUsedMs: number;
} {
const bucketIds =
bucket === "on" ? this.onScreenMoverIds : this.offScreenMoverIds;
- if (bucketIds.length === 0 || maxItems <= 0) {
- return { sampled: 0, drawn: 0, skipped: 0, budgetRemaining: true };
+ if (bucketIds.length === 0 || budgetMs <= 0) {
+ return {
+ sampled: 0,
+ drawn: 0,
+ skipped: 0,
+ budgetRemaining: true,
+ budgetUsedMs: 0,
+ };
}
const startCursor =
bucket === "on" ? this.onScreenCursor : this.offScreenCursor;
- const cap = Math.min(bucketIds.length, maxItems);
let sampled = 0;
let drawn = 0;
@@ -704,7 +718,7 @@ export class UnitLayer implements Layer {
const processed = new Set();
let scanned = 0;
- for (let offset = 0; offset < cap; offset++) {
+ for (let offset = 0; offset < bucketIds.length; offset++) {
if (bucketIds.length === 0) {
break;
}
@@ -715,12 +729,8 @@ export class UnitLayer implements Layer {
continue;
}
- const elapsedMs = performance.now() - frameStartMs;
- const canDrawWithinTarget = elapsedMs < UNIT_DRAW_BUDGET_MS;
- const canDrawOnScreenOverrun =
- bucket === "on" &&
- elapsedMs < UNIT_DRAW_BUDGET_MS + UNIT_DRAW_SOFT_OVERRUN_MS;
- if (!canDrawWithinTarget && !canDrawOnScreenOverrun) {
+ const elapsedMs = performance.now() - passStartMs;
+ if (elapsedMs >= budgetMs) {
budgetRemaining = false;
skipped++;
break;
@@ -834,7 +844,13 @@ export class UnitLayer implements Layer {
: 0;
}
- return { sampled, drawn, skipped, budgetRemaining };
+ return {
+ sampled,
+ drawn,
+ skipped,
+ budgetRemaining,
+ budgetUsedMs: performance.now() - passStartMs,
+ };
}
private buildMoverSpatialHash(): MoverSpatialIndex {