acdream/docs/superpowers/plans/2026-05-21-phase-o-dat-path-unification.md
Erik ff4164247a plan(O): Phase O implementation plan + spec layer-placement fix
Plan: 7 tasks decomposing spec T2..T9 with bite-sized TDD-style steps,
exact file paths, commit-message templates, and a T4 safety-check
branch (refactor in place if ObjectMeshManager._dats call sites <=20;
fall back to thin adapter otherwise).

Spec fix: §4.1 mesh-pipeline files now correctly placed under
src/AcDream.App/Rendering/Wb/ instead of Core (ObjectMeshManager uses
Silk.NET.OpenGL types from Managed* wrappers, and CLAUDE.md forbids
Core depending on GL). §4.2's layer split (TextureHelpers in Core,
rest in App) was already correct.

Plan task order: T2 (setup) -> T5 (Core helpers, lowest risk) ->
T3 (App GL infra) -> T4 (App mesh pipeline + dat-shim) -> T7 (drop
refs + cleanup) -> T8 (visual verification) -> T9 (ship). T5 moved
earlier than spec order to validate the namespace migration flow on
small-blast-radius files before the load-bearing T4.

Self-review: all 12 spec decisions (O-D1..O-D12) mapped to plan tasks;
placeholders intentional + explained (MIT license body fetched at T2
step 4; commit-message parameters filled at task close).

Spec: docs/superpowers/specs/2026-05-21-phase-o-dat-path-unification-design.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 14:49:19 +02:00

45 KiB

Phase O — DatPath Unification Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Extract the load-bearing WorldBuilder code we use into acdream's own source tree, route all dat reads through our DatCollection, and drop the two <ProjectReference> entries that pull in WB. End state: ONE dat reader in the process, ZERO references to WorldBuilder.* / Chorizite.OpenGLSDLBackend.* namespaces in our source.

Architecture: Verbatim copy of ~33 WB files (~7.7K LOC) split across two layers — src/AcDream.Core/Rendering/Wb/ for pure-CPU helpers (TextureHelpers, SceneryHelpers, TerrainUtils, TerrainEntry, CellSplitDirection); src/AcDream.App/Rendering/Wb/ for everything that touches GL (OpenGLGraphicsDevice, ManagedGL*, GLSLShader, GLHelpers, GLStateScope, ObjectMeshManager, particle pipeline, render data structs). ObjectMeshManager refactored to take our DatCollection directly (with fallback to a thin adapter if the call-site count exceeds 20). Three internal types in Chorizite promoted to public.

Tech Stack: C# .NET 10, Silk.NET.OpenGL, BCnEncoder.Net, Chorizite.DatReaderWriter (NuGet — stays). MIT attribution required for the extracted code per WB's license.

Spec: docs/superpowers/specs/2026-05-21-phase-o-dat-path-unification-design.md Audit: docs/research/2026-05-21-phase-o-t1-wb-audit.md


File Structure

Created during this phase

src/AcDream.Core/Rendering/Wb/ (new directory):

  • TextureHelpers.cs — pure pixel-format decoders (Fill* helpers for INDEX16/P8/A8/A8R8G8B8/R8G8B8/R5G6B5/A4R4G4B4/etc.).
  • SceneryHelpers.cs — stateless scenery transformations (Displace, RotateObj, ScaleObj, ObjAlign, CheckSlope).
  • TerrainUtils.cs — terrain math (GetHeight, GetNormal, OnRoad, CalculateSplitDirection).
  • TerrainEntry.cs — packed per-vertex terrain struct ([MemoryPackable] attribute STRIPPED).
  • CellSplitDirection.csSWtoNE / SEtoNW enum.

src/AcDream.App/Rendering/Wb/ (existing directory, ~25 files added):

  • OpenGLGraphicsDevice.cs — Silk.NET wrapper, GL context, ProcessGLQueue.
  • GLSLShader.cs, GLHelpers.cs, GLStateScope.cs — shader + state wrappers.
  • ManagedGLTexture.cs, ManagedGLTextureArray.cs, ManagedGLVertexBuffer.cs, ManagedGLIndexBuffer.cs, ManagedGLVertexArray.cs, ManagedGLFrameBuffer.cs, ManagedGLUniformBuffer.cs — GL resource wrappers (7 files).
  • ObjectMeshManager.cs — mesh extraction + GPU upload (REFACTORED to take DatCollection).
  • ObjectRenderBatch.cs, ObjectRenderData.cs — render-data structs.
  • DebugRenderSettings.cs — settings struct for OpenGLGraphicsDevice ctor.
  • Particle batcher + emitter files (count + names confirmed during T4 closure walk).
  • GlobalMeshBuffer.cs, modern render data structs (count + names confirmed during T4 closure walk).
  • EmbeddedResourceReader.cs, TextureFormatExtensions.cs, BufferUsageExtensions.cs — three internal types promoted to public.

Repo root: NOTICE.md updated (or created) with WB MIT attribution.

Modified during this phase

  • src/AcDream.Core/AcDream.Core.csproj — drop 2 <ProjectReference> lines.
  • src/AcDream.App/AcDream.App.csproj — drop 2 <ProjectReference> lines.
  • src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs — drop _wbDats field/ctor/dispose; drop [indoor-upload] NULL_RESULT block (lines 192-263 currently); simplify ctor to pass DatCollection directly to ObjectMeshManager.
  • src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs — update using Chorizite.OpenGLSDLBackend.Lib;using AcDream.App.Rendering.Wb;.
  • src/AcDream.Core/World/WbSceneryAdapter.cs — update using WorldBuilder.Shared.Models;using AcDream.Core.Rendering.Wb;.
  • src/AcDream.Core/World/SceneryGenerator.cs — update both using lines to new namespaces.
  • src/AcDream.Core/Textures/SurfaceDecoder.cs — update using Chorizite.OpenGLSDLBackend.Lib;using AcDream.Core.Rendering.Wb;.
  • tests/AcDream.Core.Tests/Textures/TextureDecodeConformanceTests.cs — update using line.
  • tests/AcDream.Core.Tests/World/WbSceneryAdapterTests.cs — doc-comment fix only.
  • tests/AcDream.Core.Tests/World/SceneryGeneratorTests.cs — doc-comment fix only.
  • tests/AcDream.Core.Tests/Rendering/Wb/MeshExtractionConformanceTests.cs — doc-comment fix only.
  • tests/AcDream.Core.Tests/Terrain/ClientReference.cs, ClientConformanceTests.cs — doc-comment fix only.
  • CLAUDE.md — rewrite the "WB integration cribs" section.
  • docs/architecture/worldbuilder-inventory.md — update ownership notes.

Deleted during this phase

  • tests/AcDream.Core.Tests/Terrain/SplitFormulaDivergenceTest.cs — one-time data-collection test, job done at N.5b.

Task 1: Setup (was O-T2)

Files:

  • Create: src/AcDream.Core/Rendering/Wb/ (directory only — no files yet)
  • Create/Modify: NOTICE.md (repo root)

The src/AcDream.App/Rendering/Wb/ directory already exists.

  • Step 1: Create the Core/Rendering/Wb directory

Run:

New-Item -ItemType Directory -Force -Path "src/AcDream.Core/Rendering/Wb"

Expected: directory created, no error.

  • Step 2: Check whether NOTICE.md exists

Run:

Test-Path "NOTICE.md"

Expected: True or False. If True, read it before editing. If False, the next step creates it.

  • Step 3: Create or append the WorldBuilder attribution section in NOTICE.md

Write to NOTICE.md (append if exists, create otherwise):

## WorldBuilder

Portions of acdream's rendering and dat-handling code are copied from
WorldBuilder (https://github.com/Chorizite/WorldBuilder), MIT-licensed.
The extracted code lives under:

- `src/AcDream.Core/Rendering/Wb/` — pure helpers (texture decode,
  scenery transforms, terrain math).
- `src/AcDream.App/Rendering/Wb/` — GL infrastructure and mesh pipeline.

Original copyright holders: Chorizite contributors (see WorldBuilder's
LICENSE file). Adapted by acdream maintainers to consume our
`DatCollection` directly (replacing WB's `DefaultDatReaderWriter`) and
to remove editor-only code paths.

Original MIT license text:

[Insert full MIT license body here from references/WorldBuilder/LICENSE — verify the file exists at that path and use its exact content]

If the user has a different NOTICE.md style, follow theirs; this is the minimum content.

  • Step 4: Verify the MIT license body

Run:

Test-Path "references/WorldBuilder/LICENSE"
Get-Content "references/WorldBuilder/LICENSE" -TotalCount 5

Expected: file exists; first few lines should be MIT license text. Copy the full LICENSE body into NOTICE.md, replacing the [Insert ...] placeholder.

  • Step 5: Commit
git add NOTICE.md
git add "src/AcDream.Core/Rendering/Wb/.gitkeep"  # if needed to track empty dir
git commit -m @'
chore(O-T2): create Core/Rendering/Wb directory + NOTICE.md attribution

Phase O setup: extracted-WB code home + MIT attribution per O-D5.
Spec: docs/superpowers/specs/2026-05-21-phase-o-dat-path-unification-design.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
'@

Note: New-Item -ItemType Directory doesn't track empty dirs in git. If T1 needs the directory to exist before later commits, write a placeholder file (e.g., .gitkeep or a tiny README.md describing the directory's purpose) and commit it.


Task 2: Extract pure stateless helpers to Core (was O-T5)

Moved earlier in the plan from spec's T5 position because: (a) lowest-risk extraction (pure functions, no GL, no dat behavior change); (b) unblocks the namespace migrations in our own Core files; (c) catches any namespace-update gotchas before the load-bearing T4.

Files:

  • Create: src/AcDream.Core/Rendering/Wb/TextureHelpers.cs

  • Create: src/AcDream.Core/Rendering/Wb/SceneryHelpers.cs

  • Create: src/AcDream.Core/Rendering/Wb/TerrainUtils.cs

  • Create: src/AcDream.Core/Rendering/Wb/TerrainEntry.cs

  • Create: src/AcDream.Core/Rendering/Wb/CellSplitDirection.cs

  • Modify: src/AcDream.Core/World/SceneryGenerator.cs (2 using lines)

  • Modify: src/AcDream.Core/World/WbSceneryAdapter.cs (1 using line)

  • Modify: src/AcDream.Core/Textures/SurfaceDecoder.cs (1 using line)

  • Modify: tests/AcDream.Core.Tests/Textures/TextureDecodeConformanceTests.cs (1 using line)

  • Modify: tests/AcDream.Core.Tests/Terrain/SplitFormulaDivergenceTest.cs — actually this gets DELETED at T7, but its imports still need to work between now and then OR the test is moved to deleted-list earlier. Decision: leave it broken-but-doesn't-affect-other-tests for now, delete cleanly at T7.

  • Step 1: Read each of the 5 WB source files

Read:

  • references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/TextureHelpers.cs
  • references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/SceneryHelpers.cs
  • references/WorldBuilder/WorldBuilder.Shared/Modules/Landscape/Lib/TerrainUtils.cs
  • references/WorldBuilder/WorldBuilder.Shared/Modules/Landscape/Models/TerrainEntry.cs
  • references/WorldBuilder/WorldBuilder.Shared/Modules/Landscape/Models/CellSplitDirection.cs

(Note: the worktree's references/WorldBuilder submodule is empty. Use the main checkout at /c/Users/erikn/source/repos/acdream/references/WorldBuilder/ if needed.)

Confirm each file is what the audit expected. Note any unexpected dependencies (other WB types referenced beyond NuGet packages like Silk.NET, BCnEncoder.Net, MemoryPack, etc.).

  • Step 2: Copy TextureHelpers.cs to Core

Copy references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/TextureHelpers.cs to src/AcDream.Core/Rendering/Wb/TextureHelpers.cs. Then update the file:

Change the namespace declaration line from:

namespace Chorizite.OpenGLSDLBackend.Lib;

to:

namespace AcDream.Core.Rendering.Wb;

If TextureHelpers has any using statements pointing to other WB types, note them but DO NOT change yet — copy is verbatim except for the namespace.

  • Step 3: Copy SceneryHelpers.cs to Core (same process)

Same as Step 2 for SceneryHelpers.cs. Update its namespace to AcDream.Core.Rendering.Wb.

If SceneryHelpers.cs has a using WorldBuilder.Shared.Modules.Landscape.Lib; or similar for TerrainUtils, leave it — we'll update it at Step 6.

  • Step 4: Copy TerrainUtils.cs to Core

Same. Update namespace from WorldBuilder.Shared.Modules.Landscape.LibAcDream.Core.Rendering.Wb.

  • Step 5: Copy TerrainEntry.cs to Core + strip [MemoryPackable]

Copy. Update namespace. Then strip the [MemoryPackable] attribute + any [MemoryPack...] attributes on fields:

// BEFORE
[MemoryPackable]
public partial struct TerrainEntry { ... }

// AFTER
public struct TerrainEntry { ... }

Drop the partial modifier as well if the only reason it's partial is the MemoryPack source generator (verify by checking whether any other file extends the type via a separate partial).

Drop any using MemoryPack; line.

  • Step 6: Copy CellSplitDirection.cs to Core

Enum. Update namespace. Nothing else.

  • Step 7: Update internal cross-references in the new files

After all 5 files exist in src/AcDream.Core/Rendering/Wb/:

If SceneryHelpers.cs had using WorldBuilder.Shared.Modules.Landscape.Lib; (for TerrainUtils), change to nothing — they're in the same namespace now.

If TerrainUtils.cs had using WorldBuilder.Shared.Modules.Landscape.Models; (for TerrainEntry / CellSplitDirection), change to nothing — same namespace.

Run:

Grep pattern="using (WorldBuilder|Chorizite\.OpenGLSDLBackend)" path="src/AcDream.Core/Rendering/Wb"

Expected: no matches. If any, that file's still pointing at the old namespace; fix.

  • Step 8: Update our own Core source files' using lines

Three Core files import WB types directly:

In src/AcDream.Core/World/WbSceneryAdapter.cs:2:

// BEFORE
using WorldBuilder.Shared.Models;
// AFTER
using AcDream.Core.Rendering.Wb;

Wait — WbSceneryAdapter.cs:2 actually says using WorldBuilder.Shared.Models; not ...Modules.Landscape.Models;. Verify the actual using and update accordingly. The import is for TerrainEntry. After T2 step 6, TerrainEntry is in AcDream.Core.Rendering.Wb.

In src/AcDream.Core/World/SceneryGenerator.cs:2 and :6:

// BEFORE
using Chorizite.OpenGLSDLBackend.Lib;
using WorldBuilder.Shared.Modules.Landscape.Lib;
// AFTER
using AcDream.Core.Rendering.Wb;
// (single using replaces both, since both target types now live in the new namespace)

In src/AcDream.Core/Textures/SurfaceDecoder.cs:3:

// BEFORE
using Chorizite.OpenGLSDLBackend.Lib;
// AFTER
using AcDream.Core.Rendering.Wb;
  • Step 9: Update conformance test's using

In tests/AcDream.Core.Tests/Textures/TextureDecodeConformanceTests.cs:1:

// BEFORE
using Chorizite.OpenGLSDLBackend.Lib;
// AFTER
using AcDream.Core.Rendering.Wb;

Leave tests/AcDream.Core.Tests/Terrain/SplitFormulaDivergenceTest.cs alone for now — it's deleted at T7.

  • Step 10: Build solution; expect green (or only the SplitFormulaDivergenceTest broken)

Run:

dotnet build

Expected: green. The SplitFormulaDivergenceTest.cs still has its using WbTerrainUtils = WorldBuilder.Shared.Modules.Landscape.Lib.TerrainUtils; alias — but WorldBuilder.Shared is still project-referenced at this point, so it still compiles. If anything else fails, the namespace migration missed a file. Fix and rerun.

  • Step 11: Run tests; expect green

Run:

dotnet test --no-build

Expected: green. Pay particular attention to TextureDecodeConformanceTests — those should still pass byte-for-byte after the namespace move.

  • Step 12: Commit T5
git add src/AcDream.Core/Rendering/Wb/ src/AcDream.Core/World/WbSceneryAdapter.cs src/AcDream.Core/World/SceneryGenerator.cs src/AcDream.Core/Textures/SurfaceDecoder.cs tests/AcDream.Core.Tests/Textures/TextureDecodeConformanceTests.cs
git commit -m @'
feat(O-T5): extract pure stateless helpers to AcDream.Core.Rendering.Wb

Verbatim copy of 5 WorldBuilder files into src/AcDream.Core/Rendering/Wb/:
- TextureHelpers.cs (pixel-format decoders, Chorizite Lib)
- SceneryHelpers.cs (scenery transforms, Chorizite Lib)
- TerrainUtils.cs, TerrainEntry.cs, CellSplitDirection.cs (WB.Shared Landscape)

Namespace migrated from WorldBuilder.* / Chorizite.OpenGLSDLBackend.Lib
to AcDream.Core.Rendering.Wb per O-D11. [MemoryPackable] stripped from
TerrainEntry per O-D10 (we don't serialize the struct).

Updated 3 source files + 1 test file to import from the new namespace.

Verbatim discipline (O-D1): only namespace + MemoryPack attribute changed.
All algorithm bodies byte-identical to upstream.

Spec: docs/superpowers/specs/2026-05-21-phase-o-dat-path-unification-design.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
'@

Task 3: Extract GL infrastructure to App (was O-T3)

Files:

  • Create: 11+ files under src/AcDream.App/Rendering/Wb/ (exact count after closure walk)

  • Modify: none yet (the existing App Wb/ adapter files don't import GL infra directly — they go through Silk.NET — but WbMeshAdapter.cs constructs OpenGLGraphicsDevice which it currently imports from Chorizite.OpenGLSDLBackend. That single import will be updated at T4 when ObjectMeshManager arrives.)

  • Step 1: Walk the GL-infra closure

Read references/WorldBuilder/Chorizite.OpenGLSDLBackend/OpenGLGraphicsDevice.cs first. List every WB type it references (look at field types, method parameters, constructor calls, using statements).

Then read each referenced WB type. Continue until closure is closed.

Expected closure (per audit):

  • OpenGLGraphicsDevice.cs
  • GLSLShader.cs, GLHelpers.cs, GLStateScope.cs (likely in Lib/)
  • ManagedGLTexture.cs, ManagedGLTextureArray.cs, ManagedGLVertexBuffer.cs, ManagedGLIndexBuffer.cs, ManagedGLVertexArray.cs, ManagedGLFrameBuffer.cs, ManagedGLUniformBuffer.cs (root or Lib/)
  • EmbeddedResourceReader.cs, TextureFormatExtensions.cs, BufferUsageExtensions.cs (3 internals to promote)
  • DebugRenderSettings.cs (sometimes needed by OpenGLGraphicsDevice ctor)

Write down the actual file list with paths.

  • Step 2: Check for SixLabors.ImageSharp reachability

Run:

Grep pattern="SixLabors\.ImageSharp" path="references/WorldBuilder/Chorizite.OpenGLSDLBackend"

List every file in the closure that imports ImageSharp.

If empty: document "ImageSharp not reachable" in the T3 commit message. Skip step 3.

If non-empty: proceed to step 3.

  • Step 3: Strip ImageSharp usage from the closure files (only if needed)

For each closure file that imports ImageSharp:

  • Identify the call sites (typically PNG/JPG load helpers).
  • Replace with our existing byte-handling or BCnEncoder calls.
  • Remove the using SixLabors.ImageSharp; line.

If a file's ImageSharp usage cannot be cleanly removed (e.g., it's the file's main purpose), then either: (a) drop the file from the extraction (verify it's not called by our closure), or (b) add SixLabors.ImageSharp as a <PackageReference> to AcDream.App.csproj and keep the file.

Document the decision in the T3 commit.

  • Step 4: Copy each GL-infra file to src/AcDream.App/Rendering/Wb/

For each file in the closure list:

  • Copy verbatim from WB source to src/AcDream.App/Rendering/Wb/<filename>.
  • Update the namespace from Chorizite.OpenGLSDLBackend (or .Lib) to AcDream.App.Rendering.Wb.
  • Update any using Chorizite.OpenGLSDLBackend.Lib; / using Chorizite.OpenGLSDLBackend; lines inside the copied files to point at the new namespace.
  • For the 3 internal types (EmbeddedResourceReader, TextureFormatExtensions, BufferUsageExtensions): change internal class / internal static class / etc. to public class / public static class.

Suggested copy order (each follows the same pattern): managed wrappers first (lowest deps), then helpers, then OpenGLGraphicsDevice last (depends on all the rest).

  • Step 5: Build, iterate until green

Run:

dotnet build

Common failures + fixes:

  • "The type or namespace name 'X' could not be found" → using was missed; add using AcDream.App.Rendering.Wb;. OR a transitive WB type wasn't copied — copy it.
  • "The type 'X' is defined in an assembly that is not referenced" → ImageSharp/MemoryPack/other NuGet — see step 3.
  • "Inconsistent accessibility" → an internal type isn't yet promoted to public — promote it.

Iterate until green.

  • Step 6: Run tests; expect green (no test count change)

Run:

dotnet test --no-build

Tests should pass; this task doesn't change behavior, only file location and namespace.

  • Step 7: Commit T3
git add src/AcDream.App/Rendering/Wb/
git commit -m @'
feat(O-T3): extract GL infrastructure to AcDream.App.Rendering.Wb

Verbatim copy of GL infra (~15 files) from
Chorizite.OpenGLSDLBackend into src/AcDream.App/Rendering/Wb/:
- OpenGLGraphicsDevice.cs, GLSLShader.cs, GLHelpers.cs, GLStateScope.cs
- ManagedGL{Texture,TextureArray,VertexBuffer,IndexBuffer,
  VertexArray,FrameBuffer,UniformBuffer}.cs (7 files)
- DebugRenderSettings.cs

Three internal types promoted to public per O-D9:
- EmbeddedResourceReader, TextureFormatExtensions, BufferUsageExtensions

SixLabors.ImageSharp reachability: <fill in: "not reachable" OR
"stripped from N paths" OR "added as NuGet PackageReference">.

Verbatim discipline (O-D1): namespace + access modifier changes only.
All algorithm bodies byte-identical to upstream.

Build green; tests green (no count change).

Spec: docs/superpowers/specs/2026-05-21-phase-o-dat-path-unification-design.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
'@

Task 4: Extract mesh pipeline + dat refactor (was O-T4)

The load-bearing task. Includes the ObjectMeshManager refactor or fallback to adapter, per O-D7.

Files:

  • Create: src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs

  • Create: src/AcDream.App/Rendering/Wb/ObjectRenderBatch.cs

  • Create: src/AcDream.App/Rendering/Wb/ObjectRenderData.cs

  • Create: ~5+ supporting files (particle batcher / emitter, global mesh buffer, modern render data — count + exact filenames during closure walk)

  • Modify: src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs (constructor + using updates; cleanup at T7)

  • Step 1: Walk the mesh-pipeline closure

Read references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/ObjectMeshManager.cs end-to-end. It's ~2K LOC; use Read with offsets if needed. List every WB type it references that ISN'T already in the GL-infra closure (T3) or stateless-helpers closure (T2).

Expected new types in closure (per audit):

  • ObjectRenderBatch, ObjectRenderData
  • GlobalMeshBuffer (or similar; the single-VBO/IBO holder for modern rendering)
  • ParticleBatcher, ParticleEmitterRenderer (referenced from mesh path)
  • Modern render data structs (likely a couple of small files)

Write the actual file list with paths.

  • Step 2: Count the _dats.X call sites in ObjectMeshManager.cs

Run:

Grep pattern="_dats\." path="references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/ObjectMeshManager.cs"

Count the matches.

Branch on count:

  • ≤ 20 sites: take the refactor path (O-D7). Plan steps 3-7 cover this.
  • > 20 sites: take the adapter fallback. Plan steps 3-7 are skipped; instead, plan steps A-D below (after step 7).

Document the actual count in the T4 commit message regardless.

  • Step 3: (refactor path) Copy mesh-pipeline files to App, namespace-update

Copy each closure file to src/AcDream.App/Rendering/Wb/<filename>. Update each file's namespace from Chorizite.OpenGLSDLBackend.LibAcDream.App.Rendering.Wb. Update any internal using lines.

Do NOT yet modify ObjectMeshManager's dat type — that's the next step. First, the verbatim copy + namespace.

  • Step 4: (refactor path) Refactor ObjectMeshManager dat field type

In the newly-copied src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs:

a) Drop the import:

// BEFORE
using WorldBuilder.Shared.Services;

(plus any related; remove if present.)

b) Add the import:

using AcDream.Core;
// or wherever DatCollection lives — verify with:
//   Grep pattern="^namespace.*DatCollection" path="src/AcDream.Core"
// or:
//   Grep pattern="public.*class DatCollection\b" path="src/AcDream.Core"

c) Change the field declaration from:

private readonly IDatReaderWriter _dats;

to:

private readonly DatCollection _dats;

d) Change the constructor parameter from IDatReaderWriter dats to DatCollection dats.

e) Update each _dats.X call site. The most common shapes:

  • _dats.Portal.TryGet<T>(id, out var x) — verify DatCollection exposes Portal.TryGet<T>. If yes, no change. If no, see step 5.
  • _dats.Cell.TryGet<T>(id, out var x) — verify the same.
  • _dats.HighRes.TryGet<T>(...) — same.
  • _dats.Language.TryGet<T>(...) — same.
  • _dats.ResolveId(id) — drop the call site if it's diagnostic-only (per O-D12). If it's load-bearing, see step 5.

For each call site, either: (a) leave it alone (DatCollection has the equivalent), (b) replace with DatCollection's API shape, or (c) escalate to step 5 (DatCollection needs a new method).

  • Step 5: (refactor path) Add missing methods to DatCollection if needed

If step 4 surfaced a call shape DatCollection doesn't support, add the method to src/AcDream.Core/Dats/DatCollection.cs (or wherever DatCollection lives — find via grep).

DO NOT add methods speculatively — only add what ObjectMeshManager actually calls. Document each added method in the T4 commit.

  • Step 6: (refactor path) Update WbMeshAdapter to construct ObjectMeshManager with DatCollection

In src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs:

Update line 7-15 using lines: drop using Chorizite.OpenGLSDLBackend;, using Chorizite.OpenGLSDLBackend.Lib;, using WorldBuilder.Shared.Models;, using WorldBuilder.Shared.Services;. Add using AcDream.App.Rendering.Wb; (and AcDream.Core.Rendering.Wb; if needed for any of the now-Core types referenced).

Update line 85-88 ObjectMeshManager construction. Today:

_meshManager = new ObjectMeshManager(
    _graphicsDevice,
    _wbDats,
    new ConsoleErrorLogger<ObjectMeshManager>());

Change to:

_meshManager = new ObjectMeshManager(
    _graphicsDevice,
    dats,  // the DatCollection passed to WbMeshAdapter's ctor
    new ConsoleErrorLogger<ObjectMeshManager>());

Leave _wbDats field + its construction at line 79 + its disposal at line 346 in place for now — they're cleaned up at T7.

Do NOT touch the [indoor-upload] NULL_RESULT block (lines 192-263) yet — also cleaned up at T7.

  • Step 7: (refactor path) Build, iterate until green

Run:

dotnet build

Common failures:

  • _dats.X call shape doesn't match DatCollection — fix per step 4/5.
  • Missing using for a moved type — add.

Iterate until green.

  • Step A: (adapter fallback path, only if count > 20)

Create src/AcDream.App/Rendering/Wb/DatCollectionAdapter.cs:

using AcDream.Core;  // or wherever DatCollection lives
using DatReaderWriter;
using DatReaderWriter.DBObjs;
// ... add interfaces / types needed to satisfy IDatReaderWriter

namespace AcDream.App.Rendering.Wb;

/// <summary>
/// Adapts our DatCollection to WB's IDatReaderWriter interface so
/// the extracted ObjectMeshManager can consume it without internal
/// refactoring. O-D7 fallback path — taken because ObjectMeshManager
/// has >20 _dats.X call sites and the refactor was deemed too invasive
/// for one ship-window.
/// </summary>
internal sealed class DatCollectionAdapter : IDatReaderWriter
{
    private readonly DatCollection _dats;

    public DatCollectionAdapter(DatCollection dats)
    {
        ArgumentNullException.ThrowIfNull(dats);
        _dats = dats;
    }

    // Implement each member of IDatReaderWriter by delegating to _dats.
    // Members are determined by reading IDatReaderWriter.cs (which is
    // also copied into this directory at this step, since we no longer
    // have it via the WorldBuilder.Shared project reference).
    // ...
}

You also need to copy IDatReaderWriter.cs from WB.Shared into src/AcDream.App/Rendering/Wb/ (or define a slimmed-down version with only the members ObjectMeshManager uses). Document the choice.

  • Step B: (adapter fallback) Wire the adapter in WbMeshAdapter
// BEFORE (in WbMeshAdapter ctor)
_meshManager = new ObjectMeshManager(_graphicsDevice, _wbDats, ...);

// AFTER
_meshManager = new ObjectMeshManager(
    _graphicsDevice,
    new DatCollectionAdapter(dats),
    ...);
  • Step C: (adapter fallback) Build, iterate until green

Same as step 7 but with the adapter in place.

  • Step D: (adapter fallback) Document the choice

Note in the T4 commit message that the adapter path was taken, with the actual call-site count and the rationale (e.g., "37 call sites; 22 use methods DatCollection doesn't currently expose, escalating to a separate refactor phase").

  • Step 8: (both paths) Run tests; expect green

Run:

dotnet test --no-build

Expected: green. No test count regression.

  • Step 9: (both paths) Commit T4
git add src/AcDream.App/Rendering/Wb/
git add src/AcDream.Core/Dats/  # only if DatCollection grew methods at step 5
git commit -m @'
feat(O-T4): extract mesh pipeline; <refactor OR adapter> dat-shim

Verbatim copy of ObjectMeshManager + supports (~8 files, ~3.3K LOC) from
Chorizite.OpenGLSDLBackend.Lib into src/AcDream.App/Rendering/Wb/.

