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>
110 lines
5.3 KiB
C#
110 lines
5.3 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using AcDream.Core.Content;
|
|
using DatReaderWriter.DBObjs;
|
|
using DatReaderWriter.Types;
|
|
|
|
namespace AcDream.Core.Meshing;
|
|
|
|
/// <summary>
|
|
/// OH2/S1 chunk B (docs/research/2026-09-01-overhaul/oh2-cellstruct-surface-contract.md
|
|
/// §9 item 5): retires this type's former render-production role. The old
|
|
/// <c>Build</c> method carried its own NoPos-based "skip untextured cell
|
|
/// geometry" approximation (AP-234, retired) — a second, divergent
|
|
/// interpretation of the retail CellStruct surface/subset construction
|
|
/// algorithm alongside <c>MeshExtractor.PrepareCellStructMeshData</c>, which
|
|
/// is now the ONE production interpretation (contract §9 item 5: "Core and
|
|
/// Content do not retain divergent CellStruct interpretations"). This class
|
|
/// now exposes only <see cref="HasDrawableGeometry"/>: the exact retail
|
|
/// predicate for whether a CellStruct contributes at least one drawable
|
|
/// subset, needed by the streaming build job
|
|
/// (<c>AcDream.App.Streaming.LandblockBuildFactory</c>) to decide whether a
|
|
/// cell's shell has drawable geometry before it registers the cell — a
|
|
/// question answerable without building or retaining any geometry.
|
|
/// </summary>
|
|
public static class CellMesh
|
|
{
|
|
/// <summary>
|
|
/// True iff at least one of this CellStruct's polygon construction
|
|
/// candidates (<see cref="CellStructSideCandidates.GetCandidates"/>,
|
|
/// ported from <c>D3DPolyRender::ConstructMesh</c> @0x0059DFA0, contract
|
|
/// §3.4) targets a surface slot whose resolved <c>Surface.Type</c> is
|
|
/// TEXTURED (<see cref="RetailUntexturedSurfacePolicy.IsUntextured"/>
|
|
/// is false) — retail's built-EnvCell draw admission
|
|
/// <c>(Surface.Type & (BASE1_IMAGE|BASE1_CLIPMAP)) != 0</c>
|
|
/// (<c>RenderDeviceD3D::DrawEnvCell</c> @0x0059F170 →
|
|
/// <c>D3DPolyRender::DrawMesh(..., arg4=1)</c> @0x0059D4A0, contract §4),
|
|
/// evaluated without resolving the surface's texture dependency chain.
|
|
/// It is therefore a conservative superset of what
|
|
/// <c>MeshExtractor.PrepareCellStructMeshData</c> actually emits: the
|
|
/// extractor additionally drops a textured slot whose SurfaceTexture,
|
|
/// RenderSurface, palette, or pixel format cannot be resolved (an
|
|
/// extractor-side quarantine outcome, logged there), which this predicate
|
|
/// still reports as drawable. Missing texture data is a DAT defect, not a
|
|
/// retail admission decision, so the predicate follows the admission rule.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Side candidates come only from <c>CPolygon::sides_type</c>
|
|
/// (contract §3.4); <c>NoPos</c>/<c>NoNeg</c> play no role in this
|
|
/// predicate — they mean "this side's UV-index array is absent"
|
|
/// (contract §2.3/§3.5), which cannot change whether a subset draws,
|
|
/// only what its texture coordinates are. This predicate therefore
|
|
/// never reads a polygon's UV-index arrays.
|
|
/// </remarks>
|
|
/// <param name="envCell">
|
|
/// The EnvCell whose ordered surface-override array
|
|
/// (<see cref="EnvCell.Surfaces"/>) resolves each candidate's source
|
|
/// surface-array slot to a qualified Surface DAT id, exactly like
|
|
/// <c>MeshExtractor.PrepareCellStructMeshData</c>'s
|
|
/// <c>surfaceOverrides</c> parameter.
|
|
/// </param>
|
|
/// <param name="cellStruct">The CellStruct containing the polygon geometry.</param>
|
|
/// <param name="dats">DAT object source used to resolve each candidate's surface <c>Surface.Type</c>.</param>
|
|
public static bool HasDrawableGeometry(EnvCell envCell, CellStruct cellStruct, IDatObjectSource dats)
|
|
{
|
|
// One resolve per distinct surface slot, not per polygon: a slot's
|
|
// textured-ness is a per-slot fact (contract §3.2's per-surface
|
|
// mask/type), and re-resolving the same Surface DAT record for
|
|
// every polygon that references it would be wasted DAT I/O on the
|
|
// streaming worker thread that calls this predicate.
|
|
var slotIsTextured = new Dictionary<int, bool>();
|
|
|
|
bool SlotIsTextured(int slot)
|
|
{
|
|
if (slotIsTextured.TryGetValue(slot, out bool cached))
|
|
return cached;
|
|
|
|
bool textured = false;
|
|
if (slot >= 0 && slot < envCell.Surfaces.Count)
|
|
{
|
|
uint surfaceId = 0x08000000u | envCell.Surfaces[slot];
|
|
if (dats.Get<Surface>(surfaceId) is { } surface)
|
|
textured = !RetailUntexturedSurfacePolicy.IsUntextured(surface.Type);
|
|
}
|
|
slotIsTextured[slot] = textured;
|
|
return textured;
|
|
}
|
|
|
|
foreach (var poly in cellStruct.Polygons.Values)
|
|
{
|
|
// Same degenerate-fan gate as MeshExtractor.PrepareCellStructMeshData.
|
|
if (poly.VertexIds.Count < 3) continue;
|
|
|
|
ReadOnlySpan<CellStructSideCandidate> candidates =
|
|
CellStructSideCandidates.GetCandidates((int)poly.SidesType);
|
|
|
|
foreach (var candidate in candidates)
|
|
{
|
|
short surfaceIdxRaw = candidate.SurfaceSlot == CellStructPolygonSurfaceSide.Positive
|
|
? poly.PosSurface
|
|
: poly.NegSurface;
|
|
if (surfaceIdxRaw < 0) continue;
|
|
|
|
if (SlotIsTextured(surfaceIdxRaw))
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|