Mechanical-move plan: lift TextureKey out of the GL atlas class, move the ObjectMeshData family, extract the Prepare* CPU pipeline verbatim into a new MeshExtractor in AcDream.Content (net10.0, no Silk.NET), App keeps queue/worker/upload lifecycle and delegates. Binding rules: verbatim bodies (no SurfaceDecoder switch - byte-identity is MP1b conformance foundation), compiler enumerates dependencies, BLOCKED on any hidden GL dependency, zero divergence rows. Gates: full suite green per task + user launch smoke. Boundary facts verified against source this session: the Prepare* region touches GL only via the TextureKey struct; first GL call is UploadGfxObjMeshData. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
13 KiB
MP1a — AcDream.Content Extraction 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 GL-free CPU half of the WB mesh/texture pipeline out of AcDream.App into a new AcDream.Content assembly, so the MP1b bake tool can run it without an OpenGL context — with zero behavior change (the client keeps running the exact same code, relocated).
Architecture: Mechanical move, not a rewrite. ObjectMeshManager's Prepare* family (dat read → mesh extraction → inline texture decode → ObjectMeshData) moves verbatim into a new MeshExtractor class in AcDream.Content; ObjectMeshManager keeps its queue/worker/GL-upload lifecycle and delegates. The one GL-domain entanglement is TextureAtlasManager.TextureKey (a pure-data struct nested in a GL class) — it gets lifted out first. Spec: docs/superpowers/specs/2026-07-05-modern-pipeline-design.md §6.1 ("Extraction prerequisite"). MP1b (bake tool + pak) and MP1c (streaming cutover) are separate plans.
Tech Stack: .NET 10 class library (no Silk.NET), BCnEncoder.Net.ImageSharp 1.1.2 + SixLabors.ImageSharp 3.1.12 (the inline DXT decode's packages), AcDream.Core project reference (TextureHelpers etc.).
Binding rules:
- Verbatim bodies. Method bodies move unchanged — same statements, same inline decode (do NOT "clean up" by switching to
SurfaceDecoder; the two decoders are not proven bit-identical and byte-identity is MP1b's conformance foundation). Only mechanical edits allowed: namespace/using lines,private→publicon moved members the old class must still reach, field references rehomed onto the new class. - The compiler enumerates dependencies. If moving a method drags in a type, move it if it's GL-free; if a dependency turns out to touch GL (
Silk.NET,OpenGLGraphicsDevice,GlobalMeshBuffer,TextureAtlasManagerbeyond the lifted key,ManagedGL*), STOP and report BLOCKED — do not improvise a redesign. - No divergence-register rows — a pure relocation introduces no behavior deviation. Any observed behavior change is a bug in the move.
- Verified boundary facts (2026-07-05, this worktree): the
Prepare*region's only GL-domain reference isTextureAtlasManager.TextureKey(ObjectMeshManager.cs:1286,1631,2236,2254); first real GL call isUploadGfxObjMeshDataat:1833. Line numbers drift — match by symbol.
File map
| File | Action | Responsibility |
|---|---|---|
src/AcDream.Content/AcDream.Content.csproj |
Create | GL-free content/asset assembly (net10.0) |
src/AcDream.Content/TextureKey.cs |
Create (lift) | The atlas dedup key struct, moved out of TextureAtlasManager |
src/AcDream.Content/ObjectMeshData.cs |
Create (move) | The boundary records: ObjectMeshData, MeshBatchData, TextureBatchData, VertexPositionNormalTexture, and sibling GL-free data records from ObjectMeshManager.cs's data region |
src/AcDream.Content/MeshExtractor.cs |
Create (move) | The Prepare* family + its private helpers/caches, verbatim |
src/AcDream.Content/ (as compiler demands) |
Create (move) | IDatReaderWriter.cs, DatCollectionAdapter.cs, AcSurfaceMetadata.cs, AcSurfaceMetadataTable.cs, EdgeLineBuilder.cs, GeometryUtils.cs — GL-free deps of the extractor (move only the ones the build actually requires) |
src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs |
Modify | Keeps queue/worker/upload lifecycle; delegates mesh prep to MeshExtractor; loses the moved types/methods |
src/AcDream.App/Rendering/Wb/TextureAtlasManager.cs |
Modify | Uses the lifted TextureKey |
src/AcDream.App/AcDream.App.csproj |
Modify | Add ProjectReference to Content |
AcDream.slnx |
Modify | Register the new project |
docs/architecture/worldbuilder-inventory.md |
Modify | Record the relocation (Code Structure Rule 2 requires the inventory note for project-reference changes) |
AcDream.Core is untouched. AcDream.Content references Core; App references Content; Core must NOT reference Content.
Task 1: Create the AcDream.Content project
Files:
-
Create:
src/AcDream.Content/AcDream.Content.csproj -
Modify:
AcDream.slnx,src/AcDream.App/AcDream.App.csproj -
Step 1: Project file
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<!-- MP1a: the inline DXT decode moved from ObjectMeshManager uses these
exact versions (mirror AcDream.App.csproj — keep in lockstep). -->
<PackageReference Include="BCnEncoder.Net.ImageSharp" Version="1.1.2" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\AcDream.Core\AcDream.Core.csproj" />
</ItemGroup>
</Project>
NOTE: Chorizite.DatReaderWriter flows transitively via AcDream.Core. If the moved code fails to resolve DRW types, add the explicit PackageReference pinned to the same version as Core (2.1.7) rather than bumping anything.
-
Step 2: Register in
AcDream.slnx— add the project entry following the existing<Project Path="src/AcDream.Core/AcDream.Core.csproj" />pattern (open the file, copy the shape). -
Step 3: App references Content — in
src/AcDream.App/AcDream.App.csproj, add alongside the existing ProjectReferences:
<ProjectReference Include="..\AcDream.Content\AcDream.Content.csproj" />
- Step 4: Build
Run: dotnet build
Expected: green (empty project compiles, references resolve).
- Step 5: Commit
git add src/AcDream.Content/ AcDream.slnx src/AcDream.App/AcDream.App.csproj
git commit -m "feat(pipeline): MP1a - AcDream.Content assembly scaffold"
Task 2: Lift TextureKey out of TextureAtlasManager
Files:
-
Create:
src/AcDream.Content/TextureKey.cs -
Modify:
src/AcDream.App/Rendering/Wb/TextureAtlasManager.cs(delete the nested struct, use the lifted one) -
Modify: every
TextureAtlasManager.TextureKeyreference site (find withgrep -rn "TextureAtlasManager.TextureKey" src/) -
Step 1: Move the struct. Copy the nested
TextureKeystruct out ofTextureAtlasManagerVERBATIM (fields, equality/GetHashCode if present, doc comments) into:
namespace AcDream.Content;
// MP1a (2026-07-05): lifted verbatim out of TextureAtlasManager (GL class)
// so the GL-free mesh-extraction path (MeshExtractor / ObjectMeshData) can
// reference the atlas dedup key without a Silk.NET dependency. Field set,
// equality semantics, and hash behavior are UNCHANGED.
public struct TextureKey
{
// ... the verbatim body from TextureAtlasManager.TextureKey ...
}
(The plan intentionally does not restate the body — copy it exactly from the source; it is pure data: SurfaceId/PaletteId/Stippling/IsSolid per the boundary survey.)
-
Step 2: Update all reference sites.
TextureAtlasManager.TextureKey→TextureKeywithusing AcDream.Content;. Keep ausingalias only if a name collision appears (none expected). -
Step 3: Build + full test suite
Run: dotnet build then dotnet test
Expected: green / all passing (this is a pure type relocation).
- Step 4: Commit
git add -A
git commit -m "refactor(pipeline): MP1a - lift TextureKey out of the GL atlas class"
Task 3: Move the boundary data records
Files:
-
Create:
src/AcDream.Content/ObjectMeshData.cs -
Modify:
src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs(types removed), all reference sites (grep -rn "ObjectMeshData\|VertexPositionNormalTexture\|TextureBatchData\|MeshBatchData" src/ tests/) -
Step 1: Move these types from
ObjectMeshManager.cs's data region intoAcDream.Content(namespaceAcDream.Content), VERBATIM:VertexPositionNormalTexture(the 32-byte GPU vertex — this is the pak layout target, do not alter layout/stride),ObjectMeshData,MeshBatchData,TextureBatchData, plus any small GL-free records the compiler then demands (e.g. setup-part / emitter-info records referenced byObjectMeshDatafields).Explicitly stays in App:
ObjectRenderDataandObjectRenderBatch(they holdTextureAtlasManager Atlas— a GL class) and anything else the compiler shows touching GL. IfObjectMeshDataitself turns out to hold a GL-typed field the survey missed, STOP and report BLOCKED. -
Step 2: Update reference sites (
using AcDream.Content;in the App files + any tests). -
Step 3: Build + full test suite — green / all passing.
-
Step 4: Commit
git add -A
git commit -m "refactor(pipeline): MP1a - ObjectMeshData family moved to AcDream.Content"
Task 4: Extract MeshExtractor (the Prepare* family)
Files:
-
Create:
src/AcDream.Content/MeshExtractor.cs -
Move as the compiler demands:
IDatReaderWriter.cs,DatCollectionAdapter.cs,AcSurfaceMetadata.cs,AcSurfaceMetadataTable.cs,EdgeLineBuilder.cs,GeometryUtils.csfromsrc/AcDream.App/Rendering/Wb/→src/AcDream.Content/(verbatim, namespace change only) -
Modify:
src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs -
Step 1: Create the extractor class. New class in Content:
namespace AcDream.Content;
/// <summary>
/// MP1a (2026-07-05): the GL-free CPU half of the former ObjectMeshManager —
/// dat read → polygon walk → vertex/index build → inline texture decode →
/// <see cref="ObjectMeshData"/>. Extracted VERBATIM so the MP1b bake tool
/// and the live client run the SAME extraction code (byte-identical output
/// is the pak conformance foundation). ObjectMeshManager (App) retains the
/// queue/worker lifecycle and all GL upload; it delegates here.
/// Spec: docs/superpowers/specs/2026-07-05-modern-pipeline-design.md §6.1.
/// </summary>
public sealed class MeshExtractor
{
// Constructor takes the extraction dependencies the moved methods used
// as ObjectMeshManager fields — expected from the boundary survey:
// IDatReaderWriter dats, AcSurfaceMetadataTable surfaceMetadata
// plus the CPU-side caches that belong with extraction:
// the ThreadLocal<BcDecoder>, the decoded-texture cache, bounds cache.
// Let the compiler produce the exact list — add fields here, do not
// leave them behind in App if only Prepare* uses them.
}
-
Step 2: Move the methods VERBATIM onto
MeshExtractor, making the entry pointspublic:PrepareMeshData,PrepareGfxObjMeshData,PrepareEnvCellMeshData,PrepareCellStructMeshData,PrepareSetupMeshData,PrepareCellStructEdgeLineData, and the private helpers they call (CollectParts,ComputeBounds,CollectDrawingBspPolygonIds,BuildPolygonIndices,BuildCellStructPolygonIndices,CollectEmittersFromScript, plus whatever else the compiler names). Move the fields/caches only they use. Bodies unchanged. -
Step 3: Delegate from ObjectMeshManager. It constructs one
MeshExtractorin its constructor (passing its dat reader + metadata table) and every formerPrepare*call site becomes_extractor.Prepare*. The staged-queue enqueue, worker pool, retry, andDisposequiesce logic stay inObjectMeshManageruntouched (the MP0-investigated teardown discipline is a runtime concern; the bake tool will drive the extractor with its own simple loop in MP1b). -
Step 4: Move the support files the compiler demands (list above), namespace
AcDream.Content, bodies verbatim.Building.csis NOT in scope (its GL-query fields keep it in App for now). -
Step 5: Build + FULL test suite
Run: dotnet build then dotnet test
Expected: green / all passing. The full suite is the behavior gate — no test may change.
-
Step 6: Update the inventory doc. In
docs/architecture/worldbuilder-inventory.md, add an MP1a note under the extracted-code section: the CPU mesh-extraction half now lives insrc/AcDream.Content/(list the moved files), App retains GL upload/lifecycle, Core untouched, reason = MP1 bake tool needs GL-free extraction (spec §6.1). -
Step 7: Commit
git add -A
git commit -m "refactor(pipeline): MP1a - MeshExtractor extracted to AcDream.Content (verbatim move)"
Task 5: Launch smoke (user-gated)
- Step 1:
dotnet build -c Release— green. - Step 2: Launch per the standard live-launch recipe (CLAUDE.md), user in-world for ~2 minutes: Holtburg buildings + an interior + a dungeon teleport. Acceptance: world renders identically — no missing/white/wrong geometry or textures (the #105-class signature would indicate the move broke a per-frame driver; per
feedback_extraction_perframe_driversthe lifecycle stayed in App precisely to avoid this, so any anomaly = STOP and investigate the move). - Step 3: On user confirmation, update the roadmap Track MP table (MP1a slice shipped) and commit.
git add docs/plans/2026-04-11-roadmap.md
git commit -m "docs(pipeline): MP1a shipped - Content extraction user-gated"