Fix round 1 of the two the plan allows for S4-c2 (048d5b12f), addressing every item M1-M8/A1-A8 from the campaign branch's packet section 11. M1 (BLOCKING): ParticleRenderer.DeferToRetailAlphaQueue threw whenever a mesh-particle batch's TranslucencyKind.Opaque classification (mask 0x00) reached a non-Append DrawMesh row - a real, reachable outcome since TryAppendMeshDraws applies no IsOpaque-style pre-filter unlike WbDrawDispatcher's path. Deleted both throws (ParticleRenderer.cs, WbDrawDispatcher.cs SubmitToAlphaQueue), extracted the router-input derivation into internal static ParticleRenderer.RouteParticleSubmission (directly testable), and implemented rows 1/5's immediate draw by reusing the existing per-instance billboard/mesh draw code (new DrawImmediateParticleSubmissionRhi, no new renderer, no sort per A8). materialHasAlpha (row 4) is derived from CMaterial::CheckAlphaValues @0x005396a0 (Ghidra-verified 2026-09-04): has_alpha=0 only when Ambient.a==Diffuse.a==Specular.a==Emissive.a==1.0f, else 1 - and CMaterial::SetTranslucencySimple @0x005396f0 (reached from CPhysicsPart::SetTranslucency <- Particle::Update @0x0051c290's per-frame alpha-fade) sets all four channels to (1f - translucency) uniformly, so has_alpha == "this particle's current alpha isn't 1.0" - already carried in ColorArgb's top byte (Lerp(StartAlpha,EndAlpha,tLife), ParticleSystem.cs 1039-1041/1453). M3: SubmitToAlphaQueue now takes isBuildingShell + viewProjection and feeds Row 1's detailSurfaceActive gate for translucent building-shell instances (retail DrawBuilding @0x0059f2a0 installs building detail before its own shell subsets reach DrawMesh). Row 1 (detail on) draws the instance immediately at its own walk-stream AlphaSubmitMark via new DrawImmediateAlphaInstance/DrawImmediateAlphaInstanceRhi (reusing WriteDeferredAlphaEntrySlot + the drain's pipeline/detail code for slot 0); never touches _deferredAlpha/the queue for pure-Immediate submissions (fixes a latent leak: previously reserving a _deferredAlpha slot for every candidate meant a frame with ALL-immediate content never registered a source, so ResetAlphaSubmissions never ran). M3c: DrawPreparedAlphaBatchRhi's own per-command hasDetail/DrawBuildingDetailRangeRhi branch is deleted - detail-eligible content no longer reaches the drain (spec: delayed replay is always detailEnabled=0). New AD-120 register row for the "in place -> at the entity's own stream mark" granularity adaptation this introduces. M6: RetailPViewPassExecutor.SubmitOrDrawTransparentCellShell routes an EnvCell's transparent shell through the SAME RetailAlphaMeshRouter table (mask 0x02, detailSurfaceActive = EnvCellRenderer.TransparentDetailEnabled) instead of a hand-coded branch. Detail on: unchanged immediate draw. Detail off: appends ONE token per CELL via new EnvCellAlphaDrawSource (internal, delegate-based so it's unit-testable without a GPU harness), replayed through EnvCellRenderer.RenderTransparentOrdered at the next flush - RenderTransparentOrdered already replayed a cell's batch on demand, so no new deferred-replay abstraction was needed (the original landing's claim that one was required is corrected in the packet). M4: RetailAlphaEntry.IsFirstForList deleted. Retail's per-DrawMesh- invocation "first for this list" flag is trivially true for every subset retail ever appends (DrawMesh @0x0059d4a0 sets both flags true at entry, 0059d4cc/0059d4d0, clearing each independently after ITS OWN first append within that call, 0059d5ef) - the captures confirm it (new=1 on 9,685/10,556 AM lines, impossible under "first since last drain"). acdream's deleted isFirstForList = target.Count == 0 computed the wrong quantifier. One acdream append already IS one DrawMesh call, so the field carried no information and its own pin was vacuous (feedback_every_new_pin_must_be_ shown_to_fail) - deleted rather than kept as an always-true no-op. M5: split all six AlphaFlushTranscript_* KnownFailure Facts into live AlphaFlushSites_* (site+threshold sequence only, content-independent, unconditional pass) and AlphaFlushCounts_* (KnownFailure, full tuple incl. drained counts) sharing one RunAlphaFlushTranscriptReplay helper. Rewrote the KnownFailure reason honestly: THREE independent causes, not "harness content only" - (1) zero live mesh content, (2) acdream appends one entry per INSTANCE vs retail's one per SUBSET per DrawMesh call, (3) acdream's CLIP list is structurally empty for ordinary content (M2). Added ONE live valve pin (WalkFrameDriverTests.SortCellExit_ValveDrainsThroughReplayAtThe ExactBoundary, a Theory over 2250/2249) that goes through WalkFrameDriver. Replay's own SortCellExit arm, not RetailAlphaQueue.Flush called directly. M2/A2/M8c (register): ~~AP-34~~ restored as struck-through RETIRED (it had been deleted outright, not struck, breaking the register's own convention) citing residuals AP-238/AP-239/AP-240. The chunk-2 commit's "AP-236"/ "AP-237" collided with an id ALREADY filed-and-retired on main by an unrelated #132 fix (docs/ISSUES.md:19976) - ids are never reused, so both are renumbered: AP-236->AP-238 (EnvCell; rewritten per M6 - granularity-only residual now, "never through the shared queue" is retired), AP-237->AP-239 (mask-reconstruction gap; the dangling "AP-236b" citation in RetailAlphaMeshRouter.cs never matched either filed id and is corrected to AP-239). New AP-240 filed for M2: WbDrawDispatcher.IsOpaque filters ClipMap out upstream, so ordinary clip-mapped content never reaches retail's CLIP FIFO (draws immediately on the pre-existing OpaqueAlphaToCoverage pipeline instead) - no pipeline change this round, per the contract. AP section header recounted honestly: 159 active (was stale at 161); AD section 92 (was 91, +AD-120). M7: AP-239's only cited example (cloud GfxObj 0x01004C35, surface 0x08000023) is DISPROVEN - decoded Type=0x10114 has alpha-family bits that win in BOTH retail's real priority and acdream's reconstruction, so it never actually diverges. A 2026-09-04 scan of all 6,152 Portal-dat Surfaces found 27 real Translucent+Base1ClipMap-no-alpha-family instances (Type=0x14); cross-referencing GfxObjs found surface 0x08000015 on GfxObj 0x010001EC - cited in place of the disproven cloud example. M8: (a) RenderDeviceD3D::DrawBlock is @0x005a17c0 per symbols.json, not @0x005a18d0 (that is its per-land-cell loop head) - fixed across RetailAlphaQueue.cs, RetailPViewPassExecutor.cs, WalkEvents.cs, WalkFrameDriver.cs (x3), RetailFrameWalk.cs. (b) RetailAlphaQueue.cs's four FlushAlphaList addresses were labelled "return site" but are the CALL instruction's own address (x86 CALL rel32 is 5 bytes) - relabelled "call at X (returns to X+5)". (d) worldbuilder-inventory.md:420's AP-34 citation updated. A1: RetailPViewRenderer.cs's outdoor-root passes.FlushLandscapeAlpha() call (labelled LandscapeFlush) is deleted - retail's real LandscapeFlush (PView::DrawCells @0x005a4840's own FlushAlphaList(0f) @0x005a4872) sits strictly inside if (outside_view.view_count > 0) on the INTERIOR PView; an outdoor root never reaches it (terrace-edge capture: zero 005a4877 FL lines). Verified no opaque draw intervenes between this deletion point and RetailAlphaQueue.EndFrame()'s own RenderNormalMode pass-end flush (WorldSceneRenderer.cs: DrawPostWorldParticles's PView branch is an explicit no-op; only diagnostic wireframes follow) - #132's "drain after the finished opaque world" invariant is kept by that flush alone. A4: RetailAlphaQueue's ApplyScratchRetention call sites passed the ENTRY count as the SOURCE-count argument too (observedClip + observedAlpha twice), so _sources/_sourceDrawOffsets never shrank toward the real source count. Fixed to capture _sources.Count before Clear(). A5/A6/A7: WalkStaticStreamPopulatorTests.cs's stale "same viewer distance" banner corrected; two off-by-one entry counts in the packet's section 10 sequences corrected (foundry-deep x10->x11, holtburg-doorway-still x5->x6); WalkAlphaDepthTrace.cs's "new=" comment explains why IsNew is retained but never compared (M4's finding makes a per-subset comparison meaningless at acdream's per-instance granularity); RetailAlphaMeshRouterTests.cs's IndependentReferenceRoute (never actually independently-shaped) renamed to RestatedBranchTableRoute with an honest doc comment, and the unverified "flipped in turn and failed every time" claim deleted. A3 was resolved as a side effect of M1's SubmitToAlphaQueue doc-comment rewrite (the wrong "0x02/0x03, 0x04/0x05, 0x08/0x09" enumeration no longer exists); RetailAlphaMeshRouter.cs's own MaskFromTranslucencyKind comment updated in the same spirit (M1's particle-site Opaque-reachability finding). Mutation checks (each applied to a scratch-restored copy, confirmed failing, then byte-exact reverted - diffed against a /tmp backup after restore): - A1 (RetailPViewRendererTests.DrawInside_NeverCallsFlushLandscapeAlphaDirectly): restoring the deleted "if (ctx.RootCell.IsOutdoorNode) passes.FlushLandscapeAlpha();" line reintroduces a direct RetailPViewPassExecutor.FlushLandscapeAlpha call in DrawInside's compiled body. Actual failure: "Assert.DoesNotContain() Failure: Filter matched in collection ... CompiledCall { ... Target = Void FlushLandscapeAlpha() }". - A4 (RetailAlphaQueueTests.RetainedSourceCapacity_ConvergesToTheReal SourceCountNotTheEntryCount): reverting both ApplyScratchRetention calls to (observedClip + observedAlpha, observedClip + observedAlpha) - with 100 distinct sources in the spike frame (so _sources.Capacity grows to 128, past its initial 4) followed by three low-demand frames of 10 entries via ONE repeated source - makes sourceTarget compute from 10 (Math.Max(4,10*2)=20) instead of 1 (Math.Max(4,1*2)=4). Actual failure: "Expected the retained source-array capacity to converge toward the real source count (1), but it stayed at 20". (A single-source-only spike does NOT discriminate this bug - Math.Min(sourceTarget, _sources.Capacity) clamps both formulas to the same unchanged initial 4 - hence the 100-source setup.) - M5 valve (WalkFrameDriverTests.SortCellExit_ValveDrainsThroughReplayAt TheExactBoundary, 2250 case): flipping RetailAlphaQueue.Flush's no-op comparison from "<" to "<=" (S4-c1/c2's own boundary mutation) makes the 2250-entries case a no-op instead of draining. Actual failure: "Assert.Equal() Failure: Values differ Expected: 0 Actual: 2250". - M6 (EnvCellAlphaDrawSourceTests, both Facts): adding an immediate renderTransparentOrdered(new[]{cellId}) call inside EnvCellAlphaDrawSource.AddPendingCellId (simulating the M1/M3 "drew immediately instead of deferring" bug class) makes PendingCellToken_IsDrainedAtTheFlushAndNeverBeforeIt fail with "Assert.Empty() Failure: Collection was not empty Collection: [[4660]]" and ParticleAppendedBetweenTwoCellTokens_KeepsItsPositionInTheCombined Drain fail with 'Expected: string[] ["cell:256", "particle:7", "cell:512"] Actual: List<string> ["cell:256", "cell:512", "cell:256", "particle:7", "cell:512"]'. - M1 (ParticleRendererRouteTests.OpaqueClassifiedMeshBatch_WithMaterialAlpha _RoutesToAlphaAppend): hardcoding materialHasAlpha to false inside RouteParticleSubmission makes the Opaque-mask+has-alpha case route Immediate instead of Append. Actual failure: "Assert.Equal() Failure: Values differ Expected: Append Actual: Immediate". Gates (real output lines): - dotnet build AcDream.slnx -c Release -> "Build succeeded. 0 Warning(s) 0 Error(s)". - Hermetic lane (--filter "Lane!=InstalledDat&Lane!=PreparedPackage& Lane!=Live&Lane!=Manual&Lane!=Timing&Lane!=Windows&Lane!=Linux& Lane!=SystemFont&Purpose!=Diagnostic&Status!=KnownFailure") -> "Passed! - Failed: 0, Passed: 6864, Skipped: 0, Total: 6864". - InstalledDat lane (ACDREAM_RUN_INSTALLED_DAT_TESTS=1, ACDREAM_DAT_DIR="C:/Users/erikn/Documents/Asheron's Call", --filter "Lane=InstalledDat") -> "Failed: 10, Passed: 255, Skipped: 1, Total: 266" - the exact 4 pre-existing failures (TowerAscent_StaircaseStaysConeVisible_EveryStep, LayoutImporterMediaBearingChildSweepTests, LayoutImporterInvisibleSweep Tests, Oh_doorway_still_first_frame_diff) plus the six AlphaFlushCounts_* KnownFailure Facts, nothing else; all six AlphaFlushSites_* Facts pass inside this same lane (the whole WalkTraceConformanceTests partial class carries Lane=InstalledDat, so they cannot run hermetically). - Shader classes (VulkanShaderDescriptorContractTests, VulkanShaderManifestTests, RenderPackSpirvValidatorTests) -> "Passed! - Failed: 0, Passed: 32, Skipped: 0, Total: 32". - RetailAlphaQueueTests, RetailAlphaMeshRouterTests, RetailFrameWalkTests, WalkFrameDriverTests, ParticleRenderer*Tests, EnvCellRenderer*Tests (combined filter) -> "Passed! - Failed: 0, Passed: 120, Skipped: 0, Total: 120". - Register collision grep: AP-238/AP-239/AP-240/AD-120 each count exactly 1; ~~AP-34~~ count exactly 1; live totals AP=159, AD=92. Not done / deferred: none - all M1-M8/A1-A8 items landed; A8 was a no-action note. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
47 KiB
WorldBuilder Inventory — what we extracted, adapted, or left behind
Phase O shipped 2026-05-21. The ~33 WB files we actually use have been extracted into our tree.
references/WorldBuilder/stays as a read-reference only — nothing insrc/AcDream.*references it as a project dependency.DatCollectionis now the only dat reader in process.Use this document to:
- Know where our extracted code lives (look for the "Extracted to" column / notes in each section below).
- Know what WB still has that we haven't needed yet — grep
references/WorldBuilder/if you ever need to add something.- Know what WB never had (the 🔴 list) — those are always ours.
Pre-O status (archived for context): As of Phase N.4 (2026-05-08) acdream relied heavily on WorldBuilder as a project reference for rendering and dat-handling. WorldBuilder is MIT-licensed, verified by visual inspection to render the AC world correctly (terrain, scenery, slabs, dungeons, slopes, particles), and uses the same Silk.NET + .NET stack we target.
Post-O integration model: Extracted WB code lives in two locations in our tree (see CLAUDE.md for the full breakdown):
src/AcDream.Core/Rendering/Wb/— pure helpers (no GL):TerrainUtils,TerrainEntry,RegionInfo,SceneryHelpers,TextureHelpers.src/AcDream.App/Rendering/Wb/— Vulkan/RHI infrastructure + mesh pipeline:ObjectMeshManager,WbMeshAdapter,WbDrawDispatcher, texture cache, shader infra, EnvCell/portal/scenery/terrain-blending pipeline classes.
EnvCell streaming seam: EnvCellLandblockBuildBuilder is an acdream-owned
adapter around the extracted WB rendering path. One streaming job privately builds
the complete portal-cell + shell-placement payload, and EnvCellRenderer.CommitLandblock
publishes that completed snapshot on the render thread. WB's geometry-id arithmetic
and mesh preparation remain unchanged; the transaction wrapper exists because
acdream streams asynchronously while the WB editor's manager owned its own loading
loop. Do not reintroduce worker-side RegisterCell calls or shared pending cell
collections.
DatCollectionAdapter bridges the sole DatCollection to Content's
IDatReaderWriter. Since MP1c, production ObjectMeshManager no longer reads
DAT; the adapter remains the bounded typed-object access seam for runtime
non-render content plus explicit bake/equivalence tooling.
MP1a (2026-07-05): CPU mesh-extraction half moved to AcDream.Content.
The GL-free portion of the former ObjectMeshManager — dat read → polygon
walk → vertex/index build → palette/conditional BCn texture decode →
ObjectMeshData — is now MeshExtractor in a new src/AcDream.Content/
assembly (no Silk.NET dependency), so the MP1b bake tool can run the exact
same extraction code offline without an OpenGL context. This was a
mechanical, verbatim move (namespace + visibility only) per
docs/superpowers/plans/2026-07-05-mp1a-content-extraction.md — no
behavior change, no divergence-register row.
src/AcDream.Content/MeshExtractor.cs— thePrepare*family (PrepareMeshData,PrepareSetupMeshData,PrepareGfxObjMeshData,PrepareEnvCellMeshData,PrepareCellStructMeshData,PrepareCellStructEdgeLineData) + private helpers (CollectParts,CollectEmittersFromScript,ComputeBounds,BuildPolygonIndices,BuildCellStructPolygonIndices) and the decoded-texture cache /ThreadLocal<BcDecoder>that back them.src/AcDream.Content/ObjectMeshData.cs— the CPU-side boundary records:VertexPositionNormalTexture,StagedEmitter,ObjectMeshData,MeshBatchData,TextureBatchData.src/AcDream.Content/TextureKey.cs— the atlas dedup key, lifted out of the GL-owningTextureAtlasManager(which stays in App and now references the lifted struct).src/AcDream.Content/UploadFormats.cs— Content-ownedUploadPixelFormat/UploadPixelTypeenums carried byMeshBatchData/TextureBatchDatainstead ofSilk.NET.OpenGL.PixelFormat/PixelType(Content must stay Silk.NET-free — the bake tool must not ship GL binaries). Underlying values are the GL ABI constants, numerically identical to the Silk.NET members; App casts at its single upload boundary (theAddTexturecall inUploadGfxObjMeshData) via a lifted nullable enum conversion — value- and null-preserving.src/AcDream.Content/IDatReaderWriter.cs,EdgeLineBuilder.cs— GL-free dependencies of the extractor, moved (namespace-only) alongside it.- Side-stage sink seam:
CollectEmittersFromScriptpre-loads particle GfxObj meshes mid-extraction and, pre-MP1a, enqueued them directly ontoObjectMeshManager._stagedMeshData. The extractor now takes anAction<ObjectMeshData>? sideStagedSinkconstructor parameter; App wires it to_stagedMeshData.Enqueue, preserving the original immediate-enqueue semantics exactly — including on a mid-Prepare*throw (preloads staged before a malformed-dat texture-decode exception survive, as they always did). The MP1b bake tool passes its own collector. - Stays in
src/AcDream.App/Rendering/Wb/:ObjectMeshManager(the staged-queue/worker-pool/Dispose-quiesce lifecycle and all Vulkan/RHI upload; production workers now consumeIPreparedAssetSource),ObjectRenderData/ObjectRenderBatch(hold a GLTextureAtlasManagerfield),TextureAtlasManager,GeometryUtils(used only by App-side raycasting, not by extraction),AcSurfaceMetadata/AcSurfaceMetadataTable(not on the extraction path),Building.cs(explicitly out of scope). AcDream.Coreis untouched;AcDream.ContentreferencesAcDream.Core(forTextureHelpers,Sphere/BoundingBoxviaChorizite.Core.Lib);AcDream.AppreferencesAcDream.Content.AcDream.Coredoes NOT referenceAcDream.Content(one-way dependency, per Code Structure Rule 2).- Reason: MP1 (
docs/superpowers/specs/2026-07-05-modern-pipeline-design.md§6.1) — the bake tool needs the identical mesh/texture extraction code running with no GL context, so baked pak output and live-client output stay byte-identical.
MP1b EnvCell content identity correction (2026-07-24). The extracted
WorldBuilder EnvCellRenderManager.GetEnvCellGeomId 31× polynomial is not a
safe unique resource key. A guarded full retail-DAT catalog found the concrete
collision 0x00030175 versus 0x01BC0105: different environment/surface
tuples both map to 0x00000002020E8C13 and contain different polygons.
AcDream.Core.Rendering.Wb.EnvCellGeometryIdentity now owns one namespaced
FNV-1a identity shared by the App streaming build and acdream-bake; the
legacy calculation remains executable only for the conformance test that proves
the collision. The bake additionally compares the complete source tuple before
aliasing and fails on any collision. This is an acdream resource-ownership seam,
not a second DAT interpreter; DatCollection and MeshExtractor remain the
only reader/extractor path.
MP1c production prepared-asset cutover (2026-07-24). Production
world-mesh workers no longer invoke MeshExtractor or rebuild Setup, GfxObj,
EnvCell, Surface, palette, and texture graphs during portals. The validated
machine-local acdream.pak is opened through Content's
IPreparedAssetSource; typed GfxObj and EnvCell requests deserialize immutable
ObjectMeshData while retaining the existing App worker, staging, render-thread
upload, cache, ownership, and shutdown contracts. The prepared render payload
persists exact batch translucency so App does not reconstruct a
GfxObjMesh for metadata. Setup activation uses the package TOC as an explicit
type-presence index before reading valid Setup records through the bounded DAT
cache. DatPreparedAssetSource and MeshExtractor remain explicit
bake/equivalence/UI-Studio tools, not a production fallback. Portal → HighRes
→ Language → Cell lookup precedence is encoded directly in
DatCollectionAdapter.TryResolvePreferred. The connected physical and
installed-DAT gates are recorded in
docs/research/2026-07-24-slice-c-prepared-asset-cutover-report.md.
Launcher cumulative-overlay extension (2026-08-25). Production still has
no live-DAT fallback and consumes the same prepared-payload contracts. For a
bounded recipe migration, App and Headless may receive one complete base pak
plus one cumulative filtered pak through LayeredPreparedAssetSource. The
overlay is probed first: Missing falls through to the base, while a present but
corrupt render or collision payload remains authoritative corruption. Both
mapped owners share one composite lifetime and there is never an overlay
chain. The launcher binds the overlay to the base digest in the optional
pak/content.current.json sidecar; format/global extraction migrations retain
the explicit full-rebuild path. A tiny pak/content.client-pending marker
keeps either result non-launchable until the matching client is confirmed,
including across a crash/restart. Design and gates:
docs/plans/2026-08-25-launcher-content-stabilization.md.
PAK v2 resource closeout (2026-08-27). Format 2 / bake recipe 6 retains
the fixed header and sorted random-access TOC while moving texture arrays into
one globally shared type-8 payload partition and independently applying
adaptive Brotli compression with raw fallback. Unedited DXT1/3/5 surfaces
retain exact DAT BC blocks through Vulkan upload; clip maps and authored
translucency continue through the RGBA edit path. The reader bounds decoded
blobs and retains shared texture arrays in a 64 MiB / 1,024-entry LRU. The
complete installed package is 597,229,424 bytes versus 29,908,271,024 bytes
for format 1; four- and nine-worker bakes have identical SHA-256. The
authoritative connected before/after route reduced heavy-route working set by
48.0%, private bytes by 26.1%, and prepared-mesh GPU bytes by 25.9% with equal
or faster matching reveal/frame percentiles. Format changes remain mandatory
launcher-confirmed full rebuilds with beside-active validation and atomic
promotion; overlays never cross a format boundary. Design and evidence:
docs/plans/2026-08-27-pak-v2-resource-campaign.md.
Retail GfxObj view-sphere correction (2026-08-31). Bake recipe 7
regenerates prepared GfxObj render records so their portal-view admission
uses the authored DrawingBSP root sphere retained by retail
CGfxObj::Serialize, rather than a sphere synthesized from the extracted
vertex AABB. The binary package format remains 2. Because this field affects
every GfxObj and the old records cannot be corrected without their source
DrawingBSP, migration 6→7 is an explicit full rebuild.
The production leaf follows retail RenderDeviceD3D::DrawMesh /
DrawMeshInternal: portal viewcones perform coarse authored-sphere
admission, but an admitted GfxObj is submitted once as a complete mesh.
Likewise PView::DrawCells calls DrawEnvCell once per admitted cell and
draws its complete constructed shell. Portal polygons are not GPU mesh clips;
pixel occlusion comes from the ordered walk and depth buffer.
Retail static render-shadow cell membership (2026-08-31; ownership moved
to ShadowObjectRegistry by Campaign OVERHAUL S2 chunk 2; collision cutover
by S2 chunk 3, 2026-09-02). Indoor statics are not rendered solely from
their authored parent EnvCell. Retail's CEnvCell::init_static_objects
creates a CPhysicsObj for every static, then calc_cross_cells_static +
CPartArray::AddPartsShadow places every visual part in each crossed cell's
shadow_part_list, including decorative GfxObjs that have no collision BSP —
one CELLARRAY flood feeds BOTH the collision shadow_object_list and the
render shadow_part_list (Contract B). ShadowObjectRegistry now mirrors
this exactly: RegisterMultiPart/Register's optional partArray:
parameter, when supplied (every production call site since chunk 1b), drives
ONE Contract A flood (ComputeContractACellArray) whose result becomes BOTH
GetOwnerCells/_entityToCells/_cells (collision) and the retained
per-entity retail CELLARRAY (TryGetRetailCellArray) plus per-cell part
entries (GetRetailPartEntriesInCell) — not two independent floods, and not
rebuilt per frame. A caller that supplies no part array (only test/legacy
call sites remain) keeps the pre-chunk-3 collision-only dispatch untouched.
Consumer cutover to a borrowed per-cell view (S2 chunk 5, 2026-09-03).
WalkProductionWorldData's three per-frame scene sweeps (indoor static,
outdoor static, dynamic) and their five per-cell bucket dictionaries are
deleted. GetCellStatics/GetCellDynamics/GetOutdoorStatics/
GetOutdoorDynamics instead read
ShadowObjectRegistry.GetRetailPartEntriesInCell for the QUERIED cell
directly — already in retail CELLARRAY-then-part-array insertion order —
collapse the entries to their distinct owning entity ids (an entity's own
run of entries for one cell is always contiguous, since the registry
removes-then-re-adds a whole entity's rows atomically), and resolve each id
back to its RenderProjectionRecord through
RenderSceneQuery.TryGetByLocalEntityId — a new presentation-side index
ArchRenderScene maintains alongside its existing per-cell/per-class
indices, keyed by the SAME RenderSourceMetadata.LocalEntityId the
registry is keyed by (App-only presentation state, never a second
membership source). Statics vs. dynamics is the record's own
ProjectionClass (LiveDynamicRoot/EquippedChild are dynamic; every
other class, including ActiveAnimatedStatic, is static — the same split
ArchRenderScene's own internal indexing already used); building shells
are excluded from both — they draw at their own building's shell turn
(_shellsByAnchor, still filled by the ONE surviving narrow sweep, since
buildings are the landcell building channel, out of S2's scope), never at
the cell's ordinary object-list turn. A structural consequence carried
over from chunk 3: an object mixing a colliding BSP part with a decorative
non-BSP part gets a collision row for its BSP part in every cell the
combined CELLARRAY reaches, including cells the BSP part alone would not
have crossed — membership is decided by the whole-part-array flood, not
per part; the BSP polygon test still decides actual contact at query time.
An entity the registry HAS flooded into a cell but whose projected record
the presentation journal has not applied yet this frame (the transient
race between the physics publisher and the projection journal — AD-116)
contributes to NO cell for that frame; there is no fallback to an authored
parent cell or a root-position cell any more (both deleted this chunk),
matching retail's own rule that an object not yet in a cell is not drawn.
Every distinct entity id this happens for in one frame is counted once in
WalkProductionWorldData.UnregisteredRenderMembershipCount (renamed from
UnregisteredStaticRenderFallbackCount, which under the pre-chunk-5 model
also covered the separate dynamic-record fallback) and reported by one
print-only [walk-membership] line — gated on
RenderingDiagnostics.ProbeFacilityStairsEnabled, the walk family's
existing diagnostic flag — at the start of the next frame when nonzero.
A primitive-only Setup may reuse its already-authored cylsphere collision
cell set; BSP-bearing and pure-visual statics use the all-visual-part box
walk. The Facility Hub stair Setup 0x02000623 is the installed-DAT
regression: its authored parent is 0x8A02015F, while its visual parts also
register in 0x8A02015E and the adjoining vertical cells.
Moving multipart objects and equipped children (S2 chunk 4, 2026-09-02).
Moving multipart objects follow the sibling retail route rather than their
authored/feet cell alone: CPhysicsObj::add_shadows_to_cells installs every
CPartArray part in every cell of the object's retained CELLARRAY, and
CPhysicsPart::Draw stamps each part only after that part passes a cell's
portal test. WalkProductionWorldData.GetCellDynamics/GetOutdoorDynamics
therefore read the SAME per-cell GetRetailPartEntriesInCell borrowed view
the static getters use (S2 chunk 5), filtered to the dynamic
ProjectionClasses — not a per-record TryGetRetailCellArray lookup with
its own fallback any more; an entity the registry has flooded into a cell
but the presentation scene cannot resolve yet contributes to no cell there,
counted by the shared UnregisteredRenderMembershipCount (AD-116), while
the walk classifier owns a separate projection+part drawn-pass stamp.
Retail advances that stamp after
LScape::draw + FlushAlphaList and before the interior depth clear
(PView::DrawCells @0x005A4886), so a part may draw once in the landscape
pass and once again in the post-clear interior-cell pass; acdream re-arms the
classifier at that exact walk boundary. Never replace the per-part stamp with
a whole-projection drawn-once gate: at a stair portal the torso may pass in
the first cell while head/hair fails and must retry through another crossed
cell. One binding exception is the local player: retail
RenderDeviceD3D::DrawMeshInternal bypasses Get/SetDrawnThisFrame when
CPhysicsPart::IsPartOfPlayerObj is true, repainting the player's parts at
every crossed-cell turn so later wall/depth ordering remains correct.
Retail's move-path product is the SAME add_shadows_to_cells call the
registration path uses, not a second independent flood:
CPhysicsObj::SetPositionInternal (0x00515330) takes the transition's own
cell_array on a successful move and calls
remove_shadows_from_cells/add_shadows_to_cells with it directly — the
calc_cross_cells_static cylsphere/bbox dispatch never re-runs on a move.
ShadowObjectRegistry.ReplacePositionRows mirrors this exactly:
PublishRetailProductFromExactCells publishes the retail render product from
the SAME exact cell list collision just republished into, replacing the
independent Contract A recompute an earlier chunk ran on every move; retail's
num_cells > 0 keep-when-empty gate covers both products identically, so a
move with no resolvable transition array leaves both untouched.
add_shadows_to_cells also recursively passes the root's same CELLARRAY to
every object in children (Contract B); acdream's attached projections
(equipped weapons/shields/ammunition) own no independent collision shapes, so
ShadowObjectRegistry.AttachChild/DetachChild publish PART ENTRIES only —
never a collision row — into every cell of the resolved root's current retail
CELLARRAY, re-publishing automatically whenever that array changes
(registration, move, staged apply, ReplaceMultiPartPayload).
EquippedChildRenderController.TryRealize calls AttachChild at the exact
moment an attached projection's WorldEntity is registered against its
accepted parent; CommitProjectionRemoval (the withdrawal/unparent/teardown
funnel) calls DetachChild. A nested attachment (a child of a child)
resolves to the ultimate root by walking the existing attach chain at the
registry, bounded and cycle-safe — WalkProductionWorldData never walks an
accepted-parent chain of its own; a child's part entries surface through the
SAME per-cell borrowed view (S2 chunk 5) every other dynamic record uses,
resolved to its own projection via RenderSceneQuery.TryGetByLocalEntityId,
identically to any other dynamic record.
Slice I3 prepared collision extension (2026-07-25). At its introduction,
the package remained format 1 and retained mesh type values 1–3; bake-tool 4
appended typed GfxObj,
Setup, CellStruct, and EnvCell-topology collision payloads. Core owns the
immutable flat records and deterministic raw-DAT flattener. Content owns the
strict little-endian codec and IPreparedCollisionSource.
PakPreparedAssetSource implements the render and collision interfaces over
one mmap; it does not create a second DAT reader or mapping. CellStruct
payloads alias only after exact serialized-byte comparison, while each
EnvCell topology remains independently keyed. Production traversal remained
on the parsed graph oracle until the later Slice-I cutover. Full-catalog evidence:
docs/research/2026-07-25-slice-i3-prepared-collision-package.md.
Slice I5 dual-publication seam (2026-07-25). Near-tier
LandblockBuild payloads now carry one immutable prepared-collision closure
outside the DAT lock. LandblockPhysicsPublisher installs the parsed oracle
and flat view under the same retained receipt; live objects use the strict
LiveCollisionAssetPublisher. Cell/topology ownership is landblock-scoped and
withdrawn on replacement, demotion, removal, and reset. The connected
graph-authoritative referee completed 14,064 exact samples with no mismatch or
fault. This is still one DatCollection and one prepared-package mmap, not a
second reader or a WorldBuilder runtime dependency. Evidence:
docs/research/2026-07-25-slice-i5-dual-collision-shadow.md.
Slice I6/I7 flat-authoritative closeout (2026-07-25). Production
PhysicsDataCache now publishes only immutable flat GfxObj, Setup,
CellStruct, and EnvCell-topology records from the validated package. Stable
world state strips temporary PhysicsDatBundle source material, and near
landblock builds discard raw Environment/GfxObj collision graphs once the
flat closure exists. Parsed graph constructors remain explicit
test/bake/equivalence oracles only; gameplay has no graph fallback or referee.
Both exact-binary connected routes report 0/0/0 retained parsed collision
graphs at every stable checkpoint while flat residency remains populated.
This remains one DatCollection, one preparation algorithm, and one package
mmap. Evidence:
docs/research/2026-07-25-slice-i7-closeout.md.
Prepared indoor-transit consumer correction (2026-07-26). The package
already retained exact portal planes through
FlatEnvCellTopology.PolygonIndex plus the aliased CellStruct portal-polygon
table. The production CellTransit consumer now reads that prepared
relationship directly; it no longer treats the intentionally absent parsed
CellPhysics.PortalPolygons dictionary as “this cell has no portals.” No
package schema, bake, DAT reader, collision formula, or render portal graph
changed. Evidence:
docs/research/2026-07-26-prepared-indoor-transit-regression.md.
Cell availability semantics (2026-07-31, corrected after full-catalog
audit). Raw and prepared CellStruct publication retains a CellPhysics
record when the physics root is empty but requires a valid containment root.
The installed 729,888-record raw and prepared catalogs contain zero rootless
containment payloads. A malformed null/-1 root is quarantined atomically; the
recursive inside base case applies only to a missing positive child below a
valid root. Registration-side outdoor floods still add outside cells but skip
transit when the active CLandCell is unavailable, and every later outdoor
candidate independently requires its own visible landcell before building
transit. The existing reflood retries after terrain/cell hydration. Both raw
and prepared point-in-cell paths preserve retail's zero-portals guard. No
package schema or DAT reader changed.
Evidence: docs/research/2026-07-31-cell-availability-semantics.md.
Retail VFX hook compatibility seam (2026-07-14). Chorizite.DatReaderWriter
2.1.7 models CreateBlockingParticleHook as the common hook header only, while
retail inherits the complete CreateParticleHook payload. The narrow readers in
src/AcDream.Content/Vfx/ read raw bytes through the existing DatCollection
database, delegate every ordinary hook to the package, and substitute only the
retail blocking-particle shape. Both live animation playback and PhysicsScript
loading use those cached readers; MeshExtractor uses the same PhysicsScript
loader when preloading emitter meshes. This is not a second DAT reader:
DatCollection remains the sole database owner and access path. Retail anchors:
CreateBlockingParticleHook::Execute 0x00526EF0 and
ParticleManager::CreateBlockingParticleEmitter 0x0051B8A0.
Retail particle visibility/degradation seam (2026-07-17). WorldBuilder's
particle simulator remains a useful DAT-integrator and batching reference, but
it does not carry the live retail client's CObjCell::IsInView degradation
path. EmitterDescRegistry now resolves the hardware particle GfxObj and its
ordered GfxObjDegradeInfo entries through the same sole DatCollection;
ParticleSystem ports the retail finite/infinite degraded branches; App feeds
the unified PView interior set plus the landscape renderer's independently
computed outdoor landcell set through a focused
ParticleVisibilityController. Examination and dedicated-pass emitters carry
an explicit bypass policy; missing/portal world views are empty rather than
fail-open.
No WorldBuilder dependency or second DAT access layer was introduced. Retail
anchors: CPhysicsPart::GetMaxDegradeDistance 0x0050D510,
GfxObjDegradeInfo::get_max_degrade_distance 0x0051E2D0,
CPhysicsObj::ShouldDrawParticles 0x0050FE60, and
ParticleEmitter::UpdateParticles 0x0051D180.
Hardwareless ParticleEmitterInfo records are also retained exactly. Retail
ParticleEmitter::SetInfo @ 0x0051CE90 returns false when
hw_gfxobj_id == INVALID_DID; it does not substitute the software GfxObj.
EmitterDescRegistry negative-caches that authored outcome and the hook sink
reports it once per DAT ID rather than once per owner. This is classification
and bounded diagnostics around the existing DatCollection, not a fallback
reader or invented VFX. Evidence:
docs/research/2026-07-26-retail-hardwareless-particle-emitter-diagnostics.md.
Retail shared world-alpha seam (2026-07-18). WorldBuilder's editor
renderers classify translucent mesh batches correctly, but they have no live
retail CPartCell/CShadowPart list and render particles in a separate
batcher. src/AcDream.App/Rendering/RetailAlphaQueue.cs is therefore an
acdream-owned runtime seam above the extracted mesh pipeline. During the main
world frame, WbDrawDispatcher and ParticleRenderer submit transparent
GfxObj subsets and scene particles into one stable far-to-near stream keyed by
the transformed DAT SortCenter; only adjacent compatible entries may batch.
Billboard particle textures are resident bindless sampler2DArray handles in
the per-instance vertex ABI, so different textures preserve that sorted order
inside one instanced draw; only a DAT blend-mode boundary splits the run. This
keeps dense particle fields from becoming one Vulkan draw per alternating
texture. WalkFrameDriver drains the landscape and world scopes at the exact
alpha barriers emitted by RetailFrameWalk; the frame orchestrator reaches
private viewports/UI only after the world stream completes.
Sky and sealed off-screen render targets remain independent. No DAT reader,
mesh decoder, or second scene graph was introduced. Retail anchors:
CPhysicsPart::UpdateViewerDistance 0x0050E030,
RenderDeviceD3D::DrawObjCellForDummies 0x005A0760,
CShadowPart::insertion_sort 0x006B5130,
D3DPolyRender::AddMeshToAlphaList 0x0059C230, and
D3DPolyRender::FlushAlphaList 0x0059D2E0. AP-34 is RETIRED (S4-c2 fix
round 1, 2026-09-04) — retail's own two-list FIFO alpha queue now ports the
per-cell-order mechanism directly; the EnvCell-shell granularity residual
continues as AP-238 (see the register). WorldBuilder does not
choose visible cells, portal slices, or cross-cell order: it receives the
walk's ordered records and GPU clip slots and performs asset preparation plus
order-preserving Vulkan batching only.
Retail portal-space viewport adapter (2026-07-15).
src/AcDream.App/Rendering/PortalTunnelPresentation.cs uses the extracted
Setup/GfxObj mesh pipeline and mandatory WbDrawDispatcher for retail's
synthetic CreatureMode tunnel object. The adapter does not duplicate mesh or
DAT decoding: it resolves the two client-enum assets through DatCollection,
uses the shared RetailAnimationLoader, registers Setup part refs through
WbMeshAdapter, and submits the animated SetupMesh through the existing
dispatcher. It clears world clip routing and point lights for the private scene
before installing retail's distant light. The lifecycle, camera, animation,
and draw ordering are acdream-owned ports of gmSmartBoxUI; WorldBuilder never
implemented this UI viewport.
Portal destination render-readiness seam (2026-07-16).
GpuWorldState.IsRenderReady does not treat dictionary publication as a draw
barrier. LandblockSpawnAdapter retains the complete required-id set for each
landblock: atlas-tier GfxObjs plus the synthetic geometry ids prepared by the
independent EnvCell shell pipeline. WbMeshAdapter.IsRenderDataReady opens the
gate only after ObjectMeshManager has real GPU render data. Its bounded CPU
cache retains texture payloads and stages a missing GPU object through a
deduplicated upload queue on cache hit only when the exact renderer owner is
still live, covering eviction and revisit churn without letting an unowned
cache hit recreate stale staged work. Reacquiring an exact owner stages once.
GPU upload is deliberately not an ownership acquire: atlas GfxObjs use their
landblock/entity pins, while synthetic EnvCell geometry uses a no-generic-decode
pin balanced from each landblock snapshot. A late upload after all owners have
released enters the evictable LRU instead of resurrecting a reference. After
publication pins the synthetic ids, the controller replays their immutable
environment/cell-structure/surface preparation descriptors; if a formerly
unowned mesh was evicted between worker scheduling and publication, this
schema-aware replay re-stages it without a generic GfxObj lookup.
Near-to-Far demotion is a separate App transaction: it releases EnvCell
rendering and landblock mesh pins while retaining the terrain slot. Core's
matching physics demotion preserves the terrain surface but removes indoor
cells, portals, buildings, and static shadow registrations.
Cost-budgeted publication seam (2026-07-24). WorldBuilder's editor path
publishes a complete manager-owned scene; acdream's live streamer instead
advances prepared render, physics, static, building, EnvCell, and spatial
receipts under one typed frame meter. Stable cursor work may span frames, while
building/EnvCell replacement and the final GpuWorldState spatial identity
swap remain observer-atomic. Destination live-object render ownership prepares
while the world is quiesced. GpuWorldState.IsLiveEntityProjectionResident
answers that spatial ownership question; availability-gated drawing,
collision, picking, radar/status targeting, effects, and audio continue to use
IsLiveEntityVisible. This is an acdream async-integration seam around the
extracted WB pipeline, not a second mesh or DAT implementation. Connected
evidence:
docs/research/2026-07-24-slice-e-cost-budgeted-streaming-report.md.
Bounded residency and GPU retirement seam (2026-07-18). Runtime DAT access
keeps raw file payload caching disabled and layers bounded typed-object and
decoded-pixel LRUs above the single DatCollection. ObjectMeshManager, the
standalone bindless texture cache, and the owner-scoped composite texture-array
cache all distinguish an active owner from an evictable unowned entry. Appearance
changes and landblock demotion acquire-before-publish and withdraw-before-release;
rebucketing never creates a second owner. GlobalMeshBuffer uses reclaimable,
coalescing vertex/index ranges and migrates incrementally within explicit physical
ceilings. Texture layers, terrain slots, mesh ranges, and old backing stores are
returned only after GpuFrameFlightController observes the frame fence that can
no longer reference them. This lifetime machinery is acdream-owned integration
around the extracted WB mesh pipeline; it does not add a second DAT decoder or a
reduced-distance rendering path.
Unified residency policy seam (2026-07-24). The extracted WB caches remain
the physical owners of mesh, arena, atlas, and texture resources.
AcDream.App.Rendering.Residency.ResidencyManager adds an acdream-owned typed
policy/diagnostic layer over them: generation-safe asset handles, independent
owner tokens and leases, immutable startup budgets, aggregate accounting, and
bounded trim requests. It never stores or deletes a GL name. Existing owners
perform logical eviction and fence-delayed physical release on the render
thread. The same ledger observes the prepared-package mapping, prepared/staged
CPU mesh data, decoded animation/audio data, and retained shared-alpha scratch
without double-counting the package's clean memory-mapped pages as committed
heap. Deterministic pressure tests exceed every configured ceiling and prove
zero-charge teardown. Connected evidence:
docs/research/2026-07-24-slice-d-unified-residency-report.md.
OH2/S1 exact CellStruct surface-index construction (2026-09-02). Bake
recipe 8 replaces MeshExtractor.PrepareCellStructMeshData's NoPos-based
approximation with retail's exact D3DPolyRender::ConstructMesh (0x0059DFA0)
side/candidate table: side candidates come only from CPolygon::sides_type
(AcDream.Core.Meshing.CellStructSideCandidates); NoPos/NoNeg mean "this
side's UV-index array is absent" only (CPolygon::UnPack 0x00538650), never
face suppression. The subset/material owner is the source surface-array
index, not the resolved Surface DID or texture format:
TextureBatchData.SourceSurfaceIndex/RetailSurfaceMask/RawSurfaceType/
IsCellShell carry that fact through the prepared package, and
CellSurfaceSubsets.InAscendingSurfaceOrder recovers the retail subset walk
order independent of the (Width,Height,Format) storage grouping the package
still uses for atlas dedup. Built-EnvCell draw admission is
(Surface.Type & (BASE1_IMAGE|BASE1_CLIPMAP)) != 0
(RenderDeviceD3D::DrawEnvCell 0x0059F170 →
D3DPolyRender::DrawMesh(..., arg4=1) 0x0059D4A0), applied after surface
resolution via RetailUntexturedSurfacePolicy.IsUntextured, not approximated
before it. CellMesh.Build's render-production role is retired; the
streaming build job now calls CellMesh.HasDrawableGeometry — the identical
exact admission rule, evaluated as a predicate only, with no mesh retained —
to decide whether a CellStruct contributes drawable geometry. Fixed retail
D3DCULL_CW (RenderMeshSubset 0x0059CA10) remains the raster cull for
every constructed cell-shell subset after fan expansion; the authored
sides_type is not read as GPU cull state. Retires AP-234. Contract:
docs/research/2026-09-01-overhaul/oh2-cellstruct-surface-contract.md.
Workflow: Before re-implementing any AC-specific rendering or dat-handling
algorithm, check this inventory first. If we already extracted it (🟢
sections), it's in src/AcDream.App/Rendering/Wb/ — use our copy. If WB has
it but we haven't extracted it yet, grep references/WorldBuilder/ and extract
as needed. Retail decomp remains the oracle for things WB never had (🔴 list).
Attribution: WorldBuilder is MIT-licensed. NOTICE.md includes WB attribution.
Read-reference layout (under references/WorldBuilder/, not project-referenced)
Chorizite.OpenGLSDLBackend/— full OpenGL renderer (Silk.NET). The components we use are extracted intosrc/AcDream.App/Rendering/Wb/.WorldBuilder.Shared/— data models, dat parsers, landscape module. The helpers we use are extracted intosrc/AcDream.Core/Rendering/Wb/.WorldBuilder/— Avalonia desktop app shell (not taken).WorldBuilder.{Windows,Linux,Mac}/— platform entry points (not taken).WorldBuilder.Server/— collab editing backend (not taken).Tests/+WorldBuilder.Shared.Benchmarks/— test harness (study only).
Upstream NuGet dependencies (these stay as NuGet packages, we don't vendor them):
| Package | Version | Purpose |
|---|---|---|
Chorizite.Core |
0.0.18 | Plugin framework — contains Chorizite.Core.Lib.BoundingBox, Chorizite.Core.Render.* interfaces used by every render manager |
Chorizite.DatReaderWriter |
2.1.x | dat parsing (we already use 2.1.7) |
Chorizite.DatReaderWriter.Extensions |
1.1.x | extra dat helpers |
BCnEncoder.Net |
2.2.x | DXT decode (we already use) |
SixLabors.ImageSharp |
3.1.x | image loading |
Silk.NET.OpenGL + Silk.NET.SDL |
2.23.x | GL + windowing (we use Silk's own windowing, they use SDL) |
MP3Sharp |
1.0.5 | MP3 decode |
🟢 RENDERING — take wholesale or adapt
These are what makes WB "perfect". Anything in this section, we should use from WB rather than re-implement.
Terrain
| Component | What it does |
|---|---|
TerrainRenderManager |
Full pipeline (per-chunk GPU buffers, draw orchestration) |
LandSurfaceManager |
Texture blending atlas (palCode, alpha masks, road overlays) |
TerrainGeometryGenerator |
Heightmap → mesh, normals, OnRoad, GetHeight, GetNormal |
TerrainChunk |
16×16 landblock chunk geometry |
TextureAtlasManager |
Texture atlas builder |
VertexLandscape |
Terrain vertex format |
Modern terrain adapter: acdream's bindless path uses TerrainAtlas plus
TerrainModernRenderer rather than WB's draw manager, but retains
LandSurfaceManager's layer-indexed TerrainTex.TexTiling contract. The
36-entry table is uploaded to terrain_modern.frag; base, overlay, and road
layers each use their owning repeat count while alpha masks stay at cell scale.
Retail TexMerge::CopyAndTile (0x00503580) and TexMerge::Merge
(0x005038C0) are the behavior oracle; see
docs/research/2026-07-13-retail-terrain-texture-tiling-pseudocode.md.
Scenery (procedural placement: trees, bushes, rocks, fences)
| Component | What it does |
|---|---|
SceneryRenderManager |
Generate + render per-vertex scenery |
SceneryHelpers |
Displace / RotateObj / ScaleObj / ObjAlign / CheckSlope |
SceneryInstance |
Per-spawn instance data |
acdream's streamed projection assigns generated instances local runtime IDs
through AcDream.Core.World.ProceduralSceneryIdAllocator. The namespace is
0x8XXYYIII (full X/Y bytes plus a 12-bit counter); bit 31 remains the stable
scenery classification seam. This is projection identity, not placement
behavior. The former 8-bit counter rejected dense retail-DAT landblocks before
their render transaction could publish (#218).
Static objects (buildings, slabs, props — Setup + GfxObj + ObjDesc)
| Component | What it does |
|---|---|
StaticObjectRenderManager |
Master pipeline for static objects |
ObjectRenderManagerBase + BaseObjectRenderManager |
Common render base |
ObjectMeshManager |
Mesh extraction from Setup/GfxObj, ObjDesc application |
Dungeons / interiors
| Component | What it does |
|---|---|
EnvCellRenderManager |
Dungeon interior cell geometry |
PortalRenderManager |
Portal traversal / visibility |
Sky + atmosphere
| Component | What it does |
|---|---|
SkyboxRenderManager |
Skybox rendering |
ParticleEmitterRenderer + ParticleBatcher + ActiveParticleEmitter |
Particle systems (sky particles, weather, magic) |
Visibility / culling
| Component | What it does |
|---|---|
VisibilityManager + VisibilitySnapshot |
Frustum + cell visibility |
Frustum |
Frustum-cull math |
Other rendering helpers
| Component | What it does |
|---|---|
MinimapRenderer |
Top-down minimap |
GlobalMeshBuffer |
Shared GPU mesh buffer; one reclaimable vertex/index allocation per mesh, released by ObjectMeshManager eviction |
GpuResourceManager |
GPU resource lifecycle |
InstanceData |
Instanced draw data |
TextureHelpers |
INDEX16, P8, BGRA, DXT decode + alpha (canonical port) |
DebugRenderer + DebugRendererLineDrawer + EdgeLineBuilder |
Debug primitives |
Shaders (22 total)
Located at Chorizite.OpenGLSDLBackend/Shaders/:
Landscape.{vert,frag} · StaticObject.{vert,frag} · StaticObjectModern.{vert,frag} · Particle.{vert,frag} · PortalStencil.{vert,frag} · Outline.{vert,frag} · Simple3D.{vert,frag} · InstancedLine.{vert,frag} · Text.{vert,frag} · UI.{vert,frag} · Gizmo.{vert,frag} (editor-only)
🟢 LOW-LEVEL GL / FRAMEWORK — take or replace with our own
Either take WB's wrappers wholesale, or keep our own and adapt the render managers to use ours. These wrappers are stateless or near-stateless and are the easiest to swap.
| Component | What it does |
|---|---|
OpenGLGraphicsDevice |
Silk.NET.OpenGL wrapper |
OpenGLRenderer |
Render orchestration |
GLSLShader |
Shader compile/link/uniforms |
GLHelpers + GLStateScope |
GL state utility |
ManagedGLFrameBuffer / ManagedGLIndexBuffer / ManagedGLTexture / ManagedGLTextureArray / ManagedGLUniformBuffer / ManagedGLVertexArray / ManagedGLVertexBuffer |
GL resource wrappers |
TextureParameters |
Sampler config |
GpuMemoryTracker |
Memory tracking |
Camera2D / Camera3D / CameraBase / ICamera / CameraController |
Camera primitives |
GameScene + SingleObjectScene + SceneData + ModernRenderData + RenderPass |
Scene / pass structures |
🟢 GEOMETRY / MATH UTILS — take wholesale
| Component | File |
|---|---|
TerrainUtils (OnRoad, GetNormal, GetHeight, GetRoad, palCode) |
WorldBuilder.Shared/Modules/Landscape/Lib/TerrainUtils.cs |
TerrainCacheManager |
…/Lib/TerrainCacheManager.cs |
TerrainRaycast |
…/Lib/TerrainRaycast.cs |
GeometryUtils |
WorldBuilder.Shared/Lib/GeometryUtils.cs |
RaycastingUtils (ray-vs-sphere/AABB/triangle) |
WorldBuilder.Shared/Lib/RaycastingUtils.cs |
DoubleNumerics (double-precision Vector/Matrix) |
WorldBuilder.Shared/Lib/DoubleNumerics.cs |
DatUtils |
WorldBuilder.Shared/Lib/DatUtils.cs |
BoundingBoxExtensions |
Chorizite.OpenGLSDLBackend/Lib/BoundingBoxExtensions.cs |
🟢 DATA MODELS — take selectively
| Component | What it does |
|---|---|
RegionInfo |
Landblock metadata wrapper (LandblockSizeInUnits, CellSizeInUnits, etc.) |
TerrainEntry |
Per-vertex terrain (Type/Scenery/Road/Height) |
MergedLandblock |
Merged dat data |
CellSplitDirection |
SW-NE vs NE-SW |
Cell |
Generic cell wrapper |
ObjectId |
Object identifier |
Position |
World position |
ACEnums |
AC-specific enums |
WbBuildingPortal / WbCellPortal |
Portal structures |
BuildingObject |
Building data |
🟡 EDITOR-ONLY — leave behind / delete in fork
These exist for the editor experience and have no place in a game client. Delete in fork.
Modules/Landscape/Tools/*—BrushTool,BucketFillTool,RoadLineTool,RoadVertexTool,InspectorTool,ObjectManipulationTool,Gizmo*(DragHandler, HitTester, Renderer, State),TexturePainting*,SceneRaycaster,LandscapeBrush,LandscapeToolBase,LandscapeToolContext,IToolSettingsProvider,ILandscapeBrush,ILandscapeEditorService,ILandscapeRaycastService,ILandscapeTool,ITexturePaintingToolModules/Landscape/Commands/*— undo/redo command pattern for editor (Add/Delete/Move/Rename/Reorder/etc.)LandscapeDocument+LandscapeLayer+LandscapeLayerGroup+LandscapeChunk+LandscapeLayerChunk+LandscapeLayerBase— editor document modelModules/Landscape/Models/TerrainPatch*+LandblockChangedEventArgs— editor mutation eventsModules/Landscape/Services/ILandscapeCacheService+ILandscapeDataProvider+ILandscapeObjectService+ impls — editor data flow- All
Migrations/*— SQLite schema migrations (project file format) Repositories/*+Services/*— project storage, dat repository, AceDb, SignalR sync, document manager, undo stack, world coordinates, keyword DB, project migration, semantic kernel AI helpersHubs/*— collaborative editing via SignalRStaticObject(editor model) — replace with our own scene-state data model fed from networkBackendGizmoDrawer+GizmoRenderer— editor gizmosProjectStructures, IProject, Project— editor project filesKeyBinding— editor input bindingViewportInputEvent[Extensions]— editor viewport inputEditorState— editor state container
🟡 AUDIO / FONT — we already have alternatives
Keep ours; don't take theirs.
AudioPlaybackEngine— uses MP3Sharp. We have OpenAL.FontRenderer— uses ImageSharp. We have BitmapFont/StbTrueTypeSharp + ImGui.
🔴 NOT IN WORLDBUILDER — port from retail decomp ourselves
WorldBuilder is a dat editor; it does not have:
- Network protocol — UDP framing, ISAAC, packet codec, ACE message
layer (we have this; oracle is
references/holtburger) - Physics — collision (CPhysicsObj transitions, BSP queries, sphere
sweeps), step-up, walkable validation (we have partial; oracle is the
retail decomp at
docs/research/named-retail/) - Animation — motion sequencer, cycle/non-cycle parts, animation frame interpolation (we have this; oracle is retail decomp)
- Movement — local player WASD → MoveToState wire, remote-entity
motion via UpdateMotion + dead-reckoning (we have this; oracle is
references/holtburger+ retail decomp) - Game UI — chat, vitals, inventory, spell book, allegiance, options (we have this; ImGui-based today, custom-toolkit later)
- Plugin API —
IGameState,IEvents,IActions,IPacketPipeline,IOverlay(we have this — acdream-unique) - Game events — combat, allegiance, spell casting, quest events (we have this; oracle is ACE for opcodes + retail for client behavior)
- Audio — OpenAL pipeline, sound triggers (we have this)
- TurbineChat + slash commands (we have this)
- Login + character selection flow (we have this)
- World-object mouse selection — WorldBuilder supplies mesh/DAT access but
no retail client picker. Our narrow
RetailSelectionGeometryCachereusesDatCollectionto expose each GfxObj drawing-BSP root sphere and visual polygons;WbDrawDispatchersupplies the normal draw's current part transforms to the named-retail selection accumulator.
What this means for the workflow (post-Phase O)
The CLAUDE.md "grep named → decompile → verify → port" workflow stays the rule for everything in the 🔴 list (network, physics, animation, movement, UI, plugin, audio, chat).
For anything in 🟢 that we've already extracted: the code is in our
tree at src/AcDream.{Core,App}/Rendering/Wb/. Read it there — don't
grep references/WorldBuilder/ unless you want to compare against the
original. Re-porting from retail decomp when we already have a tested
port is still how we'd get the scenery edge-vertex bug back.
For anything in 🟢 that we have NOT yet extracted: grep
references/WorldBuilder/ to find the source, then extract it using the
Phase O pattern (verbatim copy → adapt constructor to accept
IDatCollection via DatCollectionAdapter where needed → add to
src/AcDream.App/Rendering/Wb/). Do NOT add a new project reference back
to WorldBuilder.Shared or Chorizite.OpenGLSDLBackend — Phase O
permanently removed those.
When we discover a behavior mismatch with retail (rare — the extracted code is the same as the original), the resolution is: reconcile extracted code ↔ retail decomp ↔ holtburger ↔ ACE ↔ ACViewer (the existing reference hierarchy in CLAUDE.md). Our extracted code ranks at the top of that hierarchy for anything 🟢.