import { css, html, LitElement } from "lit"; import { customElement, property, state } from "lit/decorators.js"; import { live } from "lit/directives/live.js"; import { EventBus } from "../../../core/EventBus"; import { UserSettings } from "../../../core/game/UserSettings"; import { RefreshGraphicsEvent, WebGPUComputeMetricsEvent, } from "../../InputHandler"; import { TERRAIN_SHADER_KEY, TERRAIN_SHADERS, terrainShaderIdFromInt, terrainShaderIntFromId, TerrainShaderOption, } from "../webgpu/render/TerrainShaderRegistry"; import { TERRITORY_POST_SMOOTHING, TERRITORY_POST_SMOOTHING_KEY, territoryPostSmoothingIdFromInt, territoryPostSmoothingIntFromId, } from "../webgpu/render/TerritoryPostSmoothingRegistry"; import { TERRITORY_PRE_SMOOTHING, TERRITORY_PRE_SMOOTHING_KEY, territoryPreSmoothingIdFromInt, territoryPreSmoothingIntFromId, } from "../webgpu/render/TerritoryPreSmoothingRegistry"; import { TERRITORY_SHADER_KEY, TERRITORY_SHADERS, territoryShaderIdFromInt, territoryShaderIntFromId, TerritoryShaderOption, } from "../webgpu/render/TerritoryShaderRegistry"; import { Layer } from "./Layer"; type ShaderOption = TerrainShaderOption | TerritoryShaderOption; @customElement("webgpu-debug-overlay") export class WebGPUDebugOverlay extends LitElement implements Layer { @property({ type: Object }) public eventBus!: EventBus; @property({ type: Object }) public userSettings!: UserSettings; @state() private renderFps: number = 0; @state() private tickComputeMs: number = 0; private frameTimes: number[] = []; private lastDebugEnabled: boolean | null = null; static styles = css` .overlay { position: fixed; top: 16px; left: 16px; z-index: 9999; min-width: 340px; max-width: 420px; background: rgba(0, 0, 0, 0.82); border: 1px solid rgba(255, 255, 255, 0.15); border-radius: 8px; padding: 10px 12px; color: rgba(255, 255, 255, 0.92); font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 12px; pointer-events: auto; user-select: none; } .title { font-weight: 700; margin-bottom: 8px; display: flex; align-items: center; justify-content: space-between; gap: 8px; } .titleLeft { display: flex; align-items: center; gap: 8px; min-width: 0; } .closeButton { appearance: none; border: none; background: transparent; color: rgba(255, 255, 255, 0.75); font-size: 16px; line-height: 16px; padding: 2px 6px; border-radius: 6px; cursor: pointer; } .closeButton:hover { background: rgba(255, 255, 255, 0.08); color: rgba(255, 255, 255, 0.95); } .metrics { display: grid; grid-template-columns: 1fr 1fr; gap: 6px 10px; margin-bottom: 10px; } .metric { display: flex; justify-content: space-between; gap: 10px; white-space: nowrap; } .label { color: rgba(255, 255, 255, 0.7); } .value { color: rgba(255, 255, 255, 0.95); } .row { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin: 6px 0; user-select: none; } .sectionTitle { margin-top: 10px; font-weight: 700; letter-spacing: 0.02em; color: rgba(255, 255, 255, 0.85); text-transform: uppercase; font-size: 11px; } select, input[type="range"] { width: 170px; } select { background: rgba(0, 0, 0, 0.6); color: rgba(255, 255, 255, 0.92); border: 1px solid rgba(255, 255, 255, 0.2); border-radius: 6px; padding: 4px 6px; font-size: 12px; } input[type="checkbox"] { transform: translateY(1px); } .range { display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 10px; } .rangeValue { min-width: 54px; text-align: right; color: rgba(255, 255, 255, 0.8); font-variant-numeric: tabular-nums; } `; init() { this.eventBus.on(WebGPUComputeMetricsEvent, (e) => { this.syncDebugVisibility(); if (typeof e.computeMs === "number" && Number.isFinite(e.computeMs)) { this.tickComputeMs = e.computeMs; this.requestUpdate(); } }); this.requestUpdate(); } private syncDebugVisibility(): boolean { const enabled = !!this.userSettings?.webgpuDebug(); if (this.lastDebugEnabled !== enabled) { this.lastDebugEnabled = enabled; this.requestUpdate(); } return enabled; } updateFrameMetrics(frameDurationMs: number): void { if (!this.syncDebugVisibility()) { return; } if (!Number.isFinite(frameDurationMs) || frameDurationMs <= 0) { return; } this.frameTimes.push(frameDurationMs); if (this.frameTimes.length > 60) { this.frameTimes.shift(); } const avgMs = this.frameTimes.reduce((a, b) => a + b, 0) / this.frameTimes.length; this.renderFps = Math.round(1000 / Math.max(1e-6, avgMs)); this.requestUpdate(); } private close(): void { if (!this.userSettings) { return; } this.userSettings.set("settings.webgpuDebug", false); this.syncDebugVisibility(); } private selectedShaderId() { const selected = this.userSettings.getInt(TERRITORY_SHADER_KEY, 0); return territoryShaderIdFromInt(selected); } private setSelectedShaderId(id: "classic" | "retro") { this.userSettings.setInt( TERRITORY_SHADER_KEY, territoryShaderIntFromId(id), ); this.requestUpdate(); } private selectedTerrainShaderId() { const selected = this.userSettings.getInt(TERRAIN_SHADER_KEY, 0); return terrainShaderIdFromInt(selected); } private setSelectedTerrainShaderId( id: "classic" | "improved-lite" | "improved-heavy", ) { this.userSettings.setInt(TERRAIN_SHADER_KEY, terrainShaderIntFromId(id)); this.requestUpdate(); } private selectedPreSmoothingId() { const selected = this.userSettings.getInt(TERRITORY_PRE_SMOOTHING_KEY, 0); return territoryPreSmoothingIdFromInt(selected); } private setSelectedPreSmoothingId(id: "off" | "dissolve" | "budget") { this.userSettings.setInt( TERRITORY_PRE_SMOOTHING_KEY, territoryPreSmoothingIntFromId(id), ); this.requestUpdate(); } private selectedPostSmoothingId() { const selected = this.userSettings.getInt(TERRITORY_POST_SMOOTHING_KEY, 0); return territoryPostSmoothingIdFromInt(selected); } private setSelectedPostSmoothingId(id: "off" | "fade" | "dissolve") { this.userSettings.setInt( TERRITORY_POST_SMOOTHING_KEY, territoryPostSmoothingIntFromId(id), ); this.requestUpdate(); } private renderOptionControl(option: ShaderOption) { if (option.kind === "boolean") { const enabled = this.userSettings.get(option.key, option.defaultValue); return html`
${option.label}
{ const checked = (e.target as HTMLInputElement).checked; this.userSettings.set(option.key, checked); this.requestUpdate(); }} />
`; } if (option.kind === "enum") { const value = this.userSettings.getInt(option.key, option.defaultValue); return html`
${option.label}
`; } const value = this.userSettings.getFloat(option.key, option.defaultValue); return html`
${option.label}
{ const raw = (e.target as HTMLInputElement).value; const next = Number.parseFloat(raw); if (!Number.isFinite(next)) return; this.userSettings.setFloat(option.key, next); this.requestUpdate(); }} />
${value.toFixed(2)}
`; } render() { if (!this.userSettings || !this.userSettings.webgpuDebug()) { return null; } const backgroundRenderer = this.userSettings.backgroundRenderer(); const shaderId = this.selectedShaderId(); const shader = TERRITORY_SHADERS.find((s) => s.id === shaderId) ?? TERRITORY_SHADERS[0]; const terrainShaderId = this.selectedTerrainShaderId(); const terrainShader = TERRAIN_SHADERS.find((s) => s.id === terrainShaderId) ?? TERRAIN_SHADERS[0]; const preId = this.selectedPreSmoothingId(); const pre = TERRITORY_PRE_SMOOTHING.find((s) => s.id === preId) ?? TERRITORY_PRE_SMOOTHING[0]; const postId = this.selectedPostSmoothingId(); const post = TERRITORY_POST_SMOOTHING.find((s) => s.id === postId) ?? TERRITORY_POST_SMOOTHING[0]; return html`
WebGPU Debug
Renderer
Background
tick ms compute
${this.tickComputeMs.toFixed(2)}
render fps
${this.renderFps}
Terrain
Terrain Shader
${terrainShader.options.map((opt) => this.renderOptionControl(opt))}
Territory
Territory Shader
${shader.options.map((opt) => this.renderOptionControl(opt))}
Temporal
Post Compute
${pre.options.map((opt) => this.renderOptionControl(opt))}
Post Render
${post.options.map((opt) => this.renderOptionControl(opt))}
`; } }