ObjectMeshManager._dats call-site count: <N>.
Path taken: <refactor in place to DatCollection> OR <thin DatCollectionAdapter>.
Rationale: <one or two sentences>.

DatCollection additions (if any): <list new methods + why>.

WbMeshAdapter updated to construct ObjectMeshManager via the new path.
The _wbDats field + [indoor-upload] NULL_RESULT block stay in place for
now; cleaned up at T7.

SixLabors.ImageSharp: <if any was reachable in this closure, document>.

Verbatim discipline (O-D1): namespace + dat-surface only. Mesh extraction,
texture decode, particle pipeline byte-identical to upstream.

Build green; tests green (no count change).

Spec: docs/superpowers/specs/2026-05-21-phase-o-dat-path-unification-design.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
'@

Task 5: Drop project references + cleanup (was O-T7)

Files:

  • Modify: src/AcDream.App/AcDream.App.csproj

  • Modify: src/AcDream.Core/AcDream.Core.csproj

  • Modify: src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs

  • Modify: src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs (using line)

  • Modify: doc-comment-only files in tests/ (5-6 files, comment edits)

  • Delete: tests/AcDream.Core.Tests/Terrain/SplitFormulaDivergenceTest.cs

  • Step 1: Delete _wbDats field + construction from WbMeshAdapter.cs

Open src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs. Delete:

a) Line 34 (the field declaration):

private readonly DefaultDatReaderWriter? _wbDats;

b) Line 79 (the construction):

_wbDats = new DefaultDatReaderWriter(datDir);

c) Line 346 (the disposal in Dispose()):

_wbDats?.Dispose();

d) The datDir parameter — verify it's no longer used after _wbDats removal. If OpenGLGraphicsDevice ctor doesn't need it, remove datDir from WbMeshAdapter's constructor signature + all call sites. If it's still needed for something else, keep it.

  • Step 2: Delete the [indoor-upload] NULL_RESULT block

In WbMeshAdapter.cs, delete the block from line ~192 to line ~263 (the diagnostic block that uses _wbDats.ResolveId(...)). Also delete the _pendingEnvCellRequests field and its associated logic in Tick() (the isPendingEnvCell block at ~305-321).

Result: WbMeshAdapter.cs shrinks substantially — should be down from 349 to ~250 LOC.

  • Step 3: Update WbMeshAdapter.cs and WbDrawDispatcher.cs using lines

In WbMeshAdapter.cs (lines 7-15):

  • Drop: using Chorizite.OpenGLSDLBackend;, using Chorizite.OpenGLSDLBackend.Lib;, using WorldBuilder.Shared.Models;, using WorldBuilder.Shared.Services;, using DatReaderWriter;, using DatReaderWriter.DBObjs; (verify whether the last two are still needed — they probably are for GfxObj references in PopulateMetadata).
  • Add: using AcDream.App.Rendering.Wb; if not present.

In WbDrawDispatcher.cs (line 9):

  • Drop: using Chorizite.OpenGLSDLBackend.Lib;

  • Add: using AcDream.App.Rendering.Wb;

  • Step 4: Delete the <ProjectReference> lines in both csprojs

In src/AcDream.App/AcDream.App.csproj, delete lines 38-39 (the two <ProjectReference> entries to WorldBuilder.Shared and Chorizite.OpenGLSDLBackend). Also drop the comment immediately above them ("Phase N.4 Task 9: ...") since the reference rationale is now historical.

In src/AcDream.Core/AcDream.Core.csproj, delete lines 27-28 (same two references). Also drop the multi-line comment immediately above ("Phase N: WorldBuilder is acdream's rendering + dat-handling base...") since the references are gone.

  • Step 5: Delete SplitFormulaDivergenceTest.cs
Remove-Item "tests/AcDream.Core.Tests/Terrain/SplitFormulaDivergenceTest.cs"

This was a one-time data-collection test; its job finished at N.5b ship.

  • Step 6: Fix doc-only WB mentions in remaining test files

For each of these files, update comment-only references to WB to point at the extracted location instead. Files (per audit grep):

  • tests/AcDream.Core.Tests/World/WbSceneryAdapterTests.cs:10-17 — mentions WorldBuilder.Shared.Models.TerrainEntry. Update to AcDream.Core.Rendering.Wb.TerrainEntry.
  • tests/AcDream.Core.Tests/World/SceneryGeneratorTests.cs:9 — mentions WB helpers in comment. Update to refer to AcDream.Core.Rendering.Wb.
  • tests/AcDream.Core.Tests/Rendering/Wb/MeshExtractionConformanceTests.cs:12 — mentions WB's ObjectMeshManager. Either update to extracted name OR keep with a clarifying comment "(formerly WB; now ours, see Phase O)".
  • tests/AcDream.Core.Tests/Terrain/ClientReference.cs:11 and ClientConformanceTests.cs:10 — both "Ported from WorldBuilder-ACME-Edition". These are historical attributions; leave them OR update phrasing.

This is comment-edit work; no logic changes.

  • Step 7: Build solution; expect green

Run:

dotnet build

Expected: green. If a using WorldBuilder.* or using Chorizite.OpenGLSDLBackend* was missed, the build will fail with "namespace not found." Find it and update.

Also expect: if any file still references IDatReaderWriter / DefaultDatReaderWriter directly, the build fails. Grep:

Grep pattern="(IDatReaderWriter|DefaultDatReaderWriter)" path="src"

Should return zero matches.

  • Step 8: Run tests; expect green minus deleted test count

Run:

dotnet test --no-build

Expected: green. The total test count drops by however many tests were in the deleted SplitFormulaDivergenceTest.cs (likely a single test or two — record the actual delta).

  • Step 9: Verify the acceptance criteria for reference deletion

Run all three:

Grep pattern="(WorldBuilder|Chorizite\.OpenGLSDLBackend)" path="src/AcDream.App/AcDream.App.csproj"
Grep pattern="(WorldBuilder|Chorizite\.OpenGLSDLBackend)" path="src/AcDream.Core/AcDream.Core.csproj"
Grep pattern="^using (WorldBuilder|Chorizite\.OpenGLSDLBackend)" path="src"

