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,