acdream/docs/superpowers/plans/2026-07-05-mp1a-content-extraction.md
Erik bc0139e3f9 docs(pipeline): MP1a implementation plan - AcDream.Content extraction
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>
2026-07-05 19:58:29 +02:00

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, privatepublic on 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, TextureAtlasManager beyond 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 is TextureAtlasManager.TextureKey (ObjectMeshManager.cs:1286,1631,2236,2254); first real GL call is UploadGfxObjMeshData at :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.TextureKey reference site (find with grep -rn "TextureAtlasManager.TextureKey" src/)

  • Step 1: Move the struct. Copy the nested TextureKey struct out of TextureAtlasManager VERBATIM (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.TextureKeyTextureKey with using AcDream.Content;. Keep a using alias 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 into AcDream.Content (namespace AcDream.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 by ObjectMeshData fields).

    Explicitly stays in App: ObjectRenderData and ObjectRenderBatch (they hold TextureAtlasManager Atlas — a GL class) and anything else the compiler shows touching GL. If ObjectMeshData itself 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.cs from src/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 points public: 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 MeshExtractor in its constructor (passing its dat reader + metadata table) and every former Prepare* call site becomes _extractor.Prepare*. The staged-queue enqueue, worker pool, retry, and Dispose quiesce logic stay in ObjectMeshManager untouched (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.cs is 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 in src/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_drivers the 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"