Commit graph

784 commits

Author SHA1 Message Date
Erik
71d0edc3d7 fix(world #53): namespace stab Ids globally for Tier 1 cache safety
LandblockLoader.BuildEntitiesFromInfo restarted nextId at 1 per landblock,
producing colliding entity.Id values across landblocks. EntityClassificationCache
keys by entity.Id alone, so cross-LB collisions caused cache pollution:
multiple stabs sharing id=1 -> cache entry for id=1 ended up with the
CONCATENATION of multiple entities' batches -> buildings rendered up in the
air with wrong textures (visual gate observation 2026-05-10).

Audit at docs/research/2026-05-10-tier1-mutation-audit.md did not verify
entity.Id uniqueness - that was an unchecked assumption. Cache design
trusted entity.Id was globally unique; for stabs it wasn't.

Fix: optional landblockId parameter on BuildEntitiesFromInfo. When non-zero,
stab Ids are namespaced as 0xC0XXYY00 + nextId, matching the scenery
(0x80XXYY00) and interior (0x40XXYY00) namespacing already in GameWindow.cs.
The 0xC0 top byte distinguishes stabs from those. Existing tests pass
landblockId=0 and keep their legacy starting-from-1 behavior.

Known latent: if any one landblock has >256 stabs, nextId overflows the
low byte. Same pattern + same limitation as scenery/interior. Out of scope
for the immediate Tier 1 cache bug; not affecting current Holtburg play.

Adds 2 regression tests pinning the namespacing + the legacy fallback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 20:07:19 +02:00
Erik
4df19146ff docs(render #53): clarify DebugCrossCheck's wiring status
Code review of f16604b flagged that DebugCrossCheck's XML doc claimed
"called once per static-entity cache hit per frame" — overstated. The
method is currently exercised by unit tests only; the dispatcher's
cache-hit branch fires a simpler predicate assert (!isAnimated) at
production hit time, not the full live-state cross-check. Wiring the
full cross-check is the spec section 6.5 stretch goal, kept open as a
follow-up.

Doc-only change. No behavior change. 1708 / 8 baseline preserved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 19:49:13 +02:00
Erik
f16604b60b feat(render #53): DEBUG cross-check guards against the prior Tier 1 bug class
Adds EntityClassificationCache.DebugCrossCheck(entityId, liveBatches) that
asserts cached state matches a live re-classification. Wires a simpler
predicate assert into WbDrawDispatcher's cache-hit branch (asserts
isAnimated == false on cache hit). Tests #13a and #13b cover the
batch-count mismatch and clean-match cases via a custom TraceListener
that captures Debug.Assert calls.

Zero cost in Release. In DEBUG, the assert fires immediately if a future
regression mutates static-entity state outside the audit's known write
sites — the same failure mode that bit the prior Tier 1 attempt.

Phase 4 complete. Cache + invalidation + safety net all in place.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 19:43:24 +02:00
Erik
489174f21c feat(render #53): wire EntityClassificationCache.InvalidateLandblock at LB demote/unload
GpuWorldState.RemoveEntitiesFromLandblock now invokes an optional
Action<uint> callback before zeroing the entity list. GameWindow wires
this to EntityClassificationCache.InvalidateLandblock so cache entries
get swept on LB demote (Near to Far) and unload. Per spec section 5.3 W3b.

The callback receives the canonicalized landblock id (low 16 bits forced
to 0xFFFF), matching the LandblockHint stored at Populate time. Trace:
GpuWorldState._loaded keys are canonical (set by AppendLiveEntity),
LandblockEntries yields kvp.Key as LandblockId, WalkEntitiesInto
propagates entry.LandblockId into _walkScratch, the dispatcher's
populateLandblockId reads that tuple and stores it as LandblockHint.

Phase 3 (invalidation hooks) complete. The cache now stays correct across
all spec-identified mutation events: despawn, ObjDescEvent (despawn+
respawn), LB demote, LB unload.

Two integration tests added:
- RemoveEntitiesFromLandblock_FiresUnloadCallbackWithCanonicalId asserts
  the callback fires once with the canonical id even when called with a
  cell-resolved input (low 16 bits non-FFFF).
- RemoveEntitiesFromLandblock_NotLoaded_DoesNotFireCallback asserts the
  early-return path doesn't fire the callback for unknown landblocks.

Tests: 1706 passed / 8 failed (baseline). Sentinel: 110/110.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 19:32:16 +02:00
Erik
1d1afcd562 feat(render #53): wire EntityClassificationCache.InvalidateEntity at despawn
GameWindow.RemoveLiveEntityByServerGuid now invalidates the entity's
cache entry next to the existing _animatedEntities.Remove(). Fires for
DeleteObject (0xF747) and the dedup leg of ObjDescEvent (0xF625).

Adds test #15 (despawn-respawn under reused id repopulates fresh) per
spec section 7.5 — pins the audit's ObjDescEvent-as-despawn-respawn contract.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 19:22:50 +02:00
Erik
f7e38c214d fix(render #53): cache-hit fast path must fire per-entity, not per-tuple
Task 10 (commit 0cbef3c) called ApplyCacheHit inside the per-(entity, partIdx)
foreach loop, but cachedEntry.Batches is flat across all MeshRefs of the
entity. For a 3-MeshRef static building on frame 2: 3 tuples times 6 cached
batches per call = 18 instances drawn instead of 6. Severe Z-fighting and
3x perf hit on every multi-part static entity (buildings, statues, multi-
MeshRef NPCs).

This is the symmetric mirror of the Task 9 bug fixed at 00fa8ae. Both
spec section 5.2 and the plan describe the foreach as per-entity, but
_walkScratch has been per-tuple since Task 6. The implementation
faithfully ported the buggy spec.

Fix: track lastHitEntityId; the cache-hit fast path fires only on the
first tuple of each entity, and subsequent tuples skip the iteration
body via continue. Adds a regression test pinning the per-entity
amplification invariant.

Caught by code review (subagent-driven-development) before Phase 3
dispatched. The bug was invisible in the no-multi-frame-test 1702/8
baseline; would have manifested as visible Z-fighting on every multi-
part building on second-and-subsequent frames once Task 13 perf gate
captured live runs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 19:15:20 +02:00
Erik
0cbef3c8b3 feat(render #53): cache-hit fast path + dispatcher integration tests
WbDrawDispatcher.Draw now branches on cache hit before running classification:
on hit, walks the cached flat batch list and appends RestPose times entityWorld
to the matching groups; on miss, runs today's classification and populates
the cache (Task 9). Animated entities skip the cache entirely.

Adds dispatcher integration tests #11 (static entity populates + reuses)
and #12 (animated bypasses) per spec test plan section 7.2, plus the
multi-MeshRef regression test that would have caught the bug fixed in
commit 00fa8ae (cache populate must flush at entity boundary, not per-tuple).

Phase 2 (dispatcher integration) complete. End-to-end caching now live.
Invalidation hooks (Phase 3) ensure correctness across despawns + LB demotes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 18:56:33 +02:00
Erik
00fa8ae839 fix(render #53): cache Populate must flush at entity boundary, not per-MeshRef tuple
Task 9 (commit 2f489a8) called _cache.Populate inside the per-tuple
foreach loop, but _walkScratch contains one tuple per (entity, MeshRefIndex)
and the cache is keyed by entity.Id. For multi-MeshRef entities (multi-part
Setup buildings, statues, multi-MeshRef NPCs), each iteration's Populate
OVERWROTE the previous one — only the last MeshRef's batches survived.

The bug was invisible at commit time because Task 10 had not landed
(cache populates but isn't read). It would have manifested the moment
Task 10 wired the cache-hit fast path: every multi-part static building
in Holtburg would render as N stacked copies of its last part.

Fix: restructure the per-entity loop with a flush-on-entity-change pattern.
Track the previous entity's Id; when the iteration moves to a different
entity, flush the previous entity's accumulated _populateScratch via one
Populate call. After the loop, flush the final entity. _populateScratch
is now cleared at flush time, not per-iteration.

Caught by code review (subagent-driven-development) before Task 10 dispatched.
Verified: 1699/8 baseline preserved, sentinel 105/105 unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 18:36:57 +02:00
Erik
2f489a83a7 feat(render #53): cache-miss populate on first frame for static entities
Restructures Draw's per-entity loop: animated entities still skip the
cache entirely, but static entities now collect their classification into
_populateScratch and call cache.Populate at the end of the iteration.

Cache fast-path (skip slow classification on cache hit) lands in Task 10.
This intermediate state is verifiable: behavior unchanged, but the cache
is being populated as entities render. Diagnostic-friendly split.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 18:24:26 +02:00
Erik
28513eae88 feat(render #53): add optional CachedBatch collector to ClassifyBatches
ClassifyBatches now accepts a restPose parameter (the model-matrix
component without entityWorld baked in) and an optional collector. When
collector is non-null, each classified batch is appended as a CachedBatch
record. Defaults preserve today's behavior. Used in Task 9 to populate
the cache on a static-entity miss.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 18:14:35 +02:00
Erik
a65a241981 feat(render #53): inject EntityClassificationCache into WbDrawDispatcher
Adds the cache as a constructor parameter on WbDrawDispatcher and a
private field on GameWindow. The cache is passed through but not yet
consumed by Draw — that wires up in Task 9 (cache miss / populate) and
Task 10 (cache hit / fast path).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 18:05:03 +02:00
Erik
60fbfce8bc refactor(render #53): plumb landblockId through WbDrawDispatcher walkScratch
Extends the walk scratch tuple from (entity, meshRefIndex) to
(entity, meshRefIndex, landblockId). The dispatcher's per-entity loop now
has the landblock id available for EntityClassificationCache.Populate's
landblockHint argument (consumed in Task 9). No behavior change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 17:55:51 +02:00
Erik
a171e7007b feat(render #53): EntityClassificationCache.InvalidateLandblock + tests
Sweep-by-landblock removal for the streaming demote/unload path. Tests
#6, #7, #8 from spec section 7.1 lock in: (a) all matching entries removed,
(b) non-matching entries preserved, (c) idempotent on missing LB.

Phase 1 (cache foundation) complete. 11 cache tests passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 17:47:57 +02:00
Erik
aea4460eae feat(render #53): EntityClassificationCache.InvalidateEntity + tests
Idempotent removal of a cached entry by entity id. Tests #4 and #5 from
spec section 7.1 lock in the contract.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 17:42:09 +02:00
Erik
694815c499 feat(render #53): EntityClassificationCache.Populate + roundtrip tests
Implements Populate (insert-or-overwrite) and adds 5 tests covering the
populate->TryGet round-trip including the Setup pre-flatten shape. Per
spec test plan section 7.1 tests #2, #3, #9, #10, #14.

Tests use xUnit Assert.* (not FluentAssertions) to match the Task 2
implementer's choice and the existing 149 sibling assertions in the Wb
test directory.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 17:34:48 +02:00
Erik
773e9703da feat(render #53): EntityClassificationCache skeleton + first test
Adds CachedBatch, EntityCacheEntry, and EntityClassificationCache with
just TryGet (returns false on empty). The skeleton compiles and the first
test (TryGet_EmptyCache_ReturnsFalse) passes. Subsequent tasks add
Populate, InvalidateEntity, InvalidateLandblock, and the dispatcher
integration. Per spec design Section 6.1.

Note: CachedBatch / EntityCacheEntry / EntityClassificationCache are
internal (not public as the plan snippet showed). Their members
transitively reference the internal GroupKey type, so promoting them to
public produces CS0051 inconsistent-accessibility errors. The cache is
dispatcher-internal coordination state anyway, and the AcDream.App
csproj already exposes internals to AcDream.Core.Tests via
InternalsVisibleTo, so the test sees everything it needs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 17:23:37 +02:00
Erik
c02405cbb7 refactor(render): extract WbDrawDispatcher.GroupKey to internal type at namespace scope
Mechanical refactor: GroupKey was a private nested record struct on
WbDrawDispatcher. The upcoming EntityClassificationCache (ISSUE #53) needs
to store GroupKey inside CachedBatch records, so it must be visible to
both the dispatcher and the cache. Promoting to internal at file scope is
the smallest change that achieves this.

No behavior change. 1688 tests pass; 8 pre-existing failures unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 17:13:44 +02:00
Erik
2f8a574b92 docs(post-A.5 #53): Tier 1 cache — implementation plan (writing-plans)
17-task TDD-style plan for the Tier 1 entity-classification cache, sized
~5-7 days. Phases:

  Phase 1 (Tasks 1-5):  Cache foundation — extract GroupKey, build the
                        cache class with TryGet/Populate/InvalidateEntity/
                        InvalidateLandblock, and 11 pure-CPU tests.
  Phase 2 (Tasks 6-10): Dispatcher integration — plumb landblockId
                        through the walk scratch, inject the cache,
                        wire ClassifyBatches collector + cache-miss
                        populate + cache-hit fast path. +2 integration
                        tests.
  Phase 3 (Tasks 11-12): Invalidation hooks — wire InvalidateEntity from
                        RemoveLiveEntityByServerGuid + InvalidateLandblock
                        from GpuWorldState.RemoveEntitiesFromLandblock
                        via callback (W3b per spec §5.3).
  Phase 4 (Task 13):    DEBUG cross-check — assert membership predicate
                        + DebugCrossCheck method + 2 unit tests via
                        TraceListener capture.
  Phase 5 (Tasks 14-16): Verification — full suite + sentinel + visual
                        gate (user) + perf gate (user, ≤2.0 ms median).
  Phase 6 (Task 17):    Ship — ISSUES + CLAUDE.md + memory + final commit.

Plan resolves spec §11 open implementation choices: W3b for LB invalidation,
GroupKey at namespace internal, ResolveLandblockHint plumbed via walk
scratch, _populateScratch as a field.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 17:06:42 +02:00
Erik
4abb838729 docs(post-A.5 #53): Tier 1 retry — mutation audit + cache design spec
The audit at docs/research/2026-05-10-tier1-mutation-audit.md enumerates
every entity.MeshRefs write site (5 STATIC at hydration, 1 DYNAMIC at
GameWindow.cs:7580 inside TickAnimations) and verifies that all 7
Position/Rotation write sites only touch entities in _animatedEntities.
Establishes the load-bearing invariant: an entity's renderer state is
stable from spawn to despawn iff entity.Id is NOT in _animatedEntities.

The spec at docs/superpowers/specs/2026-05-10-issue-53-tier1-cache-design.md
locks in the design from brainstorming on 2026-05-10:
- Static-only cache + DEBUG cross-check (option c) — catches future
  regressions of the prior bug class without paying perf cost in Release
- Separate EntityClassificationCache class injected into WbDrawDispatcher
- Cache the rest pose, not the full model matrix (Position/Rotation read
  live each frame so Release stays correct even if the invariant breaks)
- Pre-flatten Setup multi-parts at populate time (the bulk of the win)
- 15 new tests covering all invalidation paths + DEBUG cross-check +
  Setup pre-flatten + lifecycle pin

Closes the audit + design steps of the post-A.5 polish Priority 3 work.
Implementation plan owned by superpowers:writing-plans next.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:50:26 +02:00
Erik
f66522cd6b Merge branch 'claude/cranky-varahamihira-fe423f' — Tier 1 retry cold-start handoff 2026-05-10 16:14:24 +02:00
Erik
15376c7a73 docs(post-A.5): cold-start handoff for the Tier 1 retry session (#53)
After the post-A.5 lifestone (#52) + JobKind plumbing (#54) work shipped,
only Priority 3 (Tier 1 entity-classification cache retry, ISSUE #53)
remains. This handoff captures the audit insights gathered during the
#52 investigation that the original post-A.5 handoff didn't have:

- MeshRef is a `readonly record struct` — its fields can NOT be mutated
  in place. The actual per-frame mutation for animated entities is the
  entire MeshRefs LIST replacement at GameWindow.cs:7474-7553. This
  reframes the cache design.
- _animatedEntities dict at GameWindow.cs:160 is the source of truth
  for which entities go through the per-frame rebuild path.
- Static entity = entity.Id NOT in _animatedEntities. Its MeshRefs is
  the same instance from spawn until rare events (ObjDesc / palette
  swap / part hide / scale apply).
- Recommended cache approach: static-only with explicit invalidation
  hooks on the network/spawn-time write sites enumerated in the doc.

Doc covers: where main is, what shipped this session, why the first
Tier 1 attempt failed, the pre-started audit, cache design options,
acceptance criteria, files to read, workflow for the next session, and
things-to-NOT-do.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:14:24 +02:00
Erik
da08490ab0 Merge branch 'claude/cranky-varahamihira-fe423f' — Post-A.5 polish: close #52 (lifestone) + #54 (JobKind plumbing) 2026-05-10 16:08:32 +02:00
Erik
9a55354143 docs(post-A.5 #54): close JobKind plumbing issue + update CLAUDE.md flight status
Move ISSUE #54 to Recently closed referencing commit `bf31e59`. Drop
#54 from CLAUDE.md "Currently in flight" — only #53 (Tier 1 retry)
remains open in the post-A.5 polish phase.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:04:01 +02:00
Erik
bf31e59805 fix(streaming): close #54 — plumb JobKind through BuildLandblockForStreaming
Bug A's fix (commit `9217fd9`) patched at the worker output by stripping
entities from far-tier `LoadedLandblock`s after the full `LoadNear` path
ran. The worker still wasted CPU on `LandBlockInfo` reads + entity
hydration + `SceneryGenerator` math + interior-cell walks for ~544
far-tier LBs at radius=12, just to throw the work away.

This commit plumbs `LandblockStreamJobKind` through to the factory so the
worker can branch at the source:

- `LandblockStreamer.cs`: replace the `Func<uint, LoadedLandblock?>`
  factory with `Func<uint, LandblockStreamJobKind, LoadedLandblock?>` as
  the primary ctor signature. Add a back-compat overload that wraps the
  old single-arg signature (`(id, _) => loadLandblock(id)`) so existing
  test code keeps compiling without modification — the 5 ctor sites in
  `LandblockStreamerTests.cs` now resolve to the overload. `HandleJob`
  passes `load.Kind` to the factory; the post-load entity-strip is
  retained as a `Debug.Assert` + Release safety net.

- `GameWindow.cs`: `BuildLandblockForStreaming(uint, JobKind)` branches
  on `kind == LoadFar` at the top — reads only the `LandBlock` heightmap
  dat and returns a `LoadedLandblock` with `Array.Empty<WorldEntity>()`.
  Skips `LandblockLoader.Load` (which reads `LandBlockInfo`),
  `BuildSceneryEntitiesForStreaming`, and `BuildInteriorEntitiesForStreaming`
  entirely. Near-tier path is unchanged. Both call sites updated to pass
  the kind through the lambda: `(id, kind) => BuildLandblockForStreaming(id, kind)`.

Tests: 1688/1696 (8 pre-existing physics/input failures unchanged).
Streaming-targeted filter (30 tests covering LandblockStreamer +
StreamingController + StreamingRegion) all green via the back-compat
overload — no test code needed updating.

Per-LB worker cost on far-tier: was ~tens of ms (full hydration,
including LandBlockInfo + scenery generation + interior cells); now a
single `LandBlock` dat read (~sub-ms).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:03:16 +02:00
Erik
b19f1d10ec docs(post-A.5 #52): close lifestone issue + update CLAUDE.md flight status
Move ISSUE #52 from Active to Recently closed with full root-cause writeup
referencing commit `e40159f`. Strip lifestone reference from CLAUDE.md
"Currently in flight"; remaining post-A.5 polish scope is #53 (Tier 1
retry) + #54 (JobKind plumbing).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:51:46 +02:00
Erik
e40159f4d6 fix(render): close #52 — lifestone visible (alpha-test + cull + uDrawIDOffset)
Three root causes regressed the Holtburg lifestone since the WB rendering
migration (Phase N.5 retirement amendment, commit dcae2b6, 2026-05-08).
All confirmed via temporary [LIFESTONE-DIAG] instrumentation and visually
verified by the user through the +Acdream test character.

1. **Alpha-test discard** in mesh_modern.frag transparent pass killed
   high-α pixels of dat-flagged transparent surfaces. Native AC
   transparent surfaces routinely include effectively-opaque pixels —
   e.g. the lifestone crystal core (surface 0x080011DE) — that compose
   correctly under (SrcAlpha, 1-SrcAlpha) blending. The original N.5
   §2 rationale ("high-α belongs in opaque pass") doesn't hold for
   surfaces flagged transparent at the dat level: those pixels can't
   reach the opaque pass at all. Fix: remove `α >= 0.95 discard` from
   the transparent pass, keep `α < 0.05 discard` as a fragment-cost
   optimization (skip totally-empty pixels).

2. **Cull state** for the transparent pass was unset by
   WbDrawDispatcher after the N.5 retirement amendment deleted
   StaticMeshRenderer.cs (which had the Phase 9.2 setup at commit
   6f1971a, 2026-04-11). Closed-shell translucents — lifestone crystal,
   glow gems — need GL_CULL_FACE + GL_BACK + GL_CCW in the transparent
   pass; otherwise back faces composite over front faces in iteration
   order under DepthMask(false). Fix: re-establish Phase 9.2's exact
   GL state setup at the top of Phase 8.

3. **uDrawIDOffset uniform** was missing from mesh_modern.vert.
   gl_DrawIDARB resets to 0 at the start of each
   glMultiDrawElementsIndirect call, so the transparent pass — which
   begins later in the indirect buffer — was fetching
   Batches[0..transparentCount) instead of its actual section at
   Batches[opaqueCount..end). The lifestone crystal ended up reading
   the FIRST OPAQUE batch's TextureHandle every frame; as the camera
   moved and the front-to-back opaque sort reordered which group
   landed at BatchData[0], the crystal's apparent texture flickered to
   whatever sat first — typically the player character's body parts.
   Fix: add `uniform int uDrawIDOffset` to the vertex shader, change
   Batches[gl_DrawIDARB] → Batches[uDrawIDOffset + gl_DrawIDARB], and
   set the uniform per-pass in WbDrawDispatcher (0 for opaque,
   _opaqueDrawCount for transparent). Mirrors WorldBuilder's
   BaseObjectRenderManager.cs line 845.

Tests: 1688/1696 passing (8 pre-existing physics/input failures
unchanged). N.5b conformance sentinel 94/94 clean.

Visual: Holtburg lifestone now renders with the spinning blue crystal
correctly composed over the pedestal. Other transparent content (glass,
particle effects, NPC clothing) is unaffected — the same uniform fix
applies globally and is correct for all transparent draws.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:49:05 +02:00
Erik
c111312e13 docs(post-A.5): cold-start handoff for the next session
Captures the three deferred items from A.5 ship:
- ISSUE #52: lifestone visual missing (1-3 hours, fast win)
- ISSUE #54: JobKind plumbing through BuildLandblockForStreaming
  (~30 min - 1 hour, worker-thread efficiency cleanup)
- ISSUE #53: Tier 1 entity-classification cache retry (~5-7 days,
  biggest perf win remaining; needs animation-mutation audit before
  designing to avoid the freeze-pose bug from the first attempt)

Doc covers: A.5 final state + 3 high-value gotchas, files to read,
per-priority detail with effort estimates and acceptance criteria,
what NOT to do, the first-30-minute workflow, and the full A.5
commit chain for reference.

Phase is sized ~1 week if all three priorities land. The audit
step on Tier 1 is the highest-leverage investment.

Tier 2 + Tier 3 (static/dynamic split + GPU compute culling) are
explicitly out-of-scope for this phase — separate multi-week phases
per docs/plans/2026-05-10-perf-tiers-2-3-roadmap.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 10:16:10 +02:00
Erik
d3d78fa14f Merge branch 'claude/hopeful-darwin-ae8b87' — Phase A.5 SHIP + Quality Preset system
Phase A.5 — Two-tier Streaming + Horizon LOD shipped.

Headline: 2.3 km terrain horizon (radius=4 near + 12 far) with off-thread
mesh build, fog blend at N₁, mipmaps + 16x AF, MSAA 4x + A2C foliage,
depth-write audit, BUDGET_OVER diag, Quality Preset system (Low/Medium/
High/Ultra) with env-var overrides + F11 mid-session re-apply.

~999 tests pass, 8 pre-existing physics/input failures unchanged.

Two structural-to-A.5 bug fixes shipped post-T26:
- Bug A (9217fd9): far-tier worker strips entities (T13/T16 had only
  wired the controller side; far-tier was loading full entity layers,
  ~71K entities instead of ~10K, 5x perf regression).
- Bug B (0ad8c99): WalkEntities scratch list reused across frames
  (was 480 KB / frame allocation).

Tier 1 entity-classification cache attempted as polish (3639a6f),
reverted (9b49009) — broke animation by caching mutable per-frame
state. Retry deferred to post-A.5 polish phase (ISSUE #53).

Deferred to post-A.5 polish:
- Tier 1 retry with animation-mutation audit (ISSUE #53)
- Lifestone missing visual (ISSUE #52)
- JobKind plumbing through BuildLandblockForStreaming (ISSUE #54)
- Tier 2 (static/dynamic split) + Tier 3 (GPU compute cull) —
  separate multi-week phases. Roadmap at
  docs/plans/2026-05-10-perf-tiers-2-3-roadmap.md.

SHIP commit: 9245db5.
2026-05-10 10:09:03 +02:00
Erik
9245db5b04 phase(A.5): SHIP — two-tier streaming + horizon LOD + Quality Preset system
Final state: A.5 delivers a 2.3 km terrain horizon (radius=4 near + 12
far) with off-thread mesh build, fog blend at the N₁ boundary, mipmaps
+ 16x anisotropic on terrain, MSAA 4x + A2C foliage, depth-write audit
+ lock-in test, BUDGET_OVER diag flag, and a full Quality Preset system
(Low/Medium/High/Ultra) with env-var overrides + F11 mid-session
re-apply.

Acceptance:
- N.5b conformance sentinel: 89+ tests passing (TerrainSlot,
  TerrainModernConformance, Wb*, MatrixComposition, TextureCacheBindless,
  SplitFormulaDivergence). All clean.
- Build green; ~999 tests passing across all projects; 8 pre-existing
  physics/input failures unchanged (out of A.5 scope).
- Standstill at horizon-safe preset (radius=4/12, MSAA off, A2C off,
  aniso 4x), Holtburg, AMD Radeon RX 9070 XT @ 1440p:
  entity dispatcher cpu_us median ~3.5ms, p95 ~4ms (~200-240 FPS).
  Terrain dispatcher cpu_us median ~21µs (well under 1ms budget).
- Visual gate (partial): horizon visible at ~2.3km; fog blend smooths
  N₁ boundary cleanly; system stable through walking traverse.
  Lifestone missing — known issue from earlier in development chain,
  deferred to post-A.5 (ISSUE #52).

Two post-T26 perf bug fixes that were structural to A.5's promise:
- (Bug A, 9217fd9) Far-tier worker now strips entities. Without this,
  T13/T16 shipped only the controller side of two-tier; the worker
  loaded full entity layers for far-tier LBs. Result was ~71K entities
  in GpuWorldState instead of ~10K — a 5x perf regression. Patch is
  at the worker-output level; cleaner JobKind plumbing through
  BuildLandblockForStreaming is post-A.5 (ISSUE #54).
- (Bug B, 0ad8c99) WalkEntities switched from per-frame fresh-list
  allocation to a caller-provided scratch list reused across frames.
  Eliminated ~480 KB / frame GC pressure on the render thread.

Tier 1 entity-classification cache attempted as ship-prep polish
(commit 3639a6f) but reverted (9b49009) — caching meshRef.PartTransform
froze the per-frame animation pose. Retry is a post-A.5 phase with
animation-mutation audit + animated-bypass design (ISSUE #53).

Decisions (per spec §4):
- N₁=4 (full detail, 81 LBs), N₂=12 (terrain only, 544 LBs).
- Bucketing Change #1 (animated-walk fix in WalkEntities) +
  Change #2 (cached AABB on WorldEntity) shipped. Change #3
  (sub-LB cell cull) NOT shipped — budget hit without it.
- Single-worker off-thread mesh build (Q6 Option A).
- Hysteresis radius+2 on both tiers (Q7 Option A).
- Mipmaps + 16x anisotropic + A2C with MSAA 4x + depth-write audit
  all shipped (Q8 Option C).
- Acceptance gate: refresh-rate-relative + per-preset (Q9 Option B
  reshape after Quality Preset addition).
- Quality Preset system (T22.5, mid-execution scope add): Low /
  Medium / High / Ultra with per-preset radii + MSAA + anisotropic +
  A2C + completions; 6 env-var overrides; settings.json persistence;
  F11 mid-session re-apply.

Deferred to post-A.5 polish phase:
- Tier 1 retry with animation audit (ISSUE #53)
- Lifestone missing (ISSUE #52)
- JobKind plumbing through BuildLandblockForStreaming (ISSUE #54)
- Tier 2 (static/dynamic group split) — multi-week phase
- Tier 3 (GPU compute culling) — multi-week phase
- Re-test full High preset (crashed at original attempt; should work
  post-Bug-A; not retested)

Spec: docs/superpowers/specs/2026-05-09-phase-a5-two-tier-streaming-design.md
Plan: docs/superpowers/plans/2026-05-09-phase-a5-two-tier-streaming.md
Perf-tier roadmap: docs/plans/2026-05-10-perf-tiers-2-3-roadmap.md
Memory: ~/.claude/projects/.../memory/project_phase_a5_state.md (5 gotchas)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 10:08:13 +02:00
Erik
8f43a58037 Merge branch 'claude/bold-proskuriakova-d4fb2c' — ISSUES.md #13 PlayerDescription trailer parser + F.5a roadmap entry
Closes ISSUES.md #13. PlayerDescriptionParser now walks the full
trailer (Options1 / Shortcuts / HotbarSpells / DesiredComps /
SpellbookFilters / Options2 / GameplayOptions blob / Inventory /
Equipped) ported from holtburger events.rs:503-625 +
shortcuts.rs:13-34. The trickiest piece — gameplay_options — uses a
4-byte-aligned forward heuristic (TryHeuristicInventoryStart)
mirroring holtburger's find_inventory_start_after_gameplay_options.

Trailer walk wrapped in its own inner try/catch so a malformed
trailer cannot destroy upstream attribute/skill/spell/enchantment
data; new Parsed.TrailerTruncated flag distinguishes clean parse
from graceful-degradation parse, with diagnostic log under
ACDREAM_DUMP_VITALS=1.

GameEventWiring registers parsed Inventory + Equipped into
ItemRepository at login (acceptance criterion: ItemRepository.Count
> 0 after login, exercised by GameEventWiringTests). 20 PD parser
tests + 1 wiring acceptance test; 282/282 AcDream.Core.Net.Tests
pass.

Plan: docs/superpowers/plans/2026-05-10-issue-13-pd-trailer.md.

Roadmap update: F.5a (visible-at-login dev panels) added as the
first deliverable that actually consumes the new trailer data —
ImGui dev panels under ACDREAM_DEVTOOLS=1 binding to
AcDream.UI.Abstractions ViewModels.

13 task commits + 1 review-followup + 1 nit-fix + 1 roadmap = 16
commits on the branch.
2026-05-10 10:07:06 +02:00
Erik
d93d823539 docs(A.5 T27): roadmap + ISSUES + CLAUDE.md updates for A.5 ship
Roadmap (2026-04-11-roadmap.md):
- Status header updated to 2026-05-10 / A.5 as the shipped phase.
- A.5 row added to shipped table (after A.3): two-tier streaming,
  QualityPreset, Bug A/B fixes, deferred items, plan archive link.
- A.5 sub-piece in Phase A section marked SHIPPED with archive link
  (replaces the old "not yet brainstormed" entry).
- N.6 bullet changed from "Currently in flight" to "Planned
  (post-A.5 polish takes priority)"; A.5's landing means the
  "direct higher-radius comparison once A.5 lands" item is now
  available.

ISSUES.md:
- #52 (A.5/lifestone-missing): Holtburg lifestone not rendering since
  A.5 dev; two root-cause candidates; investigation approach.
- #53 (A.5/tier1-redo): classification cache reverted at 9b49009;
  animation-mutation audit required before retry; 1-week estimate.
- #54 (A.5/jobkind-plumbing): Bug A's post-load strip wastes worker
  CPU; proper fix plumbs JobKind through BuildLandblockForStreaming;
  30 min–1 hour estimate.

CLAUDE.md:
- "Currently in flight" paragraph updated from N.6 to Post-A.5 polish
  (issues #52/#53/#54) with note that N.6 follows.
- A.5 shipped paragraph added (mirrors N.5b/N.5/N.4 format).
- WB integration cribs: new bullet documenting the two-tier streaming
  architecture (StreamingRegion / StreamingController / LandblockStreamer
  / GpuWorldState, N₁/N₂ defaults, QualitySettings, spec pointer).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 10:06:40 +02:00
Erik
68d6898339 roadmap: add F.5a — visible-at-login dev panels (consumes #13)
Sub-phase under existing F.5 (Core panels) capturing the immediate
follow-up to ISSUES.md #13: now that PlayerDescriptionParser surfaces
the full trailer (Inventory / Equipped / Shortcuts / HotbarSpells /
DesiredComps / Options1+2 / SpellbookFilters) and GameEventWiring
populates ItemRepository at login, F.5a wires that data into minimal
ImGui dev panels under ACDREAM_DEVTOOLS=1 so it's observable in-game.

Establishes the binding pattern (AcDream.UI.Abstractions ViewModels →
ImGui renderer) that the eventual D.2b retail-skinned F.5 panels
reuse. Spec to brainstorm before code.
2026-05-10 10:06:30 +02:00
Erik
a28a5b7583 docs(A.5 T27): spec + plan amendments for T22.5 + ship
Spec (2026-05-09-phase-a5-two-tier-streaming-design.md):
- §2 acceptance metrics reshaped from absolute 240 FPS to
  refresh-rate-relative + per-preset (95th-pct ≤ 1000ms/refresh
  standstill; ≤ 1.5× walking) to match the Quality Preset reality.
- New §4.10 Quality Preset System (T22.5): enum Low/Medium/High/Ultra,
  QualitySettings schema, canonical preset values table, env-var
  override table, wiring notes (GameWindow.OnLoad + ReapplyQualityPreset),
  MSAA mid-session unsupported caveat, file list, test count (12).
- New §11 What was deferred: 8 items (Tier 1 cache, lifestone, JobKind
  plumbing, Tier 2/3, ToEntries alloc, InvalidateEntity wiring, High
  preset retest). Former §11 References renumbered to §12.

Plan (2026-05-09-phase-a5-two-tier-streaming.md):
- New Task 22.5 section inserted between T22 and T23: full inline spec
  with schema, preset table, env-var list, wiring steps, acceptance
  criteria, deferred items, commit SHAs. Includes file-name corrections
  (SettingsState → DisplaySettings, DisplayTab → SettingsPanel).
- Self-review cross-check table: new §4.10 row pointing at T22.5.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 10:06:26 +02:00
Erik
9b49009dd5 Revert "feat(perf): Tier 1 entity classification cache"
This reverts commit 3639a6f4ac.
2026-05-10 09:53:26 +02:00
Erik
95aaa6c92e docs: close ISSUES.md #13 — PD trailer parser shipped
Issue #13 closed in `078919c`. Full trailer (Options1 / Shortcuts /
HotbarSpells / DesiredComps / SpellbookFilters / Options2 /
GameplayOptions blob / Inventory / Equipped) now walked by
PlayerDescriptionParser. ItemRepository wired up to receive parsed
Inventory + Equipped at login. 20 PD parser tests + 1 wiring
acceptance test. 282/282 Net.Tests pass.

Plan archived at
docs/superpowers/plans/2026-05-10-issue-13-pd-trailer.md.
2026-05-10 09:47:51 +02:00
Erik
3639a6f4ac feat(perf): Tier 1 entity classification cache
Per docs/plans/2026-05-10-perf-tiers-2-3-roadmap.md Tier 1: cache the
per-(entity, meshRef, batch) classification (TextureCache lookup,
GroupKey hash, _groups dict insert) so the per-frame Draw inner loop
becomes "look up cache → walk assignments → append matrix to group's
Matrices list."

For static entities (~95% of world: trees, rocks, buildings, scenery),
the answer never changes between frames. Cache once at first visit;
reuse permanently. Per-frame work for static drops from 4 expensive
operations per (meshRef, batch) to 1 list-append.

Estimated entity dispatcher: 3.5ms → ~1-1.5ms median at radius=12.
Should land inside the 2.0ms spec budget.

Implementation:
- New EntityClassificationCache class (per-meshRef list of cached
  (group ref, baked-PartTransform) tuples) keyed by entity.Id.
- ClassifyEntity does the one-time work; result populates _groups and
  the cache.
- Draw inner loop: cache lookup → for each assignment, model =
  PartTransform × entityWorld; group.Matrices.Add(model).
- Cache miss when ClassifyEntity finds NO mesh loaded yet (Vao == 0)
  → don't store; retry next frame. Avoids cache thrash during the
  streaming-in window.
- Public InvalidateEntity(uint id) + ClearEntityCache() for explicit
  invalidation hooks. Wiring (palette swap on ObjDescEvent, MeshRefs
  hot-swap) is post-A.5 follow-up — for now, cache-stale entities
  show their pre-swap appearance until next respawn.

Tier 2 (static/dynamic split with persistent groups) and Tier 3 (GPU
compute culling) tracked in the roadmap doc.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 09:45:18 +02:00
Erik
078919cc18 feat(net): #13 register PD trailer inventory+equipped in ItemRepository
After PlayerDescription is dispatched, the Inventory and Equipped lists
produced by the parser are now fed into ItemRepository via AddOrUpdate +
MoveItem so inventory/paperdoll panels see items after login.

Acceptance test PlayerDescription_RegistersInventoryEntries_InItemRepository
confirms ItemCount goes 0→2 for a synthetic PD with two inventory entries.
282 Net.Tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 09:43:46 +02:00
Erik
58095d8d4b test(net): #13 end-to-end PD trailer fixture covering every section
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 09:39:36 +02:00
Erik
462f9d6377 docs(perf): roadmap for Tier 2 + Tier 3 entity-dispatcher optimizations
Captured 2026-05-10 during Phase A.5 polish discussion. User asked why
the 9070 XT @ 1440p doesn't hit Unreal-level FPS for an old game like
AC. Answer: architectural — we rebuild the entire draw plan from
scratch every frame instead of caching pre-baked static-world data.

Tier 1 (entity-classification cache) lands as A.5 polish (separate
commit). Tiers 2 + 3 documented here for future scheduling:

- Tier 2 — Static/dynamic split with persistent groups
  ~2-week phase. Static entities (~95% of world) get permanent GPU-
  resident matrix slots, populated at spawn, dirty-tracked for delta
  upload. Per-frame CPU cost for static = LB-cull + dirty-flag check
  only. Estimated entity dispatcher: 3.5ms → 0.5-1ms median.
  400-600 FPS at standstill, radius=12.

- Tier 3 — GPU-side culling (compute pre-pass)
  ~1-month phase. Per-instance frustum cull moves to GPU compute
  shader. Compute writes draw-indirect buffer; rasterizer reads it.
  Estimated CPU dispatcher: ~0.05ms (essentially free).
  600-1000+ FPS at standstill, radius=12.

Doc captures effort estimates, sub-decisions, risks, mitigations, and
scheduling triggers for each tier. Also notes the architectural
ceiling (~800-1500 FPS for a C# + GL client; reaching native engine
performance requires becoming a different engine).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 09:38:38 +02:00
Erik
91693ea44c feat(net): #13 heuristic inventory locator after gameplay_options blob
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 09:37:46 +02:00
Erik
0ad8c99c37 fix(A.5): WalkEntities scratch-list pattern (Bug B — T17 GC pressure)
T17's WalkEntities helper allocated a fresh List<(WorldEntity, int)>
per frame to hold the (entity, meshRefIndex) pairs that pass visibility
filters. At ~10K entities × ~3 mesh refs = ~30K tuples × 16 bytes =
~480 KB / frame of GC pressure on the render thread. The implementer's
self-review flagged this as a future N.6 optimization; the post-T26
diagnostic showed it materially contributing to the perf regression
(though Bug A — far-tier entity load — was the dominant factor).

Refactor: split WalkEntities into two overloads.
- WalkEntities(...) — test-friendly, allocates a fresh ToDraw list per
  call. Tests keep using this signature unchanged.
- WalkEntitiesInto(..., scratch, ref result) — no-alloc, clears + populates
  a caller-provided scratch list. Draw uses this with a per-dispatcher
  _walkScratch field reused across frames.

Test count unchanged (40 streaming + 8 bucketing tests still pass).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 09:13:20 +02:00
Erik
9217fd93cd fix(A.5): strip far-tier entities in worker (Bug A — far tier optimization)
Phase A.5's two-tier streaming spec promised that far-tier landblocks
ship terrain ONLY — no entities, no scenery, no interior cells. T13/T16
wired the controller side (RecenterTo emits ToLoadFar/ToLoadNear/ToPromote;
controller passes JobKind to the worker), but the worker's HandleJob
never branched on Kind: every load called BuildLandblockForStreaming
which runs the full hydration + scenery generation + interior cell path.

Result: at default radii (N₁=4 / N₂=12), 540 far-tier LBs each loaded
their full entity layer (~132 entities/LB → ~71K entities total) into
GpuWorldState. The dispatcher then walked all ~54K entities per frame
(post-frustum-cull), driving the entity dispatcher cpu_us from ~3.6ms
median (T24 baseline) to ~18-21ms (post-T22.5 horizon-test). User-
observed: 40 FPS / 25ms frame time at horizon-safe settings; system
crash at full High preset.

Minimum-diff fix: in LandblockStreamer.HandleJob, after
_loadLandblock returns, strip Entities to empty for LoadFar before
posting Loaded. Worker still does wasted hydration CPU (off the render
thread, harmless). Render-side dispatcher walk drops from ~54K to ~10K
entities/frame.

Math: post-fix entity dispatcher should drop to ~3-4ms median at N₁=4 /
N₂=12 (matches T24's 3.6ms at radius=5 single-tier, since N₁=4 has 33%
fewer near entities than N₁=5).

Future optimization (N.6 / A.6): plumb JobKind through
BuildLandblockForStreaming so the worker also skips the wasted CPU.
Out of A.5 scope.

Bug B (T17 WalkEntities allocation) is a smaller perf hit — defer if
post-Bug-A FPS is acceptable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 09:10:42 +02:00
Erik
d9a5e40203 feat(net): #13 strict inventory+equipped reader (no GAMEPLAY_OPTIONS)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 08:49:10 +02:00
Erik
98eebef740 feat(net): #13 read options2 gated on CHARACTER_OPTIONS2 flag
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 08:46:32 +02:00
Erik
b17dc3b152 feat(net): #13 read optional spellbook_filters u32 2026-05-10 08:44:05 +02:00
Erik
28d2c6018e feat(A.5 T22.5): wire QualityPreset into renderer + streaming (commit 2/2)
GameWindow.OnLoad resolves QualitySettings.From(_persistedDisplay.Quality)
+ WithEnvOverrides() immediately after LoadAndApplyPersistedSettings, stores
result in _resolvedQuality field. All six quality dimensions applied:

- NearRadius / FarRadius: replace old T16 env-var-only block; preset drives
  the radii, legacy ACDREAM_STREAM_RADIUS override still honoured.
- MsaaSamples: WindowOptions.Samples reads from startup quality resolution
  in Run() (pre-window-create read from SettingsStore). MSAA cannot change
  at runtime; ReapplyQualityPreset logs a restart-required warning if the
  new preset would change it.
- AnisotropicLevel: TerrainAtlas.SetAnisotropic() called after Build() and
  again in ReapplyQualityPreset. Temporarily removes bindless residency
  before the GL TexParameter call, re-makes resident after.
- AlphaToCoverage: WbDrawDispatcher.AlphaToCoverage property gates the
  glEnable/glDisable(SampleAlphaToCoverage) pair around the opaque pass.
- MaxCompletionsPerFrame: set on StreamingController after construction
  and after each mid-session restart.

ReapplyQualityPreset(QualityPreset) method handles mid-session changes
(Settings panel Quality dropdown Save): rebuilds streamer + controller for
radius changes, toggles A2C and aniso immediately, logs MSAA restart caveat.
onSaveDisplay callback updated to call ReapplyQualityPreset when Quality
field changes.

TerrainModernRenderer.Atlas property added to expose the atlas for
mid-session aniso updates.

991 tests passing, 8 pre-existing failures unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 08:43:06 +02:00
Erik
75e8e260f2 feat(net): #13 read desired_comps list in PD trailer
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 08:39:31 +02:00
Erik
afa4200107 feat(A.5 T22.5): QualityPreset schema + tests (commit 1/2)
Add QualityPreset enum + QualitySettings readonly record struct with
From(preset) table and WithEnvOverrides() env-var override layer.
Four presets (Low/Medium/High/Ultra) drive NearRadius, FarRadius,
MsaaSamples, AnisotropicLevel, AlphaToCoverage, MaxCompletionsPerFrame.
Env vars (ACDREAM_NEAR_RADIUS, ACDREAM_FAR_RADIUS, ACDREAM_MSAA_SAMPLES,
ACDREAM_ANISOTROPIC, ACDREAM_A2C, ACDREAM_MAX_COMPLETIONS_PER_FRAME)
override individual preset fields for dev spot-testing.

DisplaySettings gains a Quality: QualityPreset field (default High);
SettingsStore persists/loads it under display."quality" as an enum
name string with Enum.TryParse fallback. 12 new QualityPresetTests
cover the preset table (radii, msaa, aniso, a2c, completions) and all
six env-var override paths. 415 UI.Abstractions tests passing.

Wiring into GameWindow / WbDrawDispatcher / TerrainAtlas follows in
commit 2 of this task.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 08:37:17 +02:00
Erik
8cbb991d95 feat(net): #13 read hotbar spells (SPELL_LISTS8 + legacy path)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 08:35:03 +02:00
Erik
c473feedb3 feat(A.5 T23): BUDGET_OVER flag in [WB-DIAG] / [TERRAIN-DIAG]
Per Phase A.5 spec §2 acceptance criterion 6: entity dispatcher median
≤ 2.0ms; terrain dispatcher median ≤ 1.0ms at standstill. When the
median exceeds the budget, prefix the DIAG line with " BUDGET_OVER" so
the regression is grep-friendly during perf testing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 08:28:45 +02:00