Expected: all three return zero matches.

Grep pattern="DefaultDatReaderWriter" path="src"

Expected: zero matches.

If any return matches: something was missed in this task. Fix before commit.

  • Step 10: Commit T7
git add src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs src/AcDream.App/AcDream.App.csproj src/AcDream.Core/AcDream.Core.csproj tests/
git rm tests/AcDream.Core.Tests/Terrain/SplitFormulaDivergenceTest.cs
git commit -m @'
feat(O-T7): drop WB project references; cleanup _wbDats + diagnostic block

End of Phase O extraction:
- Dropped <ProjectReference> entries to WorldBuilder.Shared and
  Chorizite.OpenGLSDLBackend from AcDream.App.csproj and
  AcDream.Core.csproj.
- Deleted _wbDats field + ctor + dispose from WbMeshAdapter.cs.
- Deleted [indoor-upload] NULL_RESULT diagnostic block (lines 192-263);
  the Phase 2 cell-resolution diagnostic served its purpose, and its
  dependency on _wbDats.ResolveId goes away with this commit (per O-D12).
- Deleted SplitFormulaDivergenceTest.cs (one-time N.5b data-collection
  test; job done).
- Updated comment-only WB mentions in 5 test files to point at the
  extracted namespace.

Verified acceptance criteria:
- Zero refs to WorldBuilder.* / Chorizite.OpenGLSDLBackend.* in any csproj.
- Zero "using WorldBuilder.*" / "using Chorizite.OpenGLSDLBackend*" in src/.
- DefaultDatReaderWriter referenced in zero places.

Build green; tests green (count: -<N> from deleted test, no regressions).

Spec: docs/superpowers/specs/2026-05-21-phase-o-dat-path-unification-design.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
'@

Task 6: Visual verification gate (was O-T8)

Goal: Confirm that the extraction didn't regress rendering by side-by-side with main against retail in three scenes.

Files: none modified; this is a runtime test.

  • Step 1: Capture pre-extraction screenshots (do this BEFORE Task 2 if not already done)

If not already captured: pause this plan, switch back to main (or a commit at HEAD before Task 1), launch acdream at three scenes:

  • Holtburg town (outdoor + scenery)
  • An inn interior (EnvCell)
  • A dungeon (portals)

Save screenshots to docs/research/phase-o-pre/holtburg-town.png, inn-interior.png, dungeon.png (not committed; reference only).

If you forgot to capture pre-screenshots before starting: you can either roll back to a pre-O commit (git checkout <sha> on a scratch branch), or compare visually against the retail client side-by-side instead.

  • Step 2: Launch acdream after Task 5 with the test character

Use the launch script from CLAUDE.md (PowerShell):

$env:ACDREAM_DAT_DIR   = "$env:USERPROFILE\Documents\Asheron's Call"
$env:ACDREAM_LIVE      = "1"
$env:ACDREAM_TEST_HOST = "127.0.0.1"
$env:ACDREAM_TEST_PORT = "9000"
$env:ACDREAM_TEST_USER = "testaccount"
$env:ACDREAM_TEST_PASS = "testpassword"
dotnet build  # must be green first
dotnet run --project src\AcDream.App\AcDream.App.csproj --no-build -c Debug 2>&1 |
    Tee-Object -FilePath "launch-O-verification.log"

Run in the background per CLAUDE.md guidance.

  • Step 3: Visit the three scenes

In the running client:

  1. Holtburg town — walk around, check terrain blending, scenery placement (trees/bushes), building exteriors. Compare with pre-O screenshot.
  2. Inn interior — enter the inn at Holtburg, look at EnvCell geometry (walls, floor, fireplace), check that statics render correctly.
  3. Dungeon — find any portal (Holtburg Town network portal works for spawn-based visuals; for a real dungeon, walk to a dungeon entrance). Check portal visibility, room geometry.

User confirms each scene reads identical to pre-O.

  • Step 4: Measure resident memory at radius=4 + 50 entities visible

In the running client:

  • Set quality preset to Low (radius=4 — see CLAUDE.md "Quality Preset system").
  • Wait for streaming to settle.
  • Open Task Manager (Windows) → Details tab → find AcDream.App.exe → Working Set (Memory).
  • Record the value.

Compare to a pre-O baseline (same scene, same settings, on main HEAD before Phase O). Expected delta: ≥50 MB reduction (validates that the second DefaultDatReaderWriter cache is actually gone).

If <50 MB: investigate why. The dat-reader duplication should be ~50-100 MB per WbMeshAdapter.cs:27 comment.

  • Step 5: Close client gracefully and record findings

Use the graceful close from CLAUDE.md (WM_CLOSE) so ACE's session clears quickly.

Write a brief verification note (in chat or in the T8 commit message):

  • Three scenes: identical / different (describe difference).
  • Memory delta: X MB reduction.
  • User confirmation: yes / no.

Do NOT proceed to Task 7 if any scene regressed or memory delta is <50 MB.

  • Step 6: Tag the verification commit (or just push to next task)

No code change at T8 — but optionally record the verification artifacts:

git add docs/research/  # if you committed any post-O screenshots
git commit --allow-empty -m @'
chore(O-T8): visual verification passed

Three scenes side-by-side against pre-O baseline: identical.
- Holtburg town (outdoor + scenery): OK
- Holtburg inn interior (EnvCell): OK
- <Dungeon name> (portals): OK

Memory measurement at radius=4 + 50 entities visible:
- Pre-O: <X> MB working set
- Post-O: <Y> MB working set
- Delta: <Z> MB reduction (>= 50 MB target)

User confirmed identical rendering.

Spec: docs/superpowers/specs/2026-05-21-phase-o-dat-path-unification-design.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
'@

Task 7: Ship (was O-T9)

Files:

  • Modify: CLAUDE.md — rewrite "WB integration cribs" section.

  • Modify: docs/architecture/worldbuilder-inventory.md — update ownership notes.

  • Modify: docs/plans/2026-04-11-roadmap.md — move Phase O from "in flight" to "shipped".

  • Modify: docs/plans/2026-05-12-milestones.md — note Phase O ship if appropriate.

  • Step 1: Rewrite the "WB integration cribs" section in CLAUDE.md

