feat(core): S1 one CellStruct interpretation, installed-DAT scan, retire AP-234

Campaign OVERHAUL S1 chunk B.

- CellMesh.Build's render role is deleted; CellMesh.HasDrawableGeometry
  is the exact predicate (any sides_type candidate whose resolved surface
  passes the built-EnvCell (Surface.Type & 6) != 0 test), and the
  streaming build job consumes it. Core and Content no longer carry two
  CellStruct interpretations.
- Installed-DAT scan over the complete Environment corpus (5,346
  landblocks, 729,888 cells, 8,601,560 polygons, 8,608,746 candidates):
  3,197 old-rule admissions were untextured surfaces the NoPos proxy let
  through; zero new-only; zero unexplained; zero unknown sides values;
  counts pinned as goldens against the contract's DAT hashes.
- Canonical pins: 0xF4180104 has eight ST_DOUBLE clip-map polygons and 44
  drawable side calls; the canonical NoPos surfaces are type 0x11 and are
  constructed but skipped; two independent extractions of all ten
  canonical cells hash identically.
- AP-234 retired from the divergence register (161 active rows) with
  ConstructMesh 0x0059DFA0 / DrawEnvCell 0x0059F170 / DrawMesh 0x0059D4A0.
- StipplingSurfaceEquivalenceTests no longer claims a build-time NoPos
  skip; the cell half is report-only, the building half stays pinned.
- Inventory and policy remarks updated; plan ledger records chunk A and
  the capture kit.

Core Meshing+Conformance 95/95, Content 208/208 (Lane!=Timing,
Lane!=PreparedPackage), App EnvCell/LandblockBuild/Streaming 432/432,
Release solution build 0 warnings / 0 errors.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-09-02 19:11:17 +02:00
parent acf172469e
commit b681717c30
9 changed files with 631 additions and 154 deletions

View file

@ -14,19 +14,53 @@ namespace AcDream.Core.Tests.Conformance;
/// Retail suppresses portal-fill drawing at DRAW time via the skipNoTexture
/// rule: building/cell surface batches whose CSurface.type lacks BASE1_IMAGE
/// (0x2) and BASE1_CLIPMAP (0x4) are skipped (D3DPolyRender inner draw,
/// Ghidra 0x0059d4a0; default on @0x00820e30). acdream suppresses them at
/// BUILD time via Stippling.NoPos in all four extraction paths
/// (MeshExtractor.PrepareGfxObjMeshData + PrepareCellStructMeshData
/// [moved from ObjectMeshManager in MP1a], CellMesh.Build:44, GfxObjMesh.Build:71).
/// Ghidra 0x0059d4a0; default on @0x00820e30).
///
/// These criteria are equivalent ONLY if NoPos ⇔ untextured-surface holds on
/// the content. This sweep pins both directions across the populated
/// Holtburg-area landblocks (building shell models + every Environment
/// CellStruct their cells reference + the door setup parts):
/// (a) every NoPos poly's positive surface is untextured (else our skip
/// drops something retail draws), and
/// (b) every untextured-surface poly is NoPos (else we draw something
/// retail skips on building/cell passes — the would-be phantom class).
/// <b>STALE PREMISE, CORRECTED 2026-09-02 (OH2/S1 chunk B).</b> This test's
/// original premise was that acdream suppresses the same geometry at BUILD
/// time via <c>Stippling.NoPos</c>, in all four extraction paths, and that
/// the NoPos/untextured correlation below is why that build-time shortcut is
/// safe. That premise is no longer true for ANY of the four paths:
/// - <c>MeshExtractor.PrepareGfxObjMeshData</c> and <c>GfxObjMesh.Build</c>
/// stopped using NoPos as an emission gate at #426 (2026-08-23) — they
/// always emit a polygon's positive side regardless of NoPos, using
/// <c>RetailUntexturedSurfacePolicy.IsUntextured(Surface.Type)</c> only
/// to pick solid-color vs. textured fill, never to skip a face.
/// - <c>MeshExtractor.PrepareCellStructMeshData</c> and the former
/// <c>CellMesh.Build</c> (now <c>CellMesh.HasDrawableGeometry</c>, a
/// predicate with no render-production role) stopped using NoPos at
/// OH2/S1: side candidates come only from <c>CPolygon::sides_type</c>
/// (contract §3.4), and built-EnvCell subset admission is the exact
/// <c>(Surface.Type &amp; 6) != 0</c> test applied AFTER surface
/// resolution (contract §4) — see
/// docs/research/2026-09-01-overhaul/oh2-cellstruct-surface-contract.md
/// and the retired AP-234 register row.
///
/// The NoPos ⇔ untextured-surface correlation this sweep measures is
/// therefore a fact ABOUT THE INSTALLED CONTENT, not a description of how
/// acdream currently decides what to draw. It remains worth pinning as a
/// content-shape sanity signal (a violation would mean an author left NoPos
/// set on a real texture, or textured a "NoPos" aperture fill — either is
/// surprising authored content worth knowing about), so the building-model
/// half below stays a LOAD-BEARING assertion: it is still an exact,
/// evaluable fact about the DAT content (poly.PosSurface resolves directly
/// off the GfxObj's own surface array, no EnvCell instance needed). The
/// CellStruct half is downgraded to a REPORT ONLY (see
/// <see cref="CellPortalPolyMismatches"/> below) — it was already a narrower
/// proxy check (NoPos-poly-is-a-portal-poly, not a real per-EnvCell surface
/// resolution — a CellStruct alone cannot resolve a surface without a
/// specific EnvCell's override array), and its original justification (are
/// we skipping only aperture fills, never wall geometry?) no longer applies
/// now that CellStruct extraction does not use NoPos to decide any of that.
///
/// This sweep pins both directions across the populated Holtburg-area
/// landblocks (building shell models + every Environment CellStruct their
/// cells reference + the door setup parts):
/// (a) every NoPos poly's positive surface is untextured (else the old
/// build-time-suppression approximation, when it existed, would have
/// dropped something retail draws), and
/// (b) every untextured-surface poly is NoPos (else the old approximation
/// would have drawn something retail's build/cell passes skip).
/// Violations of (b) on PLAIN OBJECT GfxObjs are allowed — retail's bypass
/// draws solid batches for non-building/non-cell meshes, and so do we.
/// </summary>
@ -53,8 +87,13 @@ public sealed class StipplingSurfaceEquivalenceTests
using var dats = new DatCollection(datDir, DatAccessType.Read);
int polysChecked = 0;
var aViolations = new List<string>(); // NoPos but TEXTURED (our skip would drop a retail-drawn poly)
var bViolations = new List<string>(); // untextured but NOT NoPos (we'd draw what retail skips)
// Building-model (GfxObj) violations only — LOAD-BEARING (Assert.Empty
// below). See the class docstring: this direction remains a real,
// per-polygon-resolvable fact about the DAT content even though
// neither GfxObj extraction path (#426) nor either CellStruct path
// (OH2/S1) still uses NoPos as a build-time emission gate.
var aViolations = new List<string>(); // NoPos but TEXTURED
var bViolations = new List<string>(); // untextured but NOT NoPos
bool IsTextured(DatReaderWriter.DBObjs.Surface? s) =>
s is not null &&
@ -98,6 +137,19 @@ public sealed class StipplingSurfaceEquivalenceTests
}
// ---- cell structs referenced by those landblocks' interior cells ----
// REPORT ONLY (not asserted — see the class docstring). Direction (a)
// can't be evaluated without a specific EnvCell (a CellStruct alone
// has no surface array to resolve against), so this was always a
// narrower proxy: every NoPos poly must be a portal polygon
// (referenced by the struct's Portals list). Its original purpose —
// justifying a build-time NoPos-based skip that removes only
// aperture fills, never wall geometry — no longer applies, because
// neither MeshExtractor.PrepareCellStructMeshData nor
// CellMesh.HasDrawableGeometry uses NoPos to decide that any more
// (OH2/S1). Kept as a content-shape signal: a mismatch here would
// still be surprising authored content worth knowing about, but it
// no longer describes a bug in acdream's extraction.
var cellPortalPolyMismatches = new List<string>();
foreach (var envId in environments)
{
var env = dats.Get<DatReaderWriter.DBObjs.Environment>(envId);
@ -109,28 +161,23 @@ public sealed class StipplingSurfaceEquivalenceTests
var poly = kv.Value;
polysChecked++;
bool noPos = poly.Stippling.HasFlag(DatReaderWriter.Enums.StipplingType.NoPos);
// CellStruct polys resolve surfaces through the EnvCell's
// surface list at runtime; the struct itself stores only the
// index. Direction (a) can't be evaluated without a specific
// EnvCell, so for structs we pin only the NoPos→portal-poly
// correspondence: every NoPos poly must be a portal polygon
// (referenced by the struct's Portals list), i.e. our skip
// removes only aperture fills, never wall geometry.
if (noPos)
{
bool isPortalPoly = cs.Portals.Any(p => p == kv.Key);
if (!isPortalPoly)
aViolations.Add($"env 0x{envId:X8} struct {csId} poly {kv.Key}: NoPos but not a portal poly");
cellPortalPolyMismatches.Add($"env 0x{envId:X8} struct {csId} poly {kv.Key}: NoPos but not a portal poly");
}
}
}
}
_out.WriteLine($"checked {polysChecked} polys across {buildingModels.Count} building models + {environments.Count} environments");
_out.WriteLine($"(a) NoPos-but-textured (skip would drop retail-drawn): {aViolations.Count}");
_out.WriteLine($"(a) building NoPos-but-textured (load-bearing): {aViolations.Count}");
foreach (var v in aViolations.Take(20)) _out.WriteLine($" {v}");
_out.WriteLine($"(b) untextured-but-not-NoPos on buildings (we'd draw what retail skips): {bViolations.Count}");
_out.WriteLine($"(b) building untextured-but-not-NoPos (load-bearing): {bViolations.Count}");
foreach (var v in bViolations.Take(20)) _out.WriteLine($" {v}");
_out.WriteLine($"(c) cell NoPos-but-not-portal-poly (REPORT ONLY, not asserted): {cellPortalPolyMismatches.Count}");
foreach (var v in cellPortalPolyMismatches.Take(20)) _out.WriteLine($" {v}");
Assert.Empty(aViolations);
Assert.Empty(bViolations);