fix(render): S3 chunk 4 round 1 — loop-shape weather pin, one weather gate, dead terrain-clip stack deleted, sky drawn once

Campaign OVERHAUL S3 chunk 4 fix round 1 (docs/research/2026-09-01-overhaul/s3-walk-ownership-map.md
§10.4), on top of 6ba4b0b87. K1, K2, K3, K4, K6, K7, K8 (K5 was the lead's own plan-doc note, already
done).

K1 (blocking) — the Assert.Single pin over DrawLandscapeDynamicsPhase's DrawWeatherOnce call site
stayed green even wrapped in a foreach (it counts distinct call-site offsets, not loop shape). Added
a real LOOP-SHAPE pin reusing CompiledCallGraph.ReadBranches: asserts no backward branch (target
offset < its own offset) spans the DrawWeatherOnce call's IL offset — the shape every C#
for/foreach/while loop compiles to.

K2 (minor) — the Collect-time "OC" print fired on ctx.WeatherGateOpen alone while the Replay-time
draw independently re-derived clipAssembly.OutsideViewSlices.Length > 0, which could diverge on an
interior root whose landscape turn ran but whose reassembled outside-view slices ended up empty.
WalkFrameDriver now owns one WeatherTurnFired flag, set unconditionally inside OnWeatherTurn (so it
tracks "did the print's own gate hold" regardless of the transcript flag) and reset every
BeginFrame/AbortFrame; RetailPViewRenderer.DrawLandscapeDynamicsPhase gates DrawWeatherOnce on it
instead of re-deriving its own condition.

K3 (minor) — ClipFrame.SetTerrainClip (the terrain OutsideView writer) had no remaining caller after
chunk 4's original round and K4 below; deleted along with IWorldPassSurface.SetTerrainClip,
RhiWorldPassSurface.SetTerrainClip, the now-orphaned PublishTerrainClip helper, and PrepareClipFrame's
terrain-clip publish call. The terrain/sky shaders still declare the TerrainClip UBO (verified:
terrain_modern.vert, terrain_atmospheric.vert, sky.vert all read uTerrainClipCount/uTerrainClipPlanes),
so the section binding itself (WorldFrameSectionBinding.BindTerrainClip) stays — its existing
zeroed-ring fallback (for when nothing published the section) now binds that same all-zero disabled
block on every frame, identical bytes to the old permanent NoClip/Reset default.

K4 (major) — DrawWalkSky still looped the OUTDOOR case once per active landscape view under a doorway
scissor + BindTerrainClip + EnableClipDistances (the INTERIOR case already drew once unclipped, FW4
slice 6). Retail draws GameSky::Draw(sky,0) ONCE, unconditionally, before LScape::draw's block loop,
for both root kinds. Collapsed DrawWalkSky to one unconditional, unclipped draw; deleted
BeginDoorwayScissor and the RetailPViewPassExecutor.EnableClipDistances wrapper (both lost their only
caller). _surface.BeginScissor/EndScissor and IWorldPassSurface.EnableClipDistances stay:
RhiWorldPassSurface.ClearInteriorDepth still ends an active scissor, and WorldScenePassExecutor (the
separate flat-world path) still calls EnableClipDistances directly.

K6 (minor) — the punch-fan CPU/GPU equivalence pin hand-built a ClipViewSlice from ClipPlaneSet.From's
raw output, which could pass even if ClipFrameAssembler.Assemble's own packing/array-construction
diverged from that output. Rewrote it to build a real PortalVisibilityFrame and run it through
ClipFrameAssembler.Assemble, reading the planes back through assembly.OutsideViewSlices[0].Planes —
the exact outsideSlicesList.Add(new ClipViewSlice(slot, AabbOf(poly), planes)) path
ReassembleOutsideViewFromWalk (the walk's real interior-root producer) shares.

K7 (note) — restored the "a real look-in slice never reuses the reserved no-clip slot 0" assertion
the deleted VisibleClipSlotsInLookInTurn used to prove, via a new internal test-only accessor
(WalkFrameDriver.LookInSliceClipSlotAt) reading the same _lookInSlices storage
InteriorFloodViewClipPlanesAt resolves through _clipFrame.GetSlotPlanes(slice.ClipSlot).

K8 (note) — documented in DrawWeatherOnce's own comment that the weather mesh drawing before the rain
particles is this method's own call-order choice; retail's single GameSky::Draw(sky,1) imposes no
order between acdream's two substitutes.

Every new/changed pin's mutation was hand-verified this session (temporarily applied, ran the
specific test, confirmed the exact failing assertion, then reverted — see the parent task's structured
report for the four failing-assertion texts).

No register row added or removed — every change here deletes an acdream-only rule or repairs a pin;
none introduces a new deviation.

Full solution build: 0 warnings/0 errors. App hermetic (Lane!=InstalledDat/PreparedPackage/Live/
Manual/Timing/Windows/Linux/SystemFont & Purpose!=Diagnostic & Status!=KnownFailure): 6831/6831.
InstalledDat: 244 pass/1 skip/4 known — identical to 6ba4b0b87's own baseline (2x #383 layout tests,
TowerAscent, and the pre-existing #458 WalkLandscape.CheckBlocks block-visibility divergence,
unrelated to and untouched by this round).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-09-03 14:04:53 +02:00
parent 08e864ea40
commit ff607a1e04
10 changed files with 345 additions and 155 deletions

View file

@ -25,11 +25,15 @@
// Pure CPU byte-packing. The GL upload machinery this file used to carry
// alongside the packing (a per-flight-slot region SSBO + terrain UBO arena,
// reservation/upload-once bookkeeping, and their disposal) was deleted at
// Campaign V slice V11: the RHI arm (see RhiWorldPassSurface.PrepareClipFrame /
// SetTerrainClip in WorldPassSurface.cs) reads RegionBytes/TerrainBytes below and
// copies them into a frame ring allocation instead, so nothing here owns a GPU
// resource anymore. The byte layout is asserted by ClipFrameLayoutTests so a
// silent std430/std140 drift can't reach the GPU.
// Campaign V slice V11: the RHI arm (see RhiWorldPassSurface.PrepareClipFrame
// in WorldPassSurface.cs) reads RegionBytes below and copies it into a frame
// ring allocation instead, so nothing here owns a GPU resource anymore. S3
// chunk 4 fix round 1 (K3) deleted SetTerrainClip (its only writer) — the
// terrain UBO half of PrepareClipFrame's publication is gone with it;
// WorldFrameSectionBinding.BindTerrainClip's existing zeroed-ring fallback
// binds TerrainBytes's permanent all-zero state instead. The byte layout is
// asserted by ClipFrameLayoutTests so a silent std430/std140 drift can't
// reach the GPU.
using System;
using System.Numerics;
using System.Runtime.InteropServices;
@ -226,20 +230,18 @@ public sealed class ClipFrame : IDisposable
count * sizeof(float) * 4));
}
/// <summary>
/// Set the terrain OutsideView clip region (the single region the terrain
/// shader gates against). <paramref name="planes"/> length 0 ungates terrain
/// (count 0). U.3 callers never touch this — <see cref="NoClip"/> leaves it
/// at count 0. U.4 calls it with the OutsideView planes.
/// </summary>
public void SetTerrainClip(ReadOnlySpan<Vector4> planes)
{
int count = Math.Min(planes.Length, MaxPlanes);
Array.Clear(_terrainBytes);
WriteInt(_terrainBytes, 0, count);
for (int i = 0; i < count; i++)
WriteVec4(_terrainBytes, CellClipPlanesOffset + i * 16, planes[i]);
}
// S3 chunk 4 fix round 1 (K3): SetTerrainClip (the terrain OutsideView
// writer) is deleted — no production caller has written a non-empty
// terrain clip since S3 chunk 4's original round deleted the walk's
// per-outside-view-slice sky/terrain-clip loop, and fix round 1's K4
// deletes the sky loop's own now-dead SetTerrainClip wrapper too. Every
// KEEP clip (exit seals, punch fans) already read their planes through
// GetSlotPlanes above / ClipViewSlice.Planes directly, never through
// this terrain-specific slot. _terrainBytes therefore stays at its
// NoClip/Reset default (count 0, all zero) for the life of a frame —
// see WorldPassSurface.cs's PrepareClipFrame for how the TerrainClip
// UBO section still gets bound as that all-zero disabled block even
// with nothing left to publish it.
/// <summary>
/// No-op: this container owns no GPU resource of its own on the RHI arm —

View file

@ -11,65 +11,43 @@ namespace AcDream.App.Rendering;
/// methods — the cutover changes ORDER only.</summary>
internal sealed partial class RetailPViewPassExecutor
{
/// <summary>The walk's single sky turn: retail draws GameSky once
/// inside <c>LScape::draw</c>, clipped by the active views; looping
/// today's per-slice scissor + terrain-clip block reproduces today's
/// pixels while the driver sees ONE turn.</summary>
internal void DrawWalkSky(
RetailPViewFrameInput frame, ClipFrameAssembly clipAssembly)
/// <summary>The walk's single sky turn — retail draws GameSky exactly
/// once per frame, UNCLIPPED, for both root kinds:
/// <c>LScape::draw</c> @0x00506330 calls <c>GameSky::Draw(sky,0)</c>
/// @0x0050633c once, before the block loop, with no view clip of any
/// kind (view-culled landscape content — terrain, the sky dome, the
/// weather pass alike — is never GPU-clipped to a portal view; S3
/// chunk 3's terrain turn and chunk 1's weather turn already draw this
/// way).
///
/// <para>S3 chunk 4 fix round 1 (K4): this used to loop the OUTDOOR case
/// once per active landscape view under a doorway scissor + the terrain
/// clip block (FW4 slice 6 had already retired that loop for the
/// INTERIOR case, on the same reasoning this now applies uniformly) —
/// an interior root's exit-portal aperture exactness comes from the
/// depth clear, the exit seals, and the interior repaint (FW4 slice 6's
/// own argument), not a GPU clip on the sky dome; the same is true
/// outdoors, where there is no aperture at all to bound. The former
/// per-slice scissor left stale sky color wherever the authored
/// exit-portal bands were narrower than the real opening.</para></summary>
internal void DrawWalkSky(RetailPViewFrameInput frame)
{
if (!frame.RenderSky)
return;
// FW4 slice 6: an INTERIOR root draws the sky ONCE, unclipped —
// the same LScape::draw rule as the terrain turn below (view-culled
// landscape content is never view-clipped; the clear + seals +
// interior repaint own aperture exactness). The former per-slice
// scissor left stale sky color wherever the authored exit-portal
// bands were narrower than the real opening.
if (!frame.RootCell.IsOutdoorNode)
_sky?.RenderSky(
frame.Camera,
frame.CameraWorldPosition,
frame.DayFraction,
frame.ActiveDayGroup,
frame.SkyKeyframe,
frame.EnvironOverrideActive);
if (_particles is not null && _particleRenderer is not null)
{
_sky?.RenderSky(
_particleRenderer.Draw(
frame.Camera,
frame.CameraWorldPosition,
frame.DayFraction,
frame.ActiveDayGroup,
frame.SkyKeyframe,
frame.EnvironOverrideActive);
if (_particles is not null && _particleRenderer is not null)
{
_particleRenderer.Draw(
frame.Camera,
frame.CameraWorldPosition,
ParticleRenderPass.SkyPreScene);
}
return;
}
ReadOnlySpan<ClipViewSlice> slices = clipAssembly.OutsideViewSlices;
for (int sliceIndex = 0; sliceIndex < slices.Length; sliceIndex++)
{
ClipViewSlice slice = slices[sliceIndex];
bool scissor = BeginDoorwayScissor(slice.NdcAabb);
_surface.BindTerrainClip();
EnableClipDistances();
_sky?.RenderSky(
frame.Camera,
frame.CameraWorldPosition,
frame.DayFraction,
frame.ActiveDayGroup,
frame.SkyKeyframe,
frame.EnvironOverrideActive);
DisableClipDistances();
if (_particles is not null && _particleRenderer is not null)
{
_particleRenderer.Draw(
frame.Camera,
frame.CameraWorldPosition,
ParticleRenderPass.SkyPreScene);
}
if (scissor)
_surface.EndScissor();
ParticleRenderPass.SkyPreScene);
}
}
@ -160,11 +138,10 @@ internal sealed partial class RetailPViewPassExecutor
/// ORDER, never the leaf mechanics:
///
/// <list type="bullet">
/// <item><see cref="DrawSky"/> → the sky block of <c>DrawLandscapeSlice</c>,
/// looped over the active slices internally (retail draws GameSky once
/// inside <c>LScape::draw</c> clipped by the active views; the per-slice
/// scissor+clip here reproduces today's pixels while the driver still sees
/// ONE sky turn).</item>
/// <item><see cref="DrawSky"/> → S3 chunk 4 fix round 1's single unclipped
/// sky draw (retail's <c>GameSky::Draw(sky,0)</c> inside <c>LScape::draw</c>,
/// before the block loop) — one call, no scissor, no clip, for both root
/// kinds.</item>
/// <item><see cref="DrawLandCellBatch"/> → S3 chunk 3's per-land-cell
/// terrain turn, UNCLIPPED for both root kinds (retail never view-clips
/// terrain) — no scissor, no terrain clip, one call per order-merged
@ -232,7 +209,7 @@ internal sealed class WalkProductionLeafRenderer : IWalkFrameLeafRenderer
?? throw new ArgumentNullException(nameof(drawExitSeals));
}
public void DrawSky() => _passes.DrawWalkSky(_frame, _clipAssembly);
public void DrawSky() => _passes.DrawWalkSky(_frame);
public void DrawLandCellBatch(
IReadOnlyList<(uint LandblockId, int SideCellCount, int CellIndex)> cells) =>

View file

@ -214,6 +214,23 @@ public RetailPViewPassExecutor(
/// the end of the walk for an outdoor root) instead of wherever the GPU
/// draw happens to run at Replay.
/// </para>
/// <para>
/// S3 chunk 4 fix round 1 (K2): the caller (<see
/// cref="RetailPViewRenderer.DrawLandscapeDynamicsPhase"/>) now gates
/// this call on <c>WalkFrameDriver.WeatherTurnFired</c> — the walk's own
/// record of "a Landscape turn ran with the weather gate open" — rather
/// than re-deriving the gate independently, so this method's own
/// <see cref="ShouldDrawWeatherOnce(bool,bool,uint)"/> check is a
/// provably redundant safety net (the caller reads the same
/// <paramref name="frame"/>'s toggles the flag was computed from).
/// </para>
/// <para>
/// S3 chunk 4 fix round 1 (K8): the weather MESH draws before the rain
/// PARTICLE emitters below purely as this method's own call order —
/// retail's single <c>GameSky::Draw(sky,1)</c> call imposes no ordering
/// between acdream's two substitutes (the mesh and the particle
/// system), so this order is a bookkeeping choice, not a retail fact.
/// </para>
/// </summary>
public void DrawWeatherOnce(RetailPViewFrameInput frame)
{
@ -430,10 +447,16 @@ public RetailPViewPassExecutor(
return submitted;
}
private bool BeginDoorwayScissor(Vector4 ndcAabb) =>
_surface.BeginScissor(ndcAabb);
private void EnableClipDistances() => _surface.EnableClipDistances();
// S3 chunk 4 fix round 1 (K4): BeginDoorwayScissor and the
// EnableClipDistances wrapper are deleted — their only caller was
// DrawWalkSky's per-outside-view-slice loop (RetailPViewPassExecutor.
// WalkLeaf.cs), itself deleted by the same fix (retail draws the sky
// ONCE, unclipped, exactly like the terrain and the weather — see
// DrawWalkSky's own doc comment). _surface.BeginScissor/EndScissor and
// IWorldPassSurface.EnableClipDistances stay: RhiWorldPassSurface.
// ClearInteriorDepth still ends an active scissor, and
// WorldScenePassExecutor (the separate flat-world path, out of this
// chunk's scope) still calls EnableClipDistances directly.
private void DisableClipDistances() => _surface.DisableClipDistances();
}

View file

@ -451,7 +451,8 @@ internal sealed class RetailPViewRenderer
DrawLandscapeDynamicsPhase(
ctx,
passes,
clipAssembly);
clipAssembly,
walkDriver);
};
try
{
@ -461,7 +462,8 @@ internal sealed class RetailPViewRenderer
DrawLandscapeDynamicsPhase(
ctx,
passes,
clipAssembly);
clipAssembly,
walkDriver);
}
}
finally
@ -624,16 +626,17 @@ internal sealed class RetailPViewRenderer
private void DrawLandscapeDynamicsPhase(
RetailPViewFrameInput ctx,
RetailPViewPassExecutor passes,
ClipFrameAssembly clipAssembly)
ClipFrameAssembly clipAssembly,
Walk.WalkFrameDriver walkDriver)
{
if (clipAssembly.OutsideViewSlices.Length == 0)
return;
// Ownerless OUTDOOR-cell emitters — now unconditional: the old
// hasBuildingLookIns gate only existed to sequence this submission
// around the OLD static barrier drains, which no longer run here
// (the walk owns its own alpha barriers — WalkFrameDriver.OnBuildingTurn).
passes.DrawUnattachedSceneParticles(ctx, outdoorCells: true);
if (clipAssembly.OutsideViewSlices.Length != 0)
{
// Ownerless OUTDOOR-cell emitters — now unconditional: the old
// hasBuildingLookIns gate only existed to sequence this submission
// around the OLD static barrier drains, which no longer run here
// (the walk owns its own alpha barriers — WalkFrameDriver.OnBuildingTurn).
passes.DrawUnattachedSceneParticles(ctx, outdoorCells: true);
}
// S3 chunk 4 (§10.2): the former per-outside-view-slice loop
// (SetTerrainClip + ClearClipRouting + DrawLandscapeSliceLate, one
@ -648,7 +651,19 @@ internal sealed class RetailPViewRenderer
// emitter. The OC transcript print no longer lives here — it moved
// to WalkFrameDriver.OnWeatherTurn, fired by RetailFrameWalk.
// DrawLandscape at Collect time (S3 chunk 4, O3).
passes.DrawWeatherOnce(ctx);
//
// S3 chunk 4 fix round 1 (K2): gated on walkDriver.WeatherTurnFired
// — the SAME predicate that gated the Collect-time "OC" transcript
// print (WalkFrameDriver.OnWeatherTurn, fired only when a Landscape
// turn actually ran with ctx.WeatherGateOpen open) — instead of
// independently re-deriving "clipAssembly.OutsideViewSlices.Length
// > 0", which could diverge from the print's gate on an interior
// root whose landscape turn ran but whose reassembled outside-view
// slices ended up empty (the transcript would then report a weather
// turn this call never actually drew). Gate closed -> neither the
// print nor this draw fires; gate open -> both do.
if (walkDriver.WeatherTurnFired)
passes.DrawWeatherOnce(ctx);
}
/// <summary>Campaign FW4 slice 2 — the walk-flood seal draw. Retail's

View file

@ -597,6 +597,23 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
private readonly List<int> _floodViewRouteScratch = new();
private WalkPlane _lookInCyPlane;
/// <summary>S3 chunk 4 fix round 1 (K2): set exactly when <see
/// cref="IWalkEventSink.OnWeatherTurn"/> fires this frame — i.e. a
/// Landscape turn actually ran (retail's <c>RetailFrameWalk.DrawLandscape</c>
/// only reaches that call for an outdoor root, or an interior root whose
/// flood still has a surviving exit view) AND <c>ctx.WeatherGateOpen</c>
/// held. This is "the walk's own predicate" the S3 chunk-4 fix round 1
/// unifies the transcript print and the Replay-time weather draw
/// on — before this, the print's own gate (<c>ctx.WeatherGateOpen</c>
/// alone) and the draw's gate (<c>clipAssembly.OutsideViewSlices.Length
/// &gt; 0</c>, re-derived independently at Replay) could diverge on an
/// interior root whose landscape turn ran but whose reassembled outside-
/// view slices ended up empty, letting the transcript report a weather
/// turn the frame never actually drew. Reset every Collect
/// (<see cref="BeginFrame"/>/<see cref="AbortFrame"/>); read at Replay
/// by <see cref="AcDream.App.Rendering.RetailPViewRenderer.DrawLandscapeDynamicsPhase"/>.</summary>
internal bool WeatherTurnFired { get; private set; }
/// <summary>S3 chunk 3 (§9.2 B2), fix round 1 (F2), round 2 (F10):
/// <see cref="Replay"/>'s PENDING terrain batch — every
/// <see cref="WalkFrameEventKind.LandCell"/> event since the last flush,
@ -693,6 +710,25 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
return routeIndex;
}
/// <summary>Test-only accessor (S3 chunk 4 fix round 1, K7): the packed
/// GPU clip slot one look-in route's slice was assigned by <see
/// cref="CaptureViews"/>'s call to <see cref="AppendClipSlot"/> — the
/// SAME value <see cref="InteriorFloodViewClipPlanesAt"/> resolves
/// through <c>_clipFrame.GetSlotPlanes(slice.ClipSlot)</c> for the root-
/// flood case. Restores the "a real look-in slice never reuses the
/// reserved no-clip slot 0" pin the deleted <c>VisibleClipSlotsInLookInTurn</c>
/// used to prove. <paramref name="routeIndex"/> indexes <c>_lookInTurns</c>
/// directly, matching <see cref="SphereVisibleInLookInTurn"/>'s own
/// parameter (not the flood-cell indirection <see
/// cref="InteriorFloodViewClipPlanesAt"/> goes through).</summary>
internal uint LookInSliceClipSlotAt(int routeIndex, int sliceOffset = 0)
{
WalkLookInTurn turn = _lookInTurns[routeIndex];
if ((uint)sliceOffset >= (uint)turn.SliceCount)
throw new ArgumentOutOfRangeException(nameof(sliceOffset));
return _lookInSlices[turn.SliceStart + sliceOffset].ClipSlot;
}
internal List<WalkBuilding> VisitedBuildings { get; } = new();
internal HashSet<uint> VisitedLandscapeCellIds { get; } = new();
@ -764,6 +800,7 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
_landscapeTurnsThisFrame = 0;
_currentDcStage = null;
_readyToReplay = false;
WeatherTurnFired = false;
_stream.Reset();
_events.Clear();
_markPositions.Clear();
@ -917,6 +954,7 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
_landscapeTurnsThisFrame = 0;
_currentDcStage = null;
_readyToReplay = false;
WeatherTurnFired = false;
_stream.Reset();
_events.Clear();
_markPositions.Clear();
@ -1550,20 +1588,25 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
/// <summary>S3 chunk 4 (O3): retail's <c>GameSky::Draw(sky,1)</c>
/// @0x00506ff0 weather pass — <see cref="RetailFrameWalk.DrawLandscape"/>
/// only calls this when its own gate already held, so this handler is
/// print-only, matching <see cref="IWalkEventSink.OnSortCellTurn"/>'s own
/// shape (no stream side effect; the flag gate wraps the entire body).
/// The GPU draw stays where chunk 1 round 2 put it — <see
/// cref="RetailPViewPassExecutor.DrawWeatherOnce"/>, called directly
/// from <see cref="RetailPViewRenderer.DrawLandscapeDynamicsPhase"/> at
/// Replay time — this hook exists ONLY to move the "OC" transcript print
/// to retail's real Collect-time turn.</summary>
/// only calls this when its own gate already held, so this handler
/// records <see cref="WeatherTurnFired"/> unconditionally (S3 chunk 4
/// fix round 1, K2 — the one predicate the Replay-time draw now reads,
/// so a landscape turn that ran with the weather gate open can never be
/// under-reported to the draw side) and is otherwise print-only,
/// matching <see cref="IWalkEventSink.OnSortCellTurn"/>'s own shape (no
/// stream side effect). The GPU draw stays where chunk 1 round 2 put
/// it — <see cref="RetailPViewPassExecutor.DrawWeatherOnce"/>, called
/// directly from <see cref="RetailPViewRenderer.DrawLandscapeDynamicsPhase"/>
/// at Replay time, now gated on THIS flag instead of re-deriving its
/// own independent condition.</summary>
void IWalkEventSink.OnWeatherTurn(uint viewerCellId)
{
RequireOpenFrame();
WeatherTurnFired = true;
if (!AcDream.Core.Rendering.RenderingDiagnostics.DumpWalkTranscriptEnabled)
return;
RequireOpenFrame();
WalkTranscriptDump.PrintObjectCellTurn(viewerCellId);
}

View file

@ -29,8 +29,8 @@ internal interface IRenderFrameGlState
internal interface IWorldPassSurface
{
/// <summary>
/// Publishes this frame's per-cell clip-region table and terrain clip block,
/// and routes both to the renderers that read them.
/// Publishes this frame's per-cell clip-region table, and routes it to the
/// renderers that read it.
///
/// <para><paramref name="terrainUploadCount"/> is how many distinct terrain
/// clip blocks the frame will issue. GL reserves that many arena records
@ -38,12 +38,21 @@ internal interface IWorldPassSurface
/// slice can still reference it is the hazard the reservation exists for. The
/// RHI arm ignores it: a ring allocation is distinct memory by construction
/// and lives until the frame retires.</para>
///
/// <para>S3 chunk 4 fix round 1 (K3): this no longer also publishes the
/// terrain clip block — <c>ClipFrame.SetTerrainClip</c>, its only
/// writer, is deleted, so <see cref="ClipFrame.TerrainBytes"/> stays at
/// its permanent NoClip/Reset default (count 0, all zero) for the life of
/// a frame. The terrain/sky shaders still declare the <c>TerrainClip</c>
/// UBO (binding=2), so <see cref="WorldFrameSectionBinding.BindTerrainClip"/>'s
/// existing zeroed-ring fallback (used whenever nothing published the
/// section) now binds that all-zero disabled block on every frame instead
/// of only when something raced the publish — the observable result is
/// identical either way, since a zeroed publish and a zeroed fallback are
/// the same bytes.</para>
/// </summary>
void PrepareClipFrame(int terrainUploadCount);
/// <summary>Replaces the terrain clip planes and republishes the block.</summary>
void SetTerrainClip(ReadOnlySpan<Vector4> planes);
/// <summary>
/// Re-asserts the terrain clip block at its binding.
///
@ -122,13 +131,12 @@ internal sealed class RhiWorldPassSurface : IWorldPassSurface
_scope.Sections.ClipRegions = Publish(
_clipFrame.RegionBytes,
GpuRingUsage.Storage);
PublishTerrainClip();
}
public void SetTerrainClip(ReadOnlySpan<Vector4> planes)
{
_clipFrame.SetTerrainClip(planes);
PublishTerrainClip();
// S3 chunk 4 fix round 1 (K3): the terrain-clip publish half is
// deleted — SetTerrainClip (its only writer) is gone, so publishing
// ClipFrame.TerrainBytes here would only ever republish permanent
// zero bytes. WorldFrameSectionBinding.BindTerrainClip's existing
// zeroed-ring fallback (for when nothing published the section)
// binds that same all-zero disabled block instead.
}
/// <summary>No-op: there is no ambient binding to re-assert. See the interface.</summary>
@ -176,11 +184,6 @@ internal sealed class RhiWorldPassSurface : IWorldPassSurface
_scope.ClearInteriorDepth();
}
private void PublishTerrainClip() =>
_scope.Sections.TerrainClip = Publish(
_clipFrame.TerrainBytes,
GpuRingUsage.Uniform);
private GpuBufferSection Publish(ReadOnlySpan<byte> data, GpuRingUsage usage)
{
IGpuFrame frame = _frames.CurrentFrame