Chunk H of Campaign OVERHAUL v2's S3 walk-ownership program (docs/research/2026-09-01-overhaul/s3-walk-ownership-map.md §10.6), landing the three post-hoc three-lens findings on S3 chunk 4 round 2. Pure dead-state deletion, comment truth, one test deletion, one stronger pin — no admission, order, or behavior change; no new flag or probe; no register row (AD-17's correction is the lead's own, already on the campaign branch). H1 — ClipPlaneSet.cs: deleted ScissorNdcAabb (no production reader — only AppendOutsideSlice consumes ClipPlaneSet, and it reads only IsNothingVisible/Count/PlaneArray) and renamed UseScissorFallback -> IsPlaneOverflow with a doc stating the true consumer contract: Count==0 overflow means the region draws UNCLIPPED via the no-clip slot, never a scissor box. Rewrote the file header's convexity-rule and Count==0-states sections to the two states that exist (Empty / overflow) instead of three, and simplified the private Scissor(...) AABB-computing helpers into a parameterless Overflow() factory since nothing reads the AABB any more. ClipPlaneSetTests.cs: renamed every UseScissorFallback reference, deleted the AABB assertions (From_MultiplePolygons's four ScissorNdcAabb.X/Y/Z/W checks, Empty_StaticProperty_DrawsNothing's degenerate-box check, ScissorFallback_IsNotNothingVisible's bounds check) that pinned the deleted consumer contract. H2 — TerrainModernRenderer.cs: deleted Draw's clipPlanes/ndcClipAabb parameters (grep: TerrainModernRenderer.Draw has exactly one production caller, WorldScenePassExecutor.DrawFlatTerrain, and it never passed either), which made CollectVisibleCells's own clipPlanes/ndcClipAabb/ viewProjection parameters and the IsAabbVisibleThroughClipRegion + IsAabbOutsideHomogeneousPlane helper pair they fed entirely dead — deleted along with the stale "Retail publishes landcell in_view from the clipped landscape view" comment; CollectVisibleCells is frustum-only now, matching DrawLandCells's own "retail never view-clips terrain" doc. Deleted the terrainUploadCount computation at RetailPViewRenderer.cs (the DrawInside/DrawLandscapeDynamicsPhase caller) and PrepareClipFrame's GL-era reservation-count parameter end to end: IWorldPassSurface.PrepareClipFrame(int), RhiWorldPassSurface's matching body and its now-pointless "_ = terrainUploadCount" discard, RetailPViewPassExecutor.PrepareClipFrame's pass-through, and WorldScenePassExecutor.PrepareFlatWorldClip's hardcoded `1` argument — PrepareClipFrame() takes nothing on every arm. TerrainParticleCellVisibilityTests.cs: deleted the three tests that only existed to pin the removed clip-plane/scissor mechanism (RejectsCellsOutsideDoorwayClipPlanes, RejectsCellsOutsideDoorwayScissorAabb, UnionsCellsFromEveryLandscapeSlice) and updated the two surviving frustum tests to the new signature. H3 — deleted tests/AcDream.App.Tests/Rendering/Issue130DoorwayStripTests.cs outright. #130 was a background-color strip along a doorway's top edge from a since-fixed under-inclusion in the CPU clip pipeline; its scissor half was already retired at S3 chunk 4 fix round 2 (L7), and its surviving plane-gap half exercised only PortalVisibilityBuilder.Build and ClipFrameAssembler.Assemble, both confirmed zero-production-caller legacy paths (grep) — the one production stage it actually touched, ClipPlaneSet.From, is already pinned through the real producers (BeginWalkFrame/ReassembleOutsideViewFromWalk) by the K6/L4 punch-fan equivalence pin in ClipFrameLayoutTests, so nothing production-relevant lost coverage. H4 — comment truth pass: - ClipFrameAssembler.cs: rewrote the file-header "renderer uses scissor for passes that need that fallback" line and the TerrainClipMode summary's "that path never produces Scissor" clause. Deleted the TerrainClipMode enum's Scissor member — grepped every reader first (WorldRenderDiagnostics.cs, WorldSceneDiagnosticsController.cs, WorldSceneRendererTests.cs all only pass the value through to a string Append or a mock parameter; WorldSceneRenderer.cs, the only writer, sets only Planes (default) or Skip, never Scissor) — kept Planes/Skip for the flat path and corrected Skip's stale doc ("No outside_view slice is visible; skip landscape indoors", a leftover from the pre-round-2 walk meaning) to what the flat path's own writer actually means now ("the PView walk ran instead of the flat-world path this frame"). Corrected ClipViewSlice's doc: the AABB is retained for WorldRenderDiagnostics only, not for a scissor consumer. - WorldPassSurface.cs: "Four concerns... the clip-frame publication, the doorway scissor, gl_ClipDistance enablement, and retail's interior depth clear" -> three concerns (the doorway scissor was retired at L2), with a pointer to ClearInteriorDepth's own truthful doc. EnableClipDistances' "all three world vertex shaders already write 1.0" -> only portal_depth.vert writes gl_ClipDistance now (grep: sky.vert and terrain_modern.vert/terrain_atmospheric.vert no longer declare it at all — L3 deleted the block outright; portal_depth.vert still writes 1.0 past its active clip count). RhiWorldPassSurface's class doc dropped "the scissor is dynamic state on the borrowed encoder" (no producer left). - RetailPViewPassExecutorTests.cs: DrawLandscapeDynamicsPhase_CallsDraw WeatherOnceExactlyOnce's doc corrected "unconditionally" (false since K2) to "conditional on WalkFrameDriver.WeatherTurnFired, see the L1 pin". Added the switch-table scope note (matching L8's own note on DrawWalkSky_RenderSkyCallSiteHasNoEnclosingBackwardBranch) to the K1 pin's doc (DrawLandscapeDynamicsPhase_DrawWeatherOnceCallSiteHasNoEnclosing BackwardBranch). - RetailPViewRenderer.cs: the DrawWeatherOnce citation cited the callee address (GameSky::Draw(sky,1) @0x00506ff0) where §10.1 cites the call site (@0x00506396) — corrected to cite the call site with the callee address alongside it. H5 — MUTATION CHECK (mandatory, run against the production gate at RetailPViewRenderer.DrawLandscapeDynamicsPhase): Added DrawLandscapeDynamicsPhase_ExactlyOneBranchGuardsDrawWeatherOnce, which widens the L1 pin's window to start at the DrawUnattachedSceneParticles call (the last call before the whole gate) and asserts EXACTLY ONE branch total in that window — the L1 pin's own window (getter-call to draw-call) misses a conjoined gate's extra, earlier condition because that condition's own brfalse lands BEFORE the getter call's offset. - Mutation 1 (the conjoined gate this pin exists to reject): changed the gate to `if (clipAssembly.OutsideViewSlices.Length != 0 && walkDriver.WeatherTurnFired)`, built, ran the new pin — FAILED with: Assert.Single() Failure: The collection contained 2 matching items Expected: (predicate expression) Collection: [CompiledBranch { Offset = 7, OpCode = brfalse.s, TargetOffset = 17 }, CompiledBranch { Offset = 24, OpCode = brfalse.s, TargetOffset = 42 }, CompiledBranch { Offset = 33, OpCode = brfalse.s, TargetOffset = 42 }] Match indices: 1, 2 (offset 7 = the DrawUnattachedSceneParticles block's own guard, correctly excluded since it precedes that call's offset; offsets 24 and 33 are the conjoined gate's two brfalse's, both inside the widened window — exactly the failure this pin exists to catch). Restored the single-flag gate; rebuilt; the new pin and all 13 sibling tests in RetailPViewPassExecutorTests pass again (14/14). - Mutation M1 (the existing round-2 regression: restore the pre-fix gate `if (clipAssembly.OutsideViewSlices.Length != 0)`), built, ran the full suite — the L1 pin (DrawLandscapeDynamicsPhase_GatesDrawWeatherOnceOnWalkDriverWeatherTurnFired) still FAILS, as required, with: Assert.Equal() Failure: Values differ Expected: typeof(AcDream.App.Rendering.Walk.WalkFrameDriver) Actual: typeof(AcDream.App.Rendering.ClipFrameAssembly) (the new H5 pin does NOT fail under M1 — a single-flag gate still produces exactly one branch in the widened window, which is correct: catching "wrong condition source" is L1's job, catching "an extra conjoined condition" is H5's). Restored the correct gate; rebuilt; 14/14 green again. H6 grep sweep (extends §10.5's L6 sweep per the plan): grep -rnE "ScissorNdcAabb|UseScissorFallback|TerrainClipMode\.Scissor|ndcClipAabb|terrainUploadCount" --include=*.cs --include=*.vert --include=*.frag src tests -> empty (every mention, including in this commit's own explanatory comments, was rephrased to avoid the literal deleted identifiers). The base L6 sweep (SetTerrainClip|BeginDoorwayScissor|BindTerrainClip| TerrainBytes|active scissor|TerrainClipMode\.Scissor|NdcScissorRect) is also still empty. Gates: dotnet build tests/AcDream.App.Tests -c Release -> 0 warnings / 0 errors. Hermetic lane -> 6825/6825 passed, 0 failed. InstalledDat lane (ACDREAM_DAT_DIR set) -> 243 passed / 4 known failures (2x #383 layout tests, TowerAscentReplayTests's TowerAscent KnownFailure, WalkTraceConformanceTests.Oh_doorway_still_first_frame_diff #458 KnownFailure) / 1 skipped / 248 total — one fewer test than before H3, as expected from Issue130DoorwayStripTests's deletion. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
197 lines
8.3 KiB
C#
197 lines
8.3 KiB
C#
using AcDream.App.Rendering.Gpu;
|
|
|
|
namespace AcDream.App.Rendering;
|
|
|
|
/// <summary>
|
|
/// Restores the frame-global rendering convention shared by frame clear and
|
|
/// exceptional world-pass rollback. The raw-GL implementation this contract
|
|
/// used to have alongside it (<c>RenderFrameGlStateController</c>) was deleted
|
|
/// at Campaign V slice V11; <see cref="NullRenderFrameGlState"/> is the only
|
|
/// implementation left.
|
|
/// </summary>
|
|
internal interface IRenderFrameGlState
|
|
{
|
|
void RestoreFrameDefaults();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Campaign V slice V6j: the small graphics surface the two world pass executors
|
|
/// touch directly, expressed once so their ordering logic — which is retail's,
|
|
/// and heavily tested — is written once for both backends.
|
|
///
|
|
/// <para>Everything else those executors do is delegation to a renderer. What is
|
|
/// left is exactly this: the clip-frame publication, <c>gl_ClipDistance</c>
|
|
/// enablement, and retail's interior depth clear. Three concerns, each of
|
|
/// which genuinely differs between GL and Vulkan, and none of which is
|
|
/// expressible on the pinned RHI contract. (S3 landing hygiene, H4: the
|
|
/// doorway scissor this used to also cover was retired at S3 chunk 4 fix
|
|
/// round 2 — see <see cref="ClearInteriorDepth"/>'s implementation doc.)</para>
|
|
/// </summary>
|
|
internal interface IWorldPassSurface
|
|
{
|
|
/// <summary>
|
|
/// Publishes this frame's per-cell clip-region table, and routes it to the
|
|
/// renderers that read it.
|
|
///
|
|
/// <para>S3 chunk 4 fix round 1 (K3) deleted the walk's per-frame terrain
|
|
/// screen-space clip publish this used to also carry — its only writer
|
|
/// was retired along with the per-outside-view-slice terrain/sky/weather
|
|
/// clip loop — and fix round 2 (L3) deleted the terrain/sky shaders' own
|
|
/// declaration of that clip block along with the section binder that used
|
|
/// to re-assert it, so this contract now publishes exactly one section,
|
|
/// the per-cell clip-region table.</para>
|
|
///
|
|
/// <para>S3 landing hygiene (H2) deleted the GL-era reservation-count
|
|
/// parameter this used to also take (how many distinct terrain clip
|
|
/// blocks the frame would issue): the RHI arm always ignored it (a ring
|
|
/// allocation is distinct memory by construction and lives until the
|
|
/// frame retires), and the last GL implementation that read it
|
|
/// (<c>GlWorldPassSurface</c>) was deleted at Campaign V slice V11 —
|
|
/// nothing was left to size a reservation for.</para>
|
|
/// </summary>
|
|
void PrepareClipFrame();
|
|
|
|
/// <summary>
|
|
/// Enables every <c>gl_ClipDistance</c> slot.
|
|
///
|
|
/// <para>Vulkan activates every element the shader declares and has no
|
|
/// enable, so this is a no-op there — and that is safe rather than a
|
|
/// divergence: only <c>portal_depth.vert</c> writes <c>gl_ClipDistance</c>
|
|
/// in the world pass now (S3 landing hygiene, H4 — S3 chunk 4 fix round 2
|
|
/// (L3) deleted the sky and terrain shaders' own declaration of the block
|
|
/// this contract used to also publish; see <see cref="PrepareClipFrame"/>'s
|
|
/// doc). It still writes <c>1.0</c> ("keep everything") into every slot
|
|
/// past its active count, so a frame with no clip planes clips nothing on
|
|
/// either backend (plan §5.5.14 item 4).</para>
|
|
/// </summary>
|
|
void EnableClipDistances();
|
|
|
|
/// <summary>Disables every <c>gl_ClipDistance</c> slot. See <see cref="EnableClipDistances"/>.</summary>
|
|
void DisableClipDistances();
|
|
|
|
/// <summary>
|
|
/// Retail's interior depth clear, between the landscape slice and the
|
|
/// interior cells (<c>PView::DrawCells @ 0x005A4840</c>). Retail gates it
|
|
/// on <c>outside_view.view_count > 0</c> AND a nonzero persistent
|
|
/// <c>portalsDrawnCount</c> (S3 chunk 2); the walk driver decides, this
|
|
/// seam only executes.
|
|
/// </summary>
|
|
void ClearInteriorDepth();
|
|
}
|
|
|
|
/// <summary>
|
|
/// The clip table is a ring section published on the world pass scope, and the
|
|
/// depth clear is a scoped <c>vkCmdClearAttachments</c>. The raw-GL
|
|
/// implementation this used to sit alongside (<c>GlWorldPassSurface</c>) was
|
|
/// deleted at Campaign V slice V11.
|
|
/// </summary>
|
|
internal sealed class RhiWorldPassSurface : IWorldPassSurface
|
|
{
|
|
private readonly IWorldPassScope _scope;
|
|
private readonly ICurrentGpuFrameSource _frames;
|
|
private readonly ClipFrame _clipFrame;
|
|
|
|
public RhiWorldPassSurface(
|
|
IWorldPassScope scope,
|
|
ICurrentGpuFrameSource frames,
|
|
ClipFrame clipFrame)
|
|
{
|
|
_scope = scope ?? throw new ArgumentNullException(nameof(scope));
|
|
_frames = frames ?? throw new ArgumentNullException(nameof(frames));
|
|
_clipFrame = clipFrame ?? throw new ArgumentNullException(nameof(clipFrame));
|
|
}
|
|
|
|
public void PrepareClipFrame()
|
|
{
|
|
_scope.Sections.ClipRegions = Publish(
|
|
_clipFrame.RegionBytes,
|
|
GpuRingUsage.Storage);
|
|
}
|
|
|
|
/// <summary>No-op: Vulkan activates every declared clip distance. See the interface.</summary>
|
|
public void EnableClipDistances()
|
|
{
|
|
}
|
|
|
|
/// <summary>No-op: Vulkan activates every declared clip distance. See the interface.</summary>
|
|
public void DisableClipDistances()
|
|
{
|
|
}
|
|
|
|
public void ClearInteriorDepth()
|
|
{
|
|
// S3 chunk 4 fix round 2 (L2): BeginScissor/EndScissor are deleted —
|
|
// no producer of a narrowed scissor rectangle remains anywhere in the
|
|
// walk (K4 deleted the doorway scissor that used to bracket the sky;
|
|
// fix round 1 already deleted the terrain/weather per-slice scissor
|
|
// loop). VulkanGpuPassEncoder's constructor sets the full-attachment
|
|
// scissor exactly once, at pass begin (VulkanGpuPassEncoder.cs:87),
|
|
// and nothing narrows it after that any more, so
|
|
// vkCmdClearAttachments's own unscissored clear and every draw after
|
|
// it already cover the whole target with no bracket needed here.
|
|
_scope.ClearInteriorDepth();
|
|
}
|
|
|
|
private GpuBufferSection Publish(ReadOnlySpan<byte> data, GpuRingUsage usage)
|
|
{
|
|
IGpuFrame frame = _frames.CurrentFrame
|
|
?? throw new InvalidOperationException(
|
|
"The world clip frame requires an open IGpuFrame (see GpuDeviceFrameLifetime).");
|
|
// A logically empty table still reserves one slot so the bound range is
|
|
// never zero-length — the same rule the light buffers already state.
|
|
int byteCount = Math.Max(data.Length, ClipFrame.CellClipStrideBytes);
|
|
GpuRingAllocation allocation = frame.AllocateRing(byteCount, usage);
|
|
allocation.Data.Clear();
|
|
if (!data.IsEmpty)
|
|
data.CopyTo(allocation.Data);
|
|
return new GpuBufferSection(
|
|
allocation.Buffer,
|
|
allocation.OffsetBytes,
|
|
(uint)byteCount);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Campaign V slice V6j: the frame-default restore on a backend with no ambient
|
|
/// state to restore.
|
|
///
|
|
/// <para>Both executors call <see cref="IRenderFrameGlState.RestoreFrameDefaults"/>
|
|
/// when aborting a failed frame. On Vulkan every piece of state that call
|
|
/// re-establishes is either baked into a pipeline or set per draw, so there is
|
|
/// nothing to put back and saying so is more honest than composing a GL
|
|
/// controller that would have no context to talk to.</para>
|
|
/// </summary>
|
|
internal sealed class NullRenderFrameGlState : IRenderFrameGlState
|
|
{
|
|
public static NullRenderFrameGlState Instance { get; } = new();
|
|
|
|
private NullRenderFrameGlState()
|
|
{
|
|
}
|
|
|
|
public void RestoreFrameDefaults()
|
|
{
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Campaign V slice V6j: the GL state reader on a backend with no GL state.
|
|
///
|
|
/// <para><c>WorldRenderDiagnostics</c> reads live GL state for explicitly enabled
|
|
/// probes only. Every snapshot below is the truthful answer for a Vulkan frame —
|
|
/// there is no ambient capability state to sample — which keeps every other
|
|
/// diagnostic the class emits (render signature, PView input, out-stage routing,
|
|
/// phantom objects) working unchanged on both backends.</para>
|
|
/// </summary>
|
|
internal sealed class NullRenderGlStateReader : IRenderGlStateReader
|
|
{
|
|
public static NullRenderGlStateReader Instance { get; } = new();
|
|
|
|
private NullRenderGlStateReader()
|
|
{
|
|
}
|
|
|
|
public RenderGlStateSnapshot CaptureState() => default;
|
|
|
|
public RenderGlScissorSnapshot CaptureScissor() => default;
|
|
}
|