Commit graph

833 commits

Author SHA1 Message Date
Erik
ecb0f2d65f docs(planning): add M0-M7 milestones layer + CLAUDE.md milestone discipline
Introduce a morale + scope-management layer above the roadmap. Seven
milestones (M0-M7) from "Connect & explore" to "v1.0", each defined by a
concrete playable scenario and ~6-10 weeks of work. Each milestone hit
triggers a recorded demo video + a freeze list of phases that go
off-limits until M7's polish pass.

M0 ("Connect & explore") is declared done retroactively — ~25 shipped
phases through 2026-05-12 (terrain, network, audio, particles, chat,
input, streaming, WB rendering migration, sky/lighting, particle system,
combat/spell/item data layers) are frozen. Currently working toward M1
("Walkable + clickable world"): L.2 collision + B.4 interaction, ~4-6
weeks.

CLAUDE.md gains a "Milestone discipline" section above the existing
"Roadmap discipline" — sets the two-altitude orientation (milestones
above, phases below), names M1 as the current target, and codifies the
four motivation rules: (1) one active milestone at a time, (2) frozen
phases off-limits, (3) recorded demo video per landing, (4) state both
altitudes at session start.

Addresses the "everything is half-built" feeling caused by per-phase
vertical-slice ships never adding up to a milestone-level "done" event.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 20:36:50 +02:00
Erik
d1d02c34c2 Merge branch 'claude/sharp-chatelet-023dda' — Phase L.2d slice 1 + 1.5 (BSP-hit diagnostic probe; doorway blocker identified as Door entity, not building BSP) 2026-05-12 19:47:24 +02:00
Erik
34b7f1faa1 docs(phys L.2d): slice 1 + 1.5 shipped handoff + 3rd plan-of-record reframe
L.2d as scoped is essentially closed at the Holtburg site. The slice-1.5
trace settled the question: the "I can't walk through doorways" symptom
is a closed Door entity (Setup 0x020019FF named "Door") at the building
threshold, not a building-BSP-collision issue. Building BSP is healthy.

The two prior framings turned out wrong:
- L.2a handoff (2026-05-12): "per-cell walkability missing" — based on
  hit attribution pointing at the building, missed the Door cylinder
  also colliding per tick.
- L.2d slice 1 spec (2026-05-13 morning): "BSP shape fidelity, three
  hypotheses X/Y/Z" — ruled out by the trace once the probe labeling
  bug was fixed in slice 1.5.

Handoff doc captures full evidence, side findings (building double-
registration latent bug, missing PhysicsState in entity-source log),
and a candidates list for the next-session ordering discussion.

Plan-of-record L.2d sub-direction paragraph updated to match: "watch-
and-wait" mode, no more slices until a new shape-fidelity bug is
observed at a different site. Door-state handling becomes its own
sub-phase, scope deferred to project-ordering discussion.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 19:46:45 +02:00
Erik
8bacef0598 fix(phys L.2d slice 1.5): probe captures hit poly under StepSphereUp recursion
First Holtburg-doorway capture showed all 191 [resolve-bldg] entries
labeled "n/a (cylinder)" — including hits attributed to the building
0xA9B47900 which [entity-source] confirmed was registered as type=BSP.
The label was a probe bug, not a real cylinder route.

Root cause: BSPQuery's grounded-path (Path 5) returns early via
`StepSphereUp(transition, worldNormal, engine)` when no step is already
in progress. The slice-1 side-channel write at line 1546 came AFTER
that early return, so it never fired for the dominant grounded-player
case. Compounding: StepSphereUp recurses into ResolveWithTransition →
FindObjCollisions, whose per-entity `LastBspHitPoly = null` clear
wiped any earlier write before the outer attribution emitter read it.

Fix:
1. BSPQuery Path 5: move LastBspHitPoly write to the top of
   `if (hit0 || hitPoly0 != null)` blocks (both foot- and head-sphere),
   BEFORE the StepSphereUp early return. Recursion-safe — the inner
   resolve's BSP writes will overwrite with the inner entity's poly,
   but for the dominant case (same wall hit on both outer and inner)
   that's still the correct attribution.
2. TransitionTypes.FindObjCollisions: drop the per-entity clear of
   LastBspHitPoly. With BSPQuery now writing at hit-detection time
   instead of response-computation time, the side-channel value is
   reliable without per-iteration zeroing.
3. TransitionTypes [resolve-bldg] emission: key the "n/a (cylinder)"
   label on `obj.CollisionType` directly, not on LastBspHitPoly being
   null. A BSP entity with a null poly now logs "n/a (BSP path —
   side-channel not written, missing BSPQuery wire site)" so any
   future BSPQuery path that's missing the wire is visible in the
   trace rather than being silently mis-labeled.

Verified: build green, the 2 slice-1 tests still pass, 8 pre-existing
failures unchanged.

Spec: docs/superpowers/specs/2026-05-13-l2d-cbuildingobj-collision-design.md
First capture (showing the label bug): launch-l2d-slice1.log lines
12086-12120 (representative [resolve-bldg] entries for obj=0xA9B47900).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 19:25:32 +02:00
Erik
66dc23e087 feat(phys L.2d slice 1): BSP-hit diagnostic probe + plan-of-record correction
Adds ACDREAM_PROBE_BUILDING — a read-only per-shadow-entry probe that
captures full BSP collision evidence whenever TransitionTypes.FindObjCollisions
attributes a hit (via the existing L.2a slice 3 chain). One multi-line
[resolve-bldg] entry per attributed hit: partIdx, hasPhys, bspR vs
vAabbR, world-space entOrigin_lb, and the actual hit polygon's vertices
in both object-local and world space.

Paired with a one-time [entity-source] line at every ShadowObjects.Register
call site in GameWindow so entityId from a probe line is greppable to its
WorldEntity source within a single log file.

Plumbing: BSPQuery writes the resolved hit polygon to a new
PhysicsDiagnostics.LastBspHitPoly side-channel at the 5 SetCollisionNormal
sites in Paths 5/6 + CollideWithPt. TransitionTypes clears that field
before each shadow-entry dispatch and reads it back at the L.2a slice 3
attribution site to emit the probe line.

Spec component 4 originally described an out ResolvedPolygon? parameter
on BSPQuery.FindCollisions; the static side-channel achieves the same
observable behavior without plumbing through BSPQuery's recursive private
methods. Deviation noted in PhysicsDiagnostics.LastBspHitPoly's XML doc.

Reframes the plan-of-record's L.2d sub-direction paragraph: the 2026-05-12
handoff proposed porting CBuildingObj + per-cell walkability, but ACE
BuildingObj.cs:39-52 + named-retail acclient_2013_pseudo_c.txt:701260
show find_building_collisions is one BSP test on Parts[0]. Per-cell
walkability belongs to L.2e, not L.2d. L.2d slice 1 is the diagnostic;
slice 2 is the actual fix scoped from slice 1's evidence (one of three
hypotheses: wrong BSP loaded / over-registered parts / BSPQuery flaw).

