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:
parent
573526dae5
commit
c44536451d
5 changed files with 178 additions and 58 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -29,6 +29,7 @@ references/*
|
||||||
launch.log
|
launch.log
|
||||||
launch-*.log
|
launch-*.log
|
||||||
launch.utf8.log
|
launch.utf8.log
|
||||||
|
n4-verify*.log
|
||||||
|
|
||||||
# ImGui auto-saved window/docking state (per-user, not source)
|
# ImGui auto-saved window/docking state (per-user, not source)
|
||||||
imgui.ini
|
imgui.ini
|
||||||
|
|
|
||||||
73
CLAUDE.md
73
CLAUDE.md
|
|
@ -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,
|
align with this document. When the architecture doc and reality diverge,
|
||||||
update one or the other — never leave them out of sync.
|
update one or the other — never leave them out of sync.
|
||||||
|
|
||||||
**WorldBuilder is acdream's rendering + dat-handling base** as of
|
**WorldBuilder is acdream's rendering + dat-handling base, integrated
|
||||||
2026-05-08. Before re-implementing any AC-specific rendering or
|
as of Phase N.4 ship (2026-05-08).** WB's `ObjectMeshManager` is the
|
||||||
dat-handling algorithm, **read `docs/architecture/worldbuilder-inventory.md`
|
production mesh pipeline; `WbMeshAdapter` is the seam; `WbDrawDispatcher`
|
||||||
FIRST**. If WorldBuilder has it, port from WorldBuilder (or call into
|
is the production draw path (default-on, see `WbFoundationFlag`). Before
|
||||||
our fork once wired up), not from retail decomp. WorldBuilder is
|
re-implementing any AC-specific rendering or dat-handling algorithm,
|
||||||
MIT-licensed, verified to render the world correctly, and uses the same
|
**read `docs/architecture/worldbuilder-inventory.md` FIRST**. If
|
||||||
Silk.NET stack we target. Re-porting from retail decomp when WB already
|
WorldBuilder has it, port from WorldBuilder (or call into our fork via
|
||||||
has a tested port is how subtle bugs (the scenery edge-vertex bug, the
|
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
|
triangle-Z bug) keep slipping in. Retail decomp remains the oracle for
|
||||||
network, physics, animation, movement, UI, plugin, audio, chat — see
|
network, physics, animation, movement, UI, plugin, audio, chat — see
|
||||||
the inventory doc's 🔴 list for the full scope of "we still write this
|
the inventory doc's 🔴 list for the full scope of "we still write this
|
||||||
ourselves".
|
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
|
**Execution phases:** R1→R8 in the architecture doc. Each phase has clear
|
||||||
goals, test criteria, and builds on the previous. Don't skip phases.
|
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
|
acceptance criteria. Do not drift from the spec without explicit user
|
||||||
approval.
|
approval.
|
||||||
|
|
||||||
**Currently in flight: Phase N.4 — Rendering Pipeline Foundation.** Plan
|
**Currently in flight: Phase N.5 — Modern Rendering Path.** Roadmap entry
|
||||||
at [`docs/superpowers/plans/2026-05-08-phase-n4-rendering-foundation.md`](docs/superpowers/plans/2026-05-08-phase-n4-rendering-foundation.md).
|
at [`docs/plans/2026-04-11-roadmap.md`](docs/plans/2026-04-11-roadmap.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).
|
Builds on N.4's `WbDrawDispatcher` to adopt WB's modern rendering primitives:
|
||||||
This is a 3-4 week phase adopting WB's `ObjectMeshManager` + `TextureAtlasManager`
|
bindless textures (eliminate `glBindTexture` calls) and
|
||||||
as our shared rendering infrastructure. The plan is a **living document** —
|
`glMultiDrawElementsIndirect` (one GL call per pass instead of one per
|
||||||
task checkboxes get marked as commits land, adjustments are appended in-place,
|
group). Together these target a 2-5× CPU win on draw-heavy scenes by
|
||||||
weeks 2-4 may be revised based on week 1 discoveries. Read the plan's "Plan
|
eliminating the remaining per-group state changes. Plan + spec to be
|
||||||
Living-Document Convention" section before contributing. After N.4 ships
|
written when work begins.
|
||||||
this pointer is removed and the plan's status flips to "Final."
|
|
||||||
|
**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:**
|
**Rules:**
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# acdream — strategic roadmap
|
# 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.
|
**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 ✓ |
|
| 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.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.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:
|
Plus polish that doesn't get its own phase number:
|
||||||
- FlyCamera default speed lowered + Shift-to-boost
|
- 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
|
byte-identical equivalence per format** before substitution; updated
|
||||||
`SurfaceDecoderTests` to match the new A8 split semantics. Visual
|
`SurfaceDecoderTests` to match the new A8 split semantics. Visual
|
||||||
verification at Holtburg passed 2026-05-08 — no texture regressions.
|
verification at Holtburg passed 2026-05-08 — no texture regressions.
|
||||||
- **N.4 — Rendering pipeline foundation.** **Rebranded from "object
|
- **✓ SHIPPED — N.4 — Rendering pipeline foundation.** Shipped 2026-05-08.
|
||||||
meshing" 2026-05-08 after brainstorm.** WB's `ObjectMeshManager` is
|
WB's `ObjectMeshManager` is integrated as the production mesh pipeline
|
||||||
not a static helper — it's a 2070-line stateful asset pipeline that
|
behind `ACDREAM_USE_WB_FOUNDATION=1` (default-on). The integration is
|
||||||
owns GPU resources (VAO/VBO/IBO), an LRU cache + memory budget,
|
three pieces: `WbMeshAdapter` (single seam owning the WB pipeline,
|
||||||
background staging, a shared texture atlas, and a bindless rendering
|
drains the staged-upload queue per frame, populates
|
||||||
path. Adopting it wholesale is the foundation that N.5 + N.6 + N.7
|
`AcSurfaceMetadataTable` for translucency / luminosity / fog),
|
||||||
build on. Concretely: (1) integrate `ObjectMeshManager` +
|
`WbDrawDispatcher` (production draw path — groups all visible
|
||||||
`TextureAtlasManager` as the shared infrastructure; (2) build a
|
(entity, batch) pairs, uploads matrices in a single `glBufferData`,
|
||||||
per-instance customization layer that threads `CreaturePalette` /
|
fires one `glDrawElementsInstancedBaseVertexBaseInstance` per group
|
||||||
`GfxObjRemapping` / `HiddenParts` / `TextureChanges` / `SubPalettes` /
|
with `BaseInstance` slicing the shared instance VBO), and the
|
||||||
`AnimPartChange` through WB's atlas keys; (3) extend WB's
|
`LandblockSpawnAdapter` + `EntitySpawnAdapter` bridge that wires our
|
||||||
`MeshBatchData` to carry our surface metadata (`Translucency` /
|
streaming loader to WB's `IncrementRefCount` / `PrepareMeshDataAsync`
|
||||||
`Luminosity` / `Diffuse` / `SurfOpacity` / `NeedsUvRepeat` /
|
lifecycle (atlas tier vs per-instance customized).
|
||||||
`DisableFog`) — likely a fork patch on the `acdream` branch; (4)
|
Issue #47 (close-detail mesh) preserved; sky pass structurally
|
||||||
decide animation cache strategy (per-frame transform via uniform/SSBO
|
independent of the WB foundation. Perf wins shipped as part of N.4:
|
||||||
vs. cache invalidation); (5) adapter from our streaming loader's
|
per-entity AABB frustum cull, opaque front-to-back sort, palette-hash
|
||||||
Setup/Static spawn events to WB's `IncrementRefCount` lifecycle.
|
memoization. Legacy `InstancedMeshRenderer` retained as flag-off
|
||||||
**Estimate: 3-4 weeks.** No visible change yet — visual verification =
|
fallback until N.6 fully retires it. Plan archived at
|
||||||
"world looks identical to before." Foundation enables the next phases.
|
`docs/superpowers/plans/2026-05-08-phase-n4-rendering-foundation.md`.
|
||||||
- **N.5 — Terrain rendering.** Wire WB's `TerrainRenderManager` +
|
- **N.5 — Modern rendering path.** **Rebranded from "Terrain rendering"
|
||||||
`LandSurfaceManager` + `TerrainGeometryGenerator` onto the foundation
|
2026-05-08 after N.4 perf review.** N.4 left two big remaining wins
|
||||||
N.4 builds. Closes N.2's deferred terrain math substitution: visual
|
on the table that pair naturally: (1) bindless textures via
|
||||||
mesh and physics both switch to WB's `CalculateSplitDirection` +
|
`GL_ARB_bindless_texture` (WB already populates
|
||||||
`GetHeight` + `GetNormal` in lockstep, resolving ISSUE #51. **Estimate:
|
`ObjectRenderBatch.BindlessTextureHandle`; switch our shader to
|
||||||
2-3 weeks** (was 3-4 — atlas + GPU pipeline already in place from N.4).
|
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
|
- **N.6 — Static objects rendering.** Wire WB's
|
||||||
`StaticObjectRenderManager` onto N.4's foundation; replace our
|
`StaticObjectRenderManager` onto the modern rendering path; **fully
|
||||||
`StaticMeshRenderer` + `InstancedMeshRenderer`. Mostly draw
|
delete** legacy `StaticMeshRenderer` + `InstancedMeshRenderer` (they
|
||||||
orchestration at this point — most of the substance landed in N.4.
|
remain as `ACDREAM_USE_WB_FOUNDATION=0` escape hatches through N.5).
|
||||||
**Estimate: 1-2 weeks** (was 2-3).
|
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
|
- **N.7 — EnvCells / dungeons.** Replace EnvCell rendering with WB's
|
||||||
`EnvCellRenderManager` + `PortalRenderManager` on top of N.4's
|
`EnvCellRenderManager` + `PortalRenderManager` on top of N.4's
|
||||||
foundation. **Estimate: 1-2 weeks** (was 2-3 — naturally smaller now
|
foundation. **Estimate: 1-2 weeks** (was 2-3 — naturally smaller now
|
||||||
|
|
|
||||||
|
|
@ -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.
|
- 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>`)."
|
- 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.
|
**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) |
|
| 20 — Per-instance decode conformance | ✅ structural (Adj. 5) | (no test file) |
|
||||||
| 21 — Week 3 wrap-up | ✅ | (this commit) |
|
| 21 — Week 3 wrap-up | ✅ | (this commit) |
|
||||||
| 22+23 — WbDrawDispatcher + side-table population | ✅ | `01cff41` |
|
| 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` |
|
| 24 — Sky-pass preservation check | ✅ structural (independent) | `5df9135` |
|
||||||
| 25 — Component micro-tests round-out | ✅ all spec tests covered | — |
|
| 25 — Component micro-tests round-out | ✅ all spec tests covered | — |
|
||||||
| 26 — Visual verification + flag default-on | pending | — |
|
| 26 — Visual verification + flag default-on | ✅ | (this commit) |
|
||||||
| 27 — Delete legacy code paths | pending | — |
|
| 27 — Delete legacy code paths | ⚠️ deferred to N.6 (legacy retained as flag-off escape hatch) | — |
|
||||||
| 28 — Update memory + ISSUES + finalize plan | pending | — |
|
| 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
|
- GameWindow's CreateObject handler builds the `PartOverride[]` from the
|
||||||
server-sent `AnimPartChanges` list.
|
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)
|
### Task 6 (original — kept for history)
|
||||||
|
|
||||||
**Files:**
|
**Files:**
|
||||||
|
|
|
||||||
|
|
@ -7,18 +7,25 @@ namespace AcDream.App.Rendering.Wb;
|
||||||
/// free at hot-path cadence).
|
/// free at hot-path cadence).
|
||||||
///
|
///
|
||||||
/// <para>
|
/// <para>
|
||||||
/// Set <c>ACDREAM_USE_WB_FOUNDATION=1</c> to route static-scenery + atlas
|
/// <b>Default-on as of Phase N.4 ship (2026-05-08).</b> The WB foundation
|
||||||
/// content through WB's <c>ObjectMeshManager</c>; per-instance customized
|
/// (<c>WbMeshAdapter</c> + <c>WbDrawDispatcher</c>) is the production
|
||||||
/// content (server <c>CreateObject</c> entities) takes the existing
|
/// rendering path. Set <c>ACDREAM_USE_WB_FOUNDATION=0</c> to fall back
|
||||||
/// <see cref="TextureCache.GetOrUploadWithPaletteOverride"/> path either
|
/// to the legacy <c>InstancedMeshRenderer</c> path — kept as an escape
|
||||||
/// way. Flag becomes default-on at end of Phase N.4 after visual
|
/// hatch until N.6 fully replaces it.
|
||||||
/// verification.
|
/// </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>
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class WbFoundationFlag
|
public static class WbFoundationFlag
|
||||||
{
|
{
|
||||||
private static bool _isEnabled =
|
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;
|
public static bool IsEnabled => _isEnabled;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue