diff --git a/src/client/graphics/layers/PerformanceOverlay.ts b/src/client/graphics/layers/PerformanceOverlay.ts
index c76bd7f7c..4f8150e45 100644
--- a/src/client/graphics/layers/PerformanceOverlay.ts
+++ b/src/client/graphics/layers/PerformanceOverlay.ts
@@ -549,6 +549,20 @@ export class PerformanceOverlay extends LitElement implements Layer {
rfQueueMax: number | null;
rfHandlerAvg: number | null;
rfHandlerMax: number | null;
+ renderSubmittedCount: number | null;
+ renderNoopCount: number | null;
+ renderCpuTotalAvg: number | null;
+ renderCpuTotalMax: number | null;
+ renderGetTextureAvg: number | null;
+ renderGetTextureMax: number | null;
+ renderFrameComputeAvg: number | null;
+ renderFrameComputeMax: number | null;
+ renderTerritoryPassAvg: number | null;
+ renderTerritoryPassMax: number | null;
+ renderTemporalResolveAvg: number | null;
+ renderTemporalResolveMax: number | null;
+ renderSubmitAvg: number | null;
+ renderSubmitMax: number | null;
traceLines: string[];
topMsgs: Array<{
type: string;
@@ -572,6 +586,20 @@ export class PerformanceOverlay extends LitElement implements Layer {
rfQueueMax: null,
rfHandlerAvg: null,
rfHandlerMax: null,
+ renderSubmittedCount: null,
+ renderNoopCount: null,
+ renderCpuTotalAvg: null,
+ renderCpuTotalMax: null,
+ renderGetTextureAvg: null,
+ renderGetTextureMax: null,
+ renderFrameComputeAvg: null,
+ renderFrameComputeMax: null,
+ renderTerritoryPassAvg: null,
+ renderTerritoryPassMax: null,
+ renderTemporalResolveAvg: null,
+ renderTemporalResolveMax: null,
+ renderSubmitAvg: null,
+ renderSubmitMax: null,
traceLines: [],
topMsgs: [],
};
@@ -620,6 +648,62 @@ export class PerformanceOverlay extends LitElement implements Layer {
rfQueueMax: typeof rfQueueMax === "number" ? rfQueueMax : null,
rfHandlerAvg: typeof rfHandlerAvg === "number" ? rfHandlerAvg : null,
rfHandlerMax: typeof rfHandlerMax === "number" ? rfHandlerMax : null,
+ renderSubmittedCount:
+ typeof metrics.renderSubmittedCount === "number"
+ ? metrics.renderSubmittedCount
+ : null,
+ renderNoopCount:
+ typeof metrics.renderNoopCount === "number"
+ ? metrics.renderNoopCount
+ : null,
+ renderCpuTotalAvg:
+ typeof metrics.renderCpuTotalMsAvg === "number"
+ ? metrics.renderCpuTotalMsAvg
+ : null,
+ renderCpuTotalMax:
+ typeof metrics.renderCpuTotalMsMax === "number"
+ ? metrics.renderCpuTotalMsMax
+ : null,
+ renderGetTextureAvg:
+ typeof metrics.renderGetTextureMsAvg === "number"
+ ? metrics.renderGetTextureMsAvg
+ : null,
+ renderGetTextureMax:
+ typeof metrics.renderGetTextureMsMax === "number"
+ ? metrics.renderGetTextureMsMax
+ : null,
+ renderFrameComputeAvg:
+ typeof metrics.renderFrameComputeMsAvg === "number"
+ ? metrics.renderFrameComputeMsAvg
+ : null,
+ renderFrameComputeMax:
+ typeof metrics.renderFrameComputeMsMax === "number"
+ ? metrics.renderFrameComputeMsMax
+ : null,
+ renderTerritoryPassAvg:
+ typeof metrics.renderTerritoryPassMsAvg === "number"
+ ? metrics.renderTerritoryPassMsAvg
+ : null,
+ renderTerritoryPassMax:
+ typeof metrics.renderTerritoryPassMsMax === "number"
+ ? metrics.renderTerritoryPassMsMax
+ : null,
+ renderTemporalResolveAvg:
+ typeof metrics.renderTemporalResolveMsAvg === "number"
+ ? metrics.renderTemporalResolveMsAvg
+ : null,
+ renderTemporalResolveMax:
+ typeof metrics.renderTemporalResolveMsMax === "number"
+ ? metrics.renderTemporalResolveMsMax
+ : null,
+ renderSubmitAvg:
+ typeof metrics.renderSubmitMsAvg === "number"
+ ? metrics.renderSubmitMsAvg
+ : null,
+ renderSubmitMax:
+ typeof metrics.renderSubmitMsMax === "number"
+ ? metrics.renderSubmitMsMax
+ : null,
traceLines,
topMsgs,
};
@@ -813,6 +897,63 @@ export class PerformanceOverlay extends LitElement implements Layer {
${this.formatMs(worker.rfHandlerMax, 0)}
+ ${worker.renderSubmittedCount !== null ||
+ worker.renderNoopCount !== null
+ ? html`
+ render submits / noops
+ ${worker.renderSubmittedCount ?? 0} /
+ ${worker.renderNoopCount ?? 0}
+
`
+ : html``}
+ ${worker.renderCpuTotalAvg !== null
+ ? html`
+ render CPU breakdown (avg/max, submitted frames)
+
+
+ cpu total
+ ${this.formatMs(worker.renderCpuTotalAvg)} /
+ ${this.formatMs(worker.renderCpuTotalMax, 0)}
+
+
+ getCurrentTexture
+ ${this.formatMs(worker.renderGetTextureAvg)} /
+ ${this.formatMs(worker.renderGetTextureMax, 0)}
+
+
+ frame compute
+ ${this.formatMs(worker.renderFrameComputeAvg)} /
+ ${this.formatMs(worker.renderFrameComputeMax, 0)}
+
+
+ territory pass
+ ${this.formatMs(worker.renderTerritoryPassAvg)} /
+ ${this.formatMs(worker.renderTerritoryPassMax, 0)}
+
+
+ temporal resolve
+ ${this.formatMs(worker.renderTemporalResolveAvg)} /
+ ${this.formatMs(worker.renderTemporalResolveMax, 0)}
+
+
+ submit
+ ${this.formatMs(worker.renderSubmitAvg)} /
+ ${this.formatMs(worker.renderSubmitMax, 0)}
+
`
+ : html``}
${worker.topMsgs.length
? html`
top msgs (count | queue avg/max | handler avg/max)
diff --git a/src/client/graphics/layers/TerritoryLayer.ts b/src/client/graphics/layers/TerritoryLayer.ts
index ef7aaeb4e..ba3767b69 100644
--- a/src/client/graphics/layers/TerritoryLayer.ts
+++ b/src/client/graphics/layers/TerritoryLayer.ts
@@ -1,6 +1,5 @@
import { Theme } from "../../../core/configuration/Config";
import { EventBus } from "../../../core/EventBus";
-import { UnitType } from "../../../core/game/Game";
import { TileRef } from "../../../core/game/GameMap";
import { GameView } from "../../../core/game/GameView";
import { UserSettings } from "../../../core/game/UserSettings";
@@ -49,12 +48,14 @@ export class TerritoryLayer implements Layer {
private lastPaletteSignature: string | null = null;
private lastPatternsEnabled: boolean | null = null;
- private lastDefensePostsSignature: string | null = null;
private lastTerrainShaderSignature: string | null = null;
private lastTerritoryShaderSignature: string | null = null;
private lastPreSmoothingSignature: string | null = null;
private lastPostSmoothingSignature: string | null = null;
+ private lastPaletteSyncMs = 0;
+ private lastUserSettingsSyncMs = 0;
+
private lastMousePosition: { x: number; y: number } | null = null;
private hoveredOwnerSmallId: number | null = null;
private lastHoverUpdateMs = 0;
@@ -97,11 +98,9 @@ export class TerritoryLayer implements Layer {
this.redraw();
}
- this.refreshPaletteIfNeeded();
- this.refreshDefensePostsIfNeeded();
- this.applyTerrainShaderSettings();
- this.applyTerritoryShaderSettings();
- this.applyTerritorySmoothingSettings();
+ const now = performance.now();
+ this.syncUserSettingsMaybe(now);
+ this.syncPaletteMaybe(now);
// Renderer tick and dirty-tile marking are driven in the worker from
// simulation-derived tile updates (tileUpdateSink). The main thread only
@@ -151,10 +150,6 @@ export class TerritoryLayer implements Layer {
this.territoryRenderer.refreshPalette();
this.lastPaletteSignature = this.computePaletteSignature();
- this.lastDefensePostsSignature = this.computeDefensePostsSignature();
- // Ensure defense posts buffer is uploaded on first tick.
- this.territoryRenderer.markDefensePostsDirty();
-
// Run an initial tick to upload state and build the colour texture. Without
// this, the first render call may occur before the initial compute pass
// has been executed, resulting in undefined colours.
@@ -174,12 +169,11 @@ export class TerritoryLayer implements Layer {
this.redraw();
}
- // Apply user settings even while the game is paused (settings modal).
- this.refreshPaletteIfNeeded();
- this.refreshDefensePostsIfNeeded();
- this.applyTerrainShaderSettings();
- this.applyTerritoryShaderSettings();
- this.applyTerritorySmoothingSettings();
+ // Apply user settings even while the game is paused (settings modal), but
+ // avoid heavy polling work in the RAF hot path.
+ const now = performance.now();
+ this.syncUserSettingsMaybe(now);
+ this.syncPaletteMaybe(now);
this.ensureTerritoryCanvasAttached(context.canvas);
this.updateHoverHighlight();
@@ -377,7 +371,6 @@ export class TerritoryLayer implements Layer {
}
private computePaletteSignature(): string {
- const patternsEnabled = this.userSettings.territoryPatterns();
const players = this.game.playerViews();
const fnvByte = (hash: number, byte: number): number =>
@@ -391,7 +384,7 @@ export class TerritoryLayer implements Layer {
return hash;
};
- let hash = patternsEnabled ? 2166136261 : 2166136262;
+ let hash = 2166136261;
hash = fnv32(hash, players.length);
let maxSmallId = 0;
@@ -416,15 +409,19 @@ export class TerritoryLayer implements Layer {
return `${hash}`;
}
- private refreshPaletteIfNeeded() {
+ private syncPaletteMaybe(nowMs: number, force: boolean = false) {
if (!this.territoryRenderer) {
return;
}
- const patternsEnabled = this.userSettings.territoryPatterns();
- if (patternsEnabled !== this.lastPatternsEnabled) {
- this.lastPatternsEnabled = patternsEnabled;
- this.territoryRenderer.setPatternsEnabled(patternsEnabled);
+
+ // Palette rebuild is relatively expensive (builds & transfers full rows),
+ // so only check periodically. The worker handles most palette dirtiness,
+ // but the main thread still owns the theme-derived colour mapping.
+ if (!force && nowMs - this.lastPaletteSyncMs < 500) {
+ return;
}
+ this.lastPaletteSyncMs = nowMs;
+
const signature = this.computePaletteSignature();
if (signature !== this.lastPaletteSignature) {
this.lastPaletteSignature = signature;
@@ -432,6 +429,29 @@ export class TerritoryLayer implements Layer {
}
}
+ private syncUserSettingsMaybe(nowMs: number, force: boolean = false) {
+ if (!this.territoryRenderer) {
+ return;
+ }
+
+ // Shader settings are user-driven and change rarely; avoid allocating
+ // signatures and arrays every RAF.
+ if (!force && nowMs - this.lastUserSettingsSyncMs < 250) {
+ return;
+ }
+ this.lastUserSettingsSyncMs = nowMs;
+
+ const patternsEnabled = this.userSettings.territoryPatterns();
+ if (patternsEnabled !== this.lastPatternsEnabled) {
+ this.lastPatternsEnabled = patternsEnabled;
+ this.territoryRenderer.setPatternsEnabled(patternsEnabled);
+ }
+
+ this.applyTerrainShaderSettings();
+ this.applyTerritoryShaderSettings();
+ this.applyTerritorySmoothingSettings();
+ }
+
private applyTerritoryShaderSettings(force: boolean = false) {
if (!this.territoryRenderer) {
return;
@@ -507,29 +527,4 @@ export class TerritoryLayer implements Layer {
);
}
}
-
- private computeDefensePostsSignature(): string {
- // Active + completed posts only.
- const parts: string[] = [];
- for (const u of this.game.units(UnitType.DefensePost)) {
- if (!u.isActive() || u.isUnderConstruction()) continue;
- const tile = u.tile();
- parts.push(
- `${u.owner().smallID()},${this.game.x(tile)},${this.game.y(tile)}`,
- );
- }
- parts.sort();
- return parts.join("|");
- }
-
- private refreshDefensePostsIfNeeded() {
- if (!this.territoryRenderer) {
- return;
- }
- const signature = this.computeDefensePostsSignature();
- if (signature !== this.lastDefensePostsSignature) {
- this.lastDefensePostsSignature = signature;
- this.territoryRenderer.markDefensePostsDirty();
- }
- }
}
diff --git a/src/core/worker/Worker.worker.ts b/src/core/worker/Worker.worker.ts
index 61f71cd52..67d90712b 100644
--- a/src/core/worker/Worker.worker.ts
+++ b/src/core/worker/Worker.worker.ts
@@ -85,6 +85,21 @@ class WorkerProfiler {
private traceHead = 0;
private readonly traceCap = 160;
+ private renderSubmittedCount = 0;
+ private renderNoopCount = 0;
+ private renderGetTextureSum = 0;
+ private renderGetTextureMax = 0;
+ private renderFrameComputeSum = 0;
+ private renderFrameComputeMax = 0;
+ private renderTerritoryPassSum = 0;
+ private renderTerritoryPassMax = 0;
+ private renderTemporalResolveSum = 0;
+ private renderTemporalResolveMax = 0;
+ private renderSubmitSum = 0;
+ private renderSubmitMax = 0;
+ private renderCpuTotalSum = 0;
+ private renderCpuTotalMax = 0;
+
start(): void {
if (this.reportTimer) return;
this.lastReportWallMs = Date.now();
@@ -178,6 +193,60 @@ class WorkerProfiler {
}
}
+ recordRenderBreakdown(b: {
+ submitted: boolean;
+ getTextureMs?: number;
+ frameComputeMs?: number;
+ territoryPassMs?: number;
+ temporalResolveMs?: number;
+ submitMs?: number;
+ cpuTotalMs?: number;
+ }): void {
+ if (!this.config.enabled) return;
+ if (!b.submitted) {
+ this.renderNoopCount++;
+ return;
+ }
+ this.renderSubmittedCount++;
+
+ if (typeof b.getTextureMs === "number") {
+ this.renderGetTextureSum += b.getTextureMs;
+ this.renderGetTextureMax = Math.max(
+ this.renderGetTextureMax,
+ b.getTextureMs,
+ );
+ }
+ if (typeof b.frameComputeMs === "number") {
+ this.renderFrameComputeSum += b.frameComputeMs;
+ this.renderFrameComputeMax = Math.max(
+ this.renderFrameComputeMax,
+ b.frameComputeMs,
+ );
+ }
+ if (typeof b.territoryPassMs === "number") {
+ this.renderTerritoryPassSum += b.territoryPassMs;
+ this.renderTerritoryPassMax = Math.max(
+ this.renderTerritoryPassMax,
+ b.territoryPassMs,
+ );
+ }
+ if (typeof b.temporalResolveMs === "number") {
+ this.renderTemporalResolveSum += b.temporalResolveMs;
+ this.renderTemporalResolveMax = Math.max(
+ this.renderTemporalResolveMax,
+ b.temporalResolveMs,
+ );
+ }
+ if (typeof b.submitMs === "number") {
+ this.renderSubmitSum += b.submitMs;
+ this.renderSubmitMax = Math.max(this.renderSubmitMax, b.submitMs);
+ }
+ if (typeof b.cpuTotalMs === "number") {
+ this.renderCpuTotalSum += b.cpuTotalMs;
+ this.renderCpuTotalMax = Math.max(this.renderCpuTotalMax, b.cpuTotalMs);
+ }
+ }
+
trace(line: string): void {
if (!this.config.enabled || !this.config.includeTrace) return;
if (this.traceRing.length < this.traceCap) {
@@ -234,6 +303,10 @@ class WorkerProfiler {
msgCountsObj[k] = c;
}
+ const renderTotal = this.renderSubmittedCount + this.renderNoopCount;
+ const rAvg = (sum: number): number =>
+ this.renderSubmittedCount > 0 ? sum / this.renderSubmittedCount : 0;
+
const metrics: WorkerMetricsMessage = {
type: "worker_metrics",
intervalMs,
@@ -248,6 +321,45 @@ class WorkerProfiler {
simPumpExecMsAvg:
this.simExecCount > 0 ? this.simExecSum / this.simExecCount : 0,
simPumpExecMsMax: this.simExecMax,
+ renderSubmittedCount:
+ renderTotal > 0 ? this.renderSubmittedCount : undefined,
+ renderNoopCount: renderTotal > 0 ? this.renderNoopCount : undefined,
+ renderGetTextureMsAvg:
+ this.renderSubmittedCount > 0
+ ? rAvg(this.renderGetTextureSum)
+ : undefined,
+ renderGetTextureMsMax:
+ this.renderSubmittedCount > 0 ? this.renderGetTextureMax : undefined,
+ renderFrameComputeMsAvg:
+ this.renderSubmittedCount > 0
+ ? rAvg(this.renderFrameComputeSum)
+ : undefined,
+ renderFrameComputeMsMax:
+ this.renderSubmittedCount > 0 ? this.renderFrameComputeMax : undefined,
+ renderTerritoryPassMsAvg:
+ this.renderSubmittedCount > 0
+ ? rAvg(this.renderTerritoryPassSum)
+ : undefined,
+ renderTerritoryPassMsMax:
+ this.renderSubmittedCount > 0 ? this.renderTerritoryPassMax : undefined,
+ renderTemporalResolveMsAvg:
+ this.renderSubmittedCount > 0
+ ? rAvg(this.renderTemporalResolveSum)
+ : undefined,
+ renderTemporalResolveMsMax:
+ this.renderSubmittedCount > 0
+ ? this.renderTemporalResolveMax
+ : undefined,
+ renderSubmitMsAvg:
+ this.renderSubmittedCount > 0 ? rAvg(this.renderSubmitSum) : undefined,
+ renderSubmitMsMax:
+ this.renderSubmittedCount > 0 ? this.renderSubmitMax : undefined,
+ renderCpuTotalMsAvg:
+ this.renderSubmittedCount > 0
+ ? rAvg(this.renderCpuTotalSum)
+ : undefined,
+ renderCpuTotalMsMax:
+ this.renderSubmittedCount > 0 ? this.renderCpuTotalMax : undefined,
msgCounts: msgCountsObj,
msgHandlerMsAvg: toAvgRecord(this.msgHandlerSum, this.msgCounts),
msgHandlerMsMax: toMaxRecord(this.msgHandlerMax),
@@ -268,6 +380,20 @@ class WorkerProfiler {
this.simExecSum = 0;
this.simExecCount = 0;
this.simExecMax = 0;
+ this.renderSubmittedCount = 0;
+ this.renderNoopCount = 0;
+ this.renderGetTextureSum = 0;
+ this.renderGetTextureMax = 0;
+ this.renderFrameComputeSum = 0;
+ this.renderFrameComputeMax = 0;
+ this.renderTerritoryPassSum = 0;
+ this.renderTerritoryPassMax = 0;
+ this.renderTemporalResolveSum = 0;
+ this.renderTemporalResolveMax = 0;
+ this.renderSubmitSum = 0;
+ this.renderSubmitMax = 0;
+ this.renderCpuTotalSum = 0;
+ this.renderCpuTotalMax = 0;
this.msgCounts.clear();
this.msgHandlerSum.clear();
this.msgHandlerMax.clear();
@@ -856,6 +982,12 @@ ctx.addEventListener("message", async (e: MessageEvent) => {
let renderGpuWaitMs: number | undefined;
let renderWaitPrevGpuTimedOut: boolean | undefined;
let renderGpuWaitTimedOut: boolean | undefined;
+ let renderSubmitted: boolean | undefined;
+ let renderFrameComputeMs: number | undefined;
+ let renderTerritoryPassMs: number | undefined;
+ let renderTemporalResolveMs: number | undefined;
+ let renderSubmitMs: number | undefined;
+ let renderCpuTotalMs: number | undefined;
try {
if ("viewSize" in message && message.viewSize) {
renderer.setViewSize(
@@ -872,7 +1004,7 @@ ctx.addEventListener("message", async (e: MessageEvent) => {
}
const r: any = renderer as any;
if (typeof r.renderAsync === "function") {
- const breakdown = await r.renderAsync();
+ const breakdown = await r.renderAsync(!!profiler.config.enabled);
if (breakdown) {
renderWaitPrevGpuMs = breakdown.waitPrevGpuMs;
renderCpuMs = breakdown.cpuMs;
@@ -880,6 +1012,12 @@ ctx.addEventListener("message", async (e: MessageEvent) => {
renderGpuWaitMs = breakdown.gpuWaitMs;
renderWaitPrevGpuTimedOut = breakdown.waitPrevGpuTimedOut;
renderGpuWaitTimedOut = breakdown.gpuWaitTimedOut;
+ renderSubmitted = breakdown.submitted;
+ renderFrameComputeMs = breakdown.frameComputeMs;
+ renderTerritoryPassMs = breakdown.territoryPassMs;
+ renderTemporalResolveMs = breakdown.temporalResolveMs;
+ renderSubmitMs = breakdown.submitMs;
+ renderCpuTotalMs = breakdown.cpuTotalMs;
}
} else {
renderer.render();
@@ -890,6 +1028,17 @@ ctx.addEventListener("message", async (e: MessageEvent) => {
const endedAt = performance.now();
const endedAtWallMs = Date.now();
if (id) {
+ if (typeof renderSubmitted === "boolean") {
+ profiler.recordRenderBreakdown({
+ submitted: renderSubmitted,
+ getTextureMs: renderGetTextureMs,
+ frameComputeMs: renderFrameComputeMs,
+ territoryPassMs: renderTerritoryPassMs,
+ temporalResolveMs: renderTemporalResolveMs,
+ submitMs: renderSubmitMs,
+ cpuTotalMs: renderCpuTotalMs,
+ });
+ }
sendMessage({
type: "render_done",
id,
@@ -907,6 +1056,12 @@ ctx.addEventListener("message", async (e: MessageEvent) => {
renderGpuWaitMs,
renderWaitPrevGpuTimedOut,
renderGpuWaitTimedOut,
+ renderSubmitted,
+ renderFrameComputeMs,
+ renderTerritoryPassMs,
+ renderTemporalResolveMs,
+ renderSubmitMs,
+ renderCpuTotalMs,
} as RenderDoneMessage);
}
}
diff --git a/src/core/worker/WorkerMessages.ts b/src/core/worker/WorkerMessages.ts
index 69a0f2bbf..8a63eefd1 100644
--- a/src/core/worker/WorkerMessages.ts
+++ b/src/core/worker/WorkerMessages.ts
@@ -301,6 +301,16 @@ export interface RenderDoneMessage extends BaseWorkerMessage {
renderGpuWaitMs?: number;
renderWaitPrevGpuTimedOut?: boolean;
renderGpuWaitTimedOut?: boolean;
+
+ /**
+ * Additional optional breakdown for CPU-side render encoding.
+ */
+ renderSubmitted?: boolean;
+ renderFrameComputeMs?: number;
+ renderTerritoryPassMs?: number;
+ renderTemporalResolveMs?: number;
+ renderSubmitMs?: number;
+ renderCpuTotalMs?: number;
}
export interface RendererReadyMessage extends BaseWorkerMessage {
@@ -330,6 +340,26 @@ export interface WorkerMetricsMessage extends BaseWorkerMessage {
simPumpDelayMsMax: number;
simPumpExecMsAvg: number;
simPumpExecMsMax: number;
+
+ /**
+ * Optional render_frame breakdown collected inside the worker renderer.
+ * Values are based on the last metrics interval.
+ */
+ renderSubmittedCount?: number;
+ renderNoopCount?: number;
+ renderGetTextureMsAvg?: number;
+ renderGetTextureMsMax?: number;
+ renderFrameComputeMsAvg?: number;
+ renderFrameComputeMsMax?: number;
+ renderTerritoryPassMsAvg?: number;
+ renderTerritoryPassMsMax?: number;
+ renderTemporalResolveMsAvg?: number;
+ renderTemporalResolveMsMax?: number;
+ renderSubmitMsAvg?: number;
+ renderSubmitMsMax?: number;
+ renderCpuTotalMsAvg?: number;
+ renderCpuTotalMsMax?: number;
+
msgCounts: Record;
msgHandlerMsAvg: Record;
msgHandlerMsMax: Record;
diff --git a/src/core/worker/WorkerTerritoryRenderer.ts b/src/core/worker/WorkerTerritoryRenderer.ts
index 67b118479..594745d5c 100644
--- a/src/core/worker/WorkerTerritoryRenderer.ts
+++ b/src/core/worker/WorkerTerritoryRenderer.ts
@@ -746,7 +746,16 @@ export class WorkerTerritoryRenderer {
* Render one frame.
* Runs render passes to draw to the canvas.
*/
- render(onGetTextureMs?: (ms: number) => void): boolean {
+ render(
+ onGetTextureMs?: (ms: number) => void,
+ profile?: {
+ cpuTotalMs: number;
+ frameComputeMs: number;
+ territoryPassMs: number;
+ temporalResolveMs: number;
+ submitMs: number;
+ },
+ ): boolean {
if (
!this.ready ||
!this.device ||
@@ -756,10 +765,13 @@ export class WorkerTerritoryRenderer {
return false;
}
+ // Without post-smoothing, stable frames can simply be skipped.
if (!this.frameDirty && !this.postSmoothingEnabled) {
return false;
}
+ const cpuStart = profile ? performance.now() : 0;
+
const nowSec = performance.now() / 1000;
this.resources.writeTemporalUniformBuffer(nowSec);
@@ -770,10 +782,12 @@ export class WorkerTerritoryRenderer {
onGetTextureMs(performance.now() - getTexStart);
}
+ let frameComputeMs = 0;
if (
this.preSmoothingEnabled &&
this.resources.consumeVisualStateSyncNeeded()
) {
+ const start = profile ? performance.now() : 0;
const visualStateTexture = this.resources.getVisualStateTexture();
if (visualStateTexture) {
encoder.copyTextureToTexture(
@@ -786,20 +800,36 @@ export class WorkerTerritoryRenderer {
},
);
}
+ if (profile) {
+ frameComputeMs += performance.now() - start;
+ }
}
for (const pass of this.frameComputePasses) {
if (!pass.needsUpdate()) {
continue;
}
+ const start = profile ? performance.now() : 0;
pass.execute(encoder, this.resources);
+ if (profile) {
+ frameComputeMs += performance.now() - start;
+ }
+ }
+ if (profile) {
+ profile.frameComputeMs = frameComputeMs;
}
- // Execute render passes in dependency order
+ let territoryPassMs = 0;
+ let temporalResolveMs = 0;
+
+ // Execute render passes in dependency order.
for (const pass of this.renderPassOrder) {
if (!pass.needsUpdate()) {
continue;
}
+
+ const passStart = profile ? performance.now() : 0;
+
if (pass === this.territoryRenderPass && this.postSmoothingEnabled) {
if (!this.resources.getCurrentColorTexture()) {
const viewWidth = this.canvas?.width ?? 1;
@@ -810,27 +840,56 @@ export class WorkerTerritoryRenderer {
this.device.canvasFormat,
);
}
+
const currentTexture = this.resources.getCurrentColorTexture();
if (currentTexture) {
pass.execute(encoder, this.resources, currentTexture.createView());
}
+
+ if (profile) {
+ territoryPassMs += performance.now() - passStart;
+ }
continue;
}
pass.execute(encoder, this.resources, swapchainView);
+
+ if (profile) {
+ const dt = performance.now() - passStart;
+ if (pass === this.territoryRenderPass) {
+ territoryPassMs += dt;
+ } else if (pass === this.temporalResolvePass) {
+ temporalResolveMs += dt;
+ }
+ }
}
+ const submitStart = profile ? performance.now() : 0;
this.device.device.queue.submit([encoder.finish()]);
+
+ if (profile) {
+ profile.territoryPassMs = territoryPassMs;
+ profile.temporalResolveMs = temporalResolveMs;
+ profile.submitMs = performance.now() - submitStart;
+ profile.cpuTotalMs = performance.now() - cpuStart;
+ }
+
if (!this.postSmoothingEnabled) {
this.frameDirty = false;
}
return true;
}
- async renderAsync(): Promise<{
+ async renderAsync(profilePhases: boolean = false): Promise<{
waitPrevGpuMs: number;
cpuMs: number;
getTextureMs: number;
+ submitted: boolean;
+ frameComputeMs?: number;
+ territoryPassMs?: number;
+ temporalResolveMs?: number;
+ submitMs?: number;
+ cpuTotalMs?: number;
gpuWaitMs: number;
waitPrevGpuTimedOut: boolean;
gpuWaitTimedOut: boolean;
@@ -842,6 +901,12 @@ export class WorkerTerritoryRenderer {
const waitPrevGpuMs = 0;
let cpuMs = 0;
let getTextureMs = 0;
+ let submitted = false;
+ let frameComputeMs: number | undefined;
+ let territoryPassMs: number | undefined;
+ let temporalResolveMs: number | undefined;
+ let submitMs: number | undefined;
+ let cpuTotalMs: number | undefined;
const gpuWaitMs = 0;
const waitPrevGpuTimedOut = false;
const gpuWaitTimedOut = false;
@@ -851,9 +916,19 @@ export class WorkerTerritoryRenderer {
this.lastGpuWork = null;
const cpuStart = performance.now();
- const submitted = this.render((ms) => {
+ const profile = profilePhases
+ ? {
+ cpuTotalMs: 0,
+ frameComputeMs: 0,
+ territoryPassMs: 0,
+ temporalResolveMs: 0,
+ submitMs: 0,
+ }
+ : undefined;
+
+ submitted = this.render((ms) => {
getTextureMs = ms;
- });
+ }, profile);
cpuMs = performance.now() - cpuStart;
if (!submitted) {
@@ -862,6 +937,7 @@ export class WorkerTerritoryRenderer {
waitPrevGpuMs,
cpuMs,
getTextureMs,
+ submitted,
gpuWaitMs,
waitPrevGpuTimedOut,
gpuWaitTimedOut,
@@ -875,12 +951,26 @@ export class WorkerTerritoryRenderer {
waitPrevGpuMs,
cpuMs,
getTextureMs,
+ submitted,
+ frameComputeMs,
+ territoryPassMs,
+ temporalResolveMs,
+ submitMs,
+ cpuTotalMs,
gpuWaitMs,
waitPrevGpuTimedOut,
gpuWaitTimedOut,
};
}
+ if (profile) {
+ frameComputeMs = profile.frameComputeMs;
+ territoryPassMs = profile.territoryPassMs;
+ temporalResolveMs = profile.temporalResolveMs;
+ submitMs = profile.submitMs;
+ cpuTotalMs = profile.cpuTotalMs;
+ }
+
const p = q.onSubmittedWorkDone() as Promise;
this.lastGpuWork = p.catch(() => {});
@@ -888,6 +978,12 @@ export class WorkerTerritoryRenderer {
waitPrevGpuMs,
cpuMs,
getTextureMs,
+ submitted,
+ frameComputeMs,
+ territoryPassMs,
+ temporalResolveMs,
+ submitMs,
+ cpuTotalMs,
gpuWaitMs,
waitPrevGpuTimedOut,
gpuWaitTimedOut,