Final allowed fix round for S4-c2 on cc8e5677a. This lands every item in the campaign packet section 12 without adding a flush site, shader, distance, overflow recovery draw, or graphical-client run. R2-1 particle row 5: ParticleRenderer now constructs and owns the actual particle-mesh-opaque pipeline using the existing particle_mesh shaders and layout, Blend=None, depth test/write enabled with WorldCompare, dynamic per-batch cull, clockwise front face, and no alpha-to-coverage. The production dispatch selects it for an opaque-classified mesh particle whose clamped material alpha is 1.0. Nonopaque mesh pipelines remain depth-write-off. The production route keeps cached reserve/immediate delegates and the warmed append/immediate paths allocate 0 B. R2-2 EnvCell exact per-subset routing: ObjectMeshManager carries Content's TextureBatchData.RetailSurfaceMask onto ObjectRenderBatch at the real upload boundary. EnvCellRenderer scans the active prepared cell snapshot and feeds each real transparent batch's exact mask through RetailAlphaMeshRouter. Pure 0x08 Base1ClipMap reaches CLIP, 0x02 alpha-family reaches ALPHA, and table Immediate subsets draw at the cell turn. Separate fixed-route draw sources coalesce to at most one token per (cell,list) and filtered replay draws only that list's subsets; a mixed cell contributes to both lists without duplicate replay. Detail-on routes eligible subsets immediately with the detail pass. The warmed dispatch/source allocation pins measure 0 B; the production scan/filter is covered behaviorally and uses only retained scratch/enumerators (static allocation audit). R2-3 capacity cleanup: RetailAlphaQueue registers a source before the 3,000 entry capacity return. A source whose first append is rejected is therefore reset by flush, EndFrame, or abort, but its rejected payload is never prepared or drawn. R2-4 production proof and prose: both actual Wb submit sites are exercised; the particle tests call the production dispatcher and inspect the constructed owner's production pipeline/selector; EnvCell tests upload real Content batch masks through ObjectMeshManager and drain real filtered MDI calls. The A1 positive proof executes WorldSceneRenderer's real outdoor frame owner through RetailPViewRenderer.DrawInside and RetailAlphaQueue.EndFrame and observes [DrawBuilding x N, RenderNormalMode] with no LandscapeFlush. The packet and register now state the varying retail first-for-list truth and the exact per-subset EnvCell/AP-238, visible AP-239 compositing, and AP-240 feeder scope. Physical active register counts remain AP=159 and AD=92. Final clean-state gates (actual output): - Release solution build: Build succeeded; 0 Warning(s); 0 Error(s). - Hermetic solution filter: every project green, 16,728 passed / 0 failed / 0 skipped total; AcDream.App.Tests 6,875/6,875. - InstalledDat: 255 passed / 10 failed / 1 skipped / 266 total, exactly the allowed identities: TowerAscent_StaircaseStaysConeVisible_EveryStep; MainGameUiAndChatInput_MediaBearingChildrenNowBuildAsRealWidgets (#383); EveryAuthoredInvisibleWidget_StartsHiddenAcrossAllLayouts (#383); Oh_doorway_still_first_frame_diff (#458); and the six AlphaFlushCounts_{CathedralArrival,CathedralLeak,CathedralStairArch, FoundryDeep,HoltburgDoorwayStill,TerraceEdge}_MatchesRetailFrame2. All six AlphaFlushSites_* pass in the same lane. - VulkanShaderDescriptorContractTests + VulkanShaderManifestTests + RenderPackSpirvValidatorTests: 32 passed / 0 failed / 0 skipped. - Corrected queue/router/walk/driver/particle/Wb/EnvCell/PView production filter: 211 passed / 0 failed / 0 skipped. - Explicit warmed production allocation pins: 2 passed; both measure 0 B. - Register: physical AP-238/AP-239/AP-240/AD-120/~~AP-34~~ rows each count exactly 1; active physical rows AP=159 and AD=92. - git diff --check: PASS. Production mutation checks (each applied, run to the named first failure, and exactly reversed before the final gates): 1. Restoring the particle row-5 throw fails OpaqueClassifiedMeshBatch_WithNoMaterialAlpha_DrawsImmediateOnOpaqueDepthState first with InvalidOperationException: mutation: row 5 unreachable. 2. Deleting row 5's immediate callback fails that same production-dispatch test's first collection assertion: expected [(Mesh, 11, True)], actual []. 3. Constructing the actual owner pipeline with depthWrite:false fails ImmediateOpaqueMesh_UsesProductionParticleMeshOpaquePipelineDescription first at Assert.True(description.Depth.Write): expected true, actual false. 4. Mapping the production selector back to _meshAlphaPipeline fails that same test first at Assert.Same: expected particle-mesh-opaque, actual particle-mesh-alpha. 5. Dropping RetailSurfaceMask at the real ObjectMeshManager upload boundary fails the mixed-cell production scan first: expected Clip | Alpha, actual Immediate. 6. Hardcoding the uploaded EnvCell scan to MaskAlphaFamily fails the mixed-cell production scan first: expected Clip | Alpha, actual Alpha; the pure-mask pin also reports expected Clip, actual Alpha. 7. Inverting the production detail predicate fails the detail-on production pin first: expected Immediate, actual Clip. 8. Removing both EnvCell replay filters fails the mixed production drain's real MDI assertions: each call expected DrawCount 1, actual DrawCount 2. 9. Moving RegisterSource below the full-capacity return fails every RejectedFirstUseSource_IsCleanedWithoutPrepareOrDraw row (flush, EndFrame, abort) at the first ResetCount assertion: expected 1, actual 0. 10. Hardcoding detailSurfaceActive=false at the real Wb dispatch fails both production submit-site tests at their first queue-count assertion: expected 0, actual 1. 11. Restoring RetailPViewRenderer's removed outdoor LandscapeFlush call fails OutdoorProductionPView_DrainsBuildingThenRenderNormalModeWithoutLandscapeFlush first at the real drain sequence: expected [DrawBuilding, RenderNormalMode], actual [DrawBuilding, LandscapeFlush, RenderNormalMode]. 12. Deleting WorldSceneRenderer's final EndFrame owner drain fails that same full-path A1 test first: expected [DrawBuilding, RenderNormalMode], actual [DrawBuilding]. Not done/deferred: none. No retail conflict or infeasible contract item was found. No graphical client was launched. Co-Authored-By: Codex <noreply@openai.com>
266 lines
13 KiB
C#
266 lines
13 KiB
C#
using System.Globalization;
|
|
using System.Text.RegularExpressions;
|
|
using AcDream.App.Rendering;
|
|
|
|
namespace AcDream.App.Tests.Rendering.Walk;
|
|
|
|
/// <summary>
|
|
/// S4-c1 fix round 1, F3: parser for the OH alpha/depth captures
|
|
/// (docs/research/2026-09-01-overhaul/oh-capture/*.alphadepth.log —
|
|
/// tools/walk-oracle/oh/oh-capture-alpha-depth.cdb.template documents the
|
|
/// line formats). Reads the two lines this round's gate cares about:
|
|
///
|
|
/// <list type="bullet">
|
|
/// <item><c>PM poly=<ptr> mode=<0|1> counterBefore=<hex></c> —
|
|
/// one per <c>D3DPolyRender::DrawPortalPolyInternal</c> @0x0059bc90 ENTRY
|
|
/// (mode 1 = far-Z building punch, mode 0 = true-depth exit seal). The
|
|
/// breakpoint is at function ENTRY, before the boundary guard runs inside
|
|
/// the function — a PM line is emitted for every ATTEMPT, guard-rejected
|
|
/// or not; <c>counterBefore</c> is the persistent
|
|
/// <c>portalsDrawnCount</c> global sampled before THIS call's own possible
|
|
/// increment.</item>
|
|
/// <item><c>PC ov=<n> counter=<hex> fc=<0|1></c> — one per
|
|
/// <c>PView::DrawCells</c> @0x005a4840 ENTRY (root turn AND every building
|
|
/// look-in's own re-entrant call alike — the breakpoint doesn't
|
|
/// distinguish). <c>counter</c> is <c>portalsDrawnCount</c> sampled at
|
|
/// THIS call's entry, before its own possible read-then-zero.</item>
|
|
/// </list>
|
|
///
|
|
/// Frame delimiting mirrors <see cref="WalkOracleTrace"/> exactly (same
|
|
/// <c>F <n></c> marker, same "events between F_n and F_(n+1) belong to
|
|
/// frame n" rule, same final-frame drop) so a caller can parse the SAME
|
|
/// capture file with both parsers and get position-consistent frame
|
|
/// numbering — <see cref="WalkOracleTrace"/> for the pose, this type for
|
|
/// the PM/PC sequences.
|
|
/// </summary>
|
|
public static class WalkAlphaDepthTrace
|
|
{
|
|
public static IReadOnlyList<WalkAlphaDepthFrame> Parse(IEnumerable<string> lines)
|
|
{
|
|
var frames = new List<WalkAlphaDepthFrame>();
|
|
List<(int Mode, int CounterBefore)>? currentPm = null;
|
|
List<(int Ov, int Counter, int ForceClear)>? currentPc = null;
|
|
List<(bool IsClip, bool IsNew)>? currentAm = null;
|
|
List<(uint ThreshBits, string Ret, int AmCountAtThisPoint)>? currentFl = null;
|
|
int currentNumber = 0;
|
|
|
|
foreach (string line in lines)
|
|
{
|
|
Match frameMatch = FramePattern.Match(line);
|
|
if (frameMatch.Success)
|
|
{
|
|
if (currentPm is not null)
|
|
{
|
|
frames.Add(new WalkAlphaDepthFrame(
|
|
currentNumber, currentPm, currentPc!, currentAm!, currentFl!));
|
|
}
|
|
currentNumber = int.Parse(
|
|
frameMatch.Groups[1].Value, CultureInfo.InvariantCulture);
|
|
currentPm = new List<(int, int)>();
|
|
currentPc = new List<(int, int, int)>();
|
|
currentAm = new List<(bool, bool)>();
|
|
currentFl = new List<(uint, string, int)>();
|
|
continue;
|
|
}
|
|
if (currentPm is null)
|
|
continue;
|
|
|
|
Match pm = PmPattern.Match(line);
|
|
if (pm.Success)
|
|
{
|
|
currentPm.Add((
|
|
int.Parse(pm.Groups[1].Value, CultureInfo.InvariantCulture),
|
|
int.Parse(pm.Groups[2].Value, NumberStyles.HexNumber, CultureInfo.InvariantCulture)));
|
|
continue;
|
|
}
|
|
Match pc = PcPattern.Match(line);
|
|
if (pc.Success)
|
|
{
|
|
currentPc!.Add((
|
|
int.Parse(pc.Groups[1].Value, CultureInfo.InvariantCulture),
|
|
int.Parse(pc.Groups[2].Value, NumberStyles.HexNumber, CultureInfo.InvariantCulture),
|
|
int.Parse(pc.Groups[3].Value, CultureInfo.InvariantCulture)));
|
|
continue;
|
|
}
|
|
// S4-c2: AM mesh=<ptr> surf=<n> csurf=<ptr> new=<0|1> clip=<0|1>
|
|
// listSel=<0|1> — D3DPolyRender::AddMeshToAlphaList @0x0059c230's
|
|
// own printf. listSel nonzero selects CLIP
|
|
// (alphaedMeshCountClip/List); zero selects ALPHA
|
|
// (alphaedMeshCountAlpha/List) — Ghidra-verified 2026-09-04, see
|
|
// RetailAlphaList's own doc comment. "new" is retail's per-entry
|
|
// first-for-list flag (param_4); "clip" (param_5, unused here)
|
|
// is overrideClipmap. S4-c2 fix round 2: DrawMesh sets one flag
|
|
// per list at entry and clears that list's flag after its first
|
|
// successful append; later subsets in the same call can therefore
|
|
// carry new=0 (the capture has 9,685 true and 871 false). IsNew is
|
|
// retained as a capture fact but is not compared to acdream: its
|
|
// retained Wb/particle/EnvCell submission units are not one-to-one
|
|
// DrawMesh invocations, and Vulkan has no consumer for retail's
|
|
// material-capture optimization bit. The KnownFailure count gate
|
|
// below remains about zero replay content, not this parsed field.
|
|
Match am = AmPattern.Match(line);
|
|
if (am.Success)
|
|
{
|
|
currentAm!.Add((
|
|
IsClip: am.Groups["listSel"].Value != "0",
|
|
IsNew: am.Groups["new"].Value != "0"));
|
|
continue;
|
|
}
|
|
// S4-c2: FL thresh=<hex float bits> ret=<return address> —
|
|
// D3DPolyRender::FlushAlphaList @0x0059d2e0's own printf. `ret`
|
|
// is the CALLER's return address (the call instruction's own
|
|
// address + 5, x86 CALL rel32 being 5 bytes) — NOT the call
|
|
// instruction's address itself.
|
|
Match fl = FlPattern.Match(line);
|
|
if (fl.Success)
|
|
{
|
|
currentFl!.Add((
|
|
uint.Parse(
|
|
fl.Groups["thresh"].Value, NumberStyles.HexNumber, CultureInfo.InvariantCulture),
|
|
fl.Groups["ret"].Value,
|
|
currentAm!.Count));
|
|
continue;
|
|
}
|
|
// Anything else (PM/PC/FL/AM already handled; cdb chrome) is out
|
|
// of this parser's scope — ignored, matching WalkOracleTrace's
|
|
// own catch-all.
|
|
}
|
|
|
|
// The last STARTED frame is deliberately never appended — the
|
|
// truncated detach frame, same rule as WalkOracleTrace.
|
|
return frames;
|
|
}
|
|
|
|
/// <summary>
|
|
/// S4-c2 gate G-c2: reconstructs retail's own sequence of ACTUAL flush
|
|
/// drains (site, threshold, drained-CLIP-count, drained-ALPHA-count)
|
|
/// purely from the capture's AM/FL text — no execution needed on this
|
|
/// side. Mirrors <c>D3DPolyRender::FlushAlphaList</c>'s exact no-op rule
|
|
/// (Ghidra-verified 2026-09-04: no-op only when BOTH running counts are
|
|
/// strictly below <c>threshold * 3000</c>) so a threshold that DOESN'T
|
|
/// drain (the overwhelming majority of 0.75f valve calls at ordinary
|
|
/// scene complexity) leaves the running counts untouched for the NEXT
|
|
/// flush call to inherit — exactly retail's own accumulation behavior.
|
|
/// </summary>
|
|
internal static IReadOnlyList<(RetailAlphaFlushSite Site, float Threshold, int DrainedClip, int DrainedAlpha)>
|
|
BuildExpectedFlushTranscript(WalkAlphaDepthFrame frame)
|
|
{
|
|
var result = new List<(RetailAlphaFlushSite, float, int, int)>();
|
|
int runningClip = 0;
|
|
int runningAlpha = 0;
|
|
int lastAmCount = 0;
|
|
|
|
foreach ((uint threshBits, string ret, int amCountAtThisPoint) in frame.FlEvents)
|
|
{
|
|
// AM lines recorded (in interleaved parse order) between the
|
|
// previous FL and this one accumulate onto the running counts —
|
|
// exactly retail's own accumulation, since a no-op FlushAlphaList
|
|
// leaves both counters untouched for the next call to inherit.
|
|
for (int i = lastAmCount; i < amCountAtThisPoint; i++)
|
|
{
|
|
if (frame.AmEvents[i].IsClip)
|
|
runningClip++;
|
|
else
|
|
runningAlpha++;
|
|
}
|
|
lastAmCount = amCountAtThisPoint;
|
|
|
|
float threshold = BitConverter.UInt32BitsToSingle(threshBits);
|
|
bool noOp = runningClip < threshold * 3000f && runningAlpha < threshold * 3000f;
|
|
if (!noOp)
|
|
{
|
|
result.Add((MapSite(ret), threshold, runningClip, runningAlpha));
|
|
runningClip = 0;
|
|
runningAlpha = 0;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private static RetailAlphaFlushSite MapSite(string ret) => ret switch
|
|
{
|
|
"0059f310" => RetailAlphaFlushSite.DrawBuilding,
|
|
"005a1a0c" => RetailAlphaFlushSite.SortCellExit,
|
|
"005a4877" => RetailAlphaFlushSite.LandscapeFlush,
|
|
"00453b90" => RetailAlphaFlushSite.RenderNormalMode,
|
|
_ => throw new InvalidOperationException(
|
|
$"FL return address 0x{ret} is not one of the four normal-world flush sites "
|
|
+ "(OH1 contract §7) — the fifth private CreatureMode::Render caller, or an "
|
|
+ "unexpected address, reached this capture."),
|
|
};
|
|
|
|
public static IReadOnlyList<WalkAlphaDepthFrame> Load(string root, string fixtureName)
|
|
{
|
|
return Parse(File.ReadLines(ResolvePath(root, fixtureName)));
|
|
}
|
|
|
|
/// <summary>S4-c1 fix round 2 (R2-2): retail's <c>portalsDrawnCount</c>
|
|
/// (wo(008719b4)) is a PERSISTENT session global carried by the running
|
|
/// client across every captured frame, so each capture's own FIRST PM or
|
|
/// PC line — wherever it falls, including the "F 1" preamble text a cdb
|
|
/// session prints before the first parsed frame marker (terrace-edge and
|
|
/// holtburg-doorway-still both have their first sample there) — already
|
|
/// carries the value a real session would have accumulated before the
|
|
/// capture began. Scans <paramref name="fixtureName"/>'s raw lines,
|
|
/// ignoring frame boundaries entirely, for the first line matching
|
|
/// either <see cref="PmPattern"/> or <see cref="PcPattern"/> and returns
|
|
/// its counter field.</summary>
|
|
public static int LoadInitialCounter(string root, string fixtureName)
|
|
{
|
|
foreach (string line in File.ReadLines(ResolvePath(root, fixtureName)))
|
|
{
|
|
Match pm = PmPattern.Match(line);
|
|
if (pm.Success)
|
|
return int.Parse(pm.Groups[2].Value, NumberStyles.HexNumber, CultureInfo.InvariantCulture);
|
|
Match pc = PcPattern.Match(line);
|
|
if (pc.Success)
|
|
return int.Parse(pc.Groups[2].Value, NumberStyles.HexNumber, CultureInfo.InvariantCulture);
|
|
}
|
|
throw new InvalidOperationException(
|
|
$"{fixtureName}: no PM/PC line found to seed PortalsDrawnCount from.");
|
|
}
|
|
|
|
private static string ResolvePath(string root, string fixtureName)
|
|
{
|
|
string repoRoot = FindRepositoryRoot();
|
|
return Path.Combine(repoRoot, Path.Combine(root.Split('/')), fixtureName + ".log");
|
|
}
|
|
|
|
private static string FindRepositoryRoot()
|
|
{
|
|
DirectoryInfo? dir = new(AppContext.BaseDirectory);
|
|
while (dir is not null)
|
|
{
|
|
if (File.Exists(Path.Combine(dir.FullName, "AcDream.slnx")))
|
|
return dir.FullName;
|
|
dir = dir.Parent;
|
|
}
|
|
throw new InvalidOperationException(
|
|
"AcDream.slnx not found above the test base directory; walk-oracle fixtures unavailable.");
|
|
}
|
|
|
|
private static readonly Regex FramePattern = new(@"^F (\d+)\s*$", RegexOptions.Compiled);
|
|
private static readonly Regex PmPattern = new(
|
|
@"^PM poly=[0-9a-f]+ mode=([01]) counterBefore=([0-9a-f]{4})\s*$", RegexOptions.Compiled);
|
|
private static readonly Regex PcPattern = new(
|
|
@"^PC ov=(\d+) counter=([0-9a-f]{4}) fc=([01])\s*$", RegexOptions.Compiled);
|
|
private static readonly Regex AmPattern = new(
|
|
@"^AM mesh=[0-9a-f]+ surf=-?\d+ csurf=[0-9a-f]+ new=(?<new>[01]) clip=(?<clip>[01]) "
|
|
+ @"listSel=(?<listSel>[01])\s*$",
|
|
RegexOptions.Compiled);
|
|
private static readonly Regex FlPattern = new(
|
|
@"^FL thresh=(?<thresh>[0-9a-f]{8}) ret=(?<ret>[0-9a-f]{8})\s*$", RegexOptions.Compiled);
|
|
}
|
|
|
|
/// <summary>One capture frame's PM (mode, counterBefore), PC (ov, counter,
|
|
/// forceClear), AM (isClip, isNew), and FL (thresholdBits, returnAddress,
|
|
/// cumulativeAmCountAtThisPoint) sequences, in the exact order the retail
|
|
/// trace recorded them. Pointers/surface indices are intentionally not
|
|
/// carried — every S4 gate's own comparison ignores them.</summary>
|
|
public sealed record WalkAlphaDepthFrame(
|
|
int Number,
|
|
IReadOnlyList<(int Mode, int CounterBefore)> PmEvents,
|
|
IReadOnlyList<(int Ov, int Counter, int ForceClear)> PcEvents,
|
|
IReadOnlyList<(bool IsClip, bool IsNew)> AmEvents,
|
|
IReadOnlyList<(uint ThreshBits, string Ret, int AmCountAtThisPoint)> FlEvents);
|