Add WebGPU Debug Overlay and Shader Management

- Introduced WebGPUComputeMetricsEvent to track compute timing.
- Added WebGPUDebugOverlay component for displaying WebGPU performance metrics.
- Refactored TerritoryLayer to utilize new shader management for territory rendering.
- Updated shaders to support new parameters for enhanced visual effects.
- Removed deprecated territory border mode settings from UserSettingModal and SettingsModal.
- Enhanced GroundTruthData to manage new textures for owner indices and relations.
- Improved shader parameter handling in TerritoryRenderer and related classes.

This commit enhances the WebGPU rendering pipeline, providing better performance insights and visual fidelity through improved shader management and debugging capabilities.
This commit is contained in:
scamiv
2026-01-18 19:16:40 +01:00
parent 29e74af95f
commit d82c33863f
12 changed files with 1356 additions and 117 deletions
@@ -0,0 +1,286 @@
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 { WebGPUComputeMetricsEvent } from "../../InputHandler";
import {
TERRITORY_SHADER_KEY,
TERRITORY_SHADERS,
territoryShaderIdFromInt,
territoryShaderIntFromId,
} from "../webgpu/render/TerritoryShaderRegistry";
import { Layer } from "./Layer";
@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[] = [];
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;
}
.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;
}
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) => {
if (typeof e.computeMs === "number" && Number.isFinite(e.computeMs)) {
this.tickComputeMs = e.computeMs;
this.requestUpdate();
}
});
this.requestUpdate();
}
updateFrameMetrics(frameDurationMs: number): void {
if (!this.userSettings || !this.userSettings.webgpuDebug()) {
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 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 renderOptionControl(
option: (typeof TERRITORY_SHADERS)[number]["options"][number],
) {
if (option.kind === "boolean") {
const enabled = this.userSettings.get(option.key, option.defaultValue);
return html`
<div class="row">
<div class="label">${option.label}</div>
<input
type="checkbox"
.checked=${live(enabled)}
@change=${(e: Event) => {
const checked = (e.target as HTMLInputElement).checked;
this.userSettings.set(option.key, checked);
this.requestUpdate();
}}
/>
</div>
`;
}
if (option.kind === "enum") {
const value = this.userSettings.getInt(option.key, option.defaultValue);
return html`
<div class="row">
<div class="label">${option.label}</div>
<select
.value=${live(String(value))}
@change=${(e: Event) => {
const raw = (e.target as HTMLSelectElement).value;
const next = Number.parseInt(raw, 10);
if (!Number.isFinite(next)) return;
this.userSettings.setInt(option.key, next);
this.requestUpdate();
}}
>
${option.options.map(
(o) => html`<option value=${String(o.value)}>${o.label}</option>`,
)}
</select>
</div>
`;
}
const value = this.userSettings.getFloat(option.key, option.defaultValue);
return html`
<div class="row">
<div class="label">${option.label}</div>
<div class="range">
<input
type="range"
min=${String(option.min)}
max=${String(option.max)}
step=${String(option.step)}
.value=${live(String(value))}
@input=${(e: Event) => {
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();
}}
/>
<div class="rangeValue">${value.toFixed(2)}</div>
</div>
</div>
`;
}
render() {
if (!this.userSettings || !this.userSettings.webgpuDebug()) {
return null;
}
const shaderId = this.selectedShaderId();
const shader =
TERRITORY_SHADERS.find((s) => s.id === shaderId) ?? TERRITORY_SHADERS[0];
return html`
<div class="overlay">
<div class="title">
<div>WebGPU Debug</div>
</div>
<div class="metrics">
<div class="metric">
<div class="label">tick ms compute</div>
<div class="value">${this.tickComputeMs.toFixed(2)}</div>
</div>
<div class="metric">
<div class="label">render fps</div>
<div class="value">${this.renderFps}</div>
</div>
</div>
<div class="row">
<div class="label">Territory Shader</div>
<select
.value=${live(String(territoryShaderIntFromId(shaderId)))}
@change=${(e: Event) => {
const raw = (e.target as HTMLSelectElement).value;
const next = territoryShaderIdFromInt(Number.parseInt(raw, 10));
this.setSelectedShaderId(next);
}}
>
${TERRITORY_SHADERS.map(
(s) =>
html`<option value=${String(territoryShaderIntFromId(s.id))}>
${s.label}
</option>`,
)}
</select>
</div>
${shader.options.map((opt) => this.renderOptionControl(opt))}
</div>
`;
}
}