Renderer fallback, Pixi name layer, and smoothed unit motion#4054
Renderer fallback, Pixi name layer, and smoothed unit motion#4054scamiv wants to merge 53 commits into
Conversation
## Description: Show bonus amount on currency packs - Add `bonusAmount` field to `PackSchema` (non-negative int) - Render a rotated green corner ribbon (`+X FREE!`) on pack tiles when `bonusAmount > 0` - Add `cosmetics.free` translation key with `numFree` param <img width="720" height="359" alt="Screenshot 2026-05-12 at 7 40 12 AM" src="https://github.com/user-attachments/assets/3dd70fc4-c922-47f4-aee6-055047b58563" /> Describe the PR. ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: evan
…based architecture Refactor the monolithic TerritoryWebGLRenderer into a modular, extensible architecture that separates ground truth computation from rendering passes. This change also includes related improvements to game state management and hover information handling. WebGPU Architecture Refactor: - Extract all shaders to external .wgsl files (no inlined shaders) - Separate ground truth data management (GroundTruthData) from rendering - Create pass-based architecture with ComputePass and RenderPass interfaces - Implement compute passes: StateUpdatePass, DefendedClearPass, DefendedUpdatePass - Implement render pass: TerritoryRenderPass - Add TerritoryRenderer orchestrator with dependency-based execution ordering - Add WebGPUDevice for device initialization and management - Add ShaderLoader utility for loading .wgsl files via Vite ?raw imports Performance Optimizations: - Dependency order computed once at init (topological sort) - Early exit checks at orchestrator and pass levels - Bind groups rebuilt when textures/buffers are recreated - Zero per-frame allocations (reuse command encoders and staging buffers) Architecture Benefits: - Easy to extend with new compute/render passes (borders, temporal smoothing, etc.) - Clear separation between tick-based compute and frame-based rendering - All shaders in external files for better maintainability - Ground truth data computed once and reused by all passes Related Changes: - Add defended tile state support to GameMap (isDefended/setDefended) - Expose tileStateView() for direct GPU state access - Extract hover info logic to HoverInfo utility - Remove TerrainLayer (terrain now rendered by WebGPU territory pass) - Update GameRenderer to use transparent overlay canvas - Add viewOffset() method to TransformHandler Files: - Deleted: TerritoryWebGLRenderer.ts (1217 lines), TerrainLayer.ts (77 lines) - Added: 17 new files in webgpu/ directory structure - Updated: TerritoryLayer.ts, GameRenderer.ts, PlayerInfoOverlay.ts, GameMap.ts, GameView.ts, GameImpl.ts, TransformHandler.ts, vite-env.d.ts
Updated the terrain recomputation logic to trigger asynchronously, improving performance by allowing rendering to continue without blocking. This change ensures that the terrain will be ready for the next frame, which may result in displaying stale terrain for one frame but enhances overall rendering efficiency.
Modified the workgroup size in the state-update compute shader from 1 to 64 for improved parallel processing. Adjusted the dispatch logic in StateUpdatePass to calculate the correct number of workgroups based on the new size, enhancing performance during state updates. Removed unnecessary terrain parameter upload in TerritoryRenderer to streamline resource management.
Replaced tile sampling for terrain colors with direct extraction from the theme object, significantly improving performance. Updated shore, water, shoreline water, plains, highland, and mountain color computations to utilize theme properties, eliminating the need for tile searches. This change enhances efficiency in terrain color management while maintaining visual fidelity.
Replace epoch-based defended texture tracking with hard-clear approach for improved reliability and performance. Remove complex dirty state tracking and epoch incrementing logic in favor of direct hard clears before rebuilds. Changes: - Remove wasDefensePostsDirty tracking from TerritoryRenderer - Replace numUpdates > 0 checks with hasStateUpdates boolean - Hard-clear defended texture before restamping instead of epoch management - Mark DefendedUpdatePass as dirty when rebuilding defended state - Rebuild bind group in DefendedUpdatePass when missing, not just on buffer change This eliminates potential transient mismatches where defended rendering disappeared between rebuilds and simplifies the update pipeline.
Store defended influence in defendedStrengthTexture and sample it in territory render shader Recompute defended strength on tick for state-updated tiles and for post-change dirty tiles, with full-map fallback when diffs are large Pack defense posts by owner on GPU (owner offsets + posts buffer) Remove old defended clear/update passes and epoch-based params
…t.meta.glob(..., { as: "raw", eager: true })
- 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.
- Included <webgpu-debug-overlay> component in the main layout
Add user-selectable temporal smoothing pipeline to create smooth visual transitions between simulation ticks (~10Hz) and display frames (~60Hz). Pre-render smoothing provides sharp tile dissolve transitions using compute shaders, while post-render smoothing blends frames for fluid animation. Includes tick timing with exponential moving averages for stable temporal parameters. New Components: - TerritoryPreSmoothingRegistry & TerritoryPostSmoothingRegistry for mode selection - VisualStateSmoothingPass compute shader for pre-render dissolve effects - TemporalResolvePass render shader for post-render temporal compositing - Enhanced GroundTruthData with temporal uniforms and history textures Performance: No full-map per-frame compute, single fullscreen post-render pass. Compatible with all territory shaders (classic, retro, future variants). Files: 12 files changed, 1357 insertions(+), 8 deletions(-)
- Add terrain-compute-improved-lite.wgsl and terrain-compute-improved-heavy.wgsl - Create TerrainShaderRegistry.ts for shader management - Refactor TerrainComputePass to support dynamic shader switching - Update TerritoryRenderer, TerritoryLayer, and GroundTruthData for new shader integration - Enhance WebGPUDebugOverlay with additional debugging capabilities
…ault values - Changed section title from "Shaders" to "Terrain" in WebGPUDebugOverlay. - Updated default values for various terrain shader parameters to improve rendering quality, including noise strength, blend width, lighting strength, and cavity strength.
- Modified terrain shader parameters in GroundTruthData for better rendering. - Added new user-configurable settings for water effects in TerrainShaderRegistry. - Enhanced terrain compute shaders to incorporate water depth and blur adjustments. - Refactored shader logic to improve water color blending and depth calculations
…oad in tests Track permanently failed texture URLs so getTexture does not call PIXI.Assets.load again after a rejection. Extend resetWarningsForTests to clear failedTextures and to reset preloadPromise, fontReady, and fontFamily so tests can run preload() and loadBaseAssets() from a clean state.
Replace node-canvas in the namelayer asset builder with skia-canvas so icon and emoji atlases are rasterized through the same cross-platform rendering backend. This removes the previous dependency on host Cairo/Pango emoji rendering, which could produce monochrome emoji sprites depending on the local font stack. Add twemoji-colr-font as a pinned COLR/CPAL emoji font input for the build. The emoji atlas now renders from that explicit font instead of whichever OS emoji font happens to be installed, making the generated sprite sheet stable across developer machines and CI. Generalize atlas frame packing for both SVG icons and emoji glyphs: render into a scratch canvas, trim to alpha bounds, center, and scale into the cell with consistent padding. This prevents icon frames from touching cell edges while preserving the existing Pixi atlas JSON contract. Regenerate namelayer emoji and icon atlases. The builder now validates generated atlas frames and fails if any frame is empty or if the emoji atlas contains no color pixels, catching the monochrome-regeneration failure at build time.
Make layer initialization awaitable and have GameRenderer wait for each layer init before input and worker ticks start. This prevents the Pixi NameLayer from seeing players before its async font, atlas, and renderer setup has completed. Also store the AlternateViewEvent handler on NameLayer so destroy() can unsubscribe it, matching the existing resize listener cleanup and preventing torn-down instances from mutating state.
Pass the pinned Twemoji font path to FontLibrary.use as an explicit path array, name the color-detection threshold used by atlas validation, and make SVG viewBox width/height extraction easier to read. Validated with the namelayer asset build and eslint for scripts/build-namelayer-assets.mjs.
Keep the NameLayer emoji atlas generation deterministic by loading Twemoji COLR from resources/fonts instead of resolving it through the twemoji-colr-font npm package. The font file and its OFL license are now checked in next to the other namelayer font assets. This removes the deprecated package dependency while preserving the current Twemoji output until the project chooses a final emoji font/set.
WalkthroughAdds backend-driven territory rendering (WebGPU/WebGL/Classic) with selection and status/debug overlays, a Pixi-based NameLayer with generated font/atlases, a refactored UnitLayer mover/trail engine, new user settings and i18n, and supporting build/config updates. ChangesTerritory renderer architecture and overlays
NameLayer Pixi refactor and asset pipeline
UnitLayer dynamic movers and trails
Settings, selects, labels, and small UI tweaks
Startup sequencing and container tweak
Sequence Diagram(s)(omitted) Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
|
There was a problem hiding this comment.
Actionable comments posted: 18
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/client/graphics/layers/PlayerInfoOverlay.ts (1)
118-143:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winGuard
playerProfilefrom stale hover updates.
hide()clears the visible target, butplayerProfilestays live, and an olderprofile()promise can still win later. That can briefly show the wrong relation data for the next hovered player.Suggested fix
public hide() { this.setVisible(false); this.unit = null; this.player = null; + this.playerProfile = null; this.isWilderness = false; this.isIrradiatedWilderness = false; } public maybeShow(x: number, y: number) { this.hide(); const worldCoord = this.transform.screenToWorldCoordinates(x, y); const info = getHoverInfo(this.game, worldCoord); if (info.player) { this.player = info.player; + this.playerProfile = null; + const hoveredPlayerId = info.player.id(); this.player.profile().then((p) => { - this.playerProfile = p; + if (this.player?.id() === hoveredPlayerId) { + this.playerProfile = p; + } }); this.setVisible(true);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/client/graphics/layers/PlayerInfoOverlay.ts` around lines 118 - 143, hide() currently doesn't clear playerProfile and an earlier async player.profile() call can overwrite profile state for a new hover; update hide() to also set playerProfile = null, and in maybeShow() when you call this.player.profile() capture the target (e.g., const current = this.player or player id) and only assign this.playerProfile = p if the hovered player still matches (e.g., this.player === current or ids match), so stale promises don't update the UI; touch symbols: hide(), maybeShow(), playerProfile, player.profile(), and this.player.
🟡 Minor comments (4)
src/client/components/baseComponents/setting/SettingSelect.ts-13-13 (1)
13-13:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winUse a unique default
idper instance.A fixed default id can create duplicate DOM ids across multiple
setting-selectcomponents, which breaks label-to-select targeting.🔧 Proposed fix
export class SettingSelect extends LitElement { + private static nextId = 0; `@property`() label = "Setting"; `@property`() description = ""; - `@property`() id = "setting-select-input"; + `@property`() id = `setting-select-input-${SettingSelect.nextId++}`;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/client/components/baseComponents/setting/SettingSelect.ts` at line 13, The default id property on the SettingSelect component is static ("setting-select-input") and can collide across instances; change the `@property`() id handling in the SettingSelect class so each instance gets a unique id (e.g., append a generated suffix or use an incrementing counter) when the component is constructed or in connectedCallback if id is not explicitly provided; update references in the class (e.g., the id property and any label-for or querySelector usage) to use the new unique id so label-to-select targeting remains correct per instance.src/client/graphics/layers/TerritoryLayer.ts-28-31 (1)
28-31:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winLocalize the renderer status messages.
These strings go through
publishStatus()and surface in the renderer status UI, so hardcoded English here skips the client i18n path. Please translate the fixed parts and use placeholders for backend/reason values.As per coding guidelines,
src/client/**/*.{ts,tsx}: All user-visible text must go throughtranslateText()function with corresponding entries inresources/lang/en.json.Also applies to: 56-60, 129-133, 206-212
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/client/graphics/layers/TerritoryLayer.ts` around lines 28 - 31, Replace hardcoded user-facing status strings passed to publishStatus() (e.g., in the settingsChanged() method and the other occurrences at the blocks around lines 56-60, 129-133, 206-212) with calls to translateText() using message keys and placeholders for dynamic values (backend/reason); for example use translateText("renderer.retrying", { backend: backendName }) instead of "Retrying renderer selection", and similarly add keys like "renderer.failed", "renderer.selected" with placeholders for backend and reason, then add corresponding entries in resources/lang/en.json. Locate the calls in the methods settingsChanged, selectConfiguredBackend (and the other status-publishing blocks noted) and replace hardcoded English with translateText() keys, passing the backend/reason values as the interpolation object so UI localization is preserved.src/client/graphics/layers/WebGLTerritoryBackend.ts-452-873 (1)
452-873:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winMove the debug panel and overlay labels into i18n.
The panel title, control labels, button text, and overlay lines are all visible to users. Keeping them as inline English strings here bypasses
translateText()andresources/lang/en.json.As per coding guidelines,
src/client/**/*.{ts,tsx}: All user-visible text must go throughtranslateText()function with corresponding entries inresources/lang/en.json.Also applies to: 1670-1719
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/client/graphics/layers/WebGLTerritoryBackend.ts` around lines 452 - 873, The UI text in ensureSmoothingDebugUi (e.g., titleText "Territory smoothing", closeButton "x"/title "Close", tripleText "triple buffer (olderPrev)", modeText "delay mode:", contestedText "contested draw", contestedModeText "contested pattern:", allBordersText "hide all borders", staticBordersText "hide static borders", seedSamplingText "seed sampling", motionModeText "motion mode", stripeColorsText "fixed stripe colors (red=expand, blue=retreat, green=owner)" and any other inline labels/options added by seedSamplingSelect/modeSelect/contestedModeSelect/motionModeSelect) must be wrapped with translateText() and have matching keys added to resources/lang/en.json; update each element creation to call translateText('your.key') for textContent/title/option text and use translated option strings for select values, and add the corresponding i18n entries in resources/lang/en.json (use clear keys like ui.debug.territorySmoothing.title, ui.debug.territorySmoothing.close, ui.debug.territorySmoothing.tripleBuffer, etc.) so all visible strings go through the translation system.src/client/graphics/webgpu/TerritoryRenderer.ts-82-92 (1)
82-92:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winTranslate these exported failure reasons.
These strings are returned through
create().reasonandgetFailureReason(), so they are part of the client status surface. They should come fromtranslateText()with keys inresources/lang/en.json, not hardcoded English. As per coding guidelines,src/client/**/*.{ts,tsx}: All user-visible text must go throughtranslateText()function with corresponding entries inresources/lang/en.json.Also applies to: 114-120
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/client/graphics/webgpu/TerritoryRenderer.ts` around lines 82 - 92, The hardcoded user-facing failure messages returned from create() (and exposed via getFailureReason()) should be replaced with translated strings: call translateText() for each reason and add matching keys in resources/lang/en.json (e.g., "webgpu.tileBufferSizeMismatch" and "webgpu.notAvailable"); update the two return objects that currently contain "Tile state buffer size mismatch; GPU renderer disabled." and "WebGPU not available; GPU renderer disabled." (also the similar strings at the later block around lines 114-120) to use translateText("...") and ensure the new keys are present in resources/lang/en.json with corresponding English text.
🧹 Nitpick comments (1)
src/client/graphics/webgpu/render/TerritoryRenderPass.ts (1)
122-124: ⚡ Quick winAvoid rebuilding the bind group every frame.
execute()currently callsrebuildBindGroup()unconditionally. In this hot path, caching/rebuilding only on resource identity changes will reduce avoidable GPU/CPU overhead.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/client/graphics/webgpu/render/TerritoryRenderPass.ts` around lines 122 - 124, In execute() you currently call rebuildBindGroup() every frame; change this to only rebuild when resources change by adding a cached state or "dirty" flag checked in execute() (e.g., a private boolean like needsRebuild or cached resource identifiers such as lastTextureId/lastSamplerId) and only call rebuildBindGroup() when that flag/identity differs; set that flag or update the cached ids inside the code paths that recreate or replace textures/samplers (where texture recreation occurs) so rebuildBindGroup() runs only on actual resource identity changes rather than every frame.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/client/ClientGameRunner.ts`:
- Around line 145-148: When assigning and starting the new runner in the promise
chain (currentGameRunner = r; await r.start()), wrap the start() call in a
try/catch so that if r.start() (or downstream renderer.initialize()) throws you
immediately perform cleanup: call the runner's shutdown method(s) (e.g.,
r.stop() or r.dispose() / whichever cleanup API exists), clear currentGameRunner
(set to null/undefined), and then rethrow the error so the rejection is
propagated; apply the same pattern to the other startup site referenced (the
block around lines 370-404) to prevent timers/listeners/audio leaks on partial
startup failures.
In `@src/client/graphics/GameRenderer.ts`:
- Around line 246-255: The code queries for webgpuDebugOverlay and then uses it
even when the instanceof check fails; update the block around the
webgpuDebugOverlay variable so that after const webgpuDebugOverlay =
document.querySelector("webgpu-debug-overlay") as WebGPUDebugOverlay you
immediately check if (!(webgpuDebugOverlay instanceof WebGPUDebugOverlay)) and
return early or throw to avoid dereferencing a bad value, then only assign
webgpuDebugOverlay.eventBus = eventBus, webgpuDebugOverlay.userSettings =
userSettings and call webgpuDebugOverlay.requestUpdate() when the guard passes;
this same guarding pattern should be applied to the other similar overlay usages
in the file.
In `@src/client/graphics/layers/ClassicCanvasTerritoryLayer.ts`:
- Around line 557-589: The border-paint path ignores the computed isHighlighted
and playerIsFocused flags so highlight/focus never affect rendering; update the
border branch in ClassicCanvasTerritoryLayer (the code that calls
owner.borderColor(...) and paintTile(...)) to actually use those flags when
deciding the color: either extend owner.borderColor(tile, ...) to accept
highlight/focus (e.g., owner.borderColor(tile, isDefended, isHighlighted,
playerIsFocused)) or compute a modified borderColor before paintTile based on
isHighlighted/playerIsFocused (for example choose a highlighted/focused variant
or blend), remove the eslint ignore on playerIsFocused, and pass that resulting
color into paintTile so highlights and focused-player rendering take effect.
In `@src/client/graphics/layers/ClassicTerritoryBackend.ts`:
- Around line 55-58: ClassicTerritoryBackend.dispose is currently empty while
ClassicCanvasTerritoryLayer.init registers MouseOverEvent, AlternateViewEvent,
and DragEvent handlers; fix by having the layer/backend keep references to the
actual listener functions (store them as instance properties when bind/register
in ClassicCanvasTerritoryLayer.init) and remove/unregister those handlers in
ClassicTerritoryBackend.dispose by calling the appropriate EventBus removal API
(e.g., eventBus.off/removeListener/unsubscribe depending on the EventBus
implementation) for MouseOverEvent, AlternateViewEvent and DragEvent so the
deactivated backend stops receiving events and releases its state.
In `@src/client/graphics/layers/PerformanceOverlay.ts`:
- Around line 1314-1353: The new hardcoded UI labels in PerformanceOverlay (e.g.
"UnitLayer Counters", "tracked", "sampled", "drawn", "skipped",
"moverCanvasScale", "rescale(last/avg/count)", "draw", "on", "off", "avgOnDebt",
"maxOnDebt") must be wrapped with translateText() and corresponding keys added
to resources/lang/en.json; update the template render in PerformanceOverlay
(where unitLayerCounters is used) to call translateText("...") for each visible
string and add matching English entries in resources/lang/en.json using clear,
namespaced keys (e.g. performance.unitLayer.counters,
performance.unitLayer.tracked, etc.).
In `@src/client/graphics/layers/RendererStatusPanel.ts`:
- Around line 259-263: Replace all hardcoded user-visible strings in
RendererStatusPanel.ts with calls to translateText() and add matching en.json
keys; specifically update getRendererName(id) (currently returning "WebGPU",
"WebGL", "Classic", "Auto") to return translateText(...) values (e.g., keys like
"renderer.name.webgpu", "renderer.name.webgl", "renderer.name.classic",
"renderer.name.auto") and similarly replace UI labels "Renderer", "settings",
"Active", "Pending", "Saved", "Skipped this cycle…", "Fallback from …" found in
the other referenced blocks (around lines 267-277, 403-423, 426-436) with
translateText() calls using descriptive keys (e.g., "renderer.title",
"renderer.settings", "renderer.status.active", "renderer.status.pending",
"renderer.status.saved", "renderer.status.skipped", "renderer.status.fallback")
and add those keys to resources/lang/en.json.
In `@src/client/graphics/layers/SettingsModal.ts`:
- Around line 545-548: In the SettingsModal component replace the two hardcoded
strings "WebGPU Debug" and "Territory shader selection + options" with calls to
translateText(), e.g., translateText('settings.webgpuDebug.title') and
translateText('settings.webgpuDebug.description'), and add corresponding keys
and English values into resources/lang/en.json; ensure you import translateText
where used and keep the translation keys consistent with existing naming
conventions in the SettingsModal component.
In `@src/client/graphics/layers/StructureIconsLayer.ts`:
- Around line 138-149: The DPR resize multiplies the canvas backing buffer but
culling/visibility must stay in CSS pixels; update resizePixiCanvasElement to
set the canvas.style.width/height to window.innerWidth/innerHeight (and keep the
backing store scaled by resolution) and ensure you maintain
viewportWidthCss/viewportHeightCss as CSS pixels
(window.innerWidth/innerHeight). Then change the visibility checks in
computeNewLocation to compare screenPos against
viewportWidthCss/viewportHeightCss (use the suggested onScreen condition with
margin) so culling uses CSS bounds rather than this.pixicanvas.width/height.
In `@src/client/graphics/layers/TerritoryBackend.ts`:
- Around line 91-98: The loop over territoryRendererOrder currently calls
createBackend(id) outside the try, so if the backend constructor throws the loop
exits without recording the failure or falling back; move the creation into the
try (or wrap createBackend(id) in the same try) and catch any errors thrown
during construction or init, record the failure in the failures array (including
id and error), ensure any partially constructed backend is cleaned up if needed,
and continue to the next id; apply the same pattern to the later fallback loop
around the code that currently calls createBackend and await backend.init
(referencing createBackend, backend.init, territoryRendererOrder,
shouldContinue).
In `@src/client/graphics/layers/TerritoryLayer.ts`:
- Around line 88-95: dispose() currently leaves selectionToken valid and
initializeCandidate() can return false without disposing the candidate; update
dispose() to cancel/invalidate any in-flight selectionToken (call its
cancel/abort method or set a flag on the token) before nulling activeBackend and
removing listeners, and ensure initializeCandidate() disposes/cleans up the
candidate when it returns false or on cancellation. Specifically, in
TerritoryLayer.dispose() cancel the selectionToken (if present) and await/ensure
any backend activation completes or is aborted, then call
activeBackend.dispose() and set activeBackend = null; in initializeCandidate()
detect cancellation/failure paths and call candidate.dispose()/cleanup to remove
listeners and GPU state before returning false, referencing the selectionToken,
initializeCandidate, activeBackend, settingsChanged and TERRITORY_RENDERER_KEY
symbols.
In `@src/client/graphics/layers/UnitLayer.ts`:
- Around line 2218-2231: The returned bounding rect and any draw math are
incorrectly using the sprite's width for the height, which breaks non-square
sprites; in the block using resolveSprite (and the similar block around the
other occurrence), keep using const width = (sprite as { width: number }).width
and const height = (sprite as { height: number }).height, but change the
returned h calculation from width + pad * 2 to height + pad * 2 and ensure any
draw/Y math uses height (e.g., drawY, outY, and the h value) instead of width so
clearing/redrawing uses the sprite's actual height.
In `@src/client/graphics/layers/WebGPUDebugOverlay.ts`:
- Around line 204-209: The WebGPU event handlers are registered inline so they
cannot be removed on detach, causing handler accumulation; store the listener
functions as class properties (e.g., this.onComputeMetrics and the corresponding
render/other handler used in lines ~213-220), register them with
this.eventBus.on(WebGPUComputeMetricsEvent, this.onComputeMetrics) and the other
event similarly, and then remove them in disconnectedCallback by calling
this.eventBus.off(WebGPUComputeMetricsEvent, this.onComputeMetrics) (and the
matching off calls for the other event handlers).
- Around line 510-530: Replace all hardcoded visible strings in the
WebGPUDebugOverlay template with calls to translateText(...) and add
corresponding keys to resources/lang/en.json; specifically wrap the header
"WebGPU Debug" and metric labels like "tick ms compute" and "render fps", plus
section titles "Terrain", "Territory", and "Temporal" with translateText (e.g.,
translateText('webgpu.debug.title') etc.), update the template in
WebGPUDebugOverlay (the JSX/HTML template where this.tickComputeMs and
this.renderFps are rendered) to call translateText for those labels, and add
matching entries in en.json for each key you introduce; keep numeric values
(this.tickComputeMs.toFixed(2), this.renderFps) unchanged so only labels are
localized.
In `@src/client/graphics/webgpu/render/TerrainShaderRegistry.ts`:
- Around line 36-155: TERRAIN_SHADERS contains hardcoded, user-facing labels and
option labels (e.g., the "label" fields on shader objects like id "classic",
"improved-lite", "improved-heavy" and the option "label" values such as "Noise
Strength", "Biome Blend Width", etc.); replace each raw string with a call to
translateText(...) and add corresponding keys to resources/lang/en.json
(suggested keys: e.g. "terrain.shader.classic.label",
"terrain.shader.improvedLite.label", "terrain.shader.improvedHeavy.label" and
option keys like "terrain.shader.improvedLite.noiseStrength",
"terrain.shader.improvedLite.blendWidth",
"terrain.shader.improvedLite.waterBlurStrength", plus the improvedHeavy option
keys) so every user-visible string in TERRAIN_SHADERS uses
translateText('your.key') and the new keys are populated in
resources/lang/en.json with the English text.
In `@src/client/graphics/webgpu/render/TerritoryPostSmoothingRegistry.ts`:
- Around line 15-62: The TERRITORY_POST_SMOOTHING array currently contains
hard-coded user-facing strings ("Off", "Fade", "Dissolve", "Blend Strength",
"Dissolve Width"); update the definitions in TERRITORY_POST_SMOOTHING so each
label and each option.label calls translateText() with a new i18n key (e.g.
"territory.postSmoothing.off", "territory.postSmoothing.fade",
"territory.postSmoothing.dissolve", "territory.postSmoothing.blendStrength",
"territory.postSmoothing.dissolveWidth") and add corresponding entries to
resources/lang/en.json; ensure keys are consistent and used for both the
top-level label fields and the nested options' label fields so all user-visible
text goes through translateText().
In `@src/client/graphics/webgpu/render/TerritoryPreSmoothingRegistry.ts`:
- Around line 15-53: Replace hard-coded user-visible strings in the
TERRITORY_PRE_SMOOTHING array with i18n keys and translateText() calls: change
label values "Off", "Dissolve", "Budgeted Reveal" and the option label "Reveal
Curve" to calls like translateText("[...]") (keeping the same unique ids "off",
"dissolve", "budget" etc. so behavior doesn't change) and add corresponding keys
and English entries to resources/lang/en.json (e.g. territory.preSmoothing.off,
territory.preSmoothing.dissolve, territory.preSmoothing.budget,
territory.preSmoothing.revealCurve) so translateText resolves them; ensure you
import/usage of translateText is consistent with surrounding modules (same
import pattern) and leave non-user-visible fields (id, wgslPath, option keys)
unchanged.
In `@src/client/graphics/webgpu/TerritoryRenderer.ts`:
- Around line 263-288: In setViewSize, always apply the requested canvas.width
and canvas.height immediately (use nextWidth/nextHeight) even if this.resources
or this.device are not ready; then early-return only after setting the canvas
dimensions. When device/resources exist continue to call
this.resources.setViewSize(nextWidth,nextHeight), this.device.reconfigure(),
and, if this.postSmoothingEnabled,
this.resources.ensurePostSmoothingTextures(...) and
this.resources.invalidateHistory(). This ensures WebGPUDevice.create sees the
latest canvas size while preserving existing behavior when resources/device are
available.
In `@src/client/UserSettingModal.ts`:
- Around line 780-784: The option labels for the selects in UserSettingModal are
hardcoded in English; update the arrays used in the .options property (the
arrays around the existing entries { value: 0, label: "Off" } etc. and the
similar array at lines ~794-798) to call translateText("...") for each label
(e.g., translateText("userSetting.off"), translateText("userSetting.simple"),
translateText("userSetting.glow")), and add the corresponding keys and English
strings to resources/lang/en.json; ensure you keep the same value numbers and
only replace the label strings with translateText calls so the Select components
(in UserSettingModal) remain unchanged.
---
Outside diff comments:
In `@src/client/graphics/layers/PlayerInfoOverlay.ts`:
- Around line 118-143: hide() currently doesn't clear playerProfile and an
earlier async player.profile() call can overwrite profile state for a new hover;
update hide() to also set playerProfile = null, and in maybeShow() when you call
this.player.profile() capture the target (e.g., const current = this.player or
player id) and only assign this.playerProfile = p if the hovered player still
matches (e.g., this.player === current or ids match), so stale promises don't
update the UI; touch symbols: hide(), maybeShow(), playerProfile,
player.profile(), and this.player.
---
Minor comments:
In `@src/client/components/baseComponents/setting/SettingSelect.ts`:
- Line 13: The default id property on the SettingSelect component is static
("setting-select-input") and can collide across instances; change the
`@property`() id handling in the SettingSelect class so each instance gets a
unique id (e.g., append a generated suffix or use an incrementing counter) when
the component is constructed or in connectedCallback if id is not explicitly
provided; update references in the class (e.g., the id property and any
label-for or querySelector usage) to use the new unique id so label-to-select
targeting remains correct per instance.
In `@src/client/graphics/layers/TerritoryLayer.ts`:
- Around line 28-31: Replace hardcoded user-facing status strings passed to
publishStatus() (e.g., in the settingsChanged() method and the other occurrences
at the blocks around lines 56-60, 129-133, 206-212) with calls to
translateText() using message keys and placeholders for dynamic values
(backend/reason); for example use translateText("renderer.retrying", { backend:
backendName }) instead of "Retrying renderer selection", and similarly add keys
like "renderer.failed", "renderer.selected" with placeholders for backend and
reason, then add corresponding entries in resources/lang/en.json. Locate the
calls in the methods settingsChanged, selectConfiguredBackend (and the other
status-publishing blocks noted) and replace hardcoded English with
translateText() keys, passing the backend/reason values as the interpolation
object so UI localization is preserved.
In `@src/client/graphics/layers/WebGLTerritoryBackend.ts`:
- Around line 452-873: The UI text in ensureSmoothingDebugUi (e.g., titleText
"Territory smoothing", closeButton "x"/title "Close", tripleText "triple buffer
(olderPrev)", modeText "delay mode:", contestedText "contested draw",
contestedModeText "contested pattern:", allBordersText "hide all borders",
staticBordersText "hide static borders", seedSamplingText "seed sampling",
motionModeText "motion mode", stripeColorsText "fixed stripe colors (red=expand,
blue=retreat, green=owner)" and any other inline labels/options added by
seedSamplingSelect/modeSelect/contestedModeSelect/motionModeSelect) must be
wrapped with translateText() and have matching keys added to
resources/lang/en.json; update each element creation to call
translateText('your.key') for textContent/title/option text and use translated
option strings for select values, and add the corresponding i18n entries in
resources/lang/en.json (use clear keys like ui.debug.territorySmoothing.title,
ui.debug.territorySmoothing.close, ui.debug.territorySmoothing.tripleBuffer,
etc.) so all visible strings go through the translation system.
In `@src/client/graphics/webgpu/TerritoryRenderer.ts`:
- Around line 82-92: The hardcoded user-facing failure messages returned from
create() (and exposed via getFailureReason()) should be replaced with translated
strings: call translateText() for each reason and add matching keys in
resources/lang/en.json (e.g., "webgpu.tileBufferSizeMismatch" and
"webgpu.notAvailable"); update the two return objects that currently contain
"Tile state buffer size mismatch; GPU renderer disabled." and "WebGPU not
available; GPU renderer disabled." (also the similar strings at the later block
around lines 114-120) to use translateText("...") and ensure the new keys are
present in resources/lang/en.json with corresponding English text.
---
Nitpick comments:
In `@src/client/graphics/webgpu/render/TerritoryRenderPass.ts`:
- Around line 122-124: In execute() you currently call rebuildBindGroup() every
frame; change this to only rebuild when resources change by adding a cached
state or "dirty" flag checked in execute() (e.g., a private boolean like
needsRebuild or cached resource identifiers such as lastTextureId/lastSamplerId)
and only call rebuildBindGroup() when that flag/identity differs; set that flag
or update the cached ids inside the code paths that recreate or replace
textures/samplers (where texture recreation occurs) so rebuildBindGroup() runs
only on actual resource identity changes rather than every frame.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: fe3ae018-85b9-4bb4-930c-124aea231d4c
⛔ Files ignored due to path filters (6)
package-lock.jsonis excluded by!**/package-lock.jsonresources/fonts/namelayer_overpass.pngis excluded by!**/*.pngresources/fonts/overpass-regular.otfis excluded by!**/*.otfresources/fonts/twemoji-colr.woff2is excluded by!**/*.woff2resources/images/namelayer-emojis.pngis excluded by!**/*.pngresources/images/namelayer-icons.pngis excluded by!**/*.png
📒 Files selected for processing (98)
Dockerfileeslint.config.jsindex.htmlpackage.jsonresources/fonts/namelayer_overpass.xmlresources/fonts/overpass-OFL.txtresources/fonts/twemoji-colr-OFL.txtresources/images/namelayer-emojis.jsonresources/images/namelayer-icons.jsonresources/lang/en.jsonscripts/build-namelayer-assets.mjssrc/client/ClientGameRunner.tssrc/client/InputHandler.tssrc/client/UserSettingModal.tssrc/client/components/CosmeticButton.tssrc/client/components/DesktopNavBar.tssrc/client/components/MobileNavBar.tssrc/client/components/baseComponents/setting/SettingSelect.tssrc/client/graphics/GameRenderer.tssrc/client/graphics/HoverInfo.tssrc/client/graphics/PlayerIcons.tssrc/client/graphics/TransformHandler.tssrc/client/graphics/layers/ClassicCanvasTerritoryLayer.tssrc/client/graphics/layers/ClassicTerritoryBackend.tssrc/client/graphics/layers/Layer.tssrc/client/graphics/layers/NameLayer.tssrc/client/graphics/layers/NameLayerAssets.tssrc/client/graphics/layers/NameLayerLayout.tssrc/client/graphics/layers/PerformanceOverlay.tssrc/client/graphics/layers/PlayerInfoOverlay.tssrc/client/graphics/layers/RendererStatusPanel.tssrc/client/graphics/layers/SegmentMotionSample.tssrc/client/graphics/layers/SegmentTrailRaster.tssrc/client/graphics/layers/SettingsModal.tssrc/client/graphics/layers/StructureIconsLayer.tssrc/client/graphics/layers/TerritoryBackend.tssrc/client/graphics/layers/TerritoryLayer.tssrc/client/graphics/layers/TerritoryWebGLRenderer.tssrc/client/graphics/layers/TrailLifecycle.tssrc/client/graphics/layers/UnitLayer.tssrc/client/graphics/layers/WebGLTerritoryBackend.tssrc/client/graphics/layers/WebGPUDebugOverlay.tssrc/client/graphics/layers/WebGPUTerritoryBackend.tssrc/client/graphics/webgpu/TerritoryRenderer.tssrc/client/graphics/webgpu/compute/ComputePass.tssrc/client/graphics/webgpu/compute/DefendedStrengthFullPass.tssrc/client/graphics/webgpu/compute/DefendedStrengthPass.tssrc/client/graphics/webgpu/compute/StateUpdatePass.tssrc/client/graphics/webgpu/compute/TerrainComputePass.tssrc/client/graphics/webgpu/compute/VisualStateSmoothingPass.tssrc/client/graphics/webgpu/core/GroundTruthData.tssrc/client/graphics/webgpu/core/ShaderLoader.tssrc/client/graphics/webgpu/core/WebGPUDevice.tssrc/client/graphics/webgpu/render/RenderPass.tssrc/client/graphics/webgpu/render/TemporalResolvePass.tssrc/client/graphics/webgpu/render/TerrainShaderRegistry.tssrc/client/graphics/webgpu/render/TerritoryPostSmoothingRegistry.tssrc/client/graphics/webgpu/render/TerritoryPreSmoothingRegistry.tssrc/client/graphics/webgpu/render/TerritoryRenderPass.tssrc/client/graphics/webgpu/render/TerritoryShaderRegistry.tssrc/client/graphics/webgpu/shaders/compute/defended-strength-full.wgslsrc/client/graphics/webgpu/shaders/compute/defended-strength.wgslsrc/client/graphics/webgpu/shaders/compute/state-update.wgslsrc/client/graphics/webgpu/shaders/compute/terrain-compute-improved-heavy.wgslsrc/client/graphics/webgpu/shaders/compute/terrain-compute-improved-lite.wgslsrc/client/graphics/webgpu/shaders/compute/terrain-compute.wgslsrc/client/graphics/webgpu/shaders/compute/visual-state-smoothing.wgslsrc/client/graphics/webgpu/shaders/render/retro.wgslsrc/client/graphics/webgpu/shaders/render/temporal-resolve.wgslsrc/client/graphics/webgpu/shaders/render/territory.wgslsrc/client/vite-env.d.tssrc/core/CosmeticSchemas.tssrc/core/configuration/Config.tssrc/core/configuration/PastelTheme.tssrc/core/configuration/PastelThemeDark.tssrc/core/execution/NukeExecution.tssrc/core/execution/TradeShipExecution.tssrc/core/execution/TransportShipExecution.tssrc/core/game/GameImpl.tssrc/core/game/GameMap.tssrc/core/game/GameView.tssrc/core/game/MotionPlans.tssrc/core/game/UnitImpl.tssrc/core/game/UserSettings.tssrc/core/pathfinding/PathFinder.tssrc/core/pathfinding/PathFinderStepper.tssrc/core/pathfinding/transformers/SmoothingWaterTransformer.tssrc/core/pathfinding/types.tssrc/server/PublicAssetManifest.tstests/MiniMapTransformerPlanSegments.test.tstests/MotionPlansSegments.test.tstests/NameLayer.test.tstests/PathFinderStepperPriming.test.tstests/SegmentTrailRaster.test.tstests/TerritoryBackendSelection.test.tstests/UnitLayerTrailLifecycle.test.tstests/core/pathfinding/PathFinderStepper.test.tstests/server/PublicAssetManifest.test.ts
| .then(async (r) => { | ||
| currentGameRunner = r; | ||
| r.start(); | ||
| await r.start(); | ||
| }) |
There was a problem hiding this comment.
Handle partial startup cleanup when start() fails.
If renderer.initialize() fails at Line 403, start() rejects after startup side effects are already active. In Line 145-148, the rejection path does not stop that partially started runner, so timers/listeners/audio can leak.
Suggested fix
createClientGame(
lobbyConfig,
clientID,
eventBus,
transport,
userSettings,
terrainLoad,
terrainMapFileLoader,
)
.then(async (r) => {
currentGameRunner = r;
- await r.start();
+ try {
+ await r.start();
+ } catch (e) {
+ r.stop();
+ throw e;
+ }
})
.catch((e) => {
console.error("error creating client game", e);Also applies to: 370-404
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/client/ClientGameRunner.ts` around lines 145 - 148, When assigning and
starting the new runner in the promise chain (currentGameRunner = r; await
r.start()), wrap the start() call in a try/catch so that if r.start() (or
downstream renderer.initialize()) throws you immediately perform cleanup: call
the runner's shutdown method(s) (e.g., r.stop() or r.dispose() / whichever
cleanup API exists), clear currentGameRunner (set to null/undefined), and then
rethrow the error so the rejection is propagated; apply the same pattern to the
other startup site referenced (the block around lines 370-404) to prevent
timers/listeners/audio leaks on partial startup failures.
| const webgpuDebugOverlay = document.querySelector( | ||
| "webgpu-debug-overlay", | ||
| ) as WebGPUDebugOverlay; | ||
| if (!(webgpuDebugOverlay instanceof WebGPUDebugOverlay)) { | ||
| console.error("webgpu debug overlay not found"); | ||
| } | ||
| webgpuDebugOverlay.eventBus = eventBus; | ||
| webgpuDebugOverlay.userSettings = userSettings; | ||
| webgpuDebugOverlay.requestUpdate(); | ||
|
|
There was a problem hiding this comment.
Guard missing overlay elements before assignment/use.
Line 252 and Line 262 can throw at runtime because the code continues after a failed instanceof check and dereferences possibly invalid elements. Please return early (or throw) when the query fails.
Proposed fix
const webgpuDebugOverlay = document.querySelector(
"webgpu-debug-overlay",
) as WebGPUDebugOverlay;
if (!(webgpuDebugOverlay instanceof WebGPUDebugOverlay)) {
- console.error("webgpu debug overlay not found");
+ throw new Error("webgpu-debug-overlay element not found");
}
webgpuDebugOverlay.eventBus = eventBus;
webgpuDebugOverlay.userSettings = userSettings;
webgpuDebugOverlay.requestUpdate();
const rendererStatusPanel = document.querySelector(
"renderer-status-panel",
) as RendererStatusPanel;
if (!(rendererStatusPanel instanceof RendererStatusPanel)) {
- console.error("renderer status panel not found");
+ throw new Error("renderer-status-panel element not found");
}
rendererStatusPanel.userSettings = userSettings;
rendererStatusPanel.requestUpdate();Also applies to: 256-264
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/client/graphics/GameRenderer.ts` around lines 246 - 255, The code queries
for webgpuDebugOverlay and then uses it even when the instanceof check fails;
update the block around the webgpuDebugOverlay variable so that after const
webgpuDebugOverlay = document.querySelector("webgpu-debug-overlay") as
WebGPUDebugOverlay you immediately check if (!(webgpuDebugOverlay instanceof
WebGPUDebugOverlay)) and return early or throw to avoid dereferencing a bad
value, then only assign webgpuDebugOverlay.eventBus = eventBus,
webgpuDebugOverlay.userSettings = userSettings and call
webgpuDebugOverlay.requestUpdate() when the guard passes; this same guarding
pattern should be applied to the other similar overlay usages in the file.
| const owner = this.game.owner(tile) as PlayerView; | ||
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
| const isHighlighted = | ||
| this.highlightedTerritory && | ||
| this.highlightedTerritory.id() === owner.id(); | ||
| const myPlayer = this.game.myPlayer(); | ||
|
|
||
| if (this.game.isBorder(tile)) { | ||
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
| const playerIsFocused = owner && this.game.focusedPlayer() === owner; | ||
| if (myPlayer) { | ||
| const alternativeColor = this.alternateViewColor(owner); | ||
| this.paintTile(this.alternativeImageData, tile, alternativeColor, 255); | ||
| } | ||
| const isDefended = this.game.hasUnitNearby( | ||
| tile, | ||
| this.game.config().defensePostRange(), | ||
| UnitType.DefensePost, | ||
| owner.id(), | ||
| ); | ||
|
|
||
| this.paintTile( | ||
| this.imageData, | ||
| tile, | ||
| owner.borderColor(tile, isDefended), | ||
| 255, | ||
| ); | ||
| } else { | ||
| // Alternative view only shows borders. | ||
| this.clearAlternativeTile(tile); | ||
|
|
||
| this.paintTile(this.imageData, tile, owner.territoryColor(tile), 150); | ||
| } |
There was a problem hiding this comment.
Use the hover/focus flags when painting border tiles.
Lines 559-566 compute isHighlighted and playerIsFocused, but the border color path never branches on either value. That makes the repaint work in updateHighlightedTerritory() and the focused-player refresh in tick() effectively no-op, so those highlights never become visible.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/client/graphics/layers/ClassicCanvasTerritoryLayer.ts` around lines 557 -
589, The border-paint path ignores the computed isHighlighted and
playerIsFocused flags so highlight/focus never affect rendering; update the
border branch in ClassicCanvasTerritoryLayer (the code that calls
owner.borderColor(...) and paintTile(...)) to actually use those flags when
deciding the color: either extend owner.borderColor(tile, ...) to accept
highlight/focus (e.g., owner.borderColor(tile, isDefended, isHighlighted,
playerIsFocused)) or compute a modified borderColor before paintTile based on
isHighlighted/playerIsFocused (for example choose a highlighted/focused variant
or blend), remove the eslint ignore on playerIsFocused, and pass that resulting
color into paintTile so highlights and focused-player rendering take effect.
| dispose() { | ||
| // Classic layers own only offscreen canvases and event-bus listeners. | ||
| // The event bus does not currently expose unsubscribe hooks. | ||
| } |
There was a problem hiding this comment.
Clean up EventBus listeners when this backend is deactivated.
ClassicCanvasTerritoryLayer.init() registers MouseOverEvent, AlternateViewEvent, and DragEvent handlers, but dispose() is empty here. With renderer fallback and manual backend switching in this PR, each old classic backend instance will keep receiving events and retain its layer state after it is supposedly inactive.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/client/graphics/layers/ClassicTerritoryBackend.ts` around lines 55 - 58,
ClassicTerritoryBackend.dispose is currently empty while
ClassicCanvasTerritoryLayer.init registers MouseOverEvent, AlternateViewEvent,
and DragEvent handlers; fix by having the layer/backend keep references to the
actual listener functions (store them as instance properties when bind/register
in ClassicCanvasTerritoryLayer.init) and remove/unregister those handlers in
ClassicTerritoryBackend.dispose by calling the appropriate EventBus removal API
(e.g., eventBus.off/removeListener/unsubscribe depending on the EventBus
implementation) for MouseOverEvent, AlternateViewEvent and DragEvent so the
deactivated backend stops receiving events and releases its state.
| ${unitLayerCounters | ||
| ? html`<div class="layers-section"> | ||
| <div class="performance-line section-header"> | ||
| <span>UnitLayer Counters</span> | ||
| </div> | ||
| <div class="performance-line"> | ||
| tracked: ${Number(unitLayerCounters.moversTrackedTotal ?? 0)} | ||
| sampled: ${Number(unitLayerCounters.moversSampled ?? 0)} | ||
| drawn: ${Number(unitLayerCounters.moversDrawn ?? 0)} | ||
| skipped: ${Number(unitLayerCounters.moversSkipped ?? 0)} | ||
| </div> | ||
| <div class="performance-line"> | ||
| moverCanvasScale: | ||
| ${Number(unitLayerCounters.moverCanvasScale ?? 0).toFixed(0)} | ||
| rescale(last/avg/count): | ||
| ${Number(unitLayerCounters.moverCanvasRescaleLastMs ?? 0).toFixed(2)}ms | ||
| / | ||
| ${Number(unitLayerCounters.moverCanvasRescaleAvgMs ?? 0).toFixed(2)}ms | ||
| / | ||
| ${Number(unitLayerCounters.moverCanvasRescaleCount ?? 0).toFixed(0)} | ||
| </div> | ||
| <div class="performance-line"> | ||
| draw: | ||
| ${Number(unitLayerCounters.drawTimeMs ?? 0).toFixed(2)}ms | ||
| </div> | ||
| <div class="performance-line"> | ||
| 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 | ||
| </div> | ||
| <div class="performance-line"> | ||
| avgOnDebt: ${Number(unitLayerCounters.avgOnScreenDebt ?? 0).toFixed(2)} | ||
| maxOnDebt: ${Number(unitLayerCounters.maxOnScreenDebt ?? 0).toFixed(0)} | ||
| </div> | ||
| </div>` |
There was a problem hiding this comment.
Localize new UnitLayer counter labels via translateText().
The new visible strings are hardcoded (for example “UnitLayer Counters”, “tracked”, “draw”, “avgOnDebt”). In src/client/**/*.{ts,tsx}, these must go through translateText() with resources/lang/en.json entries.
As per coding guidelines: "src/client/**/*.{ts,tsx}: All user-visible text must go through translateText() function with corresponding entries in resources/lang/en.json".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/client/graphics/layers/PerformanceOverlay.ts` around lines 1314 - 1353,
The new hardcoded UI labels in PerformanceOverlay (e.g. "UnitLayer Counters",
"tracked", "sampled", "drawn", "skipped", "moverCanvasScale",
"rescale(last/avg/count)", "draw", "on", "off", "avgOnDebt", "maxOnDebt") must
be wrapped with translateText() and corresponding keys added to
resources/lang/en.json; update the template render in PerformanceOverlay (where
unitLayerCounters is used) to call translateText("...") for each visible string
and add matching English entries in resources/lang/en.json using clear,
namespaced keys (e.g. performance.unitLayer.counters,
performance.unitLayer.tracked, etc.).
| export const TERRAIN_SHADERS: TerrainShaderDefinition[] = [ | ||
| { | ||
| id: "classic", | ||
| label: "Classic", | ||
| wgslPath: "compute/terrain-compute.wgsl", | ||
| options: [], | ||
| }, | ||
| { | ||
| id: "improved-lite", | ||
| label: "Improved (Lite)", | ||
| wgslPath: "compute/terrain-compute-improved-lite.wgsl", | ||
| options: [ | ||
| { | ||
| kind: "range", | ||
| key: "settings.webgpu.terrain.improvedLite.noiseStrength", | ||
| label: "Noise Strength", | ||
| defaultValue: 0.005, | ||
| min: 0, | ||
| max: 0.08, | ||
| step: 0.005, | ||
| }, | ||
| { | ||
| kind: "range", | ||
| key: "settings.webgpu.terrain.improvedLite.blendWidth", | ||
| label: "Biome Blend Width", | ||
| defaultValue: 5, | ||
| min: 0.5, | ||
| max: 5, | ||
| step: 0.25, | ||
| }, | ||
| { | ||
| kind: "range", | ||
| key: "settings.webgpu.terrain.improvedLite.waterBlurStrength", | ||
| label: "Water Blur Strength", | ||
| defaultValue: 1, | ||
| min: 0, | ||
| max: 1, | ||
| step: 0.05, | ||
| }, | ||
| ], | ||
| }, | ||
| { | ||
| id: "improved-heavy", | ||
| label: "Improved (Heavy)", | ||
| wgslPath: "compute/terrain-compute-improved-heavy.wgsl", | ||
| options: [ | ||
| { | ||
| kind: "range", | ||
| key: "settings.webgpu.terrain.improvedHeavy.noiseStrength", | ||
| label: "Noise Strength", | ||
| defaultValue: 0.01, | ||
| min: 0, | ||
| max: 0.1, | ||
| step: 0.005, | ||
| }, | ||
| { | ||
| kind: "range", | ||
| key: "settings.webgpu.terrain.improvedHeavy.detailNoiseStrength", | ||
| label: "Detail Noise Strength", | ||
| defaultValue: 0.01, | ||
| min: 0, | ||
| max: 0.08, | ||
| step: 0.005, | ||
| }, | ||
| { | ||
| kind: "range", | ||
| key: "settings.webgpu.terrain.improvedHeavy.blendWidth", | ||
| label: "Biome Blend Width", | ||
| defaultValue: 4.5, | ||
| min: 0.5, | ||
| max: 6, | ||
| step: 0.25, | ||
| }, | ||
| { | ||
| kind: "range", | ||
| key: "settings.webgpu.terrain.improvedHeavy.waterDepthStrength", | ||
| label: "Water Depth Strength", | ||
| defaultValue: 0.35, | ||
| min: 0, | ||
| max: 1, | ||
| step: 0.05, | ||
| }, | ||
| { | ||
| kind: "range", | ||
| key: "settings.webgpu.terrain.improvedHeavy.waterDepthCurve", | ||
| label: "Water Depth Curve", | ||
| defaultValue: 2, | ||
| min: 0.5, | ||
| max: 4, | ||
| step: 0.25, | ||
| }, | ||
| { | ||
| kind: "range", | ||
| key: "settings.webgpu.terrain.improvedHeavy.waterDepthBlur", | ||
| label: "Water Depth Blur", | ||
| defaultValue: 0.6, | ||
| min: 0, | ||
| max: 1, | ||
| step: 0.05, | ||
| }, | ||
| { | ||
| kind: "range", | ||
| key: "settings.webgpu.terrain.improvedHeavy.lightingStrength", | ||
| label: "Lighting Strength", | ||
| defaultValue: 0.3, | ||
| min: 0, | ||
| max: 1, | ||
| step: 0.05, | ||
| }, | ||
| { | ||
| kind: "range", | ||
| key: "settings.webgpu.terrain.improvedHeavy.cavityStrength", | ||
| label: "Cavity Strength", | ||
| defaultValue: 0.15, | ||
| min: 0, | ||
| max: 1, | ||
| step: 0.05, | ||
| }, | ||
| ], | ||
| }, |
There was a problem hiding this comment.
Move shader labels/options to i18n keys instead of raw strings.
These labels look user-visible ("Classic", "Improved (Lite)", "Noise Strength", etc.) but are hardcoded in English. Please switch to translated strings via translateText() and add matching keys to resources/lang/en.json.
As per coding guidelines, “All user-visible text must go through translateText() function with corresponding entries in resources/lang/en.json”.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/client/graphics/webgpu/render/TerrainShaderRegistry.ts` around lines 36 -
155, TERRAIN_SHADERS contains hardcoded, user-facing labels and option labels
(e.g., the "label" fields on shader objects like id "classic", "improved-lite",
"improved-heavy" and the option "label" values such as "Noise Strength", "Biome
Blend Width", etc.); replace each raw string with a call to translateText(...)
and add corresponding keys to resources/lang/en.json (suggested keys: e.g.
"terrain.shader.classic.label", "terrain.shader.improvedLite.label",
"terrain.shader.improvedHeavy.label" and option keys like
"terrain.shader.improvedLite.noiseStrength",
"terrain.shader.improvedLite.blendWidth",
"terrain.shader.improvedLite.waterBlurStrength", plus the improvedHeavy option
keys) so every user-visible string in TERRAIN_SHADERS uses
translateText('your.key') and the new keys are populated in
resources/lang/en.json with the English text.
| export const TERRITORY_POST_SMOOTHING: TerritoryPostSmoothingDefinition[] = [ | ||
| { | ||
| id: "off", | ||
| label: "Off", | ||
| wgslPath: "", | ||
| options: [], | ||
| }, | ||
| { | ||
| id: "fade", | ||
| label: "Fade", | ||
| wgslPath: "render/temporal-resolve.wgsl", | ||
| options: [ | ||
| { | ||
| kind: "range", | ||
| key: "settings.webgpu.territory.postSmoothing.blendStrength", | ||
| label: "Blend Strength", | ||
| defaultValue: 0.2, | ||
| min: 0.01, | ||
| max: 1, | ||
| step: 0.01, | ||
| }, | ||
| ], | ||
| }, | ||
| { | ||
| id: "dissolve", | ||
| label: "Dissolve", | ||
| wgslPath: "render/temporal-resolve.wgsl", | ||
| options: [ | ||
| { | ||
| kind: "range", | ||
| key: "settings.webgpu.territory.postSmoothing.blendStrength", | ||
| label: "Blend Strength", | ||
| defaultValue: 1, | ||
| min: 0, | ||
| max: 1, | ||
| step: 0.01, | ||
| }, | ||
| { | ||
| kind: "range", | ||
| key: "settings.webgpu.territory.postSmoothing.dissolveWidth", | ||
| label: "Dissolve Width", | ||
| defaultValue: 0.08, | ||
| min: 0.01, | ||
| max: 0.4, | ||
| step: 0.01, | ||
| }, | ||
| ], | ||
| }, |
There was a problem hiding this comment.
Use i18n keys for post-smoothing labels.
"Off", "Fade", "Dissolve", "Blend Strength", and "Dissolve Width" are user-facing text and should be routed through translateText() with entries in resources/lang/en.json.
As per coding guidelines, “All user-visible text must go through translateText() function with corresponding entries in resources/lang/en.json”.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/client/graphics/webgpu/render/TerritoryPostSmoothingRegistry.ts` around
lines 15 - 62, The TERRITORY_POST_SMOOTHING array currently contains hard-coded
user-facing strings ("Off", "Fade", "Dissolve", "Blend Strength", "Dissolve
Width"); update the definitions in TERRITORY_POST_SMOOTHING so each label and
each option.label calls translateText() with a new i18n key (e.g.
"territory.postSmoothing.off", "territory.postSmoothing.fade",
"territory.postSmoothing.dissolve", "territory.postSmoothing.blendStrength",
"territory.postSmoothing.dissolveWidth") and add corresponding entries to
resources/lang/en.json; ensure keys are consistent and used for both the
top-level label fields and the nested options' label fields so all user-visible
text goes through translateText().
| export const TERRITORY_PRE_SMOOTHING: TerritoryPreSmoothingDefinition[] = [ | ||
| { | ||
| id: "off", | ||
| label: "Off", | ||
| wgslPath: "", | ||
| options: [], | ||
| }, | ||
| { | ||
| id: "dissolve", | ||
| label: "Dissolve", | ||
| wgslPath: "compute/visual-state-smoothing.wgsl", | ||
| options: [ | ||
| { | ||
| kind: "range", | ||
| key: "settings.webgpu.territory.preSmoothing.curveExp", | ||
| label: "Reveal Curve", | ||
| defaultValue: 1, | ||
| min: 0.25, | ||
| max: 3, | ||
| step: 0.05, | ||
| }, | ||
| ], | ||
| }, | ||
| { | ||
| id: "budget", | ||
| label: "Budgeted Reveal", | ||
| wgslPath: "compute/visual-state-smoothing.wgsl", | ||
| options: [ | ||
| { | ||
| kind: "range", | ||
| key: "settings.webgpu.territory.preSmoothing.curveExp", | ||
| label: "Reveal Curve", | ||
| defaultValue: 1, | ||
| min: 0.25, | ||
| max: 3, | ||
| step: 0.05, | ||
| }, | ||
| ], | ||
| }, |
There was a problem hiding this comment.
Use i18n keys for pre-smoothing labels.
"Off", "Dissolve", "Budgeted Reveal", and "Reveal Curve" are user-visible labels and should come from translateText() + resources/lang/en.json keys.
As per coding guidelines, “All user-visible text must go through translateText() function with corresponding entries in resources/lang/en.json”.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/client/graphics/webgpu/render/TerritoryPreSmoothingRegistry.ts` around
lines 15 - 53, Replace hard-coded user-visible strings in the
TERRITORY_PRE_SMOOTHING array with i18n keys and translateText() calls: change
label values "Off", "Dissolve", "Budgeted Reveal" and the option label "Reveal
Curve" to calls like translateText("[...]") (keeping the same unique ids "off",
"dissolve", "budget" etc. so behavior doesn't change) and add corresponding keys
and English entries to resources/lang/en.json (e.g. territory.preSmoothing.off,
territory.preSmoothing.dissolve, territory.preSmoothing.budget,
territory.preSmoothing.revealCurve) so translateText resolves them; ensure you
import/usage of translateText is consistent with surrounding modules (same
import pattern) and leave non-user-visible fields (id, wgslPath, option keys)
unchanged.
| setViewSize(width: number, height: number): void { | ||
| if (!this.resources || !this.device) { | ||
| return; | ||
| } | ||
|
|
||
| const nextWidth = Math.max(1, Math.floor(width)); | ||
| const nextHeight = Math.max(1, Math.floor(height)); | ||
|
|
||
| if (nextWidth === this.canvas.width && nextHeight === this.canvas.height) { | ||
| return; | ||
| } | ||
|
|
||
| this.canvas.width = nextWidth; | ||
| this.canvas.height = nextHeight; | ||
| this.resources.setViewSize(nextWidth, nextHeight); | ||
| this.device.reconfigure(); | ||
|
|
||
| if (this.postSmoothingEnabled && this.resources) { | ||
| this.resources.ensurePostSmoothingTextures( | ||
| nextWidth, | ||
| nextHeight, | ||
| this.device.canvasFormat, | ||
| ); | ||
| this.resources.invalidateHistory(); | ||
| } | ||
| } |
There was a problem hiding this comment.
Apply the requested canvas size even before WebGPU is ready.
Right now setViewSize() returns before touching canvas.width / canvas.height. If the caller resizes during async startup, WebGPUDevice.create() still sees the constructor's 1x1 canvas and the renderer stays mis-sized until another resize happens.
Minimal fix
setViewSize(width: number, height: number): void {
- if (!this.resources || !this.device) {
- return;
- }
-
const nextWidth = Math.max(1, Math.floor(width));
const nextHeight = Math.max(1, Math.floor(height));
if (nextWidth === this.canvas.width && nextHeight === this.canvas.height) {
return;
}
this.canvas.width = nextWidth;
this.canvas.height = nextHeight;
+ if (!this.resources || !this.device) {
+ return;
+ }
this.resources.setViewSize(nextWidth, nextHeight);
this.device.reconfigure();📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| setViewSize(width: number, height: number): void { | |
| if (!this.resources || !this.device) { | |
| return; | |
| } | |
| const nextWidth = Math.max(1, Math.floor(width)); | |
| const nextHeight = Math.max(1, Math.floor(height)); | |
| if (nextWidth === this.canvas.width && nextHeight === this.canvas.height) { | |
| return; | |
| } | |
| this.canvas.width = nextWidth; | |
| this.canvas.height = nextHeight; | |
| this.resources.setViewSize(nextWidth, nextHeight); | |
| this.device.reconfigure(); | |
| if (this.postSmoothingEnabled && this.resources) { | |
| this.resources.ensurePostSmoothingTextures( | |
| nextWidth, | |
| nextHeight, | |
| this.device.canvasFormat, | |
| ); | |
| this.resources.invalidateHistory(); | |
| } | |
| } | |
| setViewSize(width: number, height: number): void { | |
| const nextWidth = Math.max(1, Math.floor(width)); | |
| const nextHeight = Math.max(1, Math.floor(height)); | |
| if (nextWidth === this.canvas.width && nextHeight === this.canvas.height) { | |
| return; | |
| } | |
| this.canvas.width = nextWidth; | |
| this.canvas.height = nextHeight; | |
| if (!this.resources || !this.device) { | |
| return; | |
| } | |
| this.resources.setViewSize(nextWidth, nextHeight); | |
| this.device.reconfigure(); | |
| if (this.postSmoothingEnabled && this.resources) { | |
| this.resources.ensurePostSmoothingTextures( | |
| nextWidth, | |
| nextHeight, | |
| this.device.canvasFormat, | |
| ); | |
| this.resources.invalidateHistory(); | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/client/graphics/webgpu/TerritoryRenderer.ts` around lines 263 - 288, In
setViewSize, always apply the requested canvas.width and canvas.height
immediately (use nextWidth/nextHeight) even if this.resources or this.device are
not ready; then early-return only after setting the canvas dimensions. When
device/resources exist continue to call
this.resources.setViewSize(nextWidth,nextHeight), this.device.reconfigure(),
and, if this.postSmoothingEnabled,
this.resources.ensurePostSmoothingTextures(...) and
this.resources.invalidateHistory(). This ensures WebGPUDevice.create sees the
latest canvas size while preserving existing behavior when resources/device are
available.
| .options=${[ | ||
| { value: 0, label: "Off" }, | ||
| { value: 1, label: "Simple" }, | ||
| { value: 2, label: "Glow" }, | ||
| ]} |
There was a problem hiding this comment.
Localize option labels in both new selects.
These option labels are user-visible but hardcoded in English, so they bypass localization.
🔧 Proposed fix
.options=${[
- { value: 0, label: "Off" },
- { value: 1, label: "Simple" },
- { value: 2, label: "Glow" },
+ { value: 0, label: translateText("user_setting.territory_border_mode_off") },
+ { value: 1, label: translateText("user_setting.territory_border_mode_simple") },
+ { value: 2, label: translateText("user_setting.territory_border_mode_glow") },
]}
@@
.options=${[
- { value: "auto", label: "Auto" },
- { value: "classic", label: "Classic" },
- { value: "webgl", label: "WebGL" },
- { value: "webgpu", label: "WebGPU" },
+ { value: "auto", label: translateText("user_setting.renderer_auto") },
+ { value: "classic", label: translateText("user_setting.renderer_classic") },
+ { value: "webgl", label: translateText("user_setting.renderer_webgl") },
+ { value: "webgpu", label: translateText("user_setting.renderer_webgpu") },
]}As per coding guidelines, src/client/**/*.{ts,tsx}: all user-visible text must go through translateText() with corresponding entries in resources/lang/en.json.
Also applies to: 794-798
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/client/UserSettingModal.ts` around lines 780 - 784, The option labels for
the selects in UserSettingModal are hardcoded in English; update the arrays used
in the .options property (the arrays around the existing entries { value: 0,
label: "Off" } etc. and the similar array at lines ~794-798) to call
translateText("...") for each label (e.g., translateText("userSetting.off"),
translateText("userSetting.simple"), translateText("userSetting.glow")), and add
the corresponding keys and English strings to resources/lang/en.json; ensure you
keep the same value numbers and only replace the label strings with
translateText calls so the Select components (in UserSettingModal) remain
unchanged.
|
A core assumption of this PR is that Pixi is the right tool for text and icons, which is what makes a multi-backend architecture (Pixi alongside WebGL/WebGPU) more defensible on maintenance grounds. I think that assumption is worth examining. The current NamePass:
The PR's NameLayer:
On a feature-by-feature comparison to Pixi glyph rendering, we're basically on par. Except that our renderer is efficiently integrated with the data it is rendering. The current MSDF renderer is overall less lines of code than it requires to integrate Pixi in this PR. Overall, I think that this a substantial amount of infrastructure being taken on for maximal progressive hardware support. I think that there should be correspondingly substantial evidence from a real survey of hardware demographics to justify it. The PR ultimately represents a 4 renderer matrix, requiring parallel implementations, and a per-feature judgment on which layer will render what. There are some meritable parts I think are worth saving:
|
that reads like the pixi implementation, yeah, but pixi is a well maintained external dependency, it will be much less work in the future while supporting a lot more then what it used today for future expension. That said, rendering the namelayer with pixi is just one of many options, out of many evaluated ones (shader, canvas,dom) it seems to be the most well performing, easy to maintain, way to implement this. pixi is also offering a (future)webgpu renderer which would be available to us "for free" once it is declared stable. (they are also waiting on browser support) |
read the description, this originally is multiple pr's with separate concerns, nothing requires "parallel implementation" |
Any thing facilitated by GPU compute will need parallel implementations, for example. |
The cost is in maintaining the code needed to keep Pixi integrated and part of the pipeline, which is already more code than the text shader itself. Text shading is "public science" where as the integration and coordination infrastructure needed to keep Pixi is "local art". By taking on the text shader, we get a consistency and unified GL pipeline. |
gpu compute is mainly used for inter-tick per-frame rendering in the current implementation in this pr. As you can see theres a alternate implementation in the webgl branch and classic does what it always did. There is work on using webgpu as authoritative source for distance calculations, but that requires either the event-driven architecture or the move of the renderer into the worker, or sab, testcases for each of those exist but are not part of this pr on purpose, as neither the data model for those nor webgpu in general is up to it yet. theres also work on unifying webgl and webgpu shaders in a common slang framework. The architecture set by this is aimed at going in that direction with minimal integration risk in the form of incremental pr's. i decided to focus this pr on the most "simple" merge of the currently "stable" branches. |
This is the kind of "parallel implementation" I'm referring to.
But this is only possible if WebGPU is available. If it's unavailable, you'll need a "parallel implementation" for anything you'd otherwise be using compute shaders for. |
|
fell free to still take this once you actually reviewed the code, no grudge. I still believe this is a terribly misguided decision and im sure the future will back me up on it. |
Summary
deployment at: https://fallback-pixi-motion-v0-31-12.openfront.dev/
This branch consolidates the renderer fallback, WebGL territory renderer, WebGPU territory renderer, Pixi NameLayer, and smoothed unit-motion work onto the
v0.31.12base.The branch was originally planned as several individual feature branches / PR-sized threads:
Source branches / PRs that fed into this integration:
webgl-butter2webgl-butter2-rebased-v0-31-12(not a PR)you-want-this), #2867 testpr: Webgl terrain (webgl-terrain)webgpu-temporal-playgroundwebgpu-temporal-playground-rebased-v0-31-12(not a PR)webgpu-territory-renderer)pixi-namelayerpixi-namelayer)motionplaner/ motion-plan work, pluspackedMotionpackedMotion), #3292 Packed unit updates / MotionPlans (packedUnitUpdates-minimal)units4-noLos-zigzagy,units4-watertrans,units4-4way-int,units4-mole,units4-rebase-untestedunits4-noLos-zigzagy: deploy run 22525349252units4-watertrans: deploy run 22524371221units4-4way-int: deploy run 22603862445units4-mole: deploy run 22603633460units4-rebase-untested: deploy run 23122340339renderer-fallback-v0-31-12fallback-pixi-motion-v0-31-12(this PR branch)Those threads are now integrated and rebased as one holistic change.
Base target remains
v0.31.12, not latestmain.What changed
Territory renderer fallback controller
TerritoryLayeras the public layer used byGameRenderer, but turns it into a controller over three backends:classicwebglwebgpusettings.territoryRendererwith:autoclassicwebglwebgpuAutoattempts:WebGPU -> WebGL -> Classictick/renderLayer/redrawexceptions.Renderer status and debug UI
Classic, WebGL, and WebGPU territory rendering
ClassicTerritoryBackend.WebGLTerritoryBackend.WebGPUTerritoryBackend.WebGPU territory rendering
improved-heavyretrofade0.01.WebGL territory rendering
Pixi NameLayer
Asset pipeline
build:namelayer-assetsscript for regenerating NameLayer assets.meta.imagereferencesUnit motion plans and UnitLayer rendering
UnitLayerdynamic mover rendering around persistent canvases and bucketed scheduling.v0.31.12asset set, but render as visible small cell markers.Pathfinding support for motion plans
PathFinderStepperand smoothing water transformer behavior so segment plans are available for the relevant water path configurations.Performance and observability
UnitLayercounters into the performance overlay.Staging deployment lifetime
openfront.devstaging deployment lifetime from25hto720h(30 days).Budgets and defaults
Current
UnitLayermotion/dynamic-mover budgets:2msper render frame.0.1ms.30render frames.2frames.1,2,3,4.1.2,2.4,4.8.0.2.160ms.300ms.Renderer defaults:
settings.territoryRenderer:auto.WebGPU -> WebGL -> Classic.Deployment default:
720h.Tests and validation
Added/updated focused coverage includes:
Manual test checklist
Recommended before marking ready:
Auto.navigator.gpufalls back to WebGL.Review notes
TerritoryLayer/backend selection,UnitLayer, WebGL/WebGPU renderer code, NameLayer assets, and the public asset manifest changes.Please complete the following:
Please put your Discord username so you can be contacted if a bug or regression is found:
excessive discussion in: https://discord.com/channels/1359946986937258015/1459562868935622809 and others, search for deployment url's