phase(N.5): retirement amendment — InstancedMeshRenderer + StaticMeshRenderer + WbFoundationFlag deleted
Final cross-cutting review of N.5 found that Task 15's deletion of mesh_instanced.vert/.frag left InstancedMeshRenderer orphaned — ACDREAM_USE_WB_FOUNDATION=0 silently rendered terrain+sky only with no entities. The SHIP commit's "[x] ACDREAM_USE_WB_FOUNDATION=0 still works" claim was inaccurate. Resolution: formal retirement of the legacy renderer path within N.5 instead of deferring to N.6. Deleted: - src/AcDream.App/Rendering/InstancedMeshRenderer.cs - src/AcDream.App/Rendering/StaticMeshRenderer.cs - src/AcDream.App/Rendering/Wb/WbFoundationFlag.cs GameWindow simplified — capability detection is unconditional, missing bindless throws NotSupportedException with a clear message at startup. WbDrawDispatcher + mesh_modern shader load are mandatory after init. No escape hatch. GpuWorldState simplified — WbFoundationFlag.IsEnabled guards on AddLandblock/RemoveLandblock removed; adapter calls are unconditional when the adapter is non-null. PendingSpawnIntegrationTests updated — WbFoundationFlag.ForTestsOnly_ForceEnable static ctor removed (flag is gone; adapter calls are unconditional). The ApplyLoadedTerrain physics-data loop was also simplified: the EnsureUploaded sub-loop that fed InstancedMeshRenderer is gone; _pendingCellMeshes is now explicitly cleared to prevent unbounded accumulation (the worker thread still populates it, but WB handles EnvCell geometry through its own pipeline). Spec §2 Decision 5 + §10 Out-of-Scope updated. Plan ship-amendment section added. Roadmap updated (N.5 ships with retirement; N.6 scope narrowed to perf-only). CLAUDE.md "WB integration cribs" updated. Perf baseline doc updated. WbDrawDispatcher class summary docstring corrected to describe the as-shipped SSBO + multi-draw-indirect path. ISSUES.md #51 updated (terrain not in N.5 scope; deferred to N.7). Bindless support is now a hard requirement. Modern desktop GPUs universally expose GL_ARB_bindless_texture + GL_ARB_shader_draw_parameters; if a user hits the NotSupportedException, that's a real bug report worth investigating, not a silent fallback. Build: 0 errors, 0 warnings. Tests: 71/71 (Wb+MatrixComposition+TextureCacheBindless filter). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
55ecec683f
commit
dcae2b6b94
13 changed files with 211 additions and 1140 deletions
30
CLAUDE.md
30
CLAUDE.md
|
|
@ -55,9 +55,11 @@ ourselves".
|
|||
`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).
|
||||
- **Modern path is mandatory as of N.5 ship amendment (2026-05-08).**
|
||||
`WbFoundationFlag`, `InstancedMeshRenderer`, and `StaticMeshRenderer`
|
||||
are deleted. Missing `GL_ARB_bindless_texture` or
|
||||
`GL_ARB_shader_draw_parameters` throws `NotSupportedException` at
|
||||
startup. There is no legacy fallback.
|
||||
- **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).
|
||||
|
|
@ -500,21 +502,25 @@ 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.6 — Retire legacy renderers + perf polish.**
|
||||
**Currently in flight: Phase N.6 — Perf polish.**
|
||||
Roadmap entry at [`docs/plans/2026-04-11-roadmap.md`](docs/plans/2026-04-11-roadmap.md).
|
||||
Builds on N.5. Retires `InstancedMeshRenderer` + `StaticMeshRenderer` entirely.
|
||||
Optional candidates: WB atlas adoption, persistent-mapped buffers, GPU-side
|
||||
culling via compute pre-pass, GL_TIME_ELAPSED query double-buffering, direct
|
||||
N.4 vs N.5 perf measurement. Plan + spec written when work begins.
|
||||
Builds on N.5. Legacy renderers (`InstancedMeshRenderer`, `StaticMeshRenderer`,
|
||||
`WbFoundationFlag`) were retired in the N.5 ship amendment — N.6 scope is
|
||||
perf-only: WB atlas adoption, persistent-mapped buffers, GPU-side culling,
|
||||
GL_TIME_ELAPSED query double-buffering, direct N.4 vs N.5 perf measurement,
|
||||
legacy `Texture2D`/`sampler2D` TextureCache path retirement (Sky/Terrain/Debug).
|
||||
Plan + spec written when work begins.
|
||||
|
||||
**Phase N.5 (Modern Rendering Path) shipped 2026-05-08.** `WbDrawDispatcher`
|
||||
**Phase N.5 (Modern Rendering Path) shipped + amended 2026-05-08.** `WbDrawDispatcher`
|
||||
on bindless textures + `glMultiDrawElementsIndirect`. CPU dispatcher 1.23ms/frame
|
||||
at Holtburg (~810 fps). Plan archived at
|
||||
at Holtburg (~810 fps). **Ship amendment:** `InstancedMeshRenderer`,
|
||||
`StaticMeshRenderer`, `WbFoundationFlag` deleted in same phase — modern path is
|
||||
mandatory; missing bindless throws at startup. Plan archived at
|
||||
[`docs/superpowers/plans/2026-05-08-phase-n5-modern-rendering.md`](docs/superpowers/plans/2026-05-08-phase-n5-modern-rendering.md).
|
||||
|
||||
**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
|
||||
`ObjectMeshManager` is integrated and is the production rendering path
|
||||
(mandatory as of N.5 ship amendment). 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:**
|
||||
|
|
|
|||
|
|
@ -82,11 +82,12 @@ ground. This is the bug class fixed in
|
|||
|
||||
**Sequencing implication:** Phase N.2 (terrain math helpers
|
||||
substitution) cannot be shipped in isolation — it must land alongside
|
||||
N.5 (visual terrain renderer migration), at which point both physics
|
||||
and visual mesh switch to WB's formula together. Roadmap N.2 entry
|
||||
flags this dependency.
|
||||
visual terrain renderer migration (originally N.5, now moved to N.7
|
||||
scope), at which point both physics and visual mesh switch to WB's
|
||||
formula together. N.5 shipped entity rendering only; terrain remains
|
||||
on acdream's own pipeline through N.7.
|
||||
|
||||
**Research needed (when N.5 picks this up):**
|
||||
**Research needed (when N.7 picks this up):**
|
||||
1. Quantify divergence: run WB's `CalculateSplitDirection` and our
|
||||
`IsSplitSWtoNE` across all (lbX, lbY, cellX, cellY) tuples for a
|
||||
representative landblock set; record disagreement rate.
|
||||
|
|
@ -97,8 +98,8 @@ flags this dependency.
|
|||
server-authoritative Z within tolerance) is invalidated by the
|
||||
formula change.
|
||||
|
||||
**Acceptance:** Resolved when N.5 lands and both physics + visual
|
||||
mesh use WB's split formula, OR when we decide to keep the AC2D
|
||||
**Acceptance:** Resolved when N.7 lands and both physics + visual
|
||||
terrain use WB's split formula, OR when we decide to keep the AC2D
|
||||
formula and patch WB's renderer in our fork.
|
||||
|
||||
---
|
||||
|
|
@ -998,8 +999,8 @@ If the coat texture's UVs at the upper region map to texel-bytes whose palette i
|
|||
|
||||
**Files (diagnostic env vars committed for next-session reuse):**
|
||||
|
||||
- `src/AcDream.App/Rendering/InstancedMeshRenderer.cs:210-275`
|
||||
— `ACDREAM_NO_CULL` env var
|
||||
- ~~`src/AcDream.App/Rendering/InstancedMeshRenderer.cs:210-275`
|
||||
— `ACDREAM_NO_CULL` env var~~ (file deleted in N.5 ship amendment)
|
||||
- `src/AcDream.App/Rendering/GameWindow.cs` — `ACDREAM_HIDE_PART=N`
|
||||
hides specific humanoid part; `ACDREAM_DUMP_CLOTHING=1` dumps
|
||||
AnimPartChanges + TextureChanges + per-part Surface chain coverage.
|
||||
|
|
|
|||
|
|
@ -59,8 +59,8 @@
|
|||
| 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 ✓ |
|
||||
| N.5 | Modern rendering path — lifted `WbDrawDispatcher` onto bindless textures (`GL_ARB_bindless_texture`) + `glMultiDrawElementsIndirect`. Per-frame entity rendering: 3 SSBO uploads (instance matrices @ binding=0, batch data @ binding=1, indirect commands) + 2 indirect draw calls (opaque + transparent). ~12-15 GL calls per frame regardless of group count, down from hundreds-of-per-group in N.4. CPU dispatcher: 1.23 ms/frame median at Holtburg courtyard (1662 groups, ~810 fps sustained). All textures on the WB modern path use 1-layer `Texture2DArray` + `sampler2DArray`. Legacy callers keep `Texture2D` / `sampler2D` via the parallel `TextureCache` path until N.6 retires them. Three gotchas captured in memory: texture target lock-in, bindless Dispose order (two-phase non-resident before delete), GL_TIME_ELAPSED double-buffering. Plan archived at `docs/superpowers/plans/2026-05-08-phase-n5-modern-rendering.md`. | 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 (retired early in N.5 ship amendment). | Live ✓ |
|
||||
| N.5 | Modern rendering path — lifted `WbDrawDispatcher` onto bindless textures (`GL_ARB_bindless_texture`) + `glMultiDrawElementsIndirect`. Per-frame entity rendering: 3 SSBO uploads (instance matrices @ binding=0, batch data @ binding=1, indirect commands) + 2 indirect draw calls (opaque + transparent). ~12-15 GL calls per frame regardless of group count, down from hundreds-of-per-group in N.4. CPU dispatcher: 1.23 ms/frame median at Holtburg courtyard (1662 groups, ~810 fps sustained). All textures on the WB modern path use 1-layer `Texture2DArray` + `sampler2DArray`. Legacy callers keep `Texture2D` / `sampler2D` via the parallel `TextureCache` path until N.6 retires them. Three gotchas captured in memory: texture target lock-in, bindless Dispose order (two-phase non-resident before delete), GL_TIME_ELAPSED double-buffering. **Ship amendment 2026-05-08:** legacy renderers (`InstancedMeshRenderer`, `StaticMeshRenderer`, `WbFoundationFlag`) retired within N.5 — modern path is mandatory; missing bindless throws `NotSupportedException` at startup. N.6 scope narrowed accordingly. Plan archived at `docs/superpowers/plans/2026-05-08-phase-n5-modern-rendering.md`. | Live ✓ |
|
||||
|
||||
Plus polish that doesn't get its own phase number:
|
||||
- FlyCamera default speed lowered + Shift-to-boost
|
||||
|
|
@ -647,16 +647,17 @@ for our deletions/additions; merge upstream `master` periodically.
|
|||
`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 — Retire legacy renderers + perf polish.** **Currently in flight.**
|
||||
Builds on N.5. Retires `InstancedMeshRenderer` + `StaticMeshRenderer`
|
||||
entirely — they remain as `ACDREAM_USE_WB_FOUNDATION=0` escape hatches
|
||||
through N.5 but are deleted when N.6 ships. Optional N.6 candidates: WB
|
||||
atlas adoption for memory savings on shared content, persistent-mapped
|
||||
buffers if `glBufferData` shows up in profiling, GPU-side culling via
|
||||
compute pre-pass, GL_TIME_ELAPSED query double-buffering (deferred from
|
||||
N.5 — diagnostic shows `gpu_us=0/0` under `ACDREAM_WB_DIAG=1`), direct
|
||||
N.4 vs N.5 perf measurement. Plan + spec written when work begins.
|
||||
**Estimate: 1-2 weeks** (was 2-3).
|
||||
- **N.6 — Perf polish.** **Currently in flight.**
|
||||
Builds on N.5. Legacy renderer retirement was pulled forward into N.5
|
||||
ship amendment — `InstancedMeshRenderer`, `StaticMeshRenderer`, and
|
||||
`WbFoundationFlag` are already gone. N.6 scope: WB atlas adoption for
|
||||
memory savings on shared content, persistent-mapped buffers if
|
||||
`glBufferData` shows up in profiling, GPU-side culling via compute
|
||||
pre-pass, GL_TIME_ELAPSED query double-buffering (deferred from N.5 —
|
||||
diagnostic shows `gpu_us=0/0` under `ACDREAM_WB_DIAG=1`), direct N.4
|
||||
vs N.5 perf measurement, retire the legacy `Texture2D`/`sampler2D` path
|
||||
in `TextureCache` (currently kept for Sky + Terrain + Debug).
|
||||
Plan + spec written when work begins. **Estimate: 1-2 weeks.**
|
||||
- **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
|
||||
|
|
|
|||
|
|
@ -44,8 +44,11 @@ half of the lower bound estimate.
|
|||
8 pre-existing failures in `MotionInterpreter` / `BSPStepUp` /
|
||||
`PositionManager` / `PlayerMovementController` / `Dispatcher` are
|
||||
carry-forward from before N.5 and unrelated to rendering.
|
||||
- [ ] **`ACDREAM_USE_WB_FOUNDATION=0` still works** — to be verified at
|
||||
Task 14 (legacy escape hatch check).
|
||||
- [N/A] **`ACDREAM_USE_WB_FOUNDATION=0` still works** — escape hatch
|
||||
formally retired in N.5 ship amendment. `InstancedMeshRenderer`,
|
||||
`StaticMeshRenderer`, and `WbFoundationFlag` deleted. Missing
|
||||
bindless throws `NotSupportedException` at startup with a clear
|
||||
error message. No fallback path.
|
||||
|
||||
## Visual verification (Task 14)
|
||||
|
||||
|
|
|
|||
|
|
@ -2561,10 +2561,10 @@ SHIP commit at Task 19.
|
|||
`FullyQualifiedName~Wb|FullyQualifiedName~MatrixComposition`.
|
||||
Pre-existing 8 failures in physics/input/movement tests carry
|
||||
forward unchanged from before N.5.
|
||||
- [x] **`ACDREAM_USE_WB_FOUNDATION=0` still works** — Task 15 confirmed
|
||||
InstancedMeshRenderer remains intact as the escape hatch; if
|
||||
bindless is missing, `_meshShader` stays null + `_wbDrawDispatcher`
|
||||
stays null, falling through to InstancedMeshRenderer naturally.
|
||||
- [N/A] **`ACDREAM_USE_WB_FOUNDATION=0` still works** — escape hatch
|
||||
formally retired in N.5 ship amendment (see section below).
|
||||
`InstancedMeshRenderer`, `StaticMeshRenderer`, and `WbFoundationFlag`
|
||||
deleted. Missing bindless throws `NotSupportedException` at startup.
|
||||
|
||||
### Plan amendments captured during execution
|
||||
|
||||
|
|
@ -2613,7 +2613,7 @@ adjustments captured beyond the plan:
|
|||
- **Persistent-mapped buffers** (Decision 7 deferral). Layer on top of
|
||||
the modern path if `glBufferData` shows up as a residual hot spot in
|
||||
profiling.
|
||||
- **Retire `InstancedMeshRenderer`** entirely — N.6 primary scope.
|
||||
- ~~**Retire `InstancedMeshRenderer`** entirely — N.6 primary scope.~~ **Done in N.5 ship amendment.**
|
||||
- **WB atlas adoption** for memory savings on shared content (trees,
|
||||
walls, etc).
|
||||
- **GPU-side culling** via compute pre-pass.
|
||||
|
|
@ -2655,3 +2655,52 @@ CLAUDE.md "WB integration cribs" updated with N.5 patterns (Task 16).
|
|||
**Deleted:**
|
||||
- `src/AcDream.App/Rendering/Shaders/mesh_instanced.vert`
|
||||
- `src/AcDream.App/Rendering/Shaders/mesh_instanced.frag`
|
||||
|
||||
---
|
||||
|
||||
## Ship amendment — 2026-05-08
|
||||
|
||||
### Problem discovered in cross-cutting review
|
||||
|
||||
Task 15's deletion of `mesh_instanced.vert/.frag` left `InstancedMeshRenderer`
|
||||
orphaned. The `_staticMesh` construction was gated on `_meshShader is not null`,
|
||||
and `_meshShader` was only assigned when bindless was present. So with
|
||||
`ACDREAM_USE_WB_FOUNDATION=0`, the flag path produced `_meshShader=null` →
|
||||
`_staticMesh=null` → terrain+sky only with no entity rendering. The SHIP
|
||||
commit's `[x] ACDREAM_USE_WB_FOUNDATION=0 still works` claim was inaccurate.
|
||||
|
||||
### Resolution
|
||||
|
||||
User authorized **Option B**: formal retirement of the legacy path in N.5
|
||||
instead of restoring it. Reasons: bindless + WB foundation has been default-on
|
||||
since N.4, escape hatch was never exercised in practice, N.6 was already
|
||||
planning to retire it — we did it now instead.
|
||||
|
||||
**Files deleted:**
|
||||
- `src/AcDream.App/Rendering/InstancedMeshRenderer.cs`
|
||||
- `src/AcDream.App/Rendering/StaticMeshRenderer.cs`
|
||||
- `src/AcDream.App/Rendering/Wb/WbFoundationFlag.cs`
|
||||
|
||||
**GameWindow simplified:**
|
||||
- `_staticMesh` field removed
|
||||
- Capability detection block is unconditional (no `WbFoundationFlag.IsEnabled` guard)
|
||||
- Missing bindless throws `NotSupportedException` at startup with a clear message
|
||||
- `_wbMeshAdapter`, `_wbEntitySpawnAdapter`, `_wbDrawDispatcher` all construct
|
||||
unconditionally after the capability check
|
||||
- Draw path: `_wbDrawDispatcher!.Draw(...)` — no null-conditional, no else branch
|
||||
|
||||
**GpuWorldState simplified:**
|
||||
- `WbFoundationFlag.IsEnabled` guards removed from `AddLandblock` /
|
||||
`RemoveLandblock`; adapter calls are unconditional when adapter is non-null
|
||||
|
||||
**Test file updated:**
|
||||
- `PendingSpawnIntegrationTests.cs`: removed `static WbFoundationFlag.ForTestsOnly_ForceEnable()` ctor
|
||||
(no longer needed — `GpuWorldState` adapter calls are unconditional)
|
||||
|
||||
**Spec §2 Decision 5 updated:** two-way flag → mandatory modern path.
|
||||
**Spec §10 Out-of-scope updated:** `InstancedMeshRenderer` deletion crossed off (done).
|
||||
**Roadmap updated:** N.5 entry notes retirement; N.6 scope narrowed.
|
||||
**Perf baseline doc updated:** acceptance gate row corrected to N/A.
|
||||
**CLAUDE.md updated:** WB integration cribs no longer reference WbFoundationFlag.
|
||||
|
||||
Build: green (0 errors, 0 warnings). Tests: 71/71 in Wb+MatrixComposition+TextureCacheBindless filter.
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ This section records the brainstorm outcomes that the rest of the doc relies on.
|
|||
| 2 | Translucent rendering | **WB's two-pass alpha-test** (opaque pass discards `α<0.95`, transparent pass discards `α≥0.95`) | Single blend mode per pass enables one indirect call per pass. Loses native `Additive` blend on GfxObj surfaces; sky + particles have own renderers and aren't affected. Falsifiable at visual verification — if we see a regression, add an additive sub-pass (~30-min fix). |
|
||||
| 3 | Per-instance + per-draw data delivery | **All-SSBO**: `Instances[]` at binding=0 (mat4 per instance), `Batches[]` at binding=1 (texture handle + layer + flags per group) | Matches WB's modern shader. SSBOs avoid the 16-attrib stride limit, scale to large instance counts, give clean per-draw indexing via `gl_DrawIDARB`. |
|
||||
| 4 | Bindless handle residency | **Resident on upload, never release** | acdream's content set is bounded (~1-5K unique textures per session). Handles persist for process lifetime; no eviction code in N.5. Diagnostic logging of handle count under `ACDREAM_WB_DIAG=1` to spot growth. |
|
||||
| 5 | Escape hatch | **Two-way flag (no change)**. `ACDREAM_USE_WB_FOUNDATION=0/1` controls `WbFoundationFlag`; flag-on is the N.5 modern path; flag-off falls back to legacy `InstancedMeshRenderer`. N.4's draw method is replaced in place. | N.4's grouped-instanced draw is not preserved as an A/B fallback; legacy `InstancedMeshRenderer` is the existing safety net for "modern rendering broken on this GPU." |
|
||||
| 5 | Escape hatch | **Modern path mandatory (N.5 ship amendment)**. `WbFoundationFlag` and `ACDREAM_USE_WB_FOUNDATION` env var have been deleted. Missing `GL_ARB_bindless_texture` or `GL_ARB_shader_draw_parameters` throws `NotSupportedException` at startup with a clear error message. No fallback. | Escape hatch was never exercised after N.4 ship. Legacy `InstancedMeshRenderer` + `StaticMeshRenderer` deleted in the N.5 retirement commit. N.6 scope narrowed accordingly. |
|
||||
| 6 | Perf measurement | **CPU stopwatch + GL timer queries** logged via `[WB-DIAG]` | Captures both CPU dispatcher time and GPU rendering time. Acceptance gate compares before/after numbers in fixed Holtburg/Foundry scenes. |
|
||||
| 7 | Persistent-mapped buffers | **Defer to N.6** | Bindless+indirect win is 70-80% of achievable savings. Persistent-mapped + ring + sync is the last 5-10% with non-trivial sync-fence complexity; not worth the risk in N.5's 2-3 week budget. Add post-N.5 if profiling shows residual `glBufferData` cost. |
|
||||
| 8 | Per-instance highlight (selection blink) | **Defer to a Phase B.4 follow-up** | Retail pulses click targets as visual confirmation; the right mechanism is per-instance highlight color (NOT WB's global `uHighlightColor` which would tint everything in our single-indirect-call design). Field is reserved in design (extend `InstanceData` to include `vec4 highlightColor`); N.5 ships without the field, future phase plumbs it without shader rewrite. |
|
||||
|
|
@ -540,7 +540,7 @@ The following are NOT N.5 work. They become possible follow-ons.
|
|||
- **GPU-side culling (compute pre-pass).** Future phase.
|
||||
- **Texture array repacking for multi-layer per-instance composites.** Future, if many palette-overrides actually share dimensions and could be packed.
|
||||
- **Selection-blink highlight color.** Decision 8. Phase B.4 follow-up. Field reserved in `InstanceData` design (extend stride to 80 bytes when implementing).
|
||||
- **Deletion of legacy `InstancedMeshRenderer`.** N.6.
|
||||
- ~~**Deletion of legacy `InstancedMeshRenderer`.** N.6.~~ **Done in N.5 ship amendment** — `InstancedMeshRenderer`, `StaticMeshRenderer`, and `WbFoundationFlag` were deleted in the retirement commit.
|
||||
- **Terrain wiring through WB.** Future.
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -25,17 +25,16 @@ public sealed class GameWindow : IDisposable
|
|||
private DatCollection? _dats;
|
||||
private float _lastMouseX;
|
||||
private float _lastMouseY;
|
||||
private InstancedMeshRenderer? _staticMesh;
|
||||
private Shader? _meshShader;
|
||||
private TextureCache? _textureCache;
|
||||
/// <summary>Phase N.4: WB-backed rendering pipeline adapter. Non-null only
|
||||
/// when <c>ACDREAM_USE_WB_FOUNDATION=1</c> is set; null otherwise.</summary>
|
||||
/// <summary>Phase N.4+: WB-backed rendering pipeline adapter. Always non-null
|
||||
/// after <c>OnLoad</c> completes (modern path is mandatory as of N.5).</summary>
|
||||
private AcDream.App.Rendering.Wb.WbMeshAdapter? _wbMeshAdapter;
|
||||
private AcDream.App.Rendering.Wb.EntitySpawnAdapter? _wbEntitySpawnAdapter;
|
||||
private AcDream.App.Rendering.Wb.WbDrawDispatcher? _wbDrawDispatcher;
|
||||
/// <summary>Phase N.5: ARB_bindless_texture + ARB_shader_draw_parameters
|
||||
/// support. Non-null only when both extensions are present and WbFoundation
|
||||
/// is enabled. Passed to TextureCache and (later) WbDrawDispatcher.</summary>
|
||||
/// support. Required at startup — missing bindless throws
|
||||
/// <see cref="NotSupportedException"/> in <c>OnLoad</c>.</summary>
|
||||
private AcDream.App.Rendering.Wb.BindlessSupport? _bindlessSupport;
|
||||
private SamplerCache? _samplerCache;
|
||||
private DebugLineRenderer? _debugLines;
|
||||
|
|
@ -970,10 +969,6 @@ public sealed class GameWindow : IDisposable
|
|||
Path.Combine(shadersDir, "terrain.vert"),
|
||||
Path.Combine(shadersDir, "terrain.frag"));
|
||||
|
||||
// mesh_instanced is the default; Task 10 (N.5) moves the final shader
|
||||
// selection to after capability detection so mesh_modern can be chosen
|
||||
// when bindless + ARB_shader_draw_parameters are available. See below.
|
||||
|
||||
// Phase G.1/G.2: shared scene-lighting UBO. Stays bound at
|
||||
// binding=1 for the lifetime of the process — every shader that
|
||||
// declares `layout(std140, binding = 1) uniform SceneLighting`
|
||||
|
|
@ -1423,43 +1418,41 @@ public sealed class GameWindow : IDisposable
|
|||
_heightTable = heightTable;
|
||||
_surfaceCache = new Dictionary<uint, AcDream.Core.Terrain.SurfaceInfo>();
|
||||
|
||||
// N.5: detect ARB_bindless_texture + ARB_shader_draw_parameters when WB
|
||||
// foundation is on. Store the BindlessSupport for TextureCache + future
|
||||
// WbDrawDispatcher. Mesh shader load stays as mesh_instanced for now —
|
||||
// Task 10 swaps to mesh_modern after the dispatcher is rewired.
|
||||
if (AcDream.App.Rendering.Wb.WbFoundationFlag.IsEnabled)
|
||||
// N.5: detect ARB_bindless_texture + ARB_shader_draw_parameters.
|
||||
// The modern path (SSBO + glMultiDrawElementsIndirect + bindless textures)
|
||||
// is mandatory as of Phase N.5 — missing extensions throw at startup with
|
||||
// a clear error so users can file a real bug report rather than silently
|
||||
// falling back to a half-working renderer.
|
||||
if (AcDream.App.Rendering.Wb.BindlessSupport.TryCreate(_gl, out var bindless))
|
||||
{
|
||||
if (AcDream.App.Rendering.Wb.BindlessSupport.TryCreate(_gl, out var bindless))
|
||||
if (bindless!.HasShaderDrawParameters(_gl))
|
||||
{
|
||||
if (bindless!.HasShaderDrawParameters(_gl))
|
||||
{
|
||||
_bindlessSupport = bindless;
|
||||
Console.WriteLine("[N.5] modern path capabilities present (bindless + ARB_shader_draw_parameters)");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("[N.5] GL_ARB_shader_draw_parameters not present — modern dispatch path will not activate");
|
||||
}
|
||||
_bindlessSupport = bindless;
|
||||
Console.WriteLine("[N.5] modern path capabilities present (bindless + ARB_shader_draw_parameters)");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("[N.5] GL_ARB_bindless_texture not present — modern dispatch path will not activate");
|
||||
Console.WriteLine("[N.5] GL_ARB_shader_draw_parameters not present — modern path not available");
|
||||
}
|
||||
}
|
||||
|
||||
// N.5 Task 10/15: load mesh_modern when both extensions are present.
|
||||
// If bindless is missing _meshShader stays null, _wbDrawDispatcher won't
|
||||
// be constructed (its guard requires _bindlessSupport non-null), and
|
||||
// rendering falls back to InstancedMeshRenderer — but only when
|
||||
// _meshShader is non-null (see _staticMesh construction below).
|
||||
if (_bindlessSupport is not null)
|
||||
else
|
||||
{
|
||||
_meshShader = new Shader(_gl,
|
||||
Path.Combine(shadersDir, "mesh_modern.vert"),
|
||||
Path.Combine(shadersDir, "mesh_modern.frag"));
|
||||
Console.WriteLine("[N.5] mesh_modern shader loaded");
|
||||
Console.WriteLine("[N.5] GL_ARB_bindless_texture not present — modern path not available");
|
||||
}
|
||||
// else: bindless missing — _meshShader stays null.
|
||||
|
||||
if (_bindlessSupport is null)
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
"acdream requires GL_ARB_bindless_texture + GL_ARB_shader_draw_parameters " +
|
||||
"(GL 4.3+ with bindless support). Your GPU/driver does not expose these extensions. " +
|
||||
"If this is unexpected, please file a bug report with your GPU vendor + driver version.");
|
||||
}
|
||||
|
||||
// Mesh shader always loads (modern path is the only path).
|
||||
_meshShader = new Shader(_gl,
|
||||
Path.Combine(shadersDir, "mesh_modern.vert"),
|
||||
Path.Combine(shadersDir, "mesh_modern.frag"));
|
||||
Console.WriteLine("[N.5] mesh_modern shader loaded");
|
||||
|
||||
_textureCache = new TextureCache(_gl, _dats, _bindlessSupport);
|
||||
// Two persistent GL sampler objects (Repeat + ClampToEdge) so
|
||||
|
|
@ -1469,17 +1462,14 @@ public sealed class GameWindow : IDisposable
|
|||
// references/WorldBuilder/Chorizite.OpenGLSDLBackend/OpenGLGraphicsDevice.cs:115-132.
|
||||
_samplerCache = new SamplerCache(_gl);
|
||||
|
||||
// Phase N.4 — WB rendering pipeline foundation. Constructed only when
|
||||
// ACDREAM_USE_WB_FOUNDATION=1 is set; otherwise the legacy renderer
|
||||
// path stays in charge. The full ObjectMeshManager bring-up lives in
|
||||
// WbMeshAdapter (Task 9): OpenGLGraphicsDevice + DefaultDatReaderWriter
|
||||
// + ObjectMeshManager. WbMeshAdapter opens its own file handles for
|
||||
// the dat files (independent of our DatCollection).
|
||||
if (AcDream.App.Rendering.Wb.WbFoundationFlag.IsEnabled)
|
||||
// Phase N.4+N.5 — WB rendering pipeline foundation. The modern path is
|
||||
// mandatory as of N.5 ship amendment: WbMeshAdapter + WbDrawDispatcher
|
||||
// always construct. WbMeshAdapter owns ObjectMeshManager and opens its
|
||||
// own file handles for the dat files (independent of our DatCollection).
|
||||
{
|
||||
var wbLogger = Microsoft.Extensions.Logging.Abstractions.NullLogger<AcDream.App.Rendering.Wb.WbMeshAdapter>.Instance;
|
||||
_wbMeshAdapter = new AcDream.App.Rendering.Wb.WbMeshAdapter(_gl, _datDir, _dats, wbLogger);
|
||||
Console.WriteLine("[N.4] WbFoundation flag is ENABLED — routing static content through ObjectMeshManager.");
|
||||
Console.WriteLine("[N.4+N.5] WB foundation + modern path active — routing all content through ObjectMeshManager.");
|
||||
}
|
||||
|
||||
// Phase N.4 Task 12: construct LandblockSpawnAdapter under the feature flag
|
||||
|
|
@ -1488,68 +1478,51 @@ public sealed class GameWindow : IDisposable
|
|||
// one that carries the adapter so AddLandblock/RemoveLandblock notify WB.
|
||||
// Phase N.4 Task 17: also construct EntitySpawnAdapter for server-spawned
|
||||
// per-instance content under the same flag.
|
||||
// N.5 mandatory path: spawn adapters + dispatcher always construct.
|
||||
// _wbMeshAdapter, _meshShader, _textureCache, and _bindlessSupport are
|
||||
// all guaranteed non-null here (startup throws above if any are missing).
|
||||
{
|
||||
AcDream.App.Rendering.Wb.LandblockSpawnAdapter? wbSpawnAdapter = null;
|
||||
AcDream.App.Rendering.Wb.EntitySpawnAdapter? wbEntitySpawnAdapter = null;
|
||||
if (AcDream.App.Rendering.Wb.WbFoundationFlag.IsEnabled && _wbMeshAdapter is not null)
|
||||
var wbSpawnAdapter = new AcDream.App.Rendering.Wb.LandblockSpawnAdapter(_wbMeshAdapter!);
|
||||
// Sequencer factory: look up Setup + MotionTable from dats and build
|
||||
// an AnimationSequencer. Falls back to a no-op sequencer when the
|
||||
// entity has no motion table (static props, etc.). Uses _animLoader
|
||||
// which is initialised earlier in OnLoad; it is non-null here.
|
||||
var capturedDats = _dats;
|
||||
var capturedAnimLoader = _animLoader;
|
||||
AcDream.Core.Physics.AnimationSequencer SequencerFactory(AcDream.Core.World.WorldEntity e)
|
||||
{
|
||||
wbSpawnAdapter = new AcDream.App.Rendering.Wb.LandblockSpawnAdapter(_wbMeshAdapter);
|
||||
// Sequencer factory: look up Setup + MotionTable from dats and build
|
||||
// an AnimationSequencer. Falls back to a no-op sequencer when the
|
||||
// entity has no motion table (static props, etc.). Uses _animLoader
|
||||
// which is initialised at line 1004; it is non-null here because
|
||||
// OnLoad wires _dats + _animLoader before this block runs.
|
||||
var capturedDats = _dats;
|
||||
var capturedAnimLoader = _animLoader;
|
||||
AcDream.Core.Physics.AnimationSequencer SequencerFactory(AcDream.Core.World.WorldEntity e)
|
||||
if (capturedDats is not null && capturedAnimLoader is not null)
|
||||
{
|
||||
if (capturedDats is not null && capturedAnimLoader is not null)
|
||||
var setup = capturedDats.Get<DatReaderWriter.DBObjs.Setup>(e.SourceGfxObjOrSetupId);
|
||||
if (setup is not null)
|
||||
{
|
||||
var setup = capturedDats.Get<DatReaderWriter.DBObjs.Setup>(e.SourceGfxObjOrSetupId);
|
||||
if (setup is not null)
|
||||
uint mtableId = (uint)setup.DefaultMotionTable;
|
||||
if (mtableId != 0)
|
||||
{
|
||||
uint mtableId = (uint)setup.DefaultMotionTable;
|
||||
if (mtableId != 0)
|
||||
{
|
||||
var mtable = capturedDats.Get<DatReaderWriter.DBObjs.MotionTable>(mtableId);
|
||||
if (mtable is not null)
|
||||
return new AcDream.Core.Physics.AnimationSequencer(setup, mtable, capturedAnimLoader);
|
||||
}
|
||||
// Setup exists but no motion table — no-op sequencer.
|
||||
return new AcDream.Core.Physics.AnimationSequencer(
|
||||
setup,
|
||||
new DatReaderWriter.DBObjs.MotionTable(),
|
||||
capturedAnimLoader);
|
||||
var mtable = capturedDats.Get<DatReaderWriter.DBObjs.MotionTable>(mtableId);
|
||||
if (mtable is not null)
|
||||
return new AcDream.Core.Physics.AnimationSequencer(setup, mtable, capturedAnimLoader);
|
||||
}
|
||||
// Setup exists but no motion table — no-op sequencer.
|
||||
return new AcDream.Core.Physics.AnimationSequencer(
|
||||
setup,
|
||||
new DatReaderWriter.DBObjs.MotionTable(),
|
||||
capturedAnimLoader);
|
||||
}
|
||||
// Complete fallback: empty setup + empty motion table + null loader.
|
||||
return new AcDream.Core.Physics.AnimationSequencer(
|
||||
new DatReaderWriter.DBObjs.Setup(),
|
||||
new DatReaderWriter.DBObjs.MotionTable(),
|
||||
new NullAnimLoader());
|
||||
}
|
||||
wbEntitySpawnAdapter = new AcDream.App.Rendering.Wb.EntitySpawnAdapter(
|
||||
_textureCache, SequencerFactory, _wbMeshAdapter);
|
||||
_wbEntitySpawnAdapter = wbEntitySpawnAdapter;
|
||||
// Complete fallback: empty setup + empty motion table + null loader.
|
||||
return new AcDream.Core.Physics.AnimationSequencer(
|
||||
new DatReaderWriter.DBObjs.Setup(),
|
||||
new DatReaderWriter.DBObjs.MotionTable(),
|
||||
new NullAnimLoader());
|
||||
}
|
||||
var wbEntitySpawnAdapter = new AcDream.App.Rendering.Wb.EntitySpawnAdapter(
|
||||
_textureCache!, SequencerFactory, _wbMeshAdapter!);
|
||||
_wbEntitySpawnAdapter = wbEntitySpawnAdapter;
|
||||
_worldState = new AcDream.App.Streaming.GpuWorldState(wbSpawnAdapter, wbEntitySpawnAdapter);
|
||||
}
|
||||
|
||||
// Task 15: _meshShader is null when bindless is missing; skip constructing
|
||||
// _staticMesh in that case. All downstream _staticMesh usages are already
|
||||
// null-safe (null-conditional operators or explicit null guards).
|
||||
if (_meshShader is not null && _textureCache is not null)
|
||||
_staticMesh = new InstancedMeshRenderer(_gl, _meshShader, _textureCache, _wbMeshAdapter);
|
||||
|
||||
if (AcDream.App.Rendering.Wb.WbFoundationFlag.IsEnabled
|
||||
&& _wbMeshAdapter is not null && _wbEntitySpawnAdapter is not null
|
||||
&& _bindlessSupport is not null)
|
||||
{
|
||||
// _meshShader is non-null here: the _bindlessSupport guard implies
|
||||
// the if(_bindlessSupport is not null) block above ran and assigned it.
|
||||
// _textureCache is always non-null (assigned unconditionally above).
|
||||
_wbDrawDispatcher = new AcDream.App.Rendering.Wb.WbDrawDispatcher(
|
||||
_gl, _meshShader!, _textureCache!, _wbMeshAdapter, _wbEntitySpawnAdapter, _bindlessSupport);
|
||||
_gl, _meshShader!, _textureCache!, _wbMeshAdapter!, _wbEntitySpawnAdapter, _bindlessSupport!);
|
||||
}
|
||||
|
||||
// Phase G.1 sky renderer — its own shader (sky.vert / sky.frag)
|
||||
|
|
@ -2075,7 +2048,7 @@ public sealed class GameWindow : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
if (_dats is null || _staticMesh is null) return;
|
||||
if (_dats is null) return;
|
||||
if (spawn.Position is null || spawn.SetupTableId is null)
|
||||
{
|
||||
// Can't place a mesh without both. Most of these are inventory
|
||||
|
|
@ -2410,10 +2383,9 @@ public sealed class GameWindow : IDisposable
|
|||
continue;
|
||||
}
|
||||
_physicsDataCache.CacheGfxObj(mr.GfxObjId, gfx);
|
||||
var subMeshes = AcDream.Core.Meshing.GfxObjMesh.Build(gfx, _dats);
|
||||
_staticMesh.EnsureUploaded(mr.GfxObjId, subMeshes);
|
||||
if (dumpClothing)
|
||||
{
|
||||
var subMeshes = AcDream.Core.Meshing.GfxObjMesh.Build(gfx, _dats);
|
||||
int tris = 0; int subs = 0;
|
||||
foreach (var sm in subMeshes) { tris += sm.Indices.Length / 3; subs++; }
|
||||
dumpClothingTotalTris += tris;
|
||||
|
|
@ -5244,44 +5216,25 @@ public sealed class GameWindow : IDisposable
|
|||
portalPlanes, origin.X, origin.Y);
|
||||
}
|
||||
|
||||
// Upload every GfxObj referenced by this landblock's entities.
|
||||
// EnsureUploaded is idempotent so duplicates across landblocks are free.
|
||||
if (_staticMesh is not null)
|
||||
// N.5: WbMeshAdapter.Tick() handles GPU upload for all GfxObj meshes via
|
||||
// ObjectMeshManager.PrepareMeshDataAsync. The legacy EnsureUploaded loop
|
||||
// (and _pendingCellMeshes drain) are retired with InstancedMeshRenderer.
|
||||
// Cache GfxObj physics data (BSP trees) for the physics engine — this
|
||||
// loop is physics-only, not renderer-side.
|
||||
foreach (var entity in lb.Entities)
|
||||
{
|
||||
// Task 8: drain any pending EnvCell room-mesh sub-meshes first.
|
||||
// The worker thread pre-built these CPU-side and stored them in
|
||||
// _pendingCellMeshes. We must upload them here (render thread) before
|
||||
// the per-MeshRef loop below tries to look them up via GfxObjMesh.Build,
|
||||
// which would fail because EnvCell ids (0xAAAA01xx) aren't real GfxObj
|
||||
// dat ids. EnsureUploaded is idempotent so calling it here then seeing
|
||||
// the same id again in the loop below is safe.
|
||||
foreach (var entity in lb.Entities)
|
||||
foreach (var meshRef in entity.MeshRefs)
|
||||
{
|
||||
foreach (var meshRef in entity.MeshRefs)
|
||||
{
|
||||
if (_pendingCellMeshes.TryRemove(meshRef.GfxObjId, out var cellSubMeshes))
|
||||
_staticMesh.EnsureUploaded(meshRef.GfxObjId, cellSubMeshes);
|
||||
}
|
||||
}
|
||||
|
||||
// Now upload regular GfxObj sub-meshes (stabs, scenery, interior stabs).
|
||||
// Skip any ids already uploaded (includes the cell meshes just drained).
|
||||
foreach (var entity in lb.Entities)
|
||||
{
|
||||
foreach (var meshRef in entity.MeshRefs)
|
||||
{
|
||||
// Skip EnvCell synthetic ids — already handled above (or already
|
||||
// uploaded on a prior tick). GfxObj ids are 0x01xxxxxx; Setup ids
|
||||
// are 0x02xxxxxx; anything else is not a GfxObj dat record.
|
||||
if ((meshRef.GfxObjId & 0xFF000000u) != 0x01000000u) continue;
|
||||
var gfx = _dats.Get<DatReaderWriter.DBObjs.GfxObj>(meshRef.GfxObjId);
|
||||
if (gfx is null) continue;
|
||||
_physicsDataCache.CacheGfxObj(meshRef.GfxObjId, gfx);
|
||||
var subMeshes = AcDream.Core.Meshing.GfxObjMesh.Build(gfx, _dats);
|
||||
_staticMesh.EnsureUploaded(meshRef.GfxObjId, subMeshes);
|
||||
}
|
||||
if ((meshRef.GfxObjId & 0xFF000000u) != 0x01000000u) continue;
|
||||
var gfx = _dats.Get<DatReaderWriter.DBObjs.GfxObj>(meshRef.GfxObjId);
|
||||
if (gfx is null) continue;
|
||||
_physicsDataCache.CacheGfxObj(meshRef.GfxObjId, gfx);
|
||||
}
|
||||
}
|
||||
// Drain _pendingCellMeshes to prevent unbounded accumulation.
|
||||
// The data is no longer consumed (WB handles EnvCell geometry through
|
||||
// its own pipeline), but the worker thread still populates this dict.
|
||||
_pendingCellMeshes.Clear();
|
||||
|
||||
// Task 7: register static entities into the ShadowObjectRegistry so the
|
||||
// Transition system can find and collide against them during movement.
|
||||
|
|
@ -6386,20 +6339,11 @@ public sealed class GameWindow : IDisposable
|
|||
animatedIds.Add(k);
|
||||
}
|
||||
|
||||
if (_wbDrawDispatcher is not null)
|
||||
{
|
||||
_wbDrawDispatcher.Draw(camera, _worldState.LandblockEntries, frustum,
|
||||
neverCullLandblockId: playerLb,
|
||||
visibleCellIds: visibility?.VisibleCellIds,
|
||||
animatedEntityIds: animatedIds);
|
||||
}
|
||||
else
|
||||
{
|
||||
_staticMesh?.Draw(camera, _worldState.LandblockEntries, frustum,
|
||||
neverCullLandblockId: playerLb,
|
||||
visibleCellIds: visibility?.VisibleCellIds,
|
||||
animatedEntityIds: animatedIds);
|
||||
}
|
||||
// N.5: WbDrawDispatcher is always non-null (modern path mandatory).
|
||||
_wbDrawDispatcher!.Draw(camera, _worldState.LandblockEntries, frustum,
|
||||
neverCullLandblockId: playerLb,
|
||||
visibleCellIds: visibility?.VisibleCellIds,
|
||||
animatedEntityIds: animatedIds);
|
||||
|
||||
// Phase G.1 / E.3: draw all live particles after opaque
|
||||
// scene geometry so alpha blending composites correctly.
|
||||
|
|
@ -8781,11 +8725,10 @@ public sealed class GameWindow : IDisposable
|
|||
_liveSession?.Dispose();
|
||||
_audioEngine?.Dispose(); // Phase E.2: stop all voices, close AL context
|
||||
_wbDrawDispatcher?.Dispose();
|
||||
_staticMesh?.Dispose();
|
||||
_skyRenderer?.Dispose(); // depends on sampler cache; dispose first
|
||||
_samplerCache?.Dispose();
|
||||
_textureCache?.Dispose();
|
||||
_wbMeshAdapter?.Dispose(); // Phase N.4 WB foundation — null when flag off
|
||||
_wbMeshAdapter?.Dispose(); // Phase N.4+N.5 WB foundation (mandatory modern path)
|
||||
|
||||
_meshShader?.Dispose();
|
||||
_terrain?.Dispose();
|
||||
|
|
|
|||
|
|
@ -1,596 +0,0 @@
|
|||
// src/AcDream.App/Rendering/InstancedMeshRenderer.cs
|
||||
//
|
||||
// True instanced rendering for static-object meshes.
|
||||
// Groups entities by GfxObjId. All instance model matrices are written into
|
||||
// a single shared instance VBO once per frame. Each sub-mesh is drawn with
|
||||
// DrawElementsInstanced — one GL draw call per (GfxObj × sub-mesh) instead
|
||||
// of one per entity. For a scene with N unique GfxObjs and M total entities
|
||||
// this reduces draw calls from M*subMeshes to N*subMeshes.
|
||||
//
|
||||
// Matrix layout:
|
||||
// System.Numerics.Matrix4x4 is row-major. Written to the float[] buffer in
|
||||
// natural memory order (M11..M44). The GLSL shader reads 4 vec4 attributes
|
||||
// (aInstanceRow0-3) and constructs mat4(row0, row1, row2, row3). Because
|
||||
// GLSL mat4() takes column vectors, the rows of the C# matrix become the
|
||||
// columns of the GLSL mat4 — which is the same transpose that UniformMatrix4
|
||||
// with transpose=false produces. Visual result is identical to the old
|
||||
// SetMatrix4("uModel", ...) path.
|
||||
//
|
||||
// Architecture note: public API matches StaticMeshRenderer so GameWindow only
|
||||
// needs to update the shader and uniform setup at the call sites.
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using AcDream.Core.Meshing;
|
||||
using AcDream.Core.Terrain;
|
||||
using AcDream.Core.World;
|
||||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
public sealed unsafe class InstancedMeshRenderer : IDisposable
|
||||
{
|
||||
private readonly GL _gl;
|
||||
private readonly Shader _shader;
|
||||
private readonly TextureCache _textures;
|
||||
|
||||
/// <summary>
|
||||
/// Optional WB adapter. Held but currently unused — Phase N.4 Adjustment 2
|
||||
/// (2026-05-08) reverted Task 9's renderer-level routing. Tier-routing decisions
|
||||
/// (atlas vs per-instance) belong at the spawn-callback layer (Task 11
|
||||
/// LandblockSpawnAdapter for atlas-tier; Task 17 EntitySpawnAdapter for
|
||||
/// per-instance), not in the renderer which is intentionally tier-blind. The
|
||||
/// constructor parameter is preserved so GameWindow's wire-up doesn't shift
|
||||
/// when later tasks need adapter access.
|
||||
/// </summary>
|
||||
private readonly WbMeshAdapter? _wbMeshAdapter;
|
||||
|
||||
// One GPU bundle per unique GfxObj id. Each GfxObj can have multiple sub-meshes.
|
||||
private readonly Dictionary<uint, List<SubMeshGpu>> _gpuByGfxObj = new();
|
||||
|
||||
// Shared instance VBO — filled every frame with all instance model matrices.
|
||||
private readonly uint _instanceVbo;
|
||||
|
||||
// Per-frame scratch: reused float buffer for instance matrix data.
|
||||
// 16 floats per mat4. Grown on demand; never shrunk.
|
||||
private float[] _instanceBuffer = new float[256 * 16]; // start at 256 instances
|
||||
|
||||
// ── Instance grouping scratch ─────────────────────────────────────────────
|
||||
//
|
||||
// Reused every frame to avoid per-frame allocation.
|
||||
//
|
||||
// **Group key = (GfxObjId, PaletteOverrideHash, SurfaceOverridesHash).**
|
||||
//
|
||||
// An earlier implementation grouped on <c>GfxObjId</c> alone and resolved
|
||||
// the per-sub-mesh texture from the first instance in the group — which
|
||||
// is fine for scenery where every tree shares the same palette, but
|
||||
// utterly broken for NPCs: every humanoid uses the same base body
|
||||
// GfxObjs and they all piled into one group, so the first NPC's palette
|
||||
// was used for every NPC in the frame. Frustum culling + iteration
|
||||
// order meant that "first NPC" changed as the camera turned — producing
|
||||
// the "NPC clothing changes when I turn" symptom.
|
||||
//
|
||||
// Now we also key by the entity's PaletteOverride + per-MeshRef
|
||||
// SurfaceOverrides signature so only entities that decode to the
|
||||
// SAME texture for every sub-mesh can share a batch. Entities with
|
||||
// unique appearance fall to single-instance groups (still correct,
|
||||
// marginally slower than true instancing).
|
||||
private readonly Dictionary<GroupKey, InstanceGroup> _groups = new();
|
||||
|
||||
private readonly record struct GroupKey(uint GfxObjId, ulong TextureSignature);
|
||||
|
||||
public InstancedMeshRenderer(GL gl, Shader shader, TextureCache textures,
|
||||
WbMeshAdapter? wbMeshAdapter = null)
|
||||
{
|
||||
_gl = gl;
|
||||
_shader = shader;
|
||||
_textures = textures;
|
||||
_wbMeshAdapter = wbMeshAdapter;
|
||||
|
||||
_instanceVbo = _gl.GenBuffer();
|
||||
}
|
||||
|
||||
// ── Upload ────────────────────────────────────────────────────────────────
|
||||
|
||||
public void EnsureUploaded(uint gfxObjId, IReadOnlyList<GfxObjSubMesh> subMeshes)
|
||||
{
|
||||
if (_gpuByGfxObj.ContainsKey(gfxObjId))
|
||||
return;
|
||||
|
||||
// Phase N.4 Adjustment 2 (2026-05-08): renderer is tier-blind. Tier-routing
|
||||
// (atlas vs per-instance) lives at the spawn-callback layer (Tasks 11 + 17),
|
||||
// not here. Smoke-test of the original Task 9 routing showed it caught
|
||||
// characters / NPCs (server-spawned, per-instance tier) along with static
|
||||
// scenery, because EnsureUploaded is called from both spawn paths.
|
||||
var list = new List<SubMeshGpu>(subMeshes.Count);
|
||||
foreach (var sm in subMeshes)
|
||||
list.Add(UploadSubMesh(sm));
|
||||
_gpuByGfxObj[gfxObjId] = list;
|
||||
}
|
||||
|
||||
private SubMeshGpu UploadSubMesh(GfxObjSubMesh sm)
|
||||
{
|
||||
uint vao = _gl.GenVertexArray();
|
||||
_gl.BindVertexArray(vao);
|
||||
|
||||
// ── Vertex buffer (positions, normals, UVs) ───────────────────────────
|
||||
uint vbo = _gl.GenBuffer();
|
||||
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, vbo);
|
||||
fixed (void* p = sm.Vertices)
|
||||
_gl.BufferData(BufferTargetARB.ArrayBuffer,
|
||||
(nuint)(sm.Vertices.Length * sizeof(Vertex)), p, BufferUsageARB.StaticDraw);
|
||||
|
||||
uint stride = (uint)sizeof(Vertex);
|
||||
_gl.EnableVertexAttribArray(0);
|
||||
_gl.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, stride, (void*)0);
|
||||
_gl.EnableVertexAttribArray(1);
|
||||
_gl.VertexAttribPointer(1, 3, VertexAttribPointerType.Float, false, stride, (void*)(3 * sizeof(float)));
|
||||
_gl.EnableVertexAttribArray(2);
|
||||
_gl.VertexAttribPointer(2, 2, VertexAttribPointerType.Float, false, stride, (void*)(6 * sizeof(float)));
|
||||
// Note: location 3 (uint TerrainLayer) is NOT used by mesh_instanced.vert;
|
||||
// that slot is reserved for per-instance mat4 row 0 from the instance VBO.
|
||||
|
||||
// ── Index buffer ──────────────────────────────────────────────────────
|
||||
uint ebo = _gl.GenBuffer();
|
||||
_gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, ebo);
|
||||
fixed (void* p = sm.Indices)
|
||||
_gl.BufferData(BufferTargetARB.ElementArrayBuffer,
|
||||
(nuint)(sm.Indices.Length * sizeof(uint)), p, BufferUsageARB.StaticDraw);
|
||||
|
||||
// ── Per-instance model matrix (locations 3-6) ─────────────────────────
|
||||
// Bind the shared instance VBO. The VAO captures this binding at each
|
||||
// attribute location. At draw time we re-call VertexAttribPointer with
|
||||
// the per-group byte offset (to address different groups in the VBO
|
||||
// without DrawElementsInstancedBaseInstance).
|
||||
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, _instanceVbo);
|
||||
// mat4 = 4 × vec4, stride = 64 bytes, divisor = 1 (advance once per instance)
|
||||
for (uint row = 0; row < 4; row++)
|
||||
{
|
||||
uint loc = 3 + row;
|
||||
_gl.EnableVertexAttribArray(loc);
|
||||
_gl.VertexAttribPointer(loc, 4, VertexAttribPointerType.Float, false, 64, (void*)(row * 16));
|
||||
_gl.VertexAttribDivisor(loc, 1);
|
||||
}
|
||||
|
||||
_gl.BindVertexArray(0);
|
||||
|
||||
return new SubMeshGpu
|
||||
{
|
||||
Vao = vao,
|
||||
Vbo = vbo,
|
||||
Ebo = ebo,
|
||||
IndexCount = sm.Indices.Length,
|
||||
SurfaceId = sm.SurfaceId,
|
||||
Translucency = sm.Translucency,
|
||||
};
|
||||
}
|
||||
|
||||
// ── Draw ──────────────────────────────────────────────────────────────────
|
||||
|
||||
public void Draw(ICamera camera,
|
||||
IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax, IReadOnlyList<WorldEntity> Entities)> landblockEntries,
|
||||
FrustumPlanes? frustum = null,
|
||||
uint? neverCullLandblockId = null,
|
||||
HashSet<uint>? visibleCellIds = null,
|
||||
// L-fix1 (2026-04-28): set of entity ids that should bypass the
|
||||
// landblock-level frustum cull. Animated entities (other
|
||||
// players, NPCs, monsters) are always rendered if their
|
||||
// landblock is loaded — without this they vanish whenever the
|
||||
// camera rotates away from their landblock, even though
|
||||
// they're within visible distance of the player. Pass null /
|
||||
// empty to keep the previous "cull everything by landblock"
|
||||
// behavior.
|
||||
HashSet<uint>? animatedEntityIds = null)
|
||||
{
|
||||
_shader.Use();
|
||||
|
||||
var vp = camera.View * camera.Projection;
|
||||
_shader.SetMatrix4("uViewProjection", vp);
|
||||
|
||||
// Phase G: lighting + ambient + fog are owned by the
|
||||
// SceneLighting UBO (binding=1) uploaded once per frame by
|
||||
// GameWindow. The instanced mesh fragment shader reads it
|
||||
// directly — no per-draw uniform uploads needed.
|
||||
|
||||
// ── Collect and group instances ───────────────────────────────────────
|
||||
CollectGroups(landblockEntries, frustum, neverCullLandblockId, visibleCellIds, animatedEntityIds);
|
||||
|
||||
// ── Build and upload the instance buffer ──────────────────────────────
|
||||
// Count total instances.
|
||||
int totalInstances = 0;
|
||||
foreach (var grp in _groups.Values)
|
||||
totalInstances += grp.Count;
|
||||
|
||||
// Grow the scratch buffer if needed.
|
||||
int needed = totalInstances * 16;
|
||||
if (_instanceBuffer.Length < needed)
|
||||
_instanceBuffer = new float[needed + 256 * 16]; // extra headroom
|
||||
|
||||
// Write all groups contiguously. Record each group's starting offset
|
||||
// (in units of instances, not bytes) so we can address them at draw time.
|
||||
int instanceOffset = 0;
|
||||
foreach (var grp in _groups.Values)
|
||||
{
|
||||
grp.BufferOffset = instanceOffset;
|
||||
foreach (ref readonly var inst in CollectionsMarshal.AsSpan(grp.Entries))
|
||||
WriteMatrix(_instanceBuffer, instanceOffset++ * 16, inst.Model);
|
||||
}
|
||||
|
||||
// Upload all instance data in a single DynamicDraw call.
|
||||
if (totalInstances > 0)
|
||||
{
|
||||
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, _instanceVbo);
|
||||
fixed (void* p = _instanceBuffer)
|
||||
_gl.BufferData(BufferTargetARB.ArrayBuffer,
|
||||
(nuint)(totalInstances * 16 * sizeof(float)), p, BufferUsageARB.DynamicDraw);
|
||||
}
|
||||
|
||||
// ── Pass 1: Opaque + ClipMap ──────────────────────────────────────────
|
||||
// Diagnostic: ACDREAM_NO_CULL=1 disables backface culling entirely.
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("ACDREAM_NO_CULL"), "1", StringComparison.Ordinal))
|
||||
{
|
||||
_gl.Disable(EnableCap.CullFace);
|
||||
}
|
||||
foreach (var (key, grp) in _groups)
|
||||
{
|
||||
if (!_gpuByGfxObj.TryGetValue(key.GfxObjId, out var subMeshes))
|
||||
continue;
|
||||
|
||||
bool hasOpaqueSubMesh = false;
|
||||
foreach (var sub in subMeshes)
|
||||
{
|
||||
if (sub.Translucency == TranslucencyKind.Opaque ||
|
||||
sub.Translucency == TranslucencyKind.ClipMap)
|
||||
{
|
||||
hasOpaqueSubMesh = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!hasOpaqueSubMesh) continue;
|
||||
|
||||
// For this group, instance data starts at grp.BufferOffset in the VBO.
|
||||
// We need to tell the VAO to read from that offset.
|
||||
uint byteOffset = (uint)(grp.BufferOffset * 64); // 64 bytes per mat4
|
||||
|
||||
foreach (var sub in subMeshes)
|
||||
{
|
||||
if (sub.Translucency != TranslucencyKind.Opaque &&
|
||||
sub.Translucency != TranslucencyKind.ClipMap)
|
||||
continue;
|
||||
|
||||
_shader.SetInt("uTranslucencyKind", (int)sub.Translucency);
|
||||
|
||||
// Bind VAO + re-point instance attributes to the group's slice
|
||||
// in the shared VBO. This updates the VAO's stored offset for
|
||||
// locations 3-6 without touching the vertex or index bindings.
|
||||
_gl.BindVertexArray(sub.Vao);
|
||||
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, _instanceVbo);
|
||||
for (uint row = 0; row < 4; row++)
|
||||
{
|
||||
_gl.VertexAttribPointer(3 + row, 4, VertexAttribPointerType.Float,
|
||||
false, 64, (void*)(byteOffset + row * 16));
|
||||
}
|
||||
|
||||
// Resolve texture from the first instance (all instances in this
|
||||
// group share the same GfxObj so they have compatible overrides
|
||||
// only in the degenerate case of mixed-palette entities using the
|
||||
// same GfxObj — rare enough to accept the approximation here).
|
||||
if (grp.Count == 0) continue;
|
||||
var firstEntry = grp.Entries[0];
|
||||
uint tex = ResolveTex(firstEntry.Entity, firstEntry.MeshRef, sub);
|
||||
_gl.ActiveTexture(TextureUnit.Texture0);
|
||||
_gl.BindTexture(TextureTarget.Texture2D, tex);
|
||||
|
||||
_gl.DrawElementsInstanced(PrimitiveType.Triangles,
|
||||
(uint)sub.IndexCount,
|
||||
DrawElementsType.UnsignedInt,
|
||||
(void*)0,
|
||||
(uint)grp.Count);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Pass 2: Translucent (AlphaBlend, Additive, InvAlpha) ─────────────
|
||||
_gl.Enable(EnableCap.Blend);
|
||||
_gl.DepthMask(false);
|
||||
// Diagnostic: ACDREAM_NO_CULL=1 disables backface culling (used 2026-05-01
|
||||
// to test if our mesh winding (0,i,i+1) vs ACME's (i+1,i,0) is causing
|
||||
// visible polygons to be culled, especially around the neck/coat seam).
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("ACDREAM_NO_CULL"), "1", StringComparison.Ordinal))
|
||||
{
|
||||
_gl.Disable(EnableCap.CullFace);
|
||||
}
|
||||
else
|
||||
{
|
||||
_gl.Enable(EnableCap.CullFace);
|
||||
_gl.CullFace(TriangleFace.Back);
|
||||
_gl.FrontFace(FrontFaceDirection.Ccw);
|
||||
}
|
||||
|
||||
foreach (var (key, grp) in _groups)
|
||||
{
|
||||
if (!_gpuByGfxObj.TryGetValue(key.GfxObjId, out var subMeshes))
|
||||
continue;
|
||||
|
||||
bool hasTranslucentSubMesh = false;
|
||||
foreach (var sub in subMeshes)
|
||||
{
|
||||
if (sub.Translucency != TranslucencyKind.Opaque &&
|
||||
sub.Translucency != TranslucencyKind.ClipMap)
|
||||
{
|
||||
hasTranslucentSubMesh = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!hasTranslucentSubMesh) continue;
|
||||
|
||||
uint byteOffset = (uint)(grp.BufferOffset * 64);
|
||||
|
||||
foreach (var sub in subMeshes)
|
||||
{
|
||||
if (sub.Translucency == TranslucencyKind.Opaque ||
|
||||
sub.Translucency == TranslucencyKind.ClipMap)
|
||||
continue;
|
||||
|
||||
switch (sub.Translucency)
|
||||
{
|
||||
case TranslucencyKind.Additive:
|
||||
_gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.One);
|
||||
break;
|
||||
case TranslucencyKind.InvAlpha:
|
||||
_gl.BlendFunc(BlendingFactor.OneMinusSrcAlpha, BlendingFactor.SrcAlpha);
|
||||
break;
|
||||
default: // AlphaBlend
|
||||
_gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
|
||||
break;
|
||||
}
|
||||
|
||||
_shader.SetInt("uTranslucencyKind", (int)sub.Translucency);
|
||||
|
||||
_gl.BindVertexArray(sub.Vao);
|
||||
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, _instanceVbo);
|
||||
for (uint row = 0; row < 4; row++)
|
||||
{
|
||||
_gl.VertexAttribPointer(3 + row, 4, VertexAttribPointerType.Float,
|
||||
false, 64, (void*)(byteOffset + row * 16));
|
||||
}
|
||||
|
||||
if (grp.Count == 0) continue;
|
||||
var firstEntry = grp.Entries[0];
|
||||
uint tex = ResolveTex(firstEntry.Entity, firstEntry.MeshRef, sub);
|
||||
_gl.ActiveTexture(TextureUnit.Texture0);
|
||||
_gl.BindTexture(TextureTarget.Texture2D, tex);
|
||||
|
||||
_gl.DrawElementsInstanced(PrimitiveType.Triangles,
|
||||
(uint)sub.IndexCount,
|
||||
DrawElementsType.UnsignedInt,
|
||||
(void*)0,
|
||||
(uint)grp.Count);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore default GL state.
|
||||
_gl.DepthMask(true);
|
||||
_gl.Disable(EnableCap.Blend);
|
||||
_gl.Disable(EnableCap.CullFace);
|
||||
_gl.BindVertexArray(0);
|
||||
}
|
||||
|
||||
// ── Grouping ──────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Iterates all visible landblock entries and groups every (entity, meshRef)
|
||||
/// pair by GfxObjId. Clears previous frame's groups before filling.
|
||||
/// </summary>
|
||||
private void CollectGroups(
|
||||
IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax, IReadOnlyList<WorldEntity> Entities)> landblockEntries,
|
||||
FrustumPlanes? frustum,
|
||||
uint? neverCullLandblockId,
|
||||
HashSet<uint>? visibleCellIds,
|
||||
HashSet<uint>? animatedEntityIds)
|
||||
{
|
||||
foreach (var grp in _groups.Values)
|
||||
grp.Entries.Clear();
|
||||
|
||||
foreach (var entry in landblockEntries)
|
||||
{
|
||||
// L-fix1 (2026-04-28): the landblock cull decision is now
|
||||
// PER-LANDBLOCK boolean, not a continue. We still need to
|
||||
// walk the entity list because animated entities (in
|
||||
// animatedEntityIds) bypass the cull and render anyway.
|
||||
bool landblockVisible = frustum is null
|
||||
|| entry.LandblockId == neverCullLandblockId
|
||||
|| FrustumCuller.IsAabbVisible(frustum.Value, entry.AabbMin, entry.AabbMax);
|
||||
|
||||
// Fast path: no animated entities globally → if landblock is
|
||||
// culled, skip the whole entity list (preserves the original
|
||||
// O(visible-landblocks) cost when the caller doesn't care
|
||||
// about animated bypass).
|
||||
if (!landblockVisible && (animatedEntityIds is null || animatedEntityIds.Count == 0))
|
||||
continue;
|
||||
|
||||
foreach (var entity in entry.Entities)
|
||||
{
|
||||
if (entity.MeshRefs.Count == 0)
|
||||
continue;
|
||||
|
||||
// L-fix1: when the landblock is frustum-culled, only
|
||||
// render entities flagged as animated. This keeps
|
||||
// remote players / NPCs / monsters visible even when
|
||||
// their landblock rotates out of the view frustum.
|
||||
bool isAnimated = animatedEntityIds?.Contains(entity.Id) == true;
|
||||
if (!landblockVisible && !isAnimated)
|
||||
continue;
|
||||
|
||||
// Step 4: portal visibility filter. If we have a visible cell set,
|
||||
// skip interior entities whose parent cell isn't visible.
|
||||
// visibleCellIds == null means camera is outdoors → show all interiors.
|
||||
if (entity.ParentCellId.HasValue && visibleCellIds is not null
|
||||
&& !visibleCellIds.Contains(entity.ParentCellId.Value))
|
||||
continue;
|
||||
|
||||
var entityRoot =
|
||||
Matrix4x4.CreateFromQuaternion(entity.Rotation) *
|
||||
Matrix4x4.CreateTranslation(entity.Position);
|
||||
|
||||
// Hash the entity's PaletteOverride once — shared by every
|
||||
// MeshRef on this entity, so we compute it outside the loop.
|
||||
ulong palHash = HashPaletteOverride(entity.PaletteOverride);
|
||||
|
||||
foreach (var meshRef in entity.MeshRefs)
|
||||
{
|
||||
if (!_gpuByGfxObj.TryGetValue(meshRef.GfxObjId, out var cachedMeshes))
|
||||
continue;
|
||||
|
||||
var model = meshRef.PartTransform * entityRoot;
|
||||
|
||||
// Texture signature = palette hash ^ surface-overrides hash.
|
||||
// Two instances can share a batch only when their ResolveTex
|
||||
// would return identical handles for every sub-mesh — that
|
||||
// means identical palette AND identical surface overrides.
|
||||
ulong surfHash = HashSurfaceOverrides(meshRef.SurfaceOverrides);
|
||||
ulong texSig = palHash ^ surfHash;
|
||||
var key = new GroupKey(meshRef.GfxObjId, texSig);
|
||||
|
||||
if (!_groups.TryGetValue(key, out var group))
|
||||
{
|
||||
group = new InstanceGroup();
|
||||
_groups[key] = group;
|
||||
}
|
||||
|
||||
group.Entries.Add(new InstanceEntry(model, entity, meshRef));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static ulong HashPaletteOverride(AcDream.Core.World.PaletteOverride? p)
|
||||
{
|
||||
if (p is null) return 0UL;
|
||||
ulong h = 0xCBF29CE484222325UL;
|
||||
const ulong prime = 0x100000001B3UL;
|
||||
h = (h ^ p.BasePaletteId) * prime;
|
||||
foreach (var sp in p.SubPalettes)
|
||||
{
|
||||
h = (h ^ sp.SubPaletteId) * prime;
|
||||
h = (h ^ sp.Offset) * prime;
|
||||
h = (h ^ sp.Length) * prime;
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Order-independent hash of a SurfaceOverrides dictionary. XOR of each
|
||||
/// (key, value) pair keeps the result stable regardless of Dictionary
|
||||
/// iteration order, so two instances whose override maps contain the
|
||||
/// same pairs will hash identically.
|
||||
/// </summary>
|
||||
private static ulong HashSurfaceOverrides(IReadOnlyDictionary<uint, uint>? overrides)
|
||||
{
|
||||
if (overrides is null || overrides.Count == 0) return 0UL;
|
||||
ulong acc = 0UL;
|
||||
foreach (var kvp in overrides)
|
||||
{
|
||||
ulong pair = ((ulong)kvp.Key << 32) | kvp.Value;
|
||||
acc ^= pair;
|
||||
}
|
||||
// Fold with a prime so the zero case doesn't collide with "empty".
|
||||
return (acc ^ 0xCBF29CE484222325UL) * 0x100000001B3UL;
|
||||
}
|
||||
|
||||
// ── Matrix write ──────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Writes a System.Numerics Matrix4x4 into <paramref name="buf"/> starting
|
||||
/// at <paramref name="offset"/> as 16 consecutive floats in row-major order
|
||||
/// (the C# natural memory layout). The GLSL shader reads each 4-float row
|
||||
/// as a column of the mat4 — identical to what UniformMatrix4(transpose=false)
|
||||
/// produces for the uniform path.
|
||||
/// </summary>
|
||||
private static void WriteMatrix(float[] buf, int offset, in Matrix4x4 m)
|
||||
{
|
||||
buf[offset + 0] = m.M11; buf[offset + 1] = m.M12; buf[offset + 2] = m.M13; buf[offset + 3] = m.M14;
|
||||
buf[offset + 4] = m.M21; buf[offset + 5] = m.M22; buf[offset + 6] = m.M23; buf[offset + 7] = m.M24;
|
||||
buf[offset + 8] = m.M31; buf[offset + 9] = m.M32; buf[offset + 10] = m.M33; buf[offset + 11] = m.M34;
|
||||
buf[offset + 12] = m.M41; buf[offset + 13] = m.M42; buf[offset + 14] = m.M43; buf[offset + 15] = m.M44;
|
||||
}
|
||||
|
||||
// ── Texture resolution ────────────────────────────────────────────────────
|
||||
|
||||
private uint ResolveTex(WorldEntity entity, MeshRef meshRef, SubMeshGpu sub)
|
||||
{
|
||||
uint overrideOrigTex = 0;
|
||||
bool hasOrigTexOverride = meshRef.SurfaceOverrides is not null
|
||||
&& meshRef.SurfaceOverrides.TryGetValue(sub.SurfaceId, out overrideOrigTex);
|
||||
uint? origTexOverride = hasOrigTexOverride ? overrideOrigTex : (uint?)null;
|
||||
|
||||
if (entity.PaletteOverride is not null)
|
||||
{
|
||||
return _textures.GetOrUploadWithPaletteOverride(
|
||||
sub.SurfaceId, origTexOverride, entity.PaletteOverride);
|
||||
}
|
||||
else if (hasOrigTexOverride)
|
||||
{
|
||||
return _textures.GetOrUploadWithOrigTextureOverride(sub.SurfaceId, overrideOrigTex);
|
||||
}
|
||||
else
|
||||
{
|
||||
return _textures.GetOrUpload(sub.SurfaceId);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Disposal ──────────────────────────────────────────────────────────────
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var subs in _gpuByGfxObj.Values)
|
||||
{
|
||||
foreach (var sub in subs)
|
||||
{
|
||||
_gl.DeleteBuffer(sub.Vbo);
|
||||
_gl.DeleteBuffer(sub.Ebo);
|
||||
_gl.DeleteVertexArray(sub.Vao);
|
||||
}
|
||||
}
|
||||
_gl.DeleteBuffer(_instanceVbo);
|
||||
_gpuByGfxObj.Clear();
|
||||
_groups.Clear();
|
||||
}
|
||||
|
||||
// ── Private types ─────────────────────────────────────────────────────────
|
||||
|
||||
private sealed class SubMeshGpu
|
||||
{
|
||||
public uint Vao;
|
||||
public uint Vbo;
|
||||
public uint Ebo;
|
||||
public int IndexCount;
|
||||
public uint SurfaceId;
|
||||
public TranslucencyKind Translucency;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// All instances of one GfxObj for this frame, plus their starting offset
|
||||
/// in the shared instance VBO (in units of instances, not bytes).
|
||||
/// </summary>
|
||||
private sealed class InstanceGroup
|
||||
{
|
||||
public readonly List<InstanceEntry> Entries = new();
|
||||
public int BufferOffset;
|
||||
|
||||
public int Count => Entries.Count;
|
||||
}
|
||||
|
||||
private readonly struct InstanceEntry
|
||||
{
|
||||
public readonly Matrix4x4 Model;
|
||||
public readonly WorldEntity Entity;
|
||||
public readonly MeshRef MeshRef;
|
||||
|
||||
public InstanceEntry(Matrix4x4 model, WorldEntity entity, MeshRef meshRef)
|
||||
{
|
||||
Model = model;
|
||||
Entity = entity;
|
||||
MeshRef = meshRef;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,293 +0,0 @@
|
|||
// src/AcDream.App/Rendering/StaticMeshRenderer.cs
|
||||
using System.Numerics;
|
||||
using AcDream.Core.Meshing;
|
||||
using AcDream.Core.Terrain;
|
||||
using AcDream.Core.World;
|
||||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
public sealed unsafe class StaticMeshRenderer : IDisposable
|
||||
{
|
||||
private readonly GL _gl;
|
||||
private readonly Shader _shader;
|
||||
private readonly TextureCache _textures;
|
||||
|
||||
// One GPU bundle per unique GfxObj id. Each GfxObj can have multiple sub-meshes.
|
||||
private readonly Dictionary<uint, List<SubMeshGpu>> _gpuByGfxObj = new();
|
||||
|
||||
public StaticMeshRenderer(GL gl, Shader shader, TextureCache textures)
|
||||
{
|
||||
_gl = gl;
|
||||
_shader = shader;
|
||||
_textures = textures;
|
||||
}
|
||||
|
||||
public void EnsureUploaded(uint gfxObjId, IReadOnlyList<GfxObjSubMesh> subMeshes)
|
||||
{
|
||||
if (_gpuByGfxObj.ContainsKey(gfxObjId))
|
||||
return;
|
||||
|
||||
var list = new List<SubMeshGpu>(subMeshes.Count);
|
||||
foreach (var sm in subMeshes)
|
||||
list.Add(UploadSubMesh(sm));
|
||||
_gpuByGfxObj[gfxObjId] = list;
|
||||
}
|
||||
|
||||
private SubMeshGpu UploadSubMesh(GfxObjSubMesh sm)
|
||||
{
|
||||
uint vao = _gl.GenVertexArray();
|
||||
_gl.BindVertexArray(vao);
|
||||
|
||||
uint vbo = _gl.GenBuffer();
|
||||
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, vbo);
|
||||
fixed (void* p = sm.Vertices)
|
||||
_gl.BufferData(BufferTargetARB.ArrayBuffer,
|
||||
(nuint)(sm.Vertices.Length * sizeof(Vertex)), p, BufferUsageARB.StaticDraw);
|
||||
|
||||
uint ebo = _gl.GenBuffer();
|
||||
_gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, ebo);
|
||||
fixed (void* p = sm.Indices)
|
||||
_gl.BufferData(BufferTargetARB.ElementArrayBuffer,
|
||||
(nuint)(sm.Indices.Length * sizeof(uint)), p, BufferUsageARB.StaticDraw);
|
||||
|
||||
uint stride = (uint)sizeof(Vertex);
|
||||
_gl.EnableVertexAttribArray(0);
|
||||
_gl.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, stride, (void*)0);
|
||||
_gl.EnableVertexAttribArray(1);
|
||||
_gl.VertexAttribPointer(1, 3, VertexAttribPointerType.Float, false, stride, (void*)(3 * sizeof(float)));
|
||||
_gl.EnableVertexAttribArray(2);
|
||||
_gl.VertexAttribPointer(2, 2, VertexAttribPointerType.Float, false, stride, (void*)(6 * sizeof(float)));
|
||||
_gl.EnableVertexAttribArray(3);
|
||||
_gl.VertexAttribIPointer(3, 1, VertexAttribIType.UnsignedInt, stride, (void*)(8 * sizeof(float)));
|
||||
|
||||
_gl.BindVertexArray(0);
|
||||
|
||||
return new SubMeshGpu
|
||||
{
|
||||
Vao = vao,
|
||||
Vbo = vbo,
|
||||
Ebo = ebo,
|
||||
IndexCount = sm.Indices.Length,
|
||||
SurfaceId = sm.SurfaceId,
|
||||
// Capture translucency at upload time so the draw loop never
|
||||
// has to look it up from external state.
|
||||
Translucency = sm.Translucency,
|
||||
};
|
||||
}
|
||||
|
||||
public void Draw(ICamera camera,
|
||||
IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax, IReadOnlyList<WorldEntity> Entities)> landblockEntries,
|
||||
FrustumPlanes? frustum = null,
|
||||
uint? neverCullLandblockId = null)
|
||||
{
|
||||
_shader.Use();
|
||||
_shader.SetMatrix4("uView", camera.View);
|
||||
_shader.SetMatrix4("uProjection", camera.Projection);
|
||||
|
||||
// ── Pass 1: Opaque + ClipMap ──────────────────────────────────────────
|
||||
// Depth write on (default). No blending. ClipMap surfaces use the
|
||||
// alpha-discard path in the fragment shader (uTranslucencyKind == 1).
|
||||
foreach (var entry in landblockEntries)
|
||||
{
|
||||
// Per-landblock frustum cull. Never cull the player's landblock.
|
||||
if (frustum is not null &&
|
||||
entry.LandblockId != neverCullLandblockId &&
|
||||
!FrustumCuller.IsAabbVisible(frustum.Value, entry.AabbMin, entry.AabbMax))
|
||||
continue;
|
||||
|
||||
foreach (var entity in entry.Entities)
|
||||
{
|
||||
if (entity.MeshRefs.Count == 0)
|
||||
continue;
|
||||
|
||||
foreach (var meshRef in entity.MeshRefs)
|
||||
{
|
||||
if (!_gpuByGfxObj.TryGetValue(meshRef.GfxObjId, out var subMeshes))
|
||||
continue;
|
||||
|
||||
var entityRoot =
|
||||
Matrix4x4.CreateFromQuaternion(entity.Rotation) *
|
||||
Matrix4x4.CreateTranslation(entity.Position);
|
||||
var model = meshRef.PartTransform * entityRoot;
|
||||
_shader.SetMatrix4("uModel", model);
|
||||
|
||||
foreach (var sub in subMeshes)
|
||||
{
|
||||
// Skip translucent sub-meshes in the first pass.
|
||||
if (sub.Translucency != TranslucencyKind.Opaque &&
|
||||
sub.Translucency != TranslucencyKind.ClipMap)
|
||||
continue;
|
||||
|
||||
_shader.SetInt("uTranslucencyKind", (int)sub.Translucency);
|
||||
|
||||
uint tex = ResolveTex(entity, meshRef, sub);
|
||||
_gl.ActiveTexture(TextureUnit.Texture0);
|
||||
_gl.BindTexture(TextureTarget.Texture2D, tex);
|
||||
|
||||
_gl.BindVertexArray(sub.Vao);
|
||||
_gl.DrawElements(PrimitiveType.Triangles, (uint)sub.IndexCount, DrawElementsType.UnsignedInt, (void*)0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Pass 2: Translucent (AlphaBlend, Additive, InvAlpha) ─────────────
|
||||
// Depth test on so translucents composite correctly behind opaque geometry.
|
||||
// Depth write OFF so translucents don't occlude each other or downstream
|
||||
// opaque draws. Blend function is set per-draw based on TranslucencyKind.
|
||||
//
|
||||
// NOTE: translucent draws are NOT sorted by depth — overlapping translucent
|
||||
// surfaces can composite in the wrong order. Portal-sized billboards don't
|
||||
// overlap in practice so this is acceptable and avoids a larger refactor.
|
||||
_gl.Enable(EnableCap.Blend);
|
||||
_gl.DepthMask(false);
|
||||
|
||||
// Phase 9.2: enable back-face culling for the translucent pass so
|
||||
// closed-shell translucents (lifestone crystal, glow gems, any
|
||||
// convex blended mesh) don't draw their back faces over their
|
||||
// front faces in arbitrary iteration order. Without this, the
|
||||
// 58 triangles of the lifestone crystal composited with an
|
||||
// "inside-out" look where the user saw through one face into
|
||||
// the hollow interior. With back-face culling on, back faces are
|
||||
// dropped at rasterization time, front faces composite as-is,
|
||||
// and depth ordering within the front-facing subset is a
|
||||
// non-issue for closed convex-ish shells. Matches WorldBuilder's
|
||||
// per-batch CullMode handling in
|
||||
// references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/
|
||||
// BaseObjectRenderManager.cs:361-365.
|
||||
//
|
||||
// Our fan triangulation emits pos-side polygons as
|
||||
// (0, i, i+1) which is CCW in standard OpenGL conventions, so
|
||||
// GL_BACK + CCW front is the correct state. Neg-side polygons
|
||||
// (if any) use reversed winding and get culled here — that's a
|
||||
// known limitation and matches the opaque-pass behavior since
|
||||
// neg-side polys are virtually never translucent in AC content.
|
||||
_gl.Enable(EnableCap.CullFace);
|
||||
_gl.CullFace(TriangleFace.Back);
|
||||
_gl.FrontFace(FrontFaceDirection.Ccw);
|
||||
|
||||
foreach (var entry in landblockEntries)
|
||||
{
|
||||
// Same per-landblock frustum cull for pass 2.
|
||||
if (frustum is not null &&
|
||||
entry.LandblockId != neverCullLandblockId &&
|
||||
!FrustumCuller.IsAabbVisible(frustum.Value, entry.AabbMin, entry.AabbMax))
|
||||
continue;
|
||||
|
||||
foreach (var entity in entry.Entities)
|
||||
{
|
||||
if (entity.MeshRefs.Count == 0)
|
||||
continue;
|
||||
|
||||
foreach (var meshRef in entity.MeshRefs)
|
||||
{
|
||||
if (!_gpuByGfxObj.TryGetValue(meshRef.GfxObjId, out var subMeshes))
|
||||
continue;
|
||||
|
||||
var entityRoot =
|
||||
Matrix4x4.CreateFromQuaternion(entity.Rotation) *
|
||||
Matrix4x4.CreateTranslation(entity.Position);
|
||||
var model = meshRef.PartTransform * entityRoot;
|
||||
_shader.SetMatrix4("uModel", model);
|
||||
|
||||
foreach (var sub in subMeshes)
|
||||
{
|
||||
if (sub.Translucency == TranslucencyKind.Opaque ||
|
||||
sub.Translucency == TranslucencyKind.ClipMap)
|
||||
continue;
|
||||
|
||||
// Set per-draw blend function.
|
||||
switch (sub.Translucency)
|
||||
{
|
||||
case TranslucencyKind.Additive:
|
||||
// src*a + dst — portal swirls, glows
|
||||
_gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.One);
|
||||
break;
|
||||
|
||||
case TranslucencyKind.InvAlpha:
|
||||
// src*(1-a) + dst*a
|
||||
_gl.BlendFunc(BlendingFactor.OneMinusSrcAlpha, BlendingFactor.SrcAlpha);
|
||||
break;
|
||||
|
||||
default: // AlphaBlend
|
||||
// src*a + dst*(1-a)
|
||||
_gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
|
||||
break;
|
||||
}
|
||||
|
||||
_shader.SetInt("uTranslucencyKind", (int)sub.Translucency);
|
||||
|
||||
uint tex = ResolveTex(entity, meshRef, sub);
|
||||
_gl.ActiveTexture(TextureUnit.Texture0);
|
||||
_gl.BindTexture(TextureTarget.Texture2D, tex);
|
||||
|
||||
_gl.BindVertexArray(sub.Vao);
|
||||
_gl.DrawElements(PrimitiveType.Triangles, (uint)sub.IndexCount, DrawElementsType.UnsignedInt, (void*)0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Restore default GL state for subsequent renderers (terrain etc.).
|
||||
_gl.DepthMask(true);
|
||||
_gl.Disable(EnableCap.Blend);
|
||||
_gl.Disable(EnableCap.CullFace);
|
||||
|
||||
_gl.BindVertexArray(0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the GL texture id for a sub-mesh, honouring palette and
|
||||
/// texture overrides carried on the entity and the mesh-ref.
|
||||
/// </summary>
|
||||
private uint ResolveTex(WorldEntity entity, MeshRef meshRef, SubMeshGpu sub)
|
||||
{
|
||||
uint overrideOrigTex = 0;
|
||||
bool hasOrigTexOverride = meshRef.SurfaceOverrides is not null
|
||||
&& meshRef.SurfaceOverrides.TryGetValue(sub.SurfaceId, out overrideOrigTex);
|
||||
uint? origTexOverride = hasOrigTexOverride ? overrideOrigTex : (uint?)null;
|
||||
|
||||
if (entity.PaletteOverride is not null)
|
||||
{
|
||||
return _textures.GetOrUploadWithPaletteOverride(
|
||||
sub.SurfaceId, origTexOverride, entity.PaletteOverride);
|
||||
}
|
||||
else if (hasOrigTexOverride)
|
||||
{
|
||||
return _textures.GetOrUploadWithOrigTextureOverride(sub.SurfaceId, overrideOrigTex);
|
||||
}
|
||||
else
|
||||
{
|
||||
return _textures.GetOrUpload(sub.SurfaceId);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var subs in _gpuByGfxObj.Values)
|
||||
{
|
||||
foreach (var sub in subs)
|
||||
{
|
||||
_gl.DeleteBuffer(sub.Vbo);
|
||||
_gl.DeleteBuffer(sub.Ebo);
|
||||
_gl.DeleteVertexArray(sub.Vao);
|
||||
}
|
||||
}
|
||||
_gpuByGfxObj.Clear();
|
||||
}
|
||||
|
||||
private sealed class SubMeshGpu
|
||||
{
|
||||
public uint Vao;
|
||||
public uint Vbo;
|
||||
public uint Ebo;
|
||||
public int IndexCount;
|
||||
public uint SurfaceId;
|
||||
/// <summary>
|
||||
/// Cached from GfxObjSubMesh.Translucency at upload time.
|
||||
/// Avoids any per-draw lookup into external state.
|
||||
/// </summary>
|
||||
public TranslucencyKind Translucency;
|
||||
}
|
||||
}
|
||||
|
|
@ -13,26 +13,29 @@ namespace AcDream.App.Rendering.Wb;
|
|||
/// <summary>
|
||||
/// Draws entities using WB's <see cref="ObjectRenderData"/> (a single global
|
||||
/// VAO/VBO/IBO under modern rendering) with acdream's <see cref="TextureCache"/>
|
||||
/// for texture resolution and <see cref="AcSurfaceMetadataTable"/> for
|
||||
/// for bindless texture resolution and <see cref="AcSurfaceMetadataTable"/> for
|
||||
/// translucency classification.
|
||||
///
|
||||
/// <para>
|
||||
/// <b>Atlas-tier</b> entities (<c>ServerGuid == 0</c>): mesh data comes from WB's
|
||||
/// <see cref="ObjectMeshManager"/> via <see cref="WbMeshAdapter.TryGetRenderData"/>.
|
||||
/// Textures resolve through <see cref="TextureCache.GetOrUpload"/> using the batch's
|
||||
/// <c>SurfaceId</c>.
|
||||
/// Textures resolve through the bindless-suffixed
|
||||
/// <see cref="TextureCache.GetOrUploadBindless"/> variants, returning 64-bit
|
||||
/// resident handles stored in the per-group SSBO.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// <b>Per-instance-tier</b> entities (<c>ServerGuid != 0</c>): mesh data also from
|
||||
/// WB, but textures resolve through <see cref="TextureCache"/> with palette and
|
||||
/// surface overrides applied. <see cref="AnimatedEntityState"/> is currently
|
||||
/// WB, but textures resolve through
|
||||
/// <see cref="TextureCache.GetOrUploadWithPaletteOverrideBindless"/> with palette
|
||||
/// and surface overrides applied. <see cref="AnimatedEntityState"/> is currently
|
||||
/// unused at draw time — GameWindow's spawn path already bakes AnimPartChanges +
|
||||
/// GfxObjDegradeResolver (Issue #47 close-detail mesh) into <c>MeshRefs</c>.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// <b>GL strategy (N.5):</b> <c>glMultiDrawElementsIndirect</c> with SSBOs.
|
||||
/// <b>GL strategy (N.5 — mandatory):</b> <c>glMultiDrawElementsIndirect</c> with SSBOs
|
||||
/// and <c>GL_ARB_bindless_texture</c> + <c>GL_ARB_shader_draw_parameters</c>.
|
||||
/// All visible (entity, batch) pairs are bucketed by <see cref="GroupKey"/>;
|
||||
/// each group becomes one <c>DrawElementsIndirectCommand</c>. Three GPU buffers
|
||||
/// are uploaded per frame: instance matrices (SSBO binding 0), per-group batch
|
||||
|
|
@ -42,17 +45,17 @@ namespace AcDream.App.Rendering.Wb;
|
|||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// <b>Shader:</b> <c>mesh_modern</c> when bindless + ARB_shader_draw_parameters
|
||||
/// are available (N.5 path). Falls back to <c>mesh_instanced</c> when the GPU
|
||||
/// lacks those extensions.
|
||||
/// <b>Shader:</b> <c>mesh_modern</c> (bindless + <c>gl_DrawIDARB</c> /
|
||||
/// <c>gl_BaseInstanceARB</c>). Missing bindless/draw-parameters throws
|
||||
/// <see cref="NotSupportedException"/> at startup — there is no legacy fallback.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// <b>Modern rendering assumption:</b> WB's <c>_useModernRendering</c> path (GL
|
||||
/// 4.3 + bindless) puts every mesh in a single shared VAO/VBO/IBO and uses
|
||||
/// <c>FirstIndex</c> + <c>BaseVertex</c> per batch. The dispatcher honors those
|
||||
/// offsets via <c>DrawElementsInstancedBaseVertex(BaseInstance)</c>. The legacy
|
||||
/// per-mesh-VAO path also works since FirstIndex/BaseVertex are zero there.
|
||||
/// offsets inside each <c>DrawElementsIndirectCommand</c> via
|
||||
/// <c>glMultiDrawElementsIndirect</c>.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed unsafe class WbDrawDispatcher : IDisposable
|
||||
|
|
|
|||
|
|
@ -1,39 +0,0 @@
|
|||
namespace AcDream.App.Rendering.Wb;
|
||||
|
||||
/// <summary>
|
||||
/// Process-lifetime cache of <c>ACDREAM_USE_WB_FOUNDATION</c> env var.
|
||||
/// Read once at static-init time; all consumers import this rather than
|
||||
/// re-reading the env var per call (env-var lookups on Windows are not
|
||||
/// free at hot-path cadence).
|
||||
///
|
||||
/// <para>
|
||||
/// <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") != "0";
|
||||
|
||||
public static bool IsEnabled => _isEnabled;
|
||||
|
||||
/// <summary>
|
||||
/// FOR TESTS ONLY. Forces <see cref="IsEnabled"/> to <c>true</c> so
|
||||
/// integration tests can exercise the WB adapter path without having to
|
||||
/// set the env var before static initialisation. Never call from
|
||||
/// production code.
|
||||
/// </summary>
|
||||
internal static void ForTestsOnly_ForceEnable() => _isEnabled = true;
|
||||
}
|
||||
|
|
@ -144,7 +144,7 @@ public sealed class GpuWorldState
|
|||
}
|
||||
|
||||
_loaded[landblock.LandblockId] = landblock;
|
||||
if (WbFoundationFlag.IsEnabled && _wbSpawnAdapter is not null)
|
||||
if (_wbSpawnAdapter is not null)
|
||||
_wbSpawnAdapter.OnLandblockLoaded(_loaded[landblock.LandblockId]);
|
||||
RebuildFlatView();
|
||||
}
|
||||
|
|
@ -195,7 +195,7 @@ public sealed class GpuWorldState
|
|||
|
||||
public void RemoveLandblock(uint landblockId)
|
||||
{
|
||||
if (WbFoundationFlag.IsEnabled && _wbSpawnAdapter is not null)
|
||||
if (_wbSpawnAdapter is not null)
|
||||
_wbSpawnAdapter.OnLandblockUnloaded(landblockId);
|
||||
|
||||
// Rescue persistent entities before removal. These get appended
|
||||
|
|
|
|||
|
|
@ -19,16 +19,9 @@ namespace AcDream.Core.Tests.Rendering.Wb;
|
|||
/// </summary>
|
||||
public sealed class PendingSpawnIntegrationTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Force-enable WbFoundationFlag for this test class.
|
||||
/// GpuWorldState gates its adapter calls on this static-cached flag;
|
||||
/// calling the internal test hook lets us exercise the full integration
|
||||
/// path without needing the env var set before process startup.
|
||||
/// </summary>
|
||||
static PendingSpawnIntegrationTests()
|
||||
{
|
||||
WbFoundationFlag.ForTestsOnly_ForceEnable();
|
||||
}
|
||||
// N.5 ship amendment: WbFoundationFlag was deleted — GpuWorldState
|
||||
// no longer gates adapter calls on the flag; they are unconditional
|
||||
// when the adapter is non-null. No static ctor hook needed.
|
||||
|
||||
[Fact]
|
||||
public void LiveEntity_ParkedBeforeLandblock_DrainsButIsNotRegisteredWithAdapter()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue