fix(content): S1 review round - retail default sides shape, mask hoist, upload order

Campaign OVERHAUL S1 review fixes (two Opus lens reviews, findings verified
by the lead against the decomp):

- a raw sides_type outside 0/1/2 constructs retail's default single-side
  shape (ConstructMesh @0x0059DFA0 loop bounds default to 1) instead of
  dropping the polygon; still counted as a data anomaly (corpus has none);
- the positive-surface stippling mask OR runs once per polygon before the
  degenerate-fan guard, as retail's count loop does (pseudo-C 426859-426866);
- untextured slots keep their mask accounting but bake no texture and no
  vertices (contract §9 item 3); the dev pak shrinks by 114 KB;
- failed surface-override / Surface / texture-dependency lookups are
  attempted and logged once per slot, not once per candidate;
- cell-shell batches upload in ascending source surface index, retail's
  built-EnvCell subset draw order (ConstructMesh attribute-range scan,
  DrawMesh @0x0059D4A0); ordinary GfxObj meshes keep storage order;
- CellMesh.HasDrawableGeometry documented as the admission rule without
  texture-dependency resolution (a conservative superset of emission);
- the stippling/surface equivalence sweep's cell half is pinned at zero
  again; InAscendingSurfaceOrder is marked bake/upload/test-only;
- plan §5: reviewer findings are verified by the lead, one skeptic at most
  for a blocking finding, never more than five agents per step.

Three new Content tests pin the mask hoist, the vertex-free untextured
slot, and the single-side fallback. Content 213/213, Core Meshing and
Conformance green, App hermetic 6,757/6,757, Release build 0/0.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-09-02 19:37:25 +02:00
parent 0840d5fb77
commit e2543d0ef0
10 changed files with 265 additions and 73 deletions

View file

@ -2041,6 +2041,34 @@ namespace AcDream.App.Rendering.Wb
#region Private: GPU Upload
/// <summary>
/// Upload-time batch order for one prepared mesh. Ordinary GfxObj
/// meshes keep the (Width,Height,Format) storage grouping. A cell-shell
/// mesh (any batch with <see cref="TextureBatchData.IsCellShell"/>) is
/// ordered by ascending <see cref="TextureBatchData.SourceSurfaceIndex"/>,
/// retail's built-EnvCell subset draw order (contract §3.6:
/// <c>D3DPolyRender::ConstructMesh</c> @0x0059DFA0 emits one attribute
/// range per nonempty surface index ascending, and
/// <c>DrawMesh</c> @0x0059D4A0 draws them in that order). Runs once per
/// mesh upload; the allocation is not per-frame.
/// </summary>
private static IEnumerable<((int Width, int Height, TextureFormat Format) Format, TextureBatchData Batch)> OrderedUploadBatches(ObjectMeshData meshData)
{
var pairs = new List<((int Width, int Height, TextureFormat Format), TextureBatchData)>();
bool cellShell = false;
foreach (var (format, batches) in meshData.TextureBatches)
{
foreach (var batch in batches)
{
pairs.Add((format, batch));
cellShell |= batch.IsCellShell;
}
}
if (cellShell)
return pairs.OrderBy(p => p.Item2.SourceSurfaceIndex); // stable: OrderBy preserves storage order on ties
return pairs;
}
private ObjectRenderData? UploadGfxObjMeshData(ObjectMeshData meshData)
{
if (meshData.Vertices.Length == 0) return null;
@ -2111,20 +2139,18 @@ namespace AcDream.App.Rendering.Wb
}
// OH2/S1 (docs/research/2026-09-01-overhaul/oh2-cellstruct-surface-contract.md
// §3.6 point 5): for a cell-shell mesh (batch.IsCellShell true),
// this dictionary walk groups by (Width,Height,Format) for
// atlas/storage packing only — it is NOT retail's ascending
// source-surface-index subset draw order. The resulting
// `renderBatches` list order is what EnvCellRenderer later
// replays as draw order. Recovering the exact retail order
// requires walking AcDream.Content.CellSurfaceSubsets.
// InAscendingSurfaceOrder(meshData) instead; no consumer does
// that yet (OH7's ordered draw stream is the intended owner) so
// this upload path still does not preserve retail's subset
// order for cell shells.
foreach (var (format, batches) in meshData.TextureBatches)
// §3.6): TextureBatches groups by (Width,Height,Format) for
// atlas/storage packing only. For a cell-shell mesh the
// `renderBatches` list order is what EnvCellRenderer replays
// as subset draw order, and retail draws built-EnvCell
// subsets in ascending source surface-array index
// (ConstructMesh @0x0059DFA0 attribute-range scan, DrawMesh
// @0x0059D4A0 ascending subset loop). So cell-shell batches
// are uploaded in that order; ordinary GfxObj meshes keep the
// storage order. This runs once per mesh at upload time, not
// per frame.
foreach (var (format, batch) in OrderedUploadBatches(meshData))
{
foreach (var batch in batches)
{
if (batch.Indices.Count == 0) continue;