refactor(render): S3 chunk 4 — delete the clip rules retail does not have and the cathedral discriminators
Campaign OVERHAUL S3 chunk 4 (docs/research/2026-09-01-overhaul/s3-walk-ownership-map.md §10). Retail
polygon-clips exactly three things on the CPU — portal polygons (PView::GetClip), punch fans
(DrawPortalPolyInternal against building_view), and exit seals (setup_view + polyClipFinish);
ordinary meshes, cell shells, terrain, particles and the weather are never view-clipped. Every
acdream-only clip rule beyond those three is deleted; the two that remain get a new CPU/GPU
equivalence pin.
DELETED (no retail counterpart):
- RetailPViewRenderer.DrawLandscapeDynamicsPhase's per-outside-view-slice weather loop
(SetTerrainClip + ClearClipRouting + the old DrawLandscapeSliceLate leaf, one call per active
landscape view) — retail's GameSky::Draw(sky,1) @0x00506ff0 runs ONCE, unclipped, after
LScape::draw's whole block loop. RetailPViewPassExecutor.DrawWeatherOnce stays the one call site;
it now also submits the rain particles (ParticleRenderPass.SkyPostScene) as ONE unclipped
submission, folded in from the deleted loop's per-slice clipSlot draw.
- RetailPViewPassExecutor.DrawLandscapeSliceLate, RetailPViewLandscapeLateSliceContext,
SetTerrainClip (the walk-level wrapper — its only caller was the deleted loop),
ClearClipRouting (same), SetCellShellClipRouting (already had no caller), and
UseIndoorMembershipOnlyRouting (4 call sites in RetailPViewRenderer.cs) — all "first view clips
the shell" residue: SetCellShellClipRouting was the ONLY place EnvCellRenderer.SetClipRouting
ever received a non-null value anywhere in the app, so once it's gone,
UseIndoorMembershipOnlyRouting's own clear calls are provably permanent no-ops. AbortFrame's
matching TryAbort(SetClipRouting(null))/TryAbort(ClearClipRouting) lines are dropped for the
same reason. KEPT: BeginDoorwayScissor, _surface.BindTerrainClip(), EnableClipDistances/
DisableClipDistances, and IWorldPassSurface.SetTerrainClip/ClipFrame.SetTerrainClip/
PrepareClipFrame's terrain-clip publish — all still have real consumers (DrawWalkSky's per-slice
outdoor sky mesh draw; the always-bound TerrainClip UBO section) or are shared classic (non-walk)
infrastructure out of this chunk's ownership (WorldScenePassExecutor, PortalTunnelPresentation).
- WbDrawDispatcher.WalkClassify.ResolvePartClipSlots (renamed ResolvePartVisible, returns bool) and
RetailWholeMeshSlot: the per-view clip-slot LIST always collapsed to either nothing or one
element (RenderDeviceD3D::DrawMesh @0x005A0860 submits the whole mesh once any view admits it) —
never anything but a Boolean wearing a list-shaped costume. IWalkLookInViewSource.
VisibleClipSlotsInLookInTurn is deleted outright (no consumer besides that collapse);
SphereVisibleInLookInTurn gained VisibleClipSlotsInLookInTurn's own testSphere parameter so the
no-authored-sphere case ("any admitted view, geometry ignored") still has a home.
EmitClassifiedBatches now emits exactly one batch per surviving TryClassifyBatch using the
entity's own classic slot (always 0 while _clipRoutingActive is never armed by the walk path).
- The six ProbeCathedralSkip*/ProbeCathedralShellOrderEnabled discriminators (RenderingDiagnostics,
their six docs/launch-options.md rows, every read site in WalkFrameDriver/
RetailPViewPassExecutor.WalkLeaf/RetailPViewRenderer, and the WalkFrameDriver.
TraceCathedralShellOrder method + RetailPViewRenderer._probeCathedralShellOrderFrame it served) —
the investigation is closed: the cathedral leak is retail behavior, not an acdream defect to
discriminate.
O1-O4 (the ledger's carried chunk-1 items, closed here because this chunk owns the weather path):
IWalkEventSink gains OnWeatherTurn(viewerCellId), fired by RetailFrameWalk.DrawLandscape
UNCONDITIONALLY at retail's own call-site shape (GameSky::Draw(sky,1) is itself unconditional from
LScape::draw; the is_player_outside gate lives inside it) but gated by the walk context's own new
WeatherGateOpen/ViewerCellId members (default false/0, so every other IRetailFrameWalkContext
implementer — test fixtures, the FW1 conformance replay context — is unaffected).
WalkProductionFrameContext wires RetailPViewFrameInput.ViewerCellId and
RetailPViewPassExecutor.ShouldDrawWeatherOnce(RenderSky, RenderWeather, PlayerCellId) into those two
members. WalkFrameDriver.OnWeatherTurn prints the "OC" transcript line at COLLECT time; the print is
removed from DrawWeatherOnce (the DRAW stays there, at Replay). Because DrawLandscape's new call sits
at the very end of the method, an outdoor root's transcript naturally ends with the line and an
interior root's lands strictly between "LS" and the flood's first "EC" — exactly retail's position —
with no extra plumbing.
Kept items each get a synthetic-view CPU/GPU equivalence pin (ClipFrameLayoutTests): exit seals via
the real ClipFrame.AppendSlot/GetSlotPlanes round trip, punch fans via the direct
ClipViewSlice.Planes = cps.PlaneArray assignment ClipFrameAssembler.Assemble uses — both assert every
polygon edge midpoint has ~0 signed distance under the GPU-read-back planes and non-negative distance
under every plane.
Every new pin's mutation was hand-verified this session (temporarily reintroduced, confirmed the
exact failing assertion, then reverted): a duplicated OnWeatherTurn call fails Assert.Single with
"2 matching items" on both the outdoor and interior position tests; suppressing the Collect-time
call fails the same assertions with "did not contain any matching items"; perturbing one GPU plane's
offset by 0.05 fails the exit-seal equivalence pin with "the closest plane was only 0.050000012
away". WalkProductionFrameContextTests pins that the ViewerCellId/WeatherGateOpen storage seam
itself never drops or swaps its own two constructor/Reset arguments.
No register row added or removed — every change here deletes an acdream-only rule; none introduces
a new deviation. Full solution build: 0 warnings/0 errors. App hermetic 6,829/6,829 (was 6,828 base
+ 1 new pin). InstalledDat 244 pass/1 skip/4 known (2x #383, TowerAscent, and the pre-existing #458
WalkLandscape.CheckBlocks block-visibility divergence — unrelated to this chunk, untouched by it).
Core.Tests Rendering 216/216.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
157b327a0b
commit
62c2231d0a
18 changed files with 713 additions and 591 deletions
|
|
@ -331,12 +331,6 @@ issue is closed, the strip was missed; delete both.
|
|||
| `ACDREAM_PROBE_PVINPUT` | "throwaway apparatus — strip once the jitter source is pinned" | `=1` | one `[pv-input]` line/frame with 6-dp-precision `PortalVisibilityBuilder.Build` inputs (camera eye, player position, VP elements) + resulting flood-cell count; deliberately runs WITHOUT the heavier `[flap]` probe so the log stays diffable | print-only | `RenderingDiagnostics.ProbePvInputEnabled` |
|
||||
| `ACDREAM_PROBE_REMOTE_SLIDE` | bug b, temporary — strip once two-client roof capture lands | `=1` OR `=<comma-separated hex GUID list>` | gates `[remote-slide-up]`/`[remote-slide-vec]`/`[remote-slide-snap]`/`[remote-slide-enq]` lines across `LiveEntityNetworkUpdateController`, `InterpolationManager`, `RuntimeRemotePhysicsUpdater`, `RuntimeRemoteSteadyStatePosition` tracing two candidate remote-slide "blip" producers | print-only; `BeginRemoteSlideAttribution`/GUID-stamping calls are UNCONDITIONAL at several call sites (self-guard is internal), so a `[ThreadStatic]` field write happens on every remote tick regardless of the flag (cheap, non-allocating); a GUID allow-list narrows output to specific entities for a readable two-client capture | `PhysicsDiagnostics.ProbeRemoteSlideEnabled` + `ProbeRemoteSlideGuids` (raw string parsed via `ParseHexIdList` unless it's the literal `"1"`) |
|
||||
| `ACDREAM_PROBE_REMOTE_TELEPORT` | c4 route 4b-3, temporary | `=1` | gates one `[remote-teleport]` line per routed remote teleport arm in `LiveEntityNetworkUpdateController.ApplyRemoteContactRouting` | print-only; a 2026-08-04 fix moved the enabled-check to the CALL SITE because the probe's internal self-guard did not prevent `teleportStatus.ToString()` from being evaluated/allocated on every teleport regardless of flag state — now properly guarded | `PhysicsDiagnostics.ProbeRemoteTeleportEnabled` |
|
||||
| `ACDREAM_PROBE_CATHEDRAL_SKIP_PUNCH` | Campaign FW floating-stair discriminator (committed by the `b3b7d922` investigation checkpoint) | `=1` | suppresses only the far-Z portal punch of the authored building whose look-in owns cell `0xF4180112`, the moving transparent seam at the `0xF4180107/0112` floating stairs | **Behavior-changing diagnostic**; never use as a production fix; scheduled for deletion in Campaign OVERHAUL S5 | `RenderingDiagnostics.ProbeCathedralSkipStairBuildingPunch` |
|
||||
| `ACDREAM_PROBE_CATHEDRAL_SKIP_CELL_SHELLS` | Campaign FW floating-stair carrier discriminator | `=1` | suppresses only the visible EnvCell shells of cells `0xF4180107` and `0xF4180112`; missing walls are intentional so the moving wall-textured triangles can be attributed or exonerated | **Behavior-changing diagnostic**; never use as a production fix | `RenderingDiagnostics.ProbeCathedralSkipFloatingStairCellShells` |
|
||||
| `ACDREAM_PROBE_CATHEDRAL_SKIP_SOUTH_LOOKIN_SHELLS` | Campaign FW south-building carrier discriminator | `=1` | suppresses the visible EnvCell shells of `0xF4180112/0113/0114` only during the south building's pre-clear look-in; the ordinary interior-root repaint is unchanged | **Behavior-changing diagnostic**; never use as a production fix | `RenderingDiagnostics.ProbeCathedralSkipSouthLookInCellShells` |
|
||||
| `ACDREAM_PROBE_CATHEDRAL_SKIP_LOOKIN_SHELL` | Campaign FW exact look-in shell discriminator | full hexadecimal cell id, e.g. `F4180112` | suppresses one exact EnvCell shell only when reached through a building look-in; its ordinary interior-root repaint is unchanged | **Behavior-changing diagnostic**; never use as a production fix | `RenderingDiagnostics.ProbeCathedralSkipLookInShellCellId` |
|
||||
| `ACDREAM_PROBE_CATHEDRAL_SKIP_SOUTH_BUILDING_SHELL` | Campaign FW south-building carrier discriminator | `=1` | suppresses only the own exterior shell of the cathedral building anchored at `0xF4180112`; its portal walk, look-in cells, and particles remain live | **Behavior-changing diagnostic**; never use as a production fix | `RenderingDiagnostics.ProbeCathedralSkipSouthBuildingShell` |
|
||||
| `ACDREAM_PROBE_CATHEDRAL_SHELL_ORDER` | Campaign FW floating-stair shell-order trace | `=1` | samples the ONE walk's actual replay event order every fifth cathedral frame (`CLEAR`, `SEALS`, punches, marks, and EnvCell shells) and reports render-stamp-deduplicated whole-shell turns for cells `0xF4180104/0106/0107/0112/0113/0114` | print-only; never changes admission, depth state, or draw order | `RenderingDiagnostics.ProbeCathedralShellOrderEnabled` |
|
||||
| `ACDREAM_PROBE_FACILITY_STAIRS` | #177 Facility Hub stair probe (committed by the `b3b7d922` investigation checkpoint) | `=1` | traces the classifier admission, ordered-stream append, ring upload, and final indirect draw of the authored stair GfxObj `0x010000DE` and the local-player setup parts in cells `0x8A02015E/015F/01C1`, plus the walk root signature for those cells; Campaign OVERHAUL S2 chunk 5 added a `[walk-membership]` line, printed once at the start of a frame only when `WalkProductionWorldData.UnregisteredRenderMembershipCount` was nonzero the PRIOR frame (an entity the registry flooded into a cell but the presentation scene could not resolve yet that frame) | print-only; never influences admission, order, or depth state; scheduled for deletion in Campaign OVERHAUL S5 | `RenderingDiagnostics.ProbeFacilityStairsEnabled` (read sites in `RetailPViewRenderer`, `WalkPView`, `WalkProductionWorldData`, `WalkStaticStreamPopulator`, `WbDrawDispatcher.*`) |
|
||||
| `ACDREAM_PROBE_SEAMDRAW` | #176, "throwaway apparatus" | `"1"`/`"true"`/blank → default #176 Facility Hub cell set (7 fixed hex ids); otherwise comma-separated hex cell-id list | change-deduped + 2 s-heartbeat `[seam-cell]`/`[seam-snap]`/`[seam-ent]`/`[seam-mask]` lines from `EnvCellRenderer.Render` and `WbDrawDispatcher` describing per-instance transforms and resolved light-set identities at target cells | print-only | `RenderingDiagnostics.ProbeSeamDrawEnabled` / `SeamDrawTargetCells` |
|
||||
| `ACDREAM_PROBE_STEP_WALK` | a6.p3 issue #98 | `=1` | gates `[step-walk]` lines at select points in the transition sub-step loop and step-down probe (requested vs adjusted offset, sphere positions, contact planes, walkable flags) | print-only; no DebugPanel mirror | `PhysicsDiagnostics.ProbeStepWalkEnabled` |
|
||||
|
|
|
|||
|
|
@ -243,17 +243,6 @@ internal sealed class WalkProductionLeafRenderer : IWalkFrameLeafRenderer
|
|||
|
||||
public void DrawCellShell(uint cellId)
|
||||
{
|
||||
// Output-gated binary discriminator for the moving wall-textured
|
||||
// triangles at the 0xF4180107/0112 floating stairs. Missing walls are
|
||||
// intentional while enabled: this identifies the visual carrier and
|
||||
// must not become a production admission rule.
|
||||
if (AcDream.Core.Rendering.RenderingDiagnostics
|
||||
.ProbeCathedralSkipFloatingStairCellShells
|
||||
&& cellId is 0xF4180107u or 0xF4180112u)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Retail DrawEnvCell @0x0059F170 submits the prepared EnvCell mesh
|
||||
// whole after stamping it drawn-this-frame. obj_view_set prepares
|
||||
// legacy polygon/object tests, but the use_built_mesh branch calls
|
||||
|
|
|
|||
|
|
@ -133,8 +133,6 @@ public RetailPViewPassExecutor(
|
|||
{
|
||||
List<Exception>? failures = null;
|
||||
TryAbort(_frameGlState.RestoreFrameDefaults);
|
||||
TryAbort(() => _envCells.SetClipRouting(null));
|
||||
TryAbort(_entities.ClearClipRouting);
|
||||
TryAbort(_noSceneParticleEntityIds.Clear);
|
||||
if (failures is { Count: > 0 })
|
||||
throw new AggregateException("Retail PView pass abort failed.", failures);
|
||||
|
|
@ -160,19 +158,6 @@ public RetailPViewPassExecutor(
|
|||
public void PrepareClipFrame(int terrainUploadCount) =>
|
||||
_surface.PrepareClipFrame(terrainUploadCount);
|
||||
|
||||
public void SetTerrainClip(ReadOnlySpan<Vector4> planes) =>
|
||||
_surface.SetTerrainClip(planes);
|
||||
|
||||
public void ClearClipRouting() => _entities.ClearClipRouting();
|
||||
|
||||
public void UseIndoorMembershipOnlyRouting()
|
||||
{
|
||||
// Retail viewcone-checks meshes and draws whole cell shells. This clears
|
||||
// any terrain-slice routing before the indoor membership passes.
|
||||
_envCells.SetClipRouting(null);
|
||||
_entities.ClearClipRouting();
|
||||
}
|
||||
|
||||
public void PrepareCellBatches(
|
||||
RetailPViewFrameInput frame,
|
||||
HashSet<uint> visibleCellIds) =>
|
||||
|
|
@ -187,90 +172,31 @@ public RetailPViewPassExecutor(
|
|||
public void DrawOpaqueCellShells(HashSet<uint> cellIds) =>
|
||||
_envCells.Render(WbRenderPass.Opaque, cellIds);
|
||||
|
||||
public void SetCellShellClipRouting(IReadOnlyDictionary<uint, int>? routing) =>
|
||||
_envCells.SetClipRouting(routing);
|
||||
|
||||
public bool CellHasTransparentShell(uint cellId) =>
|
||||
_envCells.CellHasTransparent(cellId);
|
||||
|
||||
public void DrawTransparentCellShellsOrdered(IReadOnlyList<uint> cellIds) =>
|
||||
_envCells.RenderTransparentOrdered(cellIds);
|
||||
|
||||
public void DrawLandscapeSliceLate(
|
||||
RetailPViewFrameInput frame,
|
||||
RetailPViewLandscapeLateSliceContext context)
|
||||
{
|
||||
ClipViewSlice slice = context.Slice;
|
||||
bool scissor = BeginDoorwayScissor(slice.NdcAabb);
|
||||
_surface.BindTerrainClip();
|
||||
|
||||
DisableClipDistances();
|
||||
if (context.Dynamics.Count > 0)
|
||||
{
|
||||
var dynamicsEntry = (
|
||||
frame.PlayerLandblockId ?? 0u,
|
||||
Vector3.Zero,
|
||||
Vector3.Zero,
|
||||
context.Dynamics,
|
||||
(IReadOnlyDictionary<uint, WorldEntity>?)null);
|
||||
_entities.Draw(
|
||||
frame.Camera,
|
||||
new[] { dynamicsEntry },
|
||||
frame.Frustum,
|
||||
neverCullLandblockId: frame.PlayerLandblockId,
|
||||
visibleCellIds: null,
|
||||
animatedEntityIds: frame.AnimatedEntityIds);
|
||||
}
|
||||
|
||||
// Late-stage particle owners submit ONCE per frame through
|
||||
// DrawLandscapeStaticParticles after the slice loop (retail: one
|
||||
// unclipped alpha-list insertion per emitter), not per slice here.
|
||||
EnableClipDistances();
|
||||
// S3 chunk 1 fix round 2 (§11.6 H1): the sky/rain MESH draw and its
|
||||
// OC print used to live HERE, re-running once per active landscape
|
||||
// view (2-4 times for an interior root's exit views) — retail draws
|
||||
// it ONCE per frame, after LScape::draw's whole landblock loop
|
||||
// completes (see DrawWeatherOnce's own doc comment). Only the
|
||||
// per-slice rain PARTICLE emitters stay here — they legitimately
|
||||
// clip against this slice's own view (ParticleRenderPass.
|
||||
// SkyPostScene, clipSlot=slice.Slot), unlike the unclipped mesh.
|
||||
if (ShouldDrawWeatherOnce(frame.RenderSky, frame.RenderWeather, frame.PlayerCellId))
|
||||
{
|
||||
DisableClipDistances();
|
||||
if (_particles is not null && _particleRenderer is not null)
|
||||
{
|
||||
_particleRenderer.Draw(
|
||||
frame.Camera,
|
||||
frame.CameraWorldPosition,
|
||||
ParticleRenderPass.SkyPostScene,
|
||||
clipSlot: checked((uint)slice.Slot));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
DisableClipDistances();
|
||||
}
|
||||
|
||||
if (scissor)
|
||||
_surface.EndScissor();
|
||||
_entities.ClearClipRouting();
|
||||
DisableClipDistances();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// S3 chunk 1 fix round 2 (§11.6 H1): retail draws the weather pass
|
||||
/// EXACTLY ONCE per frame — <c>GameSky::Draw</c> @0x00506ff0 with
|
||||
/// <c>arg2==1</c> runs AFTER <c>LScape::draw</c>'s landblock loop
|
||||
/// finishes (@0x00506396), not once per active landscape view. The old
|
||||
/// call site inside <see cref="DrawLandscapeSliceLate"/> ran once per
|
||||
/// <c>RetailPViewRenderer</c>'s <c>OutsideViewSlices</c> entry — 2-4 for
|
||||
/// an interior root's exit views — printing 2-4 "OC" lines and
|
||||
/// re-submitting the rain mesh that many times; every capture shows
|
||||
/// exactly one. This call is UNCLIPPED and sets no scissor (S3 chunk 4
|
||||
/// deletes the slice loop's remaining scissor/clip apparatus outright);
|
||||
/// callers invoke it ONCE, after the slice loop that still runs
|
||||
/// <see cref="DrawLandscapeSliceLate"/> per view for the per-slice rain
|
||||
/// particles and dynamics.
|
||||
/// finishes (@0x00506396), not once per active landscape view. This call
|
||||
/// is UNCLIPPED and sets no scissor — S3 chunk 4 (§10.2) deleted the
|
||||
/// former per-outside-view-slice loop (<c>SetTerrainClip</c> +
|
||||
/// <c>ClearClipRouting</c> + the old <c>DrawLandscapeSliceLate</c> leaf,
|
||||
/// one call per active landscape view) that used to run before this
|
||||
/// call and re-submit the rain mesh once per doorway aperture; this is
|
||||
/// now the ONLY weather call site, matching retail's ONE unclipped
|
||||
/// <c>GameSky::Draw(sky,1)</c>.
|
||||
/// <para>
|
||||
/// S3 chunk 4 (§10.2): the rain PARTICLE emitters
|
||||
/// (<c>ParticleRenderPass.SkyPostScene</c>) moved here too, as ONE
|
||||
/// unclipped submission — retail inserts emitters into one unclipped
|
||||
/// alpha list (no per-doorway <c>clipSlot</c>), the same rule as the
|
||||
/// mesh draw above.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Gated exactly as before —
|
||||
/// <see cref="ShouldDrawWeatherOnce(bool,bool,uint)"/>, retail's
|
||||
|
|
@ -278,9 +204,15 @@ public RetailPViewPassExecutor(
|
|||
/// two render toggles — via <c>_sky?.RenderWeather(...)</c> (null-
|
||||
/// conditional, matching every other <c>_sky?.RenderSky(...)</c> call
|
||||
/// site in this codebase): a missing sky asset silently skips the GL
|
||||
/// draw while the print below still fires, because the print's only
|
||||
/// job is transcript fidelity to retail's own trace, which never has a
|
||||
/// null <c>GameSky</c>.
|
||||
/// draw.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// S3 chunk 4 (O3): the transcript "OC" print that used to live here
|
||||
/// moved to <c>WalkFrameDriver.OnWeatherTurn</c>, fired by
|
||||
/// <c>RetailFrameWalk.DrawLandscape</c> at Collect time (retail's real
|
||||
/// turn — between the landscape and the flood for an interior root, at
|
||||
/// the end of the walk for an outdoor root) instead of wherever the GPU
|
||||
/// draw happens to run at Replay.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public void DrawWeatherOnce(RetailPViewFrameInput frame)
|
||||
|
|
@ -297,16 +229,13 @@ public RetailPViewPassExecutor(
|
|||
frame.SkyKeyframe,
|
||||
frame.EnvironOverrideActive);
|
||||
|
||||
// GameSky::Draw @0x00506ff0's weather branch (arg2==1) calls
|
||||
// DrawObjCellForDummies(after_sky_cell) @0x005070da when
|
||||
// LScape::weather_enabled != 0 — the after-sky cell's DID is the
|
||||
// viewer's own land cell, the SAME id the frame-root "P" line
|
||||
// already carries (terrace-edge.walk.log:1416 "OC f418000b" = the P
|
||||
// cell, once per complete outdoor frame — S3 chunk 1 fix round 1
|
||||
// §11.5 G8's original finding; this round fixes the call site that
|
||||
// reported it 2-4 times). Print-only — never gates weather
|
||||
// rendering itself.
|
||||
AcDream.App.Rendering.Walk.WalkTranscriptDump.PrintObjectCellTurn(frame.ViewerCellId);
|
||||
if (_particles is not null && _particleRenderer is not null)
|
||||
{
|
||||
_particleRenderer.Draw(
|
||||
frame.Camera,
|
||||
frame.CameraWorldPosition,
|
||||
ParticleRenderPass.SkyPostScene);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@ internal sealed class RetailPViewRenderer
|
|||
// Output-only Facility Hub staircase probe. This counter is consulted
|
||||
// only while ACDREAM_PROBE_FACILITY_STAIRS=1 and never affects the walk.
|
||||
private ulong _probeFacilityStairFrame;
|
||||
private ulong _probeCathedralShellOrderFrame;
|
||||
private string? _probeFacilityStairRootSignature;
|
||||
private string? _probeCathedralStairRootSignature;
|
||||
|
||||
|
|
@ -127,7 +126,6 @@ internal sealed class RetailPViewRenderer
|
|||
// Per-slice trim still applies in DrawEnvCellShells (Task 4 makes it self-contained).
|
||||
_drawableCellsScratch.Clear();
|
||||
HashSet<uint> drawableCells = _drawableCellsScratch;
|
||||
passes.UseIndoorMembershipOnlyRouting();
|
||||
|
||||
// Campaign FW3.4a: THE ONE WALK. Builds walkContext/walkLandscape/
|
||||
// walkCameraCell exactly as the pre-FW3.4a pre-walk collection pass
|
||||
|
|
@ -154,6 +152,14 @@ internal sealed class RetailPViewRenderer
|
|||
// reached before the world pass has published its scope.
|
||||
float viewportWidth = attachment?.Width ?? 1024f;
|
||||
float viewportHeight = attachment?.Height ?? 720f;
|
||||
// S3 chunk 4 (O3): the pre-computed weather gate — the SAME
|
||||
// predicate RetailPViewPassExecutor.DrawWeatherOnce checks at
|
||||
// Replay — handed to the walk context so RetailFrameWalk.
|
||||
// DrawLandscape can fire IWalkEventSink.OnWeatherTurn at
|
||||
// retail's real Collect-time turn without importing App render
|
||||
// toggles into the pure decomp port.
|
||||
bool weatherGateOpen = RetailPViewPassExecutor.ShouldDrawWeatherOnce(
|
||||
ctx.RenderSky, ctx.RenderWeather, ctx.PlayerCellId);
|
||||
if (_walkFrameContextScratch is null)
|
||||
{
|
||||
_walkFrameContextScratch = new Walk.WalkProductionFrameContext(
|
||||
|
|
@ -163,7 +169,9 @@ internal sealed class RetailPViewRenderer
|
|||
forward,
|
||||
ctx.ViewProjection,
|
||||
viewportWidth,
|
||||
viewportHeight);
|
||||
viewportHeight,
|
||||
ctx.ViewerCellId,
|
||||
weatherGateOpen);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -172,7 +180,9 @@ internal sealed class RetailPViewRenderer
|
|||
forward,
|
||||
ctx.ViewProjection,
|
||||
viewportWidth,
|
||||
viewportHeight);
|
||||
viewportHeight,
|
||||
ctx.ViewerCellId,
|
||||
weatherGateOpen);
|
||||
}
|
||||
Walk.WalkProductionFrameContext walkContext = _walkFrameContextScratch;
|
||||
_walkLandscape!.SetViewer(ctx.ViewerCellId, ctx.ViewerEyePos);
|
||||
|
|
@ -285,20 +295,6 @@ internal sealed class RetailPViewRenderer
|
|||
_drawableCellsScratch.UnionWith(walkDriver.VisitedCells);
|
||||
walkDriver.CopyVisibleCellsTo(_visibleCellsScratch);
|
||||
|
||||
if (AcDream.Core.Rendering.RenderingDiagnostics
|
||||
.ProbeCathedralShellOrderEnabled
|
||||
&& ((ctx.ViewerCellId & 0xFFFF0000u) == 0xF4180000u
|
||||
|| (ctx.PlayerCellId & 0xFFFF0000u) == 0xF4180000u))
|
||||
{
|
||||
_probeCathedralShellOrderFrame++;
|
||||
walkDriver.TraceCathedralShellOrder(
|
||||
_probeCathedralShellOrderFrame,
|
||||
ctx.ViewerCellId,
|
||||
ctx.PlayerCellId,
|
||||
ctx.RootCell.CellId,
|
||||
ctx.ViewerEyePos);
|
||||
}
|
||||
|
||||
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeFacilityStairsEnabled)
|
||||
{
|
||||
static int TargetMembership(IReadOnlyCollection<uint> cells)
|
||||
|
|
@ -452,7 +448,6 @@ internal sealed class RetailPViewRenderer
|
|||
// replay.
|
||||
_walkPreClearDynamics = () =>
|
||||
{
|
||||
passes.UseIndoorMembershipOnlyRouting();
|
||||
DrawLandscapeDynamicsPhase(
|
||||
ctx,
|
||||
passes,
|
||||
|
|
@ -461,7 +456,6 @@ internal sealed class RetailPViewRenderer
|
|||
try
|
||||
{
|
||||
DrawWalkDrivenStatics(ctx, walkExecutor, walkDriver!);
|
||||
passes.UseIndoorMembershipOnlyRouting();
|
||||
if (ctx.RootCell.IsOutdoorNode)
|
||||
{
|
||||
DrawLandscapeDynamicsPhase(
|
||||
|
|
@ -641,28 +635,20 @@ internal sealed class RetailPViewRenderer
|
|||
// (the walk owns its own alpha barriers — WalkFrameDriver.OnBuildingTurn).
|
||||
passes.DrawUnattachedSceneParticles(ctx, outdoorCells: true);
|
||||
|
||||
// The per-slice rain PARTICLE emitters still run through each
|
||||
// active landscape view so doorway scissor/clip state matches the
|
||||
// old executor. The dynamics collection is intentionally empty: the
|
||||
// walk already drew those records at OnLandscapeCellTurn.
|
||||
foreach (var slice in clipAssembly.OutsideViewSlices)
|
||||
{
|
||||
passes.SetTerrainClip(slice.Planes);
|
||||
passes.ClearClipRouting();
|
||||
passes.DrawLandscapeSliceLate(
|
||||
ctx,
|
||||
new RetailPViewLandscapeLateSliceContext(
|
||||
slice,
|
||||
Array.Empty<WorldEntity>()));
|
||||
}
|
||||
|
||||
// S3 chunk 1 fix round 2 (§11.6 H1): the weather MESH draw + its OC
|
||||
// print run ONCE here, after the slice loop above — not once per
|
||||
// slice inside it (DrawWeatherOnce's own doc comment has the
|
||||
// decomp citation and the retail evidence).
|
||||
// S3 chunk 4 (§10.2): the former per-outside-view-slice loop
|
||||
// (SetTerrainClip + ClearClipRouting + DrawLandscapeSliceLate, one
|
||||
// call per active landscape view) is DELETED — retail draws the
|
||||
// weather mesh and its rain particles ONCE, unclipped, after
|
||||
// LScape::draw's whole landblock loop (GameSky::Draw(sky,1)
|
||||
// @0x00506ff0), never once per doorway aperture. DrawWeatherOnce
|
||||
// is the ONE call that remains — it now also submits the rain
|
||||
// particles (moved from the deleted loop's per-slice, per-doorway
|
||||
// ParticleRenderPass.SkyPostScene draw) as ONE unclipped
|
||||
// submission, matching retail's one alpha-list insertion per
|
||||
// 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);
|
||||
|
||||
passes.UseIndoorMembershipOnlyRouting();
|
||||
}
|
||||
|
||||
/// <summary>Campaign FW4 slice 2 — the walk-flood seal draw. Retail's
|
||||
|
|
@ -872,10 +858,3 @@ public sealed class RetailPViewFrameResult
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>#131/#132: the late landscape phase's per-slice payload —
|
||||
/// outside-stage dynamics to mesh-draw, plus the particle owners not already
|
||||
/// submitted at a pre-building barrier.</summary>
|
||||
public readonly record struct RetailPViewLandscapeLateSliceContext(
|
||||
ClipViewSlice Slice,
|
||||
IReadOnlyList<WorldEntity> Dynamics);
|
||||
|
|
|
|||
|
|
@ -17,6 +17,31 @@ public interface IRetailFrameWalkContext : IWalkBuildingFrameContext
|
|||
|
||||
float ViewportWidth { get; }
|
||||
float ViewportHeight { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Campaign OVERHAUL S3 chunk 4 (O3): the walk's own viewer cell — the
|
||||
/// SAME cell the frame-root "P" transcript line carries — surfaced here
|
||||
/// so <see cref="RetailFrameWalk.DrawLandscape"/> can hand it to <see
|
||||
/// cref="IWalkEventSink.OnWeatherTurn"/> without reaching into an
|
||||
/// App-level frame input. Default 0 so no existing implementer (test
|
||||
/// fixtures, the FW1 conformance replay context) needs updating unless
|
||||
/// it actually cares about the weather turn.
|
||||
/// </summary>
|
||||
uint ViewerCellId => 0u;
|
||||
|
||||
/// <summary>
|
||||
/// Campaign OVERHAUL S3 chunk 4 (O3): the pre-computed retail weather
|
||||
/// gate — <c>SmartBox::is_player_outside</c> @0x00451e80 ANDed with the
|
||||
/// two App-level render toggles (<c>RenderSky</c>/<c>RenderWeather</c>),
|
||||
/// exactly
|
||||
/// <see cref="AcDream.App.Rendering.RetailPViewPassExecutor.ShouldDrawWeatherOnce"/>'s
|
||||
/// predicate. Computed by the production caller (which owns those
|
||||
/// toggles) and handed in here so the pure decomp port in <see
|
||||
/// cref="RetailFrameWalk"/> never imports App render state. Default
|
||||
/// false — see <see cref="ViewerCellId"/>'s own doc comment for why that
|
||||
/// is safe for every other implementer.
|
||||
/// </summary>
|
||||
bool WeatherGateOpen => false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -216,6 +241,17 @@ public sealed class RetailFrameWalk
|
|||
cellIndex);
|
||||
}
|
||||
}
|
||||
|
||||
// S3 chunk 4 (O3): LScape::draw calls GameSky::Draw(sky, 1)
|
||||
// @0x00506396 UNCONDITIONALLY once every block has drawn — the
|
||||
// is_player_outside gate lives INSIDE GameSky::Draw, not here. ctx
|
||||
// carries the pre-computed gate (App render toggles are not this
|
||||
// pure decomp port's business); see IWalkEventSink.OnWeatherTurn's
|
||||
// own doc comment for why this single call site, at the very end of
|
||||
// this method, naturally lands the turn at retail's own position for
|
||||
// both root kinds.
|
||||
if (ctx.WeatherGateOpen)
|
||||
sink.OnWeatherTurn(ctx.ViewerCellId);
|
||||
}
|
||||
|
||||
/// <summary><c>RenderDeviceD3D::DrawBuilding</c> @0x0059f2a0 at the walk
|
||||
|
|
|
|||
|
|
@ -234,4 +234,38 @@ public interface IWalkEventSink
|
|||
/// immediately, as before). Default no-op.
|
||||
/// </summary>
|
||||
void OnInteriorFloodDrawTurn(IReadOnlyList<uint> cells, int outsideViewCount) { }
|
||||
|
||||
/// <summary>
|
||||
/// Campaign OVERHAUL S3 chunk 4 (§10.2/O3): fires at the END of
|
||||
/// <see cref="RetailFrameWalk.DrawLandscape"/>, immediately after its
|
||||
/// per-land-cell loop — retail <c>LScape::draw</c> @0x00506330 calls
|
||||
/// <c>GameSky::Draw(sky, 1)</c> @0x00506396 UNCONDITIONALLY once every
|
||||
/// block has drawn; the weather gate itself
|
||||
/// (<c>SmartBox::is_player_outside</c> @0x00451e80) lives INSIDE
|
||||
/// <c>GameSky::Draw</c>, not at this call site. <see
|
||||
/// cref="RetailFrameWalk.DrawLandscape"/> mirrors that split: it only
|
||||
/// fires this hook when <see cref="IRetailFrameWalkContext.WeatherGateOpen"/>
|
||||
/// holds (the pre-computed gate — App-level render toggles have no
|
||||
/// business inside the pure decomp port), carrying <paramref
|
||||
/// name="viewerCellId"/> (<see cref="IRetailFrameWalkContext.ViewerCellId"/>
|
||||
/// — the SAME cell the frame-root "P" transcript line already carries,
|
||||
/// retail's <c>DrawObjCellForDummies(after_sky_cell)</c>
|
||||
/// @0x005070da's argument). Because <c>DrawLandscape</c> only ever runs
|
||||
/// once per frame (the outdoor root's own call, or an interior root's
|
||||
/// through its surviving exit views) and this hook sits at its very
|
||||
/// end, an OUTDOOR root's walk naturally ends its transcript with this
|
||||
/// turn, and an INTERIOR root's turn lands strictly between the
|
||||
/// landscape and the flood's own draw turn (<see
|
||||
/// cref="OnInteriorFloodDrawTurn"/>, called by <see
|
||||
/// cref="RetailFrameWalk.DrawInside"/> right after <c>DrawLandscape</c>
|
||||
/// returns) — exactly where retail's own <c>GameSky::Draw(sky,1)</c>
|
||||
/// sits relative to the flood's <c>DrawCells</c> reverse loops. The
|
||||
/// mesh DRAW itself is unrelated to this hook (production still calls
|
||||
/// <c>RetailPViewPassExecutor.DrawWeatherOnce</c> directly from <see
|
||||
/// cref="RetailPViewRenderer.DrawLandscapeDynamicsPhase"/>, at Replay
|
||||
/// time) — this hook exists ONLY so the transcript print happens at
|
||||
/// retail's real turn (Collect time), not wherever the GPU draw
|
||||
/// happens to run. Default no-op.
|
||||
/// </summary>
|
||||
void OnWeatherTurn(uint viewerCellId) { }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -377,21 +377,25 @@ internal interface IWalkLookInViewSource
|
|||
{
|
||||
IReadOnlyList<uint> LookInCellTurns { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Campaign OVERHAUL S3 chunk 4 (§10.2): retail's real question — does
|
||||
/// ANY active view slice for this DrawObjCell turn admit the part's
|
||||
/// drawing sphere (<c>RenderDeviceD3D::DrawMesh</c> @0x005A0860's
|
||||
/// per-view <c>viewconeCheck</c> loop)? Once any view admits, retail
|
||||
/// submits the WHOLE constructed mesh once
|
||||
/// (<c>DrawMeshInternal</c> @0x0059F360) — the portal polygon is never
|
||||
/// forwarded as a GPU mesh clip, so this is a pure Boolean, never a
|
||||
/// per-view clip-slot list (the deleted <c>VisibleClipSlotsInLookInTurn</c>
|
||||
/// always collapsed to that same Boolean one call site up anyway).
|
||||
/// <paramref name="testSphere"/> false asks "does this route have any
|
||||
/// admitted view at all", ignoring the sphere geometry — retail's own
|
||||
/// no-authored-sphere case, where there is nothing to sphere-test.
|
||||
/// </summary>
|
||||
bool SphereVisibleInLookInTurn(
|
||||
int routeIndex,
|
||||
in Vector3 center,
|
||||
float radius);
|
||||
|
||||
/// <summary>Returns the GPU clip slots for every view slice in this
|
||||
/// DrawObjCell turn which admits the part's retail drawing sphere. Retail
|
||||
/// runs DrawMeshInternal once per surviving portal_view slice; a boolean
|
||||
/// sphere gate alone is insufficient because the mesh polygons must remain
|
||||
/// clipped to that aperture.</summary>
|
||||
IReadOnlyList<uint> VisibleClipSlotsInLookInTurn(
|
||||
int routeIndex,
|
||||
in Vector3 center,
|
||||
float radius,
|
||||
bool testSphere);
|
||||
bool testSphere = true);
|
||||
|
||||
/// <summary>Output-only description of the exact CY/edge-plane distances
|
||||
/// used by one DrawObjCell turn. Kept behind the existing Facility probe;
|
||||
|
|
@ -590,7 +594,6 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
private readonly List<WalkLookInTurn> _lookInTurns = new();
|
||||
private readonly List<WalkLookInSlice> _lookInSlices = new();
|
||||
private readonly List<WalkPlane> _lookInPlanes = new();
|
||||
private readonly List<uint> _visibleClipSlotScratch = new();
|
||||
private readonly List<int> _floodViewRouteScratch = new();
|
||||
private WalkPlane _lookInCyPlane;
|
||||
|
||||
|
|
@ -772,7 +775,6 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
_lookInTurns.Clear();
|
||||
_lookInSlices.Clear();
|
||||
_lookInPlanes.Clear();
|
||||
_visibleClipSlotScratch.Clear();
|
||||
_floodViewRouteScratch.Clear();
|
||||
_dispatcher.EndWalkPartFrame();
|
||||
_cellShellsDrawnThisFrame.Clear();
|
||||
|
|
@ -926,7 +928,6 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
_lookInTurns.Clear();
|
||||
_lookInSlices.Clear();
|
||||
_lookInPlanes.Clear();
|
||||
_visibleClipSlotScratch.Clear();
|
||||
_lookInCyPlane = ctx.CyPlane;
|
||||
_cellShellsDrawnThisFrame.Clear();
|
||||
_cellParticleTurnsDrawnThisFrame.Clear();
|
||||
|
|
@ -1195,121 +1196,6 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
_pendingTerrainBatch.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Output-only Campaign FW cathedral trace. The event list described here
|
||||
/// is the same list <see cref="Replay"/> immediately executes, so this
|
||||
/// captures the actual clear/seal/punch/shell submission order without
|
||||
/// inserting a second visibility walk or changing rendering behavior.
|
||||
/// </summary>
|
||||
internal void TraceCathedralShellOrder(
|
||||
ulong frame,
|
||||
uint viewerCellId,
|
||||
uint playerCellId,
|
||||
uint rootCellId,
|
||||
Vector3 viewerEye)
|
||||
{
|
||||
if (!AcDream.Core.Rendering.RenderingDiagnostics
|
||||
.ProbeCathedralShellOrderEnabled
|
||||
|| !_readyToReplay
|
||||
|| (frame != 1 && frame % 5 != 0))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
static bool IsCathedralCell(uint cellId) =>
|
||||
(cellId & 0xFFFF0000u) == 0xF4180000u
|
||||
&& (cellId & 0xFFFFu) >= 0x0100u;
|
||||
|
||||
static bool IsFloatingStairCell(uint cellId) => cellId is
|
||||
0xF4180104u or 0xF4180106u or 0xF4180107u
|
||||
or 0xF4180112u or 0xF4180113u or 0xF4180114u;
|
||||
|
||||
var order = new System.Text.StringBuilder(1024);
|
||||
order.Append("[cathedral-shell-order] f=").Append(frame)
|
||||
.Append(" viewer=0x").Append(viewerCellId.ToString("X8"))
|
||||
.Append(" player=0x").Append(playerCellId.ToString("X8"))
|
||||
.Append(" root=0x").Append(rootCellId.ToString("X8"))
|
||||
.Append(" eye=(").Append(viewerEye.X.ToString("F4"))
|
||||
.Append(',').Append(viewerEye.Y.ToString("F4"))
|
||||
.Append(',').Append(viewerEye.Z.ToString("F4")).Append(')')
|
||||
.Append(" flood=[");
|
||||
for (int i = 0; i < InteriorFloodCells.Count; i++)
|
||||
{
|
||||
if (i != 0)
|
||||
order.Append(',');
|
||||
order.Append("0x").Append(InteriorFloodCells[i].ToString("X8"));
|
||||
}
|
||||
order.Append("] lookIn=[");
|
||||
for (int i = 0; i < LookInCellTurns.Count; i++)
|
||||
{
|
||||
if (i != 0)
|
||||
order.Append(',');
|
||||
order.Append("0x").Append(LookInCellTurns[i].ToString("X8"));
|
||||
}
|
||||
order.Append("] replay=");
|
||||
|
||||
var clips = new System.Text.StringBuilder(1024);
|
||||
clips.Append("[cathedral-shell-clips] f=").Append(frame).Append(" slots=");
|
||||
bool wroteClip = false;
|
||||
|
||||
for (int i = 0; i < _events.Count; i++)
|
||||
{
|
||||
WalkFrameEvent e = _events[i];
|
||||
switch (e.Kind)
|
||||
{
|
||||
case WalkFrameEventKind.StreamMark:
|
||||
order.Append('>').Append(i).Append(":M").Append(e.IntArg);
|
||||
break;
|
||||
case WalkFrameEventKind.AlphaSubmitMark:
|
||||
order.Append('>').Append(i).Append(":AM").Append(e.IntArg);
|
||||
break;
|
||||
case WalkFrameEventKind.Sky:
|
||||
order.Append('>').Append(i).Append(":SKY");
|
||||
break;
|
||||
case WalkFrameEventKind.LandCell:
|
||||
// S3 chunk 3 fix round 1 (F6): the trace token now
|
||||
// carries the LOD side/index too — ":LC<lb>/<side>:<idx>".
|
||||
order.Append('>').Append(i).Append(":LC")
|
||||
.Append(e.CellId.ToString("x8"))
|
||||
.Append('/').Append(e.IntArg >> 8)
|
||||
.Append(':').Append(e.IntArg & 0xFF);
|
||||
break;
|
||||
case WalkFrameEventKind.CellShell when IsCathedralCell(e.CellId):
|
||||
order.Append('>').Append(i).Append(":S")
|
||||
.Append((e.CellId & 0xFFFFu).ToString("X4"));
|
||||
|
||||
if (IsFloatingStairCell(e.CellId))
|
||||
{
|
||||
if (wroteClip)
|
||||
clips.Append(';');
|
||||
wroteClip = true;
|
||||
clips.Append('S')
|
||||
.Append((e.CellId & 0xFFFFu).ToString("X4"))
|
||||
.Append("=whole-once");
|
||||
}
|
||||
break;
|
||||
case WalkFrameEventKind.PunchFan:
|
||||
order.Append('>').Append(i).Append(":P").Append(e.IntArg);
|
||||
break;
|
||||
case WalkFrameEventKind.AlphaBarrier:
|
||||
order.Append('>').Append(i).Append(":AB");
|
||||
break;
|
||||
case WalkFrameEventKind.LandscapeFlush:
|
||||
order.Append('>').Append(i).Append(":LF");
|
||||
break;
|
||||
case WalkFrameEventKind.ClearInteriorDepth:
|
||||
order.Append('>').Append(i).Append(":CLEAR");
|
||||
break;
|
||||
case WalkFrameEventKind.ExitSeals:
|
||||
order.Append('>').Append(i).Append(":SEALS");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine(order.ToString());
|
||||
Console.WriteLine(clips.ToString());
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// IWalkEventSink
|
||||
// ------------------------------------------------------------------
|
||||
|
|
@ -1566,21 +1452,14 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
// look-in flood appended (keeps every range single-stage).
|
||||
MarkIfGrown();
|
||||
WalkFrameStaticRecords shell = _worldData.GetBuildingShellStatics(building);
|
||||
bool skipSouthBuildingShell =
|
||||
AcDream.Core.Rendering.RenderingDiagnostics
|
||||
.ProbeCathedralSkipSouthBuildingShell
|
||||
&& WalkProductionWorldData.AnchorCellId(building) == 0xF4180112u;
|
||||
if (!skipSouthBuildingShell)
|
||||
_populator.PopulateCell(
|
||||
_stream, WalkDrawStage.BuildingShell, building.PositionCellId,
|
||||
shell.Records, shell.TupleLandblockId, _cameraWorldPosition, _viewProjection,
|
||||
alphaSubmissions: _alphaSubmissions);
|
||||
if (_alphaSubmissions.Count != _alphaSubmitMark)
|
||||
{
|
||||
_populator.PopulateCell(
|
||||
_stream, WalkDrawStage.BuildingShell, building.PositionCellId,
|
||||
shell.Records, shell.TupleLandblockId, _cameraWorldPosition, _viewProjection,
|
||||
alphaSubmissions: _alphaSubmissions);
|
||||
if (_alphaSubmissions.Count != _alphaSubmitMark)
|
||||
{
|
||||
MarkIfGrown();
|
||||
MarkAlphaIfGrown();
|
||||
}
|
||||
MarkIfGrown();
|
||||
MarkAlphaIfGrown();
|
||||
}
|
||||
// Campaign OVERHAUL S2 chunk 6: a building's shell fires no particle
|
||||
// turn of its own any more. Retail's add_particle_shadow_to_cell
|
||||
|
|
@ -1597,22 +1476,6 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
ArgumentNullException.ThrowIfNull(polygon);
|
||||
RequireOpenFrame();
|
||||
|
||||
// Output-gated binary discriminator for the cathedral's moving
|
||||
// transparent seam. Cell 0xF4180112 belongs to a distinct authored
|
||||
// building which is reached as a look-in while the camera remains in
|
||||
// 0xF4180107/0108. Suppress only that building's far-Z punch so a
|
||||
// visual gate can prove whether the apparent "beam" is the aperture
|
||||
// depth write or the seven-part stair Setup behind it. Never enabled
|
||||
// in normal production.
|
||||
if (AcDream.Core.Rendering.RenderingDiagnostics
|
||||
.ProbeCathedralSkipStairBuildingPunch
|
||||
&& Array.Exists(
|
||||
building.Portals,
|
||||
portal => portal.OtherCellId == 0xF4180112u))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
MarkIfGrown();
|
||||
Matrix4x4 worldTransform = _worldData.GetBuildingWorldTransform(building);
|
||||
_events.Add(
|
||||
|
|
@ -1685,6 +1548,25 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
EmitFloodTurns(WalkDrawStage.CellStatic, cells);
|
||||
}
|
||||
|
||||
/// <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>
|
||||
void IWalkEventSink.OnWeatherTurn(uint viewerCellId)
|
||||
{
|
||||
if (!AcDream.Core.Rendering.RenderingDiagnostics.DumpWalkTranscriptEnabled)
|
||||
return;
|
||||
|
||||
RequireOpenFrame();
|
||||
WalkTranscriptDump.PrintObjectCellTurn(viewerCellId);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Turn handlers
|
||||
// ------------------------------------------------------------------
|
||||
|
|
@ -1801,18 +1683,8 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
WalkTranscriptDump.PrintEnvCellShell(cells[i]);
|
||||
if (_cellShellsDrawnThisFrame.Add(cells[i]))
|
||||
{
|
||||
bool skipSouthLookInShell =
|
||||
stage == WalkDrawStage.LookInStatic
|
||||
&& ((AcDream.Core.Rendering.RenderingDiagnostics
|
||||
.ProbeCathedralSkipSouthLookInCellShells
|
||||
&& cells[i] is 0xF4180112u or 0xF4180113u or 0xF4180114u)
|
||||
|| AcDream.Core.Rendering.RenderingDiagnostics
|
||||
.ProbeCathedralSkipLookInShellCellId == cells[i]);
|
||||
if (!skipSouthLookInShell)
|
||||
{
|
||||
MarkIfGrown();
|
||||
_events.Add(WalkFrameEvent.CellShell(cells[i]));
|
||||
}
|
||||
MarkIfGrown();
|
||||
_events.Add(WalkFrameEvent.CellShell(cells[i]));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1921,42 +1793,21 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
cellId, sliceStart, _lookInSlices.Count - sliceStart));
|
||||
}
|
||||
|
||||
/// <summary>See the interface member's own doc comment (S3 chunk 4,
|
||||
/// §10.2): replaces the deleted <c>VisibleClipSlotsInLookInTurn</c>'s
|
||||
/// per-slice loop with the SAME admission condition per slice, short-
|
||||
/// circuiting to a Boolean the moment any slice admits instead of
|
||||
/// accumulating a slot list nothing downstream read for its content
|
||||
/// (every caller only ever asked <c>Count == 0</c>).</summary>
|
||||
public bool SphereVisibleInLookInTurn(
|
||||
int routeIndex,
|
||||
in Vector3 center,
|
||||
float radius)
|
||||
float radius,
|
||||
bool testSphere = true)
|
||||
{
|
||||
if ((uint)routeIndex >= (uint)_lookInTurns.Count)
|
||||
return false;
|
||||
|
||||
WalkLookInTurn turn = _lookInTurns[routeIndex];
|
||||
for (int sliceOffset = 0; sliceOffset < turn.SliceCount; sliceOffset++)
|
||||
{
|
||||
WalkLookInSlice slice = _lookInSlices[turn.SliceStart + sliceOffset];
|
||||
if (WalkVisibilityMath.ViewconeCheck(
|
||||
center,
|
||||
radius,
|
||||
_lookInCyPlane,
|
||||
CollectionsMarshal.AsSpan(_lookInPlanes).Slice(
|
||||
slice.PlaneStart,
|
||||
slice.PlaneCount)) != WalkBoundingType.Outside)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public IReadOnlyList<uint> VisibleClipSlotsInLookInTurn(
|
||||
int routeIndex,
|
||||
in Vector3 center,
|
||||
float radius,
|
||||
bool testSphere)
|
||||
{
|
||||
_visibleClipSlotScratch.Clear();
|
||||
if ((uint)routeIndex >= (uint)_lookInTurns.Count)
|
||||
return _visibleClipSlotScratch;
|
||||
|
||||
WalkLookInTurn turn = _lookInTurns[routeIndex];
|
||||
for (int sliceOffset = 0; sliceOffset < turn.SliceCount; sliceOffset++)
|
||||
{
|
||||
|
|
@ -1970,10 +1821,10 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
slice.PlaneStart,
|
||||
slice.PlaneCount)) != WalkBoundingType.Outside)
|
||||
{
|
||||
_visibleClipSlotScratch.Add(slice.ClipSlot);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return _visibleClipSlotScratch;
|
||||
return false;
|
||||
}
|
||||
|
||||
public string DescribeLookInTurn(
|
||||
|
|
|
|||
|
|
@ -95,26 +95,35 @@ public sealed class WalkProductionFrameContext : IWalkFrameContext, IRetailFrame
|
|||
Vector3 forward,
|
||||
Matrix4x4 viewProjection,
|
||||
float viewportWidth,
|
||||
float viewportHeight)
|
||||
float viewportHeight,
|
||||
uint viewerCellId = 0u,
|
||||
bool weatherGateOpen = false)
|
||||
{
|
||||
_cells = cells ?? throw new ArgumentNullException(nameof(cells));
|
||||
_buildings = buildings ?? throw new ArgumentNullException(nameof(buildings));
|
||||
_rays = new InverseViewProjectionRayCaster(
|
||||
viewProjection, viewportWidth, viewportHeight);
|
||||
Reset(worldViewpoint, forward, viewProjection, viewportWidth, viewportHeight);
|
||||
Reset(
|
||||
worldViewpoint, forward, viewProjection, viewportWidth, viewportHeight,
|
||||
viewerCellId, weatherGateOpen);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// FW6 allocation closeout: rebind this retained context to one frame's
|
||||
/// camera values while preserving its active-view scratch. The cell and
|
||||
/// building registries are lifetime owners and therefore never change.
|
||||
/// S3 chunk 4 (O3): <paramref name="viewerCellId"/>/<paramref
|
||||
/// name="weatherGateOpen"/> feed <see cref="IRetailFrameWalkContext"/>'s
|
||||
/// own two new members — see that interface's doc comments.
|
||||
/// </summary>
|
||||
internal void Reset(
|
||||
Vector3 worldViewpoint,
|
||||
Vector3 forward,
|
||||
Matrix4x4 viewProjection,
|
||||
float viewportWidth,
|
||||
float viewportHeight)
|
||||
float viewportHeight,
|
||||
uint viewerCellId = 0u,
|
||||
bool weatherGateOpen = false)
|
||||
{
|
||||
// Validate/invert before publishing any new frame value so a bad
|
||||
// camera matrix leaves the previous usable binding intact.
|
||||
|
|
@ -123,6 +132,8 @@ public sealed class WalkProductionFrameContext : IWalkFrameContext, IRetailFrame
|
|||
_viewProjection = viewProjection;
|
||||
ViewportWidth = viewportWidth;
|
||||
ViewportHeight = viewportHeight;
|
||||
ViewerCellId = viewerCellId;
|
||||
WeatherGateOpen = weatherGateOpen;
|
||||
_activeViewVertCount = 0;
|
||||
// The retail CY near plane: N = forward, d = -dot(eye, forward) - znear.
|
||||
CyPlane = new WalkPlane(forward, -Vector3.Dot(worldViewpoint, forward) - ZNear);
|
||||
|
|
@ -131,6 +142,8 @@ public sealed class WalkProductionFrameContext : IWalkFrameContext, IRetailFrame
|
|||
public Vector3 WorldViewpoint { get; private set; }
|
||||
public float ViewportWidth { get; private set; }
|
||||
public float ViewportHeight { get; private set; }
|
||||
public uint ViewerCellId { get; private set; }
|
||||
public bool WeatherGateOpen { get; private set; }
|
||||
public WalkPlane CyPlane { get; private set; }
|
||||
public IWalkRayCaster Rays => _rays;
|
||||
public IWalkFrameContext CellContext => this;
|
||||
|
|
|
|||
|
|
@ -170,18 +170,22 @@ internal static class WalkTranscriptDump
|
|||
/// line's own DrawSortCell call; it has no separate per-cell OC hook of
|
||||
/// its OWN). S3 chunk 1 fix round 1 (G8), corrected round 2 (§11.6 H1):
|
||||
/// this SAME line kind ALSO prints EXACTLY once more per COMPLETE
|
||||
/// outdoor frame, from a second call site entirely —
|
||||
/// <c>RetailPViewPassExecutor.DrawWeatherOnce</c>, called ONCE after the
|
||||
/// slice loop (round 1 had this inside
|
||||
/// <c>DrawLandscapeSliceLate</c>'s per-slice loop, printing 2-4 times
|
||||
/// for an interior root's exit views — a mismatch every capture
|
||||
/// disproves) — matching retail's <c>GameSky::Draw</c> @0x00506ff0
|
||||
/// outdoor frame, matching retail's <c>GameSky::Draw</c> @0x00506ff0
|
||||
/// (<c>arg2==1</c>) calling <c>DrawObjCellForDummies(after_sky_cell)</c>
|
||||
/// @0x005070da when the player stands outside — the after-sky cell's DID
|
||||
/// is the viewer's own land cell (the SAME id the frame-root "P" line
|
||||
/// already carries). That trailing OC is NOT a walk turn (no owning DC,
|
||||
/// no reversal derivation applies to it) — see this method's own two
|
||||
/// call sites for the addresses.</summary>
|
||||
/// already carries). S3 chunk 4 (O3) moved this trailing print's call
|
||||
/// site from the Replay-time <c>RetailPViewPassExecutor.DrawWeatherOnce</c>
|
||||
/// (which now only draws — no print) to Collect time:
|
||||
/// <c>WalkFrameDriver.OnWeatherTurn</c>, fired by
|
||||
/// <c>RetailFrameWalk.DrawLandscape</c> at the exact end of its
|
||||
/// per-land-cell loop — an interior root's turn therefore lands between
|
||||
/// the "LS" line and the flood's own EC/OC pairs, and an outdoor root's
|
||||
/// transcript ends with it, matching retail's own position (see
|
||||
/// <c>IWalkEventSink.OnWeatherTurn</c>'s doc comment for the full
|
||||
/// citation). That trailing OC is NOT a walk turn (no owning DC, no
|
||||
/// reversal derivation applies to it) — see this method's own two call
|
||||
/// sites for the addresses.</summary>
|
||||
internal static void PrintObjectCellTurn(uint cellId)
|
||||
{
|
||||
if (!Enabled) return;
|
||||
|
|
|
|||
|
|
@ -55,7 +55,6 @@ namespace AcDream.App.Rendering.Wb;
|
|||
/// </summary>
|
||||
public sealed partial class WbDrawDispatcher
|
||||
{
|
||||
private static readonly IReadOnlyList<uint> RetailWholeMeshSlot = new uint[] { 0u };
|
||||
private readonly HashSet<WalkDrawnPartKey> _walkDrawnParts = new();
|
||||
private bool _walkPartFrameActive;
|
||||
private readonly Dictionary<string, string> _facilityStairProbeSignatures = new();
|
||||
|
|
@ -388,7 +387,7 @@ public sealed partial class WbDrawDispatcher
|
|||
Matrix4x4 model = restPose * entity.RootWorld;
|
||||
int selectionPartIndex = unchecked((partIndex << 16) | (setupPartIndex & 0xFFFF));
|
||||
|
||||
IReadOnlyList<uint>? partClipSlots = ResolvePartClipSlots(
|
||||
bool visible = ResolvePartVisible(
|
||||
lookInViews,
|
||||
lookInRouteIndex,
|
||||
partData,
|
||||
|
|
@ -396,7 +395,6 @@ public sealed partial class WbDrawDispatcher
|
|||
out Vector3 sphereCenter,
|
||||
out float sphereRadius,
|
||||
out bool hasSphere);
|
||||
bool visible = partClipSlots is not { Count: 0 };
|
||||
bool firstAdmission = visible
|
||||
&& TryStampWalkPart(in projection, selectionPartIndex);
|
||||
EmitFacilityStairPartProbe(
|
||||
|
|
@ -415,7 +413,7 @@ public sealed partial class WbDrawDispatcher
|
|||
partData, model, in entity, meshRef, paletteIdentity,
|
||||
entityHasCutoutSubset, slot, lights, indoor,
|
||||
selectionLighting, detailCategory, opacity,
|
||||
partClipSlots, batches);
|
||||
batches);
|
||||
selectionParts.Add(new WalkClassifiedSelectionPart(
|
||||
entity.ServerGuid, entity.LocalEntityId, selectionPartIndex,
|
||||
(uint)gfxObjId, model));
|
||||
|
|
@ -441,7 +439,7 @@ public sealed partial class WbDrawDispatcher
|
|||
}
|
||||
|
||||
Matrix4x4 model = meshRef.PartTransform * entity.RootWorld;
|
||||
IReadOnlyList<uint>? partClipSlots = ResolvePartClipSlots(
|
||||
bool visible = ResolvePartVisible(
|
||||
lookInViews,
|
||||
lookInRouteIndex,
|
||||
renderData,
|
||||
|
|
@ -449,7 +447,6 @@ public sealed partial class WbDrawDispatcher
|
|||
out Vector3 sphereCenter,
|
||||
out float sphereRadius,
|
||||
out bool hasSphere);
|
||||
bool visible = partClipSlots is not { Count: 0 };
|
||||
bool firstAdmission = visible
|
||||
&& TryStampWalkPart(in projection, partIndex);
|
||||
EmitFacilityStairPartProbe(
|
||||
|
|
@ -468,7 +465,7 @@ public sealed partial class WbDrawDispatcher
|
|||
renderData, model, in entity, meshRef, paletteIdentity,
|
||||
entityHasCutoutSubsetOverride: null, slot, lights, indoor,
|
||||
selectionLighting, detailCategory, opacity,
|
||||
partClipSlots, batches);
|
||||
batches);
|
||||
selectionParts.Add(new WalkClassifiedSelectionPart(
|
||||
entity.ServerGuid, entity.LocalEntityId, partIndex,
|
||||
(uint)meshRef.GfxObjId, model));
|
||||
|
|
@ -524,7 +521,22 @@ public sealed partial class WbDrawDispatcher
|
|||
lights = InstanceLightSet.From(selected);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<uint>? ResolvePartClipSlots(
|
||||
/// <summary>
|
||||
/// Campaign OVERHAUL S3 chunk 4 (§10.2): retail asks exactly one
|
||||
/// question per part — <c>RenderDeviceD3D::DrawMesh</c> @0x005A0860
|
||||
/// tests the authored drawing sphere against each active PortalList
|
||||
/// view, and once ANY view admits it, <c>DrawMeshInternal</c>
|
||||
/// @0x0059F360 submits the complete constructed mesh WHOLE. The former
|
||||
/// <c>ResolvePartClipSlots</c> asked the walk's now-deleted
|
||||
/// <c>VisibleClipSlotsInLookInTurn</c> for the
|
||||
/// per-view slot LIST and then collapsed it to either "nothing" or the
|
||||
/// single-element <c>RetailWholeMeshSlot</c> sentinel regardless of how
|
||||
/// many views actually admitted — the list was never anything but a
|
||||
/// Boolean wearing a list-shaped costume (the portal polygon is never
|
||||
/// forwarded as a GPU mesh clip). This method asks the Boolean
|
||||
/// directly.
|
||||
/// </summary>
|
||||
private static bool ResolvePartVisible(
|
||||
IWalkLookInViewSource? lookInViews,
|
||||
int routeIndex,
|
||||
ObjectRenderData renderData,
|
||||
|
|
@ -537,34 +549,21 @@ public sealed partial class WbDrawDispatcher
|
|||
sphereRadius = 0f;
|
||||
hasSphere = false;
|
||||
if (lookInViews is null)
|
||||
return null;
|
||||
return true;
|
||||
|
||||
if (renderData.SelectionSphere is not { Radius: > 0f } sphere)
|
||||
{
|
||||
IReadOnlyList<uint> admittedViews = lookInViews.VisibleClipSlotsInLookInTurn(
|
||||
routeIndex,
|
||||
Vector3.Zero,
|
||||
radius: 0f,
|
||||
testSphere: false);
|
||||
return admittedViews.Count == 0
|
||||
? admittedViews
|
||||
: RetailWholeMeshSlot;
|
||||
// No authored drawing sphere: retail has nothing to sphere-test
|
||||
// against, so any admitted view already means "draw" —
|
||||
// testSphere:false asks exactly that (any surviving slice for
|
||||
// this route, geometry ignored).
|
||||
return lookInViews.SphereVisibleInLookInTurn(
|
||||
routeIndex, Vector3.Zero, radius: 0f, testSphere: false);
|
||||
}
|
||||
|
||||
hasSphere = true;
|
||||
TransformDrawingSphere(sphere, localToWorld, out sphereCenter, out sphereRadius);
|
||||
IReadOnlyList<uint> visibleViews = lookInViews.VisibleClipSlotsInLookInTurn(
|
||||
routeIndex,
|
||||
in sphereCenter,
|
||||
sphereRadius,
|
||||
testSphere: true);
|
||||
// RenderDeviceD3D::DrawMesh @0x005A0860 tests the authored drawing
|
||||
// sphere against each active PortalList view. Once any view admits
|
||||
// it, DrawMeshInternal @0x0059F360 submits the complete constructed
|
||||
// mesh. The portal polygon is not forwarded as a GPU mesh clip.
|
||||
return visibleViews.Count == 0
|
||||
? visibleViews
|
||||
: RetailWholeMeshSlot;
|
||||
return lookInViews.SphereVisibleInLookInTurn(routeIndex, in sphereCenter, sphereRadius);
|
||||
}
|
||||
|
||||
private void EmitFacilityStairPartProbe(
|
||||
|
|
@ -819,7 +818,17 @@ public sealed partial class WbDrawDispatcher
|
|||
/// Walks one resolved mesh's batches through <see cref="TryClassifyBatch"/>
|
||||
/// and appends every surviving one to <paramref name="sink"/> at
|
||||
/// <c>Alpha = 1f</c> (see this file's type doc comment for why statics
|
||||
/// never carry a per-part opacity multiplier here).
|
||||
/// never carry a per-part opacity multiplier here). Campaign OVERHAUL S3
|
||||
/// chunk 4 (§10.2): both call sites now gate on <see
|
||||
/// cref="ResolvePartVisible"/>'s plain Boolean before calling this method
|
||||
/// at all — the FORMER per-view clip-SLOT list this method used to loop
|
||||
/// over always collapsed to either nothing (never reached this method;
|
||||
/// the caller's <c>firstAdmission</c> gate skips it) or the single
|
||||
/// <c>RetailWholeMeshSlot</c> sentinel, so looping was never anything but
|
||||
/// one iteration in practice. <paramref name="slot"/> (the entity's OWN
|
||||
/// classic clip-routing slot, always 0 while <c>_clipRoutingActive</c> is
|
||||
/// never armed by the walk path — see <see cref="ResolveSlotForFrame"/>)
|
||||
/// is therefore the ONE clip slot every surviving batch carries.
|
||||
/// </summary>
|
||||
private void EmitClassifiedBatches(
|
||||
ObjectRenderData renderData,
|
||||
|
|
@ -834,7 +843,6 @@ public sealed partial class WbDrawDispatcher
|
|||
Vector2 selectionLighting,
|
||||
uint detailCategory,
|
||||
float opacity,
|
||||
IReadOnlyList<uint>? clipSlots,
|
||||
List<WalkClassifiedBatch> sink)
|
||||
{
|
||||
bool entityHasCutoutSubset = entityHasCutoutSubsetOverride ?? renderData.HasCutoutSubset;
|
||||
|
|
@ -847,23 +855,10 @@ public sealed partial class WbDrawDispatcher
|
|||
if (!survives)
|
||||
continue;
|
||||
|
||||
if (clipSlots is null)
|
||||
{
|
||||
sink.Add(new WalkClassifiedBatch(
|
||||
key, model, slot, lights, indoor ? 1u : 0u, Alpha: opacity,
|
||||
selectionLighting, detailCategory, IsOpaque: IsOpaque(key.Translucency),
|
||||
LocalSortCenter: renderData.SortCenter));
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int clipIndex = 0; clipIndex < clipSlots.Count; clipIndex++)
|
||||
{
|
||||
sink.Add(new WalkClassifiedBatch(
|
||||
key, model, clipSlots[clipIndex], lights, indoor ? 1u : 0u,
|
||||
Alpha: opacity, selectionLighting, detailCategory,
|
||||
IsOpaque: IsOpaque(key.Translucency),
|
||||
LocalSortCenter: renderData.SortCenter));
|
||||
}
|
||||
sink.Add(new WalkClassifiedBatch(
|
||||
key, model, slot, lights, indoor ? 1u : 0u, Alpha: opacity,
|
||||
selectionLighting, detailCategory, IsOpaque: IsOpaque(key.Translucency),
|
||||
LocalSortCenter: renderData.SortCenter));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -110,85 +110,6 @@ public static class RenderingDiagnostics
|
|||
public static bool ProbeFacilityStairsEnabled { get; set; } =
|
||||
Environment.GetEnvironmentVariable("ACDREAM_PROBE_FACILITY_STAIRS") == "1";
|
||||
|
||||
/// <summary>
|
||||
/// Temporary cathedral discriminator: suppresses only the far-Z portal
|
||||
/// punch for the authored building whose look-in owns cell 0xF4180112.
|
||||
/// This is a binary visual experiment for the moving transparent seam at
|
||||
/// the 0xF4180107/0112 floating stairs, never a production admission rule.
|
||||
/// </summary>
|
||||
public static bool ProbeCathedralSkipStairBuildingPunch { get; set; } =
|
||||
Environment.GetEnvironmentVariable("ACDREAM_PROBE_CATHEDRAL_SKIP_PUNCH") == "1";
|
||||
|
||||
/// <summary>
|
||||
/// Temporary cathedral carrier discriminator: suppresses only the visible
|
||||
/// EnvCell shells for cells 0xF4180107 and 0xF4180112. This deliberately
|
||||
/// removes their authored walls so a visual A/B can prove whether the
|
||||
/// moving wall-textured triangles at the floating stairs come from either
|
||||
/// cell shell. It is never a production visibility rule.
|
||||
/// </summary>
|
||||
public static bool ProbeCathedralSkipFloatingStairCellShells { get; set; } =
|
||||
Environment.GetEnvironmentVariable("ACDREAM_PROBE_CATHEDRAL_SKIP_CELL_SHELLS") == "1";
|
||||
|
||||
/// <summary>
|
||||
/// Temporary cathedral carrier discriminator: suppresses the visible
|
||||
/// EnvCell shells for 0xF4180112/0113/0114 only while they are reached by
|
||||
/// a building look-in. Their ordinary interior-root repaint remains
|
||||
/// untouched. This separates a pre-clear look-in cell shell from the
|
||||
/// south building's own exterior shell; it is never a production rule.
|
||||
/// </summary>
|
||||
public static bool ProbeCathedralSkipSouthLookInCellShells { get; set; } =
|
||||
Environment.GetEnvironmentVariable(
|
||||
"ACDREAM_PROBE_CATHEDRAL_SKIP_SOUTH_LOOKIN_SHELLS") == "1";
|
||||
|
||||
/// <summary>
|
||||
/// Narrow form of <see cref="ProbeCathedralSkipSouthLookInCellShells"/>:
|
||||
/// suppresses one exact EnvCell shell only during a building look-in.
|
||||
/// Accepts a full hexadecimal cell id, with or without a <c>0x</c>
|
||||
/// prefix, through <c>ACDREAM_PROBE_CATHEDRAL_SKIP_LOOKIN_SHELL</c>.
|
||||
/// Zero means disabled. Behavior-changing diagnostic only.
|
||||
/// </summary>
|
||||
public static uint ProbeCathedralSkipLookInShellCellId { get; set; } =
|
||||
ParseHexCellId(
|
||||
Environment.GetEnvironmentVariable(
|
||||
"ACDREAM_PROBE_CATHEDRAL_SKIP_LOOKIN_SHELL"));
|
||||
|
||||
/// <summary>
|
||||
/// Temporary cathedral carrier discriminator: suppresses only the own
|
||||
/// exterior shell of the south cathedral building (authored anchor cell
|
||||
/// 0xF4180112). Its portal walk, look-in cells, and particles remain live.
|
||||
/// This is a binary visual experiment, never a production rule.
|
||||
/// </summary>
|
||||
public static bool ProbeCathedralSkipSouthBuildingShell { get; set; } =
|
||||
Environment.GetEnvironmentVariable(
|
||||
"ACDREAM_PROBE_CATHEDRAL_SKIP_SOUTH_BUILDING_SHELL") == "1";
|
||||
|
||||
/// <summary>
|
||||
/// Output-only cathedral floating-stair trace. Samples the ONE frame
|
||||
/// walk's recorded clear/seal/punch/shell replay order and reports the
|
||||
/// render-stamp-deduplicated whole-shell turns for cells 0xF4180104/0106/0107
|
||||
/// and 0xF4180112/0113/0114. It never changes admission, depth state, or
|
||||
/// draw order. Initial state from
|
||||
/// <c>ACDREAM_PROBE_CATHEDRAL_SHELL_ORDER=1</c>.
|
||||
/// </summary>
|
||||
public static bool ProbeCathedralShellOrderEnabled { get; set; } =
|
||||
Environment.GetEnvironmentVariable("ACDREAM_PROBE_CATHEDRAL_SHELL_ORDER") == "1";
|
||||
|
||||
private static uint ParseHexCellId(string? raw)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(raw))
|
||||
return 0;
|
||||
ReadOnlySpan<char> value = raw.AsSpan().Trim();
|
||||
if (value.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
|
||||
value = value[2..];
|
||||
return uint.TryParse(
|
||||
value,
|
||||
NumberStyles.HexNumber,
|
||||
CultureInfo.InvariantCulture,
|
||||
out uint cellId)
|
||||
? cellId
|
||||
: 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Phase U.4c (2026-05-31) flap-convergence probe. When true, the portal
|
||||
/// visibility pass emits, EVERY frame the camera root is an indoor cell, a
|
||||
|
|
|
|||
|
|
@ -202,4 +202,125 @@ public class ClipFrameLayoutTests
|
|||
Assert.Equal(expected.Z, ReadFloat(bytes, offset + 8), 6);
|
||||
Assert.Equal(expected.W, ReadFloat(bytes, offset + 12), 6);
|
||||
}
|
||||
|
||||
// ── S3 chunk 4 (§10.2 KEEP): CPU/GPU clip-plane equivalence pins ────────
|
||||
//
|
||||
// Grepped first (per the chunk 4 contract): ClipPlaneSetTests.cs pins
|
||||
// sign correctness (inside >= 0, just-outside < 0 on SOME plane) and
|
||||
// AppendSlot_FromClipPlaneSet_AxisAlignedSquare_PacksFourPlanes pins
|
||||
// bit-exact GPU packing of ClipPlaneSet's own output — neither asks
|
||||
// whether a point ON an edge has ~0 distance under the planes actually
|
||||
// read back through each KEEP item's real production accessor. These
|
||||
// two tests close that gap, one per KEEP item (exit seals, punch fans).
|
||||
|
||||
/// <summary>
|
||||
/// KEEP item 1 — exit seals: <c>RetailPViewPassExecutor.DrawExitPortalMask</c>
|
||||
/// reads its clip planes through <c>WalkFrameDriver.
|
||||
/// InteriorFloodViewClipPlanesAt</c> → <see cref="ClipFrame.GetSlotPlanes"/>,
|
||||
/// the packed-byte round trip <c>ClipFrameAssembler.Assemble</c> builds
|
||||
/// via <see cref="ClipFrame.AppendSlot(ClipPlaneSet)"/>. For a synthetic
|
||||
/// quad "view" (a stand-in for one live portal view), every edge
|
||||
/// midpoint has near-zero signed distance under the GPU-read-back
|
||||
/// planes AND non-negative distance under every plane (retail's
|
||||
/// <c>polyClipFinish</c> keeps the whole convex region, never clips a
|
||||
/// point that sits ON the boundary) — proving the bytes
|
||||
/// <c>InteriorFloodViewClipPlanesAt</c> hands the seal leaf really are
|
||||
/// the CPU view polygon's own edge planes, not merely SOME bytes that
|
||||
/// happen to round-trip.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GetSlotPlanes_ExitSealPath_EqualsCpuViewPolygonEdgePlanes_ForASyntheticView()
|
||||
{
|
||||
Vector2[] verts =
|
||||
[
|
||||
new(-1f, -0.5f), new(1f, -0.5f), new(1f, 0.5f), new(-1f, 0.5f),
|
||||
];
|
||||
var cv = new CellView();
|
||||
cv.Add(new ViewPolygon(verts));
|
||||
ClipPlaneSet cps = ClipPlaneSet.From(cv);
|
||||
Assert.Equal(4, cps.Count);
|
||||
|
||||
// The EXACT production path: ClipFrameAssembler.Assemble packs the
|
||||
// ClipPlaneSet into a slot; the seal leaf reads it back through
|
||||
// ClipFrame.GetSlotPlanes (WalkFrameDriver.InteriorFloodViewClipPlanesAt).
|
||||
var frame = ClipFrame.NoClip();
|
||||
int slot = frame.AppendSlot(cps);
|
||||
ReadOnlySpan<Vector4> gpuPlanes = frame.GetSlotPlanes(checked((uint)slot));
|
||||
Assert.Equal(4, gpuPlanes.Length);
|
||||
|
||||
AssertEveryEdgeMidpointLiesOnSomeGpuPlane(verts, gpuPlanes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// KEEP item 2 — punch fans: <c>RetailPViewPassExecutor.DrawWalkPunchFan</c>
|
||||
/// reads its clip planes through <c>clipAssembly.OutsideViewSlices
|
||||
/// [activeViewIndex].Planes</c> — <see cref="ClipViewSlice"/>'s
|
||||
/// <c>Planes</c> field, which <c>ClipFrameAssembler.Assemble</c> sets
|
||||
/// DIRECTLY to <c>cps.PlaneArray</c> (no packed-byte round trip at
|
||||
/// all). This pin is therefore the more fundamental of the two: it
|
||||
/// proves <see cref="ClipPlaneSet.From(CellView)"/>'s own output is
|
||||
/// geometrically correct for the polygon it was built from — the
|
||||
/// property BOTH keep items ultimately depend on. Same synthetic-view
|
||||
/// method as the exit-seal pin, a different (non-axis-aligned)
|
||||
/// synthetic polygon so the two pins are not testing the identical
|
||||
/// input.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ClipViewSlicePlanes_PunchFanPath_EqualsCpuViewPolygonEdgePlanes_ForASyntheticView()
|
||||
{
|
||||
Vector2[] verts =
|
||||
[
|
||||
new(0f, 0.6f), new(-0.6f, -0.4f), new(0.5f, -0.5f), new(0.7f, 0.2f),
|
||||
];
|
||||
var cv = new CellView();
|
||||
cv.Add(new ViewPolygon(verts));
|
||||
ClipPlaneSet cps = ClipPlaneSet.From(cv);
|
||||
Assert.True(cps.Count >= 3);
|
||||
|
||||
// ClipFrameAssembler.Assemble: `planes = cps.PlaneArray; slices.Add(
|
||||
// new ClipViewSlice(slot, AabbOf(poly), planes));` — the SAME array
|
||||
// reference DrawWalkPunchFan reads through
|
||||
// clipAssembly.OutsideViewSlices[activeViewIndex].Planes.
|
||||
var slice = new ClipViewSlice(0, default, cps.PlaneArray);
|
||||
|
||||
AssertEveryEdgeMidpointLiesOnSomeGpuPlane(verts, slice.Planes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CPU/GPU equivalence: a point on a convex polygon's edge must sit
|
||||
/// (a) non-negative under EVERY plane (still inside-or-on the region —
|
||||
/// no plane clips it away) and (b) within <paramref name="verts"/>'s
|
||||
/// own floating-point tolerance of ZERO under at least one plane (that
|
||||
/// edge's own plane). MUTATION: perturbing any one GPU plane's offset
|
||||
/// or normal after it left <see cref="ClipPlaneSet.From(CellView)"/>
|
||||
/// (a packing/binding bug) breaks either (a) for a nearby edge or (b)
|
||||
/// for its own edge, failing the corresponding assertion below.
|
||||
/// </summary>
|
||||
private static void AssertEveryEdgeMidpointLiesOnSomeGpuPlane(
|
||||
Vector2[] verts, ReadOnlySpan<Vector4> gpuPlanes)
|
||||
{
|
||||
const float eps = 1e-4f;
|
||||
for (int i = 0; i < verts.Length; i++)
|
||||
{
|
||||
Vector2 a = verts[i];
|
||||
Vector2 b = verts[(i + 1) % verts.Length];
|
||||
Vector2 mid = (a + b) / 2f;
|
||||
var clip = new Vector4(mid.X, mid.Y, 0f, 1f);
|
||||
|
||||
float minAbsDistance = float.PositiveInfinity;
|
||||
foreach (Vector4 plane in gpuPlanes)
|
||||
{
|
||||
float distance = Vector4.Dot(plane, clip);
|
||||
Assert.True(
|
||||
distance >= -eps,
|
||||
$"edge {i} midpoint ({mid.X},{mid.Y}) must be inside-or-on every "
|
||||
+ $"GPU plane; plane {plane} gave distance {distance}");
|
||||
minAbsDistance = MathF.Min(minAbsDistance, MathF.Abs(distance));
|
||||
}
|
||||
Assert.True(
|
||||
minAbsDistance < eps,
|
||||
$"edge {i} midpoint ({mid.X},{mid.Y}) should lie ~on its OWN GPU plane; "
|
||||
+ $"the closest plane was only {minAbsDistance} away");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -113,45 +113,25 @@ public sealed class RetailPViewPassExecutorTests
|
|||
|
||||
/// <summary>
|
||||
/// S3 chunk 1 fix round 2 (§11.6 H1): the weather MESH draw + its OC
|
||||
/// print moved OUT of the per-slice loop into
|
||||
/// <see cref="RetailPViewPassExecutor.DrawWeatherOnce"/>
|
||||
/// (that method's own doc comment has the full decomp citation).
|
||||
/// <see cref="RetailPViewPassExecutor.DrawLandscapeSliceLate"/> must no
|
||||
/// longer call either <see cref="SkyRenderer.RenderWeather"/> or
|
||||
/// <see cref="WalkTranscriptDump"/>'s OC print — only the per-slice
|
||||
/// rain PARTICLE emitters (<c>ParticleRenderer.Draw</c>) stay here.
|
||||
/// MUTATION: re-inlining either call back into this method makes the
|
||||
/// corresponding <c>Assert.DoesNotContain</c> fail.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void DrawLandscapeSliceLate_NoLongerDrawsOrPrintsTheWeatherPass()
|
||||
{
|
||||
MethodInfo method = typeof(RetailPViewPassExecutor).GetMethod(
|
||||
nameof(RetailPViewPassExecutor.DrawLandscapeSliceLate),
|
||||
BindingFlags.Instance | BindingFlags.Public)!;
|
||||
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(method);
|
||||
|
||||
Assert.DoesNotContain(
|
||||
calls,
|
||||
call => call.Target.DeclaringType == typeof(SkyRenderer)
|
||||
&& call.Target.Name == nameof(SkyRenderer.RenderWeather));
|
||||
Assert.DoesNotContain(
|
||||
calls,
|
||||
call => call.Target.DeclaringType == typeof(WalkTranscriptDump)
|
||||
&& call.Target.Name == "PrintObjectCellTurn");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// S3 chunk 1 fix round 2 (§11.6 H1): the new call site itself — exactly
|
||||
/// one <see cref="SkyRenderer.RenderWeather"/> draw and exactly one OC
|
||||
/// print per invocation (retail draws the weather pass exactly once per
|
||||
/// frame; this method is the ONE call site production now uses — see
|
||||
/// <see cref="DrawLandscapeDynamicsPhase_CallsDrawWeatherOnceExactlyOnceAfterTheSliceLoop"/>).
|
||||
/// MUTATION: deleting either call makes the matching
|
||||
/// print moved OUT of <see cref="RetailPViewPassExecutor.DrawWeatherOnce"/>
|
||||
/// entirely — S3 chunk 4 (O3) relocated the print to
|
||||
/// <c>WalkFrameDriver.OnWeatherTurn</c>, fired at Collect time by
|
||||
/// <c>RetailFrameWalk.DrawLandscape</c> (see the real transcript pins in
|
||||
/// <c>WalkFrameDriverTranscriptTests</c>: <c>Collect_OutdoorRoot_...</c>
|
||||
/// and <c>Collect_InteriorRoot_...</c>). This method now draws the
|
||||
/// weather MESH and the rain PARTICLES only — the former per-outside-
|
||||
/// view-slice loop that used to run before this call (<c>SetTerrainClip</c>
|
||||
/// + <c>ClearClipRouting</c> + the old <c>DrawLandscapeSliceLate</c> leaf)
|
||||
/// is deleted outright (§10.2): retail draws the weather mesh and its
|
||||
/// rain particles ONCE, unclipped, never once per doorway aperture.
|
||||
/// MUTATION: re-inlining a
|
||||
/// <c>WalkTranscriptDump.PrintObjectCellTurn</c> call back into this
|
||||
/// method makes the <c>Assert.DoesNotContain</c> below fail; deleting
|
||||
/// either the mesh or the particle call makes the matching
|
||||
/// <c>Assert.Single</c> fail (zero matches instead of one).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void DrawWeatherOnce_DrawsTheWeatherMeshAndPrintsExactlyOnce()
|
||||
public void DrawWeatherOnce_DrawsTheWeatherMeshAndParticlesButNeverPrints()
|
||||
{
|
||||
MethodInfo method = typeof(RetailPViewPassExecutor).GetMethod(
|
||||
nameof(RetailPViewPassExecutor.DrawWeatherOnce),
|
||||
|
|
@ -164,45 +144,37 @@ public sealed class RetailPViewPassExecutorTests
|
|||
&& call.Target.Name == nameof(SkyRenderer.RenderWeather));
|
||||
Assert.Single(
|
||||
calls,
|
||||
call => call.Target.DeclaringType == typeof(WalkTranscriptDump)
|
||||
&& call.Target.Name == "PrintObjectCellTurn");
|
||||
call => call.Target.DeclaringType == typeof(ParticleRenderer)
|
||||
&& call.Target.Name == nameof(ParticleRenderer.Draw));
|
||||
Assert.DoesNotContain(
|
||||
calls,
|
||||
call => call.Target.DeclaringType == typeof(WalkTranscriptDump));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// S3 chunk 1 fix round 2 (§11.6 H1): the actual wiring fix — production
|
||||
/// must call <c>DrawWeatherOnce</c> exactly ONCE, positioned AFTER the
|
||||
/// (single, in-loop) <c>DrawLandscapeSliceLate</c> call site. A
|
||||
/// <c>foreach</c> loop compiles to ONE call instruction regardless of
|
||||
/// how many <c>OutsideViewSlices</c> it iterates at runtime — an
|
||||
/// interior root's 2-4 exit views would otherwise still print 2-4 OC
|
||||
/// lines even after <see cref="DrawLandscapeSliceLate_NoLongerDrawsOrPrintsTheWeatherPass"/>
|
||||
/// passes, if <c>DrawWeatherOnce</c> were called FROM INSIDE that same
|
||||
/// loop instead of after it. IL offsets (not list position) prove
|
||||
/// source order for this straight-line method. MUTATION: moving the
|
||||
/// <c>DrawWeatherOnce</c> call back inside the <c>foreach</c> (or before
|
||||
/// the loop) makes the offset-ordering assertion fail; adding a second
|
||||
/// call site makes <c>Assert.Single</c> fail.
|
||||
/// S3 chunk 4 (§10.2): the former per-outside-view-slice loop
|
||||
/// (<c>SetTerrainClip</c> + <c>ClearClipRouting</c> + the old
|
||||
/// <c>DrawLandscapeSliceLate</c> leaf, one call per active landscape
|
||||
/// view) is deleted — <c>DrawLandscapeDynamicsPhase</c> now calls
|
||||
/// <see cref="RetailPViewPassExecutor.DrawWeatherOnce"/> exactly once,
|
||||
/// unconditionally, with no loop of any kind around it (a deleted
|
||||
/// symbol cannot be re-introduced without a compile error, so this pin
|
||||
/// only needs to rule out a NEW multi-call path). MUTATION: adding a
|
||||
/// second call site (e.g. reintroducing a per-slice loop around a new
|
||||
/// leaf) makes <c>Assert.Single</c> fail.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void DrawLandscapeDynamicsPhase_CallsDrawWeatherOnceExactlyOnceAfterTheSliceLoop()
|
||||
public void DrawLandscapeDynamicsPhase_CallsDrawWeatherOnceExactlyOnce()
|
||||
{
|
||||
MethodInfo method = typeof(RetailPViewRenderer).GetMethod(
|
||||
"DrawLandscapeDynamicsPhase",
|
||||
BindingFlags.Instance | BindingFlags.NonPublic)!;
|
||||
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(method);
|
||||
|
||||
CompiledCall sliceLate = Assert.Single(
|
||||
calls,
|
||||
call => call.Target.DeclaringType == typeof(RetailPViewPassExecutor)
|
||||
&& call.Target.Name == nameof(RetailPViewPassExecutor.DrawLandscapeSliceLate));
|
||||
CompiledCall weatherOnce = Assert.Single(
|
||||
Assert.Single(
|
||||
calls,
|
||||
call => call.Target.DeclaringType == typeof(RetailPViewPassExecutor)
|
||||
&& call.Target.Name == nameof(RetailPViewPassExecutor.DrawWeatherOnce));
|
||||
|
||||
Assert.True(
|
||||
sliceLate.Offset < weatherOnce.Offset,
|
||||
"DrawWeatherOnce must be called AFTER the per-slice loop, not from inside it.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -187,6 +187,15 @@ public sealed partial class WalkFrameDriverTests
|
|||
public float ViewportWidth => 640f;
|
||||
public float ViewportHeight => 480f;
|
||||
|
||||
/// <summary>S3 chunk 4 (O1/O2/O3): overrides <see
|
||||
/// cref="IRetailFrameWalkContext"/>'s default-false/-zero weather-turn
|
||||
/// members so a test can arm the gate and pin the printed "OC" cell
|
||||
/// id. Real production values wire <c>RetailPViewFrameInput.ViewerCellId</c>
|
||||
/// / <c>RetailPViewPassExecutor.ShouldDrawWeatherOnce(...)</c> — see
|
||||
/// <c>WalkProductionFrameContext</c>.</summary>
|
||||
public uint ViewerCellId { get; set; }
|
||||
public bool WeatherGateOpen { get; set; }
|
||||
|
||||
public Vector3 ViewpointInBuilding(WalkBuilding building) => Vector3.Zero;
|
||||
|
||||
public float ViewerDistanceTo(WalkBuilding building) =>
|
||||
|
|
@ -782,9 +791,6 @@ public sealed partial class WalkFrameDriverTests
|
|||
0, in insideCone, 0.1f));
|
||||
Assert.False(driver.SphereVisibleInLookInTurn(
|
||||
0, new Vector3(10_000f, 0f, 10f), 0.1f));
|
||||
uint clipSlot = Assert.Single(driver.VisibleClipSlotsInLookInTurn(
|
||||
0, in insideCone, 0.1f, testSphere: true));
|
||||
Assert.NotEqual(0u, clipSlot);
|
||||
Assert.Equal(2, clipFrame.SlotCount);
|
||||
|
||||
// The punch polygon reached the leaf renderer in WORLD space: the
|
||||
|
|
|
|||
|
|
@ -465,4 +465,250 @@ public sealed partial class WalkFrameDriverTests
|
|||
RenderingDiagnostics.DumpWalkTranscriptEnabled = previous;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign OVERHAUL S3 chunk 4 (O1/O2): a REAL once-per-frame pin for
|
||||
/// the weather "OC" transcript line, observed through the printed
|
||||
/// lines produced by driving the actual <see cref="RetailFrameWalk"/> +
|
||||
/// <see cref="WalkFrameDriver"/> pair — not IL offsets (the ledger's
|
||||
/// own finding: an IL-offset "call order" check cannot fail when the
|
||||
/// call is moved back inside a loop the reviewers can reproduce with the
|
||||
/// suite green). An outdoor root's <c>DrawLandscape</c> is the LAST
|
||||
/// thing <see cref="RetailFrameWalk.WalkFrame"/> does, so retail's
|
||||
/// <c>GameSky::Draw(sky,1)</c> turn — and therefore this "OC" line — is
|
||||
/// the frame's last event.
|
||||
/// <para>
|
||||
/// MUTATION (a, print twice): if <c>IWalkEventSink.OnWeatherTurn</c>
|
||||
/// fired twice (e.g. a regression reintroducing a per-slice loop), the
|
||||
/// <c>Assert.Single</c> below would see 2 matches and fail with
|
||||
/// "Collection contained more than one match".
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// MUTATION (b, wrong cell): the print always carries whatever <see
|
||||
/// cref="IRetailFrameWalkContext.ViewerCellId"/> the context supplies
|
||||
/// (production wires <c>RetailPViewFrameInput.ViewerCellId</c>, never
|
||||
/// <c>PlayerCellId</c> — see <c>WalkProductionFrameContext</c>'s ctor/
|
||||
/// <c>Reset</c> call sites in <c>RetailPViewRenderer.DrawInside</c>); if
|
||||
/// production started wiring <c>PlayerCellId</c> instead, this test's
|
||||
/// own <see cref="TestContext.ViewerCellId"/> would still print
|
||||
/// correctly (the walk layer only ever sees the ONE value it is
|
||||
/// handed) — the <c>Assert.Equal(ctx.ViewerCellId, ...)</c> below pins
|
||||
/// that the walk layer's own plumbing never substitutes a different
|
||||
/// value of its own.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// MUTATION (c, wrong time): if the print instead fired lazily at
|
||||
/// Replay time (as it used to, from <c>DrawWeatherOnce</c>) this test —
|
||||
/// which only calls <see cref="WalkFrameDriver.Collect"/>, never
|
||||
/// <see cref="WalkFrameDriver.Replay"/> — would show ZERO
|
||||
/// <see cref="WalkOracleEventKind.ObjectCellTurn"/> events, and
|
||||
/// <c>Assert.Single</c> would fail with "no match" instead of finding
|
||||
/// exactly one.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Collect_OutdoorRoot_WeatherGateOpen_PrintsExactlyOneOcAtFrameEndMatchingTheViewerCell()
|
||||
{
|
||||
bool previous = RenderingDiagnostics.DumpWalkTranscriptEnabled;
|
||||
RenderingDiagnostics.DumpWalkTranscriptEnabled = true;
|
||||
TextWriter originalOut = Console.Out;
|
||||
var capture = new StringWriter();
|
||||
try
|
||||
{
|
||||
Console.SetOut(capture);
|
||||
|
||||
using var fx = new DispatcherFixture();
|
||||
const uint viewerCellId = 0xF4180003u; // low word < 0x100 -> outdoor root
|
||||
var ctx = new TestContext { WeatherGateOpen = true, ViewerCellId = viewerCellId };
|
||||
var landscape = new WalkLandscape { MidWidth = 1, Blocks = new WalkLandBlock?[1] };
|
||||
var leaf = new RecordingLeafRenderer(new List<string>());
|
||||
var driver = new WalkFrameDriver(fx.Dispatcher, leaf, new FakeWorldData());
|
||||
var walk = new RetailFrameWalk();
|
||||
|
||||
// Two Collect calls — WalkOracleTrace.Parse (like every real
|
||||
// capture) discards the final in-progress frame.
|
||||
driver.Collect(
|
||||
walk, viewerCellId, null, landscape, ctx, Matrix4x4.Identity, Vector3.Zero);
|
||||
driver.Collect(
|
||||
walk, viewerCellId, null, landscape, ctx, Matrix4x4.Identity, Vector3.Zero);
|
||||
|
||||
Console.Out.Flush();
|
||||
string[] lines = capture.ToString()
|
||||
.Split('\n', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(l => l.TrimEnd('\r'))
|
||||
.ToArray();
|
||||
|
||||
IReadOnlyList<WalkOracleFrame> frames = WalkOracleTrace.Parse(lines);
|
||||
WalkOracleFrame frame = Assert.Single(frames);
|
||||
Assert.NotNull(frame.Pose);
|
||||
Assert.Equal(viewerCellId, frame.Pose!.CellId);
|
||||
|
||||
WalkOracleEvent oc = Assert.Single(
|
||||
frame.Events, e => e.Kind == WalkOracleEventKind.ObjectCellTurn);
|
||||
Assert.Equal(viewerCellId, oc.CellId);
|
||||
Assert.Same(frame.Events[frame.Events.Count - 1], oc);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.SetOut(originalOut);
|
||||
RenderingDiagnostics.DumpWalkTranscriptEnabled = previous;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign OVERHAUL S3 chunk 4 (O1/O2/O3): the interior-root sibling of
|
||||
/// <see cref="Collect_OutdoorRoot_WeatherGateOpen_PrintsExactlyOneOcAtFrameEndMatchingTheViewerCell"/> —
|
||||
/// retail's <c>GameSky::Draw(sky,1)</c> runs at the END of <c>LScape::
|
||||
/// draw</c>, which an interior root calls from <c>DrawInside</c> BEFORE
|
||||
/// <c>OnInteriorFloodDrawTurn</c> draws the flood's own cells (first the
|
||||
/// reverse EnvCell-shell loop, then the reverse object-list loop) — so
|
||||
/// the weather "OC" line must sit strictly between the "LS" line and the
|
||||
/// flood's FIRST "EC" line, never mixed in among the flood's own EC/OC
|
||||
/// pairs (which fire after it, per-cell, from
|
||||
/// <c>WalkFrameDriver.EmitFloodTurns</c>).
|
||||
/// <para>
|
||||
/// MUTATION (a, print twice): a second <c>OnWeatherTurn</c> firing would
|
||||
/// add a second <see cref="WalkOracleEventKind.ObjectCellTurn"/> to the
|
||||
/// LS-to-first-EC window, failing <c>Assert.Single</c> on the filtered
|
||||
/// list below.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// MUTATION (c, wrong time — the ledger's actual round-1 regression):
|
||||
/// the OLD call site printed the weather OC from the Replay-time
|
||||
/// <c>LandscapeFlush</c> leaf, i.e. at the FRAME'S END (after every
|
||||
/// flood EC/OC), not between the landscape and the flood. Moving the
|
||||
/// print there again would move it OUT of the LS-to-first-EC window,
|
||||
/// making the windowed <c>Assert.Single</c> below find zero matches
|
||||
/// instead of one.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Collect_InteriorRoot_WeatherGateOpen_PrintsOneOcBetweenLandscapeAndTheFloodsFirstShell()
|
||||
{
|
||||
bool previous = RenderingDiagnostics.DumpWalkTranscriptEnabled;
|
||||
RenderingDiagnostics.DumpWalkTranscriptEnabled = true;
|
||||
TextWriter originalOut = Console.Out;
|
||||
var capture = new StringWriter();
|
||||
try
|
||||
{
|
||||
Console.SetOut(capture);
|
||||
|
||||
using var fx = new DispatcherFixture();
|
||||
var ctx = new TestContext();
|
||||
(WalkCell cell1, WalkCell cell2) = BuildTranscriptFixtureCells(ctx);
|
||||
// Production always hands the walk the SAME cell id as both the
|
||||
// Collect root and the "P" line's own cell (ctx.ViewerCellId ==
|
||||
// cameraCellId at the real call site).
|
||||
ctx.ViewerCellId = cell1.CellId;
|
||||
ctx.WeatherGateOpen = true;
|
||||
var landscape = new WalkLandscape { MidWidth = 1, Blocks = new WalkLandBlock?[1] };
|
||||
var worldData = new FakeWorldData();
|
||||
var leaf = new RecordingLeafRenderer(new List<string>());
|
||||
using ClipFrame clipFrame = ClipFrame.NoClip();
|
||||
var driver = new WalkFrameDriver(fx.Dispatcher, leaf, worldData, clipFrame: clipFrame);
|
||||
var walk = new RetailFrameWalk();
|
||||
|
||||
driver.Collect(
|
||||
walk, cell1.CellId, cell1, landscape, ctx, Matrix4x4.Identity, Vector3.Zero);
|
||||
driver.Collect(
|
||||
walk, cell1.CellId, cell1, landscape, ctx, Matrix4x4.Identity, Vector3.Zero);
|
||||
|
||||
Console.Out.Flush();
|
||||
string[] lines = capture.ToString()
|
||||
.Split('\n', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(l => l.TrimEnd('\r'))
|
||||
.ToArray();
|
||||
|
||||
IReadOnlyList<WalkOracleFrame> frames = WalkOracleTrace.Parse(lines);
|
||||
WalkOracleFrame frame = Assert.Single(frames);
|
||||
List<WalkOracleEvent> events = frame.Events.ToList();
|
||||
|
||||
int landscapeIndex = events.FindIndex(e => e.Kind == WalkOracleEventKind.Landscape);
|
||||
int firstShellIndex = events.FindIndex(e => e.Kind == WalkOracleEventKind.EnvCellShell);
|
||||
Assert.True(landscapeIndex >= 0, "expected an LS line (ov=1 from the fixture's exit portal)");
|
||||
Assert.True(firstShellIndex >= 0, "expected the flood's own EC lines");
|
||||
Assert.True(
|
||||
landscapeIndex < firstShellIndex,
|
||||
"the landscape turn must precede the flood's own shells");
|
||||
|
||||
List<WalkOracleEvent> ocInWindow = events
|
||||
.Where((e, i) => e.Kind == WalkOracleEventKind.ObjectCellTurn
|
||||
&& i > landscapeIndex && i < firstShellIndex)
|
||||
.ToList();
|
||||
WalkOracleEvent weatherOc = Assert.Single(ocInWindow);
|
||||
Assert.Equal(cell1.CellId, weatherOc.CellId);
|
||||
|
||||
// The flood's OWN EC/OC pairs (cell1, cell2) still exist, all AFTER
|
||||
// the window above — confirms the weather OC is not merely one of
|
||||
// those, even though cell1's id happens to match ctx.ViewerCellId.
|
||||
List<WalkOracleEvent> floodOc = events
|
||||
.Where(e => e.Kind == WalkOracleEventKind.ObjectCellTurn)
|
||||
.Skip(1) // drop the weather OC found above
|
||||
.ToList();
|
||||
Assert.Equal(
|
||||
new HashSet<uint> { cell1.CellId, cell2.CellId },
|
||||
floodOc.Select(e => e.CellId!.Value).ToHashSet());
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.SetOut(originalOut);
|
||||
RenderingDiagnostics.DumpWalkTranscriptEnabled = previous;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign OVERHAUL S3 chunk 4 (O1): the gate's negative half — an
|
||||
/// indoor player (or either render toggle off) never prints the weather
|
||||
/// "OC" line at all, matching <see
|
||||
/// cref="RetailPViewPassExecutor.ShouldDrawWeatherOnce"/>'s own pinned
|
||||
/// predicate (<c>RetailPViewPassExecutorTests.
|
||||
/// ShouldDrawWeatherOnce_MatchesRetailIsPlayerOutsideGate</c>) — this
|
||||
/// test proves the WALK actually honors that gate rather than firing
|
||||
/// unconditionally. MUTATION: dropping the <c>if (ctx.WeatherGateOpen)</c>
|
||||
/// guard in <see cref="RetailFrameWalk.DrawLandscape"/> would print an
|
||||
/// extra "OC" line at the frame's end, making <c>Assert.Same</c> below
|
||||
/// see a DIFFERENT cell (the flood's own object turn is what would then
|
||||
/// occupy that slot) or the event count assertion fail.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Collect_WeatherGateClosed_NeverFiresTheWeatherTurn()
|
||||
{
|
||||
bool previous = RenderingDiagnostics.DumpWalkTranscriptEnabled;
|
||||
RenderingDiagnostics.DumpWalkTranscriptEnabled = true;
|
||||
TextWriter originalOut = Console.Out;
|
||||
var capture = new StringWriter();
|
||||
try
|
||||
{
|
||||
Console.SetOut(capture);
|
||||
|
||||
using var fx = new DispatcherFixture();
|
||||
const uint viewerCellId = 0xF4180003u;
|
||||
var ctx = new TestContext { WeatherGateOpen = false, ViewerCellId = viewerCellId };
|
||||
var landscape = new WalkLandscape { MidWidth = 1, Blocks = new WalkLandBlock?[1] };
|
||||
var leaf = new RecordingLeafRenderer(new List<string>());
|
||||
var driver = new WalkFrameDriver(fx.Dispatcher, leaf, new FakeWorldData());
|
||||
var walk = new RetailFrameWalk();
|
||||
|
||||
driver.Collect(
|
||||
walk, viewerCellId, null, landscape, ctx, Matrix4x4.Identity, Vector3.Zero);
|
||||
driver.Collect(
|
||||
walk, viewerCellId, null, landscape, ctx, Matrix4x4.Identity, Vector3.Zero);
|
||||
|
||||
Console.Out.Flush();
|
||||
string[] lines = capture.ToString()
|
||||
.Split('\n', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(l => l.TrimEnd('\r'))
|
||||
.ToArray();
|
||||
|
||||
IReadOnlyList<WalkOracleFrame> frames = WalkOracleTrace.Parse(lines);
|
||||
WalkOracleFrame frame = Assert.Single(frames);
|
||||
Assert.DoesNotContain(
|
||||
frame.Events, e => e.Kind == WalkOracleEventKind.ObjectCellTurn);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.SetOut(originalOut);
|
||||
RenderingDiagnostics.DumpWalkTranscriptEnabled = previous;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -154,6 +154,54 @@ public sealed class WalkProductionFrameContextTests
|
|||
Assert.Equal(-Vector3.Dot(eye, forward) - WalkProductionFrameContext.ZNear, ctx.CyPlane.D);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign OVERHAUL S3 chunk 4 (O1/O2): <see
|
||||
/// cref="WalkProductionFrameContext"/> is the ONE place production
|
||||
/// converts <c>RetailPViewFrameInput.ViewerCellId</c> and the
|
||||
/// pre-computed <c>RetailPViewPassExecutor.ShouldDrawWeatherOnce</c>
|
||||
/// gate into <see cref="IRetailFrameWalkContext.ViewerCellId"/> / <see
|
||||
/// cref="IRetailFrameWalkContext.WeatherGateOpen"/> — this pins that
|
||||
/// both the constructor and <see cref="WalkProductionFrameContext.Reset"/>
|
||||
/// store and expose EXACTLY what they were given, both at construction
|
||||
/// and after a later rebind (this driver instance is retained and
|
||||
/// re-bound once per frame — <see
|
||||
/// cref="Reset_RebindsCameraValuesAndRetainsTheRayCaster"/>'s own
|
||||
/// pattern). Defaults (0 / false) match every OTHER caller that never
|
||||
/// wires the weather turn (fixtures, the FW1 conformance harness).
|
||||
/// MUTATION: swapping either argument for the other's default at the
|
||||
/// production call site (<c>RetailPViewRenderer.DrawInside</c>) is a
|
||||
/// code-visible defect this test does not itself catch (that requires
|
||||
/// reading the production call site — the real end-to-end proof lives
|
||||
/// in <c>WalkFrameDriverTranscriptTests</c>'s printed-transcript pins,
|
||||
/// which observe whatever value actually reaches the walk); this test
|
||||
/// instead pins that the STORAGE seam itself never drops or swaps its
|
||||
/// own two arguments.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Constructor_and_Reset_StoreTheViewerCellIdAndWeatherGate()
|
||||
{
|
||||
var ctx = new WalkProductionFrameContext(
|
||||
new CellVisibility(), new WalkBuildingRegistry(), Vector3.Zero, Vector3.UnitY,
|
||||
SimpleViewProjection(), 1024f, 768f);
|
||||
Assert.Equal(0u, ctx.ViewerCellId);
|
||||
Assert.False(ctx.WeatherGateOpen);
|
||||
|
||||
var ctxArmed = new WalkProductionFrameContext(
|
||||
new CellVisibility(), new WalkBuildingRegistry(), Vector3.Zero, Vector3.UnitY,
|
||||
SimpleViewProjection(), 1024f, 768f,
|
||||
viewerCellId: 0xF4180003u, weatherGateOpen: true);
|
||||
Assert.Equal(0xF4180003u, ctxArmed.ViewerCellId);
|
||||
Assert.True(ctxArmed.WeatherGateOpen);
|
||||
|
||||
// A later Reset (the per-frame rebind) fully replaces both — no
|
||||
// stale carry-over from the previous frame's values.
|
||||
ctxArmed.Reset(
|
||||
Vector3.Zero, Vector3.UnitY, SimpleViewProjection(), 1024f, 768f,
|
||||
viewerCellId: 0xA9B40100u, weatherGateOpen: false);
|
||||
Assert.Equal(0xA9B40100u, ctxArmed.ViewerCellId);
|
||||
Assert.False(ctxArmed.WeatherGateOpen);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_WithBadProjectionLeavesThePreviousBindingUsable()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -50,15 +50,10 @@ public sealed class WalkStaticStreamPopulatorTests
|
|||
public IReadOnlyList<uint> LookInCellTurns { get; } = [0x8C040112u];
|
||||
|
||||
public bool SphereVisibleInLookInTurn(
|
||||
int routeIndex,
|
||||
in Vector3 center,
|
||||
float radius) => slots.Length != 0;
|
||||
|
||||
public IReadOnlyList<uint> VisibleClipSlotsInLookInTurn(
|
||||
int routeIndex,
|
||||
in Vector3 center,
|
||||
float radius,
|
||||
bool testSphere) => slots;
|
||||
bool testSphere = true) => slots.Length != 0;
|
||||
}
|
||||
|
||||
// ── Synthetic RenderProjectionRecord construction ──────────────────────
|
||||
|
|
|
|||
|
|
@ -49,24 +49,13 @@ public sealed class WbDrawDispatcherLookInConeTests
|
|||
public bool SphereVisibleInLookInTurn(
|
||||
int routeIndex,
|
||||
in Vector3 center,
|
||||
float radius)
|
||||
float radius,
|
||||
bool testSphere = true)
|
||||
{
|
||||
RouteIndex = routeIndex;
|
||||
Center = center;
|
||||
Radius = radius;
|
||||
return true;
|
||||
}
|
||||
|
||||
public IReadOnlyList<uint> VisibleClipSlotsInLookInTurn(
|
||||
int routeIndex,
|
||||
in Vector3 center,
|
||||
float radius,
|
||||
bool testSphere)
|
||||
{
|
||||
RouteIndex = routeIndex;
|
||||
Center = center;
|
||||
Radius = radius;
|
||||
return [7u];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue