Fix round 1 of the two the plan allows for S4-c2 (048d5b12f), addressing every item M1-M8/A1-A8 from the campaign branch's packet section 11. M1 (BLOCKING): ParticleRenderer.DeferToRetailAlphaQueue threw whenever a mesh-particle batch's TranslucencyKind.Opaque classification (mask 0x00) reached a non-Append DrawMesh row - a real, reachable outcome since TryAppendMeshDraws applies no IsOpaque-style pre-filter unlike WbDrawDispatcher's path. Deleted both throws (ParticleRenderer.cs, WbDrawDispatcher.cs SubmitToAlphaQueue), extracted the router-input derivation into internal static ParticleRenderer.RouteParticleSubmission (directly testable), and implemented rows 1/5's immediate draw by reusing the existing per-instance billboard/mesh draw code (new DrawImmediateParticleSubmissionRhi, no new renderer, no sort per A8). materialHasAlpha (row 4) is derived from CMaterial::CheckAlphaValues @0x005396a0 (Ghidra-verified 2026-09-04): has_alpha=0 only when Ambient.a==Diffuse.a==Specular.a==Emissive.a==1.0f, else 1 - and CMaterial::SetTranslucencySimple @0x005396f0 (reached from CPhysicsPart::SetTranslucency <- Particle::Update @0x0051c290's per-frame alpha-fade) sets all four channels to (1f - translucency) uniformly, so has_alpha == "this particle's current alpha isn't 1.0" - already carried in ColorArgb's top byte (Lerp(StartAlpha,EndAlpha,tLife), ParticleSystem.cs 1039-1041/1453). M3: SubmitToAlphaQueue now takes isBuildingShell + viewProjection and feeds Row 1's detailSurfaceActive gate for translucent building-shell instances (retail DrawBuilding @0x0059f2a0 installs building detail before its own shell subsets reach DrawMesh). Row 1 (detail on) draws the instance immediately at its own walk-stream AlphaSubmitMark via new DrawImmediateAlphaInstance/DrawImmediateAlphaInstanceRhi (reusing WriteDeferredAlphaEntrySlot + the drain's pipeline/detail code for slot 0); never touches _deferredAlpha/the queue for pure-Immediate submissions (fixes a latent leak: previously reserving a _deferredAlpha slot for every candidate meant a frame with ALL-immediate content never registered a source, so ResetAlphaSubmissions never ran). M3c: DrawPreparedAlphaBatchRhi's own per-command hasDetail/DrawBuildingDetailRangeRhi branch is deleted - detail-eligible content no longer reaches the drain (spec: delayed replay is always detailEnabled=0). New AD-120 register row for the "in place -> at the entity's own stream mark" granularity adaptation this introduces. M6: RetailPViewPassExecutor.SubmitOrDrawTransparentCellShell routes an EnvCell's transparent shell through the SAME RetailAlphaMeshRouter table (mask 0x02, detailSurfaceActive = EnvCellRenderer.TransparentDetailEnabled) instead of a hand-coded branch. Detail on: unchanged immediate draw. Detail off: appends ONE token per CELL via new EnvCellAlphaDrawSource (internal, delegate-based so it's unit-testable without a GPU harness), replayed through EnvCellRenderer.RenderTransparentOrdered at the next flush - RenderTransparentOrdered already replayed a cell's batch on demand, so no new deferred-replay abstraction was needed (the original landing's claim that one was required is corrected in the packet). M4: RetailAlphaEntry.IsFirstForList deleted. Retail's per-DrawMesh- invocation "first for this list" flag is trivially true for every subset retail ever appends (DrawMesh @0x0059d4a0 sets both flags true at entry, 0059d4cc/0059d4d0, clearing each independently after ITS OWN first append within that call, 0059d5ef) - the captures confirm it (new=1 on 9,685/10,556 AM lines, impossible under "first since last drain"). acdream's deleted isFirstForList = target.Count == 0 computed the wrong quantifier. One acdream append already IS one DrawMesh call, so the field carried no information and its own pin was vacuous (feedback_every_new_pin_must_be_ shown_to_fail) - deleted rather than kept as an always-true no-op. M5: split all six AlphaFlushTranscript_* KnownFailure Facts into live AlphaFlushSites_* (site+threshold sequence only, content-independent, unconditional pass) and AlphaFlushCounts_* (KnownFailure, full tuple incl. drained counts) sharing one RunAlphaFlushTranscriptReplay helper. Rewrote the KnownFailure reason honestly: THREE independent causes, not "harness content only" - (1) zero live mesh content, (2) acdream appends one entry per INSTANCE vs retail's one per SUBSET per DrawMesh call, (3) acdream's CLIP list is structurally empty for ordinary content (M2). Added ONE live valve pin (WalkFrameDriverTests.SortCellExit_ValveDrainsThroughReplayAtThe ExactBoundary, a Theory over 2250/2249) that goes through WalkFrameDriver. Replay's own SortCellExit arm, not RetailAlphaQueue.Flush called directly. M2/A2/M8c (register): ~~AP-34~~ restored as struck-through RETIRED (it had been deleted outright, not struck, breaking the register's own convention) citing residuals AP-238/AP-239/AP-240. The chunk-2 commit's "AP-236"/ "AP-237" collided with an id ALREADY filed-and-retired on main by an unrelated #132 fix (docs/ISSUES.md:19976) - ids are never reused, so both are renumbered: AP-236->AP-238 (EnvCell; rewritten per M6 - granularity-only residual now, "never through the shared queue" is retired), AP-237->AP-239 (mask-reconstruction gap; the dangling "AP-236b" citation in RetailAlphaMeshRouter.cs never matched either filed id and is corrected to AP-239). New AP-240 filed for M2: WbDrawDispatcher.IsOpaque filters ClipMap out upstream, so ordinary clip-mapped content never reaches retail's CLIP FIFO (draws immediately on the pre-existing OpaqueAlphaToCoverage pipeline instead) - no pipeline change this round, per the contract. AP section header recounted honestly: 159 active (was stale at 161); AD section 92 (was 91, +AD-120). M7: AP-239's only cited example (cloud GfxObj 0x01004C35, surface 0x08000023) is DISPROVEN - decoded Type=0x10114 has alpha-family bits that win in BOTH retail's real priority and acdream's reconstruction, so it never actually diverges. A 2026-09-04 scan of all 6,152 Portal-dat Surfaces found 27 real Translucent+Base1ClipMap-no-alpha-family instances (Type=0x14); cross-referencing GfxObjs found surface 0x08000015 on GfxObj 0x010001EC - cited in place of the disproven cloud example. M8: (a) RenderDeviceD3D::DrawBlock is @0x005a17c0 per symbols.json, not @0x005a18d0 (that is its per-land-cell loop head) - fixed across RetailAlphaQueue.cs, RetailPViewPassExecutor.cs, WalkEvents.cs, WalkFrameDriver.cs (x3), RetailFrameWalk.cs. (b) RetailAlphaQueue.cs's four FlushAlphaList addresses were labelled "return site" but are the CALL instruction's own address (x86 CALL rel32 is 5 bytes) - relabelled "call at X (returns to X+5)". (d) worldbuilder-inventory.md:420's AP-34 citation updated. A1: RetailPViewRenderer.cs's outdoor-root passes.FlushLandscapeAlpha() call (labelled LandscapeFlush) is deleted - retail's real LandscapeFlush (PView::DrawCells @0x005a4840's own FlushAlphaList(0f) @0x005a4872) sits strictly inside if (outside_view.view_count > 0) on the INTERIOR PView; an outdoor root never reaches it (terrace-edge capture: zero 005a4877 FL lines). Verified no opaque draw intervenes between this deletion point and RetailAlphaQueue.EndFrame()'s own RenderNormalMode pass-end flush (WorldSceneRenderer.cs: DrawPostWorldParticles's PView branch is an explicit no-op; only diagnostic wireframes follow) - #132's "drain after the finished opaque world" invariant is kept by that flush alone. A4: RetailAlphaQueue's ApplyScratchRetention call sites passed the ENTRY count as the SOURCE-count argument too (observedClip + observedAlpha twice), so _sources/_sourceDrawOffsets never shrank toward the real source count. Fixed to capture _sources.Count before Clear(). A5/A6/A7: WalkStaticStreamPopulatorTests.cs's stale "same viewer distance" banner corrected; two off-by-one entry counts in the packet's section 10 sequences corrected (foundry-deep x10->x11, holtburg-doorway-still x5->x6); WalkAlphaDepthTrace.cs's "new=" comment explains why IsNew is retained but never compared (M4's finding makes a per-subset comparison meaningless at acdream's per-instance granularity); RetailAlphaMeshRouterTests.cs's IndependentReferenceRoute (never actually independently-shaped) renamed to RestatedBranchTableRoute with an honest doc comment, and the unverified "flipped in turn and failed every time" claim deleted. A3 was resolved as a side effect of M1's SubmitToAlphaQueue doc-comment rewrite (the wrong "0x02/0x03, 0x04/0x05, 0x08/0x09" enumeration no longer exists); RetailAlphaMeshRouter.cs's own MaskFromTranslucencyKind comment updated in the same spirit (M1's particle-site Opaque-reachability finding). Mutation checks (each applied to a scratch-restored copy, confirmed failing, then byte-exact reverted - diffed against a /tmp backup after restore): - A1 (RetailPViewRendererTests.DrawInside_NeverCallsFlushLandscapeAlphaDirectly): restoring the deleted "if (ctx.RootCell.IsOutdoorNode) passes.FlushLandscapeAlpha();" line reintroduces a direct RetailPViewPassExecutor.FlushLandscapeAlpha call in DrawInside's compiled body. Actual failure: "Assert.DoesNotContain() Failure: Filter matched in collection ... CompiledCall { ... Target = Void FlushLandscapeAlpha() }". - A4 (RetailAlphaQueueTests.RetainedSourceCapacity_ConvergesToTheReal SourceCountNotTheEntryCount): reverting both ApplyScratchRetention calls to (observedClip + observedAlpha, observedClip + observedAlpha) - with 100 distinct sources in the spike frame (so _sources.Capacity grows to 128, past its initial 4) followed by three low-demand frames of 10 entries via ONE repeated source - makes sourceTarget compute from 10 (Math.Max(4,10*2)=20) instead of 1 (Math.Max(4,1*2)=4). Actual failure: "Expected the retained source-array capacity to converge toward the real source count (1), but it stayed at 20". (A single-source-only spike does NOT discriminate this bug - Math.Min(sourceTarget, _sources.Capacity) clamps both formulas to the same unchanged initial 4 - hence the 100-source setup.) - M5 valve (WalkFrameDriverTests.SortCellExit_ValveDrainsThroughReplayAt TheExactBoundary, 2250 case): flipping RetailAlphaQueue.Flush's no-op comparison from "<" to "<=" (S4-c1/c2's own boundary mutation) makes the 2250-entries case a no-op instead of draining. Actual failure: "Assert.Equal() Failure: Values differ Expected: 0 Actual: 2250". - M6 (EnvCellAlphaDrawSourceTests, both Facts): adding an immediate renderTransparentOrdered(new[]{cellId}) call inside EnvCellAlphaDrawSource.AddPendingCellId (simulating the M1/M3 "drew immediately instead of deferring" bug class) makes PendingCellToken_IsDrainedAtTheFlushAndNeverBeforeIt fail with "Assert.Empty() Failure: Collection was not empty Collection: [[4660]]" and ParticleAppendedBetweenTwoCellTokens_KeepsItsPositionInTheCombined Drain fail with 'Expected: string[] ["cell:256", "particle:7", "cell:512"] Actual: List<string> ["cell:256", "cell:512", "cell:256", "particle:7", "cell:512"]'. - M1 (ParticleRendererRouteTests.OpaqueClassifiedMeshBatch_WithMaterialAlpha _RoutesToAlphaAppend): hardcoding materialHasAlpha to false inside RouteParticleSubmission makes the Opaque-mask+has-alpha case route Immediate instead of Append. Actual failure: "Assert.Equal() Failure: Values differ Expected: Append Actual: Immediate". Gates (real output lines): - dotnet build AcDream.slnx -c Release -> "Build succeeded. 0 Warning(s) 0 Error(s)". - Hermetic lane (--filter "Lane!=InstalledDat&Lane!=PreparedPackage& Lane!=Live&Lane!=Manual&Lane!=Timing&Lane!=Windows&Lane!=Linux& Lane!=SystemFont&Purpose!=Diagnostic&Status!=KnownFailure") -> "Passed! - Failed: 0, Passed: 6864, Skipped: 0, Total: 6864". - InstalledDat lane (ACDREAM_RUN_INSTALLED_DAT_TESTS=1, ACDREAM_DAT_DIR="C:/Users/erikn/Documents/Asheron's Call", --filter "Lane=InstalledDat") -> "Failed: 10, Passed: 255, Skipped: 1, Total: 266" - the exact 4 pre-existing failures (TowerAscent_StaircaseStaysConeVisible_EveryStep, LayoutImporterMediaBearingChildSweepTests, LayoutImporterInvisibleSweep Tests, Oh_doorway_still_first_frame_diff) plus the six AlphaFlushCounts_* KnownFailure Facts, nothing else; all six AlphaFlushSites_* Facts pass inside this same lane (the whole WalkTraceConformanceTests partial class carries Lane=InstalledDat, so they cannot run hermetically). - Shader classes (VulkanShaderDescriptorContractTests, VulkanShaderManifestTests, RenderPackSpirvValidatorTests) -> "Passed! - Failed: 0, Passed: 32, Skipped: 0, Total: 32". - RetailAlphaQueueTests, RetailAlphaMeshRouterTests, RetailFrameWalkTests, WalkFrameDriverTests, ParticleRenderer*Tests, EnvCellRenderer*Tests (combined filter) -> "Passed! - Failed: 0, Passed: 120, Skipped: 0, Total: 120". - Register collision grep: AP-238/AP-239/AP-240/AD-120 each count exactly 1; ~~AP-34~~ count exactly 1; live totals AP=159, AD=92. Not done / deferred: none - all M1-M8/A1-A8 items landed; A8 was a no-action note. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
663 lines
34 KiB
C#
663 lines
34 KiB
C#
using System.Linq;
|
|
using System.Numerics;
|
|
using System.Reflection;
|
|
using System.Reflection.Emit;
|
|
using AcDream.App.Composition;
|
|
using AcDream.App.Rendering;
|
|
using AcDream.App.Rendering.Gpu;
|
|
using AcDream.App.Rendering.Gpu.Vk;
|
|
using AcDream.App.Rendering.Sky;
|
|
using AcDream.App.Rendering.Walk;
|
|
using AcDream.App.Rendering.Wb;
|
|
using AcDream.App.Tests.Architecture;
|
|
using AcDream.App.Tests.Rendering.Gpu;
|
|
|
|
namespace AcDream.App.Tests.Rendering;
|
|
|
|
public sealed class RetailPViewPassExecutorTests
|
|
{
|
|
[Fact]
|
|
public void Extracted_contracts_retain_no_window_callbacks_or_visibility_owner()
|
|
{
|
|
Assert.DoesNotContain(
|
|
typeof(RetailPViewFrameInput).GetProperties(),
|
|
property => typeof(Delegate).IsAssignableFrom(property.PropertyType));
|
|
|
|
FieldInfo[] fields = typeof(RetailPViewPassExecutor).GetFields(
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
Assert.DoesNotContain(fields, field => field.FieldType == typeof(GameWindow));
|
|
Assert.DoesNotContain(fields, field => field.FieldType == typeof(CellVisibility));
|
|
Assert.DoesNotContain(fields, field => field.FieldType == typeof(RetailPViewFrameInput));
|
|
Assert.DoesNotContain(fields, field => field.FieldType == typeof(RetailPViewFrameResult));
|
|
Assert.DoesNotContain(fields, field => field.FieldType == typeof(ClipFrameAssembly));
|
|
Assert.DoesNotContain(
|
|
fields,
|
|
field => typeof(Delegate).IsAssignableFrom(field.FieldType));
|
|
}
|
|
|
|
[Fact]
|
|
public void Concrete_executor_accumulates_walk_terrain_batch_timing()
|
|
{
|
|
// S3 chunk 3 fix round 1 (F3): the walk leaf no longer brackets
|
|
// itself with Begin()/Complete() (that stopwatch-restart pair would
|
|
// push one timing SAMPLE per batch, not one per frame) — it times
|
|
// itself with a raw Stopwatch.GetTimestamp() delta and hands the
|
|
// elapsed ticks to AccumulateWalkBatch, which only accumulates.
|
|
MethodInfo landscape = typeof(RetailPViewPassExecutor).GetMethod(
|
|
"DrawWalkLandCellBatch",
|
|
BindingFlags.Instance | BindingFlags.NonPublic)!;
|
|
IReadOnlyList<CompiledCall> landscapeCalls = CompiledCallGraph.Read(landscape);
|
|
int terrainDraw = RequiredCallIndex(
|
|
landscapeCalls,
|
|
typeof(TerrainModernRenderer),
|
|
nameof(TerrainModernRenderer.DrawLandCells));
|
|
int accumulate = RequiredCallIndex(
|
|
landscapeCalls,
|
|
typeof(TerrainDrawDiagnosticsController),
|
|
nameof(TerrainDrawDiagnosticsController.AccumulateWalkBatch));
|
|
|
|
Assert.True(terrainDraw < accumulate);
|
|
Assert.DoesNotContain(
|
|
landscapeCalls,
|
|
call => call.Target.DeclaringType == typeof(TerrainDrawDiagnosticsController)
|
|
&& call.Target.Name == nameof(TerrainDrawDiagnosticsController.Begin));
|
|
Assert.DoesNotContain(
|
|
landscapeCalls,
|
|
call => call.Target.DeclaringType == typeof(TerrainDrawDiagnosticsController)
|
|
&& call.Target.Name == nameof(TerrainDrawDiagnosticsController.Complete));
|
|
}
|
|
|
|
[Fact]
|
|
public void Concrete_executor_pushes_the_walk_terrain_frame_sample_at_replay_end()
|
|
{
|
|
// S3 chunk 3 fix round 1 (F3): DrawWalkDrivenStatics is the ONE call
|
|
// site of driver.Replay in production — CompleteWalkTerrainFrame
|
|
// must run immediately after it, so the frame's sample is pushed
|
|
// exactly once, at "the end of the walk replay".
|
|
MethodInfo drawWalkDrivenStatics = typeof(RetailPViewRenderer).GetMethod(
|
|
"DrawWalkDrivenStatics",
|
|
BindingFlags.Instance | BindingFlags.NonPublic)!;
|
|
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(drawWalkDrivenStatics);
|
|
int replay = RequiredCallIndex(
|
|
calls,
|
|
typeof(AcDream.App.Rendering.Walk.WalkFrameDriver),
|
|
nameof(AcDream.App.Rendering.Walk.WalkFrameDriver.Replay));
|
|
int completeWalkFrame = RequiredCallIndex(
|
|
calls,
|
|
typeof(RetailPViewPassExecutor),
|
|
nameof(RetailPViewPassExecutor.CompleteWalkTerrainFrame));
|
|
|
|
Assert.True(replay < completeWalkFrame);
|
|
}
|
|
|
|
[Fact]
|
|
public void Frame_composition_constructs_one_walk_executor()
|
|
{
|
|
MethodInfo compose = typeof(FrameRootCompositionPhase).GetMethod(
|
|
"ComposeCore",
|
|
BindingFlags.Instance | BindingFlags.NonPublic)!;
|
|
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(compose);
|
|
|
|
int executor = RequiredCallIndex(
|
|
calls,
|
|
typeof(RetailPViewPassExecutor),
|
|
".ctor");
|
|
int renderer = RequiredCallIndex(
|
|
calls,
|
|
typeof(WorldScenePViewRenderer),
|
|
".ctor");
|
|
|
|
Assert.True(executor < renderer);
|
|
Assert.Single(
|
|
calls,
|
|
call => call.Target.DeclaringType == typeof(RetailPViewPassExecutor)
|
|
&& call.Target.Name == ".ctor");
|
|
Assert.Single(
|
|
calls,
|
|
call => call.Target.DeclaringType == typeof(WorldScenePViewRenderer)
|
|
&& call.Target.Name == ".ctor");
|
|
}
|
|
|
|
/// <summary>
|
|
/// S3 chunk 1 fix round 2 (§11.6 H1): the weather MESH draw + its OC
|
|
/// print moved OUT of <see cref="RetailPViewPassExecutor.DrawWeatherOnce"/>
|
|
/// entirely — S3 chunk 4 (O3) relocated the print to
|
|
/// <c>WalkFrameDriver.OnWeatherTurn</c>, fired at Collect time by
|
|
/// <c>RetailFrameWalk.DrawLandscape</c> (see the real transcript pins in
|
|
/// <c>WalkFrameDriverTranscriptTests</c>: <c>Collect_OutdoorRoot_...</c>
|
|
/// and <c>Collect_InteriorRoot_...</c>). This method now draws the
|
|
/// weather MESH and the rain PARTICLES only — the former per-outside-
|
|
/// view-slice loop that used to run before this call (the walk's own
|
|
/// screen-space terrain-clip writer + its per-frame clip-routing reset
|
|
/// call + the old <c>DrawLandscapeSliceLate</c> leaf) is deleted outright (§10.2): retail
|
|
/// draws the weather mesh and its
|
|
/// rain particles ONCE, unclipped, never once per doorway aperture.
|
|
/// MUTATION: re-inlining a
|
|
/// <c>WalkTranscriptDump.PrintObjectCellTurn</c> call back into this
|
|
/// method makes the <c>Assert.DoesNotContain</c> below fail; deleting
|
|
/// either the mesh or the particle call makes the matching
|
|
/// <c>Assert.Single</c> fail (zero matches instead of one).
|
|
/// </summary>
|
|
[Fact]
|
|
public void DrawWeatherOnce_DrawsTheWeatherMeshAndParticlesButNeverPrints()
|
|
{
|
|
MethodInfo method = typeof(RetailPViewPassExecutor).GetMethod(
|
|
nameof(RetailPViewPassExecutor.DrawWeatherOnce),
|
|
BindingFlags.Instance | BindingFlags.Public)!;
|
|
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(method);
|
|
|
|
Assert.Single(
|
|
calls,
|
|
call => call.Target.DeclaringType == typeof(SkyRenderer)
|
|
&& call.Target.Name == nameof(SkyRenderer.RenderWeather));
|
|
Assert.Single(
|
|
calls,
|
|
call => call.Target.DeclaringType == typeof(ParticleRenderer)
|
|
&& call.Target.Name == nameof(ParticleRenderer.Draw));
|
|
Assert.DoesNotContain(
|
|
calls,
|
|
call => call.Target.DeclaringType == typeof(WalkTranscriptDump));
|
|
}
|
|
|
|
/// <summary>
|
|
/// S4-c2 fix round 1 (M6, contract C4): a cell's transparent shell's
|
|
/// immediate-vs-append decision must come from the SAME
|
|
/// <see cref="RetailAlphaMeshRouter"/> table every other alpha submitter
|
|
/// uses — never a hand-coded <c>if (detailEnabled)</c> branch that
|
|
/// bypasses it. Pins the STRUCTURE: <see cref="EnvCellRenderer.TransparentDetailEnabled"/>
|
|
/// is read to feed <see cref="RetailAlphaMeshRouter.Route"/>'s own
|
|
/// <c>detailSurfaceActive</c> argument BEFORE the route call, and the
|
|
/// route call precedes BOTH possible outcomes
|
|
/// (<see cref="EnvCellRenderer.RenderTransparentOrdered"/> for
|
|
/// Immediate, <see cref="RetailAlphaQueue.TryAppend"/> for Append).
|
|
/// Mutation check: deleting the <c>Route</c> call and branching directly
|
|
/// on <c>TransparentDetailEnabled</c> instead removes the
|
|
/// <see cref="RetailAlphaMeshRouter"/> call entirely — <c>RequiredCallIndex</c>
|
|
/// fails its <c>index >= 0</c> assertion for the route call.
|
|
/// </summary>
|
|
[Fact]
|
|
public void SubmitOrDrawTransparentCellShell_RoutesThroughTheSharedTableBeforeEitherOutcome()
|
|
{
|
|
MethodInfo method = typeof(RetailPViewPassExecutor).GetMethod(
|
|
"SubmitOrDrawTransparentCellShell",
|
|
BindingFlags.Instance | BindingFlags.NonPublic)!;
|
|
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(method);
|
|
|
|
int detailProbe = RequiredCallIndex(
|
|
calls, typeof(EnvCellRenderer), "get_TransparentDetailEnabled");
|
|
int route = RequiredCallIndex(
|
|
calls, typeof(RetailAlphaMeshRouter), nameof(RetailAlphaMeshRouter.Route));
|
|
int renderImmediate = RequiredCallIndex(
|
|
calls, typeof(EnvCellRenderer), nameof(EnvCellRenderer.RenderTransparentOrdered));
|
|
int append = RequiredCallIndex(
|
|
calls, typeof(RetailAlphaQueue), nameof(RetailAlphaQueue.TryAppend));
|
|
|
|
Assert.True(detailProbe < route);
|
|
Assert.True(route < renderImmediate);
|
|
Assert.True(route < append);
|
|
}
|
|
|
|
/// <summary>
|
|
/// S3 chunk 4 (§10.2): the former per-outside-view-slice loop
|
|
/// (the walk's own screen-space terrain-clip writer + its per-frame
|
|
/// clip-routing reset call + the old <c>DrawLandscapeSliceLate</c> leaf,
|
|
/// one call per active landscape view) is deleted — <c>DrawLandscapeDynamicsPhase</c> now calls
|
|
/// <see cref="RetailPViewPassExecutor.DrawWeatherOnce"/> exactly once,
|
|
/// conditional on <see cref="AcDream.App.Rendering.Walk.WalkFrameDriver.WeatherTurnFired"/>
|
|
/// (see <see cref="DrawLandscapeDynamicsPhase_GatesDrawWeatherOnceOnWalkDriverWeatherTurnFired"/>
|
|
/// — the L1 pin), with no loop of any kind around it. This
|
|
/// <c>Assert.Single</c> alone proved insufficient at fix round 1 (K1):
|
|
/// it counts DISTINCT call-site offsets, so it stays green even with a
|
|
/// <c>foreach</c> wrapped around the one call site (the exact round-1
|
|
/// regression this file's review caught) — see
|
|
/// <see cref="DrawLandscapeDynamicsPhase_DrawWeatherOnceCallSiteHasNoEnclosingBackwardBranch"/>
|
|
/// for the pin that actually rules that out. Kept as a cheap first-line
|
|
/// check: MUTATION: adding a second, textually distinct call site (e.g.
|
|
/// a duplicated call, not a loop) makes <c>Assert.Single</c> fail.
|
|
/// </summary>
|
|
[Fact]
|
|
public void DrawLandscapeDynamicsPhase_CallsDrawWeatherOnceExactlyOnce()
|
|
{
|
|
MethodInfo method = typeof(RetailPViewRenderer).GetMethod(
|
|
"DrawLandscapeDynamicsPhase",
|
|
BindingFlags.Instance | BindingFlags.NonPublic)!;
|
|
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(method);
|
|
|
|
Assert.Single(
|
|
calls,
|
|
call => call.Target.DeclaringType == typeof(RetailPViewPassExecutor)
|
|
&& call.Target.Name == nameof(RetailPViewPassExecutor.DrawWeatherOnce));
|
|
}
|
|
|
|
/// <summary>
|
|
/// S3 chunk 4 fix round 1 (K1, blocking): the real loop-shape pin.
|
|
/// <see cref="DrawLandscapeDynamicsPhase_CallsDrawWeatherOnceExactlyOnce"/>'s
|
|
/// <c>Assert.Single</c> over call-site offsets still passes when the ONE
|
|
/// call site sits inside a <c>foreach</c> — an IL-offset ORDER pin has
|
|
/// now failed three times to be discriminating for this exact class of
|
|
/// regression, so this asks the LOOP-SHAPE question directly: does any
|
|
/// BACKWARD branch (a branch whose target offset is lower than its own
|
|
/// offset — the shape every C# loop compiles to, whether
|
|
/// <c>for</c>/<c>foreach</c>/<c>while</c>) enclose the
|
|
/// <c>DrawWeatherOnce</c> call's own IL offset? A call sitting strictly
|
|
/// between a backward branch's target and its own offset is inside that
|
|
/// loop's body and can run more than once per method invocation; a call
|
|
/// outside every backward branch's span cannot. MUTATION: wrap the call
|
|
/// in <c>foreach (var slice in clipAssembly.OutsideViewSlices)</c> —
|
|
/// the compiled <c>foreach</c> emits a backward branch (the
|
|
/// condition-check jump back to the loop body) whose span now contains
|
|
/// the call's offset, so this test fails; restore the single
|
|
/// unconditional call to make it pass again. Note for reviewers (added
|
|
/// at S3 landing hygiene, H4, matching <see cref="DrawWalkSky_RenderSkyCallSiteHasNoEnclosingBackwardBranch"/>'s
|
|
/// own note): <see cref="CompiledCallGraph.ReadBranches"/> reads only
|
|
/// <c>br</c>/<c>brtrue</c>/<c>brfalse</c>-family single-target branches —
|
|
/// it does not decode a compiled <c>switch</c> jump table, but no C# loop
|
|
/// construct (<c>for</c>/<c>foreach</c>/<c>while</c>/<c>do</c>) ever
|
|
/// compiles to one, so this pin's blind spot is not a loop shape it
|
|
/// could miss.
|
|
/// </summary>
|
|
[Fact]
|
|
public void DrawLandscapeDynamicsPhase_DrawWeatherOnceCallSiteHasNoEnclosingBackwardBranch()
|
|
{
|
|
MethodInfo method = typeof(RetailPViewRenderer).GetMethod(
|
|
"DrawLandscapeDynamicsPhase",
|
|
BindingFlags.Instance | BindingFlags.NonPublic)!;
|
|
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(method);
|
|
int callIndex = RequiredCallIndex(
|
|
calls,
|
|
typeof(RetailPViewPassExecutor),
|
|
nameof(RetailPViewPassExecutor.DrawWeatherOnce));
|
|
int callOffset = calls[callIndex].Offset;
|
|
|
|
IReadOnlyList<CompiledBranch> branches = CompiledCallGraph.ReadBranches(method);
|
|
Assert.DoesNotContain(
|
|
branches,
|
|
branch => branch.TargetOffset < branch.Offset
|
|
&& branch.TargetOffset <= callOffset
|
|
&& callOffset < branch.Offset);
|
|
}
|
|
|
|
/// <summary>
|
|
/// S3 chunk 4 fix round 2 (L1, BLOCKING). Round 1's three-lens review
|
|
/// found that neither existing pin above actually looks at the
|
|
/// production CONDITION gating <c>DrawWeatherOnce</c>: restoring the
|
|
/// pre-fix gate <c>if (clipAssembly.OutsideViewSlices.Length != 0)</c> —
|
|
/// the exact regression K2 was supposed to close — leaves both green,
|
|
/// because both only ask "is the call site shaped correctly", never
|
|
/// "does the call site read <c>WalkFrameDriver.WeatherTurnFired</c>".
|
|
/// This pin reads the compiled condition directly: with <c>c</c> the
|
|
/// index of the <c>DrawWeatherOnce</c> call, (a) <c>calls[c-1]</c> must
|
|
/// be the <c>WeatherTurnFired</c> getter — the LAST call before the draw
|
|
/// — and (b) exactly one branch must sit strictly between that getter
|
|
/// call and the draw call, be a <c>brfalse</c>/<c>brfalse.s</c>, and
|
|
/// jump FORWARD past the draw call — the compiled shape of
|
|
/// <c>if (walkDriver.WeatherTurnFired) passes.DrawWeatherOnce(ctx);</c>
|
|
/// and nothing else (an inverted test, an unconditional call, or a
|
|
/// different condition entirely all fail one of the two checks).
|
|
/// MUTATION M1 (restores the pre-fix regression): change the gate back
|
|
/// to <c>if (clipAssembly.OutsideViewSlices.Length != 0)</c> — check (a)
|
|
/// fails because <c>calls[c-1]</c> is no longer the
|
|
/// <c>WeatherTurnFired</c> getter. MUTATION M2 (drops the gate
|
|
/// entirely): make the call unconditional — check (b) fails because no
|
|
/// branch sits between the getter call and the draw call (in fact the
|
|
/// getter call itself disappears with the gate, so check (a) fails
|
|
/// first). MUTATION M3 (inverts the condition): change the gate to
|
|
/// <c>if (!walkDriver.WeatherTurnFired)</c> — <c>calls[c-1]</c> is still
|
|
/// the getter (check (a) passes), but the compiler emits a
|
|
/// <c>brtrue</c>/<c>brtrue.s</c> to skip the draw instead of a
|
|
/// <c>brfalse</c>/<c>brfalse.s</c>, so check (b)'s opcode filter finds
|
|
/// nothing and <c>Assert.Single</c> fails on zero matches.
|
|
/// </summary>
|
|
[Fact]
|
|
public void DrawLandscapeDynamicsPhase_GatesDrawWeatherOnceOnWalkDriverWeatherTurnFired()
|
|
{
|
|
MethodInfo method = typeof(RetailPViewRenderer).GetMethod(
|
|
"DrawLandscapeDynamicsPhase",
|
|
BindingFlags.Instance | BindingFlags.NonPublic)!;
|
|
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(method);
|
|
int callIndex = RequiredCallIndex(
|
|
calls,
|
|
typeof(RetailPViewPassExecutor),
|
|
nameof(RetailPViewPassExecutor.DrawWeatherOnce));
|
|
Assert.True(callIndex > 0, "Expected a call before DrawWeatherOnce — the gate condition.");
|
|
|
|
CompiledCall condition = calls[callIndex - 1];
|
|
Assert.Equal(typeof(AcDream.App.Rendering.Walk.WalkFrameDriver), condition.Target.DeclaringType);
|
|
Assert.Equal("get_WeatherTurnFired", condition.Target.Name);
|
|
|
|
int conditionOffset = condition.Offset;
|
|
int drawOffset = calls[callIndex].Offset;
|
|
IReadOnlyList<CompiledBranch> branches = CompiledCallGraph.ReadBranches(method);
|
|
Assert.Single(
|
|
branches,
|
|
branch => branch.Offset > conditionOffset
|
|
&& branch.Offset < drawOffset
|
|
&& (branch.OpCode == OpCodes.Brfalse || branch.OpCode == OpCodes.Brfalse_S)
|
|
&& branch.TargetOffset > drawOffset);
|
|
}
|
|
|
|
/// <summary>
|
|
/// S3 landing hygiene (H5): strengthens the L1 pin above against a
|
|
/// conjoined gate the post-hoc three-lens review found it does not
|
|
/// reject. L1's check (b) only looks at branches STRICTLY BETWEEN the
|
|
/// <c>WeatherTurnFired</c> getter call and the <c>DrawWeatherOnce</c>
|
|
/// call — so <c>if (clipAssembly.OutsideViewSlices.Length != 0 &&
|
|
/// walkDriver.WeatherTurnFired) passes.DrawWeatherOnce(ctx);</c> still
|
|
/// passes it: the compiler evaluates <c>OutsideViewSlices.Length != 0</c>
|
|
/// FIRST, so ITS OWN <c>brfalse</c> lands BEFORE the getter call's
|
|
/// offset — outside L1's window — while <c>calls[c-1]</c> is still the
|
|
/// getter (the added condition reads <c>OutsideViewSlices</c>, a
|
|
/// property getter, then <c>.Length</c>, a non-call <c>ldlen</c>, so no
|
|
/// OTHER call intervenes before the getter). This pin widens the window
|
|
/// to start at the call immediately before the whole gate —
|
|
/// <see cref="RetailPViewPassExecutor.DrawUnattachedSceneParticles"/>,
|
|
/// the outdoor-emitters call the method's own comment names as the last
|
|
/// call before the gate — and counts EVERY branch in that wider window
|
|
/// with <c>Assert.Single</c>: production has exactly one, the forward
|
|
/// <c>brfalse</c>/<c>brfalse.s</c> right after the getter. A conjoined
|
|
/// gate's extra, earlier condition adds a second branch this wider
|
|
/// window catches but L1's narrower one cannot.
|
|
/// MUTATION: change the gate to <c>if
|
|
/// (clipAssembly.OutsideViewSlices.Length != 0 &&
|
|
/// walkDriver.WeatherTurnFired)</c> — the branch count in the window
|
|
/// goes from 1 to 2 and <c>Assert.Single</c> fails (see the commit body
|
|
/// for the exact recorded failure text).
|
|
/// </summary>
|
|
[Fact]
|
|
public void DrawLandscapeDynamicsPhase_ExactlyOneBranchGuardsDrawWeatherOnce()
|
|
{
|
|
MethodInfo method = typeof(RetailPViewRenderer).GetMethod(
|
|
"DrawLandscapeDynamicsPhase",
|
|
BindingFlags.Instance | BindingFlags.NonPublic)!;
|
|
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(method);
|
|
int particlesIndex = RequiredCallIndex(
|
|
calls,
|
|
typeof(RetailPViewPassExecutor),
|
|
nameof(RetailPViewPassExecutor.DrawUnattachedSceneParticles));
|
|
int drawIndex = RequiredCallIndex(
|
|
calls,
|
|
typeof(RetailPViewPassExecutor),
|
|
nameof(RetailPViewPassExecutor.DrawWeatherOnce));
|
|
int particlesOffset = calls[particlesIndex].Offset;
|
|
int drawOffset = calls[drawIndex].Offset;
|
|
|
|
IReadOnlyList<CompiledBranch> branches = CompiledCallGraph.ReadBranches(method);
|
|
CompiledBranch onlyGuard = Assert.Single(
|
|
branches,
|
|
branch => branch.Offset > particlesOffset && branch.Offset < drawOffset);
|
|
|
|
Assert.True(
|
|
onlyGuard.OpCode == OpCodes.Brfalse || onlyGuard.OpCode == OpCodes.Brfalse_S,
|
|
$"Expected the sole branch between DrawUnattachedSceneParticles and "
|
|
+ $"DrawWeatherOnce to be a brfalse, was {onlyGuard.OpCode}.");
|
|
Assert.True(
|
|
onlyGuard.TargetOffset > drawOffset,
|
|
"Expected the guard branch to skip forward past DrawWeatherOnce.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// S3 chunk 4 fix round 2 (L8): the identical loop-shape question K1
|
|
/// asked of <see cref="RetailPViewPassExecutor.DrawWeatherOnce"/>'s call
|
|
/// site, applied to <see cref="RetailPViewPassExecutor.DrawWalkSky"/>'s
|
|
/// own <see cref="SkyRenderer.RenderSky"/> call — retail draws the sky
|
|
/// dome exactly once per frame too (K4's own doc comment on
|
|
/// <c>DrawWalkSky</c>), so nothing may wrap this call in a loop either.
|
|
/// Note for reviewers: <see cref="CompiledCallGraph.ReadBranches"/> reads
|
|
/// only <c>br</c>/<c>brtrue</c>/<c>brfalse</c>-family single-target
|
|
/// branches — it does not decode a compiled <c>switch</c> jump table,
|
|
/// but no C# loop construct (<c>for</c>/<c>foreach</c>/<c>while</c>/
|
|
/// <c>do</c>) ever compiles to one, so this pin's blind spot is not a
|
|
/// loop shape it could miss. MUTATION: wrapping the call in
|
|
/// <c>for (int i = 0; i < 2; i++) { _sky?.RenderSky(...); }</c> makes
|
|
/// this fail; restoring the single unconditional call makes it pass
|
|
/// again.
|
|
/// </summary>
|
|
[Fact]
|
|
public void DrawWalkSky_RenderSkyCallSiteHasNoEnclosingBackwardBranch()
|
|
{
|
|
MethodInfo method = typeof(RetailPViewPassExecutor).GetMethod(
|
|
"DrawWalkSky",
|
|
BindingFlags.Instance | BindingFlags.NonPublic)!;
|
|
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(method);
|
|
int callIndex = RequiredCallIndex(
|
|
calls,
|
|
typeof(SkyRenderer),
|
|
nameof(SkyRenderer.RenderSky));
|
|
int callOffset = calls[callIndex].Offset;
|
|
|
|
IReadOnlyList<CompiledBranch> branches = CompiledCallGraph.ReadBranches(method);
|
|
Assert.DoesNotContain(
|
|
branches,
|
|
branch => branch.TargetOffset < branch.Offset
|
|
&& branch.TargetOffset <= callOffset
|
|
&& callOffset < branch.Offset);
|
|
}
|
|
|
|
/// <summary>
|
|
/// S3 chunk 1 fix round 2 (§11.6 H1): <see
|
|
/// cref="RetailPViewPassExecutor.ShouldDrawWeatherOnce"/> is
|
|
/// <see cref="DrawWeatherOnce_DrawsTheWeatherMeshAndPrintsExactlyOnce"/>'s
|
|
/// gate, extracted as a pure predicate so this suite can pin "no OC line
|
|
/// while the player stands indoors" without a live GL/DAT
|
|
/// <see cref="SkyRenderer"/> — combined with the two structural tests
|
|
/// above (moved out of the loop; drawn/printed exactly once per call),
|
|
/// this proves both halves of the spec's pin: an outdoor root (or an
|
|
/// interior root with several exit-view slices) prints exactly one OC
|
|
/// line, and an indoor player prints none. Retail's own check:
|
|
/// <c>SmartBox::is_player_outside</c> @0x00451e80,
|
|
/// <c>(player objcell_id & 0xFFFF) < 0x100</c>. MUTATION: negating
|
|
/// the <c>< 0x100</c> comparison (or dropping either bool AND) makes
|
|
/// one of the four rows below fail.
|
|
/// </summary>
|
|
[Theory]
|
|
[InlineData(true, true, 0xF4180003u, true)] // outdoor root, player outside a land cell -> draws
|
|
[InlineData(true, true, 0xA9B40100u, false)] // player indoors (local id >= 0x100) -> no draw
|
|
[InlineData(false, true, 0xF4180003u, false)] // RenderSky off -> no draw
|
|
[InlineData(true, false, 0xF4180003u, false)] // RenderWeather off -> no draw
|
|
public void ShouldDrawWeatherOnce_MatchesRetailIsPlayerOutsideGate(
|
|
bool renderSky, bool renderWeather, uint playerCellId, bool expected)
|
|
{
|
|
Assert.Equal(
|
|
expected,
|
|
RetailPViewPassExecutor.ShouldDrawWeatherOnce(renderSky, renderWeather, playerCellId));
|
|
}
|
|
|
|
/// <summary>
|
|
/// S4-c1 C1 (T2, seal half): <c>D3DPolyRender::DrawPortalPolyInternal</c>
|
|
/// @0x0059bc90's degenerate-input guard, ported at the exit-seal
|
|
/// enumeration (<c>DrawPortalDepthWrite</c> — the SAME loop that reads
|
|
/// <c>cell.PortalPolygons[index]</c>, the LOCAL portal-polygon
|
|
/// vertices, and is the only production caller of
|
|
/// <see cref="RetailPViewPassExecutor.DrawExitPortalMask"/>). A live
|
|
/// functional test of this private method needs a real
|
|
/// <see cref="PortalDepthMaskRenderer"/> the suite has no fake for (see
|
|
/// <c>WalkFrameDriverTests.OnPunchGeometry_RejectsWholePolygonOn...</c>
|
|
/// for the punch-fan half's functional proof instead), so this pin asks
|
|
/// the compiled-call-graph question this file's other tests already use
|
|
/// for exactly this situation: does
|
|
/// <see cref="AcDream.App.Rendering.Walk.WalkVisibilityMath.IsRejectedByPortalPolygonBoundaryGuard"/>
|
|
/// run BEFORE the vertex loop's <see cref="Vector3.Transform(Vector3,
|
|
/// Matrix4x4)"/> calls and BEFORE <see cref="PortalDepthMaskRenderer.DrawDepthFan"/>
|
|
/// (retail's reject -> transform -> clip -> count order — a hit
|
|
/// must never reach either), with a conditional branch gating that
|
|
/// order directly off the guard's own return value.
|
|
/// MUTATION: move the guard call to AFTER the transform loop (or
|
|
/// delete it) — either check below fails because the guard call index
|
|
/// is no longer the smallest, or (deletion) <see cref="RequiredCallIndex"/>
|
|
/// throws for finding no call at all.
|
|
/// </summary>
|
|
[Fact]
|
|
public void DrawPortalDepthWrite_RejectsDegenerateLocalPolygons_BeforeTransformOrSubmission()
|
|
{
|
|
MethodInfo method = typeof(RetailPViewPassExecutor).GetMethod(
|
|
"DrawPortalDepthWrite",
|
|
BindingFlags.Instance | BindingFlags.NonPublic)!;
|
|
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(method);
|
|
|
|
int guardIndex = RequiredCallIndex(
|
|
calls,
|
|
typeof(AcDream.App.Rendering.Walk.WalkVisibilityMath),
|
|
nameof(AcDream.App.Rendering.Walk.WalkVisibilityMath.IsRejectedByPortalPolygonBoundaryGuard));
|
|
int transformIndex = RequiredCallIndex(calls, typeof(Vector3), nameof(Vector3.Transform));
|
|
int drawIndex = RequiredCallIndex(
|
|
calls, typeof(PortalDepthMaskRenderer), nameof(PortalDepthMaskRenderer.DrawDepthFan));
|
|
|
|
Assert.True(
|
|
guardIndex < transformIndex,
|
|
"The boundary guard must run BEFORE the world-transform loop "
|
|
+ "(retail's reject -> transform -> clip -> count order).");
|
|
Assert.True(
|
|
guardIndex < drawIndex,
|
|
"The boundary guard must run BEFORE the fan is submitted "
|
|
+ "(no draw, no `submitted` increment on a hit).");
|
|
|
|
int guardOffset = calls[guardIndex].Offset;
|
|
int transformOffset = calls[transformIndex].Offset;
|
|
IReadOnlyList<CompiledBranch> branches = CompiledCallGraph.ReadBranches(method);
|
|
Assert.Contains(
|
|
branches,
|
|
branch => branch.Offset > guardOffset
|
|
&& branch.Offset < transformOffset
|
|
&& (branch.OpCode == OpCodes.Brtrue || branch.OpCode == OpCodes.Brtrue_S
|
|
|| branch.OpCode == OpCodes.Brfalse || branch.OpCode == OpCodes.Brfalse_S));
|
|
}
|
|
|
|
/// <summary>
|
|
/// S4-c1 fix round 1, F2: retail increments <c>portalsDrawnCount</c>
|
|
/// (0x59BD70-0x59BD74) BEFORE <c>polyClipFinish</c> runs (0x59BDB0) —
|
|
/// the counter records accepted ATTEMPTS, not successful GPU fans
|
|
/// (<c>oh1-depth-lifecycle.md</c>'s "Far-Z punches and true-depth exit
|
|
/// seals" section). A polygon that survives the boundary guard but has
|
|
/// fewer than 3 vertices is still COUNTED by
|
|
/// <see cref="RetailPViewPassExecutor.DrawExitPortalMask"/>'s returned
|
|
/// <c>submitted</c> total, even though
|
|
/// <see cref="PortalDepthMaskRenderer.DrawDepthFan"/> draws nothing (its
|
|
/// own <c>< 3</c> guard stands in for retail's post-clip
|
|
/// <c>var_4 >= 3</c> check). Round 0 dropped the count too — a
|
|
/// <c>< 3</c> continue ahead of both the boundary guard and the
|
|
/// count — never observed on authored dat data (every real portal
|
|
/// polygon has >= 3 vertices) but the wrong order all the same.
|
|
/// MUTATION (verified, S4-c1 fix round 2 R2-5b): restore the old
|
|
/// <c>localVertices.Length < 3</c> pre-filter ahead of the guard —
|
|
/// this pin's synthetic 2-vertex polygon is no longer counted and the
|
|
/// assertion fails (<c>submitted</c> comes back 0, not 1). Moving the
|
|
/// <c>submitted++</c> increment itself to AFTER
|
|
/// <see cref="PortalDepthMaskRenderer.DrawDepthFan"/> — the mutation
|
|
/// this doc comment used to name alongside the pre-filter one — does
|
|
/// NOT fail this pin: <c>DrawDepthFan</c> has no effect on the local
|
|
/// <c>submitted</c> counter either way, so the final returned count is
|
|
/// identical regardless of which side of that call the increment sits
|
|
/// on. That reordering is unobservable to any assertion on the return
|
|
/// value; only the pre-filter mutation is a genuine regression check.
|
|
/// </summary>
|
|
[Fact]
|
|
public void DrawExitPortalMask_CountsAnUnclippableTwoVertexPolygon_ButDrawsNothing()
|
|
{
|
|
var cell = new LoadedCell
|
|
{
|
|
CellId = 0xA9B40105u,
|
|
WorldTransform = Matrix4x4.Identity,
|
|
};
|
|
cell.Portals.Add(new CellPortalInfo(OtherCellId: 0xFFFF, PolygonId: 0, Flags: 0, OtherPortalId: 0));
|
|
// Ordinary (non-degenerate) coordinates — the boundary guard admits
|
|
// this polygon — but only TWO vertices: retail's post-clip
|
|
// `var_4 >= 3` check (this port's DrawDepthFan `< 3` guard) drops
|
|
// the fan submission even though the count already happened.
|
|
cell.PortalPolygons.Add(
|
|
[
|
|
new Vector3(1f, 1f, 0f),
|
|
new Vector3(2f, 1f, 0f),
|
|
]);
|
|
|
|
using var device = new RecordingGpuDevice();
|
|
var frames = new GpuDeviceFrameLifetime(device);
|
|
var scope = new VulkanWorldPassScope(sampleCount: 1);
|
|
using var portalDepthMask = new PortalDepthMaskRenderer(device, frames, scope);
|
|
var diagnostics = new WorldRenderDiagnostics(new NeverCalledGlStateReader(), new NeverCalledDiagnosticLog());
|
|
|
|
// DrawPortalDepthWrite only reads _portalDepthMask, frame.Cells,
|
|
// frame.RootCell.IsOutdoorNode, frame.ViewProjection, and
|
|
// _diagnostics (short-circuited off by RenderingDiagnostics.
|
|
// ProbeSeamDrawEnabled's default-off value) — the same
|
|
// constructor-bypass pattern WalkOutsideViewReassemblyTests already
|
|
// uses for exercising a real leaf without a full GL/DAT renderer
|
|
// graph.
|
|
var executor = (RetailPViewPassExecutor)System.Runtime.CompilerServices.RuntimeHelpers
|
|
.GetUninitializedObject(typeof(RetailPViewPassExecutor));
|
|
typeof(RetailPViewPassExecutor)
|
|
.GetField("_portalDepthMask", BindingFlags.NonPublic | BindingFlags.Instance)!
|
|
.SetValue(executor, portalDepthMask);
|
|
typeof(RetailPViewPassExecutor)
|
|
.GetField("_diagnostics", BindingFlags.NonPublic | BindingFlags.Instance)!
|
|
.SetValue(executor, diagnostics);
|
|
|
|
var root = new LoadedCell { CellId = 0xF4180003u, IsOutdoorNode = false };
|
|
RetailPViewFrameInput frame = new RetailPViewFrameInput().Reset(
|
|
rootCell: root,
|
|
nearbyBuildingCells: null,
|
|
viewerEyePos: Vector3.Zero,
|
|
viewProjection: Matrix4x4.Identity,
|
|
cells: new SingleCellSource(cell),
|
|
camera: null!,
|
|
cameraWorldPosition: Vector3.Zero,
|
|
frustum: null,
|
|
playerLandblockId: null,
|
|
animatedEntityIds: null,
|
|
renderCenterLbX: 0,
|
|
renderCenterLbY: 0,
|
|
renderRadius: 0,
|
|
landblockEntries: Array.Empty<(uint, Vector3, Vector3,
|
|
IReadOnlyList<AcDream.Core.World.WorldEntity>,
|
|
IReadOnlyDictionary<uint, AcDream.Core.World.WorldEntity>?)>(),
|
|
renderSky: false,
|
|
renderWeather: false,
|
|
dayFraction: 0f,
|
|
activeDayGroup: null,
|
|
skyKeyframe: default,
|
|
environOverrideActive: false,
|
|
viewerCellId: 0,
|
|
playerCellId: 0,
|
|
playerViewPosition: Vector3.Zero,
|
|
cameraView: Matrix4x4.Identity,
|
|
cameraCellResolution: default);
|
|
|
|
int drawsBefore = device.Calls.OfType<GpuRecordedDraw>().Count();
|
|
int submitted = executor.DrawExitPortalMask(frame, cell.CellId, ReadOnlySpan<Vector4>.Empty);
|
|
int drawsAfter = device.Calls.OfType<GpuRecordedDraw>().Count();
|
|
|
|
Assert.Equal(1, submitted);
|
|
Assert.Equal(drawsBefore, drawsAfter);
|
|
}
|
|
|
|
private sealed class SingleCellSource(LoadedCell cell) : IRetailPViewCellSource
|
|
{
|
|
public LoadedCell? Find(uint cellId) => cellId == cell.CellId ? cell : null;
|
|
}
|
|
|
|
private sealed class NeverCalledGlStateReader : IRenderGlStateReader
|
|
{
|
|
public RenderGlStateSnapshot CaptureState() =>
|
|
throw new InvalidOperationException("EmitSeamMask must short-circuit before reading GL state.");
|
|
|
|
public RenderGlScissorSnapshot CaptureScissor() =>
|
|
throw new InvalidOperationException("EmitSeamMask must short-circuit before reading GL state.");
|
|
}
|
|
|
|
private sealed class NeverCalledDiagnosticLog : IRenderFrameDiagnosticLog
|
|
{
|
|
public void WriteLine(string message) =>
|
|
throw new InvalidOperationException("EmitSeamMask must short-circuit before logging (ProbeSeamDrawEnabled defaults off).");
|
|
}
|
|
|
|
private static int RequiredCallIndex(
|
|
IReadOnlyList<CompiledCall> calls,
|
|
Type declaringType,
|
|
string methodName)
|
|
{
|
|
int index = CompiledCallGraph.IndexOf(calls, declaringType, methodName);
|
|
Assert.True(
|
|
index >= 0,
|
|
$"Expected call to {declaringType.Name}.{methodName}.");
|
|
return index;
|
|
}
|
|
}
|