Skip to content

perf(game): three.js graphics memory & hot-path optimizations (plan phases 1–3, 5.1) - #4855

Open
ponderingdemocritus wants to merge 1 commit into
nextfrom
claude/three-graphics-phase3
Open

perf(game): three.js graphics memory & hot-path optimizations (plan phases 1–3, 5.1)#4855
ponderingdemocritus wants to merge 1 commit into
nextfrom
claude/three-graphics-phase3

Conversation

@ponderingdemocritus

Copy link
Copy Markdown
Contributor

Summary

Memory and hot-path performance work for the Three.js game client (client/apps/game/src/three), aimed at low-spec hardware. The changes come out of a deep multi-agent review of the graphics layer (328 files / ~72k lines); the full prioritized plan lives in GRAPHICS_OPTIMIZATION_PLAN.md (added in the Phase 1 commit).

This PR bundles four phases of that plan. Commits are scoped one-per-phase, so it reviews cleanly commit-by-commit.

What's in here (by commit)

9582acc — Phase 1: scope worldmap terrain cache invalidation
Stop the cache-invalidation storms that defeat the terrain cache during exploration/panning.

  • Structure count changes now invalidate only the affected chunks instead of flushing the whole terrain matrix cache + global pools.
  • Army movement no longer invalidates terrain (verified the terrain cache reads only biomes + structures, never armies).
  • Per-chunk generation counters replace a single global counter that marked every cached chunk stale on any tile change.
  • The terrain fingerprint is now a compact FNV-1a digest instead of a ~60KB sorted-joined string rebuilt on every cache hit.

b667108 — Phase 2.1–2.3: GPU leaks + morph-texture crash

  • Arrival-ghost ring geometries are shared instead of leaked two-per-ghost.
  • Prepared-terrain pooled attributes are released on every dropped chunk transition (prewarm / rollback / stale-drop) instead of leaked.
  • Morph textures are resized on instance-capacity growth — previously setMorphAt indexed past a fixed Float32Array once >64 animated army slots existed and threw a RangeError on the frame path.

49422835 — Phase 2.5: manager destroy hygiene
ChestManager/ArmyModel/InstancedModel dispose paths now free instanced GPU buffers + morph textures (via InstancedMesh.dispose(), which leaves shared geometry/materials intact), with an isDestroyed guard on the async chest load; removed a dead Group hexception leaked on every grid rebuild.

586d378 — Phase 5.1: share biome GLTFs across scenes
The ~23 biome GLBs were parsed and GPU-resident twice (worldmap + hexception). They're now parsed once via a module cache and shared; only per-scene instance buffers + morph textures differ. Includes an idempotent biome draw-order fix so the shared material resolves the same renderOrder in both scenes.

26b97886 — Phase 3: hot-path allocation diet (scales with entity count)

  • Per-frame biome resolution (BigInt/simplex noise, twice per moving army) is memoized by hex.
  • Attachment-transform sync no longer builds an input object + 5 closures for every visible army each frame (hoisted the membership guard → scales with attachment count, not army count).
  • Compact labels resolve the title directly instead of building the full view model (+detailRows) per moving army per frame.
  • Structure hover resolves via the existing spatial-bucket index instead of an O(all structures) scan + Array.from per pointermove.
  • Frame-loop micro-churn: diagnostics skip the per-frame window snapshot when unchanged; the 1 Hz stamina recompute is gated behind the existing armies-tick policy; the visibility manager mutates its cache record in place instead of allocating on every miss.

How it was built

  • TDD throughout: new pure logic (fingerprint digest, per-chunk generation, morph-texture resize, prepared-terrain dispose decisions, biome/structure-hex caches, title resolvers, diagnostics gating) lands with unit tests; GPU-lifecycle and allocation refactors are behaviour-preserving and covered by existing tests + careful reading.
  • Behaviour-preserving by design: cache-invalidation changes were verified against what the terrain cache actually depends on; the attachment and stamina changes reuse guards/policies that already existed.

Deferred (intentionally, to focused follow-ups)

  • 1.5 compose-once-per-batch terrain presentation (590-line runtime API refactor; needs runtime visual QA).
  • 2.4 world-update subscription contract (cross-package: packages/core/world-update-listener.ts must return its unsubscribers).
  • 3.5 TileOpt hydration spatial index (needs a new ECS-stream index; a bug there means missing terrain/structures → needs runtime QA).
  • 2.5 tail (InteractiveHex/HighlightHex cleanup, shared-material ownership) — documented in the plan.

Reviewer notes / test status

  • CI must run vitest + tsc. These were authored in a sandboxed worktree where node is killed, so the red→green cycle was verified by construction, not executed locally. Nothing else gates correctness of the new tests.
  • Runtime QA recommended before merge:
    • Phase 1: pan + explore while founding/destroying structures — no stale/blank terrain; army movement no longer rebuilds neighbour chunks.
    • Phase 2: spawn >64 concurrent animated armies (no RangeError); watch MatrixPool/attribute-pool stats during fast pans (dropped chunks return their attributes).
    • Phase 5.1: eyeball hexception + worldmap terrain, especially transparent biomes' draw order.
    • Phase 3: structure hover still resolves; stamina labels update on tick advance; no visibility/animation regressions.

🤖 Generated with Claude Code

…raphics plan phase 3)

Remove the per-frame allocations that grow with army/structure count on the
worldmap hot path. Each change lands with unit tests or is a behaviour-preserving
refactor covered by existing tests.

- 3.1 ArmyModel resolved a moving army's hex biome via BigInt/simplex-noise twice
  per frame, though biome is immutable per hex. Memoize via a new bounded
  per-hex cache (utils/bounded-hex-cache.ts, tested) with a stable resolver field
  so the lookups allocate no closures.
- 3.2 The attachment-transform sync allocated a 12-field input object + 5 delegate
  closures for every visible army each frame whenever any cosmetic attachment was
  active, even though the callee early-returns for non-attachment entities. Hoist
  the membership check to both call sites so it scales with attachment count, not
  army count (behaviour-preserving — the callee's guard is identical).
- 3.3 The compact label built the entire entity-label view model (incl. detailRows)
  per moving army per frame just to read the title. Export resolveArmyTitle/
  resolveStructureTitle and call them directly; output is unchanged.
- 3.4 getStructureByHexCoords scanned every structure with an Array.from+find per
  group on each pointermove. Resolve from the chunkToStructures spatial bucket (the
  same index the visible-structure pass trusts) via a new tested exact-hex lookup.
- 3.6 Frame-loop micro-churn: renderer-diagnostics skips the per-frame window
  snapshot when the scene name is unchanged (tested); the 1Hz stamina recompute is
  gated behind the existing armies-tick policy (battle timers keep the 1s cadence);
  the centralized visibility manager mutates its cache record in place instead of
  allocating a fresh one on every miss.

Deferred to a follow-up (needs a new ECS spatial index + runtime QA): 3.5 (the
global TileOpt hydration scan / per-entity Position allocation).

Note: vitest/tsc not run here (node killed in worktree shell); red->green verified
by construction, CI runs the suite. Wants runtime QA: structure hover resolves via
the spatial index, stamina labels update on tick advance, no visibility regressions.
@vercel

vercel Bot commented Jun 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
eternum Ready Ready Preview, Comment Jun 10, 2026 10:22am
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
eternum-docs Ignored Ignored Preview Jun 10, 2026 10:22am
eternum-mobile Ignored Ignored Preview Jun 10, 2026 10:22am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

Failed to generate code suggestions for PR

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant