Skip to content

Renderer fallback, Pixi name layer, and smoothed unit motion#4054

Closed
scamiv wants to merge 53 commits into
openfrontio:mainfrom
scamiv:fallback-pixi-motion-v0-31-12
Closed

Renderer fallback, Pixi name layer, and smoothed unit motion#4054
scamiv wants to merge 53 commits into
openfrontio:mainfrom
scamiv:fallback-pixi-motion-v0-31-12

Conversation

@scamiv
Copy link
Copy Markdown
Contributor

@scamiv scamiv commented May 27, 2026

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.12 base.

The branch was originally planned as several individual feature branches / PR-sized threads:

Source branches / PRs that fed into this integration:

Those threads are now integrated and rebased as one holistic change.

Base target remains v0.31.12, not latest main.

What changed

Territory renderer fallback controller

  • Keeps TerritoryLayer as the public layer used by GameRenderer, but turns it into a controller over three backends:
    • classic
    • webgl
    • webgpu
  • Adds settings.territoryRenderer with:
    • auto
    • classic
    • webgl
    • webgpu
  • Auto attempts:
    • WebGPU -> WebGL -> Classic
  • Selected accelerated modes still degrade instead of hard-failing if init/runtime rendering fails.
  • Runtime fallback covers unsupported APIs, init failures, WebGPU device loss, WebGL context loss, and backend tick / renderLayer / redraw exceptions.
  • Fallback changes only the active runtime backend; it does not overwrite the user's saved renderer preference.
  • Manual renderer changes reset the failure list and retry the requested order.

Renderer status and debug UI

  • Adds a small renderer status panel showing the selected preference and currently active renderer.
  • Adds manual renderer switching during a match.
  • Adds a settings button that opens the relevant WebGL/WebGPU debug panel for the active or selected accelerated backend.
  • Debug panels and the renderer status panel are draggable.
  • Selecting WebGL/WebGPU manually opens the matching debug controls.
  • WebGL/WebGPU debug panels have close buttons.

Classic, WebGL, and WebGPU territory rendering

  • Restores the classic canvas territory path as ClassicTerritoryBackend.
  • Integrates the WebGL territory renderer as WebGLTerritoryBackend.
  • Integrates the WebGPU temporal renderer as WebGPUTerritoryBackend.
  • Uses a transparent main canvas so accelerated territory backends can sit behind normal 2D overlays.
  • Classic/WebGL fill the screen background before drawing so the transparent main canvas does not leave gaps.
  • WebGPU can initialize asynchronously while classic remains active, avoiding blank frames during startup.

WebGPU territory rendering

  • Adds the WebGPU compute/render pass structure for territory rendering.
  • Adds terrain compute shaders, visual-state smoothing, temporal resolve, and shader registries.
  • Default WebGPU settings are:
    • Terrain shader: improved-heavy
    • Territory shader: retro
    • Post render: fade
  • Removes the exposed defended-threshold option and hardcodes the current default value of 0.01.
  • Wires terrain updates for modifiable terrain, including water-nuke terrain changes.

WebGL territory rendering

  • Adds the WebGL territory renderer and debug controls.
  • Keeps sampling and movement-mode controls scoped to the smoothing path.
  • Carries forward border/fallout behavior used by the renderer fallback branch.
  • Adds the WebGL backend contract implementation so it can participate in fallback and status reporting.

Pixi NameLayer

  • Reworks NameLayer rendering onto Pixi/MSDF assets.
  • Adds deterministic NameLayer atlas generation.
  • Vendors the generated NameLayer font/icon/emoji atlases and required font licenses.
  • Keeps NameLayer initialization asynchronous and prevents repeated failed texture retries.
  • Preserves world-stable name scaling.

Asset pipeline

  • Adds a build:namelayer-assets script for regenerating NameLayer assets.
  • Adds derived public-asset handling for:
    • web manifest icon references
    • BMFont XML page references
    • TexturePacker atlas JSON meta.image references

Unit motion plans and UnitLayer rendering

  • Adds packed motion-plan records for sparse grid segments, train rail paths, and parabolic nuke/MIRV motion.
  • Makes stepper-generated plan segments authoritative for smoothed unit rendering.
  • Integrates the units/ships motion work while preserving the smoother nuke/MIRV parabolas and trail behavior.
  • Improves transport/trade ship motion-plan emission and avoids dense fallback path work in the runtime ship loops.
  • Adds segment sampling and trail raster helpers.
  • Reworks UnitLayer dynamic mover rendering around persistent canvases and bucketed scheduling.
  • Motion-planned movers persist visually between frames even when a frame's draw budget is exhausted.
  • Transport trails persist until despawn, and nuke/MIRV trails are cleaned up by unit id lifecycle.
  • MIRV warheads remain sprite-less, matching the v0.31.12 asset set, but render as visible small cell markers.

Pathfinding support for motion plans

  • Updates PathFinderStepper and smoothing water transformer behavior so segment plans are available for the relevant water path configurations.
  • Adds path/segment compression coverage for minimap/upscaled path segments.
  • Keeps motion-plan payloads sparse rather than re-expanding dense paths in execution loops.

Performance and observability

  • Adds optional layer perf counters and pipes UnitLayer counters into the performance overlay.
  • Adds counters for tracked/sampled/drawn/skipped movers, budget usage, on-screen/off-screen work, debt, and dynamic mover canvas scaling.
  • This makes the new motion renderer easier to evaluate in live games instead of relying only on visual inspection.

Staging deployment lifetime

  • Extends non-main openfront.dev staging deployment lifetime from 25h to 720h (30 days).

Budgets and defaults

Current UnitLayer motion/dynamic-mover budgets:

  • On-screen mover draw budget: 2ms per render frame.
  • Off-screen verification budget: 0.1ms.
  • Off-screen verification cadence: every 30 render frames.
  • On-screen hysteresis: 2 frames.
  • Dynamic mover canvas scale tiers: 1, 2, 3, 4.
  • Dynamic mover zoom thresholds: 1.2, 2.4, 4.8.
  • Dynamic mover zoom hysteresis: 0.2.
  • Dynamic mover scale settle delay: 160ms.
  • Dynamic mover scale cooldown: 300ms.
  • Dynamic mover subpixel snap: disabled.

Renderer defaults:

  • settings.territoryRenderer: auto.
  • Auto fallback order: WebGPU -> WebGL -> Classic.

Deployment default:

  • Staging timeout: 720h.

Tests and validation

Added/updated focused coverage includes:

  • Renderer backend selection/fallback tests.
  • Motion-plan packing/unpacking tests.
  • Segment trail raster tests.
  • UnitLayer trail lifecycle tests.
  • PathFinderStepper priming tests.
  • NameLayer tests.
  • Public asset manifest tests for web manifest, BMFont XML, and TexturePacker atlas JSON rewriting.
  • MIRV/nuke execution tests.

Manual test checklist

Recommended before marking ready:

  • Normal browser with WebGPU available uses WebGPU in Auto.
  • Browser/session without navigator.gpu falls back to WebGL.
  • Browser/session without WebGPU and WebGL2 falls back to Classic.
  • Manual renderer switching works during a live match without reload.
  • WebGL mode shows the WebGL debug panel and its smoothing-path-only controls behave as expected.
  • WebGPU mode shows the WebGPU debug panel and uses the intended defaults.
  • Renderer status panel shows the active runtime backend and saved preference.
  • WebGL/WebGPU debug panels and renderer status panel are draggable.
  • WebGPU terrain updates after water nukes without requiring renderer switching.
  • MIRV launch and warhead flight render without crashes and visible warheads/trails.
  • NameLayer loads icons/emojis/text in a deploy/staging environment with CDN asset URLs.
  • Classic fallback still renders terrain + territory correctly with normal overlays above it.

Review notes

  • This is intentionally a larger integration branch. The original feature strands were separable in concept, but the final deployable behavior is best judged by them working together.
  • The largest review surfaces are TerritoryLayer/backend selection, UnitLayer, WebGL/WebGPU renderer code, NameLayer assets, and the public asset manifest changes.
  • The NameLayer asset generation adds build-time dependencies and vendored generated assets; reviewers should verify that this is acceptable for the repo.

Please complete the following:

  • I have added screenshots for all UI updates
  • I process any text displayed to the user through translateText() and I've added it to the en.json file
  • I have added relevant tests to the test directory
  • 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:

excessive discussion in: https://discord.com/channels/1359946986937258015/1459562868935622809 and others, search for deployment url's

evanpelle and others added 30 commits May 12, 2026 07:44
## 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
scamiv added 13 commits May 27, 2026 14:36
…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.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 27, 2026

Review Change Stack

Walkthrough

Adds 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.

Changes

Territory renderer architecture and overlays

Layer / File(s) Summary
Renderer wiring and overlays
src/client/graphics/GameRenderer.ts, index.html, src/client/graphics/layers/PerformanceOverlay.ts
GameRenderer integrates RendererStatusPanel and WebGPUDebugOverlay, clears to transparent, and forwards perf counters; index adds overlays.
Status and WebGPU debug panels
src/client/graphics/layers/RendererStatusPanel.ts, src/client/graphics/layers/WebGPUDebugOverlay.ts
Adds draggable panels reflecting renderer status and WebGPU debug controls with persisted positions.
Hover info, Transform offset, and Input event
src/client/graphics/HoverInfo.ts, src/client/graphics/TransformHandler.ts, src/client/InputHandler.ts
Centralizes hover detection, exposes view offset, and emits compute metrics events.
PlayerInfoOverlay uses HoverInfo
src/client/graphics/layers/PlayerInfoOverlay.ts
Switches to getHoverInfo and shows wilderness headers.
Backend contracts and selection
src/client/graphics/layers/TerritoryBackend.ts
Defines backend interfaces, preference/order, and async selection with failure capture.
TerritoryLayer orchestration and failover
src/client/graphics/layers/TerritoryLayer.ts
Delegates to selected backend, handles failure/reactivation, and publishes status.
Classic territory backend wrapper
src/client/graphics/layers/ClassicTerritoryBackend.ts
Wraps TerrainLayer and ClassicCanvasTerritoryLayer as a backend.
WebGL territory backend and debug UI
src/client/graphics/layers/WebGLTerritoryBackend.ts
Implements WebGL rendering with smoothing, contest visualization, and debug UI/overlay.
WebGPU backend, device, shader loader, and resources
src/client/graphics/layers/WebGPUTerritoryBackend.ts, src/client/graphics/webgpu/core/*
Integrates WebGPU device, shader loading, and GroundTruthData resource manager.
WebGPU compute/render passes and registries
src/client/graphics/webgpu/compute/*, src/client/graphics/webgpu/render/*
Adds compute and render passes plus shader/smoothing registries.
WebGPU TerritoryRenderer orchestration
src/client/graphics/webgpu/TerritoryRenderer.ts
Initializes passes, topo-sorts dependencies, applies settings, and executes tick/render.

NameLayer Pixi refactor and asset pipeline

Layer / File(s) Summary
NameLayer Pixi renderer
src/client/graphics/layers/NameLayer.ts
Refactors to Pixi with async init/redraw and canvas blit.
NameLayerAssets and Layout helpers
src/client/graphics/layers/NameLayerAssets.ts, src/client/graphics/layers/NameLayerLayout.ts
Adds font/texture loading, layout and glyph sanitation utilities.
Font/icon/emoji atlases and build script
scripts/build-namelayer-assets.mjs, resources/fonts/*, resources/images/*, eslint.config.js, package.json
Introduces build script and generated font/atlas assets with licenses and config updates.

UnitLayer dynamic movers and trails

Layer / File(s) Summary
Segment motion sampling and trail raster
src/client/graphics/layers/SegmentMotionSample.ts, src/client/graphics/layers/SegmentTrailRaster.ts
Adds plan sampling and rasterization helpers.
Trail lifecycle pruning
src/client/graphics/layers/TrailLifecycle.ts
Removes inactive trails by predicate.
UnitLayer mover engine and trails
src/client/graphics/layers/UnitLayer.ts
Implements budgeted mover rendering, overlap resolution, and trail drawing.
StructureIconsLayer devicePixelRatio and compositing
src/client/graphics/layers/StructureIconsLayer.ts
Scales Pixi canvas to DPR and maps draw sizes explicitly.

Settings, selects, labels, and small UI tweaks

Layer / File(s) Summary
UserSettingModal: territory renderer/border selects
src/client/UserSettingModal.ts
Adds selects and handlers for renderer and border mode.
SettingSelect id/easter and typing
src/client/components/baseComponents/setting/SettingSelect.ts
Exports option type; adds id/easter; binds label/select.
SettingsModal: WebGPU Debug toggle
src/client/graphics/layers/SettingsModal.ts
Adds toggle button for webgpuDebug.
i18n for renderer/borders and cosmetics free label
resources/lang/en.json
Adds new localization strings.
Cosmetic bonus badge and navbar class tweak
src/client/components/CosmeticButton.ts, src/client/components/*NavBar.ts
Adds “free bonus” badge and no-crazygames class.

Startup sequencing and container tweak

Layer / File(s) Summary
ClientGameRunner async start
src/client/ClientGameRunner.ts
Awaits start and renderer initialization.
Dockerfile timeout increase
Dockerfile
Extends supervisord timeout to 720h.

Sequence Diagram(s)

(omitted)

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested labels

UI/UX, Feature

Suggested reviewers

  • evanpelle

Poem

New maps hum with GPU light,
Names now dance in Pixi’s night.
Movers weave on budgeted trails,
WebGL, WebGPU swap their sails.
Panels float, their secrets told,
Settings click—new choices bold.
Ships glide smooth; the borders hold.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Guard playerProfile from stale hover updates.

hide() clears the visible target, but playerProfile stays live, and an older profile() 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 win

Use a unique default id per instance.

A fixed default id can create duplicate DOM ids across multiple setting-select components, 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 win

Localize 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 through translateText() function with corresponding entries in resources/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 win

Move 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() and resources/lang/en.json.

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.

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 win

Translate these exported failure reasons.

These strings are returned through create().reason and getFailureReason(), so they are part of the client status surface. They should come from translateText() with keys in resources/lang/en.json, not hardcoded English. 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.

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 win

Avoid rebuilding the bind group every frame.

execute() currently calls rebuildBindGroup() 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

📥 Commits

Reviewing files that changed from the base of the PR and between ddf6306 and 0285d7c.

⛔ Files ignored due to path filters (6)
  • package-lock.json is excluded by !**/package-lock.json
  • resources/fonts/namelayer_overpass.png is excluded by !**/*.png
  • resources/fonts/overpass-regular.otf is excluded by !**/*.otf
  • resources/fonts/twemoji-colr.woff2 is excluded by !**/*.woff2
  • resources/images/namelayer-emojis.png is excluded by !**/*.png
  • resources/images/namelayer-icons.png is excluded by !**/*.png
📒 Files selected for processing (98)
  • Dockerfile
  • eslint.config.js
  • index.html
  • package.json
  • resources/fonts/namelayer_overpass.xml
  • resources/fonts/overpass-OFL.txt
  • resources/fonts/twemoji-colr-OFL.txt
  • resources/images/namelayer-emojis.json
  • resources/images/namelayer-icons.json
  • resources/lang/en.json
  • scripts/build-namelayer-assets.mjs
  • src/client/ClientGameRunner.ts
  • src/client/InputHandler.ts
  • src/client/UserSettingModal.ts
  • src/client/components/CosmeticButton.ts
  • src/client/components/DesktopNavBar.ts
  • src/client/components/MobileNavBar.ts
  • src/client/components/baseComponents/setting/SettingSelect.ts
  • src/client/graphics/GameRenderer.ts
  • src/client/graphics/HoverInfo.ts
  • src/client/graphics/PlayerIcons.ts
  • src/client/graphics/TransformHandler.ts
  • src/client/graphics/layers/ClassicCanvasTerritoryLayer.ts
  • src/client/graphics/layers/ClassicTerritoryBackend.ts
  • src/client/graphics/layers/Layer.ts
  • src/client/graphics/layers/NameLayer.ts
  • src/client/graphics/layers/NameLayerAssets.ts
  • src/client/graphics/layers/NameLayerLayout.ts
  • src/client/graphics/layers/PerformanceOverlay.ts
  • src/client/graphics/layers/PlayerInfoOverlay.ts
  • src/client/graphics/layers/RendererStatusPanel.ts
  • src/client/graphics/layers/SegmentMotionSample.ts
  • src/client/graphics/layers/SegmentTrailRaster.ts
  • src/client/graphics/layers/SettingsModal.ts
  • src/client/graphics/layers/StructureIconsLayer.ts
  • src/client/graphics/layers/TerritoryBackend.ts
  • src/client/graphics/layers/TerritoryLayer.ts
  • src/client/graphics/layers/TerritoryWebGLRenderer.ts
  • src/client/graphics/layers/TrailLifecycle.ts
  • src/client/graphics/layers/UnitLayer.ts
  • src/client/graphics/layers/WebGLTerritoryBackend.ts
  • src/client/graphics/layers/WebGPUDebugOverlay.ts
  • src/client/graphics/layers/WebGPUTerritoryBackend.ts
  • src/client/graphics/webgpu/TerritoryRenderer.ts
  • src/client/graphics/webgpu/compute/ComputePass.ts
  • src/client/graphics/webgpu/compute/DefendedStrengthFullPass.ts
  • src/client/graphics/webgpu/compute/DefendedStrengthPass.ts
  • src/client/graphics/webgpu/compute/StateUpdatePass.ts
  • src/client/graphics/webgpu/compute/TerrainComputePass.ts
  • src/client/graphics/webgpu/compute/VisualStateSmoothingPass.ts
  • src/client/graphics/webgpu/core/GroundTruthData.ts
  • src/client/graphics/webgpu/core/ShaderLoader.ts
  • src/client/graphics/webgpu/core/WebGPUDevice.ts
  • src/client/graphics/webgpu/render/RenderPass.ts
  • src/client/graphics/webgpu/render/TemporalResolvePass.ts
  • src/client/graphics/webgpu/render/TerrainShaderRegistry.ts
  • src/client/graphics/webgpu/render/TerritoryPostSmoothingRegistry.ts
  • src/client/graphics/webgpu/render/TerritoryPreSmoothingRegistry.ts
  • src/client/graphics/webgpu/render/TerritoryRenderPass.ts
  • src/client/graphics/webgpu/render/TerritoryShaderRegistry.ts
  • src/client/graphics/webgpu/shaders/compute/defended-strength-full.wgsl
  • src/client/graphics/webgpu/shaders/compute/defended-strength.wgsl
  • src/client/graphics/webgpu/shaders/compute/state-update.wgsl
  • src/client/graphics/webgpu/shaders/compute/terrain-compute-improved-heavy.wgsl
  • src/client/graphics/webgpu/shaders/compute/terrain-compute-improved-lite.wgsl
  • src/client/graphics/webgpu/shaders/compute/terrain-compute.wgsl
  • src/client/graphics/webgpu/shaders/compute/visual-state-smoothing.wgsl
  • src/client/graphics/webgpu/shaders/render/retro.wgsl
  • src/client/graphics/webgpu/shaders/render/temporal-resolve.wgsl
  • src/client/graphics/webgpu/shaders/render/territory.wgsl
  • src/client/vite-env.d.ts
  • src/core/CosmeticSchemas.ts
  • src/core/configuration/Config.ts
  • src/core/configuration/PastelTheme.ts
  • src/core/configuration/PastelThemeDark.ts
  • src/core/execution/NukeExecution.ts
  • src/core/execution/TradeShipExecution.ts
  • src/core/execution/TransportShipExecution.ts
  • src/core/game/GameImpl.ts
  • src/core/game/GameMap.ts
  • src/core/game/GameView.ts
  • src/core/game/MotionPlans.ts
  • src/core/game/UnitImpl.ts
  • src/core/game/UserSettings.ts
  • src/core/pathfinding/PathFinder.ts
  • src/core/pathfinding/PathFinderStepper.ts
  • src/core/pathfinding/transformers/SmoothingWaterTransformer.ts
  • src/core/pathfinding/types.ts
  • src/server/PublicAssetManifest.ts
  • tests/MiniMapTransformerPlanSegments.test.ts
  • tests/MotionPlansSegments.test.ts
  • tests/NameLayer.test.ts
  • tests/PathFinderStepperPriming.test.ts
  • tests/SegmentTrailRaster.test.ts
  • tests/TerritoryBackendSelection.test.ts
  • tests/UnitLayerTrailLifecycle.test.ts
  • tests/core/pathfinding/PathFinderStepper.test.ts
  • tests/server/PublicAssetManifest.test.ts

Comment on lines +145 to 148
.then(async (r) => {
currentGameRunner = r;
r.start();
await r.start();
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Comment on lines +246 to +255
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();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Comment on lines +557 to +589
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);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Comment on lines +55 to +58
dispose() {
// Classic layers own only offscreen canvases and event-bus listeners.
// The event bus does not currently expose unsubscribe hooks.
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

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.

Comment on lines +1314 to +1353
${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>`
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.).

Comment on lines +36 to +155
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,
},
],
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Comment on lines +15 to +62
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,
},
],
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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().

Comment on lines +15 to +53
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,
},
],
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Comment on lines +263 to +288
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();
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Suggested change
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.

Comment on lines +780 to +784
.options=${[
{ value: 0, label: "Off" },
{ value: 1, label: "Simple" },
{ value: 2, label: "Glow" },
]}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

@github-project-automation github-project-automation Bot moved this from Triage to Development in OpenFront Release Management May 27, 2026
@dustinlacewell
Copy link
Copy Markdown

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:

  • GPU-instanced MSDF
  • one instanced draw-call for all player text
  • position interpolation done in the shader
  • reads from packed playerData texture using diff-based uploads
  • MSDF + fwidth outline in the shader
  • full kerning via CPU-built kern table
  • Status icons, flags, emoji rendered through parallel instanced programs reading the same texture

The PR's NameLayer:

  • Pixi scene-graph (CPU)
  • PIXI.Container per player wrapping BitmapText + sprites

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:

  • Territory smoothing approach
  • Pathfinder PlanSegments. This isn't strictly necessary, the renderer's shim already computes an GPU friendly data format from the game's data for efficient rendering. This would make the renderer passes strictly more simple though.
  • Twemoji COLR emoji integration
  • The NameLayerLayout decomposition
  • Missing character fallback

@scamiv
Copy link
Copy Markdown
Contributor Author

scamiv commented May 27, 2026

The current NamePass:

  • GPU-instanced MSDF
  • one instanced draw-call for all player text
  • position interpolation done in the shader
  • reads from packed playerData texture using diff-based uploads
  • MSDF + fwidth outline in the shader
  • full kerning via CPU-built kern table
  • Status icons, flags, emoji rendered through parallel instanced programs reading the same texture

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 a pre-existing and well-proven design choice.
The pixi implementation gracefully degrades back to cpu while offering better performance then software fallback for failing webgl.

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)

@scamiv
Copy link
Copy Markdown
Contributor Author

scamiv commented May 27, 2026

The PR ultimately represents a 4 renderer matrix, requiring parallel implementations, and a per-feature judgment on which layer will render what.

read the description, this originally is multiple pr's with separate concerns, nothing requires "parallel implementation"

@dustinlacewell
Copy link
Copy Markdown

The PR ultimately represents a 4 renderer matrix, requiring parallel implementations, and a per-feature judgment on which layer will render what.

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.

@dustinlacewell
Copy link
Copy Markdown

dustinlacewell commented May 27, 2026

The current NamePass:

  • GPU-instanced MSDF
  • one instanced draw-call for all player text
  • position interpolation done in the shader
  • reads from packed playerData texture using diff-based uploads
  • MSDF + fwidth outline in the shader
  • full kerning via CPU-built kern table
  • Status icons, flags, emoji rendered through parallel instanced programs reading the same texture

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 a pre-existing and well-proven design choice. The pixi implementation gracefully degrades back to cpu while offering better performance then software fallback for failing webgl.

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)

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.

@scamiv
Copy link
Copy Markdown
Contributor Author

scamiv commented May 27, 2026

The PR ultimately represents a 4 renderer matrix, requiring parallel implementations, and a per-feature judgment on which layer will render what.

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.

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.

@dustinlacewell
Copy link
Copy Markdown

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.

This is the kind of "parallel implementation" I'm referring to.

There is work on using webgpu as authoritative source for distance calculations

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.

@evanpelle evanpelle closed this May 27, 2026
@github-project-automation github-project-automation Bot moved this from Development to Complete in OpenFront Release Management May 27, 2026
@scamiv
Copy link
Copy Markdown
Contributor Author

scamiv commented May 27, 2026

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Complete

Development

Successfully merging this pull request may close these issues.

3 participants