phase(N.4): SHIP — flag default-on + finalize plan + roadmap

Phase N.4 (Rendering Pipeline Foundation) ships. WbFoundationFlag
flips to default-on (== "1" → != "0"). WB's ObjectMeshManager is
now acdream's production mesh pipeline; WbDrawDispatcher is the
production draw path. Legacy InstancedMeshRenderer is retained as
ACDREAM_USE_WB_FOUNDATION=0 escape hatch until N.6 retires it.

Visual verification at Holtburg passed:
- Scenery (trees / rocks / fences / buildings) renders correctly
- Characters connected with full close-detail geometry (Issue #47
  preserved — GfxObjDegradeResolver path intact)
- FPS substantially improved by grouped instanced draws + per-entity
  AABB cull + opaque front-to-back sort + palette-hash memoization

Three high-value WB API gotchas surfaced during Task 26 visual
verification and are now documented in CLAUDE.md "WB integration
cribs" + plan Adjustments 7-9 + memory project_phase_n4_state.md:

1. ObjectMeshManager.IncrementRefCount only bumps a counter — does
   NOT trigger mesh loading. Call PrepareMeshDataAsync explicitly.
2. ObjectRenderBatch.SurfaceId is unset — read batch.Key.SurfaceId.
3. Modern rendering (GL 4.3 + bindless = every modern GPU) packs
   every mesh into ONE global VAO/VBO/IBO. Use
   glDrawElementsInstancedBaseVertex(BaseInstance) with FirstIndex +
   BaseVertex from the batch, not naive DrawElementsInstanced.

Plan doc flipped to Final state. Roadmap N.4 → Live ✓; N.5 rebranded
from "Terrain rendering" to "Modern rendering path" (bindless +
multi-draw indirect on top of N.4's foundation; terrain rendering
moves to N.5b). CLAUDE.md "Currently in flight" pointer updated to
N.5. New memory file project_phase_n4_state.md preserves the three
WB gotchas for cross-session continuity.

n4-verify*.log added to .gitignore.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-08 18:01:23 +02:00
parent 573526dae5
commit c44536451d
5 changed files with 178 additions and 58 deletions

1
.gitignore vendored
View file

@ -29,6 +29,7 @@ references/*
launch.log
launch-*.log
launch.utf8.log
n4-verify*.log
# ImGui auto-saved window/docking state (per-user, not source)
imgui.ini

View file

@ -25,19 +25,54 @@ single source of truth for how the client is structured. All work must
align with this document. When the architecture doc and reality diverge,
update one or the other — never leave them out of sync.
**WorldBuilder is acdream's rendering + dat-handling base** as of
2026-05-08. Before re-implementing any AC-specific rendering or
dat-handling algorithm, **read `docs/architecture/worldbuilder-inventory.md`
FIRST**. If WorldBuilder has it, port from WorldBuilder (or call into
our fork once wired up), not from retail decomp. WorldBuilder is
MIT-licensed, verified to render the world correctly, and uses the same
Silk.NET stack we target. Re-porting from retail decomp when WB already
has a tested port is how subtle bugs (the scenery edge-vertex bug, the
**WorldBuilder is acdream's rendering + dat-handling base, integrated
as of Phase N.4 ship (2026-05-08).** WB's `ObjectMeshManager` is the
production mesh pipeline; `WbMeshAdapter` is the seam; `WbDrawDispatcher`
is the production draw path (default-on, see `WbFoundationFlag`). Before
re-implementing any AC-specific rendering or dat-handling algorithm,
**read `docs/architecture/worldbuilder-inventory.md` FIRST**. If
WorldBuilder has it, port from WorldBuilder (or call into our fork via
the adapter), not from retail decomp. WorldBuilder is MIT-licensed,
verified to render the world correctly, and uses the same Silk.NET
stack we target. Re-porting from retail decomp when WB already has a
tested port is how subtle bugs (the scenery edge-vertex bug, the
triangle-Z bug) keep slipping in. Retail decomp remains the oracle for
network, physics, animation, movement, UI, plugin, audio, chat — see
the inventory doc's 🔴 list for the full scope of "we still write this
ourselves".
**WB integration cribs:**
- `src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs` — single seam over WB's
`ObjectMeshManager`. Owns the WB pipeline, drains its staged-upload
queue per frame via `Tick()`, populates `AcSurfaceMetadataTable` with
per-batch translucency / luminosity / fog metadata.
- `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs` — production draw
path. Groups all visible (entity, batch) pairs, single-uploads the
matrix buffer, fires one `glDrawElementsInstancedBaseVertexBaseInstance`
per group with `BaseInstance` pointing at the slice. Per-entity
frustum cull, opaque front-to-back sort, palette-hash memoization.
- `src/AcDream.App/Rendering/Wb/LandblockSpawnAdapter.cs` /
`EntitySpawnAdapter.cs` — bridge spawn lifecycle to WB ref-counts.
Atlas tier (procedural) goes via Landblock; per-instance tier
(server-spawned, palette/texture overrides) goes via Entity.
- `WbFoundationFlag` is default-on. `ACDREAM_USE_WB_FOUNDATION=0`
falls back to legacy `InstancedMeshRenderer` (kept as escape hatch
until N.6 fully retires it).
- **WB's modern rendering path** (GL 4.3 + bindless) packs every mesh
into a single global VAO/VBO/IBO. Each batch references its slice
via `FirstIndex` (offset into IBO) + `BaseVertex` (offset into VBO).
Honor those offsets when issuing draws — `DrawElementsInstanced`
with `indices=0` will draw every entity's first triangle from the
global mesh, not the per-batch range. (This is exactly the
exploded-character bug we hit during Task 26.)
- **WB's `ObjectRenderBatch.SurfaceId` is unset** — the actual surface
id lives in `batch.Key.SurfaceId` (the `TextureKey` struct).
- **`ObjectMeshManager.IncrementRefCount` only bumps a counter** — it
does NOT trigger mesh loading. You must explicitly call
`PrepareMeshDataAsync(id, isSetup)` to fire the background decode.
Result auto-enqueues to `_stagedMeshData` which `Tick()` drains.
`WbMeshAdapter` does this for you on first registration.
**Execution phases:** R1→R8 in the architecture doc. Each phase has clear
goals, test criteria, and builds on the previous. Don't skip phases.
@ -437,15 +472,19 @@ acdream's plan lives in two files committed to the repo:
acceptance criteria. Do not drift from the spec without explicit user
approval.
**Currently in flight: Phase N.4 — Rendering Pipeline Foundation.** Plan
at [`docs/superpowers/plans/2026-05-08-phase-n4-rendering-foundation.md`](docs/superpowers/plans/2026-05-08-phase-n4-rendering-foundation.md).
Spec at [`docs/superpowers/specs/2026-05-08-phase-n4-rendering-foundation-design.md`](docs/superpowers/specs/2026-05-08-phase-n4-rendering-foundation-design.md).
This is a 3-4 week phase adopting WB's `ObjectMeshManager` + `TextureAtlasManager`
as our shared rendering infrastructure. The plan is a **living document**
task checkboxes get marked as commits land, adjustments are appended in-place,
weeks 2-4 may be revised based on week 1 discoveries. Read the plan's "Plan
Living-Document Convention" section before contributing. After N.4 ships
this pointer is removed and the plan's status flips to "Final."
**Currently in flight: Phase N.5 — Modern Rendering Path.** Roadmap entry
at [`docs/plans/2026-04-11-roadmap.md`](docs/plans/2026-04-11-roadmap.md).
Builds on N.4's `WbDrawDispatcher` to adopt WB's modern rendering primitives:
bindless textures (eliminate `glBindTexture` calls) and
`glMultiDrawElementsIndirect` (one GL call per pass instead of one per
group). Together these target a 2-5× CPU win on draw-heavy scenes by
eliminating the remaining per-group state changes. Plan + spec to be
written when work begins.
**Phase N.4 (Rendering Pipeline Foundation) shipped 2026-05-08.** WB's
`ObjectMeshManager` is integrated and is the default rendering path
behind `ACDREAM_USE_WB_FOUNDATION` (default-on). Plan archived at
[`docs/superpowers/plans/2026-05-08-phase-n4-rendering-foundation.md`](docs/superpowers/plans/2026-05-08-phase-n4-rendering-foundation.md).
**Rules:**

View file

@ -1,6 +1,6 @@
# acdream — strategic roadmap
**Status:** Living document. Updated 2026-05-08 for Phase N.3 shipping + N.4-N.9 strategy revision (rendering rebuild on shared WB infrastructure rather than independent substitutions).
**Status:** Living document. Updated 2026-05-08 for Phase N.4 shipping (`WbMeshAdapter` + `WbDrawDispatcher` + `ACDREAM_USE_WB_FOUNDATION` default-on) + N.5 rebranded to "Modern rendering path" (bindless + multi-draw indirect on top of N.4's foundation).
**Purpose:** One source of truth for where the project is and where it's going. Every observed defect or missing feature has a named phase that owns it; when something looks wrong in-game, look here to find the phase that'll address it. Implementation details live in per-phase specs under `docs/superpowers/specs/`, not in this file.
---
@ -59,6 +59,7 @@
| C.1 | PES particle system + sky-pass refinements — retail-faithful `ParticleEmitterInfo` unpack with all 13 motion integrators (`Particle::Init`/`Update` ports of `0x0051c290`/`0x0051c930`), `PhysicsScriptRunner` with `CallPES` self-loop semantics, `ParticleHookSink` with `EmitterDied` cleanup, instanced billboard `ParticleRenderer` with material-derived blend (DAT emitters never default additive — pulled from particle GfxObj surface), global back-to-front sort, BC clipmap alpha-keying, AttachLocal `is_parent_local=1` live-parent follow via `UpdateEmitterAnchor`. Sky pass: `Translucent+ClipMap` → alpha-blend cloud sheet (matches `D3DPolyRender::SetSurface` `0x0059c4d0`), raw-`Additive` fog-skip (matches `0x0059c882`), per-keyframe `SkyObjectReplace` Translucency/Luminosity/MaxBright divide-by-100, bit `0x01` pre/post-scene split (matches `GameSky::CreateDeletePhysicsObjects` `0x005073c0`), Setup-backed (`0x020xxxxx`) sky objects via `SetupMesh.Flatten`, persistent GL sampler objects (Wrap + ClampToEdge) replace per-frame wrap-mode mutation (ported from WorldBuilder's `OpenGLGraphicsDevice`), post-scene Z-offset gated on `(Properties & 4) != 0 && (Properties & 8) == 0` per `GameSky::UpdatePosition` `0x00506dd0`. Sky-PES playback disabled by default (named-retail proves `GameSky` drops `pes_id`); `ACDREAM_ENABLE_SKY_PES=1` opens the experimental path. 1325 → 1331 tests. | Live ✓ |
| N.1 | WorldBuilder-backed scenery (Chorizite/WorldBuilder fork as submodule, SceneryHelpers + TerrainUtils replace our inline ports) | Live ✓ |
| N.3 | WorldBuilder-backed texture decode — `SurfaceDecoder` delegates INDEX16 / P8 / A8R8G8B8 / R8G8B8 / A8(+Additive) to `TextureHelpers.Fill*`; `isAdditive` threaded through (terrain alpha → `FillA8Additive`, non-additive entity surfaces → `FillA8`). R5G6B5 + A4R4G4B4 newly handled (previously magenta). X8R8G8B8, DXT1/3/5, SolidColor remain ours (no WB equivalent). 9 conformance tests prove byte-identical equivalence per format. | Live ✓ |
| N.4 | Rendering pipeline foundation — adopted WB's `ObjectMeshManager` as the production mesh pipeline behind `ACDREAM_USE_WB_FOUNDATION` (default-on). `WbMeshAdapter` is the single seam (owns `ObjectMeshManager`, drains the staged-upload queue per frame, populates `AcSurfaceMetadataTable` with per-batch translucency / luminosity / fog metadata). `WbDrawDispatcher` is the production draw path: groups all visible (entity, batch) pairs, single-uploads the matrix buffer, fires one `glDrawElementsInstancedBaseVertexBaseInstance` per group with `BaseInstance` slicing into the shared instance VBO. `LandblockSpawnAdapter` + `EntitySpawnAdapter` bridge spawn lifecycle to WB ref-counts (atlas tier vs per-instance). Perf wins shipped as part of N.4: per-entity frustum cull, opaque front-to-back sort, palette-hash memoization (compute once per entity, reuse across batches). Visual verification at Holtburg passed: scenery + connected characters with full close-detail geometry (Issue #47 regression resolved). Legacy `InstancedMeshRenderer` retained as `ACDREAM_USE_WB_FOUNDATION=0` escape hatch until N.6. | Live ✓ |
Plus polish that doesn't get its own phase number:
- FlyCamera default speed lowered + Shift-to-boost
@ -604,36 +605,54 @@ for our deletions/additions; merge upstream `master` periodically.
byte-identical equivalence per format** before substitution; updated
`SurfaceDecoderTests` to match the new A8 split semantics. Visual
verification at Holtburg passed 2026-05-08 — no texture regressions.
- **N.4 — Rendering pipeline foundation.** **Rebranded from "object
meshing" 2026-05-08 after brainstorm.** WB's `ObjectMeshManager` is
not a static helper — it's a 2070-line stateful asset pipeline that
owns GPU resources (VAO/VBO/IBO), an LRU cache + memory budget,
background staging, a shared texture atlas, and a bindless rendering
path. Adopting it wholesale is the foundation that N.5 + N.6 + N.7
build on. Concretely: (1) integrate `ObjectMeshManager` +
`TextureAtlasManager` as the shared infrastructure; (2) build a
per-instance customization layer that threads `CreaturePalette` /
`GfxObjRemapping` / `HiddenParts` / `TextureChanges` / `SubPalettes` /
`AnimPartChange` through WB's atlas keys; (3) extend WB's
`MeshBatchData` to carry our surface metadata (`Translucency` /
`Luminosity` / `Diffuse` / `SurfOpacity` / `NeedsUvRepeat` /
`DisableFog`) — likely a fork patch on the `acdream` branch; (4)
decide animation cache strategy (per-frame transform via uniform/SSBO
vs. cache invalidation); (5) adapter from our streaming loader's
Setup/Static spawn events to WB's `IncrementRefCount` lifecycle.
**Estimate: 3-4 weeks.** No visible change yet — visual verification =
"world looks identical to before." Foundation enables the next phases.
- **N.5 — Terrain rendering.** Wire WB's `TerrainRenderManager` +
`LandSurfaceManager` + `TerrainGeometryGenerator` onto the foundation
N.4 builds. Closes N.2's deferred terrain math substitution: visual
mesh and physics both switch to WB's `CalculateSplitDirection` +
`GetHeight` + `GetNormal` in lockstep, resolving ISSUE #51. **Estimate:
2-3 weeks** (was 3-4 — atlas + GPU pipeline already in place from N.4).
- **✓ SHIPPED — N.4 — Rendering pipeline foundation.** Shipped 2026-05-08.
WB's `ObjectMeshManager` is integrated as the production mesh pipeline
behind `ACDREAM_USE_WB_FOUNDATION=1` (default-on). The integration is
three pieces: `WbMeshAdapter` (single seam owning the WB pipeline,
drains the staged-upload queue per frame, populates
`AcSurfaceMetadataTable` for translucency / luminosity / fog),
`WbDrawDispatcher` (production draw path — groups all visible
(entity, batch) pairs, uploads matrices in a single `glBufferData`,
fires one `glDrawElementsInstancedBaseVertexBaseInstance` per group
with `BaseInstance` slicing the shared instance VBO), and the
`LandblockSpawnAdapter` + `EntitySpawnAdapter` bridge that wires our
streaming loader to WB's `IncrementRefCount` / `PrepareMeshDataAsync`
lifecycle (atlas tier vs per-instance customized).
Issue #47 (close-detail mesh) preserved; sky pass structurally
independent of the WB foundation. Perf wins shipped as part of N.4:
per-entity AABB frustum cull, opaque front-to-back sort, palette-hash
memoization. Legacy `InstancedMeshRenderer` retained as flag-off
fallback until N.6 fully retires it. Plan archived at
`docs/superpowers/plans/2026-05-08-phase-n4-rendering-foundation.md`.
- **N.5 — Modern rendering path.** **Rebranded from "Terrain rendering"
2026-05-08 after N.4 perf review.** N.4 left two big remaining wins
on the table that pair naturally: (1) bindless textures via
`GL_ARB_bindless_texture` (WB already populates
`ObjectRenderBatch.BindlessTextureHandle`; switch our shader to
consume per-instance handles, eliminate 100% of `glBindTexture`
calls), and (2) `glMultiDrawElementsIndirect` (one GL call per pass
instead of one per group; build a `DrawElementsIndirectCommand`
buffer, fire one indirect draw, the driver pulls everything). Both
require shader changes (same shader, in fact — bindless + indirect
are the same modern path WB uses internally). Together they target a
2-5× CPU win on draw-heavy scenes (Holtburg courtyard, Foundry,
dense dungeons). Also folds in: persistent-mapped instance VBO
(`glBufferStorage` + `MAP_PERSISTENT_BIT | MAP_COHERENT_BIT` + ring
buffer + sync) and texture pre-warm at landblock load (smooths
streaming-boundary hitches). **Estimate: 2-3 weeks.**
- **N.5b — Terrain rendering on N.5 path.** Wire WB's
`TerrainRenderManager` + `LandSurfaceManager` + `TerrainGeometryGenerator`
onto the modern rendering path. Closes N.2's deferred terrain math
substitution: visual mesh and physics both switch to WB's
`CalculateSplitDirection` + `GetHeight` + `GetNormal` in lockstep,
resolving ISSUE #51. **Estimate: 1-2 weeks** (was 2-3 — modern path
primitives already in place from N.5).
- **N.6 — Static objects rendering.** Wire WB's
`StaticObjectRenderManager` onto N.4's foundation; replace our
`StaticMeshRenderer` + `InstancedMeshRenderer`. Mostly draw
orchestration at this point — most of the substance landed in N.4.
**Estimate: 1-2 weeks** (was 2-3).
`StaticObjectRenderManager` onto the modern rendering path; **fully
delete** legacy `StaticMeshRenderer` + `InstancedMeshRenderer` (they
remain as `ACDREAM_USE_WB_FOUNDATION=0` escape hatches through N.5).
Mostly draw orchestration at this point — most of the substance
landed in N.4 + N.5. **Estimate: 1-2 weeks** (was 2-3).
- **N.7 — EnvCells / dungeons.** Replace EnvCell rendering with WB's
`EnvCellRenderManager` + `PortalRenderManager` on top of N.4's
foundation. **Estimate: 1-2 weeks** (was 2-3 — naturally smaller now

View file

@ -64,7 +64,12 @@ This plan is the **execution source of truth** for N.4. It is updated as tasks l
- If a downstream task changes shape because of an earlier task's outcome, append the changes to the downstream task in-place rather than scattering deltas.
- Final commit for the phase updates this header note from "Living document — work in progress" to "Final state at <date> — phase shipped (merge `<sha>`)."
Status: **Living document — work in progress, started 2026-05-08.**
Status: **Final state at 2026-05-08 — phase shipped.** All tasks
complete; `ACDREAM_USE_WB_FOUNDATION` flipped default-on. Visual
verification at Holtburg passed. Three bugs surfaced + resolved during
Task 26 are documented as Adjustments 7-9 below and as gotchas in
CLAUDE.md. Followup work moves to N.5 (modern rendering path: bindless
+ multi-draw indirect).
**Progress (2026-05-08):** Weeks 1 + 2 + 3 ✅ COMPLETE. WB pipeline running flag-on (constructed + ref-counted + per-frame Tick draining its queues). Per-instance tier wired (`EntitySpawnAdapter` routes server-spawned entities through existing `TextureCache.GetOrUploadWithPaletteOverride` path; per-entity `AnimatedEntityState` accumulates AnimPartChange + HiddenParts data, ready for the dispatcher). Five architectural adjustments documented: 1 (DefaultDatReaderWriter discovery), 2 (renderer is tier-blind), 3 (FPS regression = dual-pipeline cost; resolves at Task 22), 4 (WorldEntity missing HiddenPartsMask + AnimPartChanges fields, plumbing deferred), 5 (Task 20 is structural — same function called both paths). Build green, 947 tests pass, 8 pre-existing failures only.
@ -93,11 +98,14 @@ Status: **Living document — work in progress, started 2026-05-08.**
| 20 — Per-instance decode conformance | ✅ structural (Adj. 5) | (no test file) |
| 21 — Week 3 wrap-up | ✅ | (this commit) |
| 22+23 — WbDrawDispatcher + side-table population | ✅ | `01cff41` |
| 22+23 fixup — load triggers + SurfaceId source | ✅ | `943652d` |
| 22+23 perf — FirstIndex/BaseVertex + #47 + grouped instanced | ✅ | `7b41efc` |
| 22+23 perf 1-4 — drop dead lookup, sort, cull, hash memo | ✅ | `573526d` |
| 24 — Sky-pass preservation check | ✅ structural (independent) | `5df9135` |
| 25 — Component micro-tests round-out | ✅ all spec tests covered | — |
| 26 — Visual verification + flag default-on | pending | — |
| 27 — Delete legacy code paths | pending | — |
| 28 — Update memory + ISSUES + finalize plan | pending | — |
| 26 — Visual verification + flag default-on | ✅ | (this commit) |
| 27 — Delete legacy code paths | ⚠️ deferred to N.6 (legacy retained as flag-off escape hatch) | — |
| 28 — Update memory + ISSUES + finalize plan | ✅ | (this commit) |
---
@ -1016,6 +1024,52 @@ plumbing decision to Task 22. Two options:
- GameWindow's CreateObject handler builds the `PartOverride[]` from the
server-sent `AnimPartChanges` list.
### Adjustment 7 (2026-05-08, Task 26 visual verification): IncrementRefCount doesn't trigger mesh load
**Discovered when** Task 26's first launch showed only terrain — zero entities visible. Diagnostic counters (added the same launch via `ACDREAM_WB_DIAG=1`) showed `entitiesSeen=14M, entitiesDrawn=14M, drawsIssued=0` — every entity was visited but no draws were issued because `TryGetRenderData` returned null for everything.
**Root cause.** WB's `ObjectMeshManager.IncrementRefCount(id)` only bumps a usage counter — it does NOT trigger mesh loading. Loading is fired separately by `PrepareMeshDataAsync(id, isSetup)`, which dispatches to a background worker pool; the result auto-enqueues to `_stagedMeshData` (line 510 of `ObjectMeshManager.cs`) which our existing `WbMeshAdapter.Tick()` already drains.
The N.4 plan assumed `IncrementRefCount` was lifecycle-aware (it isn't). `LandblockSpawnAdapter` and the original `EntitySpawnAdapter` both called `IncrementRefCount` and stopped — meshes never loaded.
**Fix** (commit `943652d`):
- `WbMeshAdapter.IncrementRefCount` now calls `_meshManager.PrepareMeshDataAsync(id, isSetup: false)` on first registration. `isSetup: false` is correct because acdream's MeshRefs already carry expanded per-part GfxObj ids (0x01XXXXXX) — WB's Setup-expansion path is unused.
- `EntitySpawnAdapter` gained an optional `IWbMeshAdapter` constructor parameter. Per-instance entities (server-spawned characters / NPCs) had been entirely skipped by `LandblockSpawnAdapter` (which filters `ServerGuid != 0`); their GfxObjs now get registered + loaded at `OnCreate` and decremented at `OnRemove`. Includes both `MeshRefs.GfxObjId` AND `PartOverrides.GfxObjId` so weapon/clothing/helmet swaps load too.
**Lesson preserved.** Future cross-session work touching WB: **`IncrementRefCount` is not lifecycle-aware. Call `PrepareMeshDataAsync` to trigger loads.** Documented in CLAUDE.md "WB integration cribs" section.
### Adjustment 8 (2026-05-08, Task 26 visual verification): SurfaceId lives in batch.Key.SurfaceId
**Discovered when** the second Task 26 launch showed `drawsIssued=4.8M/5s` (draws ARE happening) but ZERO entities visible. Inspection of `ResolveTexture` showed it was returning early because `batch.SurfaceId == 0` for every batch.
**Root cause.** WB's `ObjectMeshManager.UploadGfxObjMeshData` (line 1746 of `ObjectMeshManager.cs`) constructs `ObjectRenderBatch` and sets `Key = batch.Key` (a `TextureAtlasManager.TextureKey` struct that contains a `SurfaceId` field) but does NOT populate the top-level `ObjectRenderBatch.SurfaceId` property. That property exists on the type but stays at its default 0.
**Fix** (commit `943652d`): `WbDrawDispatcher.ResolveTexture` reads `batch.Key.SurfaceId` instead of `batch.SurfaceId`. Also handles the dummy `0xFFFFFFFF` case used by WB's environment edge wireframes.
**Lesson preserved.** **`ObjectRenderBatch.SurfaceId` is not populated by WB. Read `batch.Key.SurfaceId`.** Documented in CLAUDE.md.
### Adjustment 9 (2026-05-08, Task 26 visual verification): Modern rendering uses one global VAO/VBO/IBO
**Discovered when** the third Task 26 launch finally showed real draws — but as "exploded" character body parts scattered around the world with no scenery. Visual was completely broken even though the GL pipeline was clearly issuing draws and binding textures correctly.
**Root cause.** WB's `ObjectMeshManager` has two rendering paths controlled by `_useModernRendering = HasOpenGL43 && HasBindless`. On any modern GPU (which is everything we target), modern is true and ALL meshes share a single `GlobalMeshBuffer` — one VAO, one VBO, one IBO. Each batch's `IBO` field points to that ONE global IBO; the batch's actual slice is identified by `FirstIndex` (offset into IBO, in indices) and `BaseVertex` (offset into VBO, in vertices). The dispatcher was issuing `glDrawElementsInstanced` with `indices=0` and no base vertex — so every entity drew the same first triangle of the global mesh starting at offset 0. That produced exactly the "exploded parts at scattered positions" symptom.
**Fix** (commit `7b41efc`): switch to `glDrawElementsInstancedBaseVertexBaseInstance`, pass `(void*)(batch.FirstIndex * sizeof(ushort))` as the indices argument, pass `(int)batch.BaseVertex` as base vertex. The grouped-instanced refactor in the same commit additionally uses `BaseInstance` to slice into the shared instance VBO per group.
**Bonus discovery:** because all meshes share one VAO under modern rendering, the dispatcher only needs to bind the VAO ONCE per frame (not per draw). Every draw goes to the same VAO. Significant CPU savings.
**Lesson preserved.** **WB's modern rendering path packs everything into one global VAO/VBO/IBO. Honor `FirstIndex` and `BaseVertex`.** Documented in CLAUDE.md.
### Adjustment 10 (2026-05-08, Task 26 visual verification): AnimatedEntityState overrides clobber Issue #47 close-detail mesh
**Discovered when** Task 26's fourth launch showed scenery + connected characters — but characters were "bulky and missing detail" compared to the legacy renderer. Recognized as a re-occurrence of Issue #47 (resolved 2026-05-06 via `GfxObjDegradeResolver`).
**Root cause.** Adjustment 6 stored AnimPartChanges on `WorldEntity.PartOverrides` using the raw `NewModelId` from the network packet — without applying `GfxObjDegradeResolver`. GameWindow's spawn path correctly resolves base GfxObjs (e.g., upper arm `0x01000055`, 14 verts/17 polys) to their close-detail equivalents (`0x01001795`, 32 verts/60 polys) and bakes the result into `MeshRefs`. But `WbDrawDispatcher` then called `animState.ResolvePartGfxObj(partIdx, meshRefGfxObjId)` which returned the raw (low-detail) override from `PartOverrides`, undoing the degrade.
**Fix** (commit `7b41efc`): the dispatcher trusts `MeshRefs` as the source of truth and does NOT re-apply `animState.ResolvePartGfxObj` at draw time. `AnimatedEntityState` overrides become relevant only for hot-swap appearance updates (0xF625 `ObjDescEvent`) which today rebuild MeshRefs anyway. `IsPartHidden` similarly skipped — `HiddenPartsMask` is never populated by spawn code (legacy renderer also doesn't check it).
**Lesson preserved.** **`MeshRefs` is the source of truth at draw time** — GameWindow's spawn path bakes overrides + degrades into it. Don't re-apply overrides downstream.
### Task 6 (original — kept for history)
**Files:**

View file

@ -7,18 +7,25 @@ namespace AcDream.App.Rendering.Wb;
/// free at hot-path cadence).
///
/// <para>
/// Set <c>ACDREAM_USE_WB_FOUNDATION=1</c> to route static-scenery + atlas
/// content through WB's <c>ObjectMeshManager</c>; per-instance customized
/// content (server <c>CreateObject</c> entities) takes the existing
/// <see cref="TextureCache.GetOrUploadWithPaletteOverride"/> path either
/// way. Flag becomes default-on at end of Phase N.4 after visual
/// verification.
/// <b>Default-on as of Phase N.4 ship (2026-05-08).</b> The WB foundation
/// (<c>WbMeshAdapter</c> + <c>WbDrawDispatcher</c>) is the production
/// rendering path. Set <c>ACDREAM_USE_WB_FOUNDATION=0</c> to fall back
/// to the legacy <c>InstancedMeshRenderer</c> path — kept as an escape
/// hatch until N.6 fully replaces it.
/// </para>
///
/// <para>
/// Per-instance customized content (server <c>CreateObject</c> entities
/// with palette / texture overrides) routes through
/// <see cref="TextureCache.GetOrUploadWithPaletteOverride"/> regardless
/// of the flag — the flag controls which DRAW path consumes those
/// textures.
/// </para>
/// </summary>
public static class WbFoundationFlag
{
private static bool _isEnabled =
System.Environment.GetEnvironmentVariable("ACDREAM_USE_WB_FOUNDATION") == "1";
System.Environment.GetEnvironmentVariable("ACDREAM_USE_WB_FOUNDATION") != "0";
public static bool IsEnabled => _isEnabled;