Tests: 2 synthetic unit tests in PhysicsDiagnosticsTests.cs pin the
static API contract that the BSPQuery → side-channel → TransitionTypes
emission chain depends on. The multi-line line format itself is verified
by acceptance criterion 2 (live Holtburg-doorway capture) — covering it
here would require a heavy PhysicsEngine + Transition fixture for a
diagnostic-only emission.

Verified: dotnet build green; the 2 new tests pass; the 8 pre-existing
test failures listed in the L.2a handoff (MotionInterpreter GetMaxSpeed_*,
PositionManager.ComputeOffset_BothActive_Combined,
PlayerMovementController.Update_ForwardInput_*, Dispatcher.W_held_*,
BSPStepUpTests.{D4,C3}) remain failing — none introduced by this slice.

Spec: docs/superpowers/specs/2026-05-13-l2d-cbuildingobj-collision-design.md
Conformance anchors:
- acclient_2013_pseudo_c.txt:701260 (CBuildingObj::find_building_collisions)
- acclient_2013_pseudo_c.txt:323725 (BSPTREE::find_collisions)
- ACE references/ACE/Source/ACE.Server/Physics/Common/BuildingObj.cs:39-52

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 19:14:34 +02:00
Erik
92cd7238ff docs(phys L.2d): design spec for slice 1 BSP-hit diagnostic + L.2d reframe
Reframes L.2d direction based on ACE BuildingObj.cs:39-52 + named-retail
acclient_2013_pseudo_c.txt:701260: retail's find_building_collisions is
one BSP test on PartArray.Parts[0]. No per-cell walkability. Per-cell
work (find_cell_list, point_in_cell, sphere/box_intersects_cell) is
L.2e territory.

Slice 1 is now a read-only BSP-hit diagnostic that captures full
collision evidence per L.2a [resolve] hit=yes line. Distinguishes 3
hypotheses (wrong BSP loaded / over-registered parts / BSPQuery flaw)
before any behavior change. Slice 2 is the actual fix, scoped from
slice 1's evidence.

Authors: brainstorm session 2026-05-13 (cold-start from L.2a slice
1+2+3 evidence). Predecessor handoff at
docs/research/2026-05-12-l2a-shipped-l2d-handoff.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 19:01:44 +02:00
Erik
0c7208a3fe docs(workflow): report-only-mode rules + retail-faithful-port guardrail in CLAUDE.md
Three new bullets, all evidence-driven from /insights:

"Only stop and wait for the user when":
- Request is an investigation/audit/analysis/review/report-only — no edits
  or diagnostic drops until the user approves a fix. Use the /investigate
  skill (local at .claude/skills/investigate/SKILL.md, gitignored).
- A referenced commit/path/branch/doc doesn't exist where the user said
  it would — ask one short question; don't go hunting across branches.
  Prevents the N.5-handoff-style 30-min wrong-branch exploration.

"What NOT to do":
- Don't replace working retail-faithful logic with a modern redesign
  without explicit approval. Cites the 267 min remote-entity rewrite +
  sky-fog shader speculation both reverted in full. Retail-faithful
  first; "cleaner" second.

The /investigate skill itself stays local (option 3 — not committed).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 18:22:40 +02:00
Erik
acad14e534 Merge branch 'claude/intelligent-poitras-b2c4f9' — Phase L.2a slices 1+2+3 (resolver + cell-transit probes + entity attribution)
Ships Phase L.2a (Truth & Diagnostics) for Movement & Collision Conformance,
plus the L.2d sub-direction call backed by reproducible evidence.

Three code commits:
- ebef820 — L.2a slice 1: [resolve] + [cell-transit] probes with DebugPanel mirror.
- e0c08bc — L.2a slice 2: extend [resolve] line with hit object guid + env flag.
- a068292 — L.2a slice 3: populate previously-stub CollisionInfo entity
  attribution at the per-object call site in Transition.FindObjCollisions.

Plus a docs commit (eb401e8) shipping the cold-start handoff, next-session
prompt, L.2 plan-of-record shipped-slice notes, and CLAUDE.md status updates.

Three findings backed by reproducible Holtburg doorway evidence:
1. L.2e cell-id format gap: player CellId tracked as bare low byte
   (0x00000029), no landblock prefix.
2. L.2c wall-slide working: clean clamping with correct normal, no ok=False.
3. L.2d sub-direction confirmed: 126/140 wall hits attributed to
   obj=0xA9B47900 (landblock-baked building static); no door-range
   entity ids appear. Fix is CBuildingObj + per-cell walkability port,
   NOT door-state-toggle.

Build green; 1032/1040 unit tests pass. The 8 failing tests are
pre-existing on the branch base (verified by stash + rerun); none from
L.2a slice work.

Next session: L.2d slice 1 brainstorm + design spec
(docs/research/2026-05-12-l2d-next-session-prompt.md).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 18:16:50 +02:00
Erik
eb401e8cac docs(phys L.2a): handoff + next-session prompt + CLAUDE.md / plan-of-record updates
Documents the L.2a-slice-1/2/3 ship for the next session and updates
the project's source-of-truth docs to reflect current state.

New files:
- docs/research/2026-05-12-l2a-shipped-l2d-handoff.md — full cold-start
  handoff: what shipped, three findings (L.2e cell-id format gap, L.2c
  wall-slide working, L.2d sub-direction = CBuildingObj port), branch
  state, diagnostic surface inventory, files changed, open concerns,
  cold-start checklist, reproduction recipe.
- docs/research/2026-05-12-l2d-next-session-prompt.md — terse copy-paste
  prompt for the next Claude Code session.

Modified:
- docs/plans/2026-04-29-movement-collision-conformance.md — added "Current
  sub-direction" note under L.2d capturing the evidence-driven call:
  building-mesh fidelity issue, not door-state-toggle.
- CLAUDE.md — updated "Currently between phases" → "Currently in Phase
  L.2"; added L.2a shipped paragraph; added ACDREAM_PROBE_RESOLVE /
  ACDREAM_PROBE_CELL to Diagnostic env vars; prepended L.2d brainstorm
  to next-phase candidates list.

No code changes in this commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 18:15:16 +02:00
Erik
a068292f2a feat(phys L.2a slice 3): populate CollisionInfo entity attribution
The CollisionInfo.CollideObjectGuids list + LastCollidedObjectGuid
fields existed but were never written anywhere in the codebase — slice
2's [resolve] probe found this when 85 hit=yes lines came back with
no obj= attribution.

This commit fills the gap at the only place we have the attribution
data: the per-object iteration in Transition.FindObjCollisions, where
obj.EntityId is in scope right after each per-object BSPQuery /
CylinderCollision call. Two cases trigger an Add():
  - result != TransitionState.OK (object hard-blocked transition)
  - normal flipped invalid→valid during the call (BSPQuery captured
    a slide normal without halting — covers wall-slide cases).

Beyond the diagnostic, this also fixes a quiet structural gap — any
future physics behavior that wants "who did I just collide with"
(PvP exemption sanity check, NPC bump rules, etc.) was previously
flying blind on stub fields. Now the data flows.

Build green. Will re-test the doorway with the same trace to get
the wall's entity id.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 18:06:05 +02:00
Erik
e0c08bc57e feat(phys L.2a slice 2): include hit object guid in [resolve] probe
Extends the existing [resolve] probe line to surface
ci.LastCollidedObjectGuid (hit object) + ci.CollidedWithEnvironment
(terrain hit flag) + ci.CollideObjectGuids.Count (when >1) so the
operator can tell WHICH entity the wall is, not just the wall normal.

Tonight's L.2a slice 1 trace caught a clean wall-slide at the
Holtburg-area doorway (n=(0,1,0), 122 hit=yes lines), but had no way
to attribute the hit to a specific entity — the L.2d sub-direction
call (door collision shape vs building wall mesh) needs the entity id
to pick the right fix. This extension provides it on the next run.

Format change for [resolve] hit field:
  Before: hit=yes n=(0.00,1.00,0.00)
  After:  hit=yes n=(0.00,1.00,0.00) obj=0xCC0CXXXX
          hit=yes n=(0.00,1.00,0.00) env
          hit=yes n=(0.00,1.00,0.00) obj=0xCC0CXXXX env nObj=3

Pure additive within the existing PhysicsDiagnostics.ProbeResolveEnabled
gate. No new env var, no new file. Build green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 18:00:01 +02:00
Erik
ebef82034e feat(phys L.2a slice 1): resolver + cell-transit probes (PhysicsDiagnostics)
New static `AcDream.Core.Physics.PhysicsDiagnostics` holds two
runtime-toggleable flags initialized from env vars:

- ACDREAM_PROBE_RESOLVE=1 — emit one [resolve] line per
  PhysicsEngine.ResolveWithTransition call: input/target/output
  position+cell, ok-vs-partial, grounded-in, contact-plane status,
  wall normal if hit, walkable-polygon valid, moving entity id.
- ACDREAM_PROBE_CELL=1 — emit one [cell-transit] line per
  PlayerMovementController.CellId change: old → new cell, current
  world position, reason tag (resolver / teleport).

Both also exposed as runtime-toggleable checkboxes in the DebugPanel
"Diagnostics" section. Unlike the existing four Dump-* checkboxes
(which only mirror sticky-at-startup env vars), the two new ones
forward directly to PhysicsDiagnostics — toggling on/off takes
effect on the next physics resolve, no relaunch.

Why now: L.2's plan-of-record (docs/plans/2026-04-29-movement-collision-
conformance.md) explicitly says "Land L.2a diagnostics first. Do not
make another physics change blind." This slice closes the most-load-
bearing gap in L.2a — a general-purpose probe on the resolver outcome
and a cell-transit log — so that later L.2b/c/d/e physics changes can
be evidence-driven instead of guessed. Foundation for the indoor /
dungeon walking trajectory (G.3 unblock).

Pure additive: when both flags are off (default), the probes collapse
to a single static-bool read per resolve, zero log cost. PlayerMovement
Controller's two CellId-mutation sites are now routed through a
private UpdateCellId(reason) helper for diag chokepoint.

Build green, 1032/1040 unit tests pass. The 8 failing tests are
pre-existing on the branch base (verified by stash-and-rerun);
none touch resolver or cell-transit code; all fail identically with
this slice stashed. Investigation deferred to a follow-up.

Refs: docs/plans/2026-04-29-movement-collision-conformance.md (L.2a
shipped-slice note added in same commit).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 17:19:05 +02:00
Erik
eab347d7e4 Merge branch 'claude/trusting-elbakyan-633b52' — Phase C.1.5b (per-part PES transforms + EnvCell DefaultScript)
Closes #56. Two coupled slices in one phase:

Slice A — ParticleHookSink honors CreateParticleHook.PartIndex. Adds
new SetupPartTransforms.Compute(setup) helper (walks PlacementFrames
[Resting] → [Default] → first-available, mirrors SetupMesh.Flatten's
priority); ParticleHookSink gains a per-entity part-transforms side-table;
SpawnFromHook now applies partTransforms[PartIndex] to the hook offset
before world-space rotation. Multi-emitter scripts distribute across
mesh parts instead of collapsing to entity root.

Slice B — EntityScriptActivator handles dat-hydrated entities. The
ServerGuid==0 early-return guard is relaxed: activator keys by
ServerGuid when non-zero, else entity.Id (collision-free in 0x40xxxxxx
range). Resolver delegate returns ScriptActivationInfo(ScriptId,
PartTransforms) so one dat lookup yields both pieces. GpuWorldState
fires the activator from 4 new dat-hydration sites (AddLandblock +
AddEntitiesToExistingLandblock for OnCreate, RemoveLandblock +
RemoveEntitiesFromLandblock for OnRemove). EnvCell statics + exterior
stabs (inn fireplaces, cottage chimneys, building decorations) now fire
their Setup.DefaultScript automatically.

Reality discovery during design (spec §3): EnvCell.StaticObjects are
already hydrated as WorldEntity instances by GameWindow.BuildInterior
EntitiesForStreaming with stable entity.Id — handoff §4 Q1/Q2 (synthetic
ID scheme, separate walker class) mooted.

Visual verification 2026-05-12: Holtburg Town network portal (entity
0x7A9B405B, script 0x3300126D) swirl no longer ground-buried, emitters
distributed across the arch; Holtburg Inn fireplace flames; cottage
chimney smoke; spell cast on +Acdream — all match retail.

18 new + 4 updated tests, all Vfx/Meshing/Activator/Streaming green.
8 pre-existing Physics/Input failures unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 08:52:08 +02:00
Erik
c988e98b5a docs(vfx #C.1.5b): ship Phase C.1.5b — closes #56 + EnvCell DefaultScript dispatch
Roadmap:
- Status header now reads "Between phases" with C.1.5b in the recent-wins
  bullet list.
- New SHIPPED row in the table (C.1.5b) summarising both slices, the
  reality discovery about EnvCell statics, and the 4 visual-verified sites.
- The "IN FLIGHT — C.1.5b" sub-bullet under Phase C flipped to SHIPPED.

ISSUES.md:
- #56 removed from Active.
- #56 added to Recently closed with full commit chain (1e3c33b spec+plan,
  f3bc15e SetupPartTransforms helper, 11521f4 ParticleHookSink part-transform,
  5ca5827 activator refactor, 8735c39 GpuWorldState fire-sites), resolution
  notes, and the visual-verification record.

CLAUDE.md:
- "Currently in flight" pointer replaced by a "Currently between phases"
  marker + a C.1.5b shipped paragraph that's parallel to the existing
  C.1.5a entry.
- "After C.1.5b" → "Next phase candidates".

Visual verification 2026-05-12: portal swirl matches retail extent + lateral
spread (no ground-burial); Holtburg Inn fireplace flames; cottage chimney
smoke; spell-cast particles on +Acdream — all match retail.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 08:51:26 +02:00
Erik
8735c39a40 feat(vfx #C.1.5b): GpuWorldState fires activator for dat-hydrated entities
Four new foreach blocks in GpuWorldState wire EntityScriptActivator
into the dat-hydration spawn/despawn paths:
- AddLandblock: fires OnCreate for each entity with ServerGuid==0
  (live entities filtered out — they got OnCreate at AppendLiveEntity
  and would double-fire on pending-bucket merges).
- AddEntitiesToExistingLandblock: fires OnCreate for each entity in
  the promoted batch (all dat-hydrated by construction).
- RemoveLandblock: fires OnRemove(entity.Id) for each ServerGuid==0
  entity before the loaded record is dropped.
- RemoveEntitiesFromLandblock: fires OnRemove for the demote-tier
  entities about to be cleared (Near→Far demotion).

5 new integration tests cover the four fire-sites + the no-double-fire
invariant on pending-bucket merges. Pattern matches existing
GpuWorldStateTests (stub LandBlock heightmap + WorldEntity factory).

Closes #56 end-to-end. Slice A (per-part transforms in Tasks 1-3) +
Slice B (dat-hydrated entity DefaultScript firing, this task) both
ready for visual verification at Holtburg portal + Inn fireplace +
cottage chimney + spell cast.

Note: 8 pre-existing failures in Physics/Input/MotionInterpreter test
families are unrelated to this work (verified by re-running with this
task's changes stashed).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 00:07:38 +02:00
Erik
5ca5827abe feat(vfx #C.1.5b): activator handles dat-hydrated entities + per-part transforms
Resolver returns ScriptActivationInfo(ScriptId, PartTransforms) — one
dat lookup per spawn yields both pieces of info. The C.1.5a ServerGuid==0
guard is relaxed: activator now keys by ServerGuid when nonzero, else
entity.Id, so dat-hydrated entities (EnvCell statics, exterior stabs)
flow through the same code path as server-spawned ones. PartTransforms
pushed into ParticleHookSink before scheduling Play, closing the
activator side of #56.

GameWindow resolver lambda upgraded: now constructs ScriptActivationInfo
from setup.DefaultScript.DataId + SetupPartTransforms.Compute(setup),
swallowing dat-lookup throws the same way C.1.5a did.

Tests: 4 existing tests updated for new ScriptActivationInfo signature;
3 new tests cover entity.Id keying for dat-hydrated entities, end-to-end
part-transform pipeline (resolver → sink → particle world position), and
OnRemove with an arbitrary caller-picked key. 77 Vfx+Meshing+Activator
tests green.

GpuWorldState fire-site wiring (Task 4) lands next.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 00:02:16 +02:00
Erik
11521f4418 fix(vfx #56): ParticleHookSink applies CreateParticleHook.PartIndex transform
Adds per-entity part-transform side-table mirroring _rotationByEntity.
SpawnFromHook now transforms the hook offset through partTransforms[partIndex]
before rotating to world space. Backwards-compatible: entities without
registered part transforms fall through to identity (pre-C.1.5b behavior),
so the existing C.1.5a rotation-seed test stays green.

Adds SetEntityPartTransforms public method. Cleared on StopAllForEntity
alongside the rotation entry.

2 new xUnit tests:
- SpawnFromHook_AppliesPartTransform_WhenRegistered — part 1 lifted +Z=1,
  hook offset (1,0,0), PartIndex=1 → world (1,0,1).
- SpawnFromHook_FallsBackToIdentity_WhenPartIndexOutOfBounds — PartIndex=99
  on a 2-part array → offset applied without crash, pre-C.1.5b behavior.

Closes the renderer side of #56. EntityScriptActivator wiring (Task 3)
lands next.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 23:57:20 +02:00
Erik
f3bc15ed9d feat(vfx #C.1.5b): SetupPartTransforms helper for per-part anchor transforms
Computes Matrix4x4 per Setup part by walking PlacementFrames[Resting] →
[Default] → first-available, matching SetupMesh.Flatten's priority.
Foundation for #56 fix: ParticleHookSink will use these to apply each
CreateParticleHook's PartIndex-relative offset to the right mesh part.

4 xUnit tests cover Resting-over-Default preference, Default fallback,
empty-PlacementFrames returns empty, DefaultScale application.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 23:54:33 +02:00
Erik
1e3c33b4db docs(vfx #C.1.5b): design + plan for issue #56 + EnvCell DefaultScript
Two-slice phase:
- Slice A: ParticleHookSink applies CreateParticleHook.PartIndex via
  SetupPartTransforms.Compute(setup.PlacementFrames). Closes #56.
- Slice B: drop EntityScriptActivator's ServerGuid==0 guard so
  dat-hydrated EnvCell statics + exterior stabs fire DefaultScript.

Key reality discovery folded into the spec §3: EnvCell.StaticObjects
are already WorldEntities (via GameWindow.BuildInteriorEntitiesForStreaming),
so no synthetic-ID scheme + no new walker class needed — the handoff's
§4 Q1/Q2 options are mooted by entity.Id being collision-free.

Doc-drift fixes from C.1.5a folded into §8.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 23:51:44 +02:00
Erik
2e222ee553 Merge branch 'claude/stupefied-euler-fca3f7' — docs hygiene (CLAUDE.md + roadmap + ISSUES.md triage) 2026-05-11 23:35:14 +02:00
Erik
55dc861724 docs(hygiene): reconcile CLAUDE.md + roadmap; triage 16 → 12 open issues
CLAUDE.md:
- "Currently in flight" (was NONE) → C.1.5b with pickup-doc link
- Added C.1.5a + N.6 slice 1 + post-A.5 polish ship paragraphs
- "Next planned phase" (was N.6) → ranked candidate list
- Collapsed Tier 1 + 4 historical phase paragraphs into
  roadmap-table pointer (signal density +; line count ~net zero)
- Fixed broken project_ui_architecture.md Memory-crib reference

Roadmap header: dated 2026-05-10 → 2026-05-11 with "since the last
update" delta. New Phase C.1.5 sliced sub-piece in Phases ahead.

ISSUES.md triage:
- Closed #37 (humanoid coat — resolved by #47 GfxObjDegradeResolver),
  #49 + #50 (scenery placement — accepted WB-vs-retail divergence).
- Promoted #36 (sky-PES dispatch) to Phase C.1.5c; #2/#28/#29
  auto-close when that phase ships.
- Downgraded #39 (Run↔Walk cycle) to LOW + VERIFY-PENDING.
- Cross-referenced #41 + #46 to Phase L.2 motion conformance.
- Anti-coupling note on #4 (NOT in C.1.5c cluster).
- Chore tag on #3 (single-commit clock-drift fix).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 23:34:01 +02:00
Erik
ad261539d6 docs(vfx #C.1.5b): handoff for next slice (issue #56 + EnvCell statics)
Self-contained handoff doc for the C.1.5b session. §1 is a copy-paste
startup prompt for a fresh Claude Code session; §2+ is detailed
context (commits shipped in C.1.5a, decision space for the #56 fix
including precompute-per-spawn vs render-thread-side-table options,
EnvCell synthetic-id scheme, walker-class placement options, file
pointers, verification plan).

Slice will resolve issue #56 first (per-part transform handling for
static entities) before adding the EnvCell static-object DefaultScript
walker, per the C.1.5a final reviewer's recommendation that resolving
#56 unblocks slice 2's visual delight gate (the multi-emitter
collapse symptom affects portals, chimneys, and fireplaces alike).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 18:22:01 +02:00
Erik
88bda12d98 Merge branch 'claude/lucid-burnell-aab524' — Phase C.1.5a (portal PES wiring)
Slice 1 of Phase C.1.5: fire Setup.DefaultScript through the already-
shipped PhysicsScriptRunner when a server-spawned WorldEntity enters
the world. Portals and any other static object with a DefaultScript
now emit their retail-faithful persistent particle effects at spawn
time, mirroring retail's play_script_internal dispatch on object load.

One new ~85-line orchestrator class EntityScriptActivator wires
alongside EntitySpawnAdapter in GpuWorldState's spawn lifecycle
(AppendLiveEntity → OnCreate; RemoveEntityByServerGuid → OnRemove).
Activator construction in GameWindow uses a defensive resolver lambda
over _dats.Get<Setup>(...) and also seeds _particleSink.SetEntityRotation
so hook offsets transform from entity-local to world space correctly.
Four xUnit tests with mutation-check verification. End-to-end: 8 commits,
every per-task commit reviewed for spec compliance + code quality;
final cross-task review by Opus approved.

Visual verification at the Holtburg Town network portal: 10-hook portal
script fires with correct color, persistence, orientation, and
multi-emitter dispatch.

Known limitation surfaced and filed as issue #56: ParticleHookSink
ignores CreateParticleHook.PartIndex, so multi-emitter scripts collapse
to one root position instead of distributing across the entity's mesh
parts. Visually produces a compressed, partly-ground-buried swirl on
multi-part dat objects. Mechanism is correct; per-part transform
handling is the next vfx-pipeline concern and blocks slice 2 (EnvCell
chimneys / fireplaces).

Plan: docs/superpowers/plans/2026-05-12-phase-c1.5a-portals.md
Spec: docs/superpowers/specs/2026-05-12-phase-c1.5a-portals-design.md
Issue: docs/ISSUES.md #56

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 16:23:17 +02:00
Erik
9009318656 docs(vfx #C.1.5a): ship Phase C.1.5a + file issue #56 for per-part collapse
Visual verification at the Holtburg Town network portal passed for the
slice's mechanism: 10-hook portal script fires end-to-end with correct
color, persistence, orientation, and multi-emitter dispatch. After the
334f0c6 rotation-seed fix, the swirl is oriented correctly along the
portal's facing instead of world-NS.

Known limitation surfaced during verification and filed as issue #56:
ParticleHookSink ignores CreateParticleHook.PartIndex, so all 10 of the
portal's emitters collapse to the entity root position + identity-rotated
offset, producing a compressed and partly-ground-buried swirl instead of
the multi-tier shape retail renders. Mechanism is correct; per-part
transform handling is the next vfx-pipeline concern (will affect every
multi-emitter PES — slice 2 chimneys/fireplaces in particular).

Documentation changes:
- docs/ISSUES.md: new #56 entry with the captured entity guids
  (0x7A9B405B / 0x7A9B4080), script ids (0x3300126D / 0x3300067A),
  symptom data, root-cause hypothesis, file pointers, and acceptance
  criterion. Notes the blocks-slice-2 relationship explicitly.
- docs/superpowers/specs/2026-05-12-phase-c1.5a-portals-design.md §9:
  new limitation #1 documenting the verified PartIndex collapse symptom.
- docs/plans/2026-04-11-roadmap.md: new "C.1.5a" row in the shipped
  table referencing the spec, plan, and #56 caveat.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 16:13:12 +02:00
Erik
334f0c6a26 fix(vfx #C.1.5a): seed entity rotation in activator so hook offset rotates
Visual verification at the Holtburg Town network portal revealed the swirl
was oriented along world axes (NS) instead of the portal's actual facing
(EW), and partially buried in the ground because the hook's local-frame
Offset.Origin was being applied in world axes too.

Root cause: EntityScriptActivator.OnCreate fired _scriptRunner.Play but
never called _particleSink.SetEntityRotation. When the runner's
CreateParticleHook fires, the sink reads per-entity rotation from
_rotationByEntity (defaults to Quaternion.Identity for unknown entities)
and uses it to transform the hook's Offset.Origin from entity-local to
world space. Without the seed call, the rotation lookup falls through to
Identity and the offset goes off along world XYZ.

Fix is a single SetEntityRotation call before the Play call. Added a 4th
unit test that constructs an entity with a 90 deg yaw and asserts the spawned
particle's world position reflects the rotated offset.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:56:27 +02:00
Erik
849690c814 refactor(vfx #C.1.5a): reuse SequencerFactory's capturedDats in resolver
Code-review follow-up to 65d833d:

ResolveDefaultScript was closing over its own var capturedDatsForActivator
= _dats, but the sibling SequencerFactory in the same block already
declared var capturedDats = _dats. The two locals pointed at the same
reference and served the same purpose; the alias added no value and
muddied the closure pattern.

Reuse capturedDats. No behavior change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 14:35:02 +02:00
Erik
65d833de1e feat(vfx #C.1.5a): construct EntityScriptActivator in GameWindow
Wires the activator into the production lifecycle:
- Construct alongside _wbEntitySpawnAdapter using _scriptRunner +
  _particleSink (both built earlier in OnLoad).
- Production resolver lambda hits _dats.Get<Setup>(...) wrapped in
  try/catch returning 0 on miss/throw — matches ParticleRenderer's
  defensive read pattern.
- Pass into GpuWorldState's new optional ctor parameter.

Closes the wiring half of C.1.5a. Visual verification at the Holtburg
Town network portal is the acceptance gate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 14:30:53 +02:00
Erik
44d85022e8 feat(vfx #C.1.5a): wire EntityScriptActivator into GpuWorldState lifecycle
GpuWorldState grows a fourth optional ctor parameter for the activator,
paralleling how EntitySpawnAdapter is plumbed. AppendLiveEntity calls
OnCreate after the existing _wbEntitySpawnAdapter?.OnCreate;
RemoveEntityByServerGuid calls OnRemove after the existing OnRemove.
Symmetric, same order, null-safe.

GameWindow still passes the old 3-arg ctor — activator construction +
wire-through lands in the next commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 14:25:22 +02:00
Erik
e0529b023d test(vfx #C.1.5a): real-emitter verification in OnRemove test + unused using
Code-review follow-up to 003c502:

1. Test 3 (OnRemove_StopsScriptsAndEmitters) now wires the runner into
   the real ParticleHookSink instead of a RecordingSink, registers a
   persistent EmitterDesc, lets the CreateParticleHook actually spawn an
   emitter, then asserts the sink killed it after OnRemove. Previously
   the test only verified runner-side state — sink.StopAllForEntity was
   never observably exercised, so a regression dropping that call would
   have passed silently.

2. Removed unused `using System.Numerics` from EntityScriptActivator.cs.

No production code changes. Tests 1 and 2 unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 14:20:46 +02:00
Erik
003c502774 feat(vfx #C.1.5a): add EntityScriptActivator (no wiring yet)
New ~50-line orchestrator that fires Setup.DefaultScript through the
already-shipped PhysicsScriptRunner on entity spawn and stops scripts +
live emitters on despawn. Resolver delegate avoids DatCollection coupling
so the class is fully unit-testable with stubs.

Three xUnit tests cover the three branches: fire-with-script,
no-op-without-script, stop-on-remove. No wiring into the live spawn path
yet -- that lands in the next commit.

Spec: docs/superpowers/specs/2026-05-12-phase-c1.5a-portals-design.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 14:10:38 +02:00
Erik
ed5335b81e docs(vfx #C.1.5a): implementation plan + spec wiring-location fixes
Plan: docs/superpowers/plans/2026-05-12-phase-c1.5a-portals.md. Four
tasks, TDD-style, each task is a single commit boundary:
  1. EntityScriptActivator class + three xUnit tests
  2. Wire into GpuWorldState (new optional ctor param + two ?. calls)
  3. Construct in GameWindow with resolver lambda
  4. Visual verification at Holtburg Town network portal + roadmap update

Spec amendments correct an inaccuracy in the 2026-05-12 commit
(06d7fbd): the activator's call sites live in GpuWorldState
(AppendLiveEntity / RemoveEntityByServerGuid), not directly in
GameWindow as the original spec described. Also fixes the test file
path: tests/AcDream.Core.Tests/... not AcDream.App.Tests/... per the
existing test-project convention. No design changes — same activator,
same trigger condition, same lifecycle ordering.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 13:36:18 +02:00
Erik
06d7fbd5ef docs(vfx): Phase C.1.5a — portal PES wiring design spec
Slice 1 of Phase C.1.5: fire Setup.DefaultScript through the already-
shipped PhysicsScriptRunner on server-spawned WorldEntity create, so
portals (and any other entity with a DefaultScript) emit their retail-
faithful persistent particle effects at spawn time. Reuses the C.1
runner-sink-system-renderer chain end-to-end; one new ~50-line class
(EntityScriptActivator) plus a two-line wiring in GameWindow.

Slice 2 (C.1.5b) will cover EnvCell.StaticObjects + animation-hook
verification; spec landed separately after slice 1 verification passes.

Acceptance: visual confirmation at the Holtburg Town network portal,
side-by-side with retail.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 13:28:55 +02:00
Erik
9b447d4ca8 Merge branch 'claude/objective-brown-86e645' — Phase N.6 slice 1 (gpu_us fix + perf baseline)
Slice 1 of Phase N.6: unblocked the gpu_us diagnostic in WbDrawDispatcher
(ring-of-3 query slots, read-before-overwrite, vendor-neutral across
AMD/NVIDIA/Intel desktop GL) and captured the radius=12 perf baseline
at Holtburg with the now-working diagnostic.

Headline data: CPU dominates GPU by 30-50× at every measured radius;
GPU dispatch p95 maxes at 603µs (3.6% of 16.6ms frame budget); CPU
grows more than linearly with N₁ (3.2 → 6.7 → 12.9 ms as N₁ goes
4 → 8 → 12). The per-LB walk in the dispatcher is the next bottleneck
if perf ever becomes tight.

Recommendation in the baseline doc: C.1.5 (PES emitter wiring — portals,
chimneys, fireplaces) next; reduced-scope N.6 slice 2 after that (drop
atlas + persistent-mapped buffers per the GPU-underutilized finding);
Tier 2 only if perf escalation becomes pressing.

New issue #55 filed: static-entity slow path reports ~1.45M meshMissing
per 5s at r4 standstill (LOW severity, no visible regression).

Plan: docs/superpowers/plans/2026-05-11-phase-n6-slice1.md
Spec: docs/superpowers/specs/2026-05-11-phase-n6-slice1-design.md
Baseline: docs/plans/2026-05-11-phase-n6-perf-baseline.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 12:56:26 +02:00
Erik
41981c4d74 docs(perf #N6.1): apply final-review fixes — spec, baseline doc, issue #55
Final code review of slice 1 flagged one Important issue (the spec's
"zero cost when off" claim for the surface-dump path is technically
violated — _uploadMetadata always writes one dict entry per upload
regardless of env var) plus minor doc/consistency gaps. Applied:

1. Spec §5 "Cost when off": dropped the "Zero" claim; replaced with
   "Negligible — one Dictionary write per upload (~30-50 KB at Holtburg)
   plus a hash-table write per upload. Expensive work (file I/O,
   histogram construction) is still env-gated." This matches reality.

2. Baseline doc §5: rewrote from "Raw logs (scratch, can be deleted)"
   referencing files that were never preserved in this worktree, to
   "Reproducing the measurements" with the actual PowerShell launch
   commands. Honest about the raw logs not being kept; the captured
   medians in section 2 are the canonical record.

3. New issue #55 filed in docs/ISSUES.md — static-entity slow path
   reports ~1.45M meshMissing/5s at r4 standstill, drops to ~0 when
   walking. LOW severity (no visible regression), hypothesis points
   at a "permanently-missing entity gets re-classified every frame"
   pattern that Tier 1 cache doesn't cover.

4. Roadmap shipped table: renamed "N.6.1" row to "N.6 slice 1" to
   match every other artifact's naming. Search-discoverability fix.

None of these change the slice's conclusion or next-phase
recommendation (C.1.5 first, then reduced-scope slice 2).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 12:51:10 +02:00
Erik
76ca3ffca8 docs(perf #N6.1): apply code-quality review fixes to baseline doc
Code-quality review on commit 13abf96 flagged 3 Important issues in
the baseline document plus 2 minor roadmap consistency gaps. Applied
all of them:

1. The "CPU scales superlinearly with N₁" claim was imprecise because
   CPU growth (4.0×) is actually sublinear vs near-LB count (7.7×).
   Clarified: CPU grows more than linearly with radius N₁ but
   sublinearly with visible-LB count; frustum cull discards most far
   LBs early. The outer per-LB walk still scales with N₁, which is
   what Tier 2's persistent groups address.

2. The "40-50% memory footprint reduction from atlas packing" estimate
   was asserted without derivation and likely too optimistic given all
   surfaces are already power-of-two and same-format (RGBA8). Replaced
   with a more honest bound: "low-MB to ~10 MB absolute saving" with
   explicit per-array metadata overhead reasoning. Conclusion is
   unchanged — atlas adoption still isn't justified given GPU
   under-utilization.

3. The "spec §6 threshold for atlas is >30%" citation pointed at text
   that doesn't exist in the spec. Replaced with "A conventional
   rule-of-thumb" so a future reader doesn't chase a phantom citation.

Plus roadmap consistency:

M1: The N.6 slice 1 bullet now uses the canonical "✓ SHIPPED — Title.
   Shipped YYYY-MM-DD." prefix that every other shipped phase uses.
M2: Added N.6.1 row to the shipped table at the top of the roadmap
   (lines ~55-66) so the at-a-glance shipped list is complete.

None of these change the conclusion or the next-phase recommendation
(C.1.5 first, then reduced N.6 slice 2). The fixes improve doc accuracy
and future-readability.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 12:43:35 +02:00
Erik
13abf96a5e docs(perf): Phase N.6 slice 1 — radius=12 baseline + surface dump path
Capture authoritative CPU+GPU dispatch numbers at Holtburg with the
gpu_us diagnostic now working (commit 25cb147). Three radii (4/8/12)
x two motion modes (standstill/walking) + a surface-format histogram
from ACDREAM_DUMP_SURFACES=1.

Adds env-gated one-shot dump path (TextureCache.TickSurfaceHistogramDumpIfEnabled,
called from GameWindow.OnRender) that fires once after both (a) frame
600 of the session AND (b) the upload-metadata dict reaches 100 entries
-- the cache-size gate prevents the dump from firing during pre-world
GUI ticks where OnRender spins at high rates but no scenery has streamed.
Output writes to %LOCALAPPDATA%\acdream\n6-surfaces.txt with a try/catch
around the I/O so disk-full / permission errors don't crash mid-measurement.

Baseline document at docs/plans/2026-05-11-phase-n6-perf-baseline.md
documents:
- CPU dominates GPU by 30-50x at every radius (strongly CPU-bound)
- GPU wildly under-utilized (max gpu_us p95 ~600us vs 16,600us frame budget)
- CPU scales superlinearly with N1 (Tier 1 cache wins on inner loop but
  not outer LB walk)
- Surface atlas opportunity high (59% of textures in top-3 triples) but
  win is memory-only since GPU isn't bottlenecked

Recommendation: C.1.5 (PES emitter wiring) next, then a reduced-scope
N.6 slice 2 (drop atlas + persistent-mapped buffers -- not justified by
the GPU under-utilization observed).

Roadmap entry amended to split N.6 into slice 1 (shipped) and slice 2
(planned, reduced scope, deferred until after C.1.5).

Spec: docs/superpowers/specs/2026-05-11-phase-n6-slice1-design.md.
Plan: docs/superpowers/plans/2026-05-11-phase-n6-slice1.md (Task 4).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 12:34:10 +02:00
Erik
25cb147d97 fix(perf #N6.1): gate gpu_us read on diag for symmetric toggle behavior
Code-quality review on Task 1 (commit a7c9800) flagged an asymmetric
diag gate: the read-before-overwrite block at the top of the dispatcher
was not gated on diag, but the frame-counter increment and BeginQuery
calls were. If a maintainer toggled ACDREAM_WB_DIAG from "1" to "" mid-
session, _gpuQueryFrameIndex would freeze (gated inside if(diag)) while
the read kept firing every frame at the same slot — producing duplicate
stale samples.

Add diag to the read block's outer condition so the read/issue/increment
trio is symmetric. One-line change; behavior under the normal usage
pattern (env var set at launch, never toggled) is unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 11:28:22 +02:00
Erik
a7c98004bb feat(perf): Phase N.6 slice 1 — fix gpu_us double-buffering in WbDrawDispatcher
The dispatcher's GPU TimeElapsed queries were polled in the same frame
as the indirect draw, so glGetQueryObject(ResultAvailable) always
returned 0 and gpu_us in [WB-DIAG] was stuck at 0m/0p95.

Replace the 2 single-handle queries with ring-of-3 arrays and move the
result read to BEFORE issuing the next frame's queries into the same
slot — at frame N we read slot N%3 which holds frame N-3's queries
(oldest in the ring, ~50ms old at 60fps and definitely done across all
desktop GL drivers). Vendor-neutral: AMD/NVIDIA/Intel desktop GL all
work without driver-specific code.

The gpuQuerySlot variable is hoisted to function scope (just before
Phase 7 opaque pass) so both the opaque and transparent passes
reference the same slot — the plan placed it inside the opaque-pass
if-block, which would have been out of scope for the transparent
BeginQuery; corrected in the implementation.

No new tests — the change is purely a diagnostic readout fix, no
observable behavior in the rendering path. Build green; tests at
baseline (1711 passing, 8 pre-existing physics/MotionInterpreter
failures unchanged). Manual gpu_us verification still pending in-world.

Spec: docs/superpowers/specs/2026-05-11-phase-n6-slice1-design.md (§4).
Plan: docs/superpowers/plans/2026-05-11-phase-n6-slice1.md (Task 1).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 11:24:26 +02:00
Erik
a4931eeaa2 docs(perf): Phase N.6 slice 1 — implementation plan
Step-by-step plan for the two-commit slice: fix WbDrawDispatcher's
gpu_us double-buffering bug (ring-of-3 query slots, read-before-overwrite,
vendor-neutral) then capture the radius=12 baseline at Holtburg with
the now-working diagnostic. Includes exact old_string/new_string Edit
patterns for every code change, PowerShell launch + measurement
procedure for the manual baseline, baseline doc template with explicit
fill-in slots, and a per-criterion acceptance checklist.

Output companion to docs/superpowers/specs/2026-05-11-phase-n6-slice1-design.md
(commit 05d590c).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 11:12:26 +02:00
Erik
05d590cd54 docs(perf): Phase N.6 slice 1 — spec for gpu_us fix + radius=12 baseline
Brainstormed design for the first slice of Phase N.6 (perf polish).
Slice 1 ships two commits: (1) fix the GPU timing query double-buffering
in WbDrawDispatcher (cross-vendor ring of 3, read-before-overwrite),
(2) add an env-gated surface-format histogram dump + capture the
radius=12 perf baseline at Holtburg. Slice 2 (TextureCache cleanup +
shader migration + optional persistent-mapped buffers) is deferred
until after C.1.5 (PES emitter wiring), with the next-phase decision
to be made on the baseline numbers slice 1 produces.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 11:03:44 +02:00
Erik
175ad14f8b Merge branch 'claude/friendly-varahamihira-7b8664' — Tier 1 entity-classification cache (closes #53)
Post-A.5 polish Priority 3. EntityClassificationCache keyed by
(entityId, landblockHint) tuple. Entity dispatcher cpu_us median ~1.2 ms,
p95 ~1.5 ms — ~66% reduction vs pre-Tier-1 baseline. Closes the
post-A.5 polish phase entirely (#52, #54, #53 all closed).

See docs/ISSUES.md #53 closure + memory/project_tier1_cache.md for the
24-commit chain, 4 bug-fix iterations, and the per-tuple-vs-per-entity
recurring trap pattern documented for future cache work.
2026-05-11 00:11:42 +02:00
Erik
110fb691a8 ship(post-A.5 #53): Tier 1 entity-classification cache — closes ISSUE #53
EntityClassificationCache keyed by (entityId, landblockHint) tuple lands
per spec docs/superpowers/specs/2026-05-10-issue-53-tier1-cache-design.md
+ plan docs/superpowers/plans/2026-05-10-issue-53-tier1-cache.md.

Perf result (horizon-safe preset + High quality, AMD Radeon RX 9070 XT
@ 1440p): entity dispatcher cpu_us median ~1200 us, p95 ~1500 us. Down
from ~3500m / ~4000p95 pre-Tier-1. ~66% / ~63% reduction. Well under
the A.5 spec budget (median <= 2.0 ms, p95 <= 2.5 ms). No BUDGET_OVER
flag across 30s+ standstill captures.

Visual gate cleared after 4 bug-fix iterations:
  - 71d0edc: namespace stab Ids globally (cross-LB id collision)
  - 95ebbf3: key cache by (entityId, landblockHint) tuple (defensive)
  - c55acdc: skip cache populate when classification is incomplete
  - f928e66: incomplete-entity flag must persist across same-entity tuples

User-confirmed visually via +Acdream test character: NPCs animate,
multi-part static buildings render fully (no airborne geometry, no
Z-fighting, no missing parts, no wrong textures), Nullified Statue of
a Drudge on top of the Foundry renders all parts, trees outside
Holtburg render with branches present.

Closes the post-A.5 polish phase. Issues #52, #54, #53 all closed.

Tests: 1711 passing, 8 pre-existing physics/input failures unchanged.
N.5b sentinel: 112/112 throughout.

Memory: ~/.claude/projects/.../memory/project_tier1_cache.md +
feedback_cache_per_tuple_pattern.md capture the audit-gap and per-tuple-
vs-per-entity recurring trap for future cache work.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 00:09:57 +02:00
Erik
f928e66119 fix(render #53): incomplete-entity flag must persist across same-entity tuples
User reported (cache enabled, post-c55acdc): drudge statue renders fully
but many trees are missing branches. Cache-disabled A/B run rendered trees
correctly. So the bug is in the cache wiring.

Root cause: c55acdc's `currentEntityIncomplete = false;` reset fired
UNCONDITIONALLY at the top of every iteration. For a tree with MeshRefs
[trunk valid, branches null, leaves valid], the tuple sequence is:

  - tuple 0 (trunk): no flag set
  - tuple 1 (branches): TryGetRenderData null → set flag, continue
  - tuple 2 (leaves): unconditional reset → flag = false (WRONG)
  - end-of-entity: flag is false, scratch has trunk+leaves batches but NOT
    branches → MaybeFlushOnEntityChange populates a PARTIAL cache entry
  - cache hits forever serve trunk+leaves with no branches

Drudge happened to render correctly because its missing MeshRef was at the
END of its MeshRefs list — no later tuple reset the flag.

Adds a per-tuple `prevTupleEntityId` tracker for entity-change detection,
updated UNCONDITIONALLY at end of each tuple (including tuples that skip
via null renderData). The flag-reset block now fires ONLY on actual entity
change. Within the same entity, the flag accumulates across tuples.

Also includes ACDREAM_DISABLE_TIER1_CACHE=1 diagnostic env-var added
inline (was stashed previously) for future A/B testing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 23:56:58 +02:00
Erik
c55acdc3d5 fix(render #53): skip cache populate when classification is incomplete
User reported: the drudge statue on top of the Foundry (a multi-part
live-spawned entity with AnimPartChange + texChanges) renders only
PARTIALLY — some parts visible, some missing.

Root cause: the dispatcher's slow path skips a MeshRef when
_meshAdapter.TryGetRenderData returns null (mesh still async-decoding
via ObjectMeshManager.PrepareMeshDataAsync). The classified-batches
collector accumulates only the MeshRefs that DID resolve. At entity
boundary, the cache populates with the PARTIAL set. Frame-2 cache hits
serve that partial entry forever — even after the missing mesh loads,
the cache continues to skip those parts because classification never
reruns for cached entities.

Fix: track currentEntityIncomplete during the foreach. Set it true on
any null renderData. At entity boundary (and at end-of-loop), if the
flag is set, DROP the accumulated populate scratch instead of writing
it to the cache. The slow path retries on the next frame; once all
meshes have loaded, the populate fires correctly with the complete
classification.

Adds a regression test pinning the contract — incomplete entities
produce zero cache entries.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 23:42:46 +02:00
Erik
95ebbf3004 fix(render #53): key cache by (entityId, landblockHint) to defeat ID collision
User confirmed via A/B test (ACDREAM_DISABLE_TIER1_CACHE=1) that the
visual bug — buildings rendering up in the air outside Holtburg — is in
the cache wiring, not elsewhere. The matrix math (restPose * entityWorld
== model) was provably correct, so the bug had to be cache key collision.

Stabs were namespaced in commit 71d0edc, but scenery (0x80LLBB00 +
localIndex) and interior (0x40LLBB00 + localCounter) still have the
same 256-overflow risk. Dense LBs outside Holtburg (forest, urban) push
localIndex past 255, wrapping into the lbY byte and creating cross-LB
collisions.

Fix: change the cache key from uint entityId to (uint, uint) tuple of
(EntityId, LandblockHint). The cache is now correct-by-construction
regardless of any hydration path's Id-generation strategy. Defensive
against future regressions in any ID namespace.

InvalidateEntity becomes a sweep (was O(1)), but it's called rarely
(only on live-entity despawn). InvalidateLandblock was already a sweep.

Updated 14 existing cache tests + 1 dispatcher integration test to thread
landblockHint through TryGet / DebugCrossCheck calls.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 23:02:14 +02:00
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
6f7d73f7cf Merge branch 'claude/sharp-sanderson-6b47ae' — sharpen Phase M into design spec + opcode matrix 2026-05-10 19:22:54 +02:00