BR-1: RESOLVED as already-equivalent - premise falsified by pre-check, equivalence pinned, #113 attribution corrected

The plan's BR-1 ('implement the skipNoTexture draw-time surface gate')
died on its pre-check: acdream ALREADY suppresses every portal fill.
ReplicateProductionEmission_OnPortalFills replicates the exact emission
conditions of the production extractors on the hall/cottage fills:
pos=False neg=False for every one (Stippling.NoPos skips the positive
side at ObjectMeshManager.PrepareGfxObjMeshData:1046,
PrepareCellStructMeshData:1394, CellMesh.Build:44, GfxObjMesh.Build:71;
the fills have no negative surface). There is nothing to gate.

What ships instead: StipplingSurfaceEquivalenceTests - 2,607 polys across
13 building models + 13 environments, ZERO violations both directions:
NoPos <=> untextured-surface. Our build-time skip is proven equivalent to
retail's draw-time skipNoTexture rule (Ghidra 0x0059d4a4, default on
@0x00820e30) on this content. The pin fails loudly if future content
breaks the invariant - the cue to implement the draw-time gate then.

Corrections folded into the plan + comparison docs:
- The #113 phantom residual CANNOT be GfxObj fills (they never reach a
  vertex buffer). Plausible true sites are cell-side: flood-admitted
  cells drawn with the pass-all NoClipSlice when slot-less
  (RetailPViewRenderer.cs:71), and/or cell statics drawn unclipped +
  un-viewcone'd (object-lists-skip-portal-view-gate, confirmed).
  BR-2 opens with the probe that pins which.
- The e46d3d9 user-gate observations (filter removed phantom/doors) were
  confounded - the filter was a provable mesh no-op on shells AND doors.
- Ledger rows solid-surface-skip-missing + the acdream half of
  portal-polys-baked-unconditional re-marked REFUTED-for-fills; the
  retail mechanism descriptions and the un-consumed PortalIndex->
  CBldPortal pairing (BR-4) stand.

Suites: Core 1398 green (1392 baseline + 6 new facts) + the 4 pre-existing
#99-era failures + 1 skip. No production code.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-11 06:25:31 +02:00
parent eb689ae73f
commit 695eca2c1f
4 changed files with 265 additions and 32 deletions

View file

@ -181,6 +181,52 @@ public sealed class Issue113DoorVanishDiagnosticTests
return n.LengthSquared() < 1e-10f ? Vector3.Zero : Vector3.Normalize(n);
}
/// <summary>
/// BR-1 pre-check: replicate ObjectMeshManager.PrepareGfxObjMeshData's
/// EXACT emission conditions (lines 1040-1058: pos side emitted unless
/// Stippling.NoPos; neg side if Negative/Both or (!NoNeg and SidesType ==
/// Clockwise)) on the portal-fill polys, printing raw enum values. The
/// #113 evidence says these fills RENDER (the phantom) — if the replica
/// says "skipped", the fills reach the screen another way and BR-1's gate
/// must move.
/// </summary>
[Fact]
public void ReplicateProductionEmission_OnPortalFills()
{
var datDir = ConformanceDats.ResolveDatDir();
if (datDir is null) { _out.WriteLine("dats unavailable — skipped"); return; }
using var dats = new DatCollection(datDir, DatAccessType.Read);
foreach (uint mid in new uint[] { 0x010014C3u, 0x01000827u })
{
var gfx = dats.Get<DatReaderWriter.DBObjs.GfxObj>(mid)!;
var walked = new HashSet<ushort>();
void Walk(DatReaderWriter.Types.DrawingBSPNode? n)
{
if (n is null) return;
if (n.Polygons is not null) foreach (var pid in n.Polygons) walked.Add((ushort)pid);
Walk(n.PosNode); Walk(n.NegNode);
}
Walk(gfx.DrawingBSP!.Root);
var fills = gfx.Polygons.Keys.Where(k => !walked.Contains(k)).OrderBy(k => k).ToList();
_out.WriteLine($"=== model 0x{mid:X8} ===");
foreach (var pid in fills)
{
var poly = gfx.Polygons[pid];
bool noPos = poly.Stippling.HasFlag(DatReaderWriter.Enums.StipplingType.NoPos);
bool hasNeg = poly.Stippling.HasFlag(DatReaderWriter.Enums.StipplingType.Negative)
|| poly.Stippling.HasFlag(DatReaderWriter.Enums.StipplingType.Both)
|| (!poly.Stippling.HasFlag(DatReaderWriter.Enums.StipplingType.NoNeg)
&& poly.SidesType == DatReaderWriter.Enums.CullMode.Clockwise);
_out.WriteLine(
$" poly {pid,3}: stip={poly.Stippling}(raw={(int)poly.Stippling}) sides={poly.SidesType}(raw={(int)poly.SidesType}) " +
$"posSurf={poly.PosSurface} negSurf={poly.NegSurface} " +
$"-> production emits: pos={!noPos} neg={hasNeg && poly.NegSurface >= 0}");
}
}
}
/// <summary>
/// Phase A confirmation: retail's building/cell mesh pass skips surface
/// batches whose CSurface type has neither Base1Image (0x2) nor

View file

@ -0,0 +1,137 @@
using System;
using System.Collections.Generic;
using System.Linq;
using DatReaderWriter;
using DatReaderWriter.Options;
using Xunit;
using Xunit.Abstractions;
namespace AcDream.Core.Tests.Conformance;
/// <summary>
/// BR-1 conformance pin (holistic building-render port, plan §BR-1).
///
/// 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
/// (ObjectMeshManager.PrepareGfxObjMeshData:1046 + PrepareCellStructMeshData
/// :1394, CellMesh.Build:44, GfxObjMesh.Build:71).
///
/// 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).
/// 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>
public sealed class StipplingSurfaceEquivalenceTests
{
private readonly ITestOutputHelper _out;
public StipplingSurfaceEquivalenceTests(ITestOutputHelper output) => _out = output;
private static readonly uint[] Landblocks =
{
0xA9B40000u, // Holtburg town
0xA9B30000u, // hill cottage block
0xAAB30000u, // meeting hall block
0xA9B50000u,
0xAAB40000u,
};
[Fact]
public void NoPosStippling_Equals_UntexturedSurface_OnBuildingsAndCells()
{
var datDir = ConformanceDats.ResolveDatDir();
if (datDir is null) { _out.WriteLine("dats unavailable — skipped"); return; }
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)
bool IsTextured(DatReaderWriter.DBObjs.Surface? s) =>
s is not null &&
(s.Type.HasFlag(DatReaderWriter.Enums.SurfaceType.Base1Image)
|| s.Type.HasFlag(DatReaderWriter.Enums.SurfaceType.Base1ClipMap));
// ---- building shell GfxObjs ----
var buildingModels = new SortedSet<uint>();
var environments = new SortedSet<uint>();
foreach (uint lb in Landblocks)
{
var lbi = dats.Get<DatReaderWriter.DBObjs.LandBlockInfo>(lb | 0xFFFEu);
if (lbi is null) continue;
foreach (var b in lbi.Buildings ?? new()) buildingModels.Add(b.ModelId);
for (uint low = 0x0100; low < 0x0100 + lbi.NumCells; low++)
{
var dc = dats.Get<DatReaderWriter.DBObjs.EnvCell>(lb | low);
if (dc is not null) environments.Add(0x0D000000u | dc.EnvironmentId);
}
}
Assert.NotEmpty(buildingModels);
Assert.NotEmpty(environments);
foreach (var mid in buildingModels)
{
var gfx = dats.Get<DatReaderWriter.DBObjs.GfxObj>(mid);
if (gfx is null) continue;
foreach (var kv in gfx.Polygons)
{
var poly = kv.Value;
polysChecked++;
bool noPos = poly.Stippling.HasFlag(DatReaderWriter.Enums.StipplingType.NoPos);
DatReaderWriter.DBObjs.Surface? surf = null;
if (poly.PosSurface >= 0 && poly.PosSurface < gfx.Surfaces.Count)
surf = dats.Get<DatReaderWriter.DBObjs.Surface>(gfx.Surfaces[poly.PosSurface]);
if (noPos && IsTextured(surf))
aViolations.Add($"gfx 0x{mid:X8} poly {kv.Key}: NoPos but textured surface");
if (!noPos && surf is not null && !IsTextured(surf))
bViolations.Add($"gfx 0x{mid:X8} poly {kv.Key}: textured-less surface without NoPos (type={surf.Type})");
}
}
// ---- cell structs referenced by those landblocks' interior cells ----
foreach (var envId in environments)
{
var env = dats.Get<DatReaderWriter.DBObjs.Environment>(envId);
if (env is null) continue;
foreach (var (csId, cs) in env.Cells)
{
foreach (var kv in cs.Polygons)
{
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");
}
}
}
}
_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}");
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}");
foreach (var v in bViolations.Take(20)) _out.WriteLine($" {v}");
Assert.Empty(aViolations);
Assert.Empty(bViolations);
}
}