Find the block in CLAUDE.md that starts with "WorldBuilder is acdream's rendering + dat-handling base, integrated as of Phase N.4 ship (2026-05-08)." and continues through the WB integration cribs (the bullet list of files and patterns).

Replace it with text that reflects post-O reality:

**WorldBuilder code is extracted into acdream's source tree as of Phase
O ship (2026-XX-XX).** The pure helpers (texture decode, scenery
transforms, terrain math) live in `src/AcDream.Core/Rendering/Wb/`.
The GL-touching infrastructure and mesh pipeline live in
`src/AcDream.App/Rendering/Wb/`. There is no `<ProjectReference>` to
`WorldBuilder.Shared` or `Chorizite.OpenGLSDLBackend` in any csproj —
those projects remain in `references/WorldBuilder/` as read-references
only (grep against them when porting NEW pieces).

ALL dat reads go through `DatCollection` (Phase O ship: ONE thing
touches the DATs). The previous `DefaultDatReaderWriter` second-reader
allocation in `WbMeshAdapter` is deleted.

[update the file-path cribs below the header to point at the new
locations under `src/AcDream.{Core,App}/Rendering/Wb/`]

Preserve all of the substantive cribs about GL pipeline behavior, the indirect-draw command struct, the surface-id rules, etc. — just retarget the file paths and remove the "WB is a project reference" language.

  • Step 2: Update docs/architecture/worldbuilder-inventory.md

Add a top-of-file banner indicating the inventory now describes "what we extracted vs what we left in references/." Update the §1 status line. The body inventory tables stay valuable (they describe the WB source we might still grep). Add a column or section noting "Extracted into acdream at Phase O" for each component that moved.

  • Step 3: Update the roadmap

In docs/plans/2026-04-11-roadmap.md, find Phase O in the "active" / "in flight" section. Move it to "shipped" with a one-line summary:

Phase O — DatPath Unification (2026-XX-XX, <commit-sha>): extracted
~33 WB files / ~7.7K LOC into src/AcDream.{Core,App}/Rendering/Wb/.
ObjectMeshManager refactored to take DatCollection (<refactor or
adapter>). Memory reduction <X> MB. Reference: docs/superpowers/specs/2026-05-21-phase-o-dat-path-unification-design.md.
  • Step 4: Update milestones doc

In docs/plans/2026-05-12-milestones.md, locate the M1.5 block. Phase O pre-empted M1.5 — add a brief writeup that O has shipped and M1.5 work can resume from the 2026-05-20 baseline.

  • Step 5: Build + test final

Run:

dotnet build
dotnet test

Expected: both green.

  • Step 6: Final commit + Phase O ship marker
git add CLAUDE.md docs/architecture/worldbuilder-inventory.md docs/plans/2026-04-11-roadmap.md docs/plans/2026-05-12-milestones.md
git commit -m @'
ship(O): Phase O — DatPath Unification — SHIPPED

ONE thing touches the DATs. WB code lives in our repo:
- src/AcDream.Core/Rendering/Wb/ — pure helpers (5 files, ~782 LOC)
- src/AcDream.App/Rendering/Wb/ — GL infra + mesh pipeline (~25 files, ~7K LOC)

Project references to WorldBuilder.Shared + Chorizite.OpenGLSDLBackend
dropped from AcDream.App.csproj and AcDream.Core.csproj. references/
WorldBuilder remains in-tree as read-reference.

DefaultDatReaderWriter removed; DatCollection is the only dat reader.
Working-set memory delta: <Z> MB reduction at radius=4 + 50 entities.

Visual side-by-side passed: Holtburg town, inn interior, dungeon.

Updates:
- CLAUDE.md: rewrote WB integration cribs to point at extracted locations.
- docs/architecture/worldbuilder-inventory.md: ownership now ours.
- Roadmap: moved Phase O to "shipped".
- Milestones: M1.5 resumes from 2026-05-20 baseline.

Spec: docs/superpowers/specs/2026-05-21-phase-o-dat-path-unification-design.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
'@
  • Step 7: Merge to main

Per spec O-D6 (one ship-window). Open a PR (or fast-forward merge if the worktree branch is clean) targeting main. Use gh pr create per CLAUDE.md if a PR is desired; or:

git checkout main
git merge claude/serene-leakey-e6b8bc --ff-only
git push

Verify with:

git status
git log -10

Self-review notes

Plan reviewed against spec sections 4, 5, 6:

Spec coverage:

  • §4.1 (mesh pipeline) → Task 4. ✓
  • §4.2 (GL infra) → Task 3. ✓
  • §4.3 (stateless helpers) → Task 2. ✓
  • §4.4 (NOT extracted) → no task needed (these are explicitly skipped).
  • §4.5 (drops) → Task 5. ✓
  • §5 task breakdown T2/T3/T4/T5/T7/T8/T9 → plan Tasks 1/3/4/2/5/6/7. ✓
  • §6 acceptance criteria → checked at Task 5 step 9 + Task 6 step 4 + Task 7. ✓
  • §7 risks → mitigations baked into task steps (T4 safety check at step 2; visual screenshots before Task 1 / Task 6 step 1). ✓

Placeholder scan:

  • "[Insert full MIT license body here...]" in Task 1 step 3 — intentional, requires reading the actual LICENSE file at step 4. Acceptable since step 4 explicitly tells the executor to fetch the content.
  • "exact filenames during closure walk" in Task 3 + Task 4 file-list — intentional honesty since the audit didn't persist the full per-file table. The closure-walk steps cover producing the exact list.
  • "<fill in: ...>" in commit message templates — intentional; commit messages parameterize on findings (e.g., ImageSharp reachability) discovered during the task.

Type consistency:

  • _dats field name preserved across tasks.
  • DatCollection class name verified — see Task 4 step 4b (executor confirms via grep).
  • Namespace conventions: AcDream.Core.Rendering.Wb for Core helpers, AcDream.App.Rendering.Wb for App GL/mesh code. Consistent.

Scope check: Single ship-window per spec O-D6. Plan is one cohesive unit; no decomposition needed.