fix(render): S4 chunk 2 round 1 - particle throw, detail-off immediate draws, EnvCell FIFO, gate honesty
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>
This commit is contained in:
parent
89f1e2676f
commit
9ccb61a8ec
26 changed files with 1180 additions and 326 deletions
File diff suppressed because one or more lines are too long
|
|
@ -416,8 +416,10 @@ mesh decoder, or second scene graph was introduced. Retail anchors:
|
||||||
`RenderDeviceD3D::DrawObjCellForDummies` `0x005A0760`,
|
`RenderDeviceD3D::DrawObjCellForDummies` `0x005A0760`,
|
||||||
`CShadowPart::insertion_sort` `0x006B5130`,
|
`CShadowPart::insertion_sort` `0x006B5130`,
|
||||||
`D3DPolyRender::AddMeshToAlphaList` `0x0059C230`, and
|
`D3DPolyRender::AddMeshToAlphaList` `0x0059C230`, and
|
||||||
`D3DPolyRender::FlushAlphaList` `0x0059D2E0`. The modern per-cell-order and
|
`D3DPolyRender::FlushAlphaList` `0x0059D2E0`. AP-34 is RETIRED (S4-c2 fix
|
||||||
EnvCell-shell residual is tracked explicitly as AP-34. WorldBuilder does not
|
round 1, 2026-09-04) — retail's own two-list FIFO alpha queue now ports the
|
||||||
|
per-cell-order mechanism directly; the EnvCell-shell granularity residual
|
||||||
|
continues as AP-238 (see the register). WorldBuilder does not
|
||||||
choose visible cells, portal slices, or cross-cell order: it receives the
|
choose visible cells, portal slices, or cross-cell order: it receives the
|
||||||
walk's ordered records and GPU clip slots and performs asset preparation plus
|
walk's ordered records and GPU clip slots and performs asset preparation plus
|
||||||
order-preserving Vulkan batching only.
|
order-preserving Vulkan batching only.
|
||||||
|
|
|
||||||
|
|
@ -370,6 +370,56 @@ public sealed unsafe partial class ParticleRenderer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// S4-c2 fix round 1 (M1c): <c>DrawMesh</c> rows 1/5's "render
|
||||||
|
/// immediately; enqueue nothing" outcome for ONE particle submission —
|
||||||
|
/// reuses the exact same per-instance billboard/mesh draw code
|
||||||
|
/// <see cref="DrawOrderedRhi"/> uses for its own runs (no new renderer),
|
||||||
|
/// applied to a single-element run so no sort is introduced (A8: retail's
|
||||||
|
/// immediate <c>RenderMeshSubset</c> draws in mesh/submission order —
|
||||||
|
/// this draws exactly this submission, right where <c>DrawMesh</c> was
|
||||||
|
/// called, never re-ordered against its neighbors).
|
||||||
|
/// </summary>
|
||||||
|
private void DrawImmediateParticleSubmissionRhi(
|
||||||
|
Matrix4x4 viewProjection,
|
||||||
|
ParticleSubmissionKind kind,
|
||||||
|
int drawIndex)
|
||||||
|
{
|
||||||
|
IGpuPassEncoder encoder = _scope!.RequireEncoder();
|
||||||
|
IGpuFrame frame = RequireRhiFrame();
|
||||||
|
|
||||||
|
if (kind == ParticleSubmissionKind.Billboard)
|
||||||
|
{
|
||||||
|
ParticleDraw draw = _drawListScratch[drawIndex];
|
||||||
|
_runScratch.Clear();
|
||||||
|
_runScratch.Add(draw.Instance);
|
||||||
|
DrawInstancesRhi(encoder, frame, _runScratch, viewProjection, draw.Key.Additive);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
GlobalMeshBuffer? global = _meshAdapter?.MeshManager?.GlobalBuffer;
|
||||||
|
if (!MeshParticlesAvailable || global is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
MeshParticleDraw meshDraw = _meshDrawListScratch[drawIndex];
|
||||||
|
if (_meshInstanceScratch.Length < 1)
|
||||||
|
_meshInstanceScratch = new MeshParticleGpuInstance[256];
|
||||||
|
WriteMeshGpuInstance(ref _meshInstanceScratch[0], meshDraw.Instance);
|
||||||
|
GpuRingAllocation instances = WriteVertexRing<MeshParticleGpuInstance>(
|
||||||
|
frame,
|
||||||
|
_meshInstanceScratch.AsSpan(0, 1));
|
||||||
|
DrawMeshBatchRhi(
|
||||||
|
encoder,
|
||||||
|
frame,
|
||||||
|
global,
|
||||||
|
meshDraw.Batch,
|
||||||
|
viewProjection,
|
||||||
|
instances.Buffer,
|
||||||
|
instances.OffsetBytes,
|
||||||
|
instanceCount: 1,
|
||||||
|
firstInstance: 0);
|
||||||
|
}
|
||||||
|
|
||||||
private void DrawInstancesRhi(
|
private void DrawInstancesRhi(
|
||||||
IGpuPassEncoder encoder,
|
IGpuPassEncoder encoder,
|
||||||
IGpuFrame frame,
|
IGpuFrame frame,
|
||||||
|
|
|
||||||
|
|
@ -316,6 +316,64 @@ public sealed unsafe partial class ParticleRenderer : IDisposable
|
||||||
DrawOrdered(camera);
|
DrawOrdered(camera);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// S4-c2 fix round 1 (M1): pure router-input derivation for one particle
|
||||||
|
/// submission, extracted so it is directly unit-testable without a
|
||||||
|
/// GPU/mesh-manager harness. A billboard's blend mode (Additive bool) is
|
||||||
|
/// either the Additive or the plain Alpha surface bit — both live in
|
||||||
|
/// retail's alpha-family union (0x00010300) and construct the SAME queue
|
||||||
|
/// mask (0x02); billboards never carry a ClipMap bit (that is a mesh/
|
||||||
|
/// UV-texture concept a generated particle quad has no equivalent of),
|
||||||
|
/// so they always route to ALPHA. A mesh-particle batch's
|
||||||
|
/// <paramref name="meshTranslucency"/> CAN be <see cref="TranslucencyKind.Opaque"/>
|
||||||
|
/// (mask 0x00) — unlike <c>WbDrawDispatcher</c>'s ordinary translucent-
|
||||||
|
/// GfxObj path, <c>TryAppendMeshDraws</c> appends every batch with
|
||||||
|
/// <c>IndexCount>0</c> regardless of translucency classification, so
|
||||||
|
/// mask 0x00 is a real, reachable input here.
|
||||||
|
///
|
||||||
|
/// <para>M1(b): retail's has_alpha (<c>CMaterial::CheckAlphaValues</c>
|
||||||
|
/// @0x005396a0 — Ghidra-verified 2026-09-04: has_alpha=0 only when
|
||||||
|
/// Ambient.a==Diffuse.a==Specular.a==Emissive.a==1.0f, else has_alpha=1)
|
||||||
|
/// is driven, for a particle, by <c>CMaterial::SetTranslucencySimple</c>
|
||||||
|
/// @0x005396f0 — the only production writer reached from a particle's
|
||||||
|
/// live translucency (<c>CPhysicsPart::SetTranslucency</c> <-
|
||||||
|
/// <c>Particle::Update</c> @0x0051c290's own per-frame alpha-fade
|
||||||
|
/// interpolation) — which sets ALL FOUR alpha channels to the SAME
|
||||||
|
/// <c>1f - translucency</c> value and then calls CheckAlphaValues. So
|
||||||
|
/// has_alpha for a particle's material is exactly "this particle's
|
||||||
|
/// CURRENT alpha isn't 1.0 (fully opaque)". acdream already carries that
|
||||||
|
/// exact interpolated value — <c>Lerp(p.StartAlpha, p.EndAlpha, tLife)</c>,
|
||||||
|
/// baked into the top byte of <paramref name="meshColorArgb"/> by
|
||||||
|
/// <c>Color32</c> (ParticleSystem.cs:1039-1041,1453) — so no new state is
|
||||||
|
/// needed. Billboards never reach row 4 (their mask 0x02 always
|
||||||
|
/// satisfies row 3), so <paramref name="meshColorArgb"/> only matters for
|
||||||
|
/// mesh particles.</para>
|
||||||
|
///
|
||||||
|
/// <para>This "Scene" pass never draws during the Sky leaf (sky/off-
|
||||||
|
/// screen particles use the independent DrawOrdered path entirely, never
|
||||||
|
/// this method) and never installs a detail surface; MultiPassAlpha
|
||||||
|
/// stays false (Must Not: no environment override) — Row 2 is therefore
|
||||||
|
/// provably unreachable here. Rows 1/5 (Immediate) ARE reachable for an
|
||||||
|
/// Opaque-classified mesh-particle batch with no material alpha.</para>
|
||||||
|
/// </summary>
|
||||||
|
internal static RetailAlphaMeshDecision RouteParticleSubmission(
|
||||||
|
ParticleSubmissionKind kind, TranslucencyKind meshTranslucency, uint meshColorArgb)
|
||||||
|
{
|
||||||
|
bool isMesh = kind == ParticleSubmissionKind.Mesh;
|
||||||
|
byte mask = kind == ParticleSubmissionKind.Billboard
|
||||||
|
? RetailAlphaMeshRouter.MaskAlphaFamily
|
||||||
|
: RetailAlphaMeshRouter.MaskFromTranslucencyKind(meshTranslucency);
|
||||||
|
bool materialHasAlpha = isMesh && ((meshColorArgb >> 24) & 0xFFu) != 0xFFu;
|
||||||
|
|
||||||
|
return RetailAlphaMeshRouter.Route(
|
||||||
|
currentlyDrawingSky: false,
|
||||||
|
delayMask: RetailAlphaMeshRouter.DefaultDelayMask,
|
||||||
|
detailSurfaceActive: false,
|
||||||
|
multiPassAlpha: false,
|
||||||
|
subsetMask: mask,
|
||||||
|
materialHasAlpha: materialHasAlpha);
|
||||||
|
}
|
||||||
|
|
||||||
private void DeferToRetailAlphaQueue(ICamera camera)
|
private void DeferToRetailAlphaQueue(ICamera camera)
|
||||||
{
|
{
|
||||||
RetailAlphaQueue queue = _alphaQueue!;
|
RetailAlphaQueue queue = _alphaQueue!;
|
||||||
|
|
@ -323,16 +381,6 @@ public sealed unsafe partial class ParticleRenderer : IDisposable
|
||||||
for (int i = 0; i < _submissionScratch.Count; i++)
|
for (int i = 0; i < _submissionScratch.Count; i++)
|
||||||
{
|
{
|
||||||
ParticleSubmission submission = _submissionScratch[i];
|
ParticleSubmission submission = _submissionScratch[i];
|
||||||
// A billboard's blend mode (Additive bool) is either the
|
|
||||||
// Additive or the plain Alpha surface bit — both live in
|
|
||||||
// retail's alpha-family union (0x00010300) and construct the
|
|
||||||
// SAME queue mask (0x02); billboards never carry a ClipMap bit
|
|
||||||
// (that is a mesh/UV-texture concept a generated particle quad
|
|
||||||
// has no equivalent of), so they always route to ALPHA.
|
|
||||||
byte mask = submission.Kind == ParticleSubmissionKind.Billboard
|
|
||||||
? RetailAlphaMeshRouter.MaskAlphaFamily
|
|
||||||
: RetailAlphaMeshRouter.MaskFromTranslucencyKind(
|
|
||||||
_meshDrawListScratch[submission.DrawIndex].Batch.Translucency);
|
|
||||||
|
|
||||||
DeferredParticleDraw deferred = submission.Kind == ParticleSubmissionKind.Billboard
|
DeferredParticleDraw deferred = submission.Kind == ParticleSubmissionKind.Billboard
|
||||||
? new DeferredParticleDraw(
|
? new DeferredParticleDraw(
|
||||||
|
|
@ -346,31 +394,49 @@ public sealed unsafe partial class ParticleRenderer : IDisposable
|
||||||
_meshDrawListScratch[submission.DrawIndex],
|
_meshDrawListScratch[submission.DrawIndex],
|
||||||
viewProjection);
|
viewProjection);
|
||||||
|
|
||||||
int token = _deferredAlpha.Count;
|
RetailAlphaMeshDecision decision = submission.Kind == ParticleSubmissionKind.Billboard
|
||||||
_deferredAlpha.Add(deferred);
|
? RouteParticleSubmission(submission.Kind, default, default)
|
||||||
|
: RouteParticleSubmission(
|
||||||
|
submission.Kind,
|
||||||
|
_meshDrawListScratch[submission.DrawIndex].Batch.Translucency,
|
||||||
|
_meshDrawListScratch[submission.DrawIndex].Instance.ColorArgb);
|
||||||
|
|
||||||
// S4-c2: same unreachable-branch reasoning as
|
switch (decision.Action)
|
||||||
// WbDrawDispatcher.SubmitToAlphaQueue — this "Scene" pass never
|
|
||||||
// draws during the Sky leaf (sky/off-screen particles use the
|
|
||||||
// independent DrawOrdered path entirely, never this method) and
|
|
||||||
// never installs a detail surface; MultiPassAlpha stays false.
|
|
||||||
RetailAlphaMeshDecision decision = RetailAlphaMeshRouter.Route(
|
|
||||||
currentlyDrawingSky: false,
|
|
||||||
delayMask: RetailAlphaMeshRouter.DefaultDelayMask,
|
|
||||||
detailSurfaceActive: false,
|
|
||||||
multiPassAlpha: false,
|
|
||||||
subsetMask: mask,
|
|
||||||
materialHasAlpha: false);
|
|
||||||
if (decision.Action != RetailAlphaMeshAction.Append)
|
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
case RetailAlphaMeshAction.Append:
|
||||||
"Scene particle submissions never draw during the Sky leaf or with a "
|
{
|
||||||
+ "detail surface installed, and MultiPassAlpha stays false — DrawMesh's "
|
int token = _deferredAlpha.Count;
|
||||||
+ $"row 1/2/4/5 branches are unreachable here; got {decision.Action}.");
|
_deferredAlpha.Add(deferred);
|
||||||
}
|
// Capacity overflow (spec §5): dropped, no recovery.
|
||||||
|
queue.TryAppend(decision.List, _alphaSource, token, decision.OverrideClipmap);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// Capacity overflow (spec §5): dropped, no recovery.
|
case RetailAlphaMeshAction.Immediate:
|
||||||
queue.TryAppend(decision.List, _alphaSource, token, decision.OverrideClipmap);
|
// M1(c): rows 1/5 — draw NOW, through the same
|
||||||
|
// per-instance draw code DrawOrderedRhi uses for one
|
||||||
|
// submission (no new renderer). A8: no sort — retail's
|
||||||
|
// immediate RenderMeshSubset draws in mesh/submission
|
||||||
|
// order, so this draws exactly this submission, right
|
||||||
|
// where DrawMesh was called.
|
||||||
|
DrawImmediateParticleSubmissionRhi(viewProjection, submission.Kind, submission.DrawIndex);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case RetailAlphaMeshAction.AppendClipAndImmediate:
|
||||||
|
{
|
||||||
|
// Row 2: provably unreachable at this call site
|
||||||
|
// (MultiPassAlpha stays false), but a router row is a
|
||||||
|
// data-driven outcome, never an invariant to crash on
|
||||||
|
// (feedback_retail_dispatch_is_data_driven) — handled in
|
||||||
|
// full, exactly as spec §4 row 2 describes: append to
|
||||||
|
// CLIP AND ALSO draw immediately.
|
||||||
|
int token = _deferredAlpha.Count;
|
||||||
|
_deferredAlpha.Add(deferred);
|
||||||
|
queue.TryAppend(decision.List, _alphaSource, token, decision.OverrideClipmap);
|
||||||
|
DrawImmediateParticleSubmissionRhi(viewProjection, submission.Kind, submission.DrawIndex);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -95,27 +95,44 @@ internal static class RetailAlphaMeshRouter
|
||||||
/// spec's queue-routing mask from that already-collapsed classification.
|
/// spec's queue-routing mask from that already-collapsed classification.
|
||||||
/// <see cref="TranslucencyKind.Opaque"/> and <see cref="TranslucencyKind.ClipMap"/>
|
/// <see cref="TranslucencyKind.Opaque"/> and <see cref="TranslucencyKind.ClipMap"/>
|
||||||
/// are pre-filtered as "opaque" by <c>WbDrawDispatcher.IsOpaque</c> and
|
/// are pre-filtered as "opaque" by <c>WbDrawDispatcher.IsOpaque</c> and
|
||||||
/// never actually reach a call site that invokes this — both map
|
/// never reach THAT call site (both map defensively there rather than
|
||||||
/// defensively rather than throwing.
|
/// throwing) — but S4-c2 fix round 1 (M1) found a SECOND call site,
|
||||||
|
/// <c>ParticleRenderer.TryAppendMeshDraws</c>/<c>DeferToRetailAlphaQueue</c>,
|
||||||
|
/// which applies NO such pre-filter: a mesh-particle batch's
|
||||||
|
/// <see cref="TranslucencyKind.Opaque"/> classification is a real,
|
||||||
|
/// reachable input there, mapping to mask <c>0x00</c> — a data-driven
|
||||||
|
/// outcome the particle site now handles (immediate draw, or ALPHA via
|
||||||
|
/// the material-alpha fallback), never an invariant to assume away.
|
||||||
///
|
///
|
||||||
/// <para><b>Known gap (register row AP-236b):</b>
|
/// <para><b>Known gap (register row AP-239, formerly filed as the
|
||||||
|
/// dangling "AP-236b" — corrected S4-c2 fix round 1, A2/M8c):</b>
|
||||||
/// <see cref="TranslucencyKindExtensions.FromSurfaceType"/>'s
|
/// <see cref="TranslucencyKindExtensions.FromSurfaceType"/>'s
|
||||||
/// "Translucent override" (raw <c>Surface.Type</c> has BOTH the
|
/// "Translucent override" (raw <c>Surface.Type</c> has BOTH the
|
||||||
/// Translucent (0x10) AND Base1ClipMap (0x04) bits, but no alpha-family
|
/// Translucent (0x10) AND Base1ClipMap (0x04) bits, but no alpha-family
|
||||||
/// bit — e.g. cloud GfxObj <c>0x01004C35</c>) resolves to
|
/// bit) resolves to <see cref="TranslucencyKind.AlphaBlend"/>, discarding
|
||||||
/// <see cref="TranslucencyKind.AlphaBlend"/>, discarding the raw ClipMap
|
/// the raw ClipMap bit. Retail's <c>ConstructMesh</c> would still see
|
||||||
/// bit. Retail's <c>ConstructMesh</c> would still see that ClipMap bit
|
/// that ClipMap bit and produce mask <c>0x08</c> (routes to CLIP); this
|
||||||
/// and produce mask <c>0x08</c> (routes to CLIP); this reconstruction,
|
/// reconstruction, having only the collapsed <see cref="TranslucencyKind"/>,
|
||||||
/// having only the collapsed <see cref="TranslucencyKind"/>, produces
|
/// produces mask <c>0x02</c> (routes to ALPHA) instead. <c>GroupKey</c>
|
||||||
/// mask <c>0x02</c> (routes to ALPHA) instead. <c>GroupKey</c> does not
|
/// does not carry the raw <c>SurfaceType</c> needed to distinguish the
|
||||||
/// carry the raw <c>SurfaceType</c> needed to distinguish the two
|
/// two AlphaBlend origins, and plumbing it through would touch
|
||||||
/// AlphaBlend origins, and plumbing it through would touch
|
|
||||||
/// <c>GfxObjSubMesh</c>/<c>GroupKey</c>/every mesh-classification call
|
/// <c>GfxObjSubMesh</c>/<c>GroupKey</c>/every mesh-classification call
|
||||||
/// site — out of this chunk's bounded scope. The consequence is
|
/// site — out of this chunk's bounded scope. S4-c2 fix round 1 (M7)
|
||||||
|
/// DISPROVED this row's original cited example: cloud GfxObj
|
||||||
|
/// <c>0x01004C35</c>'s surface <c>0x08000023</c> is
|
||||||
|
/// <c>Type=0x10114</c> (ADDITIVE|ALPHA|TRANSLUCENT|BASE1_CLIPMAP) — its
|
||||||
|
/// alpha-family bits win in BOTH retail's real priority AND this
|
||||||
|
/// reconstruction, so it reproduces identically (mask <c>0x02</c> either
|
||||||
|
/// way) and never demonstrated the gap at all. A 2026-09-04 scan of
|
||||||
|
/// every Portal-dat <c>Surface</c> (6,152 scanned) found 27 REAL
|
||||||
|
/// instances of Translucent+Base1ClipMap with no alpha-family bit
|
||||||
|
/// (<c>Type=0x14</c>) — e.g. surface <c>0x08000015</c>, referenced by
|
||||||
|
/// GfxObj <c>0x010001EC</c> — confirming the gap is real, just not via
|
||||||
|
/// the disproven cloud example. The consequence remains
|
||||||
/// compositing-ORDER only (a queue-selection difference, not a blend/
|
/// compositing-ORDER only (a queue-selection difference, not a blend/
|
||||||
/// visual difference): a translucent+clipmap subset (in practice, only
|
/// visual difference): an affected subset can interleave with ordinary
|
||||||
/// clouds) can interleave with ordinary ALPHA-list content at a narrow
|
/// ALPHA-list content at a narrow overlap instead of retail's CLIP-list
|
||||||
/// overlap instead of retail's CLIP-list interleave.</para>
|
/// interleave.</para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static byte MaskFromTranslucencyKind(TranslucencyKind kind) => kind switch
|
internal static byte MaskFromTranslucencyKind(TranslucencyKind kind) => kind switch
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -33,24 +33,32 @@ internal enum RetailAlphaList : byte
|
||||||
/// Retail's four normal-world <c>D3DPolyRender::FlushAlphaList</c>
|
/// Retail's four normal-world <c>D3DPolyRender::FlushAlphaList</c>
|
||||||
/// (@0x0059d2e0) call sites (OH1 contract §7), each confirmed 2026-09-04
|
/// (@0x0059d2e0) call sites (OH1 contract §7), each confirmed 2026-09-04
|
||||||
/// against <c>docs/research/named-retail/symbols.json</c> — the capture's
|
/// against <c>docs/research/named-retail/symbols.json</c> — the capture's
|
||||||
/// return address lies inside the named function's range:
|
/// return address lies inside the named function's range. S4-c2 fix round 1
|
||||||
|
/// (M8b): every address below is the CALL INSTRUCTION's own address, not a
|
||||||
|
/// "return site" (the earlier landing's mislabel) — x86 CALL rel32 is 5
|
||||||
|
/// bytes, so the actual return address is the call address + 5 (see
|
||||||
|
/// <c>WalkAlphaDepthTrace.cs</c>'s own "ret" doc comment for the same
|
||||||
|
/// distinction on the capture side).
|
||||||
/// <list type="bullet">
|
/// <list type="bullet">
|
||||||
/// <item><see cref="DrawBuilding"/> — <c>RenderDeviceD3D::DrawBuilding</c>
|
/// <item><see cref="DrawBuilding"/> — <c>RenderDeviceD3D::DrawBuilding</c>
|
||||||
/// @0x0059f2a0, return site 0x0059f30b, threshold 0f.</item>
|
/// @0x0059f2a0, call at 0x0059f30b (returns to 0x0059f310), threshold
|
||||||
|
/// 0f.</item>
|
||||||
/// <item><see cref="SortCellExit"/> — acdream's own name for
|
/// <item><see cref="SortCellExit"/> — acdream's own name for
|
||||||
/// <c>RenderDeviceD3D::DrawBlock</c> @0x005a18d0's per-land-cell
|
/// <c>RenderDeviceD3D::DrawBlock</c> @0x005a17c0's (per-land-cell loop head
|
||||||
/// <c>FlushAlphaList(::flush)</c>, return site 0x005a1a07, threshold 0.75f
|
/// @0x005a18d0) own <c>FlushAlphaList(::flush)</c>, call at 0x005a1a07
|
||||||
|
/// (returns to 0x005a1a0c), threshold 0.75f
|
||||||
/// (the immutable global <c>::flush</c>). There is no retail symbol to
|
/// (the immutable global <c>::flush</c>). There is no retail symbol to
|
||||||
/// borrow for the walk's own leaf-event name — the walk expresses this turn
|
/// borrow for the walk's own leaf-event name — the walk expresses this turn
|
||||||
/// as <c>OnSortCellTurn</c>/<c>OnLandscapeCellTurn</c>, not a "DrawBlock"
|
/// as <c>OnSortCellTurn</c>/<c>OnLandscapeCellTurn</c>, not a "DrawBlock"
|
||||||
/// hook.</item>
|
/// hook.</item>
|
||||||
/// <item><see cref="LandscapeFlush"/> — <c>PView::DrawCells</c> @0x005a4840,
|
/// <item><see cref="LandscapeFlush"/> — <c>PView::DrawCells</c> @0x005a4840,
|
||||||
/// return site 0x005a4872, threshold 0f.</item>
|
/// call at 0x005a4872 (returns to 0x005a4877), threshold 0f.</item>
|
||||||
/// <item><see cref="RenderNormalMode"/> — <c>SmartBox::RenderNormalMode</c>
|
/// <item><see cref="RenderNormalMode"/> — <c>SmartBox::RenderNormalMode</c>
|
||||||
/// @0x00453aa0, return site 0x00453b8b, threshold 0f.</item>
|
/// @0x00453aa0, call at 0x00453b8b (returns to 0x00453b90), threshold
|
||||||
|
/// 0f.</item>
|
||||||
/// </list>
|
/// </list>
|
||||||
/// A fifth retail caller (the private <c>CreatureMode::Render</c>
|
/// A fifth retail caller (the private <c>CreatureMode::Render</c>
|
||||||
/// @0x004529d0, return site 0x00452bf0) is out of the normal-world walk this
|
/// @0x004529d0, call at 0x00452bf0) is out of the normal-world walk this
|
||||||
/// enum covers (OH1 contract §7) and has no acdream call site.
|
/// enum covers (OH1 contract §7) and has no acdream call site.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal enum RetailAlphaFlushSite
|
internal enum RetailAlphaFlushSite
|
||||||
|
|
@ -84,20 +92,32 @@ internal interface IRetailAlphaDrawSource
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// One retail <c>AlphaListEntry</c>. <see cref="OverrideClipmap"/> mirrors
|
/// One retail <c>AlphaListEntry</c>. <see cref="OverrideClipmap"/> mirrors
|
||||||
/// retail's per-entry <c>clip</c> byte (<c>SetSurface</c>'s alpha-blend-vs-
|
/// retail's per-entry <c>clip</c> byte (<c>SetSurface</c>'s alpha-blend-vs-
|
||||||
/// clip-test arm selector); <see cref="IsFirstForList"/> mirrors retail's
|
/// clip-test arm selector).
|
||||||
/// per-entry <c>new</c> byte (true only for the first entry appended to its
|
///
|
||||||
/// list since the list was last drained — retail captures the current
|
/// <para>S4-c2 fix round 1 (M4): retail's per-entry <c>new</c>
|
||||||
/// material/object matrix only for that entry). Neither drives observable
|
/// ("first-for-this-list") byte is NOT modeled here. Retail's quantifier is
|
||||||
/// acdream behavior today: Vulkan's per-batch material/matrix binding makes
|
/// per-<c>DrawMesh</c>-INVOCATION: <c>DrawMesh</c> @0x0059d4a0 sets both
|
||||||
/// retail's per-entry material-capture optimization a no-op here (see
|
/// list flags true at ENTRY (<c>0059d4cc arg3 = 1; 0059d4d0 var_c = 1</c>)
|
||||||
/// <see cref="RetailAlphaQueue"/>'s class doc comment), but both are stored
|
/// and clears each independently after its OWN first append to that list
|
||||||
/// for fidelity and are directly test-observable.
|
/// WITHIN THAT CALL (<c>0059d5ef if (var_4_1 == 0) var_c = 0; else arg3 =
|
||||||
|
/// 0;</c>) — one retail subset can append to only one list once, so this
|
||||||
|
/// flag is trivially true for every subset retail ever appends; the
|
||||||
|
/// captures confirm it (10,556 <c>AM</c> lines, <c>new=1</c> on 9,685 of
|
||||||
|
/// them — impossible under "first since the last drain", which acdream's
|
||||||
|
/// deleted <c>isFirstForList = target.Count == 0</c> computed instead). One
|
||||||
|
/// acdream append already IS one full <c>DrawMesh</c> invocation (one
|
||||||
|
/// instance, one call), so the truthful port of an always-true flag with no
|
||||||
|
/// acdream reader (Vulkan's per-batch material/matrix binding makes
|
||||||
|
/// retail's per-entry material-capture optimization a no-op here — see
|
||||||
|
/// <see cref="RetailAlphaQueue"/>'s class doc comment) is to not carry it at
|
||||||
|
/// all, rather than keep a field whose value can never vary and whose only
|
||||||
|
/// possible test is therefore vacuous
|
||||||
|
/// (<c>feedback_every_new_pin_must_be_shown_to_fail</c>).</para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal readonly record struct RetailAlphaEntry(
|
internal readonly record struct RetailAlphaEntry(
|
||||||
IRetailAlphaDrawSource Source,
|
IRetailAlphaDrawSource Source,
|
||||||
int Token,
|
int Token,
|
||||||
bool OverrideClipmap,
|
bool OverrideClipmap);
|
||||||
bool IsFirstForList);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Frame-scoped port of retail's shared <c>D3DPolyRender</c> alpha lists.
|
/// Frame-scoped port of retail's shared <c>D3DPolyRender</c> alpha lists.
|
||||||
|
|
@ -199,9 +219,10 @@ internal sealed class RetailAlphaQueue : IWorldSceneAlphaFrame
|
||||||
/// at <see cref="ListCapacity"/>, return <see langword="false"/> and the
|
/// at <see cref="ListCapacity"/>, return <see langword="false"/> and the
|
||||||
/// subset is DROPPED — no recovery, no fallback draw (spec §5). The
|
/// subset is DROPPED — no recovery, no fallback draw (spec §5). The
|
||||||
/// caller (<see cref="RetailAlphaMeshRouter"/>'s decision) supplies
|
/// caller (<see cref="RetailAlphaMeshRouter"/>'s decision) supplies
|
||||||
/// <paramref name="overrideClipmap"/>; <c>IsFirstForList</c> is computed
|
/// <paramref name="overrideClipmap"/>. Retail's per-entry "first for
|
||||||
/// here from whether <paramref name="list"/> was empty before this
|
/// this list" flag is not modeled — see <see cref="RetailAlphaEntry"/>'s
|
||||||
/// append.
|
/// own doc comment (M4) for why it is trivially true for every subset
|
||||||
|
/// retail ever appends and therefore carries no information here.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal bool TryAppend(
|
internal bool TryAppend(
|
||||||
RetailAlphaList list,
|
RetailAlphaList list,
|
||||||
|
|
@ -219,8 +240,7 @@ internal sealed class RetailAlphaQueue : IWorldSceneAlphaFrame
|
||||||
if (target.Count >= ListCapacity)
|
if (target.Count >= ListCapacity)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
bool isFirstForList = target.Count == 0;
|
target.Add(new RetailAlphaEntry(source, token, overrideClipmap));
|
||||||
target.Add(new RetailAlphaEntry(source, token, overrideClipmap, isFirstForList));
|
|
||||||
RegisterSource(source);
|
RegisterSource(source);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -313,6 +333,11 @@ internal sealed class RetailAlphaQueue : IWorldSceneAlphaFrame
|
||||||
{
|
{
|
||||||
int observedClip = _clip.Count;
|
int observedClip = _clip.Count;
|
||||||
int observedAlpha = _alpha.Count;
|
int observedAlpha = _alpha.Count;
|
||||||
|
// S4-c2 fix round 1 (A4): the real SOURCE count, captured before
|
||||||
|
// Clear() — passing the entry count here (HEAD~1's regression)
|
||||||
|
// means _sources/_sourceDrawOffsets never shrink, since the
|
||||||
|
// entry count is always >= the source count.
|
||||||
|
int observedSources = _sources.Count;
|
||||||
for (int i = 0; i < _sources.Count; i++)
|
for (int i = 0; i < _sources.Count; i++)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|
@ -327,7 +352,7 @@ internal sealed class RetailAlphaQueue : IWorldSceneAlphaFrame
|
||||||
_sources.Clear();
|
_sources.Clear();
|
||||||
_clip.Clear();
|
_clip.Clear();
|
||||||
_alpha.Clear();
|
_alpha.Clear();
|
||||||
ApplyScratchRetention(observedClip + observedAlpha, observedClip + observedAlpha);
|
ApplyScratchRetention(observedClip + observedAlpha, observedSources);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (drawFailure is not null)
|
if (drawFailure is not null)
|
||||||
|
|
@ -401,11 +426,14 @@ internal sealed class RetailAlphaQueue : IWorldSceneAlphaFrame
|
||||||
{
|
{
|
||||||
int observedClip = _clip.Count;
|
int observedClip = _clip.Count;
|
||||||
int observedAlpha = _alpha.Count;
|
int observedAlpha = _alpha.Count;
|
||||||
|
// S4-c2 fix round 1 (A4): see DrainAndReset's own comment — the
|
||||||
|
// real source count, captured before Clear().
|
||||||
|
int observedSources = _sources.Count;
|
||||||
_sources.Clear();
|
_sources.Clear();
|
||||||
_clip.Clear();
|
_clip.Clear();
|
||||||
_alpha.Clear();
|
_alpha.Clear();
|
||||||
IsCollecting = false;
|
IsCollecting = false;
|
||||||
ApplyScratchRetention(observedClip + observedAlpha, observedClip + observedAlpha);
|
ApplyScratchRetention(observedClip + observedAlpha, observedSources);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (failures is { Count: > 0 })
|
if (failures is { Count: > 0 })
|
||||||
|
|
|
||||||
|
|
@ -204,7 +204,6 @@ internal sealed class WalkProductionLeafRenderer : IWalkFrameLeafRenderer
|
||||||
private Action _clearInteriorDepth = null!;
|
private Action _clearInteriorDepth = null!;
|
||||||
private Func<int> _drawExitSeals = null!;
|
private Func<int> _drawExitSeals = null!;
|
||||||
private readonly HashSet<uint> _singleCellScratch = new();
|
private readonly HashSet<uint> _singleCellScratch = new();
|
||||||
private readonly List<uint> _singleCellListScratch = new();
|
|
||||||
|
|
||||||
internal WalkProductionLeafRenderer(
|
internal WalkProductionLeafRenderer(
|
||||||
RetailPViewPassExecutor passes,
|
RetailPViewPassExecutor passes,
|
||||||
|
|
@ -261,9 +260,10 @@ internal sealed class WalkProductionLeafRenderer : IWalkFrameLeafRenderer
|
||||||
_passes.DrawOpaqueCellShells(_singleCellScratch);
|
_passes.DrawOpaqueCellShells(_singleCellScratch);
|
||||||
if (_passes.CellHasTransparentShell(cellId))
|
if (_passes.CellHasTransparentShell(cellId))
|
||||||
{
|
{
|
||||||
_singleCellListScratch.Clear();
|
// S4-c2 fix round 1 (M6): drawn now (with detail) or appended to
|
||||||
_singleCellListScratch.Add(cellId);
|
// the retail alpha queue for later replay (without detail) —
|
||||||
_passes.DrawTransparentCellShellsOrdered(_singleCellListScratch);
|
// see SubmitOrDrawTransparentCellShell's own doc comment.
|
||||||
|
_passes.SubmitOrDrawTransparentCellShell(cellId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -164,6 +164,106 @@ public RetailPViewPassExecutor(
|
||||||
public void DrawTransparentCellShellsOrdered(IReadOnlyList<uint> cellIds) =>
|
public void DrawTransparentCellShellsOrdered(IReadOnlyList<uint> cellIds) =>
|
||||||
_envCells.RenderTransparentOrdered(cellIds);
|
_envCells.RenderTransparentOrdered(cellIds);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// S4-c2 fix round 1 (M6, contract C4): retail <c>DrawEnvCell</c>
|
||||||
|
/// @0x0059f1c2 installs the environment detail surface (may be null)
|
||||||
|
/// before <c>DrawMesh</c> @0x0059f212 and clears it @0x0059f21a — detail
|
||||||
|
/// ON makes the subset take Row 1 (immediate, WITH detail —
|
||||||
|
/// <see cref="EnvCellRenderer.TransparentDetailEnabled"/>'s own doc
|
||||||
|
/// comment); detail OFF takes Row 3 (append; mask 0x02 alpha-family for
|
||||||
|
/// an EnvCell transparent shell surface). Routed through the SAME table
|
||||||
|
/// every other alpha submitter uses so the decision is the table's, not
|
||||||
|
/// a hand-coded branch. <see cref="EnvCellRenderer.RenderTransparentOrdered"/>
|
||||||
|
/// already replays a cell's transparent batch on demand, so the queue
|
||||||
|
/// side (<see cref="EnvCellAlphaDrawSource"/>) needs no new deferred-
|
||||||
|
/// replay abstraction — one token per CELL (coarser than retail's own
|
||||||
|
/// per-subset entries; AP-236's residual documents that granularity gap).
|
||||||
|
/// </summary>
|
||||||
|
internal void SubmitOrDrawTransparentCellShell(uint cellId)
|
||||||
|
{
|
||||||
|
RetailAlphaMeshDecision decision = RetailAlphaMeshRouter.Route(
|
||||||
|
currentlyDrawingSky: false,
|
||||||
|
delayMask: RetailAlphaMeshRouter.DefaultDelayMask,
|
||||||
|
detailSurfaceActive: _envCells.TransparentDetailEnabled,
|
||||||
|
multiPassAlpha: false,
|
||||||
|
subsetMask: RetailAlphaMeshRouter.MaskAlphaFamily,
|
||||||
|
materialHasAlpha: false);
|
||||||
|
|
||||||
|
if (decision.Action == RetailAlphaMeshAction.Immediate)
|
||||||
|
{
|
||||||
|
_singleCellListScratch.Clear();
|
||||||
|
_singleCellListScratch.Add(cellId);
|
||||||
|
_envCells.RenderTransparentOrdered(_singleCellListScratch);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Row 2 (AppendClipAndImmediate) needs MultiPassAlpha, fixed false
|
||||||
|
// above — provably unreachable, same reasoning as
|
||||||
|
// WbDrawDispatcher.SubmitToAlphaQueue's ordinary-content case.
|
||||||
|
_envCellAlphaSource ??= new EnvCellAlphaDrawSource(_envCells.RenderTransparentOrdered);
|
||||||
|
int token = _envCellAlphaSource.AddPendingCellId(cellId);
|
||||||
|
_alpha.TryAppend(decision.List, _envCellAlphaSource, token, decision.OverrideClipmap);
|
||||||
|
}
|
||||||
|
|
||||||
|
private EnvCellAlphaDrawSource? _envCellAlphaSource;
|
||||||
|
|
||||||
|
private readonly List<uint> _singleCellListScratch = new(1);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The queue-facing half of <see cref="SubmitOrDrawTransparentCellShell"/>:
|
||||||
|
/// retains the cell ids submitted this frame in append order and replays
|
||||||
|
/// a drained slice through <see cref="EnvCellRenderer.RenderTransparentOrdered"/>
|
||||||
|
/// (production wiring) — one token per cell, never per subset (M6's
|
||||||
|
/// documented granularity residual). <see cref="IRetailAlphaDrawSource.PrepareAlphaDraws"/>'s
|
||||||
|
/// "only adjacent same-source entries batch" invariant means a particle
|
||||||
|
/// or ordinary GfxObj instance appended BETWEEN two cell tokens keeps
|
||||||
|
/// its own position in the combined drain — this source never
|
||||||
|
/// re-orders across another entry. Depends on a delegate rather than
|
||||||
|
/// the concrete <see cref="EnvCellRenderer"/> type (marked
|
||||||
|
/// <see langword="internal"/>, not <see langword="private"/>) so
|
||||||
|
/// <c>EnvCellAlphaDrawSourceTests</c> can pin the queue mechanics
|
||||||
|
/// without standing up a GPU-backed renderer.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class EnvCellAlphaDrawSource(Action<IReadOnlyList<uint>> renderTransparentOrdered)
|
||||||
|
: IRetailAlphaDrawSource
|
||||||
|
{
|
||||||
|
private readonly List<uint> _pendingCellIds = new();
|
||||||
|
private readonly List<uint> _preparedCellIds = new();
|
||||||
|
private readonly List<uint> _drawScratch = new();
|
||||||
|
|
||||||
|
/// <summary>Returns this cell's token — its index into
|
||||||
|
/// <see cref="_pendingCellIds"/> at the moment it was added.</summary>
|
||||||
|
internal int AddPendingCellId(uint cellId)
|
||||||
|
{
|
||||||
|
int token = _pendingCellIds.Count;
|
||||||
|
_pendingCellIds.Add(cellId);
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void PrepareAlphaDraws(ReadOnlySpan<int> tokens)
|
||||||
|
{
|
||||||
|
_preparedCellIds.Clear();
|
||||||
|
for (int i = 0; i < tokens.Length; i++)
|
||||||
|
_preparedCellIds.Add(_pendingCellIds[tokens[i]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DrawPreparedAlphaBatch(int firstPreparedDraw, int drawCount)
|
||||||
|
{
|
||||||
|
if (drawCount <= 0)
|
||||||
|
return;
|
||||||
|
_drawScratch.Clear();
|
||||||
|
for (int i = 0; i < drawCount; i++)
|
||||||
|
_drawScratch.Add(_preparedCellIds[firstPreparedDraw + i]);
|
||||||
|
renderTransparentOrdered(_drawScratch);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResetAlphaSubmissions()
|
||||||
|
{
|
||||||
|
_pendingCellIds.Clear();
|
||||||
|
_preparedCellIds.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// S3 chunk 1 fix round 2 (§11.6 H1): retail draws the weather pass
|
/// S3 chunk 1 fix round 2 (§11.6 H1): retail draws the weather pass
|
||||||
/// EXACTLY ONCE per frame — <c>GameSky::Draw</c> @0x00506ff0 with
|
/// EXACTLY ONCE per frame — <c>GameSky::Draw</c> @0x00506ff0 with
|
||||||
|
|
@ -347,9 +447,10 @@ public RetailPViewPassExecutor(
|
||||||
internal void FlushBuildingAlpha() =>
|
internal void FlushBuildingAlpha() =>
|
||||||
_alpha.Flush(RetailAlphaFlushSite.DrawBuilding, 0f);
|
_alpha.Flush(RetailAlphaFlushSite.DrawBuilding, 0f);
|
||||||
|
|
||||||
/// <summary><c>RenderDeviceD3D::DrawBlock</c> @0x005a18d0's per-land-cell
|
/// <summary><c>RenderDeviceD3D::DrawBlock</c> @0x005a17c0's (per-land-cell
|
||||||
/// <c>FlushAlphaList(::flush)</c> @0x005a1a07, the immutable global
|
/// loop head @0x005a18d0) own <c>FlushAlphaList(::flush)</c> @0x005a1a07,
|
||||||
/// 0.75f pressure valve (OH1 contract §7 site 2).</summary>
|
/// the immutable global 0.75f pressure valve (OH1 contract §7 site
|
||||||
|
/// 2).</summary>
|
||||||
internal void FlushSortCellExitAlpha() =>
|
internal void FlushSortCellExitAlpha() =>
|
||||||
_alpha.Flush(RetailAlphaFlushSite.SortCellExit, 0.75f);
|
_alpha.Flush(RetailAlphaFlushSite.SortCellExit, 0.75f);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -473,16 +473,35 @@ internal sealed class RetailPViewRenderer
|
||||||
ClearWalkFrameBindings();
|
ClearWalkFrameBindings();
|
||||||
}
|
}
|
||||||
|
|
||||||
// OUTDOOR root: the LScape-boundary alpha drain deferred from the
|
// S4-c2 fix round 1 (A1): the call that used to sit HERE
|
||||||
// landscape stage runs HERE, after punches, interior shells, cell
|
// (`passes.FlushLandscapeAlpha()`, an outdoor-root-only drain
|
||||||
// objects, and the dynamics pass — the frame's complete opaque
|
// labelled LandscapeFlush) is DELETED — it was mislabelled.
|
||||||
// world. Retail's walk draws all of those before its boundary
|
// Retail's LandscapeFlush (`PView::DrawCells` @0x005a4840's own
|
||||||
// flush (LScape::draw includes every cell's objects,
|
// `FlushAlphaList(0f)` @0x005a4872) sits strictly INSIDE
|
||||||
// DrawSortCell 0x005A17C0), so this is the same one-list far→near
|
// `if (outside_view.view_count > 0)` on the INTERIOR PView
|
||||||
// composite over finished depth; draining at the stage end instead
|
// (WalkEvents.cs's own `OnInteriorFloodDrawTurn` doc comment);
|
||||||
// let every later opaque mesh overwrite the flames (#132).
|
// an outdoor root never reaches that branch at all — confirmed
|
||||||
if (ctx.RootCell.IsOutdoorNode)
|
// by the terrace-edge capture (zero `005a4877` FL lines) and by
|
||||||
passes.FlushLandscapeAlpha();
|
// `RetailAlphaQueue`'s own flush-site enum, whose
|
||||||
|
// `LandscapeFlush` member cites that exact address. Retail's
|
||||||
|
// real outdoor-root drains are `DrawBlock`'s per-cell 0.75 valve
|
||||||
|
// (`WalkFrameEventKind.SortCellExit`, already wired) and
|
||||||
|
// `SmartBox::RenderNormalMode`'s own `FlushAlphaList(0f)`
|
||||||
|
// @0x00453b8b at the pass end — which acdream already performs
|
||||||
|
// unconditionally in `RetailAlphaQueue.EndFrame()` (called from
|
||||||
|
// `WorldSceneRenderer.CompleteWorldFrame()`). Between this
|
||||||
|
// method's `return result;` below and that `EndFrame()` call,
|
||||||
|
// `WorldSceneRenderer.Render` performs zero further opaque-world
|
||||||
|
// draws for a PView-rooted frame — `DrawPostWorldParticles`'s
|
||||||
|
// own PView branch is an explicit no-op ("already drew inside
|
||||||
|
// LScape::draw... replaying here would be too late"), and the
|
||||||
|
// diagnostics that follow only emit collision wireframes (a
|
||||||
|
// debug-only probe, off by default) — so #132's "drain after
|
||||||
|
// the finished opaque world" invariant is kept by the
|
||||||
|
// RenderNormalMode flush alone, and `DrawUnattachedSceneParticles`
|
||||||
|
// below joins the SAME final drain retail's own outdoor root
|
||||||
|
// would produce (never an extra LandscapeFlush call retail
|
||||||
|
// never makes there).
|
||||||
|
|
||||||
// Interior-cell UNATTACHED emitters (spell ground effects and
|
// Interior-cell UNATTACHED emitters (spell ground effects and
|
||||||
// swirls anchored in EnvCells) draw in this final world scope —
|
// swirls anchored in EnvCells) draw in this final world scope —
|
||||||
|
|
|
||||||
|
|
@ -240,8 +240,9 @@ public sealed class RetailFrameWalk
|
||||||
block.SideCellCount,
|
block.SideCellCount,
|
||||||
cellIndex);
|
cellIndex);
|
||||||
|
|
||||||
// S4-c2: RenderDeviceD3D::DrawBlock @0x005a18d0's own
|
// S4-c2: RenderDeviceD3D::DrawBlock @0x005a17c0's (per-land-cell
|
||||||
// FlushAlphaList(::flush) @0x005a1a07 call, immediately after
|
// loop head @0x005a18d0) own FlushAlphaList(::flush)
|
||||||
|
// @0x005a1a07 call, immediately after
|
||||||
// DrawSortCell under the SAME alwaysDrawObjects||cellInView
|
// DrawSortCell under the SAME alwaysDrawObjects||cellInView
|
||||||
// gate that already wraps this whole loop body (the early
|
// gate that already wraps this whole loop body (the early
|
||||||
// `continue` above) — see OnSortCellExit's own doc comment.
|
// `continue` above) — see OnSortCellExit's own doc comment.
|
||||||
|
|
|
||||||
|
|
@ -155,8 +155,8 @@ public interface IWalkEventSink
|
||||||
/// that cell's <see cref="OnLandscapeCellTurn(uint,int,int)"/> turn (the
|
/// that cell's <see cref="OnLandscapeCellTurn(uint,int,int)"/> turn (the
|
||||||
/// object-list turn, itself after the building's own turn) and before
|
/// object-list turn, itself after the building's own turn) and before
|
||||||
/// the loop moves to the next cell's <see cref="OnLandCellTurn"/> —
|
/// the loop moves to the next cell's <see cref="OnLandCellTurn"/> —
|
||||||
/// <c>RenderDeviceD3D::DrawBlock</c> @0x005a18d0's own
|
/// <c>RenderDeviceD3D::DrawBlock</c> @0x005a17c0's (per-land-cell loop
|
||||||
/// <c>FlushAlphaList(::flush)</c> @0x005a1a07 call, gated by the exact
|
/// head @0x005a18d0) own <c>FlushAlphaList(::flush)</c> @0x005a1a07 call, gated by the exact
|
||||||
/// same <c>alwaysDrawObjects || cellInView</c> condition as
|
/// same <c>alwaysDrawObjects || cellInView</c> condition as
|
||||||
/// <c>DrawSortCell</c> itself (Ghidra-verified 2026-09-04: retail
|
/// <c>DrawSortCell</c> itself (Ghidra-verified 2026-09-04: retail
|
||||||
/// re-evaluates the identical condition for the flush call, immediately
|
/// re-evaluates the identical condition for the flush call, immediately
|
||||||
|
|
|
||||||
|
|
@ -238,8 +238,9 @@ internal interface IWalkFrameLeafRenderer
|
||||||
/// synthetic viewer-distance threshold.</summary>
|
/// synthetic viewer-distance threshold.</summary>
|
||||||
void AlphaBarrier();
|
void AlphaBarrier();
|
||||||
|
|
||||||
/// <summary>S4-c2: retail <c>RenderDeviceD3D::DrawBlock</c> @0x005a18d0's
|
/// <summary>S4-c2: retail <c>RenderDeviceD3D::DrawBlock</c> @0x005a17c0's
|
||||||
/// per-land-cell <c>FlushAlphaList(::flush)</c> @0x005a1a07 — the
|
/// (per-land-cell loop head @0x005a18d0) own <c>FlushAlphaList(::flush)</c>
|
||||||
|
/// @0x005a1a07 — the
|
||||||
/// immutable global 0.75f pressure valve, inert below 2250 entries in
|
/// immutable global 0.75f pressure valve, inert below 2250 entries in
|
||||||
/// either list (a no-op the overwhelming majority of the time at
|
/// either list (a no-op the overwhelming majority of the time at
|
||||||
/// today's scene complexity) but pinned exact by the capacity/threshold
|
/// today's scene complexity) but pinned exact by the capacity/threshold
|
||||||
|
|
@ -377,8 +378,8 @@ internal enum WalkFrameEventKind : byte
|
||||||
CellParticles,
|
CellParticles,
|
||||||
|
|
||||||
/// <summary>S4-c2: <see cref="IWalkFrameLeafRenderer.FlushSortCellExit"/> —
|
/// <summary>S4-c2: <see cref="IWalkFrameLeafRenderer.FlushSortCellExit"/> —
|
||||||
/// retail <c>RenderDeviceD3D::DrawBlock</c> @0x005a18d0's per-land-cell
|
/// retail <c>RenderDeviceD3D::DrawBlock</c> @0x005a17c0's (per-land-cell
|
||||||
/// <c>FlushAlphaList(0.75f)</c>.</summary>
|
/// loop head @0x005a18d0) own <c>FlushAlphaList(0.75f)</c>.</summary>
|
||||||
SortCellExit,
|
SortCellExit,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1475,8 +1476,9 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
||||||
_events.Add(WalkFrameEvent.LandscapeCellParticles(cellId));
|
_events.Add(WalkFrameEvent.LandscapeCellParticles(cellId));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>S4-c2: retail <c>RenderDeviceD3D::DrawBlock</c> @0x005a18d0's
|
/// <summary>S4-c2: retail <c>RenderDeviceD3D::DrawBlock</c> @0x005a17c0's
|
||||||
/// per-land-cell <c>FlushAlphaList(::flush)</c> @0x005a1a07 (the 0.75f
|
/// (per-land-cell loop head @0x005a18d0) own <c>FlushAlphaList(::flush)</c>
|
||||||
|
/// @0x005a1a07 (the 0.75f
|
||||||
/// pressure valve) — fires once per admitted land-block cell, at the SAME
|
/// pressure valve) — fires once per admitted land-block cell, at the SAME
|
||||||
/// gate <see cref="IWalkEventSink.OnLandscapeCellTurn(uint,int,int)"/>
|
/// gate <see cref="IWalkEventSink.OnLandscapeCellTurn(uint,int,int)"/>
|
||||||
/// already fires under (<c>RetailFrameWalk.DrawLandscape</c> calls this
|
/// already fires under (<c>RetailFrameWalk.DrawLandscape</c> calls this
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,20 @@ public sealed unsafe partial class EnvCellRenderer
|
||||||
private readonly TerrainAtlas.RetailDetailTextureBinding _environmentDetail;
|
private readonly TerrainAtlas.RetailDetailTextureBinding _environmentDetail;
|
||||||
private readonly Func<bool> _buildingDetailEnabled;
|
private readonly Func<bool> _buildingDetailEnabled;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// S4-c2 fix round 1 (M6): the SAME <c>detailEnabled</c> test the
|
||||||
|
/// transparent-shell MDI path (line ~254 below) already evaluates at
|
||||||
|
/// draw time — retail <c>DrawEnvCell</c> @0x0059f1c2 installs the
|
||||||
|
/// environment detail surface (may be null) before <c>DrawMesh</c>
|
||||||
|
/// @0x0059f212 and clears it @0x0059f21a, so a non-null environment
|
||||||
|
/// detail is what makes an eligible transparent subset take Row 1
|
||||||
|
/// (immediate). <see cref="RetailPViewPassExecutor"/> reads this to
|
||||||
|
/// decide whether a cell's transparent shell draws now or is appended
|
||||||
|
/// to the retail alpha queue for later replay.
|
||||||
|
/// </summary>
|
||||||
|
internal bool TransparentDetailEnabled =>
|
||||||
|
RetailDetailTextureContract.ShouldRender(_buildingDetailEnabled(), _environmentDetail);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The RHI arm's constructor. It also completes <c>Initialize</c>'s job: the
|
/// The RHI arm's constructor. It also completes <c>Initialize</c>'s job: the
|
||||||
/// five pipelines ARE this renderer's program, so there is no second step
|
/// five pipelines ARE this renderer's program, so there is no second step
|
||||||
|
|
|
||||||
|
|
@ -587,21 +587,25 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
checked((uint)(count * DrawCommandStride)));
|
checked((uint)(count * DrawCommandStride)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawPreparedAlphaBatchRhi(
|
/// <summary>
|
||||||
|
/// The push-constant/pipeline/storage-binding setup <see cref="DrawPreparedAlphaBatchRhi"/>'s
|
||||||
|
/// drain and <see cref="DrawImmediateAlphaInstanceRhi"/>'s single-
|
||||||
|
/// instance immediate draw (S4-c2 fix round 1, M3) share — the only
|
||||||
|
/// difference between the two call sites is <paramref name="viewProjection"/>
|
||||||
|
/// (the drain reads the deferred scope's own <c>_deferredAlphaViewProjection</c>;
|
||||||
|
/// an immediate draw uses whatever the CURRENT frame's viewProjection
|
||||||
|
/// is) and whether a building-detail overlay pass follows.
|
||||||
|
/// </summary>
|
||||||
|
private GpuPushConstants BindAlphaDrawState(
|
||||||
|
IGpuPassEncoder encoder,
|
||||||
|
IGpuFrame frame,
|
||||||
GlobalMeshBuffer mesh,
|
GlobalMeshBuffer mesh,
|
||||||
int firstPreparedDraw,
|
Matrix4x4 viewProjection,
|
||||||
int drawCount)
|
out MeshPipelineSet pipelines)
|
||||||
{
|
{
|
||||||
if (_alphaCommands.Buffer is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
IWorldPassScope scope = _scope!;
|
|
||||||
IGpuPassEncoder encoder = scope.RequireEncoder();
|
|
||||||
IGpuFrame frame = RequireRhiFrame();
|
|
||||||
|
|
||||||
var pushConstants = new GpuPushConstants
|
var pushConstants = new GpuPushConstants
|
||||||
{
|
{
|
||||||
ViewProjection = _deferredAlphaViewProjection,
|
ViewProjection = viewProjection,
|
||||||
DrawIdOffset = 0,
|
DrawIdOffset = 0,
|
||||||
LightingMode = 0,
|
LightingMode = 0,
|
||||||
RenderPass = 1,
|
RenderPass = 1,
|
||||||
|
|
@ -612,7 +616,7 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
ParamB = 0f,
|
ParamB = 0f,
|
||||||
};
|
};
|
||||||
|
|
||||||
MeshPipelineSet pipelines = PipelinesFor(
|
pipelines = PipelinesFor(
|
||||||
encoder,
|
encoder,
|
||||||
frame,
|
frame,
|
||||||
out DirectionalShadowFrameBinding shadowBinding);
|
out DirectionalShadowFrameBinding shadowBinding);
|
||||||
|
|
@ -635,13 +639,34 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
GpuBindingModel.StorageInstanceDetailCategory,
|
GpuBindingModel.StorageInstanceDetailCategory,
|
||||||
_alphaDetailCategory);
|
_alphaDetailCategory);
|
||||||
AcDream.App.Rendering.WorldFrameSectionBinding.BindClipRegions(
|
AcDream.App.Rendering.WorldFrameSectionBinding.BindClipRegions(
|
||||||
encoder, scope.Sections, frame);
|
encoder, _scope!.Sections, frame);
|
||||||
AcDream.App.Rendering.WorldFrameSectionBinding.BindSceneLighting(
|
AcDream.App.Rendering.WorldFrameSectionBinding.BindSceneLighting(
|
||||||
encoder, scope.Sections, frame);
|
encoder, _scope!.Sections, frame);
|
||||||
|
return pushConstants;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// S4-c2 fix round 1 (M3c): delayed replay is ALWAYS <c>detailEnabled=0</c>
|
||||||
|
/// (OH1 contract §4/§5 — <c>FlushAlphaList</c>'s replay calls
|
||||||
|
/// <c>RenderMeshSubset(..., 0, entry.clip, ...)</c>) because every
|
||||||
|
/// detail-eligible subset now takes Row 1 immediate instead
|
||||||
|
/// (<c>SubmitToAlphaQueue</c>'s <c>detailSurfaceActive</c> gate) and
|
||||||
|
/// never reaches this drain at all — so this loop batches purely by
|
||||||
|
/// blend kind, with no per-command detail-category branch left.
|
||||||
|
/// </summary>
|
||||||
|
private void DrawPreparedAlphaBatchRhi(
|
||||||
|
GlobalMeshBuffer mesh,
|
||||||
|
int firstPreparedDraw,
|
||||||
|
int drawCount)
|
||||||
|
{
|
||||||
|
if (_alphaCommands.Buffer is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
IGpuPassEncoder encoder = _scope!.RequireEncoder();
|
||||||
|
IGpuFrame frame = RequireRhiFrame();
|
||||||
|
GpuPushConstants pushConstants = BindAlphaDrawState(
|
||||||
|
encoder, frame, mesh, _deferredAlphaViewProjection, out MeshPipelineSet pipelines);
|
||||||
|
|
||||||
bool detailEnabled = RetailDetailTextureContract.ShouldRender(
|
|
||||||
_buildingDetailEnabled(),
|
|
||||||
_buildingDetail);
|
|
||||||
if (firstPreparedDraw < 0
|
if (firstPreparedDraw < 0
|
||||||
|| drawCount < 0
|
|| drawCount < 0
|
||||||
|| firstPreparedDraw > _preparedAlphaInstanceCount - drawCount)
|
|| firstPreparedDraw > _preparedAlphaInstanceCount - drawCount)
|
||||||
|
|
@ -650,25 +675,14 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
nameof(firstPreparedDraw),
|
nameof(firstPreparedDraw),
|
||||||
"The prepared-alpha draw range exceeds its uploaded instance/category payload.");
|
"The prepared-alpha draw range exceeds its uploaded instance/category payload.");
|
||||||
}
|
}
|
||||||
ReadOnlySpan<uint> usedDetailCategories =
|
|
||||||
_detailCategoryData.AsSpan(0, _preparedAlphaInstanceCount);
|
|
||||||
int runStart = firstPreparedDraw;
|
int runStart = firstPreparedDraw;
|
||||||
int preparedEnd = firstPreparedDraw + drawCount;
|
int preparedEnd = firstPreparedDraw + drawCount;
|
||||||
while (runStart < preparedEnd)
|
while (runStart < preparedEnd)
|
||||||
{
|
{
|
||||||
TranslucencyKind blend = _deferredAlphaKinds[runStart];
|
TranslucencyKind blend = _deferredAlphaKinds[runStart];
|
||||||
bool hasDetail = detailEnabled
|
|
||||||
&& CommandContainsDetailCategory(
|
|
||||||
_indirectCommands[runStart],
|
|
||||||
usedDetailCategories);
|
|
||||||
int runEnd = runStart + 1;
|
int runEnd = runStart + 1;
|
||||||
while (runEnd < preparedEnd
|
while (runEnd < preparedEnd && _deferredAlphaKinds[runEnd] == blend)
|
||||||
&& !hasDetail
|
|
||||||
&& _deferredAlphaKinds[runEnd] == blend
|
|
||||||
&& (!detailEnabled
|
|
||||||
|| !CommandContainsDetailCategory(
|
|
||||||
_indirectCommands[runEnd],
|
|
||||||
usedDetailCategories)))
|
|
||||||
runEnd++;
|
runEnd++;
|
||||||
|
|
||||||
// ApplyRetailBlend's three cases are three pipelines, including the
|
// ApplyRetailBlend's three cases are three pipelines, including the
|
||||||
|
|
@ -682,23 +696,55 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
_alphaCommands.OffsetBytes,
|
_alphaCommands.OffsetBytes,
|
||||||
runStart,
|
runStart,
|
||||||
runEnd - runStart);
|
runEnd - runStart);
|
||||||
|
|
||||||
if (hasDetail)
|
|
||||||
{
|
|
||||||
DrawBuildingDetailRangeRhi(
|
|
||||||
encoder,
|
|
||||||
mesh,
|
|
||||||
pipelines.RetailDetailTransparent,
|
|
||||||
ref pushConstants,
|
|
||||||
_alphaCommands.Buffer!,
|
|
||||||
_alphaCommands.OffsetBytes,
|
|
||||||
runStart,
|
|
||||||
1);
|
|
||||||
}
|
|
||||||
runStart = runEnd;
|
runStart = runEnd;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// S4-c2 fix round 1 (M3b): <c>DrawMesh</c> row 1's "render immediately;
|
||||||
|
/// enqueue nothing" outcome for ONE translucent building-shell instance
|
||||||
|
/// under building detail — bind the blend pipeline, one indirect draw,
|
||||||
|
/// then the detail pass, reusing <see cref="BindAlphaDrawState"/> and
|
||||||
|
/// <see cref="DrawBuildingDetailRangeRhi"/> (the same code
|
||||||
|
/// <see cref="DrawPreparedAlphaBatchRhi"/> used to use) for the single
|
||||||
|
/// slot <see cref="WbDrawDispatcher.DrawImmediateAlphaInstance"/> just
|
||||||
|
/// prepared. The caller only reaches this method when
|
||||||
|
/// <c>detailSurfaceActive</c> was true, so the detail pass always
|
||||||
|
/// applies here — the drain's own per-command detail CHECK is gone
|
||||||
|
/// (M3c) precisely because detail-eligible content no longer reaches it.
|
||||||
|
/// </summary>
|
||||||
|
private void DrawImmediateAlphaInstanceRhi(
|
||||||
|
GlobalMeshBuffer mesh, TranslucencyKind blend, Matrix4x4 viewProjection)
|
||||||
|
{
|
||||||
|
if (_alphaCommands.Buffer is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
IGpuPassEncoder encoder = _scope!.RequireEncoder();
|
||||||
|
IGpuFrame frame = RequireRhiFrame();
|
||||||
|
GpuPushConstants pushConstants = BindAlphaDrawState(
|
||||||
|
encoder, frame, mesh, viewProjection, out MeshPipelineSet pipelines);
|
||||||
|
|
||||||
|
BindPipelineWithMesh(encoder, PipelineForBlend(pipelines, blend), mesh);
|
||||||
|
encoder.SetPushConstants(in pushConstants);
|
||||||
|
DrawIndirectRangeRhi(
|
||||||
|
encoder,
|
||||||
|
ref pushConstants,
|
||||||
|
_alphaCommands.Buffer!,
|
||||||
|
_alphaCommands.OffsetBytes,
|
||||||
|
startCommand: 0,
|
||||||
|
commandCount: 1);
|
||||||
|
|
||||||
|
DrawBuildingDetailRangeRhi(
|
||||||
|
encoder,
|
||||||
|
mesh,
|
||||||
|
pipelines.RetailDetailTransparent,
|
||||||
|
ref pushConstants,
|
||||||
|
_alphaCommands.Buffer!,
|
||||||
|
_alphaCommands.OffsetBytes,
|
||||||
|
firstCommand: 0,
|
||||||
|
commandCount: 1);
|
||||||
|
}
|
||||||
|
|
||||||
private void DrawImmediateTransparentRhi(
|
private void DrawImmediateTransparentRhi(
|
||||||
IGpuPassEncoder encoder,
|
IGpuPassEncoder encoder,
|
||||||
GlobalMeshBuffer mesh,
|
GlobalMeshBuffer mesh,
|
||||||
|
|
|
||||||
|
|
@ -903,10 +903,10 @@ public sealed partial class WbDrawDispatcher
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
"One retail alpha scope cannot combine different view-projection matrices.");
|
"One retail alpha scope cannot combine different view-projection matrices.");
|
||||||
|
|
||||||
int token = _deferredAlpha.Count;
|
var candidate = new DeferredAlphaInstance(
|
||||||
_deferredAlpha.Add(new DeferredAlphaInstance(
|
|
||||||
batch.Key, batch.Transform, batch.ClipSlot, batch.Lights,
|
batch.Key, batch.Transform, batch.ClipSlot, batch.Lights,
|
||||||
batch.IndoorFlag, batch.DetailCategory, batch.Alpha, batch.SelectionLighting));
|
batch.IndoorFlag, batch.DetailCategory, batch.Alpha, batch.SelectionLighting);
|
||||||
SubmitToAlphaQueue(queue, batch.Key.Translucency, token);
|
SubmitToAlphaQueue(
|
||||||
|
queue, batch.Key.Translucency, in candidate, batch.DetailCategory == 1u, viewProjection);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2445,8 +2445,7 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
||||||
{
|
{
|
||||||
InstanceGroup group = entry.Group;
|
InstanceGroup group = entry.Group;
|
||||||
int i = entry.InstanceIndex;
|
int i = entry.InstanceIndex;
|
||||||
int token = _deferredAlpha.Count;
|
var candidate = new DeferredAlphaInstance(
|
||||||
_deferredAlpha.Add(new DeferredAlphaInstance(
|
|
||||||
ToKey(group),
|
ToKey(group),
|
||||||
group.Matrices[i],
|
group.Matrices[i],
|
||||||
group.Slots[i],
|
group.Slots[i],
|
||||||
|
|
@ -2454,8 +2453,9 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
||||||
group.IndoorFlags[i],
|
group.IndoorFlags[i],
|
||||||
group.DetailCategories[i],
|
group.DetailCategories[i],
|
||||||
group.Opacities[i],
|
group.Opacities[i],
|
||||||
group.SelectionLighting[i]));
|
group.SelectionLighting[i]);
|
||||||
SubmitToAlphaQueue(queue, group.Translucency, token);
|
SubmitToAlphaQueue(
|
||||||
|
queue, group.Translucency, in candidate, group.DetailCategories[i] == 1u, viewProjection);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2466,40 +2466,71 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
||||||
/// clip-map subsets are filtered out upstream by <c>IsOpaque</c> and
|
/// clip-map subsets are filtered out upstream by <c>IsOpaque</c> and
|
||||||
/// never reach <c>_translucentDraws</c>/this path at all — see
|
/// never reach <c>_translucentDraws</c>/this path at all — see
|
||||||
/// <see cref="RetailAlphaMeshRouter.MaskFromTranslucencyKind"/>'s own doc
|
/// <see cref="RetailAlphaMeshRouter.MaskFromTranslucencyKind"/>'s own doc
|
||||||
/// comment). Neither call site ever draws during the Sky leaf or with a
|
/// comment). Neither call site ever draws during the Sky leaf and
|
||||||
/// building/environment detail surface installed (ordinary GfxObj/
|
|
||||||
/// particle paths never install one — OH1 contract §6), and
|
|
||||||
/// <c>MultiPassAlpha</c> stays false (no environment override — Must
|
/// <c>MultiPassAlpha</c> stays false (no environment override — Must
|
||||||
/// Not). Under those fixed inputs and <see cref="RetailAlphaMeshRouter.DefaultDelayMask"/>,
|
/// Not) — Row 2 is provably unreachable. Under
|
||||||
/// every reachable mask (0x02/0x03, 0x04/0x05, 0x08/0x09) intersects the
|
/// <see cref="RetailAlphaMeshRouter.DefaultDelayMask"/> the only
|
||||||
/// delay mask, so <c>DrawMesh</c>'s row 3 always fires — rows 1, 2, 4,
|
/// reachable mask here is 0x02 (alpha-family), which always intersects
|
||||||
/// and 5 are unreachable here and would indicate a real routing bug if
|
/// the delay mask, so an ordinary (non-building-shell) instance always
|
||||||
/// ever hit.
|
/// takes Row 3 (Append).
|
||||||
|
///
|
||||||
|
/// <para>S4-c2 fix round 1 (M3): <paramref name="isBuildingShell"/> feeds
|
||||||
|
/// Row 1's <c>curr_detail_surface != null</c> gate — retail's
|
||||||
|
/// <c>DrawBuilding</c> @0x0059f2a0 installs the building detail surface
|
||||||
|
/// BEFORE its own shell subsets reach <c>DrawMesh</c>
|
||||||
|
/// (<c>0059f2eb curr_detail_surface = building_detail_surface</c>), so a
|
||||||
|
/// translucent building-shell instance takes Row 1 (immediate, WITH
|
||||||
|
/// detail) whenever building detail is on. This IS therefore a real,
|
||||||
|
/// data-driven outcome — not an invariant to throw on
|
||||||
|
/// (feedback_retail_dispatch_is_data_driven) — handled below by drawing
|
||||||
|
/// the instance immediately instead of appending it.</para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void SubmitToAlphaQueue(
|
private void SubmitToAlphaQueue(
|
||||||
RetailAlphaQueue queue, TranslucencyKind kind, int token)
|
RetailAlphaQueue queue,
|
||||||
|
TranslucencyKind kind,
|
||||||
|
in DeferredAlphaInstance candidate,
|
||||||
|
bool isBuildingShell,
|
||||||
|
Matrix4x4 viewProjection)
|
||||||
{
|
{
|
||||||
byte mask = RetailAlphaMeshRouter.MaskFromTranslucencyKind(kind);
|
byte mask = RetailAlphaMeshRouter.MaskFromTranslucencyKind(kind);
|
||||||
|
bool detailSurfaceActive = isBuildingShell
|
||||||
|
&& RetailDetailTextureContract.ShouldRender(_buildingDetailEnabled(), _buildingDetail);
|
||||||
RetailAlphaMeshDecision decision = RetailAlphaMeshRouter.Route(
|
RetailAlphaMeshDecision decision = RetailAlphaMeshRouter.Route(
|
||||||
currentlyDrawingSky: false,
|
currentlyDrawingSky: false,
|
||||||
delayMask: RetailAlphaMeshRouter.DefaultDelayMask,
|
delayMask: RetailAlphaMeshRouter.DefaultDelayMask,
|
||||||
detailSurfaceActive: false,
|
detailSurfaceActive: detailSurfaceActive,
|
||||||
multiPassAlpha: false,
|
multiPassAlpha: false,
|
||||||
subsetMask: mask,
|
subsetMask: mask,
|
||||||
materialHasAlpha: false);
|
materialHasAlpha: false);
|
||||||
|
|
||||||
if (decision.Action != RetailAlphaMeshAction.Append)
|
if (decision.Action == RetailAlphaMeshAction.Immediate)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
// M3(b): row 1 — draw NOW, at this instance's own turn, and
|
||||||
"Ordinary translucent GfxObj/particle submissions never install a detail "
|
// never touch _deferredAlpha/the queue at all (so a frame whose
|
||||||
+ "surface or draw during the Sky leaf, and MultiPassAlpha stays false — "
|
// translucent content is ENTIRELY immediate — e.g. every visible
|
||||||
+ $"DrawMesh's row 1/2/4/5 branches are unreachable here; got {decision.Action}.");
|
// shell drawing with building detail on — still leaves nothing
|
||||||
|
// for the queue to clear; only Append/AppendClipAndImmediate,
|
||||||
|
// below, ever grow _deferredAlpha).
|
||||||
|
DrawImmediateAlphaInstance(in candidate, viewProjection);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int token = _deferredAlpha.Count;
|
||||||
|
_deferredAlpha.Add(candidate);
|
||||||
// Capacity overflow (spec §5): TryAppend returns false and the
|
// Capacity overflow (spec §5): TryAppend returns false and the
|
||||||
// subset is DROPPED. The reserved _deferredAlpha token simply never
|
// subset is DROPPED. The reserved _deferredAlpha token simply never
|
||||||
// gets prepared/drawn — no recovery, matching retail exactly.
|
// gets prepared/drawn — no recovery, matching retail exactly.
|
||||||
queue.TryAppend(decision.List, _alphaSource, token, decision.OverrideClipmap);
|
queue.TryAppend(decision.List, _alphaSource, token, decision.OverrideClipmap);
|
||||||
|
|
||||||
|
if (decision.Action == RetailAlphaMeshAction.AppendClipAndImmediate)
|
||||||
|
{
|
||||||
|
// Row 2: provably unreachable here (MultiPassAlpha stays false),
|
||||||
|
// but handled in full rather than asserted against — exactly as
|
||||||
|
// spec §4 row 2 describes: append to CLIP AND ALSO draw
|
||||||
|
// immediately.
|
||||||
|
DrawImmediateAlphaInstance(in candidate, viewProjection);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class AlphaSubmissionOrderComparer :
|
private sealed class AlphaSubmissionOrderComparer :
|
||||||
|
|
@ -2518,6 +2549,59 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
||||||
left.SubmissionOrder.CompareTo(right.SubmissionOrder);
|
left.SubmissionOrder.CompareTo(right.SubmissionOrder);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Writes ONE <see cref="DeferredAlphaInstance"/> into the CPU staging
|
||||||
|
/// arrays at <paramref name="slot"/> — the per-entry body
|
||||||
|
/// <see cref="PrepareDeferredAlphaDraws"/>'s drain loop and
|
||||||
|
/// <see cref="DrawImmediateAlphaInstance"/>'s single-instance immediate
|
||||||
|
/// path (S4-c2 fix round 1, M3) both share, so the two paths can never
|
||||||
|
/// disagree on how a <see cref="DeferredAlphaInstance"/> becomes GPU
|
||||||
|
/// data. Caller ensures capacity first.
|
||||||
|
/// </summary>
|
||||||
|
private void WriteDeferredAlphaEntrySlot(int slot, in DeferredAlphaInstance entry)
|
||||||
|
{
|
||||||
|
WriteMatrix(_instanceData, slot * 16, entry.Model);
|
||||||
|
_clipSlotData[slot] = entry.ClipSlot;
|
||||||
|
_indoorData[slot] = entry.Indoor;
|
||||||
|
_detailCategoryData[slot] = entry.DetailCategory;
|
||||||
|
_alphaData[slot] = entry.Opacity;
|
||||||
|
_selectionLightingData[slot] = entry.SelectionLighting;
|
||||||
|
int lightOffset = slot * LightManager.MaxLightsPerObject;
|
||||||
|
entry.Lights.CopyTo(_lightSetData, lightOffset);
|
||||||
|
|
||||||
|
GroupKey key = entry.Key;
|
||||||
|
_batchData[slot] = new BatchData
|
||||||
|
{
|
||||||
|
// Campaign V slice V2: table slot, not the raw handle.
|
||||||
|
TextureIndex = key.TextureSlot.Index,
|
||||||
|
TextureLayer = key.TextureLayer,
|
||||||
|
// DrawMesh invokes RenderMeshSubset with detail enabled for
|
||||||
|
// every built-mesh material subset while curr_detail_surface
|
||||||
|
// is installed. The per-instance category still rejects
|
||||||
|
// ordinary objects in mesh_detail. Review fix round 2 (F2):
|
||||||
|
// bit 0 (the #226 built-mesh marker) is unconditional here —
|
||||||
|
// every delayed-alpha entry IS a built-mesh alpha submission
|
||||||
|
// — but the previous hardcoded `1` silently dropped bits 1/2
|
||||||
|
// (Campaign VM VM6 foliage classification). A trunk instance
|
||||||
|
// promoted into the alpha-blend group mid-fade (translucency
|
||||||
|
// < 1 forces AlphaBlend even for an otherwise-Opaque trunk —
|
||||||
|
// see ClassifyBatches's #188 promotion) must keep swaying
|
||||||
|
// through the deferred-alpha replay instead of going rigid
|
||||||
|
// for the duration of its fade.
|
||||||
|
Flags = 1u | key.FoliageFlags,
|
||||||
|
};
|
||||||
|
_indirectCommands[slot] = new DrawElementsIndirectCommand
|
||||||
|
{
|
||||||
|
Count = (uint)key.IndexCount,
|
||||||
|
InstanceCount = 1,
|
||||||
|
FirstIndex = key.FirstIndex,
|
||||||
|
BaseVertex = key.BaseVertex,
|
||||||
|
BaseInstance = (uint)slot,
|
||||||
|
};
|
||||||
|
_drawCullModes[slot] = key.CullMode;
|
||||||
|
_deferredAlphaKinds[slot] = key.Translucency;
|
||||||
|
}
|
||||||
|
|
||||||
private void PrepareDeferredAlphaDraws(ReadOnlySpan<int> tokens)
|
private void PrepareDeferredAlphaDraws(ReadOnlySpan<int> tokens)
|
||||||
{
|
{
|
||||||
if (tokens.Length == 0)
|
if (tokens.Length == 0)
|
||||||
|
|
@ -2530,49 +2614,7 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
||||||
int count = tokens.Length;
|
int count = tokens.Length;
|
||||||
EnsureDeferredAlphaCapacity(count);
|
EnsureDeferredAlphaCapacity(count);
|
||||||
for (int i = 0; i < count; i++)
|
for (int i = 0; i < count; i++)
|
||||||
{
|
WriteDeferredAlphaEntrySlot(i, _deferredAlpha[tokens[i]]);
|
||||||
DeferredAlphaInstance entry = _deferredAlpha[tokens[i]];
|
|
||||||
WriteMatrix(_instanceData, i * 16, entry.Model);
|
|
||||||
_clipSlotData[i] = entry.ClipSlot;
|
|
||||||
_indoorData[i] = entry.Indoor;
|
|
||||||
_detailCategoryData[i] = entry.DetailCategory;
|
|
||||||
_alphaData[i] = entry.Opacity;
|
|
||||||
_selectionLightingData[i] = entry.SelectionLighting;
|
|
||||||
int lightOffset = i * LightManager.MaxLightsPerObject;
|
|
||||||
entry.Lights.CopyTo(_lightSetData, lightOffset);
|
|
||||||
|
|
||||||
GroupKey key = entry.Key;
|
|
||||||
_batchData[i] = new BatchData
|
|
||||||
{
|
|
||||||
// Campaign V slice V2: table slot, not the raw handle.
|
|
||||||
TextureIndex = key.TextureSlot.Index,
|
|
||||||
TextureLayer = key.TextureLayer,
|
|
||||||
// DrawMesh invokes RenderMeshSubset with detail enabled for
|
|
||||||
// every built-mesh material subset while curr_detail_surface
|
|
||||||
// is installed. The per-instance category still rejects
|
|
||||||
// ordinary objects in mesh_detail. Review fix round 2 (F2):
|
|
||||||
// bit 0 (the #226 built-mesh marker) is unconditional here —
|
|
||||||
// every delayed-alpha entry IS a built-mesh alpha submission
|
|
||||||
// — but the previous hardcoded `1` silently dropped bits 1/2
|
|
||||||
// (Campaign VM VM6 foliage classification). A trunk instance
|
|
||||||
// promoted into the alpha-blend group mid-fade (translucency
|
|
||||||
// < 1 forces AlphaBlend even for an otherwise-Opaque trunk —
|
|
||||||
// see ClassifyBatches's #188 promotion) must keep swaying
|
|
||||||
// through the deferred-alpha replay instead of going rigid
|
|
||||||
// for the duration of its fade.
|
|
||||||
Flags = 1u | key.FoliageFlags,
|
|
||||||
};
|
|
||||||
_indirectCommands[i] = new DrawElementsIndirectCommand
|
|
||||||
{
|
|
||||||
Count = (uint)key.IndexCount,
|
|
||||||
InstanceCount = 1,
|
|
||||||
FirstIndex = key.FirstIndex,
|
|
||||||
BaseVertex = key.BaseVertex,
|
|
||||||
BaseInstance = (uint)i,
|
|
||||||
};
|
|
||||||
_drawCullModes[i] = key.CullMode;
|
|
||||||
_deferredAlphaKinds[i] = key.Translucency;
|
|
||||||
}
|
|
||||||
|
|
||||||
// One upload per source per sorted alpha scope. RetailAlphaQueue later
|
// One upload per source per sorted alpha scope. RetailAlphaQueue later
|
||||||
// draws contiguous ranges from this immutable prepared payload; it must
|
// draws contiguous ranges from this immutable prepared payload; it must
|
||||||
|
|
@ -2584,6 +2626,34 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
||||||
PrepareRhiAlphaSections(count);
|
PrepareRhiAlphaSections(count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// S4-c2 fix round 1 (M3b): <c>DrawMesh</c> row 1's "render immediately;
|
||||||
|
/// enqueue nothing" outcome for ONE translucent building-shell instance
|
||||||
|
/// — retail draws the subset in place inside the mesh; the walk's
|
||||||
|
/// opaque instances are stream-batched, so "in place" becomes "at the
|
||||||
|
/// entity's own stream mark" (a register row records this granularity
|
||||||
|
/// adaptation — see <c>SubmitToAlphaQueue</c>'s caller). Reuses
|
||||||
|
/// <see cref="WriteDeferredAlphaEntrySlot"/> and
|
||||||
|
/// <see cref="PrepareRhiAlphaSections"/> for slot 0, then
|
||||||
|
/// <see cref="DrawImmediateAlphaInstanceRhi"/> for the actual pipeline/
|
||||||
|
/// detail draw. Never touches <c>_deferredAlpha</c> or the queue — this
|
||||||
|
/// instance never enters a list — and never overlaps a real queue drain
|
||||||
|
/// in time (a drain only replays previously-APPENDED tokens at a LATER
|
||||||
|
/// flush site; this call happens at the instance's own submission,
|
||||||
|
/// strictly before any such flush).
|
||||||
|
/// </summary>
|
||||||
|
private void DrawImmediateAlphaInstance(in DeferredAlphaInstance entry, Matrix4x4 viewProjection)
|
||||||
|
{
|
||||||
|
GlobalMeshBuffer? global = _meshAdapter.MeshManager?.GlobalBuffer;
|
||||||
|
if (global is null || !MeshSourceReady())
|
||||||
|
return;
|
||||||
|
|
||||||
|
EnsureDeferredAlphaCapacity(1);
|
||||||
|
WriteDeferredAlphaEntrySlot(0, in entry);
|
||||||
|
PrepareRhiAlphaSections(1);
|
||||||
|
DrawImmediateAlphaInstanceRhi(global, entry.Key.Translucency, viewProjection);
|
||||||
|
}
|
||||||
|
|
||||||
private void DrawPreparedAlphaBatch(int firstPreparedDraw, int drawCount)
|
private void DrawPreparedAlphaBatch(int firstPreparedDraw, int drawCount)
|
||||||
{
|
{
|
||||||
if (drawCount <= 0)
|
if (drawCount <= 0)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
using AcDream.App.Rendering;
|
||||||
|
|
||||||
|
namespace AcDream.App.Tests.Rendering;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// S4-c2 fix round 1 (M6, contract C4): <see cref="RetailPViewPassExecutor.EnvCellAlphaDrawSource"/>
|
||||||
|
/// is the queue-facing half of an EnvCell transparent shell's detail-off
|
||||||
|
/// submission — one token per CELL, replayed through a caller-supplied
|
||||||
|
/// delegate (production wiring:
|
||||||
|
/// <see cref="AcDream.App.Rendering.Wb.EnvCellRenderer.RenderTransparentOrdered"/>)
|
||||||
|
/// at whatever flush site the shared <see cref="RetailAlphaQueue"/> drains
|
||||||
|
/// at. These tests exercise the REAL production class end-to-end against a
|
||||||
|
/// real queue — only the render destination is faked (a delegate recording
|
||||||
|
/// what it was asked to draw), so the GPU-backed <c>EnvCellRenderer</c>
|
||||||
|
/// never needs to be constructed to pin the queue mechanics.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class EnvCellAlphaDrawSourceTests
|
||||||
|
{
|
||||||
|
/// <summary>Mutation check: if <c>SubmitOrDrawTransparentCellShell</c>
|
||||||
|
/// drew the cell immediately instead of appending it when detail is off
|
||||||
|
/// (the M1/M3 bug class this chunk fixes elsewhere), the drawn list
|
||||||
|
/// would already contain an entry BEFORE <c>Flush</c> runs — the
|
||||||
|
/// <c>Assert.Empty(drawn)</c> line fails against that mutation.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void PendingCellToken_IsDrainedAtTheFlushAndNeverBeforeIt()
|
||||||
|
{
|
||||||
|
var drawn = new List<IReadOnlyList<uint>>();
|
||||||
|
var source = new RetailPViewPassExecutor.EnvCellAlphaDrawSource(
|
||||||
|
cells => drawn.Add(cells.ToArray()));
|
||||||
|
var queue = new RetailAlphaQueue();
|
||||||
|
|
||||||
|
queue.BeginFrame();
|
||||||
|
int token = source.AddPendingCellId(0x1234u);
|
||||||
|
Assert.True(queue.TryAppend(RetailAlphaList.Alpha, source, token, false));
|
||||||
|
|
||||||
|
Assert.Empty(drawn);
|
||||||
|
|
||||||
|
queue.Flush(RetailAlphaFlushSite.DrawBuilding, 0f);
|
||||||
|
|
||||||
|
IReadOnlyList<uint> singleDraw = Assert.Single(drawn);
|
||||||
|
Assert.Equal(new uint[] { 0x1234u }, singleDraw);
|
||||||
|
queue.EndFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Two cell tokens from the SAME <see cref="RetailPViewPassExecutor.EnvCellAlphaDrawSource"/>,
|
||||||
|
/// with an unrelated source's entry appended between them, must still
|
||||||
|
/// produce TWO separate single-cell draw calls around the interposed
|
||||||
|
/// entry — <see cref="IRetailAlphaDrawSource"/>'s "only adjacent
|
||||||
|
/// same-source entries batch" invariant (the queue never groups across
|
||||||
|
/// another entry, which is what keeps compositing order exact). Mutation
|
||||||
|
/// check: an implementation that draws every prepared cell id in one
|
||||||
|
/// call regardless of the requested <c>(first, count)</c> range would
|
||||||
|
/// print both cell ids together on EACH of the two
|
||||||
|
/// <c>DrawPreparedAlphaBatch</c> invocations instead of once each — the
|
||||||
|
/// exact sequence assertion below fails against that mutation.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void ParticleAppendedBetweenTwoCellTokens_KeepsItsPositionInTheCombinedDrain()
|
||||||
|
{
|
||||||
|
var log = new List<string>();
|
||||||
|
var cellSource = new RetailPViewPassExecutor.EnvCellAlphaDrawSource(
|
||||||
|
cells => log.Add($"cell:{string.Join(',', cells)}"));
|
||||||
|
var particleSource = new RecordingSource("particle", log);
|
||||||
|
var queue = new RetailAlphaQueue();
|
||||||
|
|
||||||
|
queue.BeginFrame();
|
||||||
|
int cell1Token = cellSource.AddPendingCellId(0x100u);
|
||||||
|
Assert.True(queue.TryAppend(RetailAlphaList.Alpha, cellSource, cell1Token, false));
|
||||||
|
Assert.True(queue.TryAppend(RetailAlphaList.Alpha, particleSource, 7, false));
|
||||||
|
int cell2Token = cellSource.AddPendingCellId(0x200u);
|
||||||
|
Assert.True(queue.TryAppend(RetailAlphaList.Alpha, cellSource, cell2Token, false));
|
||||||
|
queue.EndFrame();
|
||||||
|
|
||||||
|
Assert.Equal(new[] { "cell:256", "particle:7", "cell:512" }, log);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class RecordingSource(string name, List<string> log) : IRetailAlphaDrawSource
|
||||||
|
{
|
||||||
|
private int[] _prepared = [];
|
||||||
|
|
||||||
|
public void PrepareAlphaDraws(ReadOnlySpan<int> tokens) => _prepared = tokens.ToArray();
|
||||||
|
|
||||||
|
public void DrawPreparedAlphaBatch(int firstPreparedDraw, int drawCount)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < drawCount; i++)
|
||||||
|
log.Add($"{name}:{_prepared[firstPreparedDraw + i]}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResetAlphaSubmissions()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
using AcDream.App.Rendering;
|
||||||
|
using AcDream.Core.Meshing;
|
||||||
|
|
||||||
|
namespace AcDream.App.Tests.Rendering;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// S4-c2 fix round 1 (M1, blocking): <see cref="ParticleRenderer.RouteParticleSubmission"/>
|
||||||
|
/// is the pure router-input derivation the blocking fix extracted from
|
||||||
|
/// <c>DeferToRetailAlphaQueue</c> so it is directly testable without a GPU/
|
||||||
|
/// mesh-manager harness. Before this round, a mesh-particle batch classified
|
||||||
|
/// <see cref="TranslucencyKind.Opaque"/> (mask 0x00) reaching <c>DrawMesh</c>
|
||||||
|
/// row 5 (Immediate) hit an unconditional throw in the render loop — these
|
||||||
|
/// tests pin the two outcomes the fix made reachable.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ParticleRendererRouteTests
|
||||||
|
{
|
||||||
|
/// <summary>M1(d) pin 1: an Opaque-classified mesh-particle batch (mask
|
||||||
|
/// 0x00) with a non-fully-opaque current alpha (top ColorArgb byte !=
|
||||||
|
/// 0xFF, i.e. materialHasAlpha true) routes to ALPHA (Append) — retail's
|
||||||
|
/// row 4 material-alpha fallback — not Immediate. Mutation check:
|
||||||
|
/// reverting to the pre-fix code (which threw whenever the decision
|
||||||
|
/// wasn't Append) would not distinguish this from pin 2 below at all
|
||||||
|
/// (both would just throw); reverting ONLY the materialHasAlpha
|
||||||
|
/// derivation to a hardcoded `false` makes this assertion fail because
|
||||||
|
/// the decision becomes Immediate instead of Append.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void OpaqueClassifiedMeshBatch_WithMaterialAlpha_RoutesToAlphaAppend()
|
||||||
|
{
|
||||||
|
// Top byte (alpha) = 0x80 -> not 0xFF -> materialHasAlpha = true.
|
||||||
|
const uint colorArgbWithAlpha = 0x80FFFFFFu;
|
||||||
|
|
||||||
|
RetailAlphaMeshDecision decision = ParticleRenderer.RouteParticleSubmission(
|
||||||
|
ParticleSubmissionKind.Mesh, TranslucencyKind.Opaque, colorArgbWithAlpha);
|
||||||
|
|
||||||
|
Assert.Equal(RetailAlphaMeshAction.Append, decision.Action);
|
||||||
|
Assert.Equal(RetailAlphaList.Alpha, decision.List);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>M1(d) pin 2: the SAME Opaque-classified mesh-particle batch
|
||||||
|
/// with a fully-opaque current alpha (top ColorArgb byte == 0xFF, i.e.
|
||||||
|
/// materialHasAlpha false) draws immediately and never enters a list —
|
||||||
|
/// retail's row 5 fallthrough. Mutation check: this is the EXACT case
|
||||||
|
/// that threw before the fix (mask 0x00, materialHasAlpha false ->
|
||||||
|
/// row 5 Immediate -> the old "rows 1/2/4/5 unreachable" guard fired);
|
||||||
|
/// reverting the throw's removal reproduces
|
||||||
|
/// <c>InvalidOperationException</c> here, which this test would report
|
||||||
|
/// as a failure (an unhandled exception) rather than an assertion
|
||||||
|
/// mismatch.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void OpaqueClassifiedMeshBatch_WithNoMaterialAlpha_RoutesImmediate()
|
||||||
|
{
|
||||||
|
const uint fullyOpaqueColorArgb = 0xFFFFFFFFu;
|
||||||
|
|
||||||
|
RetailAlphaMeshDecision decision = ParticleRenderer.RouteParticleSubmission(
|
||||||
|
ParticleSubmissionKind.Mesh, TranslucencyKind.Opaque, fullyOpaqueColorArgb);
|
||||||
|
|
||||||
|
Assert.Equal(RetailAlphaMeshAction.Immediate, decision.Action);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>A billboard submission always carries the alpha-family mask
|
||||||
|
/// (0x02) regardless of the (unused) mesh parameters, and therefore
|
||||||
|
/// always satisfies row 3 (Append, ALPHA) under the fixed default delay
|
||||||
|
/// mask — never Immediate, never CLIP. Mutation check: swapping
|
||||||
|
/// <see cref="RetailAlphaMeshRouter.MaskAlphaFamily"/> for
|
||||||
|
/// <see cref="RetailAlphaMeshRouter.MaskClipMap"/> in the billboard
|
||||||
|
/// branch would route this to the CLIP list instead of ALPHA.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void Billboard_AlwaysRoutesToAlphaAppend()
|
||||||
|
{
|
||||||
|
RetailAlphaMeshDecision decision = ParticleRenderer.RouteParticleSubmission(
|
||||||
|
ParticleSubmissionKind.Billboard, default, default);
|
||||||
|
|
||||||
|
Assert.Equal(RetailAlphaMeshAction.Append, decision.Action);
|
||||||
|
Assert.Equal(RetailAlphaList.Alpha, decision.List);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -152,20 +152,25 @@ public sealed class RetailAlphaMeshRouterTests
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Full brute-force sweep over every (mask 0x00-0x09) x sky x detail x
|
/// S4-c2 fix round 1 (A7): this was documented as an "INDEPENDENTLY-
|
||||||
/// multipass x hasAlpha cell at the fixed default delay mask (0x0E),
|
/// shaped reference derivation (a switch-driven truth table, not a copy
|
||||||
/// checked against an INDEPENDENTLY-shaped reference derivation (a
|
/// of <see cref="RetailAlphaMeshRouter.Route"/>'s own if-chain)" — false;
|
||||||
/// switch-driven truth table, not a copy of <see cref="RetailAlphaMeshRouter.Route"/>'s
|
/// <see cref="RestatedBranchTableRoute"/> below is the SAME five-row
|
||||||
/// own if-chain) so a control-flow slip in one is unlikely to survive in
|
/// if-chain shape with different local variable names, not a switch or
|
||||||
/// the other with the same wrong answer. Mutation check: flipping any
|
/// lookup table. It is renamed and redocumented honestly rather than
|
||||||
/// single row's condition in <c>Route</c> (e.g. row 3's <c>!= 0</c> to
|
/// rewritten into a literal 160-row table (spec §4's five rows over ten
|
||||||
/// <c>== 0</c>) changes that row's outcome for roughly half the 160
|
/// masks × sky × detail × multipass × hasAlpha) — the per-row
|
||||||
/// cells, which this sweep catches immediately (verified by hand during
|
/// <see cref="RowBoundaryCases"/> Theory above already carries one
|
||||||
/// S4-c2 development: each of the five rows' conditions was flipped in
|
/// mutation text per row boundary; this brute-force sweep's real value
|
||||||
/// turn and this test failed every time).
|
/// is catching a copy-paste/off-by-one divergence BETWEEN the two
|
||||||
|
/// restatements (a typo in one that the other doesn't share), not an
|
||||||
|
/// independent verification of the spec itself. The claim that "each of
|
||||||
|
/// the five rows' conditions was flipped in turn and this test failed
|
||||||
|
/// every time" is deleted — that hand-verification was never re-run and
|
||||||
|
/// is not reproduced in this round's evidence.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Route_MatchesIndependentTruthTableAcrossEveryCell()
|
public void Route_MatchesRestatedBranchTableAcrossEveryCell()
|
||||||
{
|
{
|
||||||
byte[] masks = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09];
|
byte[] masks = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09];
|
||||||
bool[] bothBools = [false, true];
|
bool[] bothBools = [false, true];
|
||||||
|
|
@ -180,7 +185,7 @@ public sealed class RetailAlphaMeshRouterTests
|
||||||
{
|
{
|
||||||
RetailAlphaMeshDecision actual = RetailAlphaMeshRouter.Route(
|
RetailAlphaMeshDecision actual = RetailAlphaMeshRouter.Route(
|
||||||
sky, RetailAlphaMeshRouter.DefaultDelayMask, detail, multipass, mask, hasAlpha);
|
sky, RetailAlphaMeshRouter.DefaultDelayMask, detail, multipass, mask, hasAlpha);
|
||||||
RetailAlphaMeshDecision expected = IndependentReferenceRoute(
|
RetailAlphaMeshDecision expected = RestatedBranchTableRoute(
|
||||||
sky, RetailAlphaMeshRouter.DefaultDelayMask, detail, multipass, mask, hasAlpha);
|
sky, RetailAlphaMeshRouter.DefaultDelayMask, detail, multipass, mask, hasAlpha);
|
||||||
|
|
||||||
bool matches = actual.Action == expected.Action
|
bool matches = actual.Action == expected.Action
|
||||||
|
|
@ -198,9 +203,10 @@ public sealed class RetailAlphaMeshRouterTests
|
||||||
Assert.True(mismatches == 0, $"{mismatches} mismatches; first: {firstMismatch}");
|
Assert.True(mismatches == 0, $"{mismatches} mismatches; first: {firstMismatch}");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Independent re-derivation of spec §4's table using a
|
/// <summary>Re-derivation of spec §4's table in independently-named
|
||||||
/// switch/lookup shape rather than the production if-chain.</summary>
|
/// local variables (NOT a switch/lookup shape — see this Fact's own doc
|
||||||
private static RetailAlphaMeshDecision IndependentReferenceRoute(
|
/// comment above, A7).</summary>
|
||||||
|
private static RetailAlphaMeshDecision RestatedBranchTableRoute(
|
||||||
bool sky, byte delayMask, bool detail, bool multipass, byte mask, bool hasAlpha)
|
bool sky, byte delayMask, bool detail, bool multipass, byte mask, bool hasAlpha)
|
||||||
{
|
{
|
||||||
bool row1 = sky || delayMask == 0 || detail;
|
bool row1 = sky || delayMask == 0 || detail;
|
||||||
|
|
|
||||||
|
|
@ -311,42 +311,6 @@ public sealed class RetailAlphaQueueTests
|
||||||
queue.AbortFrame();
|
queue.AbortFrame();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>The first entry appended to a list after it was last drained
|
|
||||||
/// is flagged <c>IsFirstForList</c>; later entries in the same
|
|
||||||
/// uninterrupted run are not. Mutation check: always setting the flag
|
|
||||||
/// true (or always false) fails this exact sequence assertion.</summary>
|
|
||||||
[Fact]
|
|
||||||
public void TryAppend_FlagsOnlyTheFirstEntrySinceTheLastDrain()
|
|
||||||
{
|
|
||||||
var log = new List<string>();
|
|
||||||
var source = new RecordingSource("alpha", log);
|
|
||||||
var queue = new RetailAlphaQueue();
|
|
||||||
FieldInfo alphaField = typeof(RetailAlphaQueue).GetField(
|
|
||||||
"_alpha", BindingFlags.NonPublic | BindingFlags.Instance)!;
|
|
||||||
|
|
||||||
queue.BeginFrame();
|
|
||||||
queue.TryAppend(RetailAlphaList.Alpha, source, 1, false);
|
|
||||||
queue.TryAppend(RetailAlphaList.Alpha, source, 2, false);
|
|
||||||
|
|
||||||
// Inspect BEFORE flushing: two entries in the SAME list snapshot,
|
|
||||||
// discriminating true (first) from false (second) — checking only
|
|
||||||
// the post-flush single-survivor list (as an earlier draft of this
|
|
||||||
// test did) is vacuous, since a one-element list is trivially
|
|
||||||
// "first" whether or not the flag logic is correct.
|
|
||||||
var beforeFlush = (List<RetailAlphaEntry>)alphaField.GetValue(queue)!;
|
|
||||||
Assert.Equal(2, beforeFlush.Count);
|
|
||||||
Assert.True(beforeFlush[0].IsFirstForList);
|
|
||||||
Assert.False(beforeFlush[1].IsFirstForList);
|
|
||||||
|
|
||||||
queue.Flush(RetailAlphaFlushSite.RenderNormalMode, 0f);
|
|
||||||
queue.TryAppend(RetailAlphaList.Alpha, source, 3, false);
|
|
||||||
|
|
||||||
var afterDrain = (List<RetailAlphaEntry>)alphaField.GetValue(queue)!;
|
|
||||||
Assert.Single(afterDrain);
|
|
||||||
Assert.True(afterDrain[0].IsFirstForList);
|
|
||||||
queue.AbortFrame();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void RetainedScratchConvergesAfterAOneScopeSpike()
|
public void RetainedScratchConvergesAfterAOneScopeSpike()
|
||||||
{
|
{
|
||||||
|
|
@ -371,6 +335,74 @@ public sealed class RetailAlphaQueueTests
|
||||||
Assert.Equal(0, queue.PendingCount);
|
Assert.Equal(0, queue.PendingCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// S4-c2 fix round 1 (A4): <c>DrainAndReset</c>/<c>AbortFrame</c> used to
|
||||||
|
/// pass the ENTRY count as <c>ApplyScratchRetention</c>'s SOURCE-count
|
||||||
|
/// argument too (<c>ApplyScratchRetention(observedClip + observedAlpha,
|
||||||
|
/// observedClip + observedAlpha)</c>). A single spike frame first
|
||||||
|
/// registers 100 DISTINCT sources (one entry each) so <c>_sources</c>'
|
||||||
|
/// own List-growth capacity climbs well past its initial 4 — otherwise
|
||||||
|
/// <c>Math.Min(sourceTarget, _sources.Capacity)</c> clamps ANY
|
||||||
|
/// <c>sourceTarget</c> down to that unchanged initial 4 and the two
|
||||||
|
/// formulas become indistinguishable, which is why a single-source
|
||||||
|
/// spike does not discriminate this bug. Three low-demand frames then
|
||||||
|
/// each resubmit 10 entries through the SAME ONE source (matching the
|
||||||
|
/// three consecutive observations <see cref="Residency.RetainedScratchCapacityPolicy"/>
|
||||||
|
/// requires before it recommends shrinking) — at the moment the shrink
|
||||||
|
/// fires, the ENTRY count (10) and the real SOURCE count (1) genuinely
|
||||||
|
/// diverge. Mutation check: reverting to
|
||||||
|
/// <c>ApplyScratchRetention(observedClip + observedAlpha, observedClip +
|
||||||
|
/// observedAlpha)</c> makes <c>sourceTarget</c> compute from 10
|
||||||
|
/// (<c>Math.Max(4, 10*2)=20</c>, clamped by the now-≥20 <c>_sources.Capacity</c>
|
||||||
|
/// to 20) instead of from 1 (<c>Math.Max(4, 1*2)=4</c>) — the actual
|
||||||
|
/// observed capacity under that mutation is 20, and the upper-bound
|
||||||
|
/// assertion below fails against it.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void RetainedSourceCapacity_ConvergesToTheRealSourceCountNotTheEntryCount()
|
||||||
|
{
|
||||||
|
const int budgetBytes = 128 * 1024;
|
||||||
|
var manySources = new CountingSource[100];
|
||||||
|
for (int i = 0; i < manySources.Length; i++)
|
||||||
|
manySources[i] = new CountingSource();
|
||||||
|
var queue = new RetailAlphaQueue(budgetBytes);
|
||||||
|
FieldInfo sourcesField = typeof(RetailAlphaQueue).GetField(
|
||||||
|
"_sources", BindingFlags.NonPublic | BindingFlags.Instance)!;
|
||||||
|
|
||||||
|
// 8,192 entries (enough to also push the ENTRY capacity itself past
|
||||||
|
// its low-demand budget, matching RetainedScratchConvergesAfterAOneScopeSpike's
|
||||||
|
// own spike size) spread across the 100 distinct sources, so BOTH
|
||||||
|
// _sources' own capacity AND the entry-side capacity are in their
|
||||||
|
// post-spike high-water state together.
|
||||||
|
queue.BeginFrame();
|
||||||
|
for (int i = 0; i < 8_192; i++)
|
||||||
|
queue.TryAppend(RetailAlphaList.Alpha, manySources[i % manySources.Length], i, false);
|
||||||
|
queue.EndFrame();
|
||||||
|
|
||||||
|
var sourcesAfterSpike = (List<IRetailAlphaDrawSource>)sourcesField.GetValue(queue)!;
|
||||||
|
Assert.True(
|
||||||
|
sourcesAfterSpike.Capacity > 8,
|
||||||
|
"Test setup check: the 100-distinct-source spike must grow _sources' own capacity "
|
||||||
|
+ $"past its initial 4 (observed {sourcesAfterSpike.Capacity}) — otherwise the "
|
||||||
|
+ "Math.Min clamp below hides the bug regardless of which formula runs.");
|
||||||
|
|
||||||
|
CountingSource repeatedSource = manySources[0];
|
||||||
|
for (int i = 0; i < 3; i++)
|
||||||
|
{
|
||||||
|
queue.BeginFrame();
|
||||||
|
for (int j = 0; j < 10; j++)
|
||||||
|
queue.TryAppend(RetailAlphaList.Alpha, repeatedSource, j, false);
|
||||||
|
queue.EndFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
var sources = (List<IRetailAlphaDrawSource>)sourcesField.GetValue(queue)!;
|
||||||
|
Assert.True(
|
||||||
|
sources.Capacity <= 8,
|
||||||
|
"Expected the retained source-array capacity to converge toward the real source "
|
||||||
|
+ $"count (1), but it stayed at {sources.Capacity} — the entry count (10), not the "
|
||||||
|
+ "source count (1), must have driven ApplyScratchRetention's second argument.");
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void AbortFrame_DiscardsPayloadAndAllowsTheNextFrameToRender()
|
public void AbortFrame_DiscardsPayloadAndAllowsTheNextFrameToRender()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ using AcDream.App.Rendering.Gpu;
|
||||||
using AcDream.App.Rendering.Gpu.Vk;
|
using AcDream.App.Rendering.Gpu.Vk;
|
||||||
using AcDream.App.Rendering.Sky;
|
using AcDream.App.Rendering.Sky;
|
||||||
using AcDream.App.Rendering.Walk;
|
using AcDream.App.Rendering.Walk;
|
||||||
|
using AcDream.App.Rendering.Wb;
|
||||||
using AcDream.App.Tests.Architecture;
|
using AcDream.App.Tests.Architecture;
|
||||||
using AcDream.App.Tests.Rendering.Gpu;
|
using AcDream.App.Tests.Rendering.Gpu;
|
||||||
|
|
||||||
|
|
@ -158,6 +159,44 @@ public sealed class RetailPViewPassExecutorTests
|
||||||
call => call.Target.DeclaringType == typeof(WalkTranscriptDump));
|
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>
|
/// <summary>
|
||||||
/// S3 chunk 4 (§10.2): the former per-outside-view-slice loop
|
/// S3 chunk 4 (§10.2): the former per-outside-view-slice loop
|
||||||
/// (the walk's own screen-space terrain-clip writer + its per-frame
|
/// (the walk's own screen-space terrain-clip writer + its per-frame
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
using System.Reflection;
|
||||||
|
using AcDream.App.Rendering;
|
||||||
|
using AcDream.App.Tests.Architecture;
|
||||||
|
|
||||||
|
namespace AcDream.App.Tests.Rendering;
|
||||||
|
|
||||||
|
public sealed class RetailPViewRendererTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// S4-c2 fix round 1 addendum (A1): retail's LandscapeFlush
|
||||||
|
/// (<c>PView::DrawCells</c> @0x005a4840's own <c>FlushAlphaList(0f)</c>
|
||||||
|
/// @0x005a4872) only ever fires on an INTERIOR PView, strictly inside
|
||||||
|
/// <c>if (outside_view.view_count > 0)</c> — an outdoor root never
|
||||||
|
/// reaches it (terrace-edge's capture: zero <c>005a4877</c> FL lines).
|
||||||
|
/// In production the interior case reaches
|
||||||
|
/// <see cref="RetailPViewPassExecutor.FlushLandscapeAlpha"/> through the
|
||||||
|
/// walk driver's LandscapeFlush leaf (<c>FlushWalkLandscape</c>, a
|
||||||
|
/// SEPARATE method the driver calls during <c>Replay</c>) — never as a
|
||||||
|
/// direct call compiled into <see cref="RetailPViewRenderer.DrawInside"/>'s
|
||||||
|
/// own body. The outdoor-root direct call that used to sit inline here
|
||||||
|
/// (mislabelled LandscapeFlush) is deleted; retail's real outdoor-root
|
||||||
|
/// drains are the <c>DrawBlock</c> 0.75 valve (already wired via
|
||||||
|
/// <c>WalkFrameEventKind.SortCellExit</c>) and
|
||||||
|
/// <c>SmartBox::RenderNormalMode</c>'s pass-end flush
|
||||||
|
/// (<see cref="RetailAlphaQueue.EndFrame"/>, called from
|
||||||
|
/// <c>WorldSceneRenderer.CompleteWorldFrame</c>).
|
||||||
|
///
|
||||||
|
/// <para>Mutation check: reverting the deletion (restoring
|
||||||
|
/// <c>if (ctx.RootCell.IsOutdoorNode) passes.FlushLandscapeAlpha();</c>)
|
||||||
|
/// reintroduces a direct call to
|
||||||
|
/// <see cref="RetailPViewPassExecutor.FlushLandscapeAlpha"/> inside
|
||||||
|
/// <see cref="RetailPViewRenderer.DrawInside"/>'s compiled body — the
|
||||||
|
/// <c>Assert.DoesNotContain</c> below fails against that mutation.</para>
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void DrawInside_NeverCallsFlushLandscapeAlphaDirectly()
|
||||||
|
{
|
||||||
|
MethodInfo drawInside = typeof(RetailPViewRenderer).GetMethod(
|
||||||
|
"DrawInside",
|
||||||
|
BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)!;
|
||||||
|
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(drawInside);
|
||||||
|
|
||||||
|
Assert.DoesNotContain(
|
||||||
|
calls,
|
||||||
|
call => call.Target.DeclaringType == typeof(RetailPViewPassExecutor)
|
||||||
|
&& call.Target.Name == nameof(RetailPViewPassExecutor.FlushLandscapeAlpha));
|
||||||
|
|
||||||
|
// The interior-cell unattached-emitter draw beside the deleted call
|
||||||
|
// must remain — this pin only removes the mislabelled flush, not
|
||||||
|
// the particle submission next to it.
|
||||||
|
Assert.Contains(
|
||||||
|
calls,
|
||||||
|
call => call.Target.DeclaringType == typeof(RetailPViewPassExecutor)
|
||||||
|
&& call.Target.Name == nameof(RetailPViewPassExecutor.DrawUnattachedSceneParticles));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -87,9 +87,17 @@ public static class WalkAlphaDepthTrace
|
||||||
// own printf. listSel nonzero selects CLIP
|
// own printf. listSel nonzero selects CLIP
|
||||||
// (alphaedMeshCountClip/List); zero selects ALPHA
|
// (alphaedMeshCountClip/List); zero selects ALPHA
|
||||||
// (alphaedMeshCountAlpha/List) — Ghidra-verified 2026-09-04, see
|
// (alphaedMeshCountAlpha/List) — Ghidra-verified 2026-09-04, see
|
||||||
// RetailAlphaList's own doc comment. "new" is the per-entry
|
// RetailAlphaList's own doc comment. "new" is retail's per-entry
|
||||||
// first-for-list flag (param_4); "clip" (param_5, unused here)
|
// first-for-list flag (param_4); "clip" (param_5, unused here)
|
||||||
// is overrideClipmap.
|
// is overrideClipmap. S4-c2 fix round 1 (M4/A6): IsNew is parsed
|
||||||
|
// and RETAINED purely as a captured/parsed FACT (fidelity to the
|
||||||
|
// capture's own format) but is never compared against acdream's
|
||||||
|
// own routing — M4 established that flag is trivially true for
|
||||||
|
// EVERY subset retail appends (one DrawMesh invocation owns one
|
||||||
|
// subset per list), so a per-subset comparison would be
|
||||||
|
// meaningless at acdream's coarser per-INSTANCE granularity (the
|
||||||
|
// same content-volume mismatch M5's KnownFailure count gate
|
||||||
|
// documents) — no dead-parse-as-evidence claim is made here.
|
||||||
Match am = AmPattern.Match(line);
|
Match am = AmPattern.Match(line);
|
||||||
if (am.Success)
|
if (am.Success)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1736,6 +1736,75 @@ public sealed partial class WalkFrameDriverTests
|
||||||
Assert.Single(leaf.LandCellBatches));
|
Assert.Single(leaf.LandCellBatches));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// S4-c2 fix round 1 (M5): the ONE live pin exercising the valve THIS
|
||||||
|
/// CHUNK added, through <see cref="WalkFrameDriver.Replay"/>'s own
|
||||||
|
/// <c>WalkFrameEventKind.SortCellExit</c> arm (not
|
||||||
|
/// <see cref="RetailAlphaQueue.Flush"/> called directly —
|
||||||
|
/// <c>RetailAlphaQueueTests</c> already has that boundary). The queue is
|
||||||
|
/// preloaded directly (a dummy source, never real mesh content — this
|
||||||
|
/// pin proves the VALVE's threshold behavior, not classification, which
|
||||||
|
/// is covered elsewhere) with entries sitting in ALPHA when the walk's
|
||||||
|
/// own <c>DrawBlock</c>-equivalent land-cell turn reaches its
|
||||||
|
/// <c>FlushAlphaList(0.75f)</c> call. Mutation check: forcing
|
||||||
|
/// <see cref="RetailAlphaQueue.Flush"/>'s no-op comparison to
|
||||||
|
/// <c><=</c> instead of <c><</c> (S4-c1/c2's own boundary mutation)
|
||||||
|
/// makes the 2250 case read <c>PendingCount == 2250</c> (a no-op)
|
||||||
|
/// instead of 0 — the first assertion below fails against that
|
||||||
|
/// mutation.
|
||||||
|
/// </summary>
|
||||||
|
[Theory]
|
||||||
|
[InlineData(2250, 0)]
|
||||||
|
[InlineData(2249, 2249)]
|
||||||
|
public void SortCellExit_ValveDrainsThroughReplayAtTheExactBoundary(
|
||||||
|
int preloadedCount, int expectedPendingAfter)
|
||||||
|
{
|
||||||
|
using var fx = new DispatcherFixture();
|
||||||
|
var log = new List<string>();
|
||||||
|
var leaf = new RecordingLeafRenderer(log, fx.AlphaQueue);
|
||||||
|
leaf.CellsWithoutEmitters.UnionWith(CoarseLandscapeBuckets(0xF4180000u));
|
||||||
|
var ctx = new TestContext();
|
||||||
|
var driver = new WalkFrameDriver(fx.Dispatcher, leaf, new FakeWorldData());
|
||||||
|
var walk = new RetailFrameWalk();
|
||||||
|
var landscape = new WalkLandscape { MidWidth = 1, Blocks = new WalkLandBlock?[1] };
|
||||||
|
var block = new WalkLandBlock
|
||||||
|
{
|
||||||
|
LandblockId = 0xF4180000u, SideCellCount = 1, MaxZ = 10f, MinZ = 0f,
|
||||||
|
};
|
||||||
|
block.EnsureCellArrays();
|
||||||
|
landscape.Blocks[0] = block;
|
||||||
|
var dummySource = new DummyAlphaSource();
|
||||||
|
|
||||||
|
using DrawScope draw = fx.BeginDraw(beginAlpha: true);
|
||||||
|
for (int i = 0; i < preloadedCount; i++)
|
||||||
|
{
|
||||||
|
Assert.True(fx.AlphaQueue.TryAppend(RetailAlphaList.Alpha, dummySource, i, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
|
||||||
|
walk.DrawLandscape(landscape, OneDegenerateView(), ctx, driver);
|
||||||
|
driver.EndFrame();
|
||||||
|
driver.Replay(draw.Frame, draw.Pass);
|
||||||
|
|
||||||
|
Assert.Equal(new[] { preloadedCount }, leaf.AlphaPendingAtSortCellExit);
|
||||||
|
Assert.Equal(expectedPendingAfter, fx.AlphaQueue.PendingCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class DummyAlphaSource : IRetailAlphaDrawSource
|
||||||
|
{
|
||||||
|
public void PrepareAlphaDraws(ReadOnlySpan<int> tokens)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DrawPreparedAlphaBatch(int firstPreparedDraw, int drawCount)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResetAlphaSubmissions()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── F4(b) (S3 chunk 3 fix round 1 §9.6): an interior root with one
|
// ── F4(b) (S3 chunk 3 fix round 1 §9.6): an interior root with one
|
||||||
// surviving exit view — the SAME fixture as
|
// surviving exit view — the SAME fixture as
|
||||||
// RunFrame_InteriorFloodWithExitView_FreshDriverSkipsTheGatedClearThenDrawsSealsAndFloodCells
|
// RunFrame_InteriorFloodWithExitView_FreshDriverSkipsTheGatedClearThenDrawsSealsAndFloodCells
|
||||||
|
|
|
||||||
|
|
@ -551,8 +551,11 @@ public sealed class WalkStaticStreamPopulatorTests
|
||||||
Assert.Equal(record.Transform.LocalToWorld, call.LocalToWorld);
|
Assert.Equal(record.Transform.LocalToWorld, call.LocalToWorld);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── SubmitWalkAlphaInstance: same viewer distance + per-instance data as
|
// ── SubmitWalkAlphaInstance: same per-instance data and router decision
|
||||||
// DeferTransparentGroups, through the REAL RetailAlphaQueue. ───────────
|
// as DeferTransparentGroups, through the REAL RetailAlphaQueue — S4-c2
|
||||||
|
// fix round 1 (A5): this banner's stale "same viewer distance" text is
|
||||||
|
// corrected; the FIFO cutover deleted viewer distance from the alpha
|
||||||
|
// path entirely. ────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// <summary>S4-c2: retail's queues are FIFO, not distance-sorted — this
|
/// <summary>S4-c2: retail's queues are FIFO, not distance-sorted — this
|
||||||
/// pins the routing decision instead (an AlphaBlend batch's constructed
|
/// pins the routing decision instead (an AlphaBlend batch's constructed
|
||||||
|
|
|
||||||
|
|
@ -368,33 +368,21 @@ public sealed partial class WalkTraceConformanceTests
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// S4-c2 gate G-c2: extends this file's own PM/PC transcript gate from
|
/// S4-c2 gate G-c2, split by S4-c2 fix round 1 (M5): extends this file's
|
||||||
/// depth events to the AM/FL alpha-queue stream. Reuses the EXACT SAME
|
/// own PM/PC transcript gate from depth events to the AM/FL alpha-queue
|
||||||
/// replay harness (<see cref="EmptyAlphaDepthWorldData"/>: no live
|
/// stream. Reuses the EXACT SAME replay harness
|
||||||
/// GfxObj/particle mesh content, matching this file's own established
|
/// (<see cref="EmptyAlphaDepthWorldData"/>: no live GfxObj/particle mesh
|
||||||
/// PM/PC pattern) with a real <see cref="RetailAlphaQueue"/> wired
|
/// content, matching this file's own established PM/PC pattern) with a
|
||||||
/// through <see cref="AlphaDepthLeaf"/>'s flush hooks.
|
/// real <see cref="RetailAlphaQueue"/> wired through
|
||||||
///
|
/// <see cref="AlphaDepthLeaf"/>'s flush hooks. Returns BOTH the expected
|
||||||
/// <para><b>What this DOES prove, unconditionally (no KnownFailure
|
/// and actual transcripts so <see cref="RunAlphaFlushSitesGate"/> and
|
||||||
/// needed):</b> the SEQUENCE of (site, threshold) the walk drives the
|
/// <see cref="RunAlphaFlushCountsGate"/> can assert their own dimension
|
||||||
/// queue through — which of the four normal-world sites fires, how many
|
/// over the SAME replay logic without duplicating the harness setup.
|
||||||
/// times, in what order — is purely structural (a function of cell/
|
|
||||||
/// building/land-cell traversal, never of mesh content), so it is
|
|
||||||
/// checked here as an unconditional pass/fail.</para>
|
|
||||||
///
|
|
||||||
/// <para><b>What this CANNOT prove with this harness:</b> the drained
|
|
||||||
/// entry count PER LIST. This gate's queue carries zero live content
|
|
||||||
/// (same design choice as the PM/PC gate it extends), so every 0f-
|
|
||||||
/// threshold flush observes count (0, 0) — always a real drain (0 is
|
|
||||||
/// never <c>< 0</c>), matching retail's own always-drains behavior at
|
|
||||||
/// threshold 0f, but with a DIFFERENT count than the capture's real
|
|
||||||
/// content volume. A capture pose whose expected transcript has any
|
|
||||||
/// nonzero drained count therefore diverges on the count dimension by
|
|
||||||
/// harness design, not by a routing bug — see this Fact's own
|
|
||||||
/// <c>[Trait("Status","KnownFailure")]</c> and the S4-c2 packet write-up
|
|
||||||
/// (S4 packet §9) for the full per-pose sequences.</para>
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void RunAlphaFlushTranscriptGate(string fixtureName)
|
private static (
|
||||||
|
IReadOnlyList<(RetailAlphaFlushSite Site, float Threshold, int DrainedClip, int DrainedAlpha)> Expected,
|
||||||
|
IReadOnlyList<(RetailAlphaFlushSite Site, float Threshold, int DrainedClip, int DrainedAlpha)> Actual)
|
||||||
|
RunAlphaFlushTranscriptReplay(string fixtureName)
|
||||||
{
|
{
|
||||||
IReadOnlyList<WalkOracleFrame> poseFrames = WalkOracleTrace.Load(OhCaptureRoot, fixtureName);
|
IReadOnlyList<WalkOracleFrame> poseFrames = WalkOracleTrace.Load(OhCaptureRoot, fixtureName);
|
||||||
Assert.NotEmpty(poseFrames);
|
Assert.NotEmpty(poseFrames);
|
||||||
|
|
@ -447,34 +435,74 @@ public sealed partial class WalkTraceConformanceTests
|
||||||
leaf.ActualFlushes.Add(
|
leaf.ActualFlushes.Add(
|
||||||
(RetailAlphaFlushSite.RenderNormalMode, 0f, clipBeforeFinal, alphaBeforeFinal));
|
(RetailAlphaFlushSite.RenderNormalMode, 0f, clipBeforeFinal, alphaBeforeFinal));
|
||||||
|
|
||||||
IReadOnlyList<(RetailAlphaFlushSite Site, float Threshold, int DrainedClip, int DrainedAlpha)> actual =
|
return (expected, leaf.ActualFlushes);
|
||||||
leaf.ActualFlushes;
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// S4-c2 fix round 1 (M5): the LIVE half of gate G-c2 — the SEQUENCE of
|
||||||
|
/// (site, threshold) the walk drives the queue through (which of the
|
||||||
|
/// four normal-world sites fires, how many times, in what order) is
|
||||||
|
/// purely structural (a function of cell/building/land-cell traversal,
|
||||||
|
/// never of mesh content), so it is a real, unconditional pass/fail with
|
||||||
|
/// no KnownFailure tag — a future site-sequence regression is now
|
||||||
|
/// visible in the hermetic/InstalledDat lanes instead of being buried
|
||||||
|
/// under the (expected-red) count dimension.
|
||||||
|
/// </summary>
|
||||||
|
private void RunAlphaFlushSitesGate(string fixtureName)
|
||||||
|
{
|
||||||
|
(
|
||||||
|
IReadOnlyList<(RetailAlphaFlushSite Site, float Threshold, int DrainedClip, int DrainedAlpha)> expected,
|
||||||
|
IReadOnlyList<(RetailAlphaFlushSite Site, float Threshold, int DrainedClip, int DrainedAlpha)> actual
|
||||||
|
) = RunAlphaFlushTranscriptReplay(fixtureName);
|
||||||
|
|
||||||
bool siteSequenceMatches = expected
|
bool siteSequenceMatches = expected
|
||||||
.Select(e => (e.Site, e.Threshold))
|
.Select(e => (e.Site, e.Threshold))
|
||||||
.SequenceEqual(actual.Select(a => (a.Site, a.Threshold)));
|
.SequenceEqual(actual.Select(a => (a.Site, a.Threshold)));
|
||||||
|
|
||||||
// The site+threshold sequence is a REAL, unconditional pass/fail —
|
|
||||||
// purely structural, never content-dependent.
|
|
||||||
Assert.True(
|
Assert.True(
|
||||||
siteSequenceMatches,
|
siteSequenceMatches,
|
||||||
$"{fixtureName}: flush SITE sequence diverges (content-independent — this must "
|
$"{fixtureName}: flush SITE sequence diverges (content-independent — this must "
|
||||||
+ $"match unconditionally) — expected {FormatFlushSites(expected)}, "
|
+ $"match unconditionally) — expected {FormatFlushSites(expected)}, "
|
||||||
+ $"actual {FormatFlushSites(actual)}");
|
+ $"actual {FormatFlushSites(actual)}");
|
||||||
|
}
|
||||||
|
|
||||||
// The full tuple (including per-list drained counts) is the part
|
/// <summary>
|
||||||
// this harness's zero-content design cannot prove — recorded here
|
/// S4-c2 fix round 1 (M5): the KnownFailure half of gate G-c2 — the
|
||||||
// as a real, evaluated assertion (never weakened), expected to
|
/// drained entry count PER LIST can never match by construction, for
|
||||||
// diverge on the count dimension for any pose with real content;
|
/// THREE independent reasons (not just "harness content only", the
|
||||||
|
/// prior write-up's incomplete claim): (1) this harness carries no live
|
||||||
|
/// GfxObj/particle mesh content at all (<see cref="EmptyAlphaDepthWorldData"/>),
|
||||||
|
/// so every 0f-threshold flush observes count (0, 0) — always a real
|
||||||
|
/// drain (0 is never <c>< 0</c>), matching retail's own always-drains
|
||||||
|
/// behavior at threshold 0f, but with a different count than the
|
||||||
|
/// capture's real content volume; (2) even WITH content, acdream appends
|
||||||
|
/// one queue entry per INSTANCE, where retail's <c>AddMeshToAlphaList</c>
|
||||||
|
/// appends one entry per SUBSET per <c>DrawMesh</c> call — a single
|
||||||
|
/// multi-subset instance inflates retail's count relative to acdream's;
|
||||||
|
/// (3) acdream's CLIP list is structurally empty for ordinary content
|
||||||
|
/// (M2's new register row) — <c>WbDrawDispatcher.IsOpaque</c> filters
|
||||||
|
/// clip-mapped subsets out before they ever reach a submit call, so
|
||||||
|
/// acdream's CLIP count reads 0 wherever retail's capture shows nonzero
|
||||||
|
/// CLIP entries. See the S4 packet's §9/§10/§11 subsections for the
|
||||||
|
/// full per-pose sequences.
|
||||||
|
/// </summary>
|
||||||
|
private void RunAlphaFlushCountsGate(string fixtureName)
|
||||||
|
{
|
||||||
|
(
|
||||||
|
IReadOnlyList<(RetailAlphaFlushSite Site, float Threshold, int DrainedClip, int DrainedAlpha)> expected,
|
||||||
|
IReadOnlyList<(RetailAlphaFlushSite Site, float Threshold, int DrainedClip, int DrainedAlpha)> actual
|
||||||
|
) = RunAlphaFlushTranscriptReplay(fixtureName);
|
||||||
|
|
||||||
|
// Recorded as a real, evaluated assertion (never weakened), expected
|
||||||
|
// to diverge on the count dimension for any pose with real content —
|
||||||
// callers gate on Status=KnownFailure, not on this passing.
|
// callers gate on Status=KnownFailure, not on this passing.
|
||||||
bool fullMatches = expected.SequenceEqual(actual);
|
bool fullMatches = expected.SequenceEqual(actual);
|
||||||
Assert.True(
|
Assert.True(
|
||||||
fullMatches,
|
fullMatches,
|
||||||
$"{fixtureName}: flush transcript diverges on drained COUNT only (site+threshold "
|
$"{fixtureName}: flush transcript diverges on drained COUNT only (the site+threshold "
|
||||||
+ "sequence above already verified exact) — this hermetic replay carries no live "
|
+ "sequence is separately verified exact by AlphaFlushSites_*) — see this method's "
|
||||||
+ "GfxObj/particle content (EmptyAlphaDepthWorldData, matching this file's own "
|
+ $"own doc comment for the three reasons this can never match — expected "
|
||||||
+ $"PM/PC design), so every count reads (0,0) — expected {FormatFlushCounts(expected)}, "
|
+ $"{FormatFlushCounts(expected)}, actual {FormatFlushCounts(actual)}");
|
||||||
+ $"actual {FormatFlushCounts(actual)}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string FormatFlushSites(
|
private static string FormatFlushSites(
|
||||||
|
|
@ -487,41 +515,67 @@ public sealed partial class WalkTraceConformanceTests
|
||||||
", ",
|
", ",
|
||||||
events.Select(e => $"({e.Site},thresh={e.Threshold},clip={e.DrainedClip},alpha={e.DrainedAlpha})")) + "]";
|
events.Select(e => $"({e.Site},thresh={e.Threshold},clip={e.DrainedClip},alpha={e.DrainedAlpha})")) + "]";
|
||||||
|
|
||||||
/// <summary>S4-c2 gate G-c2. See <see cref="RunAlphaFlushTranscriptGate"/>'s
|
/// <summary>S4-c2 fix round 1 (M5): the live half — see
|
||||||
/// own doc comment for what this Fact does and does not prove; tagged
|
/// <see cref="RunAlphaFlushSitesGate"/>'s own doc comment. No
|
||||||
/// KnownFailure because this hermetic harness carries no live mesh
|
/// KnownFailure tag: this dimension is content-independent and must
|
||||||
/// content, so the drained-count dimension diverges from the capture's
|
/// match unconditionally.</summary>
|
||||||
/// real content volume by design — the S4 packet's §9 subsection quotes
|
[Fact]
|
||||||
/// both sequences per pose.</summary>
|
public void AlphaFlushSites_CathedralArrival_MatchesRetailFrame2()
|
||||||
|
=> RunAlphaFlushSitesGate("cathedral-arrival.alphadepth");
|
||||||
|
|
||||||
|
/// <summary>S4-c2 fix round 1 (M5): the KnownFailure half — see
|
||||||
|
/// <see cref="RunAlphaFlushCountsGate"/>'s own doc comment for the three
|
||||||
|
/// independent reasons the drained COUNT can never match by
|
||||||
|
/// construction.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
[Trait("Status", "KnownFailure")]
|
[Trait("Status", "KnownFailure")]
|
||||||
public void AlphaFlushTranscript_CathedralArrival_MatchesRetailFrame2()
|
public void AlphaFlushCounts_CathedralArrival_MatchesRetailFrame2()
|
||||||
=> RunAlphaFlushTranscriptGate("cathedral-arrival.alphadepth");
|
=> RunAlphaFlushCountsGate("cathedral-arrival.alphadepth");
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AlphaFlushSites_CathedralLeak_MatchesRetailFrame2()
|
||||||
|
=> RunAlphaFlushSitesGate("cathedral-leak.alphadepth");
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
[Trait("Status", "KnownFailure")]
|
[Trait("Status", "KnownFailure")]
|
||||||
public void AlphaFlushTranscript_CathedralLeak_MatchesRetailFrame2()
|
public void AlphaFlushCounts_CathedralLeak_MatchesRetailFrame2()
|
||||||
=> RunAlphaFlushTranscriptGate("cathedral-leak.alphadepth");
|
=> RunAlphaFlushCountsGate("cathedral-leak.alphadepth");
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AlphaFlushSites_CathedralStairArch_MatchesRetailFrame2()
|
||||||
|
=> RunAlphaFlushSitesGate("cathedral-stair-arch.alphadepth");
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
[Trait("Status", "KnownFailure")]
|
[Trait("Status", "KnownFailure")]
|
||||||
public void AlphaFlushTranscript_CathedralStairArch_MatchesRetailFrame2()
|
public void AlphaFlushCounts_CathedralStairArch_MatchesRetailFrame2()
|
||||||
=> RunAlphaFlushTranscriptGate("cathedral-stair-arch.alphadepth");
|
=> RunAlphaFlushCountsGate("cathedral-stair-arch.alphadepth");
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AlphaFlushSites_FoundryDeep_MatchesRetailFrame2()
|
||||||
|
=> RunAlphaFlushSitesGate("foundry-deep.alphadepth");
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
[Trait("Status", "KnownFailure")]
|
[Trait("Status", "KnownFailure")]
|
||||||
public void AlphaFlushTranscript_FoundryDeep_MatchesRetailFrame2()
|
public void AlphaFlushCounts_FoundryDeep_MatchesRetailFrame2()
|
||||||
=> RunAlphaFlushTranscriptGate("foundry-deep.alphadepth");
|
=> RunAlphaFlushCountsGate("foundry-deep.alphadepth");
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AlphaFlushSites_HoltburgDoorwayStill_MatchesRetailFrame2()
|
||||||
|
=> RunAlphaFlushSitesGate("holtburg-doorway-still.alphadepth");
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
[Trait("Status", "KnownFailure")]
|
[Trait("Status", "KnownFailure")]
|
||||||
public void AlphaFlushTranscript_HoltburgDoorwayStill_MatchesRetailFrame2()
|
public void AlphaFlushCounts_HoltburgDoorwayStill_MatchesRetailFrame2()
|
||||||
=> RunAlphaFlushTranscriptGate("holtburg-doorway-still.alphadepth");
|
=> RunAlphaFlushCountsGate("holtburg-doorway-still.alphadepth");
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AlphaFlushSites_TerraceEdge_MatchesRetailFrame2()
|
||||||
|
=> RunAlphaFlushSitesGate("terrace-edge.alphadepth");
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
[Trait("Status", "KnownFailure")]
|
[Trait("Status", "KnownFailure")]
|
||||||
public void AlphaFlushTranscript_TerraceEdge_MatchesRetailFrame2()
|
public void AlphaFlushCounts_TerraceEdge_MatchesRetailFrame2()
|
||||||
=> RunAlphaFlushTranscriptGate("terrace-edge.alphadepth");
|
=> RunAlphaFlushCountsGate("terrace-edge.alphadepth");
|
||||||
|
|
||||||
private static string FormatPm(IReadOnlyList<(int Mode, int CounterBefore)> events) =>
|
private static string FormatPm(IReadOnlyList<(int Mode, int CounterBefore)> events) =>
|
||||||
"[" + string.Join(", ", events.Select(e => $"(mode={e.Mode},counterBefore={e.CounterBefore})")) + "]";
|
"[" + string.Join(", ", events.Select(e => $"(mode={e.Mode},counterBefore={e.CounterBefore})")) + "]";
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue