Commit graph

662 commits

Author SHA1 Message Date
Erik
cfe1ca3151 phase(N.5) Task 11: translucency partition contract test
Locks in Decision 2 (Opaque + ClipMap → opaque indirect; AlphaBlend +
Additive + InvAlpha → transparent indirect). Catches future refactors
that drift the partition — silent visual regression otherwise (groups
rendered in the wrong pass with the wrong blend state).

Adds public static IsOpaquePublic shim on WbDrawDispatcher; the
underlying IsOpaque stays private.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 20:53:36 +02:00
Erik
f533414edf phase(N.5) Task 10: glMultiDrawElementsIndirect dispatch — visual verified
Replaces WbDrawDispatcher's per-group glDrawElementsInstancedBaseVertexBaseInstance
loop with two glMultiDrawElementsIndirect calls (opaque + transparent).
Per-frame uploads three SSBOs:
- _instanceSsbo @ binding=0 (mat4 per instance, indexed by gl_BaseInstanceARB + gl_InstanceID)
- _batchSsbo @ binding=1 (BatchData per group, indexed by gl_DrawIDARB)
- _indirectBuffer (DrawElementsIndirectCommand[] — opaque first, transparent second)

GameWindow swaps the shader load to mesh_modern when _bindlessSupport
is non-null. Capability detection + shader load now run in the right
order (capability before TextureCache + before Shader).

Deletes the obsolete DrawGroup stub, EnsureInstanceAttribs, _instanceBuffer,
_patchedVaos. ClassifyBatches + ResolveTexture already migrated in
Task 8 to use ulong bindless handles.

BuildIndirectArrays (Task 9) wired in: _opaqueDraws + _translucentDraws
are flattened into IndirectGroupInput[], laid out via the helper into
contiguous indirect commands + parallel BatchData[]. opaqueByteOffset=0,
transparentByteOffset = opaqueCount × DrawCommandStride.

Visual verification (USER GATE) PASS: Holtburg courtyard renders
identical to N.4 — terrain, scenery, characters, NPCs all visible
without artifacts. [N.5] modern path capabilities present + mesh_modern
shader loaded log lines confirm the boot path. [WB-DIAG] hot-path
counters show healthy entity/draw activity.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 20:51:49 +02:00
Erik
b163c53622 phase(N.5) Task 9 fixup: layout assertion + DrawCommandStride const
Code quality review caught:
- sizeofDEIC was a local; promoted to public const DrawCommandStride
  so tests can reference it symbolically.
- BatchDataPublic layout invariant (size + field offsets) wasn't
  asserted in tests. Added BatchDataPublic_LayoutMatchesPrivateBatchData
  + DrawCommandStride_MatchesStructSize tests to gate Task 10's
  MemoryMarshal.Cast<BatchData, BatchDataPublic> safety.
- Plan doc updated: BatchDataPublic spec was Pack=4 (wrong — must
  match private BatchData's Pack=8 for the cast to work). Implementation
  was already correct; plan now matches.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 20:42:49 +02:00
Erik
9a7a250b62 phase(N.5) Task 9: BuildIndirectArrays — CPU layout for indirect dispatch
Pure CPU helper that lays out a group list into a contiguous indirect
buffer (DrawElementsIndirectCommand[]) and parallel BatchData[] —
opaque section first, transparent section second. Returns counts +
byte offset for the transparent section.

Tests cover: spec §5 walk-through layout; empty group list edge case;
ClipMap classification (treated as opaque, not transparent).

Static + public so tests can exercise without a GL context. Task 10
wires it into the rewritten Draw() method.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 20:38:22 +02:00
Erik
424d7b9015 phase(N.5) Task 8: InstanceGroup + GroupKey carry bindless handle + layer
Replaces uint TextureHandle (32-bit GL name) with ulong
BindlessTextureHandle (64-bit) in InstanceGroup + GroupKey + ResolveTexture
return type. Adds TextureLayer (always 0 for per-instance composites,
becomes meaningful when WB atlas is adopted in N.6).

ClassifyBatches now calls TextureCache.GetOrUpload*Bindless variants —
these return Texture2DArray-backed bindless handles (Task 3 work).

DrawGroup body throws NotImplementedException — Task 10 rewrites the
whole Draw() method to use glMultiDrawElementsIndirect, which makes
DrawGroup obsolete. CPU-only tests don't invoke DrawGroup so the build
+ test gates stay green; visual launch fails until Task 10 (intentional).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 20:32:38 +02:00
Erik
1b6995d2df phase(N.5) Task 7 fixup: BatchData Pack=8 for ulong alignment
Code quality review caught that BatchData uses Pack=4 but contains a
ulong field. With the current field order (TextureHandle first), offset
0 is always 8-byte aligned so std430 works. But adding a 4-byte field
before TextureHandle without bumping Pack would silently misalign the
GPU struct. Pack=8 makes the alignment requirement explicit and adds
a comment documenting expected std430 offsets.

No runtime change — current offsets (0/8/12) are identical under both
Pack values for this field order.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 20:29:58 +02:00
Erik
86c471d2d1 phase(N.5) Task 7: dispatcher SSBO + indirect buffer infrastructure
Adds DrawElementsIndirectCommand struct (20-byte layout for
glMultiDrawElementsIndirect). Replaces _instanceVbo field on
WbDrawDispatcher with three buffers: _instanceSsbo (mat4[]),
_batchSsbo (BatchData[]), _indirectBuffer (DEIC[]). Adds BindlessSupport
constructor parameter — non-null required since the dispatcher is only
constructed when WB foundation is on (which implies bindless is present
per Task 6 capability detection).

Existing Draw() method substitutes _instanceVbo -> _instanceSsbo for
compile. Behavior is temporarily wrong (SSBO bound as ArrayBuffer for
per-vertex attribs); Tasks 9-10 fully rewrite the draw loop and the
per-frame uploads to use BindBufferBase + glMultiDrawElementsIndirect.

GameWindow construction site updated to add _bindlessSupport guard and
pass it as the new last argument to the constructor. Dispatcher is only
constructed when bindless is guaranteed present.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 20:25:29 +02:00
Erik
12170f9d78 phase(N.5) Task 6 fixup: log symmetry + Silk extension shortcut
Code quality review caught:
- Silent failure when ARB_bindless_texture absent — the && short-circuit
  meant the most common fallback case (no bindless on the GPU) had no
  log, while ARB_shader_draw_parameters absent did log. Restructured to
  three nested ifs so each failure path logs symmetrically.
- Redundant `bindless is not null` guard removed (TryCreate's non-null
  guarantee covers it; the nested-if structure makes this implicit).
- HasShaderDrawParameters in BindlessSupport.cs replaced its manual
  GL_NUM_EXTENSIONS scan with `gl.IsExtensionPresent(...)` — same
  pattern WB uses, less code.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 20:21:10 +02:00
Erik
93ebd9e433 phase(N.5) Task 6: GameWindow capability detection + plumb BindlessSupport
Detects ARB_bindless_texture + ARB_shader_draw_parameters at startup
when WbFoundationFlag is enabled. Stores BindlessSupport on GameWindow
and passes it to TextureCache so the parallel Texture2DArray upload
path is available to future bindless callers.

Mesh shader load remains mesh_instanced for now — Task 10 swaps to
mesh_modern after Tasks 7-9 rewire the dispatcher to consume the
bindless + SSBO + indirect machinery.

Capability missing → BindlessSupport stays null → TextureCache runs
without the bindless path → legacy callers (StaticMeshRenderer,
InstancedMeshRenderer, ParticleRenderer, current WbDrawDispatcher
draw loop) are unaffected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 20:15:06 +02:00
Erik
166af9a53e phase(N.5) Task 5 fixup: shader doc + extension cleanup
Code quality review caught four issues:
- Unnecessary GL_ARB_bindless_texture extension in mesh_modern.vert
  (vert doesn't use bindless types). Removed; only the frag needs it.
- SSBO binding=1 (BatchBuffer) and UBO binding=1 (SceneLighting) are
  in distinct GL namespaces — added a comment in the vert documenting
  this so Task 10's bind site doesn't get confused.
- Misleading "0=opaque, 1=transparent" comment expanded to spell out
  the full Decision 2 two-pass alpha-test logic and what each discard
  threshold protects against.
- BatchData.flags field is reserved; documented that N.5's dispatcher
  owns all blend state, with a hook for future shader-side additive.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 20:11:03 +02:00
Erik
aad2aa67da phase(N.5) Task 5: mesh_modern.vert + .frag — bindless + SSBO + indirect
New entity shaders for the WB modern rendering path. Modeled on WB's
StaticObjectModern.* but adapted to acdream's lighting model:
- Drops uActiveCells (we cull cells on CPU in WbDrawDispatcher)
- Drops uDrawIDOffset (full passes, no pagination)
- Drops uHighlightColor (deferred to Phase B.4 follow-up; field reserved
  in InstanceData struct comment)
- Preserves mesh_instanced's SceneLighting UBO at binding=1 with 8 lights,
  fog params, lightning flash, per-channel clamp — full visual identity

vert reads InstanceData[] @ binding=0 indexed by gl_BaseInstanceARB +
gl_InstanceID for the per-entity model matrix; reads BatchData[] @
binding=1 indexed by gl_DrawIDARB for the per-group bindless texture
handle + layer.

frag samples sampler2DArray reconstructed from a uvec2 bindless handle
+ uint layer. uRenderPass uniform picks two-pass alpha-test thresholds:
0 = opaque (discard alpha<0.95), 1 = transparent (discard alpha>=0.95
and alpha<0.05).

Not yet wired to the dispatcher — Task 6 sets up shader load + capability
detection in GameWindow; Task 7-10 rewrite the dispatcher to use SSBO +
glMultiDrawElementsIndirect.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 20:05:35 +02:00
Erik
6f90997a43 docs(N.5): plan amendment — Task 5 shader matches mesh_instanced lighting
Original Task 5 draft used hardcoded vec3 ambient/sun uniforms in
mesh_modern.frag. Reading actual mesh_instanced.frag revealed it uses
a SceneLighting UBO at binding=1 with 8 lights, fog params (start/end/
lightning/mode), fog color, camera/time, and per-channel clamp.

Revised: mesh_modern.frag preserves the full SceneLighting UBO +
accumulateLights + applyFog + lightning flash + per-channel clamp.
mesh_modern.vert adds vWorldPos output (consumed by accumulateLights
and applyFog). Visual identity to N.4's lighting model preserved.

Two-pass alpha-test (N.5 Decision 2) sits inside the same shader,
gated by uRenderPass instead of uTranslucencyKind.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 20:03:12 +02:00
Erik
0bfe536858 phase(N.5) Task 3+4 fixup: two-phase Dispose + doc consistency
Code quality review caught four issues:
- Critical: Dispose interleaved MakeNonResident + DeleteTexture per
  entry, violating ARB_bindless_texture's "all handles non-resident
  before any texture deletion" requirement. Reordered to two phases:
  Phase 1 makes ALL bindless handles non-resident; Phase 2 deletes
  ALL bindless textures; Phase 3 deletes legacy Texture2D textures.
- Important: per-call _bindless?.MakeNonResident replaced with a
  single if (_bindless is not null) guard around the whole Phase 1
  block — cleaner reasoning, one null check.
- Minor: test contract comment referenced wrong task number for
  visual gate; corrected to match current plan.
- Minor: two abbreviated XML docs (GetOrUploadWithOrigTextureOverrideBindless,
  GetOrUploadWithPaletteOverrideBindless) expanded to mention the
  throw-on-null-bindless contract for IDE readers.

This fixup also completes Task 4's Dispose work — Task 4 will be
marked complete since this commit does its full job.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 19:59:10 +02:00
Erik
0d96716825 phase(N.5) Task 3: TextureCache bindless GetOrUpload + parallel cache
Adds three Bindless variants (GetOrUploadBindless,
GetOrUploadWithOrigTextureOverrideBindless,
GetOrUploadWithPaletteOverrideBindless) that decode + upload via
UploadRgba8AsLayer1Array (Texture2DArray) and cache in three new
dictionaries that mirror the legacy three-cache structure. Each entry
stores both the GL texture name (for Dispose cleanup in Task 4) and
the resident bindless handle.

Constructor gains optional BindlessSupport param; null keeps backward
compat. EnsureBindlessAvailable throws InvalidOperationException if
Bindless* methods are called without BindlessSupport (fail-fast vs
silent zero handle that would produce GPU faults).

Dispose extended to make handles non-resident before deleting the
underlying Texture2DArray names (bindless handles must be made
non-resident before the texture is deleted; skipping this causes
GPU faults on driver cleanup).

Marker test in TextureCacheBindlessTests documents the throw contract
for future engineers; real bindless integration is verified at
Task 14's visual gate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 19:53:10 +02:00
Erik
4b9a9bb721 docs(N.5): plan amendment — Task 3+4 use parallel bindless caches
Original Task 3 had Bindless* methods calling the legacy Texture2D
GetOrUpload* and converting the GL name to a bindless handle —
producing a sampler2D texture sampled via sampler2DArray (GLSL type
mismatch).

Revised: Task 3 introduces three parallel cache dictionaries
(_bindlessBySurfaceId / _bindlessByOverridden / _bindlessByPalette)
storing both the GL texture name and the resident handle. Bindless*
methods call DecodeFromDats + UploadRgba8AsLayer1Array directly with
their own caching; legacy three-cache structure mirrored exactly.

Task 4 (Dispose) updated to: (1) MakeNonResident on every bindless
handle FIRST, (2) DeleteTexture on every Texture2DArray name, (3)
DeleteTexture on every legacy Texture2D handle. Order matters per
ARB_bindless_texture spec.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 19:50:36 +02:00
Erik
0b73875d39 phase(N.5) Task 2 fixup: name TexImage3D depth + border arguments
Code quality review caught that the TexImage3D call dropped the
depth: and border: named arguments specified in the plan. The bare
positional `1, 0` is hard to disambiguate from the surrounding 10
parameters. Adds them back, no runtime change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 19:48:00 +02:00
Erik
f48a6cf65c phase(N.5) Task 2: parallel Texture2DArray upload path in TextureCache
Adds UploadRgba8AsLayer1Array — uploads pixel data as a 1-layer
Texture2DArray. Existing UploadRgba8 (Texture2D) untouched, so legacy
callers (StaticMeshRenderer, InstancedMeshRenderer, ParticleRenderer,
WbDrawDispatcher's pre-rewrite path) keep working unchanged.

Required for Task 3's Bindless* methods which need the Texture2DArray
target so the WB modern shader can sample via sampler2DArray. Same
surface may be uploaded both ways during the N.5/N.6 transition;
doubling is bounded and acceptable. After N.6 retires legacy
renderers entirely, the legacy UploadRgba8 becomes unused and is
deleted.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 19:44:10 +02:00
Erik
aba2cfc3b6 docs(N.5): plan amendment — Task 2 uses parallel upload path, not replace
Implementer caught that the original Task 2 (replace UploadRgba8 target
with Texture2DArray) would break four legacy consumers whose shaders
sample via sampler2D: WbDrawDispatcher (pre-rewrite path),
StaticMeshRenderer, InstancedMeshRenderer (legacy escape hatch),
ParticleRenderer.

Revised: Task 2 ADDS a parallel UploadRgba8AsLayer1Array. Existing
UploadRgba8 (Texture2D) stays for legacy callers. Task 3's Bindless*
methods will call the new array path with their own cache dictionaries.
Same surface may be uploaded twice during transition; bounded cost.
N.6 cleanup deletes the legacy path.

Task 3 will be amended at dispatch time to reflect parallel caches.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 19:42:18 +02:00
Erik
3a88c361ce phase(N.5) Task 1 fixup: remove unused _gl field + IsAvailable
Code quality review caught three related issues:
- _gl field stored but never used (TreatWarningsAsErrors=true would
  catch this on a clean build, but better to fix it before it bites)
- GL constructor parameter became unused after dropping _gl
- IsAvailable => true is misleading: TryCreate's out parameter is
  the canonical signal, the property carries no information

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 19:35:32 +02:00
Erik
d8c7bf67d8 docs(N.5): plan amendment — clarify Task 1 vs Task 3 file ownership
The TextureCacheBindlessTests.cs file is created in Task 3 (where it
gets meaningful test cases), not Task 1. Removed it from Task 1's
Files list and added an explicit note. Caught during Task 1 code review.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 19:34:38 +02:00
Erik
4d1a7977cb phase(N.5) Task 1: ArbBindlessTexture wrapper + capability detection
Adds Silk.NET.OpenGL.Extensions.ARB 2.23.0 package and a thin
BindlessSupport wrapper exposing GetResidentHandle / MakeNonResident /
HasShaderDrawParameters. TryCreate returns false if the bindless
extension isn't present, letting WbFoundationFlag fall back to legacy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 19:31:02 +02:00
Erik
69c6c03d10 docs(N.5): implementation plan — 19 tasks, TDD where applicable
Plan at docs/superpowers/plans/2026-05-08-phase-n5-modern-rendering.md
covers task-by-task execution of the N.5 design spec. Structure:

- Task 1: ArbBindlessTexture package + BindlessSupport wrapper
- Task 2: TextureCache uploads as 1-layer Texture2DArray
- Task 3: Bindless GetOrUpload* methods (3 variants)
- Task 4: Dispose order (handles before textures)
- Task 5: mesh_modern.vert + .frag shaders
- Task 6: GameWindow capability detection + plumb to TextureCache
- Task 7: WbDrawDispatcher SSBO + indirect buffer infrastructure
- Task 8: InstanceGroup + GroupKey carry bindless handle
- Task 9: BuildIndirectArrays helper (TDD, pure CPU, public for tests)
- Task 10: glMultiDrawElementsIndirect dispatch + visual verification
- Task 11: Translucency partition test
- Task 12: CPU stopwatch + GL_TIME_ELAPSED queries
- Task 13: Perf baseline capture (USER GATE)
- Task 14: Visual verification at Holtburg + Foundry + magic content
- Task 15: Delete legacy mesh_instanced shaders
- Task 16: CLAUDE.md WB integration cribs update
- Task 17: Memory + roadmap update
- Task 18: Plan finalization (SHIP record)
- Task 19: SHIP commit

Each task has TDD steps where applicable (failing test → impl → pass →
commit). Non-testable shader / integration tasks have build + visual
gates. Self-review checklist at bottom maps every spec decision to its
implementing task(s).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 19:27:20 +02:00
Erik
1834b16cd1 docs(N.5): design spec — bindless + multi-draw indirect on N.4 dispatcher
Brainstormed 2026-05-08 over 8 design questions. Captures:

- Texture model: sampler2DArray for ALL textures (1-layer wrap for
  per-instance composites). Matches WB's modern shader, future-proofs
  for atlas adoption in N.6+.
- Translucency: WB's two-pass alpha-test (no native Additive on GfxObj
  surfaces; falsifiable at visual verification).
- Data delivery: all-SSBO. Instances[] at binding=0, Batches[] at
  binding=1. Indexed by gl_BaseInstanceARB+gl_InstanceID and
  gl_DrawIDARB respectively.
- Bindless residency: resident on upload, never release. Bounded
  content; instrument under ACDREAM_WB_DIAG=1.
- Escape hatch: two-way flag preserved. N.5 replaces N.4's draw method
  in place; legacy InstancedMeshRenderer remains the safety net.
- Perf measurement: CPU stopwatch + GL_TIME_ELAPSED queries, logged
  via [WB-DIAG]. Acceptance gates pasted into SHIP commit.
- Persistent-mapped buffers: deferred to N.6.
- Per-instance highlight (selection blink): deferred; field reserved
  in InstanceData for Phase B.4 follow-up.

Spec at docs/superpowers/specs/2026-05-08-phase-n5-modern-rendering-design.md
covers architecture, components, per-frame data flow walk-through,
translucent rendering, error handling + fallback, testing + acceptance,
risks, and explicit out-of-scope list. Plan + task breakdown comes next.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 19:15:30 +02:00
Erik
c1e31148bb Merge branch 'claude/tender-mcclintock-a16839' — N.4 SHIP + N.5 handoff
Phase N.4 (Rendering Pipeline Foundation) shipped. WB's ObjectMeshManager
is now acdream's production mesh pipeline; WbDrawDispatcher is the
production draw path; ACDREAM_USE_WB_FOUNDATION default-on.

9 commits including: Adjustment 6 (PartOverrides + HiddenPartsMask),
Tasks 22+23 (WbDrawDispatcher + side-table), three Task 26 fixups
(load triggers, batch.Key.SurfaceId, FirstIndex/BaseVertex + #47),
perf pass 1-4 (sort, cull, hash memo), final SHIP commit (flag
default-on + roadmap + memory + CLAUDE.md), and N.5 cold-start
handoff for next session.
2026-05-08 18:14:16 +02:00
Erik
dd5ca3d2b2 docs(N.5): cold-start handoff for next session
Detailed briefing for the next agent picking up Phase N.5 (Modern
Rendering Path: bindless textures + glMultiDrawElementsIndirect on
N.4's foundation). Covers:

- Where N.4 left things (commits, what works, gotchas inherited)
- The two-feature pairing (why bindless + indirect together)
- Files to read first (WB shaders, our dispatcher, CLAUDE.md cribs)
- 8 brainstorm questions to resolve before spec
- Spec + plan structure (matching N.4's pattern)
- Acceptance criteria
- Things to explicitly NOT do

Sized for a fresh session to pick up cold without spelunking through
months of session history.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-08 18:05:36 +02:00
Erik
c44536451d 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>
2026-05-08 18:01:23 +02:00
Erik
573526dae5 phase(N.4): WbDrawDispatcher perf pass — sort, cull, hash memoization
Four small wins on top of the grouped-instanced refactor.

1. Drop unused animState lookup. Was a side-effect-free
   _entitySpawnAdapter.GetState call per per-instance entity, made
   redundant by the Issue #47 fix that trusts MeshRefs.

2. Front-to-back sort opaque groups. Squared distance from camera to
   each group's first-instance translation; ascending sort. Lets the
   GPU's depth test reject fragments behind closer geometry — real
   win on dense scenes (Holtburg courtyard, Foundry interior).

3. Per-entity AABB frustum cull. 5m-radius AABB check per entity
   before walking parts. Skips work for distant entities even when
   their landblock is partially visible. Animated entities (other
   characters, NPCs, monsters) bypass — they always need per-frame
   work for animation regardless. Conservative radius covers typical
   entity bounds; large outliers stay landblock-culled.

4. Memoize palette hash per entity. TextureCache.HashPaletteOverride
   is now internal; new GetOrUploadWithPaletteOverride overload takes
   a precomputed hash. The dispatcher computes it ONCE per entity and
   reuses across every (part, batch) lookup, avoiding the per-batch
   FNV-1a fold over SubPalettes. Trees / scenery without palette
   overrides skip entirely (palHash stays 0).

Visual output unchanged; FPS up further, especially in dense scenes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-08 17:51:03 +02:00
Erik
7b41efc281 phase(N.4): WbDrawDispatcher — FirstIndex/BaseVertex + Issue #47 + grouped instanced draws
Three bugs surfaced and resolved during Task 26 visual verification.

1. **No-scenery + exploded characters**: WB's modern rendering path
   (GL 4.3 + bindless) packs every mesh into a single global VAO/VBO/IBO
   (GlobalMeshBuffer). Each batch references its slice via FirstIndex
   (offset into IBO) + BaseVertex (offset into VBO). The dispatcher's
   DrawElementsInstanced(indices=0) read offset 0 of the global IBO
   for every entity — drawing the same first triangle from every
   entity position. Switched to glDrawElementsInstancedBaseVertex(
   BaseInstance) with the batch's offsets. Scenery + connected
   characters now render correctly.

2. **Issue #47 character regression**: Adjustment 6 stored
   AnimPartChanges on WorldEntity.PartOverrides using the raw
   server-sent NewModelId (no degrade resolver applied). The
   dispatcher's animState.ResolvePartGfxObj override path then
   clobbered MeshRefs (which GameWindow's spawn code correctly
   resolves to close-detail meshes via GfxObjDegradeResolver).
   Result: humanoids drew low-detail (~14 verts/17 polys) base
   meshes instead of close-detail (~32 verts/60 polys), losing
   bicep / shoulder / back geometry. Fix: trust MeshRefs as the
   source of truth and don't re-apply animState overrides at draw
   time. AnimatedEntityState's overrides only matter for hot-swap
   appearance updates (0xF625) which today rebuild MeshRefs anyway.

3. **Performance — sub-100 FPS on Holtburg**: per-entity
   single-instance draws meant ~16K glDraw calls/frame plus a
   64-byte glBufferSubData per call. Refactored to grouped
   instanced rendering: bucket all (entity, batch) pairs by
   GroupKey(Ibo, FirstIndex, BaseVertex, IndexCount, TextureHandle,
   Translucency); upload all matrices in ONE BufferData call;
   one glDrawElementsInstancedBaseVertexBaseInstance per group
   with BaseInstance pointing at the group's slice in the shared
   instance VBO. Down from ~16K to a few hundred draws/frame
   (~30× fewer). Bind VAO once per frame (modern WB shares one
   global VAO). Removed redundant per-draw VertexAttribPointer
   (VAO captures that state).

Result: Holtburg renders correctly with characters showing full
detail; FPS climbed substantially. Two more bugs (mesh loading
+ batch.Key.SurfaceId) were fixed in the prior commit (943652d).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-08 17:39:02 +02:00
Erik
943652dc97 phase(N.4) Tasks 22+23 fixup: trigger WB mesh loads + correct SurfaceId source
Task 26 visual verification surfaced three bugs in the dispatcher.
Two are fixed here; the third is documented as a remaining issue.

1. WB's IncrementRefCount only bumps a usage counter — it does NOT
   trigger mesh loading. Fixed in WbMeshAdapter.IncrementRefCount:
   call PrepareMeshDataAsync(id, isSetup: false) on first registration.
   Result auto-enqueues to _stagedMeshData (line 510 of WB's
   ObjectMeshManager) which Tick() drains onto the GPU.

2. EntitySpawnAdapter never registered per-instance entity meshes
   with WB. LandblockSpawnAdapter only registers atlas-tier
   (ServerGuid == 0); per-instance entities fell through. Fixed by
   adding optional IWbMeshAdapter constructor param + tracking unique
   GfxObj ids per server-guid for IncrementRefCount on OnCreate /
   DecrementRefCount on OnRemove.

3. WbDrawDispatcher.ResolveTexture used batch.SurfaceId which WB
   never populates (line 1746 of ObjectMeshManager only sets
   batch.Key — the TextureKey struct that has SurfaceId). Switched
   to batch.Key.SurfaceId.

Plus diagnostic counters (ACDREAM_WB_DIAG=1) for entity-seen / drawn
/ mesh-missing / draws-issued counts.

Status: with these fixes the dispatcher now issues real draw calls
(~16K/frame, validated via diagnostic). However visual verification
shows characters appear "exploded" (parts spaced too far apart) and
scenery (trees/rocks/fences/buildings) does not appear. Root cause
analysis pending — Adjustment 7 in the plan documents the deferred
work. Flag stays default-off; legacy renderer remains the
production path.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-08 15:50:21 +02:00
Erik
fc80c252d6 docs(N.4): mark Tasks 22-25 complete in progress table
Task 22+23: WbDrawDispatcher + surface metadata side-table (01cff41)
Task 24: sky pass structurally independent (5df9135)
Task 25: all spec-required micro-tests covered (940/948 pass, 8 pre-existing)

Remaining: Task 26 (visual verification, human-in-the-loop),
Task 27 (legacy deletion), Task 28 (finalize).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-08 15:32:10 +02:00
Erik
5df9135e0e verify(N.4) Task 24: sky pass is structurally independent of WB foundation
SkyRenderer builds its own SkySubMesh structs from GfxObjMesh.Build
at initialization time, with its own VAO/VBO/IBO resources. It reads
Luminosity, Diffuse, NeedsUvRepeat, SurfOpacity, DisableFog from
GfxObjSubMesh directly — not from AcSurfaceMetadataTable. The sky
draw path never touches InstancedMeshRenderer or WbDrawDispatcher.

No code changes needed: the flag has zero effect on sky rendering.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-08 15:31:20 +02:00
Erik
01cff4144f phase(N.4) Tasks 22+23: WbDrawDispatcher + surface metadata side-table
WbDrawDispatcher draws all entities through WB's ObjectRenderData
(VAO/VBO per GfxObj, per-batch IBO) using acdream's TextureCache for
texture resolution. Two-pass rendering (opaque+ClipMap, then
translucent) matching the existing InstancedMeshRenderer pattern.
Per-entity single-instance drawing for N.4 simplicity — true
instancing grouping deferred to N.6.

Atlas-tier entities: mesh from WB, texture from TextureCache via
batch SurfaceId. Per-instance-tier entities: AnimatedEntityState
drives part overrides + hidden-parts, palette/surface overrides
resolve through TextureCache's composite-key caches.

Side-table population (Task 23 folded in): WbMeshAdapter now takes
DatCollection and populates AcSurfaceMetadataTable on first
IncrementRefCount per GfxObj. The side-table provides TranslucencyKind
(critical for ClipMap alpha-test on vegetation) plus Luminosity,
Diffuse, SurfOpacity, NeedsUvRepeat, DisableFog for sky-pass and
lighting.

GameWindow wiring: when WbFoundationFlag is enabled, WbDrawDispatcher
draws everything and InstancedMeshRenderer is skipped. Flag-off path
is unchanged.

Matrix composition: restPose * animOverride * entityWorld, matching
the spec. Three MatrixCompositionTests verify the contract.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-08 15:30:33 +02:00
Erik
5b4fd4b61d phase(N.4) Adjustment 6: add PartOverrides + HiddenPartsMask to WorldEntity
Resolves Adjustment 4 (Option A): WorldEntity now carries the server-
sent AnimPartChange data as PartOverrides and a HiddenPartsMask bitmask.
EntitySpawnAdapter.OnCreate populates AnimatedEntityState from these
fields at spawn time. GameWindow's CreateObject handler converts the
network-layer AnimPartChange records into lightweight PartOverride
structs.

This unblocks Task 22: the WbDrawDispatcher can now resolve per-part
GfxObj overrides and hidden-part suppression from entity state.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-08 15:10:22 +02:00
Erik
16a36dda8f Merge branch 'claude/quirky-jepsen-fd60f1' — N.4 Week 4 handoff doc 2026-05-08 14:54:13 +02:00
Erik
831f7d416b docs(N.4): Week 4 handoff for the next agent
Self-contained briefing for whoever picks up Week 4 (Tasks 22-28):
the WbDrawDispatcher full draw loop + sky-pass preservation +
visual verification + flag default-on + legacy-code deletion +
plan finalization.

Highlights two unresolved decisions that need a brainstorm checkpoint
at the start of Week 4 (NOT 'just dispatch'):
- Adjustment 4 plumbing: WorldEntity needs HiddenPartsMask +
  AnimPartChanges fields, OR EntitySpawnAdapter.OnCreate takes them
  as separate parameters. Decision before Task 22 writes code.
- Surface-metadata side-table population strategy for Task 23.

References the living-document plan + spec + 5 prior adjustments
so a fresh agent has full context cold.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-08 14:54:12 +02:00
Erik
d30fcb2eb0 Merge branch 'claude/quirky-jepsen-fd60f1' — N.4 Week 3 complete 2026-05-08 14:48:21 +02:00
Erik
312d3b3ee0 docs(N.4) Task 21: mark Week 3 complete + Adjustments 4-5
Week 3 ships: AnimatedEntityState (Tasks 16+18+19, commit ce72c57),
EntitySpawnAdapter routing server-spawned content through the existing
TextureCache.GetOrUploadWithPaletteOverride path (Task 17, commit
c02c307). 947 tests pass.

Adjustment 4: WorldEntity lacks HiddenPartsMask + AnimPartChanges
fields. Adapter scaffolding ships; AnimatedEntityState gets default
values (empty mask + empty override map). Plumbing deferred to Task 22
brainstorm — either add fields to WorldEntity or thread through a
separate parameter to EntitySpawnAdapter.OnCreate.

Adjustment 5: Task 20 (per-instance decode conformance) is structural.
Both old and new paths call the same TextureCache function — bytes
identical by construction. EntitySpawnAdapterTests already cover the
routing. No separate conformance test file needed.

Next: Task 22 (Week 4) — WbDrawDispatcher full draw loop. First task
that actually draws through WB and unlocks Adjustment 3's mitigation
(dual-pipeline cost resolves when legacy renderer can short-circuit
its upload for atlas-tier content).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-08 14:48:20 +02:00
Erik
c02c307bee phase(N.4) Task 17: EntitySpawnAdapter for server-spawned per-instance content
Routes server-spawned (CreateObject) entities through the per-instance
rendering path. Filter: ServerGuid != 0. Atlas-tier entities (procedural,
ServerGuid == 0) flow through LandblockSpawnAdapter (Task 11) instead.

For entities with PaletteOverride set, walks each MeshRef.SurfaceOverrides
map and calls TextureCache.GetOrUploadWithPaletteOverride to pre-warm the
palette-composed GL texture before the first draw. Surfaces not in the
SurfaceOverrides map (i.e. whose ids are only known after opening the GfxObj
dat) are decoded lazily by the draw dispatcher on first use, consistent with
StaticMeshRenderer.

Builds AnimatedEntityState per server-guid via injected sequencer factory
(Func<WorldEntity, AnimationSequencer>). The factory decouples the adapter
from DatCollection so tests pass a stub lambda without a GL context.

OnRemove releases per-entity state. Unknown guids no-op.

Introduces ITextureCachePerInstance: thin seam interface over the palette
decode path so EntitySpawnAdapter tests can use a CapturingTextureCache
mock without constructing a GL context. TextureCache implements it.

Adjustment 4 documented in source comments: WorldEntity does not currently
expose HiddenPartsMask or AnimPartChanges (they are consumed upstream in the
network layer before the WorldEntity is built). HideParts / SetPartOverride
calls are placeholder TODO'd for when those fields are promoted.

Wired into GpuWorldState.AppendLiveEntity (OnCreate) and
RemoveEntityByServerGuid (OnRemove). Constructed in GameWindow under the
ACDREAM_USE_WB_FOUNDATION flag alongside LandblockSpawnAdapter. Sequencer
factory captures _dats + _animLoader at construction time; falls back to an
empty Setup + MotionTable via NullAnimLoader when dats are unavailable.

10 new tests: server-spawn routing, atlas-tier skip, palette decode pre-warm
(with and without surface overrides), OnRemove lifecycle, unknown-guid noop,
multi-entity isolation. All pass; 8 pre-existing failures unchanged.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-08 14:46:34 +02:00
Erik
ce72c574e9 phase(N.4) Tasks 16+18+19: AnimatedEntityState + AnimPartChange + HiddenParts
Per-entity render state for the per-instance rendering tier
(server-spawned characters / creatures / equipped items). Holds:
- partGfxObjOverrides: Dictionary<int, ulong> — AnimPartChange swaps
  (e.g. wielding a weapon replaces a hand-part's GfxObj).
- hiddenMask: ulong — HiddenParts bitmask. Bit i set hides part i.
- AnimationSequencer reference — N.4 doesn't touch the sequencer;
  this just exposes it for the draw dispatcher.

Public API: HideParts / IsPartHidden / SetPartOverride /
TryGetPartOverride / ResolvePartGfxObj. Bounds-checked
(partIdx < 0 or >= 64 → IsPartHidden returns false).

Twelve tests covering the type, the AnimPartChange resolution helper,
and the HiddenParts bitmask edge cases (theories for 0b0/0b1/MSB/all-ones,
plus negative-index + out-of-range guards).

Consumed by Task 17's EntitySpawnAdapter (creates one per CreateObject)
and Task 22's WbDrawDispatcher (reads via per-part draw loop).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-08 14:37:09 +02:00
Erik
9e1992e8a3 Merge branch 'claude/quirky-jepsen-fd60f1' — N.4 Week 2 complete 2026-05-08 14:33:19 +02:00
Erik
36f7a601c4 docs(N.4) Task 15: mark Week 2 complete + Adjustment 3 (FPS regression cause)
Week 2 ships: LandblockSpawnAdapter routes atlas-tier GfxObjs to WB
ref counts (Task 11/12), pending-spawn list integration verified
(Task 14), WbMeshAdapter.Tick drains the pipeline queues per frame
(added per Adjustment 3, fixes a real memory leak).

Task 13 (memory budget verification) is deferred: stress-test
revealed the FPS drop with flag-on isn't the queue leak we thought
— it's the dual-pipeline cost (background workers + duplicate GL
upload + duplicate I/O + legacy renderer still doing the same atlas
work). The savings only materialize in Task 22 when the dispatcher
short-circuits the legacy upload for atlas-tier content. Plan
Adjustment 3 documents this; no fix needed before Week 4 since
default-off is byte-identical to pre-N.4.

Next: Task 16 (Week 3) — AnimatedEntityState + per-instance path.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-08 14:33:19 +02:00
Erik
bf53cb4fce phase(N.4): WbMeshAdapter.Tick — drain WB pipeline queues per frame
Without this, ObjectMeshManager.StagedMeshData and
OpenGLGraphicsDevice._glThreadQueue grow unbounded as background
workers prep mesh data + queue GL actions. Visual stress test of
flag-on at radius 7 showed real FPS drop and rising frame latency
from this leak.

Tick() drains both queues:
1. _graphicsDevice.ProcessGLQueue() applies pending GL state.
2. Loop _meshManager.StagedMeshData.TryDequeue -> UploadMeshData
   to materialize VAO/VBO/IBO for each prepared mesh.

Wired into GameWindow's render loop before draw work begins.
No-op when adapter is uninitialized or disposed.

Pattern matches WB's reference ObjectRenderManagerBase.ProcessUploads
without the prioritization heuristics (we're not yet drawing the
results — Task 22's WbDrawDispatcher will add prioritization when
visual budget matters).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-08 14:24:32 +02:00
Erik
f4f0101d2c phase(N.4) Task 14: pending-spawn list integration test
Verifies Task 12's GpuWorldState wiring preserves the pending-spawn
list mechanism:

1. Live entity parked before its landblock loads — pending count = 1,
   adapter not called yet.
2. Landblock arrives with its own atlas-tier entity AND drains the
   pending live entity. Adapter sees ONLY the atlas-tier GfxObj
   (server-spawned drained entity is filtered by ServerGuid != 0).
3. Live entity arriving AFTER landblock load goes straight to flat
   view; adapter is not re-invoked.
4. Landblock unload decrements match load increments.

Three integration tests confirm the existing pending-spawn drain
semantics work correctly with the new adapter, and per-instance-tier
entities (server-spawned) never leak into WB's atlas pipeline.

To exercise the adapter code path (which GpuWorldState gates on
WbFoundationFlag.IsEnabled) without requiring the env var set before
process startup, WbFoundationFlag gains an internal
ForTestsOnly_ForceEnable() method and AcDream.App exposes internals
to AcDream.Core.Tests via InternalsVisibleTo.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-08 14:02:30 +02:00
Erik
931a690c4c phase(N.4) Task 12: wire LandblockSpawnAdapter into GpuWorldState
GpuWorldState's constructor accepts an optional LandblockSpawnAdapter.
AddLandblock calls OnLandblockLoaded with the post-merge loaded record;
RemoveLandblock calls OnLandblockUnloaded with the landblock id at the
top of the method (before state mutation).

Both calls are gated behind WbFoundationFlag.IsEnabled — no behavioral
change with flag off (existing tests pass without modification).

GameWindow constructs the adapter under the flag and threads it into
GpuWorldState. With flag on, atlas-tier scenery now drives WB ref
counts; per-instance entities (ServerGuid != 0) are filtered out by
the adapter and don't reach WB.

Foundation for Task 13 (memory budget verification under stress).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-08 13:56:40 +02:00
Erik
669768d9da phase(N.4) Task 11: LandblockSpawnAdapter (atlas-tier ref-count bridge)
Bridges LoadedLandblock load/unload events to IWbMeshAdapter ref counts.
Tier-aware by design: walks WorldEntity collection filtered by
ServerGuid == 0 (procedural / atlas-tier only). Server-spawned entities
are skipped — those will go through EntitySpawnAdapter (Task 17).

Per-landblock id-set snapshot ensures unload pairs 1:1 with load even
when underlying data is released. Duplicate-load idempotency for
defensive resilience to streaming-controller bugs.

Six tests: registers per unique id; dedups across entities; skips
server-spawned; unload matches load; unknown landblock no-ops;
duplicate load no-ops.

Wiring into GpuWorldState lands in Task 12.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-08 13:53:38 +02:00
Erik
dc6410b56f Merge branch 'claude/quirky-jepsen-fd60f1' — N.4 Adjustment 2 (Task 9 routing revert) 2026-05-08 13:48:30 +02:00
Erik
4f318bcbba fix(N.4) Adjustment 2: revert Task 9 renderer-level routing
Smoke test flag-on showed characters/NPCs disappearing along with
static scenery. Root cause: Task 9 routed all
InstancedMeshRenderer.EnsureUploaded calls through WB. But that
renderer is used for BOTH tiers in production — character per-part
spawn (line 2302, per-instance) AND streaming-loader spawns (lines
5137 + 5155, atlas).

The renderer is tier-blind by design. Tier-routing belongs at the
spawn-callback layer per the spec's data-flow section:

- LandblockSpawnAdapter (Task 11) calls IncrementRefCount per
  unique GfxObj — atlas-tier only.
- EntitySpawnAdapter (Task 17) routes through per-instance path
  via TextureCache.GetOrUploadWithPaletteOverride.

This commit removes the sentinel pattern + 4 sentinel-skip checks
from InstancedMeshRenderer. Kept the _wbMeshAdapter constructor
parameter (unused for now) so GameWindow's wire-up doesn't shift.
Kept all the real WB pipeline construction in WbMeshAdapter
(it's the substrate routing will use in Week 2).

Verified flag-on === flag-off post-revert.

Plan updated with Adjustment 2 explaining the discovery + correct
architectural placement for routing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-08 13:48:30 +02:00
Erik
05a458254a Merge branch 'claude/quirky-jepsen-fd60f1' — N.4 Week 1 complete 2026-05-08 13:32:43 +02:00
Erik
c49c6edde5 docs(N.4): mark Week 1 complete — Tasks 1-10
Foundation types + WB pipeline brought up + InstancedMeshRenderer
routes through the adapter behind ACDREAM_USE_WB_FOUNDATION=1.
Conformance tests pin GfxObjMesh.Build + SetupMesh.Flatten behavior.
Flag-off render path byte-identical to before.

Build green, 901 tests pass, 8 pre-existing failures only.

Next: Task 11 (LandblockSpawnAdapter — streaming-loader hook).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-08 13:32:43 +02:00
Erik
4ad7a985cf phase(N.4) Task 9: real WB pipeline bring-up + InstancedMeshRenderer routing
WbMeshAdapter now actually constructs the WB pipeline:
- OpenGLGraphicsDevice(gl, logger, DebugRenderSettings)
- DefaultDatReaderWriter(datDir) — opens its own file handles for now
  (memory cost ~50-100MB of duplicate index caches, acceptable for
  foundation work per plan Adjustment 1)
- ObjectMeshManager(graphicsDevice, dats, NullLogger)

InstancedMeshRenderer.EnsureUploaded routes through the adapter when
ACDREAM_USE_WB_FOUNDATION=1 is set; uses a WbManagedSentinel entry
in the local cache to mark "this GfxObj lives in WB now". CollectGroups
skips sentinel entries; both Draw passes skip them; Dispose skips them
(no GL resources to free — ObjectMeshManager owns those). Task 22's
WbDrawDispatcher will eventually draw WB-managed objects. With flag
off, behavior is byte-identical to before.

WbMeshAdapter constructor signature changed from (GL, DatCollection,
Logger) to (GL, string datDir, Logger). Updated tests to use
CreateUninitialized() for behavior tests and single null-GL guard test
for constructor validation. GameWindow updated to pass _datDir and to
wire _wbMeshAdapter into InstancedMeshRenderer.

AcDream.App.csproj gets direct ProjectReferences to WorldBuilder.Shared
and Chorizite.OpenGLSDLBackend — project refs are not transitive in
.NET, so AcDream.App must list them explicitly even though AcDream.Core
already references them.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-08 13:31:30 +02:00