Four small wins on top of the grouped-instanced refactor.
1. Drop unused animState lookup. Was a side-effect-free
_entitySpawnAdapter.GetState call per per-instance entity, made
redundant by the Issue #47 fix that trusts MeshRefs.
2. Front-to-back sort opaque groups. Squared distance from camera to
each group's first-instance translation; ascending sort. Lets the
GPU's depth test reject fragments behind closer geometry — real
win on dense scenes (Holtburg courtyard, Foundry interior).
3. Per-entity AABB frustum cull. 5m-radius AABB check per entity
before walking parts. Skips work for distant entities even when
their landblock is partially visible. Animated entities (other
characters, NPCs, monsters) bypass — they always need per-frame
work for animation regardless. Conservative radius covers typical
entity bounds; large outliers stay landblock-culled.
4. Memoize palette hash per entity. TextureCache.HashPaletteOverride
is now internal; new GetOrUploadWithPaletteOverride overload takes
a precomputed hash. The dispatcher computes it ONCE per entity and
reuses across every (part, batch) lookup, avoiding the per-batch
FNV-1a fold over SubPalettes. Trees / scenery without palette
overrides skip entirely (palHash stays 0).
Visual output unchanged; FPS up further, especially in dense scenes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Three bugs surfaced and resolved during Task 26 visual verification.
1. **No-scenery + exploded characters**: WB's modern rendering path
(GL 4.3 + bindless) packs every mesh into a single global VAO/VBO/IBO
(GlobalMeshBuffer). Each batch references its slice via FirstIndex
(offset into IBO) + BaseVertex (offset into VBO). The dispatcher's
DrawElementsInstanced(indices=0) read offset 0 of the global IBO
for every entity — drawing the same first triangle from every
entity position. Switched to glDrawElementsInstancedBaseVertex(
BaseInstance) with the batch's offsets. Scenery + connected
characters now render correctly.
2. **Issue #47 character regression**: Adjustment 6 stored
AnimPartChanges on WorldEntity.PartOverrides using the raw
server-sent NewModelId (no degrade resolver applied). The
dispatcher's animState.ResolvePartGfxObj override path then
clobbered MeshRefs (which GameWindow's spawn code correctly
resolves to close-detail meshes via GfxObjDegradeResolver).
Result: humanoids drew low-detail (~14 verts/17 polys) base
meshes instead of close-detail (~32 verts/60 polys), losing
bicep / shoulder / back geometry. Fix: trust MeshRefs as the
source of truth and don't re-apply animState overrides at draw
time. AnimatedEntityState's overrides only matter for hot-swap
appearance updates (0xF625) which today rebuild MeshRefs anyway.
3. **Performance — sub-100 FPS on Holtburg**: per-entity
single-instance draws meant ~16K glDraw calls/frame plus a
64-byte glBufferSubData per call. Refactored to grouped
instanced rendering: bucket all (entity, batch) pairs by
GroupKey(Ibo, FirstIndex, BaseVertex, IndexCount, TextureHandle,
Translucency); upload all matrices in ONE BufferData call;
one glDrawElementsInstancedBaseVertexBaseInstance per group
with BaseInstance pointing at the group's slice in the shared
instance VBO. Down from ~16K to a few hundred draws/frame
(~30× fewer). Bind VAO once per frame (modern WB shares one
global VAO). Removed redundant per-draw VertexAttribPointer
(VAO captures that state).
Result: Holtburg renders correctly with characters showing full
detail; FPS climbed substantially. Two more bugs (mesh loading
+ batch.Key.SurfaceId) were fixed in the prior commit (943652d).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Task 26 visual verification surfaced three bugs in the dispatcher.
Two are fixed here; the third is documented as a remaining issue.
1. WB's IncrementRefCount only bumps a usage counter — it does NOT
trigger mesh loading. Fixed in WbMeshAdapter.IncrementRefCount:
call PrepareMeshDataAsync(id, isSetup: false) on first registration.
Result auto-enqueues to _stagedMeshData (line 510 of WB's
ObjectMeshManager) which Tick() drains onto the GPU.
2. EntitySpawnAdapter never registered per-instance entity meshes
with WB. LandblockSpawnAdapter only registers atlas-tier
(ServerGuid == 0); per-instance entities fell through. Fixed by
adding optional IWbMeshAdapter constructor param + tracking unique
GfxObj ids per server-guid for IncrementRefCount on OnCreate /
DecrementRefCount on OnRemove.
3. WbDrawDispatcher.ResolveTexture used batch.SurfaceId which WB
never populates (line 1746 of ObjectMeshManager only sets
batch.Key — the TextureKey struct that has SurfaceId). Switched
to batch.Key.SurfaceId.
Plus diagnostic counters (ACDREAM_WB_DIAG=1) for entity-seen / drawn
/ mesh-missing / draws-issued counts.
Status: with these fixes the dispatcher now issues real draw calls
(~16K/frame, validated via diagnostic). However visual verification
shows characters appear "exploded" (parts spaced too far apart) and
scenery (trees/rocks/fences/buildings) does not appear. Root cause
analysis pending — Adjustment 7 in the plan documents the deferred
work. Flag stays default-off; legacy renderer remains the
production path.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
SkyRenderer builds its own SkySubMesh structs from GfxObjMesh.Build
at initialization time, with its own VAO/VBO/IBO resources. It reads
Luminosity, Diffuse, NeedsUvRepeat, SurfOpacity, DisableFog from
GfxObjSubMesh directly — not from AcSurfaceMetadataTable. The sky
draw path never touches InstancedMeshRenderer or WbDrawDispatcher.
No code changes needed: the flag has zero effect on sky rendering.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
WbDrawDispatcher draws all entities through WB's ObjectRenderData
(VAO/VBO per GfxObj, per-batch IBO) using acdream's TextureCache for
texture resolution. Two-pass rendering (opaque+ClipMap, then
translucent) matching the existing InstancedMeshRenderer pattern.
Per-entity single-instance drawing for N.4 simplicity — true
instancing grouping deferred to N.6.
Atlas-tier entities: mesh from WB, texture from TextureCache via
batch SurfaceId. Per-instance-tier entities: AnimatedEntityState
drives part overrides + hidden-parts, palette/surface overrides
resolve through TextureCache's composite-key caches.
Side-table population (Task 23 folded in): WbMeshAdapter now takes
DatCollection and populates AcSurfaceMetadataTable on first
IncrementRefCount per GfxObj. The side-table provides TranslucencyKind
(critical for ClipMap alpha-test on vegetation) plus Luminosity,
Diffuse, SurfOpacity, NeedsUvRepeat, DisableFog for sky-pass and
lighting.
GameWindow wiring: when WbFoundationFlag is enabled, WbDrawDispatcher
draws everything and InstancedMeshRenderer is skipped. Flag-off path
is unchanged.
Matrix composition: restPose * animOverride * entityWorld, matching
the spec. Three MatrixCompositionTests verify the contract.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Resolves Adjustment 4 (Option A): WorldEntity now carries the server-
sent AnimPartChange data as PartOverrides and a HiddenPartsMask bitmask.
EntitySpawnAdapter.OnCreate populates AnimatedEntityState from these
fields at spawn time. GameWindow's CreateObject handler converts the
network-layer AnimPartChange records into lightweight PartOverride
structs.
This unblocks Task 22: the WbDrawDispatcher can now resolve per-part
GfxObj overrides and hidden-part suppression from entity state.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Self-contained briefing for whoever picks up Week 4 (Tasks 22-28):
the WbDrawDispatcher full draw loop + sky-pass preservation +
visual verification + flag default-on + legacy-code deletion +
plan finalization.
Highlights two unresolved decisions that need a brainstorm checkpoint
at the start of Week 4 (NOT 'just dispatch'):
- Adjustment 4 plumbing: WorldEntity needs HiddenPartsMask +
AnimPartChanges fields, OR EntitySpawnAdapter.OnCreate takes them
as separate parameters. Decision before Task 22 writes code.
- Surface-metadata side-table population strategy for Task 23.
References the living-document plan + spec + 5 prior adjustments
so a fresh agent has full context cold.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Week 3 ships: AnimatedEntityState (Tasks 16+18+19, commit ce72c57),
EntitySpawnAdapter routing server-spawned content through the existing
TextureCache.GetOrUploadWithPaletteOverride path (Task 17, commit
c02c307). 947 tests pass.
Adjustment 4: WorldEntity lacks HiddenPartsMask + AnimPartChanges
fields. Adapter scaffolding ships; AnimatedEntityState gets default
values (empty mask + empty override map). Plumbing deferred to Task 22
brainstorm — either add fields to WorldEntity or thread through a
separate parameter to EntitySpawnAdapter.OnCreate.
Adjustment 5: Task 20 (per-instance decode conformance) is structural.
Both old and new paths call the same TextureCache function — bytes
identical by construction. EntitySpawnAdapterTests already cover the
routing. No separate conformance test file needed.
Next: Task 22 (Week 4) — WbDrawDispatcher full draw loop. First task
that actually draws through WB and unlocks Adjustment 3's mitigation
(dual-pipeline cost resolves when legacy renderer can short-circuit
its upload for atlas-tier content).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Routes server-spawned (CreateObject) entities through the per-instance
rendering path. Filter: ServerGuid != 0. Atlas-tier entities (procedural,
ServerGuid == 0) flow through LandblockSpawnAdapter (Task 11) instead.
For entities with PaletteOverride set, walks each MeshRef.SurfaceOverrides
map and calls TextureCache.GetOrUploadWithPaletteOverride to pre-warm the
palette-composed GL texture before the first draw. Surfaces not in the
SurfaceOverrides map (i.e. whose ids are only known after opening the GfxObj
dat) are decoded lazily by the draw dispatcher on first use, consistent with
StaticMeshRenderer.
Builds AnimatedEntityState per server-guid via injected sequencer factory
(Func<WorldEntity, AnimationSequencer>). The factory decouples the adapter
from DatCollection so tests pass a stub lambda without a GL context.
OnRemove releases per-entity state. Unknown guids no-op.
Introduces ITextureCachePerInstance: thin seam interface over the palette
decode path so EntitySpawnAdapter tests can use a CapturingTextureCache
mock without constructing a GL context. TextureCache implements it.
Adjustment 4 documented in source comments: WorldEntity does not currently
expose HiddenPartsMask or AnimPartChanges (they are consumed upstream in the
network layer before the WorldEntity is built). HideParts / SetPartOverride
calls are placeholder TODO'd for when those fields are promoted.
Wired into GpuWorldState.AppendLiveEntity (OnCreate) and
RemoveEntityByServerGuid (OnRemove). Constructed in GameWindow under the
ACDREAM_USE_WB_FOUNDATION flag alongside LandblockSpawnAdapter. Sequencer
factory captures _dats + _animLoader at construction time; falls back to an
empty Setup + MotionTable via NullAnimLoader when dats are unavailable.
10 new tests: server-spawn routing, atlas-tier skip, palette decode pre-warm
(with and without surface overrides), OnRemove lifecycle, unknown-guid noop,
multi-entity isolation. All pass; 8 pre-existing failures unchanged.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Per-entity render state for the per-instance rendering tier
(server-spawned characters / creatures / equipped items). Holds:
- partGfxObjOverrides: Dictionary<int, ulong> — AnimPartChange swaps
(e.g. wielding a weapon replaces a hand-part's GfxObj).
- hiddenMask: ulong — HiddenParts bitmask. Bit i set hides part i.
- AnimationSequencer reference — N.4 doesn't touch the sequencer;
this just exposes it for the draw dispatcher.
Public API: HideParts / IsPartHidden / SetPartOverride /
TryGetPartOverride / ResolvePartGfxObj. Bounds-checked
(partIdx < 0 or >= 64 → IsPartHidden returns false).
Twelve tests covering the type, the AnimPartChange resolution helper,
and the HiddenParts bitmask edge cases (theories for 0b0/0b1/MSB/all-ones,
plus negative-index + out-of-range guards).
Consumed by Task 17's EntitySpawnAdapter (creates one per CreateObject)
and Task 22's WbDrawDispatcher (reads via per-part draw loop).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Week 2 ships: LandblockSpawnAdapter routes atlas-tier GfxObjs to WB
ref counts (Task 11/12), pending-spawn list integration verified
(Task 14), WbMeshAdapter.Tick drains the pipeline queues per frame
(added per Adjustment 3, fixes a real memory leak).
Task 13 (memory budget verification) is deferred: stress-test
revealed the FPS drop with flag-on isn't the queue leak we thought
— it's the dual-pipeline cost (background workers + duplicate GL
upload + duplicate I/O + legacy renderer still doing the same atlas
work). The savings only materialize in Task 22 when the dispatcher
short-circuits the legacy upload for atlas-tier content. Plan
Adjustment 3 documents this; no fix needed before Week 4 since
default-off is byte-identical to pre-N.4.
Next: Task 16 (Week 3) — AnimatedEntityState + per-instance path.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Without this, ObjectMeshManager.StagedMeshData and
OpenGLGraphicsDevice._glThreadQueue grow unbounded as background
workers prep mesh data + queue GL actions. Visual stress test of
flag-on at radius 7 showed real FPS drop and rising frame latency
from this leak.
Tick() drains both queues:
1. _graphicsDevice.ProcessGLQueue() applies pending GL state.
2. Loop _meshManager.StagedMeshData.TryDequeue -> UploadMeshData
to materialize VAO/VBO/IBO for each prepared mesh.
Wired into GameWindow's render loop before draw work begins.
No-op when adapter is uninitialized or disposed.
Pattern matches WB's reference ObjectRenderManagerBase.ProcessUploads
without the prioritization heuristics (we're not yet drawing the
results — Task 22's WbDrawDispatcher will add prioritization when
visual budget matters).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Verifies Task 12's GpuWorldState wiring preserves the pending-spawn
list mechanism:
1. Live entity parked before its landblock loads — pending count = 1,
adapter not called yet.
2. Landblock arrives with its own atlas-tier entity AND drains the
pending live entity. Adapter sees ONLY the atlas-tier GfxObj
(server-spawned drained entity is filtered by ServerGuid != 0).
3. Live entity arriving AFTER landblock load goes straight to flat
view; adapter is not re-invoked.
4. Landblock unload decrements match load increments.
Three integration tests confirm the existing pending-spawn drain
semantics work correctly with the new adapter, and per-instance-tier
entities (server-spawned) never leak into WB's atlas pipeline.
To exercise the adapter code path (which GpuWorldState gates on
WbFoundationFlag.IsEnabled) without requiring the env var set before
process startup, WbFoundationFlag gains an internal
ForTestsOnly_ForceEnable() method and AcDream.App exposes internals
to AcDream.Core.Tests via InternalsVisibleTo.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
GpuWorldState's constructor accepts an optional LandblockSpawnAdapter.
AddLandblock calls OnLandblockLoaded with the post-merge loaded record;
RemoveLandblock calls OnLandblockUnloaded with the landblock id at the
top of the method (before state mutation).
Both calls are gated behind WbFoundationFlag.IsEnabled — no behavioral
change with flag off (existing tests pass without modification).
GameWindow constructs the adapter under the flag and threads it into
GpuWorldState. With flag on, atlas-tier scenery now drives WB ref
counts; per-instance entities (ServerGuid != 0) are filtered out by
the adapter and don't reach WB.
Foundation for Task 13 (memory budget verification under stress).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Bridges LoadedLandblock load/unload events to IWbMeshAdapter ref counts.
Tier-aware by design: walks WorldEntity collection filtered by
ServerGuid == 0 (procedural / atlas-tier only). Server-spawned entities
are skipped — those will go through EntitySpawnAdapter (Task 17).
Per-landblock id-set snapshot ensures unload pairs 1:1 with load even
when underlying data is released. Duplicate-load idempotency for
defensive resilience to streaming-controller bugs.
Six tests: registers per unique id; dedups across entities; skips
server-spawned; unload matches load; unknown landblock no-ops;
duplicate load no-ops.
Wiring into GpuWorldState lands in Task 12.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Smoke test flag-on showed characters/NPCs disappearing along with
static scenery. Root cause: Task 9 routed all
InstancedMeshRenderer.EnsureUploaded calls through WB. But that
renderer is used for BOTH tiers in production — character per-part
spawn (line 2302, per-instance) AND streaming-loader spawns (lines
5137 + 5155, atlas).
The renderer is tier-blind by design. Tier-routing belongs at the
spawn-callback layer per the spec's data-flow section:
- LandblockSpawnAdapter (Task 11) calls IncrementRefCount per
unique GfxObj — atlas-tier only.
- EntitySpawnAdapter (Task 17) routes through per-instance path
via TextureCache.GetOrUploadWithPaletteOverride.
This commit removes the sentinel pattern + 4 sentinel-skip checks
from InstancedMeshRenderer. Kept the _wbMeshAdapter constructor
parameter (unused for now) so GameWindow's wire-up doesn't shift.
Kept all the real WB pipeline construction in WbMeshAdapter
(it's the substrate routing will use in Week 2).
Verified flag-on === flag-off post-revert.
Plan updated with Adjustment 2 explaining the discovery + correct
architectural placement for routing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
WbMeshAdapter now actually constructs the WB pipeline:
- OpenGLGraphicsDevice(gl, logger, DebugRenderSettings)
- DefaultDatReaderWriter(datDir) — opens its own file handles for now
(memory cost ~50-100MB of duplicate index caches, acceptable for
foundation work per plan Adjustment 1)
- ObjectMeshManager(graphicsDevice, dats, NullLogger)
InstancedMeshRenderer.EnsureUploaded routes through the adapter when
ACDREAM_USE_WB_FOUNDATION=1 is set; uses a WbManagedSentinel entry
in the local cache to mark "this GfxObj lives in WB now". CollectGroups
skips sentinel entries; both Draw passes skip them; Dispose skips them
(no GL resources to free — ObjectMeshManager owns those). Task 22's
WbDrawDispatcher will eventually draw WB-managed objects. With flag
off, behavior is byte-identical to before.
WbMeshAdapter constructor signature changed from (GL, DatCollection,
Logger) to (GL, string datDir, Logger). Updated tests to use
CreateUninitialized() for behavior tests and single null-GL guard test
for constructor validation. GameWindow updated to pass _datDir and to
wire _wbMeshAdapter into InstancedMeshRenderer.
AcDream.App.csproj gets direct ProjectReferences to WorldBuilder.Shared
and Chorizite.OpenGLSDLBackend — project refs are not transitive in
.NET, so AcDream.App must list them explicitly even though AcDream.Core
already references them.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Task 6 (dat-reader bridge) obsoleted: WB ships DefaultDatReaderWriter
which takes a dat-directory path and constructs all four databases
(Portal/HighRes/Language + CellRegions) internally. We can use it
directly instead of bridging our DatCollection. Adjustment 1 noted
in the plan; full bring-up deferred to Task 9.
Task 7: GameWindow constructs WbMeshAdapter when
ACDREAM_USE_WB_FOUNDATION=1 is set; pairs with Dispose. Field is
null when flag is off, so no behavioral effect on default-off path.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Stub adapter that validates constructor args and exposes the public
shape (IncrementRefCount / DecrementRefCount / GetRenderData / Dispose).
Real ObjectMeshManager init is deferred to Task 9 — for now methods
no-op so call sites can wire the adapter without behavioral effect.
IWbMeshAdapter interface enables mocking in subsequent tasks
(LandblockSpawnAdapter tests in Task 11 need it).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Mesh extraction (4 tests): quad output, double-sided via Stippling.Both,
double-sided via SidesType=Clockwise (AC's NoNeg-clear convention),
NoPos-only emission. Pins GfxObjMesh.Build's behavior.
Setup flatten (5 tests): identity (no frames), Default frame, Resting
beats Default, motion override beats Resting, DefaultScale per part.
Pins SetupMesh.Flatten's placement-frame fallback chain.
These run BEFORE substitution per N.1/N.3 pattern — they prove
equivalence, not test the substitution.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Holds Translucency / Luminosity / Diffuse / SurfOpacity / NeedsUvRepeat /
DisableFog keyed by (gfxObjId, surfaceIdx). Populated at extraction time,
queried by the draw dispatcher. ConcurrentDictionary because mesh
extraction happens on background workers.
No fork patches required — keeps WB's MeshBatchData pristine.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Creates the src/AcDream.App/Rendering/Wb/ folder and the static flag
gate that other call sites will import. Read once at static-init time.
Set ACDREAM_USE_WB_FOUNDATION=1 to enable WB foundation routing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
28-task plan covering 4 weeks of work organized as:
- Week 1 (Tasks 1-10): WB plumbing + atlas for static scenery + conformance
- Week 2 (Tasks 11-15): streaming integration + memory budget verification
- Week 3 (Tasks 16-21): per-instance customization + animation
- Week 4 (Tasks 22-28): full draw dispatcher + visual verification + ship
Living document — task checkboxes marked as commits land; adjustments
appended in-place rather than rewriting earlier tasks. Conformance
tests run before substitution per N.1/N.3 pattern. Behind
ACDREAM_USE_WB_FOUNDATION=1 feature flag during weeks 1-3.
CLAUDE.md updated with a "Currently in flight" pointer in the Roadmap
discipline section so future agents pick up the plan as authoritative
for rendering work.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adopting WB's ObjectMeshManager + TextureAtlasManager as acdream's
shared rendering infrastructure. Two-tier split: atlas for shared
procedural content (terrain props, scenery, buildings), per-instance
path for server-spawned customized entities (characters, creatures,
equipped items).
Animation handled by composing per-frame override matrices from our
existing AnimationSequencer with cached rest poses at draw time.
Cache stays valid; AnimationSequencer untouched.
Streaming-loader integration: ~200 LOC adapter shim wires landblock
load/unload to IncrementRefCount/DecrementRefCount; pending-spawn
list mechanism preserved.
Surface metadata (Translucency/Luminosity/Diffuse/SurfOpacity/
NeedsUvRepeat/DisableFog) preserved via side-table keyed by
(GfxObjId, surfaceIdx) — no fork patches required.
Three algorithmic conformance tests run before substitution per the
N.1/N.3 pattern. Visual verification at 5 named locations.
3-4 weeks, single shippable phase. Foundation enables N.5-N.9.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
After brainstorming N.4 we recognized WB's ObjectMeshManager isn't a
static helper — it's a 2070-line stateful asset pipeline (GPU resources,
atlas system, LRU + memory budget, background staging, bindless path).
Adopting it wholesale is the foundation that N.5/N.6/N.7 build on, not
a parallel substitution.
Updates:
- N.4 expanded to capture Option A scope: ObjectMeshManager + atlas +
per-instance customization layer + animation cache strategy + streaming
adapter. Estimate 3-4 weeks.
- N.5 estimate revised down (3-4w → 2-3w) since atlas + pipeline come
from N.4. Includes N.2's deferred terrain math substitution.
- N.6 estimate revised down (2-3w → 1-2w) — most substance lands in N.4.
- N.7 estimate revised down (2-3w → 1-2w) — naturally smaller on shared
infrastructure.
- N.8 estimate revised down (1.5-2w → ~1w) — C.1 already shipped most.
- N.10 noted as likely subsumed by N.4 (OpenGLGraphicsDevice arrives
with ObjectMeshManager).
- Calendar header revised to reflect the rebalanced totals.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Audit during N.3 follow-up uncovered that WB's TerrainUtils
CalculateSplitDirection uses a different math expression than
retail's FSplitNESW (the AC2D-cited polynomial 0x0CCAC033 etc that
our visual terrain mesh and physics already share). Substituting
TerrainSurface.SampleZ with WB's GetHeight in isolation would
re-introduce the triangle-Z hover bug from earlier work — physics
and visual mesh would pick different diagonals on disputed cells.
Updates:
- ISSUE #51 documents the divergence with file references and the
research that's needed when N.5 picks this up.
- Roadmap N.2 entry flags the dependency on N.5 and the reasoning
("not low-risk after all").
N.1's conformance proved slope-filtering equivalence (boolean
walkable verdict), not formula equivalence. The lesson is captured
in memory (feedback_wb_migration_formulas.md, not in-repo).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
acdream's SurfaceDecoder now delegates INDEX16 / P8 / A8R8G8B8 / R8G8B8 /
A8 to WorldBuilder's TextureHelpers.Fill*. R5G6B5 + A4R4G4B4 newly
handled (previously fell to magenta). X8R8G8B8, DXT1/3/5, and SolidColor
remain ours — no WB equivalent.
Key resolution: A8 had a behavioral divergence between our old code
(R=G=B=A=val always) and WB's split (FillA8 → R=G=B=255,A=val for
non-additive surfaces; FillA8Additive → R=G=B=A=val for additive).
Resolved by threading isAdditive through DecodeRenderSurface — terrain
alpha masks pass true (preserves shader .r blend-weight read), entity
surfaces pass surface.Type.HasFlag(SurfaceType.Additive).
9 conformance tests prove byte-identical equivalence per format before
substitution. Updated SurfaceDecoderTests for the new A8 split.
Visual verification at Holtburg passed — no texture regressions.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Walked Holtburg with the user; no texture regressions on terrain
blending, mesh textures, scenery clipmap edges, or building surfaces.
The deliberate A8 non-additive change (R=G=B=255,A=val) produced no
visible delta on entity textures. Phase N.3 is shipped end-to-end.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Roadmap: N.3 row added to shipped table; sub-phase block updated from
ahead-estimate to shipped summary. Document header date bumped.
Plan: docs/superpowers/plans/2026-05-08-phase-n3-texture-decode-via-wb.md
captures the audit + per-format substitution strategy + A8 isAdditive
divergence resolution that drove this phase.
No ISSUES.md update — visual verification at Holtburg is the remaining
gate; if the A8 non-additive change produces a visible delta on entity
textures, an issue gets filed there.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Decode_A8_ExpandsSingleByteToRgbaWithAlphaInAllChannels renamed to
Decode_A8_NonAdditive_ProducesWhitePlusAlpha with updated expectations
(R=G=B=255, A=val) matching the new default isAdditive:false WB semantics.
Decode_CustomLscapeAlpha_TreatedIdenticallyToA8 updated to the same
non-additive expectation (255,255,255,val).
New test Decode_A8_Additive_ReplicatesByteToAllChannels documents the
isAdditive:true path (R=G=B=A=val) used by TerrainAtlas alpha maps.
8 pre-existing failures unchanged. 883 pass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Nine tests covering INDEX16 (normal + clipmap), P8, A8R8G8B8, R8G8B8,
A8Additive (matches our current DecodeA8), A8 non-additive (documents
the divergence), R5G6B5, A4R4G4B4. All run before any substitution --
they prove equivalence, not test the substitution.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Roadmap updates after Phase N.1 ship:
- Marks N.0 (submodule + project refs setup) as ✓ SHIPPED with the
c8782c9 commit reference
- Updates N.2-N.9 effort estimates with realistic post-N.1 numbers
(originals were 1-2 days / 1 week / 2 weeks; realistic numbers
factor in conformance-test discovery, ACME-vs-Chorizite delta
hunts, and the visual-verification-then-revert cycle that ate
most of N.1's calendar time)
- Adds a "Lessons from N.1" subsection so future N phases benefit
from the rotation-bug-conformance-test pattern, the ACME divergence
insight, and the "whackamole = stop" rule
- Updates total calendar estimate to 3-4 months / 10-12 engineering
weeks for N.2-N.9 (was 2-3 months / 6-8 weeks)
New handoff doc at docs/research/2026-05-08-phase-n3-handoff.md
captures everything a fresh agent picking up N.3 (texture decoding)
needs: phase context, what to read first, suggested task decomposition,
watchouts (especially the ACME-divergence and conformance-test
lessons), and where to start.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds Phase N.1 to "Phases already shipped" table at top of roadmap,
updates the Phase N section to mark N.1 with checkmark SHIPPED status,
and files the known road-edge-tree cosmetic difference at landblock
0xA9B1 in ISSUES.md as issue #50 follow-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase N.1 step 8 (final code cleanup): now that ACDREAM_USE_WB_SCENERY
has been default-on (commit b84ecbd), remove the legacy in-line
algorithms so we don't accumulate dead-code drift.
Deleted:
- SceneryGenerator.UseWbScenery (feature flag)
- SceneryGenerator.IsOnRoad / DisplaceObject / RoadHalfWidth (legacy
ports — Generate used to call them)
- The legacy in-line implementation in Generate()
- SceneryGeneratorTests.DisplaceObject_* (test the deleted method)
- SceneryWbConformanceTests.cs entirely (purpose served — proved
equivalence pre-migration; would compare WB to WB after delete)
Renamed:
- GenerateViaWb -> GenerateInternal (it's the only path now)
Kept:
- Public IsRoadVertex predicate (small surface, useful)
- WbSceneryAdapter (consumed by GenerateInternal)
- All WbSceneryAdapterTests (still cover the adapter)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase N.1 step 7: flips ACDREAM_USE_WB_SCENERY to default-on after
visual verification at Holtburg confirmed Issue #49's previously
missing edge-vertex trees are still visible and rotation is correct.
A known cosmetic difference (the road-edge tree at landblock 0xA9B1)
remains. ACME WorldBuilder applies an additional per-vertex road
check that suppresses it; we tried adding it (commit e279c46) but
it over-suppressed in other landblocks (reverted in 677a726). Filed
as a follow-up issue in ISSUES.md (added in Task 8).
ACDREAM_USE_WB_SCENERY=0 still reverts to the legacy path. Task 8
will delete the legacy path entirely once a session passes without
visual regressions on default-on.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>