refactor(rendering): delete superseded visibility probes

Delete the callerless portal-BFS research graph and spent renderer probe families while retaining the production RetailFrameWalk path, terrain diagnostics, membership invariant, and walk transcript.

Mutation evidence (all restored):

1. Restored PortalVisibilityBuilder type -> AppAssembly_ContainsNoSupersededPortalGraphTypes first failed Assert.Empty with AcDream.App.Rendering.PortalVisibilityBuilder.

2. Restored ACDREAM_PROBE_FACILITY_STAIRS -> ProductionSource_ContainsNoDeletedRendererProbe_AndRetainsWalkTranscriptProof first failed Assert.Empty on RenderingDiagnostics.cs.

3. Added a second RetailFrameWalk field -> WalkFrameOwners_AreUnique first failed Assert.Single with _frameWalk and _mutatedSecondFrameWalk.

4. Added OrderBy to OrderedStream -> OrderedWalkStream_HasNoCrossStreamReorder first failed Assert.DoesNotContain on OrderBy(.

5. Added IDatReaderWriter parameter -> FrameTimeWalkOwners_HaveNoRawDatDependency first failed Assert.Empty on RetailFrameWalk.MutatedRawDatParameter.
This commit is contained in:
Erik 2026-09-05 04:46:31 +02:00
parent b77989c323
commit bf53e2ad6e
82 changed files with 451 additions and 10108 deletions

View file

@ -1102,8 +1102,10 @@ rooting and portal recursion. Its root is the collided camera/viewer cell or
retail's synthetic outdoor cell; the player's current cell separately owns
sunlight and indoor lighting. A null root exists only for login/debug/streaming-
gap fallback frames. `RetailPViewRenderer` and `RetailPViewPassExecutor` are
composition/pass facades, and `PortalVisibilityBuilder` has no production
caller. **Campaign OVERHAUL S2 (chunks 1-5, closed 2026-09-03) gave render
composition/pass facades. The superseded `PortalVisibilityBuilder`,
`IndoorDrawPlan`, and `ViewconeCuller` research path was deleted in Campaign
OVERHAUL S5-c5; production has one visibility graph and one walk owner.
**Campaign OVERHAUL S2 (chunks 1-5, closed 2026-09-03) gave render
and physics cross-cell membership one canonical owner:**
`ShadowObjectRegistry` computes retail's exact CELLARRAY
(`calc_cross_cells_static`/`add_shadows_to_cells`, Contract A/B) once per
@ -1123,23 +1125,21 @@ cross-cell membership only. Full detail:
`docs/architecture/worldbuilder-inventory.md`'s "Retail static render-shadow
cell membership" / "Consumer cutover to a borrowed per-cell view" entries.
**Current draw discipline and known non-retail order.** The walk records an
**Current draw discipline and bounded residuals.** The walk records an
ordered event stream for landscape cells, building portals, EnvCell shells,
statics, dynamics, particles, depth boundaries, and alpha barriers, then
`WalkFrameDriver` replays it through the Vulkan leaves. Private portal/
paperdoll viewports and retained UI execute after the world stream, and
projectiles remain ordinary live-entity draws. Three current details are
explicit Campaign OVERHAUL divergences, not accepted retail contracts:
- terrain is staged before all per-cell building/object turns instead of
retail's `DrawLandCell` then `DrawSortCell` interleave for each land cell;
- depth clear/seal events are emitted for zero-outside-view roots and do not
consume retail's persistent previous-call `uint16 portalsDrawnCount` gate;
- ordinary object and particle parts now receive retail's stable per-cell
authored-CYpt order before entering the two FIFO mesh lists, which drain
CLIP before ALPHA at exact barriers. The retained differences are AP-241
(opaque/row-5 GPU channels), AP-242 (equal cross-source tie ordinal), and
AP-243 (retail's >50 m shared-cell-key optimization).
projectiles remain ordinary live-entity draws. Campaign OVERHAUL S3/S4 landed
retail's per-landcell terrain turn followed by that cell's sort-cell turn,
outside-view/latch-gated clear and seal events, and the two FIFO lists (bounded
CLIP and ALPHA) drained at their exact barriers. The remaining explicit deviations
are AP-238 (EnvCell token granularity), AP-239 (collapsed raw surface-mask
origin), AP-240 (ordinary Gfx/building pure-Clip immediate A2C placement),
AP-241 (opaque object and row-5 particle GPU channels), AP-242 (equal-key
cross-source tie ordinal), and AP-243 (retail's >50 m shared-cell-key
optimization). They are bounded residuals, not a second visibility or ordering
model.
The exact target and evidence are recorded in
`docs/research/2026-09-01-overhaul/oh1-retail-world-contract.md`; OH5, OH6, and

File diff suppressed because one or more lines are too long

View file

@ -230,10 +230,9 @@ matching retail's own rule that an object not yet in a cell is not drawn.
Every distinct entity id this happens for in one frame is counted once in
`WalkProductionWorldData.UnregisteredRenderMembershipCount` (renamed from
`UnregisteredStaticRenderFallbackCount`, which under the pre-chunk-5 model
also covered the separate dynamic-record fallback) and reported by one
print-only `[walk-membership]` line — gated on
`RenderingDiagnostics.ProbeFacilityStairsEnabled`, the walk family's
existing diagnostic flag — at the start of the next frame when nonzero.
also covered the separate dynamic-record fallback). The count remains a cheap
per-frame invariant available to tests and diagnostics consumers; Campaign
OVERHAUL S5-c5 deleted its spent print-only Facility probe and next-frame log.
A primitive-only Setup may reuse its already-authored cylsphere collision
cell set; BSP-bearing and pure-visual statics use the all-visual-part box
walk. The Facility Hub stair Setup `0x02000623` is the installed-DAT

View file

@ -249,13 +249,6 @@ $env:ACDREAM_FRAME_HISTORY = "$scratch\frames.csv"
| `ACDREAM_DUMP_PLAYSCRIPT` | `="1"` (ordinal) | Traces PhysicsScript playback: missing/empty script resolution, malformed `StartTime` entries, and other `[pes]`-prefixed hook-dispatch events | print-only (`Console.WriteLine`) at all 4 use sites (`:85-86,136,300,328`) | unset (off) | `PhysicsScriptRunner.DiagEnabled` (`PhysicsScriptRunner.cs:61-62`) — per-instance settable property seeded from the env var, not a shared static diagnostics-owner class |
| `ACDREAM_DUMP_SURFACES` | `="1"` (ordinal) | One-shot (per session) surface-format histogram dump for the atlas-opportunity audit — fires once after `_dumpFrameCounter>=600` OnRender ticks AND `_uploadMetadata.Count>=100` uploaded textures; writes to the host diagnostics directory | Doc comment claims "Zero cost when off" but `_uploadMetadata[name]=(w,h,fmt)` (`TextureCache.cs:1042`) is written **unconditionally on every texture upload regardless of the flag** — real (small) always-on dictionary-write cost. `TickSurfaceHistogramDumpIfEnabled` also re-reads `Environment.GetEnvironmentVariable` every OnRender frame (not cached) until the one-shot fires. Dump-write failures are caught and logged to stderr, not fatal. | unset (off) | `TextureCache` (`TextureCache.cs:102-113` fields, gate at `TextureCache.cs:802-812`, dump at `TextureCache.cs:814-829`), Phase N.6 slice 1 |
| `ACDREAM_FRAME_PROF` | `=1` | master toggle for the frame profiler: CPU frame time, GPU time samples, per-stage CPU attribution, per-frame alloc/GC, `[frame-prof]` report every ~5 s (doc: "permanent apparatus ... do not strip with session probes") | when on, samples `GC.GetAllocatedBytesForCurrentThread()` and stage-scope timing every frame (cheap, by design); its own XML doc claims a GPU-query self-disable tied to `ACDREAM_WB_DIAG=1` that `FrameProfiler.cs` says no longer exists — see Notes #1; startup-only in practice (its DebugPanel/DebugVM mirror is unreachable — #434)| false (off) | `RenderingDiagnostics.FrameProfEnabled` / `FrameProfiler` |
| `ACDREAM_PROBE_ENVCELL` | `=1` | emits one `[envcells]` line per indoor frame: `CellsRendered`/`TrianglesDrawn` + ourBldgs/otherBldgs/filter counts (phase a8 relic; its own render pass was removed but the probe was kept) | print-only; implicitly turned on whenever `ACDREAM_PROBE_VIS` is on (getter is `_probeEnvCellEnabled \ | \ | `RenderingDiagnostics.ProbeEnvCellEnabled` (backing field OR'd with `ProbeVisibilityEnabled`) |
| `ACDREAM_PROBE_INDOOR_ALL` | `=1` | master switch that reads as AND / writes as cascade across Walk, Lookup, Upload, Xform, Cull | print-only (every underlying probe is print-only); startup-only in practice (its DebugPanel/DebugVM mirror is unreachable — #434)| false (off) | `RenderingDiagnostics.IndoorAll` (cascades to the 5 flags below) |
| `ACDREAM_PROBE_INDOOR_CULL` | `=1` (also set by `ACDREAM_PROBE_INDOOR_ALL=1`) | emits `[indoor-cull]` per culled cell entity with cull reason (visibleCellIds-miss / frustum / landblock) | print-only; startup-only (its DebugPanel/DebugVM mirror is unreachable — #434) | false (off) | `RenderingDiagnostics.ProbeIndoorCullEnabled` |
| `ACDREAM_PROBE_INDOOR_LOOKUP` | `=1` (also via `ACDREAM_PROBE_INDOOR_ALL`) | emits `[indoor-lookup]` per visible cell entity/sec: render-data hit/miss, IsSetup, parts-hit/parts-miss tallies | print-only; startup-only (its DebugPanel/DebugVM mirror is unreachable — #434) | false (off) | `RenderingDiagnostics.ProbeIndoorLookupEnabled` |
| `ACDREAM_PROBE_INDOOR_UPLOAD` | `=1` (also via `ACDREAM_PROBE_INDOOR_ALL`) | emits `[indoor-upload]` requested/completed lines per EnvCell id at `WbMeshAdapter`'s staged-drain time | print-only; startup-only (its DebugPanel/DebugVM mirror is unreachable — #434) | false (off) | `RenderingDiagnostics.ProbeIndoorUploadEnabled` |
| `ACDREAM_PROBE_INDOOR_WALK` | `=1` (also via `ACDREAM_PROBE_INDOOR_ALL`) | emits `[indoor-walk]` per visible cell entity/sec: world position, parent cell, landblock/AABB-visible flags, "drew" flag | print-only; startup-only (its DebugPanel/DebugVM mirror is unreachable — #434) | false (off) | `RenderingDiagnostics.ProbeIndoorWalkEnabled` |
| `ACDREAM_PROBE_INDOOR_XFORM` | `=1` (also via `ACDREAM_PROBE_INDOOR_ALL`) | emits `[indoor-xform]` per visible cell entity/sec: cell-geometry SetupPart's composed world-matrix translation | print-only; startup-only (its DebugPanel/DebugVM mirror is unreachable — #434) | false (off) | `RenderingDiagnostics.ProbeIndoorXformEnabled` |
| `ACDREAM_PROBE_LOGIN_FRAMES` | `="1"` | Per-completed-frame login/portal-wormhole presentation classification (`world`/`tunnel`/`black`/`void`); logs `[login-frames]` on each classification transition | print-only. "Not a user setting; not in RuntimeOptions; not persisted" (doc comment). | unset (off) | `RenderPresentationDiagnostics.ProbeLoginFrames` (`LoginPresentationFrameProbe.cs:28-29`), consumed by `LoginPresentationFrameProbe.Process` |
| `ACDREAM_PROBE_NET` | `="1"` | Emits `[net-out]` (per outbound reliable message), `[net-tick]` (1 Hz WorldSession.Tick summary incl. reliable-transport rates), `[net-final]` (cumulative stats at Dispose), and `[cmd-gate]` (generation-gated command rejections) | print-only. Doc comment: "the counters themselves increment unconditionally in `TransportStats`; only the string work is gated" — i.e. the underlying stats tracking has a small always-on cost independent of this flag, but this flag itself gates only string/console formatting. | unset (off) | `NetDiagnostics.ProbeNet` (`NetDiagnostics.cs:56-57`), issue #260 probe family |
| `ACDREAM_PROBE_RESOLVE` | `=1` | gates one structured `[resolve]` line per `PhysicsEngine.ResolveWithTransition` call (in/target/out position+cell, ok-vs-partial, grounded/contact status, wall normal, walkable-polygon validity, responsible entity) (l.2a slice 1, general-purpose resolver probe) | print-only, ~30 Hz per moving entity while on | off | `PhysicsDiagnostics.ProbeResolveEnabled` |
@ -264,7 +257,6 @@ $env:ACDREAM_FRAME_HISTORY = "$scratch\frames.csv"
| `ACDREAM_PROBE_TUNNEL_FREEZE` | `=1` or `=N` | #419 RenderDoc apparatus: holds the teleport state in stable `Tunnel` after destination readiness and freezes the portal-space animation/roll at frame 72 (`=1`) or an explicit frame 2120 (`=N`); emits one `[tunnel-freeze]` line with the actual frame and retail Setup/animation ids | **behavior-changing diagnostic:** placement, world viewport reveal, and LoginComplete are intentionally withheld until transition cancellation/process exit. For static visual inspection only; never use in a performance or lifecycle measurement. | unset (off) | `StreamingDiagnostics.TunnelFreezeFrame`; consumed by `LocalPlayerTeleportPresentation` and `PortalTunnelPresentation` |
| `ACDREAM_PROBE_SOUND_WIRE` | `="1"` | One line per inbound server Sound event (`0xF750`) and per wire-sound play decision, with the drop reason when nothing plays — used to determine whether missing interior soundscapes are server- or client-side | print-only, consumed at `AudioHookSink.cs:159` and `EntityEffectController.cs:123` | unset (off) | `AudioDiagnostics.ProbeWireSoundsEnabled` (`AudioDiagnostics.cs:20-21`) |
| `ACDREAM_PROBE_USEABILITY_FALLBACK` | `=1` | gates a per-call log of `IsUseableTarget` calls that take the null-useability fallback path (creature/door/lifestone passes) (measures a real ace-vs-retail data gap, not a bug investigation) | print-only; measures how often ACE ships entities without `_useability` set | off | `PhysicsDiagnostics.ProbeUseabilityFallbackEnabled` |
| `ACDREAM_PROBE_VIS` | `=1` | emits `[vis]` line on root-cell CHANGE: visible cell ids, OutsideView poly/plane counts, per-cell plane counts, scissor-fallback count (phase u.2d repurposed the flag; its DebugPanel mirror is unreachable — #434) | print-only; ALSO implicitly enables the separate `ACDREAM_PROBE_ENVCELL` probe (its getter ORs with this flag — see Notes #3); startup-only in practice (its DebugPanel/DebugVM mirror is unreachable — #434)| false (off) | `RenderingDiagnostics.ProbeVisibilityEnabled` |
| `ACDREAM_REMOTE_VEL_DIAG` | `=1` | prints per-UM/per-tick remote-velocity and animation-cycle diagnostic lines; `Runtime/Physics/RemoteMotion.cs` carries diagnostic-only fields (`PrevServerPos`, `PrevServerPosTime`, `MaxRootMotionSpeedSinceLastUP`, `LastOmegaDiagLogTime`) unconditionally on every remote — small fixed per-instance memory regardless of the flag, not gated (long-lived remote-velocity/animation diagnostic, commit a.1) | print-only, but the raw-site reads in `LiveEntityNetworkUpdateController.cs` fire on every UM/tick even when off (rule-5 violation, `Environment.GetEnvironmentVariable` call per event, 6+ call sites) | off | THREE readers: `AnimationPresentationDiagnostics.FromEnvironment()` (App owner record, cached at startup, consumed by `LiveEntityAnimationPresenter` for `[SEQSTATE]`/`[CURRNODE]`/other part-diagnostic lines, throttled to 1/sec/entity) + raw `Environment.GetEnvironmentVariable` reads scattered across `LiveEntityNetworkUpdateController.cs` (6+ sites: `[UM_RAW]`, `[FWD_WIRE]`, `[VEL_DIAG]`, `[UPCYCLE_SRC]`, `[UM_STALE]`) + `RemoteServerControlledVelocityCycle.cs:68` (`[UPCYCLE]`) |
| `ACDREAM_DUMP_CELLS` | `=<comma list of hex cell ids>` | one-shot JSON dump of any cached EnvCell whose id matches the list, to `ProbeDumpCellsPath` (issue #98 fixture capture) — Standing fixture-extraction tooling (A6.P3/#98 lineage) for the physics replay harness; roundtrip-tested. Not investigation-scoped. | file I/O once per matching cell id (no-op on repeat); fixture-generation tool, not a perf-neutral no-op when ids are listed | off/unset | `PhysicsDiagnostics.ProbeDumpCellIds` (`ParseHexIdList`) |
| `ACDREAM_DUMP_CELLS_DIR` | `=<dir>` | overrides the output directory for `ACDREAM_DUMP_CELLS` — Companion output-directory knob for ACDREAM_DUMP_CELLS. | print/file-path only; no effect unless `ACDREAM_DUMP_CELLS` is also set | off/unset | `PhysicsDiagnostics.ProbeDumpCellsPath` |
@ -313,25 +305,17 @@ issue is closed, the strip was missed; delete both.
| `ACDREAM_PROBE_CELLSET` | a6.p5 | `=1` | gates `PhysicsDiagnostics.LogCellSetBuild`, one `[cellset-build]` line per `BuildCellSetAndPickContaining` call (seed cell, sphere XY, candidate list) from `CellTransit.cs:1468` | print-only; builds a `StringBuilder` of the candidate id list only when the flag is on | `PhysicsDiagnostics.ProbeCellSetEnabled` |
| `ACDREAM_PROBE_CELL_CACHE` | indoor walking phase d | `=1` | gates one `[cell-cache]` line per EnvCell first-cached in `PhysicsDataCache.CacheCellStruct` (poly counts, BSP root structure) | print-only; fires at most once per EnvCell (cache is no-op after first population); no DebugPanel mirror | `PhysicsDiagnostics.ProbeCellCacheEnabled` |
| `ACDREAM_PROBE_CHILD_CELL` | c4 route 7 | `=1` | gates one `[child-cell]` line per Runtime committed-child canonical-cell write in `RuntimeLiveEntitySessionController`, `RuntimeEntityObjectLifetime`, `RuntimeEntityDirectory` (parent/child guid, old/new cell, cause tag) | print-only | `PhysicsDiagnostics.ProbeChildCellEnabled` |
| `ACDREAM_PROBE_CLIPROUTE` | "throwaway apparatus — strip once §4 ships" | `=1` | print-on-change `[clip-route]` / `[clip-route-scis]` lines: outside-slice clip routing + region-SSBO bytes, actual GL/RHI scissor state (S3 review fix round 1, F5: the third producer, `[clip-route-disp]` — WbDrawDispatcher's per-entity clip-slot routing histogram — is deleted with the routing state it reported on) | print-only | `RenderingDiagnostics.ProbeClipRouteEnabled` |
| `ACDREAM_PROBE_CONTACT_PLANE` | spike-only, 2026-05-20 | `=1` | gates one `[cp-write]` line per write to `CollisionInfo.ContactPlane*`/`LastKnownContactPlane*` fields (field, old→new, caller method via stack walk, source line); only logs on actual value changes | print-only, but performs a stack walk to identify the caller method when firing — real CPU cost per write while on (not just a string format); suppresses no-op writes to bound volume | `PhysicsDiagnostics.ProbeContactPlaneEnabled` |
| `ACDREAM_PROBE_ENT` | #138 | `="1"` | Traces the persistent player entity across teleport streaming churn: presence in the render draw-set flat view vs. survival of the dynamics cull, to distinguish "missing from draw set" vs "present but culled" | print-only, "Observation-only — emits no behavior change" (doc comment). `LogPlayerDynOnChange` dedupes by transition to avoid per-frame spam. Marked STRIP-once-root-caused (like the dense-town FPS apparatus). | `EntityVanishProbe.Enabled` (`EntityVanishProbe.cs:23-24`), issue #138-B |
| `ACDREAM_PROBE_FLAP` | "throwaway apparatus — strip once the flap mechanism is confirmed" | `=1` | EVERY FRAME (unthrottled, not change-gated) while the camera root is indoor: `[flap]` from `PortalVisibilityBuilder.Build` (portal side-test/traverse/cull/projection) + paired `[flap-cam]` from `PhysicsCameraCollisionProbe`/`[flap-sweep]` (FindCameraCell resolution, eye positions) | print-only, but unthrottled per-frame `StringBuilder` allocation + `Console.WriteLine` on multiple call sites while indoor — heavy log volume/allocation under sustained indoor play; does not alter rendered output | `RenderingDiagnostics.ProbeFlapEnabled` |
| `ACDREAM_PROBE_GLSTATE` | "throwaway apparatus — strip once §4 ships" | `=1` | print-on-change `[gl-state]` line: depth/blend/cull/scissor/viewport/draw-FBO/color-mask/`glGetError` snapshot | print-only per its docstring; the actual state-snapshot/comparison call site lives outside `RenderingDiagnostics.cs` and was outside this pass's cited read sites | `RenderingDiagnostics.ProbeGlStateEnabled` |
| `ACDREAM_PROBE_INDOOR_BSP` | indoor walking phase 1 / cellar-lip wedge | `=1` | gates `[indoor-bsp]` (per `BSPQuery.FindCollisions` indoor call), `[neg-poly]` (near-miss polygon detail in `BSPQuery`), and `[stepdown-decide]` (step-down accept/reject inputs in `TransitionTypes`) trace lines | print-only; also un-gates the `LastBspHitPoly` diagnostic side-channel write (same as `ACDREAM_PROBE_BUILDING`) | `PhysicsDiagnostics.ProbeIndoorBspEnabled` |
| `ACDREAM_PROBE_INDOOR_LIGHT` | #176/#177 discriminator, a7.l1 | `=1` | rate-limited (1 Hz) `[indoor-light]` line from `LightManager.BuildPointLightSnapshot`: point-light pool set composition (pool/cellLess/registered/capped/byCell histogram) | print-only, explicitly "inert unless set" per the call-site comment (LightManager.cs:368-370); no DebugPanel mirror | `RenderingDiagnostics.ProbeIndoorLightEnabled` |
| `ACDREAM_PROBE_JUMP` | campaign ch round 2 | `=1` | gates the `[jump]` line in `PlayerMovementController.ReportJumpRefusal`, printed UNCONDITIONALLY (even when `OnInterfaceText` is null) to distinguish "branch never fired" from "branch fired, callback dropped it" | print-only; `Headless/Policies/HeadlessBotPolicy.cs`'s `JumpProbeHeadlessBotPolicy` doc comment references this flag as a companion but does not itself read it — it is a headless bot behavior meant to be run alongside `ACDREAM_PROBE_JUMP=1`, not a second consumer | `PhysicsDiagnostics.ProbeJumpEnabled` |
| `ACDREAM_PROBE_LOCAL_TELEPORT` | c4 route 3 d-t8 | `=1` | gates one `[local-tp]` line per local-player portal-arrival attempt (committed AND refused) from `LocalPlayerTeleportController` and `RuntimeAcceptedPositionDriveController.LogPortalArrivalAttempt` — the single Runtime chokepoint both graphical and headless hosts share | print-only; dual-host parity evidence (same line shape from both hosts) | `PhysicsDiagnostics.ProbeLocalTeleportEnabled` |
| `ACDREAM_PROBE_PARK` | issue #309 | `=1` | gates `[park]`/`[park-restore]` lines when a `RuntimeSetPositionState` placement parks or a cancelled park's withdrawal is rolled back | print-only, low volume (parks are rare); in a MULTI-session headless host, `HeadlessStaticStateAudit.ValidateProcessIsolation` THROWS `HeadlessConfigurationException` at startup if this (or any other process-global `Probe*`/`Dump*` boolean, `CollisionShadowSampleEvery`, or `PhysicsResolveCapture`) is enabled — refusal is waived only when `sessionCount==1` (logs loudly and proceeds instead) | `PhysicsDiagnostics.ProbeParkEnabled` |
| `ACDREAM_PROBE_PLACEMENT_FAIL` | issue #98 | `=1` | gates one `[place-fail]` line per Path-1 (Placement/Ethereal) `Collided` return in `BSPQuery.FindCollisions`, plus one per `Transition.DoStepDown` placement-insert rejection | print-only; low volume (fires only on actual rejection) | `PhysicsDiagnostics.ProbePlacementFailEnabled` |
| `ACDREAM_PROBE_POLY_DUMP` | a6.p3 slice 4, issue #98 | `=1` | gates one `[poly-dump]` line (full polygon geometry: cell, poly index, sides, plane, all vertices) per `AdjustSphereToPlane` push-back call | print-only; HEAVY output (one full-geometry dump per push-back call) — doc explicitly says "use briefly, then turn off" | `PhysicsDiagnostics.ProbePolyDumpEnabled` |
| `ACDREAM_PROBE_PORTAL_CHURN` | "throwaway apparatus — strip once the bound ships" | `=1` | one `[portal-churn]` summary per `PortalVisibilityBuilder.Build` call: per-cell pop/re-pop counts, re-enqueue totals, reciprocal-clip pre→post region growth | print-only | `RenderingDiagnostics.ProbePortalChurnEnabled` |
| `ACDREAM_PROBE_PUSH_BACK` | phase a6.p1 | `=1` | gates `[push-back]` (`BSPQuery.AdjustSphereToPlane`), `[push-back-disp]` (`BSPQuery.FindCollisions` 6-path dispatcher), `[push-back-cell]` (`Transition.CheckOtherCells` multi-cell BSP) lines | print-only; the `DebugVM.cs:380` "runtime mirror" is dead code — `DebugVM`/`DebugPanel` (`AcDream.UI.Abstractions/Panels/Debug/`) are never instantiated anywhere in `src/` (the ImGui frontend they required was removed at Campaign V slice V11); only the startup env var takes effect | `PhysicsDiagnostics.ProbePushBackEnabled` |
| `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_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` |
| `ACDREAM_PROBE_SWEPT` | phase w stage 0 | `=1` | gates one `[cell-swept]` line per `ResolveWithTransition` call comparing the transition's swept cell vs the legacy static `ResolveCellId` path | print-only | `PhysicsDiagnostics.ProbeSweptEnabled` |
| `ACDREAM_PROBE_TELEPORT` | 2026-06-22, "removable diagnostic" | `=1` | gates `[tp-probe]` lines (`LogTeleport`) at AIM/ENQ/BUILD/APPLY/PLACED teleport-pipeline events across `LocalPlayerTeleportController` and `RuntimeAcceptedPositionDriveController`, with cross-thread monotonic timestamps | print-only | `PhysicsDiagnostics.ProbeTeleportEnabled` |
@ -358,7 +342,7 @@ from the "must still exist" check.
| `ACDREAM_JUMP_SKILL` | As above. The fallback is 300, not the 200 that CLAUDE.md advertised. | none — server-authoritative |
| `ACDREAM_RENDER_BACKEND` | Selected the GL-vs-Vulkan backend. Campaign V deleted the OpenGL backend; Vulkan is the only one. Two comments still named it as a live co-requisite until 2026-08-24. | none |
| `ACDREAM_ANIM_SPEED_SCALE` | Animation-speed multiplier from the pre-retail-sequencer era; died with the 1.248x factor. | none |
| `ACDREAM_A8_AUDIT` | Phase A8 EnvCell batch/cull audit dump. Its only caller never existed; `EnvCellRenderer.CollectCellAuditLines` was unreachable and was deleted 2026-08-24. | `ACDREAM_PROBE_ENVCELL` |
| `ACDREAM_A8_AUDIT` | Phase A8 EnvCell batch/cull audit dump. Its only caller never existed; `EnvCellRenderer.CollectCellAuditLines` was unreachable and was deleted 2026-08-24. | none |
| `ACDREAM_AIRBORNE_DIAG` | #42 airborne-sweep `[SWEEP]`/`[SWEEP-OBJ]` XY-drift trace. Investigation closed; stripped 2026-08-24 (#435) along with its 16 siblings below. | none |
| `ACDREAM_DUMP_ENTITY` | #119 tower-staircase HYDRATE/DRAW/WALK-REJECT entity watchlist. Investigation closed; stripped 2026-08-24 (#435). | none |
| `ACDREAM_PROBE_AUTOWALK` | Issue #63 server-initiated auto-walk trace. Investigation closed; stripped 2026-08-24 (#435). | none |

View file

@ -358,15 +358,8 @@ internal sealed class FrameRootCompositionPhase
{
// Campaign V slice V6j: the world scene is composed on BOTH arms.
// Every renderer below now exists on Vulkan too; what forks is one
// pass surface, one state restorer, one GL-state reader, and the
// three renderers that stay raw GL until their own slices — sky,
// particles and the portal depth mask, which the executors already
// accept as absent.
WorldRenderDiagnostics worldRenderDiagnostics =
host.WorldRenderDiagnostics
?? new WorldRenderDiagnostics(
NullRenderGlStateReader.Instance,
d.RenderDiagnosticLog);
new(d.RenderDiagnosticLog);
IRenderFrameGlState worldFrameGlState = NullRenderFrameGlState.Instance;
IWorldPassScope? worldPassScope = d.Graphics.WorldPassScope;
IWorldPassSurface worldPassSurface = new RhiWorldPassSurface(
@ -445,10 +438,8 @@ internal sealed class FrameRootCompositionPhase
live.ParticleRenderer,
live.PortalDepthMask,
d.RetailAlphaQueue,
worldRenderDiagnostics,
terrainDrawDiagnostics);
var worldSceneDiagnostics = new WorldSceneDiagnosticsController(
worldRenderDiagnostics,
new RuntimeWorldScenePViewDiagnosticSource(
d.PlayerController,
d.PhysicsEngine,

View file

@ -33,7 +33,6 @@ internal sealed record HostInputCameraResult(
IRenderFrameSlotSource FrameSlots,
IGpuDevice GpuDevice,
GpuDeviceFrameLifetime GpuFrameLifetime,
WorldRenderDiagnostics? WorldRenderDiagnostics,
SilkKeyboardSource? KeyboardSource,
SilkMouseSource? MouseSource,
IMouseLookCursor? MouseLookCursor,
@ -86,11 +85,6 @@ internal interface IHostInputCameraCompositionFactory
GpuFrameFlightController? frameFlights,
IGpuDevice device);
/// <summary>The raw-GL state tripwire, or null on a backend that has no GL state.</summary>
WorldRenderDiagnostics? CreateWorldRenderDiagnostics(
GameWindowGraphics graphics,
IRenderFrameDiagnosticLog log);
SilkKeyboardSource CreateKeyboardSource(
IKeyboard keyboard,
HostQuiescenceGate quiescence);
@ -250,11 +244,6 @@ internal sealed class HostInputCameraCompositionPhase :
var gpuFrameLifetime = new GpuDeviceFrameLifetime(gpuDevice);
_publication.PublishGpuFrameLifetime(gpuFrameLifetime);
WorldRenderDiagnostics? diagnostics =
_factory.CreateWorldRenderDiagnostics(
graphics,
_dependencies.RenderDiagnosticLog);
IKeyboard? firstKeyboard = input.Keyboards.FirstOrDefault();
IMouse? firstMouse = input.Mice.FirstOrDefault();
SilkKeyboardSource? keyboard = null;
@ -356,7 +345,6 @@ internal sealed class HostInputCameraCompositionPhase :
frameSlots,
gpuDevice,
gpuFrameLifetime,
diagnostics,
keyboard,
mouse,
cursor,

View file

@ -16,10 +16,7 @@ namespace AcDream.App.Composition;
///
/// <para><b>What is absent, and why.</b> There is no GL fence ring: the RHI
/// device owns its own frames-in-flight through a timeline semaphore, so
/// retirement and slot indexing come from the device instead. There is no
/// <see cref="WorldRenderDiagnostics"/>: it was a raw-GL state tripwire, and
/// Vulkan has no global state for it to watch. Both nulls are read by the
/// phases that would otherwise consume them.</para>
/// retirement and slot indexing come from the device instead.</para>
/// </summary>
internal sealed class VulkanHostInputCameraCompositionFactory
: IHostInputCameraCompositionFactory
@ -57,10 +54,6 @@ internal sealed class VulkanHostInputCameraCompositionFactory
IGpuDevice device) =>
new VulkanRenderFrameSlotSource(RequireContext(graphics).Device);
public WorldRenderDiagnostics? CreateWorldRenderDiagnostics(
GameWindowGraphics graphics,
IRenderFrameDiagnosticLog log) => null;
public SilkKeyboardSource CreateKeyboardSource(
IKeyboard keyboard,
HostQuiescenceGate quiescence) =>

View file

@ -89,8 +89,7 @@ public sealed class LoadedCell
/// CEnvCell.stab_list (acclient.h ~30925), the stable set of cells potentially
/// visible from this cell, precomputed by the AC content tools. Refreshed only at
/// hydration (= retail's per-cell-entry grab_visible_cells, decomp:311878).
/// PortalVisibilityBuilder grounds set membership in it so a brittle per-frame
/// portal-side test can't drop a potentially-visible cell from the visible set.
/// The frame walk uses it to ground authored potentially-visible membership.
/// Empty when the dat carried no stab list (degenerate / old cell).
/// </summary>
public IReadOnlyList<uint> VisibleCells = System.Array.Empty<uint>();
@ -105,9 +104,8 @@ public sealed class LoadedCell
/// <summary>
/// Render unification (2026-06-07): true for the synthetic OUTDOOR cell node built by
/// <see cref="OutdoorCellNode.Build"/> — the outdoor world modelled as a flood-graph cell whose
/// shell is the landscape. <see cref="PortalVisibilityBuilder.Build"/> seeds OutsideView
/// full-screen when the root carries this flag (so terrain/sky/scenery draw as the node's shell).
/// <see cref="OutdoorCellNode.Build"/> — the outdoor world modelled as a walk root whose
/// shell is the landscape. The frame assembler seeds its outside view full-screen.
/// An explicit flag, not a cell-id heuristic: interior EnvCell ids are >= 0x100 in production but
/// test fixtures use low ids for interior cells, so keying on the id would misfire.
/// </summary>
@ -168,10 +166,8 @@ public struct PortalClipPlane
}
/// <summary>
/// Phase U.4c flap probe (diagnostic — OBSOLETE as of Stage 3). Previously tracked
/// which branch of FindCameraCell (now deleted) resolved the camera cell. Retained
/// for binary compatibility with the [flap-cam] probe log site in GameWindow.cs that
/// still prints <see cref="LastCameraCellResolution"/> (always None post-Stage 3).
/// Historical result describing which branch of the former camera-cell resolver ran.
/// The retained physics-membership route always reports <see cref="None"/>.
/// </summary>
public enum CameraCellResolution
{
@ -219,7 +215,7 @@ public sealed class CellVisibility
/// <summary>
/// Stage 3 (2026-06-02): always <see cref="CameraCellResolution.None"/> — the FindCameraCell
/// AABB grace-frame resolver was deleted; the physics membership answer is the sole root.
/// Retained for the [flap-cam] probe log line in GameWindow.cs.
/// Retained as a stable diagnostic result for existing frame records.
/// </summary>
public CameraCellResolution LastCameraCellResolution { get; private set; } = CameraCellResolution.None;

View file

@ -1,9 +1,8 @@
// ClipFrameAssembler.cs
//
// Retail view assembly policy. The production frame walk supplies its
// outside_view directly; the legacy PortalVisibilityBuilder overload remains
// only for isolated research/replay tests. Each visible polygon is packed as
// an individual GPU clip slot.
// outside_view directly. Each visible polygon is packed as an individual GPU
// clip slot.
//
// outside_view landscape slices
// reverse cell_draw_list exit masks
@ -19,34 +18,10 @@ using System.Numerics;
namespace AcDream.App.Rendering;
/// <summary>
/// S3 chunk 4 fix round 2 (L2): retired from the walk's own clip assembly —
/// <see cref="ClipFrameAssembly"/> no longer tracks a terrain clip mode at
/// all, because the walk draws the sky, terrain and weather unclipped, one
/// call each, matching retail (<c>LScape::draw</c> never installs a view
/// before any of the three). The type stays only because
/// <see cref="WorldSceneRenderer"/>'s separate flat-world safety path still
/// uses it for its own unrelated "did the flat terrain draw this frame"
/// diagnostic flag (<c>Planes</c> = drew, <c>Skip</c> = the PView walk ran
/// instead). S3 landing hygiene (H4) deleted the <c>Scissor</c> member: no
/// producer ever wrote it (<see cref="WorldSceneRenderer"/> only ever
/// assigns <c>Planes</c> or <c>Skip</c>), because there is no GPU scissor
/// consumer left anywhere in this contract, walk or flat.
/// </summary>
public enum TerrainClipMode
{
/// <summary>The flat-world path drew terrain this frame.</summary>
Planes,
/// <summary>The PView walk ran instead of the flat-world path this frame;
/// flat terrain was not drawn.</summary>
Skip,
}
/// <summary>
/// One retail portal_view slice mapped to a GPU clip slot. The AABB is
/// retained for diagnostics (<see cref="WorldRenderDiagnostics"/>) only — no
/// GPU pass reads it; there is no scissor consumer left in the walk.
/// retained as part of the exact outside-view reconstruction proof — no GPU
/// pass reads it; there is no scissor consumer left in the walk.
///
/// <para>S3 review fix round 1 (F1): <paramref name="NothingVisible"/>
/// distinguishes the "this walk view's own polygon collapsed" case (retail
@ -95,7 +70,7 @@ public sealed class ClipFrameAssembly
public Dictionary<uint, int> PerCellPlaneCounts { get; } = new();
public int ScissorFallbacks { get; internal set; }
// The assembly is frame-scoped. An owner that passes it back to Assemble may reuse the
// The assembly is frame-scoped. An owner may pass it back to reuse the
// dictionaries, per-cell arrays, and construction list after the prior frame is consumed.
// Exact-length pools keep the public array API honest: Length is always the live slice count.
private readonly Dictionary<int, Stack<ClipViewSlice[]>> _sliceArraysByLength = new();
@ -279,8 +254,7 @@ public sealed class ClipFrameAssembly
public static class ClipFrameAssembler
{
/// <summary>
/// Starts one production walk assembly without constructing a parallel
/// PortalVisibilityFrame. Outdoor roots begin with retail's full-screen
/// Starts the one production walk assembly. Outdoor roots begin with retail's full-screen
/// default view; interior roots begin empty and are populated from
/// <see cref="Walk.RetailFrameWalk.InteriorOutsideView"/> after Collect.
/// </summary>
@ -321,109 +295,6 @@ public static class ClipFrameAssembler
return assembly;
}
public static ClipFrameAssembly Assemble(
ClipFrame frame,
PortalVisibilityFrame pvFrame,
ClipFrameAssembly? reuseAssembly = null)
{
System.ArgumentNullException.ThrowIfNull(frame);
System.ArgumentNullException.ThrowIfNull(pvFrame);
frame.Reset();
ClipFrameAssembly assembly = reuseAssembly ?? new ClipFrameAssembly();
assembly.Reset(frame);
Dictionary<uint, int> cellIdToSlot = assembly.CellIdToSlot;
Dictionary<uint, int[]> cellIdToViewSlots = assembly.CellIdToViewSlots;
Dictionary<uint, ClipViewSlice[]> cellIdToViewSlices = assembly.CellIdToViewSlices;
Dictionary<uint, int> perCellPlaneCounts = assembly.PerCellPlaneCounts;
int scissorFallbacks = 0;
foreach (uint cellId in pvFrame.OrderedVisibleCells)
{
if (!pvFrame.CellViews.TryGetValue(cellId, out var view))
continue;
List<ClipViewSlice> slices = assembly.SliceScratch;
slices.Clear();
int maxPlaneCount = 0;
foreach (var poly in view.Polygons)
{
var cps = ClipPlaneSet.From(poly);
if (cps.IsNothingVisible)
continue;
int slot;
Vector4[] planes;
if (cps.Count > 0)
{
planes = cps.PlaneArray;
slot = frame.AppendSlot(planes);
if (cps.Count > maxPlaneCount)
maxPlaneCount = cps.Count;
}
else
{
planes = System.Array.Empty<Vector4>();
slot = 0;
scissorFallbacks++;
}
slices.Add(new ClipViewSlice(slot, AabbOf(poly), planes));
}
if (slices.Count == 0)
continue;
ClipViewSlice[] sliceArray = assembly.CopySlices(slices);
cellIdToViewSlices[cellId] = sliceArray;
cellIdToViewSlots[cellId] = assembly.CopySlots(sliceArray);
cellIdToSlot[cellId] = sliceArray[0].Slot;
perCellPlaneCounts[cellId] = maxPlaneCount;
}
List<ClipViewSlice> outsideSlicesList = assembly.SliceScratch;
outsideSlicesList.Clear();
int outsideMaxPlaneCount = 0;
bool outsideHasScissorFallback = false;
foreach (var poly in pvFrame.OutsideView.Polygons)
{
AppendOutsideSlice(
frame,
poly,
outsideSlicesList,
ref outsideMaxPlaneCount,
ref outsideHasScissorFallback,
ref scissorFallbacks);
}
ClipViewSlice[] outsideViewSlices = assembly.CopySlices(outsideSlicesList);
bool outdoorVisible = outsideViewSlices.Length > 0;
int outdoorSlot = outdoorVisible ? outsideViewSlices[0].Slot : 0;
Vector4 outsideViewNdcAabb = outdoorVisible
? new Vector4(pvFrame.OutsideView.MinX, pvFrame.OutsideView.MinY,
pvFrame.OutsideView.MaxX, pvFrame.OutsideView.MaxY)
: Vector4.Zero;
assembly.SetOutsideViewSlices(outsideViewSlices);
assembly.OutdoorSlot = outdoorSlot;
assembly.OutdoorVisible = outdoorVisible;
assembly.HasOutsideView = outdoorVisible;
assembly.OutsideViewNdcAabb = outsideViewNdcAabb;
// S3 chunk 4 fix round 2 (L2): equivalent to the deleted
// terrainMode == TerrainClipMode.Planes gate without needing
// TerrainClipMode at all — a scissor-fallback slice can only exist
// when outdoorVisible is already true (both branches above append a
// slice), so this reduces to the same three cases (no slices -> 0;
// any scissor fallback -> 0; all-planes -> outsideMaxPlaneCount).
assembly.OutsidePlaneCount = outsideHasScissorFallback ? 0 : outsideMaxPlaneCount;
assembly.ScissorFallbacks = scissorFallbacks;
return assembly;
}
/// <summary>
/// Campaign FW4 slice 1 — the interior root's outside-view cutover.
/// Replaces the assembly's outside-view block (slices, terrain mode,
@ -433,13 +304,8 @@ public static class ClipFrameAssembler
/// exactly ONE visibility structure per frame: <c>LScape::draw</c>'s
/// terrain clip, the punch fans' <c>building_view</c> planes, and the
/// landscape turn's view count all read the views the walk itself
/// installed. Feeding these from the old <c>PortalVisibilityBuilder</c>
/// assembly let the two systems desynchronize on camera-transition
/// boundary frames — terrain splashed through stale/fat old-apparatus
/// exit views over interior pixels the walk's flood never repainted
/// (the FW3 visual-gate stairwell/grass flash, probe-pinned
/// 2026-08-30), and punch fans indexed the old slice array with walk
/// view indices.
/// installed. This keeps terrain, punch fans, and landscape turns on
/// the same frame-owned walk views during camera transitions.
///
/// The walk stores view vertices as PIXEL screen points
/// (<c>copy_view</c>'s post-divide viewport coordinates, origin
@ -463,8 +329,7 @@ public static class ClipFrameAssembler
/// <see cref="ClipFrameAssembly.OutsideViewSlices"/> by that index. The
/// shared <see cref="AppendOutsideSlice"/> helper SKIPS a view whose
/// polygon collapses (<see cref="ClipPlaneSet.IsNothingVisible"/>) —
/// correct for <see cref="Assemble"/>'s "empty regions are omitted
/// entirely" legacy policy, but wrong here: skipping would make the
/// unsuitable here because skipping would make the
/// slice array SHORTER than the view list, so every later view's fan
/// would read the WRONG view's planes (off by however many views
/// collapsed before it). A collapsed view instead gets its own slice
@ -564,22 +429,14 @@ public static class ClipFrameAssembler
assembly.OutdoorVisible = outdoorVisible;
assembly.HasOutsideView = outdoorVisible;
assembly.OutsideViewNdcAabb = outsideViewNdcAabb;
// S3 chunk 4 fix round 2 (L2): see Assemble's matching comment — the
// same reduction applies here.
// Any scissor fallback makes the aggregate plane count zero.
assembly.OutsidePlaneCount = outsideHasScissorFallback ? 0 : outsideMaxPlaneCount;
assembly.ScissorFallbacks = scissorFallbacks;
}
/// <summary>
/// S3 chunk 4 fix round 2 (L4): the ONE place a single outside_view
/// <see cref="ViewPolygon"/> becomes an appended clip slot plus its
/// <see cref="ClipViewSlice"/> — both <see cref="Assemble"/>'s
/// outside_view loop and <see cref="ReassembleOutsideViewFromWalk"/>'s
/// outside_view loop call this instead of each carrying its own copy of
/// the plane/scissor-fallback bookkeeping (a prior round's three-lens
/// review found the duplication let a CPU/GPU equivalence pin exercise
/// one copy while production ran the other — see <c>ClipFrameLayoutTests</c>'
/// punch-fan pin). A polygon entirely outside every plane
/// Converts one outside_view <see cref="ViewPolygon"/> into an appended
/// clip slot and <see cref="ClipViewSlice"/>. A polygon entirely outside every plane
/// (<see cref="ClipPlaneSet.IsNothingVisible"/>) appends nothing and
/// returns false; the caller's own loop simply moves to the next
/// polygon either way, so the return value only matters to a caller that

View file

@ -5,7 +5,7 @@
// gl_ClipDistance, or a signal that the region OVER-includes (draws
// unclipped) because it cannot be expressed as one convex plane set.
//
// This is the bridge between PortalVisibilityBuilder's 2D NDC view polygons and
// This is the bridge between the frame walk's 2D NDC view polygons and
// the per-vertex clip the mesh/terrain shaders will perform (Phase U.2c → U.2e).
// Pure System.Numerics math; NO GL. The shader consumes each plane as
// d = nx*clip.x + ny*clip.y + 0*clip.z + dw*clip.w (>= 0 ⇒ keep)
@ -186,8 +186,7 @@ public readonly struct ClipPlaneSet
new(Array.Empty<Vector4>(), isPlaneOverflow: true, isNothingVisible: false);
/// <summary>
/// Return the polygon wound CCW with collinear vertices removed. The PortalVisibilityBuilder
/// already EnsureCcw's its output, but From() is a public entry point that must be robust to
/// Return the polygon wound CCW with collinear vertices removed. From() must be robust to
/// either winding (e.g. a hand-built CellView), so we normalize here too.
/// </summary>
private static int NormalizeAndMerge(ReadOnlySpan<Vector2> input, Span<Vector2> points)

View file

@ -1,30 +0,0 @@
// IndoorDrawPlan.cs
//
// Pure (GL-free) port of the membership half of retail PView::DrawCells (0x5a4840):
// the reverse cell_draw_list iterated per portal_view slice. EVERY visible cell with a
// non-empty view is included — there is NO "drawable" filter. Dropping cells without a
// clip-slot was the grey-walls bug (the cell's sealed shell never drew → clear color showed).
using System.Collections.Generic;
namespace AcDream.App.Rendering;
public readonly record struct CellDrawEntry(uint CellId, IReadOnlyList<ViewPolygon> Slices);
public static class IndoorDrawPlan
{
/// <summary>Reverse OrderedVisibleCells (far→near), each visible cell with its view
/// slices. Mirrors DrawCells' shell/object loops. Cells whose view is empty are skipped
/// (they are not actually visible); no other cell is ever dropped.</summary>
public static List<CellDrawEntry> ShellPass(PortalVisibilityFrame frame)
{
var result = new List<CellDrawEntry>(frame.OrderedVisibleCells.Count);
for (int i = frame.OrderedVisibleCells.Count - 1; i >= 0; i--)
{
uint cellId = frame.OrderedVisibleCells[i];
if (!frame.CellViews.TryGetValue(cellId, out var view) || view.IsEmpty)
continue;
result.Add(new CellDrawEntry(cellId, view.Polygons));
}
return result;
}
}

View file

@ -6,15 +6,15 @@ namespace AcDream.App.Rendering;
/// Factory for the OUTDOOR render root — the cell the render roots at when the camera eye is outdoors.
/// Retail roots every in-world frame at <c>viewer_cell</c> (SmartBox::RenderNormalMode →
/// DrawInside(viewer_cell), decomp:92635); when outdoors that is a <c>CLandCell</c>. acdream models it
/// as a portal-less <see cref="LoadedCell"/> carrying only <see cref="LoadedCell.IsOutdoorNode"/> (so
/// <see cref="PortalVisibilityBuilder.Build"/> seeds OutsideView FULL-SCREEN → terrain/sky/scenery draw
/// as the root's shell) and <see cref="LoadedCell.SeenOutside"/>.
/// as a portal-less <see cref="LoadedCell"/> carrying only <see cref="LoadedCell.IsOutdoorNode"/> and
/// <see cref="LoadedCell.SeenOutside"/>. <see cref="ClipFrameAssembler.BeginWalkFrame"/> seeds the
/// full-screen outside view for this root.
///
/// <para>R-A2 (2026-06-08): the node no longer carries reverse portals into nearby buildings. Retail
/// does NOT flood buildings from the land root — buildings flood SEPARATELY, per-building, during the
/// landscape draw (terrain BSP → DrawPortal → ConstructView(CBldPortal), decomp:326881/433895/433827).
/// acdream issues those via <see cref="PortalVisibilityBuilder.ConstructViewBuilding"/> per nearby
/// building inside <see cref="RetailPViewRenderer.DrawInside"/>. The pre-R-A2 design flooded all
/// acdream issues those through the retained frame walk per nearby building inside
/// <see cref="RetailPViewRenderer.DrawInside"/>. The pre-R-A2 design flooded all
/// buildings from one root through reverse portals, coupling their interior membership to a single
/// root-level portal-side test that oscillated as the chase eye grazed a doorway — the indoor flap.</para>
/// </summary>

View file

@ -71,24 +71,6 @@ public sealed class PhysicsCameraCollisionProbe : ICameraCollisionProbe
Vector3 eye = FromSpherePath(r.Position, ViewerSphereRadius);
// [flap-sweep] camera-collision probe (ACDREAM_PROBE_FLAP), paired with the
// builder's [flap]/[flap-cam]. start = the pivot-seated start cell (vs cell = the
// player feet cell); ok = the sweep found a valid position (find_valid_position != 0).
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeFlapEnabled)
{
var cp = _physics.DataCache?.GetCellStruct(startCell);
string bsp = cp?.FlatPhysicsBsp is null
? "noflatbsp"
: (cp.FlatPhysicsBsp.RootIndex < 0 ? "noroot" : "ok");
float desiredBack = Vector3.Distance(pivot, desiredEye);
float eyeBack = Vector3.Distance(pivot, eye);
System.Console.WriteLine(
$"[flap-sweep] cell=0x{cellId:X8} start=0x{startCell:X8} ok={r.Ok} resolved={(cp is not null ? "Y" : "n")} bsp={bsp} " +
$"desiredBack={desiredBack:F2} eyeBack={eyeBack:F2} pulledIn={desiredBack - eyeBack:F2} " +
$"in=({desiredEye.X:F6},{desiredEye.Y:F6},{desiredEye.Z:F6}) out=({eye.X:F6},{eye.Y:F6},{eye.Z:F6}) " +
$"viewerCell=0x{r.CellId:X8} collNormValid={r.CollisionNormalValid}");
}
// success: set_viewer(curr_pos, 0); viewer_cell = sphere_path.curr_cell (pc:92870-92871).
// Graph-tracked, no AABB/grace.
if (r.Ok) return new CameraSweepResult(eye, r.CellId);

View file

@ -93,7 +93,7 @@ public static class PortalProjection
}
/// <summary>Project a cell-local polygon to NDC, preserving the projected winding of
/// the input (NOT normalized to CCW). The caller (PortalVisibilityBuilder) is responsible
/// the input (NOT normalized to CCW). The frame-walk caller is responsible
/// for feeding camera-facing portal polygons (via the portal-side test) so the result is
/// CCW for the CCW-only <see cref="ScreenPolygonClip"/>. Returns fewer than 3 verts when
/// the polygon is entirely behind the camera / degenerate.</summary>

View file

@ -189,8 +189,8 @@ public sealed class CellView
{
if (p.IsEmpty) return false;
// Drift-tolerant, rotation-invariant dedup (2026-06-06 hang fix). PortalVisibilityBuilder.Build
// re-queues a cell every time its CellView GROWS, so the flood only terminates when Add
// Drift-tolerant, rotation-invariant dedup (2026-06-06 hang fix). A caller may
// re-queue a cell every time its CellView grows, so the flood only terminates when Add
// recognises a re-clipped region as a duplicate. Across BFS rounds the SAME region returns
// float-drifted, vertex-rotated, and/or with a ±1 vertex count (homogeneous Sutherland-Hodgman +
// EnsureCcw); the old exact index-by-index match (eps 1e-4) caught none of those, so the region

File diff suppressed because it is too large Load diff

View file

@ -62,7 +62,6 @@ internal sealed partial class RetailPViewPassExecutor : IEnvCellImmediateDrawSin
private readonly ParticleRenderer? _particleRenderer;
private readonly PortalDepthMaskRenderer? _portalDepthMask;
private readonly RetailAlphaQueue _alpha;
private readonly WorldRenderDiagnostics _diagnostics;
private readonly TerrainDrawDiagnosticsController _terrainDiagnostics;
private readonly HashSet<uint> _noSceneParticleEntityIds = [];
private readonly EnvCellAlphaDrawSource _envCellClipAlphaSource;
@ -85,7 +84,6 @@ internal sealed partial class RetailPViewPassExecutor : IEnvCellImmediateDrawSin
ParticleRenderer? particleRenderer,
PortalDepthMaskRenderer? portalDepthMask,
RetailAlphaQueue alpha,
WorldRenderDiagnostics diagnostics,
TerrainDrawDiagnosticsController terrainDiagnostics)
{
_surface = surface ?? throw new ArgumentNullException(nameof(surface));
@ -100,7 +98,6 @@ internal sealed partial class RetailPViewPassExecutor : IEnvCellImmediateDrawSin
_particleRenderer = particleRenderer;
_portalDepthMask = portalDepthMask;
_alpha = alpha ?? throw new ArgumentNullException(nameof(alpha));
_diagnostics = diagnostics ?? throw new ArgumentNullException(nameof(diagnostics));
_terrainDiagnostics = terrainDiagnostics
?? throw new ArgumentNullException(nameof(terrainDiagnostics));
_envCellClipAlphaSource = new EnvCellAlphaDrawSource(
@ -663,20 +660,6 @@ internal sealed partial class RetailPViewPassExecutor : IEnvCellImmediateDrawSin
clipSlot: 0);
}
public void EmitDiagnostics(
RetailPViewFrameInput frame,
RetailPViewFrameResult result) =>
_diagnostics.EmitRetailPViewDiagnostics(
RenderingDiagnostics.ProbeVisibilityEnabled,
RenderingDiagnostics.ProbeFlapEnabled,
result,
frame.RootCell,
frame.ViewerCellId,
frame.PlayerCellId,
frame.CameraWorldPosition,
frame.PlayerViewPosition,
frame.CameraCellResolution);
/// <summary>Retail <c>D3DPolyRender::DrawPortalPolyInternal</c>
/// @0x0059BC90. Main interior roots stamp true depth (seal); outdoor and
/// look-in apertures stamp far depth (punch). The renderer owns that
@ -737,14 +720,6 @@ internal sealed partial class RetailPViewPassExecutor : IEnvCellImmediateDrawSin
cell.WorldTransform);
}
_diagnostics.EmitSeamMask(
RenderingDiagnostics.ProbeSeamDrawEnabled,
RenderingDiagnostics.SeamDrawTargetCells,
cellId,
index,
forceFarZ,
world[..count]);
// S4-c1 fix round 1 F2: retail increments portalsDrawnCount
// (0x59BD70-0x59BD74) BEFORE polyClipFinish runs (0x59BDB0) —
// the counter records accepted ATTEMPTS, not successful GPU

View file

@ -32,12 +32,6 @@ internal sealed class RetailPViewRenderer
// cleared in finally.
private Action? _walkPreClearDynamics;
// 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 string? _probeFacilityStairRootSignature;
private string? _probeCathedralStairRootSignature;
// FW6 allocation closeout: the walk's large event/view/route scratch,
// frame context, and one-cell leaf collections are renderer-lifetime
// owners. Only their frame-local bindings change. Before this cutover all
@ -108,11 +102,8 @@ internal sealed class RetailPViewRenderer
ArgumentNullException.ThrowIfNull(ctx);
ArgumentNullException.ThrowIfNull(passes);
passes.BeginFrame();
// Campaign FW4: there is one production renderer and one visibility
// owner. The fake/legacy executor path was useful during the cutover,
// but retaining it here kept the retired PortalVisibilityBuilder and
// look-in seed machinery alive. Fail loudly if composition ever tries
// to reintroduce that split.
// There is one production renderer and one frame-walk owner. Fail
// loudly if composition attempts to split that path.
RetailPViewPassExecutor walkExecutor = passes as RetailPViewPassExecutor
?? throw new InvalidOperationException(
"The retail frame walk requires RetailPViewPassExecutor.");
@ -294,128 +285,13 @@ internal sealed class RetailPViewRenderer
viewportHeight);
}
// The walk's visited set replaces PortalVisibilityFrame's old
// OrderedVisibleCells side-channel for every production consumer.
// The walk's visited set feeds every production consumer.
_drawableCellsScratch.Clear();
_drawableCellsScratch.UnionWith(walkDriver.VisitedCells);
walkDriver.CopyVisibleCellsTo(_visibleCellsScratch);
_visibleLandscapeCellsScratch.Clear();
_visibleLandscapeCellsScratch.UnionWith(walkDriver.VisitedLandscapeCellIds);
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeFacilityStairsEnabled)
{
static int TargetMembership(IReadOnlyCollection<uint> cells)
{
const uint a = 0x8A02015Eu;
const uint b = 0x8A02015Fu;
const uint c = 0x8A0201C1u;
int mask = 0;
foreach (uint cell in cells)
{
if (cell == a) mask |= 1;
if (cell == b) mask |= 2;
if (cell == c) mask |= 4;
}
return mask;
}
_probeFacilityStairFrame++;
int floodMask = TargetMembership(walkDriver.InteriorFloodCells);
int visitedMask = TargetMembership(walkDriver.VisitedCells);
int turnMask = TargetMembership(walkDriver.LookInCells);
string signature = $"{ctx.ViewerCellId:X8}:{ctx.PlayerCellId:X8}:"
+ $"{ctx.RootCell.CellId:X8}:{floodMask}:{visitedMask}:{turnMask}:"
+ $"{walkDriver.InteriorFloodCells.Count}:{walkDriver.VisitedCells.Count}:"
+ $"{walkDriver.LookInCellTurns.Count}";
bool changed = !string.Equals(
_probeFacilityStairRootSignature,
signature,
StringComparison.Ordinal);
_probeFacilityStairRootSignature = signature;
if (changed)
{
static string DescribeMask(int mask) =>
$"15e={((mask & 1) != 0 ? 1 : 0)},"
+ $"15f={((mask & 2) != 0 ? 1 : 0)},"
+ $"1c1={((mask & 4) != 0 ? 1 : 0)}";
Console.WriteLine(
$"[facility-root] f={_probeFacilityStairFrame} changed={(changed ? 1 : 0)} "
+ $"viewer=0x{ctx.ViewerCellId:X8} player=0x{ctx.PlayerCellId:X8} "
+ $"root=0x{ctx.RootCell.CellId:X8} res={ctx.CameraCellResolution} "
+ $"eye=({ctx.ViewerEyePos.X:F4},{ctx.ViewerEyePos.Y:F4},{ctx.ViewerEyePos.Z:F4}) "
+ $"playerPos=({ctx.PlayerViewPosition.X:F4},{ctx.PlayerViewPosition.Y:F4},{ctx.PlayerViewPosition.Z:F4}) "
+ $"rootFlood={walkDriver.InteriorFloodCells.Count}"
+ $"[{DescribeMask(floodMask)}] "
+ $"visited={walkDriver.VisitedCells.Count}"
+ $"[{DescribeMask(visitedMask)}] "
+ $"turns={walkDriver.LookInCellTurns.Count}"
+ $"[{DescribeMask(turnMask)}]");
}
static int CathedralMembership(IReadOnlyCollection<uint> cells)
{
const uint oldBuilding = 0xF4180107u;
const uint stairParent = 0xF4180112u;
int mask = 0;
foreach (uint cell in cells)
{
if (cell == oldBuilding) mask |= 1;
if (cell == stairParent) mask |= 2;
}
return mask;
}
static uint BuildingAnchor(Walk.WalkBuilding building)
{
foreach (Walk.WalkBldPortal portal in building.Portals)
{
if (portal.OtherCellId != 0xFFFFFFFFu)
return portal.OtherCellId;
}
return 0u;
}
int cathedralFlood = CathedralMembership(walkDriver.InteriorFloodCells);
int cathedralVisited = CathedralMembership(walkDriver.VisitedCells);
int cathedralLookIn = CathedralMembership(walkDriver.LookInCells);
bool bucket107 = _walkWorldData.StaticBucketContains(
0xF4180107u,
0x020009A2u);
bool bucket112 = _walkWorldData.StaticBucketContains(
0xF4180112u,
0x020009A2u);
string buildingAnchors = string.Join(",", walkDriver.VisitedBuildings
.ConvertAll(building => $"0x{BuildingAnchor(building):X8}"));
string cathedralSignature =
$"{ctx.ViewerCellId:X8}:{ctx.PlayerCellId:X8}:{ctx.RootCell.CellId:X8}:"
+ $"{cathedralFlood}:{cathedralVisited}:{cathedralLookIn}:"
+ $"{(bucket107 ? 1 : 0)}:{(bucket112 ? 1 : 0)}:{buildingAnchors}";
bool cathedralChanged = !string.Equals(
_probeCathedralStairRootSignature,
cathedralSignature,
StringComparison.Ordinal);
_probeCathedralStairRootSignature = cathedralSignature;
if (cathedralChanged
&& ((ctx.ViewerCellId & 0xFFFF0000u) == 0xF4180000u
|| (ctx.PlayerCellId & 0xFFFF0000u) == 0xF4180000u))
{
static string DescribeCathedralMask(int mask) =>
$"107={((mask & 1) != 0 ? 1 : 0)},"
+ $"112={((mask & 2) != 0 ? 1 : 0)}";
Console.WriteLine(
$"[cathedral-stair] f={_probeFacilityStairFrame} "
+ $"viewer=0x{ctx.ViewerCellId:X8} player=0x{ctx.PlayerCellId:X8} "
+ $"root=0x{ctx.RootCell.CellId:X8} res={ctx.CameraCellResolution} "
+ $"eye=({ctx.ViewerEyePos.X:F4},{ctx.ViewerEyePos.Y:F4},{ctx.ViewerEyePos.Z:F4}) "
+ $"playerPos=({ctx.PlayerViewPosition.X:F4},{ctx.PlayerViewPosition.Y:F4},{ctx.PlayerViewPosition.Z:F4}) "
+ $"rootFlood=[{DescribeCathedralMask(cathedralFlood)}] "
+ $"visited=[{DescribeCathedralMask(cathedralVisited)}] "
+ $"lookIn=[{DescribeCathedralMask(cathedralLookIn)}] "
+ $"buckets=[107={(bucket107 ? 1 : 0)},112={(bucket112 ? 1 : 0)}] "
+ $"buildings=[{buildingAnchors}]");
}
}
}
// FW4 slice 1: the ONE clip-region publication, after any walk
@ -448,8 +324,6 @@ internal sealed class RetailPViewRenderer
counts,
sourceCounts,
diagnosticPartition: null);
passes.EmitDiagnostics(ctx, result);
// The one collected walk owns terrain, cell shells, buildings,
// statics, dynamics, particles, punches, and alpha barriers at
// their retail turns. Interior-root landscape services run at the

View file

@ -1,207 +0,0 @@
using System;
using System.Collections.Generic;
using System.Numerics;
namespace AcDream.App.Rendering;
/// <summary>
/// T3 (BR-5): the port of retail's <c>Render::viewconeCheck</c> (Ghidra
/// 0x0054c250) — meshes (characters, statics, emitters) are CULLED per portal
/// view by a bounding-sphere test against the view's edge planes, never
/// clipped. Retail stores each view vertex with its 3D eye-edge plane
/// (<c>view_vertex { Vec2D pt; Plane plane }</c>, acclient.h:32483) and tests
/// the object's drawing sphere against the installed view's plane set;
/// OUTSIDE → skipped (RenderDeviceD3D::DrawMesh per-view loop pc:429290-429310,
/// and the DrawCells per-cell object epilogue, Ghidra 0x005a4840).
///
/// <para>Our views are clip-space half-planes (≤8 per slice,
/// <see cref="ClipPlaneSet"/> output: (nx,ny,0,d) satisfied when
/// nx·Cx + ny·Cy + d·Cw ≥ 0 for clip-space C). Lifting one to world space —
/// the view_vertex.plane analog, a plane through the EYE and the view edge —
/// is one matrix fold: with row-vector convention (System.Numerics),
/// C = world·VP, so C·P = world·(VP·P); L = VP·P (rows of VP dotted with P)
/// is the world-space homogeneous half-plane. Sphere-vs-half-plane keeps the
/// sphere when L.xyz·c + L.w ≥ r·|L.xyz| (not entirely outside).</para>
///
/// <para>A sphere is visible through a SLICE when it is not entirely outside
/// any of the slice's planes (convex region); visible for a CELL when any of
/// the cell's slices passes. A slice with zero planes is pass-all (the
/// NoClipSlice / full-screen outdoor case). A cell with no views culls — in
/// retail an object whose cell is not in the draw list is simply never
/// reached.</para>
/// </summary>
public sealed class ViewconeCuller
{
private const int MaxRetainedCellPlaneSets = 512;
private const int MaxRetainedPlanesPerCell = 256;
private const int MaxRetainedSlicesPerCell = 64;
private readonly Dictionary<uint, PlaneSet> _cellPlanes = new();
private readonly Stack<PlaneSet> _planeSetPool = new();
private PlaneSet _outsidePlanes = new();
private readonly record struct SliceRange(int Start, int Count);
private readonly record struct LiftedPlane(Vector4 Equation, float NormalLength);
/// <summary>
/// Contiguous per-cell plane storage. Reusing two Lists per visible cell
/// avoids rebuilding a jagged array graph on every render frame while
/// retaining the exact slice boundaries used by retail's any-view test.
/// </summary>
private sealed class PlaneSet
{
public List<LiftedPlane> Planes { get; } = new();
public List<SliceRange> Slices { get; } = new();
public bool IsRetainable =>
Planes.Capacity <= MaxRetainedPlanesPerCell
&& Slices.Capacity <= MaxRetainedSlicesPerCell;
public void Reset()
{
Planes.Clear();
Slices.Clear();
}
}
/// <summary>True when the outside view is a full-screen pass-all (the
/// synthetic outdoor root) — every outside-test passes.</summary>
public bool OutsideIsFullScreen { get; private set; }
public static ViewconeCuller Build(
ClipFrameAssembly assembly,
in Matrix4x4 viewProjection,
ViewconeCuller? reuse = null)
{
ArgumentNullException.ThrowIfNull(assembly);
var culler = reuse ?? new ViewconeCuller();
culler.Reset();
foreach (var (cellId, slices) in assembly.CellIdToViewSlices)
{
PlaneSet lifted = culler.RentPlaneSet();
for (int s = 0; s < slices.Length; s++)
AppendLiftedSlice(lifted, slices[s].Planes, viewProjection);
culler._cellPlanes[cellId] = lifted;
}
var outside = assembly.OutsideViewSlices;
bool fullScreen = false;
for (int s = 0; s < outside.Length; s++)
{
AppendLiftedSlice(culler._outsidePlanes, outside[s].Planes, viewProjection);
if (outside[s].Planes.Length == 0)
fullScreen = true;
}
culler.OutsideIsFullScreen = fullScreen;
return culler;
}
private void Reset()
{
foreach (PlaneSet set in _cellPlanes.Values)
{
set.Reset();
if (set.IsRetainable && _planeSetPool.Count < MaxRetainedCellPlaneSets)
_planeSetPool.Push(set);
}
_cellPlanes.Clear();
if (_outsidePlanes.IsRetainable)
_outsidePlanes.Reset();
else
_outsidePlanes = new PlaneSet();
OutsideIsFullScreen = false;
}
private PlaneSet RentPlaneSet()
{
PlaneSet result = _planeSetPool.Count != 0
? _planeSetPool.Pop()
: new PlaneSet();
result.Reset();
return result;
}
private static void AppendLiftedSlice(
PlaneSet destination,
Vector4[] clipPlanes,
in Matrix4x4 m)
{
int start = destination.Planes.Count;
for (int i = 0; i < clipPlanes.Length; i++)
{
var p = clipPlanes[i];
var equation = new Vector4(
m.M11 * p.X + m.M12 * p.Y + m.M13 * p.Z + m.M14 * p.W,
m.M21 * p.X + m.M22 * p.Y + m.M23 * p.Z + m.M24 * p.W,
m.M31 * p.X + m.M32 * p.Y + m.M33 * p.Z + m.M34 * p.W,
m.M41 * p.X + m.M42 * p.Y + m.M43 * p.Z + m.M44 * p.W);
float normalLength = MathF.Sqrt(
equation.X * equation.X
+ equation.Y * equation.Y
+ equation.Z * equation.Z);
destination.Planes.Add(new LiftedPlane(equation, normalLength));
}
destination.Slices.Add(new SliceRange(start, clipPlanes.Length));
}
private static bool SphereInsidePlanes(
PlaneSet set,
SliceRange slice,
in Vector3 center,
float radius)
{
int end = slice.Start + slice.Count;
for (int i = slice.Start; i < end; i++)
{
LiftedPlane plane = set.Planes[i];
Vector4 l = plane.Equation;
float nLen = plane.NormalLength;
if (nLen < 1e-12f)
continue; // degenerate plane — no constraint
float dist = l.X * center.X + l.Y * center.Y + l.Z * center.Z + l.W;
if (dist < -radius * nLen)
return false; // entirely outside this edge plane
}
return true;
}
/// <summary>Sphere-vs-the-cell's-views: visible when any slice passes.
/// A cell with no views culls (not in the draw list ⇒ never reached in
/// retail). A zero-plane slice is pass-all.</summary>
public bool SphereVisibleInCell(uint cellId, in Vector3 center, float radius)
{
if (!_cellPlanes.TryGetValue(cellId, out PlaneSet? set))
return false;
for (int s = 0; s < set.Slices.Count; s++)
if (SphereInsidePlanes(set, set.Slices[s], center, radius))
return true;
return false;
}
/// <summary>Sphere-vs-the-outside-views (objects in outdoor space seen
/// from an interior root through doorways; pass-all under the outdoor
/// root's full-screen outside view).</summary>
public bool SphereVisibleOutside(in Vector3 center, float radius)
{
if (OutsideIsFullScreen)
return true;
for (int s = 0; s < _outsidePlanes.Slices.Count; s++)
if (SphereInsidePlanes(_outsidePlanes, _outsidePlanes.Slices[s], center, radius))
return true;
return false;
}
/// <summary>Sphere vs ONE outside slice (the landscape pass draws per
/// slice; its statics pre-filter tests against exactly that slice).</summary>
public bool SphereVisibleInOutsideSlice(int sliceIndex, in Vector3 center, float radius)
{
if ((uint)sliceIndex >= (uint)_outsidePlanes.Slices.Count)
return false;
return SphereInsidePlanes(
_outsidePlanes,
_outsidePlanes.Slices[sliceIndex],
center,
radius);
}
}

View file

@ -107,9 +107,8 @@ public sealed class RetailFrameWalk
/// exit-view polygons THIS walk's own <c>ConstructView</c> installed
/// during the most recent <see cref="DrawInside"/>. The production
/// renderer materializes the frame's terrain/sky/punch clip slices from
/// these views (retail has exactly one visibility structure; the old
/// PortalVisibilityBuilder assembly no longer feeds the walk path's
/// outside views). Valid after <see cref="WalkFrame"/> returns for an
/// these views (retail has exactly one visibility structure). Valid after
/// <see cref="WalkFrame"/> returns for an
/// interior root, until the next interior root's view push resets it.</summary>
internal WalkPortalView InteriorOutsideView => _interiorPView.OutsideView;

View file

@ -186,11 +186,6 @@ public sealed class WalkPView
ref WalkCellPortal portal = ref cell.Portals[j];
if (!flags.Seen || flags.InView)
{
EmitFacilityPortalProbe(
cell, j, viewIndex: -1, top, portal, flags, ctx,
projectedCount: -1, clippedCount: -1,
neighborViewsBefore: -1, neighborViewsAfter: -1,
appended: false, result: "flag-gate");
continue;
}
if (cell.CachedNeighbors[j] is null && portal.OtherCellId != 0xFFFFFFFFu)
@ -198,11 +193,6 @@ public sealed class WalkPView
cell.CachedNeighbors[j] = ctx.GetVisible(portal.OtherCellId);
if (cell.CachedNeighbors[j] is null)
{
EmitFacilityPortalProbe(
cell, j, viewIndex: -1, top, portal, flags, ctx,
projectedCount: -1, clippedCount: -1,
neighborViewsBefore: -1, neighborViewsAfter: -1,
appended: false, result: "neighbor-missing");
continue; // not loaded: silently dead
}
}
@ -223,15 +213,8 @@ public sealed class WalkPView
cell, portal.PortalSide,
cell.PortalPolygons[portal.PolygonIndex],
doClip: true, ctx, _clipScratch);
int projectedCount =
cell.PortalPolygons[portal.PolygonIndex].Vertices.Length;
if (n == 0)
{
EmitFacilityPortalProbe(
cell, j, i, top, portal, flags, ctx,
projectedCount, clippedCount: 0,
neighborViewsBefore: -1, neighborViewsAfter: -1,
appended: false, result: "clip-empty");
continue;
}
@ -257,123 +240,19 @@ public sealed class WalkPView
SetView(top, i); // restore after the far-frame excursion
if (n == 0)
{
EmitFacilityPortalProbe(
cell, j, i, top, portal, flags, ctx,
projectedCount, clippedCount: 0,
neighborViewsBefore: neighbor.TopView.ViewCount,
neighborViewsAfter: neighbor.TopView.ViewCount,
appended: false, result: "far-clip-empty");
continue;
}
}
int before = neighbor.TopView.ViewCount;
bool appended = false;
if (neighbor.NumView != 0)
appended = WalkCopyView.Append(
WalkCopyView.Append(
neighbor.TopView, _clipScratch.AsSpan(0, n),
ctx.Rays, ctx.WorldViewpoint);
EmitFacilityPortalProbe(
cell, j, i, top, portal, flags, ctx,
projectedCount, n, before, neighbor.TopView.ViewCount,
appended,
neighbor.NumView == 0 ? "neighbor-not-pushed"
: appended ? "view-appended" : "append-rejected");
}
}
}
return true;
}
/// <summary>
/// Output-only Facility Hub discriminator for the residual #177 stair
/// disappearance. It traces only portals entering cell 0x8A02015F so a
/// side-to-away camera turn can distinguish flag admission, homogeneous
/// portal clipping, and copy_view append without changing the walk.
/// </summary>
private void EmitFacilityPortalProbe(
WalkCell cell,
int portalIndex,
int viewIndex,
WalkPortalView top,
WalkCellPortal portal,
WalkPortalFlags flags,
IWalkFrameContext ctx,
int projectedCount,
int clippedCount,
int neighborViewsBefore,
int neighborViewsAfter,
bool appended,
string result)
{
if (!AcDream.Core.Rendering.RenderingDiagnostics.ProbeFacilityStairsEnabled
|| portal.OtherCellId != 0x8A02015Fu
|| (cell.CellId & 0xFFFF0000u) != 0x8A020000u)
{
return;
}
string activeBounds = "none";
if ((uint)viewIndex < (uint)top.ViewCount)
{
WalkViewPoly active = top.View.Polys[viewIndex];
activeBounds = $"({active.XMin:F2},{active.YMin:F2})-"
+ $"({active.XMax:F2},{active.YMax:F2})";
}
string projectedBounds = DescribeScreenBounds(
_projectScratch.AsSpan(0, Math.Max(projectedCount, 0)));
string clippedBounds = DescribeScreenBounds(
_clipScratch.AsSpan(0, Math.Max(clippedCount, 0)));
Console.WriteLine(
$"[facility-portal] frame={_masterTimestamp} "
+ $"from=0x{cell.CellId:X8} portal={portalIndex} "
+ $"to=0x{portal.OtherCellId:X8} back={portal.OtherPortalId} "
+ $"view={viewIndex}/{top.ViewCount} startFlags="
+ $"seen:{(flags.Seen ? 1 : 0)},in:{(flags.InView ? 1 : 0)} "
+ $"side={portal.PortalSide} exact={(portal.ExactMatch ? 1 : 0)} "
+ $"eye=({ctx.WorldViewpoint.X:F6},{ctx.WorldViewpoint.Y:F6},"
+ $"{ctx.WorldViewpoint.Z:F6}) active={activeBounds} "
+ $"projected={projectedCount}:{projectedBounds} "
+ $"clipped={clippedCount}:{clippedBounds} "
+ $"neighborViews={neighborViewsBefore}->{neighborViewsAfter} "
+ $"append={(appended ? 1 : 0)} result={result}");
}
private static string DescribeScreenBounds(ReadOnlySpan<WalkScreenPoint> points)
{
if (points.IsEmpty)
return "none";
float xmin = float.PositiveInfinity;
float xmax = float.NegativeInfinity;
float ymin = float.PositiveInfinity;
float ymax = float.NegativeInfinity;
float wmin = float.PositiveInfinity;
float wmax = float.NegativeInfinity;
int divided = 0;
for (int index = 0; index < points.Length; index++)
{
ref readonly WalkScreenPoint point = ref points[index];
wmin = MathF.Min(wmin, point.W);
wmax = MathF.Max(wmax, point.W);
if (point.W < WalkScreenClip.MinW)
continue;
float x = point.X / point.W;
float y = point.Y / point.W;
xmin = MathF.Min(xmin, x);
xmax = MathF.Max(xmax, x);
ymin = MathF.Min(ymin, y);
ymax = MathF.Max(ymax, y);
divided++;
}
return divided == 0
? $"behind(w={wmin:F4}..{wmax:F4})"
: $"({xmin:F2},{ymin:F2})-({xmax:F2},{ymax:F2});"
+ $"w={wmin:F4}..{wmax:F4};front={divided}";
}
// ------------------------------------------------------------------
// OtherPortalClip @0x005a5400 — the double clip for non-exact_match
// portals: snapshot the near-clipped poly as a temp view, then re-clip

View file

@ -84,7 +84,6 @@ internal sealed class WalkProductionWorldData : IWalkFrameWorldData
private readonly Dictionary<uint, WalkFrameStaticRecords> _cellCache = new();
private readonly Dictionary<uint, WalkFrameStaticRecords> _cellDynamicCache = new();
private readonly Dictionary<uint, WalkFrameStaticRecords> _cellObjectsCache = new();
private readonly Dictionary<string, string> _facilityShadowProbeSignatures = new();
private readonly Dictionary<uint, List<RenderProjectionRecord>> _shellsByAnchor = new();
private readonly Dictionary<uint, WalkFrameStaticRecords> _outdoorMaterialized = new();
private readonly Dictionary<uint, WalkFrameStaticRecords> _outdoorDynamicsMaterialized = new();
@ -162,21 +161,6 @@ internal sealed class WalkProductionWorldData : IWalkFrameWorldData
int renderCenterLbX,
int renderCenterLbY)
{
// Campaign OVERHAUL S2 chunk 5: report the PRIOR frame's count
// before resetting it — the count is only final once that frame's
// walk (a sequence of on-demand Get* calls this class has no other
// "frame is done" hook for) has finished, so this is necessarily a
// one-frame-delayed report, matching every other per-second/per-
// change probe in this file family (print-only; never influences
// admission).
if (UnregisteredRenderMembershipCount > 0
&& AcDream.Core.Rendering.RenderingDiagnostics.ProbeFacilityStairsEnabled)
{
Console.WriteLine(
$"[walk-membership] unregistered={UnregisteredRenderMembershipCount} "
+ $"tupleLandblock=0x{_tupleLandblockId:X8}");
}
_scene = scene;
_tupleLandblockId = tupleLandblockId;
_cellCache.Clear();
@ -217,41 +201,11 @@ internal sealed class WalkProductionWorldData : IWalkFrameWorldData
}
}
/// <summary>Facility Hub / cathedral discriminator: does the given cell's
/// borrowed retail render membership contain a record with this exact
/// authored SourceId? Reimplemented directly over the registry's
/// per-cell entries (chunk 5) rather than reading a swept bucket
/// dictionary — <see cref="RetailPViewRenderer"/>'s
/// <c>ProbeFacilityStairsEnabled</c> block is the only caller.</summary>
internal bool StaticBucketContains(uint cellId, uint sourceId)
{
IReadOnlyList<RetailPartEntry> entries =
_shadows.GetRetailPartEntriesInCell(cellId);
uint previousEntityId = 0;
bool havePrevious = false;
for (int i = 0; i < entries.Count; i++)
{
uint entityId = entries[i].EntityId;
if (havePrevious && entityId == previousEntityId)
continue;
previousEntityId = entityId;
havePrevious = true;
if (_scene.TryGetByLocalEntityId(entityId, out RenderProjectionRecord record)
&& record.Source.SourceId == sourceId)
{
return true;
}
}
return false;
}
public WalkFrameStaticRecords GetCellStatics(uint cellId)
{
if (_cellCache.TryGetValue(cellId, out WalkFrameStaticRecords cached))
return cached;
WalkFrameStaticRecords records = ResolveCellView(cellId, dynamic: false);
EmitFacilityShadowProbe(records.Records, cellId, "static");
_cellCache[cellId] = records;
return records;
}
@ -261,7 +215,6 @@ internal sealed class WalkProductionWorldData : IWalkFrameWorldData
if (_cellObjectsCache.TryGetValue(cellId, out WalkFrameStaticRecords cached))
return cached;
WalkFrameStaticRecords records = ResolveCellView(cellId, dynamic: null);
EmitFacilityShadowProbe(records.Records, cellId, "combined");
_cellObjectsCache[cellId] = records;
return records;
}
@ -271,7 +224,6 @@ internal sealed class WalkProductionWorldData : IWalkFrameWorldData
if (_cellDynamicCache.TryGetValue(cellId, out WalkFrameStaticRecords cached))
return cached;
WalkFrameStaticRecords records = ResolveCellView(cellId, dynamic: true);
EmitFacilityShadowProbe(records.Records, cellId, "dynamic");
_cellDynamicCache[cellId] = records;
return records;
}
@ -391,68 +343,6 @@ internal sealed class WalkProductionWorldData : IWalkFrameWorldData
projectionClass is RenderProjectionClass.LiveDynamicRoot
or RenderProjectionClass.EquippedChild;
/// <summary>
/// Facility Hub discriminator for retail's cross-cell render-shadow path.
/// The scene query is currently keyed by authored parent cell, while retail
/// also appends each object's parts to every cell in its physics CELLARRAY
/// through CPhysicsObj::add_shadows_to_cells / CPartArray::AddPartsShadow.
/// This probe prints the authoritative physics owner set without changing
/// admission, so a correction is made only if the live staircase/player
/// registration proves that path is populated.
/// </summary>
private void EmitFacilityShadowProbe(
ReadOnlySpan<RenderProjectionRecord> records,
uint queriedCellId,
string route)
{
if (!AcDream.Core.Rendering.RenderingDiagnostics.ProbeFacilityStairsEnabled)
return;
for (int recordIndex = 0; recordIndex < records.Length; recordIndex++)
{
ref readonly RenderProjectionRecord record = ref records[recordIndex];
bool targetPlayer = record.EntityPayload.CasterIdentity
== RenderCasterIdentityKind.LocalPlayer;
bool targetStair = record.Source.SourceId == 0x02000623u;
bool targetCathedralStair =
record.Source.SourceId == 0x020009A2u;
if (!targetStair && !targetCathedralStair && !targetPlayer)
continue;
IReadOnlyList<uint> ownerCells =
_shadows.GetOwnerCells(record.Source.LocalEntityId);
var cells = new StringBuilder(ownerCells.Count * 11 + 2);
cells.Append('[');
for (int cellIndex = 0; cellIndex < ownerCells.Count; cellIndex++)
{
if (cellIndex != 0)
cells.Append(',');
cells.Append("0x").Append(ownerCells[cellIndex].ToString("X8"));
}
cells.Append(']');
string key = $"{route}:{queriedCellId:X8}:"
+ $"{record.Source.LocalEntityId:X8}";
string signature = $"{record.Source.ParentCellId:X8}:{cells}";
if (_facilityShadowProbeSignatures.TryGetValue(key, out string? prior)
&& string.Equals(prior, signature, StringComparison.Ordinal))
{
continue;
}
_facilityShadowProbeSignatures[key] = signature;
Console.WriteLine(
$"[facility-shadow] route={route} "
+ $"kind={(targetPlayer ? "player" : targetCathedralStair ? "cathedral-stair" : "stair")} "
+ $"guid=0x{record.Source.ServerGuid:X8} "
+ $"local=0x{record.Source.LocalEntityId:X8} "
+ $"query=0x{queriedCellId:X8} "
+ $"parent=0x{record.Source.ParentCellId:X8} "
+ $"owners={cells}");
}
}
public WalkFrameStaticRecords GetBuildingShellStatics(WalkBuilding building)
{
uint anchor = BuildingShellBucketCellId(building);

View file

@ -224,8 +224,7 @@ internal sealed class WalkStaticStreamPopulator
liveDynamic,
views,
viewRouteIndex,
cellId,
diagnosticViewProjection: viewProjection);
cellId);
for (int batchIndex = 0; batchIndex < _batchScratch.Count; batchIndex++)
{
@ -257,17 +256,10 @@ internal sealed class WalkStaticStreamPopulator
WbDrawDispatcher.WalkClassifiedBatch batch = item.Batch;
if (batch.IsOpaque)
{
int commandIndex = stream.Count;
stream.Append(new OrderedDrawCommand(
batch.Key, batch.Transform, item.Stage, cellId, batch.ClipSlot,
batch.Lights, batch.IndoorFlag, batch.Alpha,
batch.SelectionLighting, batch.DetailCategory));
_dispatcher.ProbeFacilityStairCommandAppended(
commandIndex,
item.LocalEntityId,
cellId,
item.Stage,
in batch);
}
else
{
@ -318,7 +310,6 @@ internal sealed class WalkStaticStreamPopulator
lookInViews,
lookInRouteIndex,
cellId,
diagnosticViewProjection: viewProjection,
buildingSelection: buildingSelection,
buildingPartTransform: buildingPartTransform);
@ -327,17 +318,10 @@ internal sealed class WalkStaticStreamPopulator
WbDrawDispatcher.WalkClassifiedBatch batch = _batchScratch[i];
if (batch.IsOpaque)
{
int commandIndex = stream.Count;
stream.Append(new OrderedDrawCommand(
batch.Key, batch.Transform, stage, cellId, batch.ClipSlot,
batch.Lights, batch.IndoorFlag, batch.Alpha,
batch.SelectionLighting, batch.DetailCategory));
_dispatcher.ProbeFacilityStairCommandAppended(
commandIndex,
record.Source.LocalEntityId,
cellId,
stage,
in batch);
}
else
{

View file

@ -220,12 +220,6 @@ public sealed unsafe partial class EnvCellRenderer
Array.Copy(cellSet, 0, _lightSetData, i * lightStride, lightStride);
}
if (renderPass == WbRenderPass.Opaque
&& AcDream.Core.Rendering.RenderingDiagnostics.ProbeSeamDrawEnabled)
{
EmitSeamDrawProbe(_renderDrawCalls, allInstances, _seamProbeFilter);
}
int lightCount = GlobalLightPacker.Pack(_pointSnapshot, ref _globalLightData);
int globalLightUploadCount = lightCount > 0 ? lightCount : 1;

View file

@ -203,21 +203,6 @@ public sealed partial class EnvCellRenderer :
public struct LastFrameStats { public int CellsRendered; public int TrianglesDrawn; }
private LastFrameStats _lastFrameStats;
/// <summary>
/// Diagnostic accessor for the [envcells] probe (Phase A8 apparatus 2026-05-28).
/// Returns (pool-list count total, snapshot's PostPreparePoolIndex high-water).
/// A divergence between expected and actual values would indicate a pool-
/// management regression — exactly the bug class the 2026-05-28 audit caught.
/// </summary>
public (int PoolTotal, int SnapshotPoolHwm) GetPoolDiagnostics()
{
int poolTotal;
lock (_listPool) poolTotal = _listPool.Count;
int hwm;
lock (_renderLock) hwm = _activeSnapshot.PostPreparePoolIndex;
return (poolTotal, hwm);
}
// ---------------------------------------------------------------------------
// Constructor
// Campaign V slice V11: the raw-GL constructor + Initialize(Shader) two-step
@ -1004,12 +989,6 @@ public sealed partial class EnvCellRenderer :
}
}
// #176 seam-draw probe: stash this call's filter so the opaque-pass
// emitter inside RenderModernMDIInternal can report flood membership
// per target cell (null on the unfiltered/outdoor path).
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeSeamDrawEnabled)
_seamProbeFilter = filter;
// WB EnvCellRenderManager.cs:470-483:
if (allInstances.Count > 0)
{
@ -1421,104 +1400,6 @@ public sealed partial class EnvCellRenderer :
ranges.Add(new MdiDrawRange(groupIndex, firstCommand, commandCount, materialState));
}
// ---------------------------------------------------------------------------
// #176 seam-draw probe (ACDREAM_PROBE_SEAMDRAW) — throwaway apparatus.
// The in-engine replacement for the RenderDoc pixel-history the pipeline
// can't have (RenderDoc hides GL_ARB_bindless_texture → our mandatory-modern
// startup gate throws). Per opaque pass: for each target cell — flood
// membership, every shell instance (count + translation, F3 z shows the
// +0.02 lift; n≥2 for one (cell,gfx) = the runtime double-draw), and the
// cell's 8-light set resolved to stable IDENTITIES (owner-cell low16 +
// intensity; raw indices shuffle when the pool rebuilds). Plus the
// snapshot's HOT lights (intensity ≥ 50 — the portal purples; fixtures are
// ~12). Change-deduped block with a 2 s heartbeat: a purple identity
// flipping with flood membership = the snapshot-scope mechanism; two
// coincident instances = the z-fight. See RenderingDiagnostics.
// ---------------------------------------------------------------------------
private HashSet<uint>? _seamProbeFilter;
private string? _seamSig;
private long _seamLastEmitMs;
private void EmitSeamDrawProbe(
List<(ObjectRenderData renderData, ulong gfxObjId, int count, int offset)> drawCalls,
List<InstanceData> allInstances,
HashSet<uint>? filter)
{
var ci = System.Globalization.CultureInfo.InvariantCulture;
var snap = _pointSnapshot;
var sb = new System.Text.StringBuilder(640);
var sorted = new List<uint>(AcDream.Core.Rendering.RenderingDiagnostics.SeamDrawTargetCells);
sorted.Sort();
foreach (uint cell in sorted)
{
sb.Append("\n[seam-cell] cell=0x").Append(cell.ToString("X8"));
sb.Append(" flood=").Append(filter is null ? '?' : (filter.Contains(cell) ? 'Y' : 'N'));
int totalInst = 0;
foreach (var dc in drawCalls)
{
int n = 0;
InstanceData first = default;
for (int i = dc.offset; i < dc.offset + dc.count; i++)
{
if (allInstances[i].CellId != cell) continue;
if (n == 0) first = allInstances[i];
n++;
}
if (n == 0) continue;
totalInst += n;
var t = first.Transform.Translation;
sb.AppendFormat(ci, " g=0x{0:X8}:n={1}@({2:F2},{3:F2},{4:F3})",
dc.gfxObjId, n, t.X, t.Y, t.Z);
}
if (totalInst == 0) sb.Append(" inst=0");
// The 8-light set this cell's instances carry (fresh: the per-pass
// cache was cleared at the top of RenderModernMDIInternal).
int[] set = GetCellLightSet(cell);
sb.Append(" L=[");
bool any = false;
for (int k = 0; k < set.Length; k++)
{
int idx = set[k];
if (idx < 0) continue;
if (any) sb.Append(',');
if (snap is not null && idx < snap.Count)
sb.AppendFormat(ci, "{0:X4}:I{1:F0}", snap[idx].CellId & 0xFFFFu, snap[idx].Intensity);
else
sb.Append('?').Append(idx);
any = true;
}
sb.Append(']');
}
sb.Append("\n[seam-snap] pool=").Append(snap?.Count ?? 0).Append(" hot=[");
if (snap is not null)
{
bool anyHot = false;
for (int i = 0; i < snap.Count; i++)
{
var ls = snap[i];
if (ls.Intensity < 50f) continue;
if (anyHot) sb.Append(',');
sb.AppendFormat(ci, "0x{0:X8}:I{1:F0}rgb({2:F2},{3:F2},{4:F2})",
ls.CellId, ls.Intensity, ls.ColorLinear.X, ls.ColorLinear.Y, ls.ColorLinear.Z);
anyHot = true;
}
}
sb.Append(']');
string sig = sb.ToString();
long now = System.Environment.TickCount64;
bool changed = sig != _seamSig;
if (!changed && (now - _seamLastEmitMs) < 2000) return;
_seamSig = sig;
_seamLastEmitMs = now;
System.Console.WriteLine($"[seam-blk] t={now} changed={(changed ? 1 : 0)}{sig}");
}
// ---------------------------------------------------------------------------
// List pool (GetPooledList)
// Copied from WB ObjectRenderManagerBase (pattern).

View file

@ -1,171 +0,0 @@
using System.Text;
using AcDream.App.Rendering.Walk;
using AcDream.Core.Rendering;
namespace AcDream.App.Rendering.Wb;
/// <summary>
/// Temporary issue-177 live probe. The classifier-side facility trace proves
/// whether the stair GfxObj survives the retail drawing-sphere test; this
/// companion trace follows that same exact object through the ordered stream,
/// ring upload, and final indirect draw call. It is inert unless
/// <c>ACDREAM_PROBE_FACILITY_STAIRS=1</c>.
/// </summary>
public sealed partial class WbDrawDispatcher
{
private const uint FacilityStairSubmissionLocalId = 0x4F41806Cu;
private readonly List<FacilityStairSubmissionCommand> _facilityStairSubmissionCommands = new(4);
private readonly HashSet<int> _facilityStairSubmittedCommands = new();
private int _facilityStairSubmissionFrame = -1;
private bool _facilityStairPrepareSeen;
private bool _facilityStairUploadCompleted;
private int _facilityStairPreparedCount;
private int _facilityStairSubmissionRangeFirst = -1;
private int _facilityStairSubmissionRangeCount;
private int _facilityStairSubmissionRunFirst = -1;
private int _facilityStairSubmissionRunCount;
private readonly record struct FacilityStairSubmissionCommand(
int CommandIndex,
uint CellId,
WalkDrawStage Stage,
System.Numerics.Vector3 Translation,
GroupKey Key);
internal void BeginFacilityStairSubmissionProbeFrame()
{
if (!RenderingDiagnostics.ProbeFacilityStairsEnabled)
return;
_facilityStairSubmissionCommands.Clear();
_facilityStairSubmittedCommands.Clear();
_facilityStairPrepareSeen = false;
_facilityStairUploadCompleted = false;
_facilityStairPreparedCount = 0;
_facilityStairSubmissionRangeFirst = -1;
_facilityStairSubmissionRangeCount = 0;
_facilityStairSubmissionRunFirst = -1;
_facilityStairSubmissionRunCount = 0;
_facilityStairSubmissionFrame = WalkPView.MasterTimestampForDiagnostics;
}
internal void ProbeFacilityStairCommandAppended(
int commandIndex,
uint localEntityId,
uint cellId,
WalkDrawStage stage,
in WalkClassifiedBatch batch)
{
if (!RenderingDiagnostics.ProbeFacilityStairsEnabled
|| localEntityId != FacilityStairSubmissionLocalId)
{
return;
}
_facilityStairSubmissionCommands.Add(new FacilityStairSubmissionCommand(
commandIndex,
cellId,
stage,
batch.Transform.Translation,
batch.Key));
}
private void ProbeFacilityStairPrepareStarted(int streamCount)
{
if (!RenderingDiagnostics.ProbeFacilityStairsEnabled)
return;
int frame = WalkPView.MasterTimestampForDiagnostics;
_facilityStairPrepareSeen = true;
_facilityStairPreparedCount = streamCount;
EmitFacilityStairSubmissionProbe("prepare");
}
private void ProbeFacilityStairUploadCompleted(int preparedCount)
{
if (!RenderingDiagnostics.ProbeFacilityStairsEnabled)
return;
_facilityStairUploadCompleted = true;
_facilityStairPreparedCount = preparedCount;
EmitFacilityStairSubmissionProbe("upload");
}
private void ProbeFacilityStairRunSubmitted(
int rangeFirst,
int rangeCount,
int runFirst,
int runCount)
{
if (!RenderingDiagnostics.ProbeFacilityStairsEnabled
|| _facilityStairSubmissionCommands.Count == 0)
{
return;
}
int runEnd = runFirst + runCount;
int submittedBefore = _facilityStairSubmittedCommands.Count;
for (int i = 0; i < _facilityStairSubmissionCommands.Count; i++)
{
int commandIndex = _facilityStairSubmissionCommands[i].CommandIndex;
if (commandIndex < runFirst || commandIndex >= runEnd)
continue;
_facilityStairSubmittedCommands.Add(commandIndex);
_facilityStairSubmissionRangeFirst = rangeFirst;
_facilityStairSubmissionRangeCount = rangeCount;
_facilityStairSubmissionRunFirst = runFirst;
_facilityStairSubmissionRunCount = runCount;
}
if (_facilityStairSubmittedCommands.Count != submittedBefore)
EmitFacilityStairSubmissionProbe("submit");
}
private void EmitFacilityStairSubmissionProbe(string phase)
{
if (_facilityStairSubmissionFrame < 0
|| _facilityStairSubmissionCommands.Count == 0)
{
return;
}
var commandDetails = new StringBuilder();
for (int i = 0; i < _facilityStairSubmissionCommands.Count; i++)
{
FacilityStairSubmissionCommand command = _facilityStairSubmissionCommands[i];
if (i != 0)
commandDetails.Append(';');
commandDetails.Append(command.CommandIndex)
.Append(':').Append(command.Stage)
.Append(":0x").Append(command.CellId.ToString("X8"))
.Append(":(")
.Append(command.Translation.X.ToString("F4"))
.Append(',')
.Append(command.Translation.Y.ToString("F4"))
.Append(',')
.Append(command.Translation.Z.ToString("F4"))
.Append(')')
.Append(':').Append(command.Key.CullMode)
.Append(':').Append(command.Key.FirstIndex)
.Append('+').Append(command.Key.IndexCount)
.Append(':').Append(command.Key.BaseVertex);
}
Console.WriteLine(
$"[facility-stair-path] phase={phase} frame={_facilityStairSubmissionFrame} "
+ $"local=0x{FacilityStairSubmissionLocalId:X8} "
+ $"commands={_facilityStairSubmissionCommands.Count} "
+ $"detail={commandDetails} "
+ $"prepare={(_facilityStairPrepareSeen ? 1 : 0)} "
+ $"preparedCount={_facilityStairPreparedCount} "
+ $"uploaded={(_facilityStairUploadCompleted ? 1 : 0)} "
+ $"submitted={_facilityStairSubmittedCommands.Count}/"
+ $"{_facilityStairSubmissionCommands.Count} "
+ $"range={_facilityStairSubmissionRangeFirst}+"
+ $"{_facilityStairSubmissionRangeCount} "
+ $"run={_facilityStairSubmissionRunFirst}+"
+ $"{_facilityStairSubmissionRunCount}");
}
}

View file

@ -350,7 +350,6 @@ public sealed unsafe partial class WbDrawDispatcher
_orderedStream = stream;
_orderedFrame = frame;
_orderedPreparedCount = 0;
ProbeFacilityStairPrepareStarted(stream.Count);
// Fail loud before any GPU work: a PortalPunch command has no
// submission path.
@ -433,7 +432,6 @@ public sealed unsafe partial class WbDrawDispatcher
checked((uint)(count * DrawCommandStride)));
_orderedPreparedCount = count;
ProbeFacilityStairUploadCompleted(count);
}
/// <summary>
@ -587,11 +585,6 @@ public sealed unsafe partial class WbDrawDispatcher
DrawIndirectRangeRhi(
encoder, ref pushConstants, commandBuffer, commandBase,
run.FirstCommand, run.CommandCount, _orderedDrawCullModes);
ProbeFacilityStairRunSubmitted(
firstCommand,
commandCount,
run.FirstCommand,
run.CommandCount);
}
ClearDetailPushConstants(ref pushConstants);

View file

@ -55,20 +55,6 @@ public sealed partial class WbDrawDispatcher
{
private readonly HashSet<WalkDrawnPartKey> _walkDrawnParts = new();
private bool _walkPartFrameActive;
private readonly Dictionary<string, string> _facilityStairProbeSignatures = new();
private Dictionary<uint, FacilityStairProbeSnapshot> _facilityStairsCurrent = new();
private Dictionary<uint, FacilityStairProbeSnapshot> _facilityStairsPrevious = new();
private int _facilityStairsCurrentFrame = -1;
private int _facilityStairsPreviousFrame = -1;
private readonly record struct FacilityStairProbeSnapshot(
uint CellId,
int RouteIndex,
bool Admitted,
bool ScreenIntersect,
string ScreenBounds,
Vector3 WorldCenter);
private readonly record struct WalkDrawnPartKey(
RenderProjectionId ProjectionId,
int PartIndex);
@ -93,7 +79,6 @@ public sealed partial class WbDrawDispatcher
_missRequested.Clear();
_walkDrawnParts.Clear();
BeginFacilityStairSubmissionProbeFrame();
_walkPartFrameActive = true;
}
@ -275,7 +260,6 @@ public sealed partial class WbDrawDispatcher
IWalkLookInViewSource? lookInViews = null,
int lookInRouteIndex = -1,
uint lookInCellId = 0,
Matrix4x4 diagnosticViewProjection = default,
WalkBuildingSelection? buildingSelection = null,
Matrix4x4 buildingPartTransform = default)
{
@ -369,17 +353,6 @@ public sealed partial class WbDrawDispatcher
ObjectRenderData? partData = _meshAdapter.TryGetRenderData(gfxObjId);
if (partData is null)
{
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeFacilityStairsEnabled
&& entity.LocalEntityId == 0x4F41806Cu
&& gfxObjId == 0x01001FE8u)
{
Console.WriteLine(
$"[facility-part] frame={WalkPView.MasterTimestampForDiagnostics} "
+ $"local=0x{entity.LocalEntityId:X8} "
+ $"cell=0x{lookInCellId:X8} gfx=0x{gfxObjId:X8} "
+ $"part={setupPartIndex} route={lookInRouteIndex} "
+ "decision=render-data-miss");
}
if (_missRequested.Add(gfxObjId))
_meshAdapter.EnsureLoaded(gfxObjId);
continue;
@ -393,12 +366,6 @@ public sealed partial class WbDrawDispatcher
: 1f;
if (opacity <= 0f)
{
EmitFacilityStairPartProbe(
in projection, in entity, lookInCellId,
lookInRouteIndex, (uint)gfxObjId, setupPartIndex,
partData, default, diagnosticViewProjection,
default, 0f, hasSphere: false,
opacity, "opacity-zero", lookInViews);
continue;
}
@ -411,21 +378,11 @@ public sealed partial class WbDrawDispatcher
lookInRouteIndex,
partData,
model,
out Vector3 sphereCenter,
out float sphereRadius,
out bool hasSphere);
out _,
out _,
out _);
bool firstAdmission = visible
&& TryStampWalkPart(in projection, selectionPartIndex);
EmitFacilityStairPartProbe(
in projection, in entity, lookInCellId,
lookInRouteIndex, (uint)gfxObjId, setupPartIndex,
partData, model, diagnosticViewProjection,
sphereCenter, sphereRadius, hasSphere,
opacity,
!visible
? "sphere-reject"
: firstAdmission ? "admit" : "already-drawn",
lookInViews);
if (!firstAdmission)
continue;
EmitClassifiedBatches(
@ -448,12 +405,6 @@ public sealed partial class WbDrawDispatcher
: 1f;
if (opacity <= 0f)
{
EmitFacilityStairPartProbe(
in projection, in entity, lookInCellId,
lookInRouteIndex, (uint)meshRef.GfxObjId, partIndex,
renderData, default, diagnosticViewProjection,
default, 0f, hasSphere: false,
opacity, "opacity-zero", lookInViews);
continue;
}
@ -463,21 +414,11 @@ public sealed partial class WbDrawDispatcher
lookInRouteIndex,
renderData,
model,
out Vector3 sphereCenter,
out float sphereRadius,
out bool hasSphere);
out _,
out _,
out _);
bool firstAdmission = visible
&& TryStampWalkPart(in projection, partIndex);
EmitFacilityStairPartProbe(
in projection, in entity, lookInCellId,
lookInRouteIndex, (uint)meshRef.GfxObjId, partIndex,
renderData, model, diagnosticViewProjection,
sphereCenter, sphereRadius, hasSphere,
opacity,
!visible
? "sphere-reject"
: firstAdmission ? "admit" : "already-drawn",
lookInViews);
if (!firstAdmission)
continue;
EmitClassifiedBatches(
@ -585,213 +526,6 @@ public sealed partial class WbDrawDispatcher
return lookInViews.SphereVisibleInLookInTurn(routeIndex, in sphereCenter, sphereRadius);
}
private void EmitFacilityStairPartProbe(
in RenderProjectionRecord projection,
in RenderInstanceCandidate entity,
uint cellId,
int routeIndex,
uint gfxObjId,
int partIndex,
ObjectRenderData renderData,
Matrix4x4 localToWorld,
Matrix4x4 viewProjection,
Vector3 sphereCenter,
float sphereRadius,
bool hasSphere,
float opacity,
string decision,
IWalkLookInViewSource? lookInViews)
{
if (!AcDream.Core.Rendering.RenderingDiagnostics.ProbeFacilityStairsEnabled)
return;
bool targetStairCell = cellId is 0x8A02015Eu or 0x8A02015Fu;
bool targetPlayerCell = targetStairCell || cellId == 0x8A0201C1u;
bool targetStair = targetStairCell && gfxObjId == 0x010000DEu;
bool targetPlayer = targetPlayerCell
&& projection.EntityPayload.CasterIdentity
== RenderCasterIdentityKind.LocalPlayer;
// Static render projections carry the local render identity here;
// SourceId is not guaranteed to retain the Setup DID after the
// landblock projection has been materialized.
// Cathedral floating stair Setup 0x020009A2 is seven independent
// visual parts (0x01001FE8 + six 0x01001FE6 slabs). Tracing only the
// first GfxObj hid the actual camera-angle failure on the six visible
// slabs, so keep the probe keyed to the complete PartArray owner.
bool targetCathedralRamp = entity.LocalEntityId == 0x4F41806Cu;
if (!targetStair && !targetPlayer && !targetCathedralRamp)
return;
int frame = WalkPView.MasterTimestampForDiagnostics;
string screenBounds = "unavailable";
bool screenIntersect = false;
if (targetStair || targetCathedralRamp)
{
AdvanceFacilityStairProbeFrame(frame);
screenIntersect = TryProjectFacilityStairBounds(
renderData,
localToWorld,
viewProjection,
out screenBounds);
var snapshot = new FacilityStairProbeSnapshot(
cellId,
routeIndex,
string.Equals(decision, "admit", StringComparison.Ordinal),
screenIntersect,
screenBounds,
hasSphere ? sphereCenter : localToWorld.Translation);
if (!_facilityStairsCurrent.TryGetValue(
entity.LocalEntityId,
out FacilityStairProbeSnapshot existing)
|| (!existing.Admitted && snapshot.Admitted))
{
_facilityStairsCurrent[entity.LocalEntityId] = snapshot;
}
}
string key = $"{entity.ServerGuid:X8}:{entity.LocalEntityId:X8}:"
+ $"{cellId:X8}:{gfxObjId:X8}:{partIndex}";
// A route index is allocated afresh every frame. Including it in the
// signature made a stationary cathedral trace print at frame rate and
// obscure the useful admit/reject transition. Cell + part are already
// in the key; emit again only when that route's decision changes.
string signature = targetCathedralRamp
? $"{decision}:{(hasSphere ? 1 : 0)}:"
+ $"{(opacity <= 0f ? 0 : opacity < 1f ? 1 : 2)}"
: $"{routeIndex}:{decision}:{(hasSphere ? 1 : 0)}:"
+ $"{(opacity <= 0f ? 0 : opacity < 1f ? 1 : 2)}";
if (_facilityStairProbeSignatures.TryGetValue(key, out string? prior)
&& string.Equals(prior, signature, StringComparison.Ordinal))
{
return;
}
_facilityStairProbeSignatures[key] = signature;
string authored = renderData.SelectionSphere is { } sourceSphere
? $" authored=({sourceSphere.Origin.X:F4},{sourceSphere.Origin.Y:F4},"
+ $"{sourceSphere.Origin.Z:F4};r={sourceSphere.Radius:F4})"
: " authored=none";
string world = hasSphere
? $" world=({sphereCenter.X:F4},{sphereCenter.Y:F4},"
+ $"{sphereCenter.Z:F4};r={sphereRadius:F4})"
: " world=none";
string cone = targetCathedralRamp && hasSphere && lookInViews is not null
? " cone={" + lookInViews.DescribeLookInTurn(
routeIndex, in sphereCenter, sphereRadius) + "}"
: string.Empty;
Console.WriteLine(
$"[facility-part] frame={frame} "
+ $"kind={projection.EntityPayload.CasterIdentity} "
+ $"guid=0x{entity.ServerGuid:X8} local=0x{entity.LocalEntityId:X8} "
+ $"cell=0x{cellId:X8} parent=0x{entity.ParentCellId:X8} "
+ $"gfx=0x{gfxObjId:X8} part={partIndex} route={routeIndex} "
+ $"opacity={opacity:F4} decision={decision} "
+ $"screen={(screenIntersect ? 1 : 0)} ndc={screenBounds}"
+ authored + world + cone);
}
private void AdvanceFacilityStairProbeFrame(int frame)
{
if (frame == _facilityStairsCurrentFrame)
return;
if (_facilityStairsCurrentFrame >= 0)
{
if (_facilityStairsPreviousFrame >= 0)
{
foreach ((uint localId, FacilityStairProbeSnapshot previous)
in _facilityStairsPrevious)
{
if (!_facilityStairsCurrent.TryGetValue(localId, out _))
{
Console.WriteLine(
$"[facility-stair-drop] frame={_facilityStairsCurrentFrame} "
+ $"previousFrame={_facilityStairsPreviousFrame} "
+ $"local=0x{localId:X8} cell=0x{previous.CellId:X8} "
+ $"route={previous.RouteIndex} admitted={(previous.Admitted ? 1 : 0)} "
+ $"screen={(previous.ScreenIntersect ? 1 : 0)} "
+ $"lastNdc={previous.ScreenBounds} "
+ $"world=({previous.WorldCenter.X:F4},{previous.WorldCenter.Y:F4},"
+ $"{previous.WorldCenter.Z:F4}) reason=cell-not-walked");
}
}
foreach ((uint localId, FacilityStairProbeSnapshot current)
in _facilityStairsCurrent)
{
if (_facilityStairsPrevious.TryGetValue(
localId,
out FacilityStairProbeSnapshot previous)
&& current.Admitted != previous.Admitted)
{
Console.WriteLine(
$"[facility-stair-decision] frame={_facilityStairsCurrentFrame} "
+ $"local=0x{localId:X8} cell=0x{current.CellId:X8} "
+ $"admitted={(previous.Admitted ? 1 : 0)}->"
+ $"{(current.Admitted ? 1 : 0)} "
+ $"screen={(current.ScreenIntersect ? 1 : 0)} "
+ $"ndc={current.ScreenBounds}");
}
}
}
Dictionary<uint, FacilityStairProbeSnapshot> swap =
_facilityStairsPrevious;
_facilityStairsPrevious = _facilityStairsCurrent;
_facilityStairsCurrent = swap;
_facilityStairsCurrent.Clear();
_facilityStairsPreviousFrame = _facilityStairsCurrentFrame;
}
_facilityStairsCurrentFrame = frame;
}
private static bool TryProjectFacilityStairBounds(
ObjectRenderData renderData,
Matrix4x4 localToWorld,
Matrix4x4 viewProjection,
out string bounds)
{
Vector3 min = renderData.BoundingBox.Min;
Vector3 max = renderData.BoundingBox.Max;
float minX = float.PositiveInfinity;
float minY = float.PositiveInfinity;
float maxX = float.NegativeInfinity;
float maxY = float.NegativeInfinity;
int projected = 0;
for (int corner = 0; corner < 8; corner++)
{
var local = new Vector3(
(corner & 1) == 0 ? min.X : max.X,
(corner & 2) == 0 ? min.Y : max.Y,
(corner & 4) == 0 ? min.Z : max.Z);
Vector3 world = Vector3.Transform(local, localToWorld);
Vector4 clip = Vector4.Transform(new Vector4(world, 1f), viewProjection);
if (!float.IsFinite(clip.W) || clip.W <= 0.0001f)
continue;
float x = clip.X / clip.W;
float y = clip.Y / clip.W;
if (!float.IsFinite(x) || !float.IsFinite(y))
continue;
minX = MathF.Min(minX, x);
minY = MathF.Min(minY, y);
maxX = MathF.Max(maxX, x);
maxY = MathF.Max(maxY, y);
projected++;
}
if (projected == 0)
{
bounds = "behind";
return false;
}
bounds = $"({minX:F3},{minY:F3})-({maxX:F3},{maxY:F3});corners={projected}";
return maxX >= -1f && minX <= 1f && maxY >= -1f && minY <= 1f;
}
internal static bool LookInDrawingSphereVisible(
IWalkLookInViewSource lookInViews,
int routeIndex,

View file

@ -750,31 +750,6 @@ public sealed partial class WbDrawDispatcher : IDisposable
private bool _disposing;
private bool _disposed;
/// <summary>
/// Per-cell-entity last-log frame number for rate-limiting the
/// [indoor-walk] / [indoor-lookup] / [indoor-xform] / [indoor-cull]
/// probes. Defaults to 30 frames at 30Hz = 1 sec.
/// </summary>
private readonly Dictionary<ulong, int> _lastIndoorProbeFrame = new();
private int _indoorProbeFrameCounter;
private const int IndoorProbeRateLimitFrames = 30;
/// <summary>
/// Returns true at most once per <see cref="IndoorProbeRateLimitFrames"/>
/// frames per cellId. Caller must already have checked that an indoor
/// probe flag is enabled.
/// </summary>
private bool ShouldEmitIndoorProbe(ulong cellId)
{
if (!_lastIndoorProbeFrame.TryGetValue(cellId, out int last)
|| _indoorProbeFrameCounter - last >= IndoorProbeRateLimitFrames)
{
_lastIndoorProbeFrame[cellId] = _indoorProbeFrameCounter;
return true;
}
return false;
}
// Diagnostic counters logged once per ~5s under ACDREAM_WB_DIAG=1.
private int _entitiesSeen;
private int _entitiesDrawn;
@ -947,16 +922,6 @@ public sealed partial class WbDrawDispatcher : IDisposable
/// list. <see cref="Draw"/> reuses a per-dispatcher scratch field across frames to
/// avoid the 480+ KB / frame GC pressure that the test-friendly overload incurs.
/// Returns walk count via <paramref name="result"/>'s <c>EntitiesWalked</c> field.
///
/// <para>
/// When <paramref name="indoorProbeState"/> is non-null the method emits
/// <c>[indoor-cull]</c> lines for cell entities rejected by the
/// visibleCellIds or frustum filters, and <c>[indoor-walk]</c> lines for
/// cell entities that pass all filters. Rate-limited by
/// <see cref="IndoorProbeState"/>. Pass <see langword="null"/> (the default)
/// to disable all probe emission — used by the test-friendly
/// <see cref="WalkEntities"/> overload.
/// </para>
/// </summary>
internal static void WalkEntitiesInto(
IEnumerable<LandblockEntry> landblockEntries,
@ -966,7 +931,6 @@ public sealed partial class WbDrawDispatcher : IDisposable
HashSet<uint>? animatedEntityIds,
List<(WorldEntity Entity, int MeshRefIndex, uint LandblockId)> scratch,
ref WalkResult result,
IndoorProbeState? indoorProbeState = null,
EntitySet set = EntitySet.All)
{
scratch.Clear();
@ -1015,13 +979,6 @@ public sealed partial class WbDrawDispatcher : IDisposable
if (!EntityMatchesSet(entity, set)) continue;
if (entity.MeshRefs.Count == 0) continue;
// Detect cell entity for indoor probes — first MeshRef.GfxObjId
// is an EnvCell id (low 16 bits ≥ 0x0100). Cheap to compute;
// result reused for all probe checks below.
ulong cellProbeId = (ulong)entity.MeshRefs[0].GfxObjId;
bool isCellEntity = indoorProbeState is not null
&& RenderingDiagnostics.IsEnvCellId(cellProbeId);
bool shellScoped = IsShellScopedSet(set)
&& entity.IsBuildingShell
&& visibleCellIds is not null;
@ -1029,14 +986,6 @@ public sealed partial class WbDrawDispatcher : IDisposable
if (!cellInVis)
{
if (shellScoped) result.BuildingShellAnchorReject++;
if (isCellEntity && RenderingDiagnostics.ProbeIndoorCullEnabled
&& indoorProbeState!.ShouldEmit(cellProbeId))
{
Console.WriteLine(
$"[indoor-cull] cellEnt=0x{entity.Id:X8} " +
$"reason=visibleCellIds-miss " +
$"parentCell=0x{entity.ParentCellId!.Value:X8}");
}
continue;
}
if (shellScoped) result.BuildingShellAnchorPass++;
@ -1054,31 +1003,9 @@ public sealed partial class WbDrawDispatcher : IDisposable
if (!aabbVisible)
{
if (isCellEntity && RenderingDiagnostics.ProbeIndoorCullEnabled
&& indoorProbeState!.ShouldEmit(cellProbeId))
{
Console.WriteLine(
$"[indoor-cull] cellEnt=0x{entity.Id:X8} " +
$"reason=frustum " +
$"aabbMin=({entity.AabbMin.X:F1},{entity.AabbMin.Y:F1},{entity.AabbMin.Z:F1}) " +
$"aabbMax=({entity.AabbMax.X:F1},{entity.AabbMax.Y:F1},{entity.AabbMax.Z:F1})");
}
continue;
}
// Passed all filters — emit walk probe.
if (isCellEntity && RenderingDiagnostics.ProbeIndoorWalkEnabled
&& indoorProbeState!.ShouldEmit(cellProbeId))
{
Console.WriteLine(
$"[indoor-walk] cellEnt=0x{entity.Id:X8} " +
$"pos=({entity.Position.X:F1},{entity.Position.Y:F1},{entity.Position.Z:F1}) " +
$"parentCell=0x{(entity.ParentCellId ?? 0u):X8} " +
$"meshRef0=0x{cellProbeId:X8} " +
$"meshRefCount={entity.MeshRefs.Count} " +
$"landblockVisible=true aabbVisible=true cellInVis=true");
}
result.EntitiesWalked++;
for (int i = 0; i < entity.MeshRefs.Count; i++)
scratch.Add((entity, i, entry.LandblockId));
@ -1196,23 +1123,6 @@ public sealed partial class WbDrawDispatcher : IDisposable
// that populates _walkScratch (a per-dispatcher field reused across frames)
// instead of allocating a fresh List<(WorldEntity, int)> per frame.
//
// Pass an IndoorProbeState when any indoor probe is active so the static
// WalkEntitiesInto can emit rate-limited [indoor-cull] / [indoor-walk]
// lines without needing access to instance fields. Null = probes off.
IndoorProbeState? probeState = null;
if (RenderingDiagnostics.ProbeIndoorCullEnabled || RenderingDiagnostics.ProbeIndoorWalkEnabled)
{
// _currentFrame is snapped at construction time. Construct
// once per Draw() call only — a second construction within
// the same frame would stamp the dictionary with the
// (already-advanced) counter value, suppressing the second
// pass's emissions for IndoorProbeRateLimitFrames frames.
// Today Draw() is called exactly once per frame; if a
// future refactor adds a shadow / reflection / second pass,
// this assumption needs revisiting.
probeState = new IndoorProbeState(_lastIndoorProbeFrame, _indoorProbeFrameCounter);
}
var walkResult = default(WalkResult);
WalkEntitiesInto(
ToEntries(landblockEntries),
@ -1222,7 +1132,6 @@ public sealed partial class WbDrawDispatcher : IDisposable
animatedEntityIds,
_walkScratch,
ref walkResult,
probeState,
set);
_currentRenderSceneObserver?.ObserveDispatcherDraw(
set,
@ -1358,14 +1267,6 @@ public sealed partial class WbDrawDispatcher : IDisposable
? new Vector2(lighting.Luminosity, lighting.Diffuse)
: new Vector2(0f, 1f);
// #176 seam-draw probe: any entity parented to a target cell reports
// its position + light set (a floor-coincident static/plate would be
// the z-fight's second draw; the player entity is the positive
// control). Before the culled-continue, like the dump above.
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeSeamDrawEnabled
&& entity.ParentCell is { } seamPc
&& AcDream.Core.Rendering.RenderingDiagnostics.SeamDrawTargetCells.Contains(seamPc))
MaybeEmitSeamEnt(entity);
}
prevTupleEntityId = entity.Id;
@ -1468,41 +1369,6 @@ public sealed partial class WbDrawDispatcher : IDisposable
var renderData = _meshAdapter.TryGetRenderData(gfxObjId);
// [indoor-lookup] probe — emit once per cell entity per sec.
// Fires BEFORE the null-renderData early-continue so a miss still
// emits hit=false, distinguishing H2 (empty batches) from H6
// (dispatcher fails to traverse Setup).
ulong lookupCellId = (ulong)gfxObjId;
if (RenderingDiagnostics.IsEnvCellId(lookupCellId)
&& RenderingDiagnostics.ProbeIndoorLookupEnabled
// Rate-limit in a separate namespace from [indoor-walk]/[indoor-cull]
// (which key on the same gfxObjId). Without this, IndoorAll=1 would
// silence the lookup probe whenever the walk probe fired first.
&& ShouldEmitIndoorProbe(lookupCellId | 0x8000_0000_0000_0000UL))
{
bool hit = renderData is not null;
bool isSetup = hit && renderData!.IsSetup;
int partCount = isSetup ? renderData!.SetupParts.Count : 0;
int partsHit = 0, partsMiss = 0;
if (isSetup)
{
foreach (var (partId, _) in renderData!.SetupParts)
{
if (_meshAdapter.TryGetRenderData(partId) is not null) partsHit++;
else partsMiss++;
}
}
bool hasEnvCellGeom = isSetup
&& renderData!.SetupParts.Exists(t => (t.GfxObjId & 0x1_0000_0000UL) != 0);
Console.WriteLine(
$"[indoor-lookup] cellId=0x{lookupCellId:X8} " +
$"hit={hit} isSetup={isSetup} partCount={partCount} " +
$"hasEnvCellGeom={hasEnvCellGeom} partsHit={partsHit} partsMiss={partsMiss}");
}
if (renderData is null)
{
// Tier 1 cache (#53): mesh data is still async-decoding via
@ -1595,23 +1461,6 @@ public sealed partial class WbDrawDispatcher : IDisposable
var model = ComposePartWorldMatrix(
entityWorld, meshRef.PartTransform, partTransform);
// [indoor-xform] probe — only for the cell's synthetic
// geometry part (bit 32 set, per WB's PrepareEnvCellMeshData
// cellGeomId convention). One line per part per sec.
// Disambiguates hypothesis H5 (transform double-apply —
// composedT lands at 2 × cellOrigin).
if ((partGfxObjId & 0x1_0000_0000UL) != 0
&& RenderingDiagnostics.ProbeIndoorXformEnabled
&& ShouldEmitIndoorProbe(partGfxObjId))
{
Console.WriteLine(
$"[indoor-xform] cellGeomId=0x{partGfxObjId:X16} " +
$"entityWorldT=({entityWorld.Translation.X:F2},{entityWorld.Translation.Y:F2},{entityWorld.Translation.Z:F2}) " +
$"meshRefT=({meshRef.PartTransform.Translation.X:F2},{meshRef.PartTransform.Translation.Y:F2},{meshRef.PartTransform.Translation.Z:F2}) " +
$"partT=({partTransform.Translation.X:F2},{partTransform.Translation.Y:F2},{partTransform.Translation.Z:F2}) " +
$"composedT=({model.Translation.X:F2},{model.Translation.Y:F2},{model.Translation.Z:F2})");
}
var restPose = partTransform * meshRef.PartTransform;
// #188 retail CPhysicsPart::Draw (0x0050d7a0) early-out: once a
@ -1718,11 +1567,6 @@ public sealed partial class WbDrawDispatcher : IDisposable
populateEntityId, populateLandblockId, _cache,
_populateScratch, _populateSelectionScratch);
// S3 review fix round 1 (F5): the §4 flap [clip-route-disp] probe
// (2026-06-10, throwaway) is deleted with the routing state it
// reported on — it could only ever print "cullEnt=0" now that
// ResolveSlotForFrame never culls anything.
ExecuteClassifiedGroups(
vp,
camPos,
@ -1777,7 +1621,6 @@ public sealed partial class WbDrawDispatcher : IDisposable
out Vector3 cameraWorldPosition)
{
_selectionLighting?.TickLighting();
_indoorProbeFrameCounter++;
viewProjection = camera.View * camera.Projection;
_missRequested.Clear();
@ -3085,44 +2928,6 @@ public sealed partial class WbDrawDispatcher : IDisposable
/// (AP-43) and docs/research/2026-06-19-lighting-a7-fixD-round2-*.
/// </para>
/// </summary>
// #176 seam-draw probe (ACDREAM_PROBE_SEAMDRAW) — throwaway apparatus. One
// [seam-ent] line per target-cell entity, re-emitted on state change: world
// position (F3 z — entities do NOT get the +0.02 shell lift), cull/slot,
// and the SelectForObject light set resolved to identities (owner-cell
// low16 + intensity). Sig dict is bounded by the handful of entities that
// ever live in the target cells.
private readonly Dictionary<ulong, string> _seamEntSigs = new();
private void MaybeEmitSeamEnt(in RenderInstanceCandidate entity)
{
var ci = System.Globalization.CultureInfo.InvariantCulture;
var snap = _pointSnapshot;
var sb = new System.Text.StringBuilder(200);
sb.AppendFormat(ci,
"guid=0x{0:X8} cell=0x{1:X8} pos=({2:F2},{3:F2},{4:F3}) culled={5} slot={6} indoor={7} L=[",
entity.ServerGuid, entity.ParentCellId,
entity.Position.X, entity.Position.Y, entity.Position.Z,
_currentEntityCulled ? 1 : 0, _currentEntitySlot, _currentEntityIndoor ? 1 : 0);
bool any = false;
for (int k = 0; k < LightManager.MaxLightsPerObject; k++)
{
int idx = _currentEntityLightSet[k];
if (idx < 0) continue;
if (any) sb.Append(',');
if (snap is not null && idx < snap.Count)
sb.AppendFormat(ci, "{0:X4}:I{1:F0}", snap[idx].CellId & 0xFFFFu, snap[idx].Intensity);
else
sb.Append('?').Append(idx);
any = true;
}
sb.Append(']');
string sig = sb.ToString();
if (_seamEntSigs.TryGetValue(entity.Id, out var prev) && prev == sig) return;
_seamEntSigs[entity.Id] = sig;
Console.WriteLine($"[seam-ent] t={Environment.TickCount64} {sig}");
}
private void ComputeEntityLightSet(
in RenderInstanceCandidate entity)
{
@ -3632,41 +3437,6 @@ public sealed partial class WbDrawDispatcher : IDisposable
// ────────────────────────────────────────────────────────────────────────
/// <summary>
/// Thin wrapper around an instance's rate-limit dictionary + frame
/// counter, passed into the static <see cref="WalkEntitiesInto"/>
/// overload so it can emit rate-limited probe lines without access
/// to instance fields. Null = probes disabled (test-friendly overload).
/// </summary>
internal sealed class IndoorProbeState
{
private readonly Dictionary<ulong, int> _lastFrame;
private readonly int _currentFrame;
private const int RateLimit = IndoorProbeRateLimitFrames;
internal IndoorProbeState(Dictionary<ulong, int> lastFrame, int currentFrame)
{
_lastFrame = lastFrame;
_currentFrame = currentFrame;
}
/// <summary>
/// Returns true at most once per <see cref="IndoorProbeRateLimitFrames"/>
/// frames per <paramref name="cellId"/>. Side-effect: stamps the frame
/// number into the dictionary on success.
/// </summary>
internal bool ShouldEmit(ulong cellId)
{
if (!_lastFrame.TryGetValue(cellId, out int last)
|| _currentFrame - last >= RateLimit)
{
_lastFrame[cellId] = _currentFrame;
return true;
}
return false;
}
}
internal sealed class InstanceGroup
{
// Nonzero only while this exact object is registered in _groups.

View file

@ -13,7 +13,6 @@ internal interface IRenderFrameGlState
{
void RestoreFrameDefaults();
}
/// <summary>
/// Campaign V slice V6j: the small graphics surface the two world pass executors
/// touch directly, expressed once so their ordering logic — which is retail's,
@ -186,25 +185,3 @@ internal sealed class NullRenderFrameGlState : IRenderFrameGlState
{
}
}
/// <summary>
/// Campaign V slice V6j: the GL state reader on a backend with no GL state.
///
/// <para><c>WorldRenderDiagnostics</c> reads live GL state for explicitly enabled
/// probes only. Every snapshot below is the truthful answer for a Vulkan frame —
/// there is no ambient capability state to sample — which keeps every other
/// diagnostic the class emits (render signature, PView input, out-stage routing,
/// phantom objects) working unchanged on both backends.</para>
/// </summary>
internal sealed class NullRenderGlStateReader : IRenderGlStateReader
{
public static NullRenderGlStateReader Instance { get; } = new();
private NullRenderGlStateReader()
{
}
public RenderGlStateSnapshot CaptureState() => default;
public RenderGlScissorSnapshot CaptureScissor() => default;
}

View file

@ -1,79 +1,22 @@
using System.Numerics;
using System.Diagnostics;
using System.Text;
using AcDream.Core.Vfx;
using AcDream.Core.World;
namespace AcDream.App.Rendering;
internal readonly record struct IntRenderRectangle(int X, int Y, int Width, int Height);
internal readonly record struct RenderGlStateSnapshot(
bool DepthTest,
bool DepthWrite,
int DepthFunction,
bool Blend,
int BlendSource,
int BlendDestination,
bool CullFace,
int CullMode,
int FrontFace,
bool Scissor,
IntRenderRectangle ScissorBox,
IntRenderRectangle Viewport,
int DrawFramebuffer,
bool AlphaToCoverage,
bool Stencil,
int ClipBits,
int Error);
internal readonly record struct RenderGlScissorSnapshot(
bool Enabled,
IntRenderRectangle Box);
internal readonly record struct TerrainRenderDiagnosticFacts(
int VisibleSlots,
int Draws,
int LoadedSlots,
int CapacitySlots);
internal interface IRenderGlStateReader
{
RenderGlStateSnapshot CaptureState();
RenderGlScissorSnapshot CaptureScissor();
}
/// <summary>
/// Owns print-on-change world-render probes and their reusable scratch. Inputs
/// are borrowed for one call; the owner retains only copied signatures and IDs.
/// </summary>
/// <summary>Owns permanent terrain timing and diagnostic publication.</summary>
internal sealed class WorldRenderDiagnostics
{
private readonly IRenderGlStateReader _gl;
private readonly IRenderFrameDiagnosticLog _log;
private readonly Stopwatch _terrainStopwatch = new();
private readonly RollingTimingSampleWindow _terrainSamples = new(256);
private string? _lastRenderSignature;
private int _renderSignatureFrame;
private int _renderSignatureStableFrames;
private string? _lastGlStateSignature;
private long _glStateFrame;
private long _glStateStableFrames;
private string? _lastPostWorldGlStateSignature;
private long _postWorldGlStateFrame;
private long _postWorldGlStateStableFrames;
private string? _lastScissorSignature;
private long _scissorSequence;
private string? _lastClipRouteSignature;
private long _clipRouteSequence;
private readonly List<uint> _clipRouteCellKeys = [];
public WorldRenderDiagnostics(
IRenderGlStateReader gl,
IRenderFrameDiagnosticLog log)
public WorldRenderDiagnostics(IRenderFrameDiagnosticLog log)
{
_gl = gl ?? throw new ArgumentNullException(nameof(gl));
_log = log ?? throw new ArgumentNullException(nameof(log));
}
@ -86,12 +29,6 @@ internal sealed class WorldRenderDiagnostics
(long)(_terrainStopwatch.Elapsed.TotalMicroseconds * 100.0));
}
/// <summary>S3 chunk 3 fix round 1 (F3): the walk path's per-frame
/// analogue of <see cref="EndTerrainDraw"/> — pushes ONE precomputed
/// elapsed-time sample (the sum of every land-cell batch's own
/// Stopwatch.GetTimestamp() delta this frame, converted by the caller)
/// instead of stopping this owner's own stopwatch, since the walk path
/// times many small batches per frame rather than one bracketed call.</summary>
public void PushTerrainSample(long elapsedHundredthsMicroseconds) =>
_terrainSamples.PushHundredthsMicroseconds(elapsedHundredthsMicroseconds);
@ -107,442 +44,4 @@ internal sealed class WorldRenderDiagnostics
+ $"visible={facts.VisibleSlots} loaded={facts.LoadedSlots} "
+ $"capacity={facts.CapacitySlots}");
}
public void EmitGlStateTripwireIfChanged(bool enabled)
{
if (!enabled)
return;
_glStateFrame++;
string signature = FormatGlState(_gl.CaptureState());
if (signature == _lastGlStateSignature)
{
_glStateStableFrames++;
return;
}
_log.WriteLine(
$"[gl-state] frame={_glStateFrame} stable={_glStateStableFrames} {signature}");
_lastGlStateSignature = signature;
_glStateStableFrames = 0;
}
/// <summary>
/// Second sample of the same snapshot, taken at the END of the normal-world
/// phase instead of at the frame clear.
/// </summary>
/// <remarks>
/// <see cref="EmitGlStateTripwireIfChanged"/> samples immediately after the
/// clear phase has run <c>RestoreFrameDefaults</c>, so it can only observe
/// state that survives from one frame into the next. State that a world pass
/// establishes and something in private presentation puts back before the
/// next clear is invisible to it — including the draw framebuffer, which no
/// frame-global restore touches. Sampling here as well brackets the world
/// phase, so a binding that the world's geometry drew into but the retained
/// UI did not shows up as a difference between the two lines rather than as
/// no line at all.
/// </remarks>
public void EmitPostWorldGlStateIfChanged(bool enabled)
{
if (!enabled)
return;
_postWorldGlStateFrame++;
string signature = FormatGlState(_gl.CaptureState());
if (signature == _lastPostWorldGlStateSignature)
{
_postWorldGlStateStableFrames++;
return;
}
_log.WriteLine(
$"[gl-state-postworld] frame={_postWorldGlStateFrame} "
+ $"stable={_postWorldGlStateStableFrames} {signature}");
_lastPostWorldGlStateSignature = signature;
_postWorldGlStateStableFrames = 0;
}
public void EmitClipRouteScissorProbe(
bool enabled,
bool applied,
Vector4 ndcAabb)
{
if (!enabled)
return;
RenderGlScissorSnapshot snapshot = _gl.CaptureScissor();
string signature = FormattableString.Invariant(
$"applied={(applied ? 1 : 0)} scis={(snapshot.Enabled ? 1 : 0)} box=({snapshot.Box.X},{snapshot.Box.Y},{snapshot.Box.Width},{snapshot.Box.Height}) ndc=({ndcAabb.X:F3},{ndcAabb.Y:F3},{ndcAabb.Z:F3},{ndcAabb.W:F3})");
_scissorSequence++;
if (signature == _lastScissorSignature)
return;
_lastScissorSignature = signature;
_log.WriteLine($"[clip-route-scis] n={_scissorSequence} {signature}");
}
public void EmitClipRouteProbe(
bool enabled,
ClipFrame clipFrame,
ClipFrameAssembly clipAssembly,
ClipViewSlice slice,
int sliceIndex)
{
if (!enabled)
return;
var text = new StringBuilder(256);
text.Append(FormattableString.Invariant(
$"slice={sliceIndex}/{clipAssembly.OutsideViewSlices.Length} slot={slice.Slot}"));
text.Append(FormattableString.Invariant(
$" ndc=({slice.NdcAabb.X:F3},{slice.NdcAabb.Y:F3},{slice.NdcAabb.Z:F3},{slice.NdcAabb.W:F3})"));
text.Append(FormattableString.Invariant($" planes={slice.Planes.Length}["));
for (int i = 0; i < slice.Planes.Length; i++)
{
Vector4 plane = slice.Planes[i];
if (i > 0)
text.Append(' ');
text.Append(FormattableString.Invariant(
$"({plane.X:F3},{plane.Y:F3},{plane.Z:F3},{plane.W:F3})"));
}
text.Append("] cells={");
_clipRouteCellKeys.Clear();
foreach (uint key in clipAssembly.CellIdToSlot.Keys)
_clipRouteCellKeys.Add(key);
_clipRouteCellKeys.Sort();
for (int i = 0; i < _clipRouteCellKeys.Count; i++)
{
if (i > 0)
text.Append(',');
text.Append(FormattableString.Invariant(
$"0x{_clipRouteCellKeys[i]:X8}:{clipAssembly.CellIdToSlot[_clipRouteCellKeys[i]]}"));
}
text.Append('}');
ReadOnlySpan<byte> regionBytes = clipFrame.RegionBytesForTest;
int offset = slice.Slot * ClipFrame.CellClipStrideBytes;
if (offset >= 0 && offset + ClipFrame.CellClipStrideBytes <= regionBytes.Length)
{
uint count = BitConverter.ToUInt32(regionBytes.Slice(offset, 4));
text.Append(FormattableString.Invariant($" ssbo[{slice.Slot}]: n={count}"));
int planeCount = (int)Math.Min(count, (uint)ClipFrame.MaxPlanes);
for (int i = 0; i < planeCount; i++)
{
int planeOffset = offset + ClipFrame.CellClipPlanesOffset + i * 16;
float x = BitConverter.ToSingle(regionBytes.Slice(planeOffset, 4));
float y = BitConverter.ToSingle(regionBytes.Slice(planeOffset + 4, 4));
float z = BitConverter.ToSingle(regionBytes.Slice(planeOffset + 8, 4));
float w = BitConverter.ToSingle(regionBytes.Slice(planeOffset + 12, 4));
text.Append(FormattableString.Invariant($" ({x:F3},{y:F3},{z:F3},{w:F3})"));
}
}
else
{
text.Append(FormattableString.Invariant(
$" ssbo[{slice.Slot}]: OUT-OF-RANGE len={regionBytes.Length}"));
}
// S3 chunk 4 fix round 2 (L3): the "ubo: n=... p0=..." segment this
// used to append read ClipFrame's own screen-space clip gate bytes,
// deleted along with that gate — no shader declares the block any
// more, so there is nothing left here to read.
string signature = text.ToString();
_clipRouteSequence++;
if (signature == _lastClipRouteSignature)
return;
_lastClipRouteSignature = signature;
_log.WriteLine($"[clip-route] n={_clipRouteSequence} {signature}");
}
public void EmitSeamMask(
bool enabled,
IReadOnlySet<uint> targetCells,
uint cellId,
int portalIndex,
bool forceFarZ,
ReadOnlySpan<Vector3> vertices)
{
if (!enabled || !targetCells.Contains(cellId))
return;
float minimumZ = float.MaxValue;
float maximumZ = float.MinValue;
foreach (Vector3 vertex in vertices)
{
minimumZ = Math.Min(minimumZ, vertex.Z);
maximumZ = Math.Max(maximumZ, vertex.Z);
}
_log.WriteLine(FormattableString.Invariant(
$"[seam-mask] t={Environment.TickCount64} cell=0x{cellId:X8} portal={portalIndex} far={forceFarZ} n={vertices.Length} z=[{minimumZ:F3},{maximumZ:F3}]"));
}
public void EmitPViewInput(
bool enabled,
IReadOnlySet<uint> visibleCells,
int outsideViewCount,
Matrix4x4 viewProjection,
bool outdoorRoot,
Vector3 eye,
Vector3 player,
Vector3 rawPlayer,
float yaw,
float? terrainHeight)
{
if (!enabled)
return;
string terrain = terrainHeight is { } height
? FormattableString.Invariant(
$"terrZ={height:F3} eyeAbove={eye.Z - height:F3}")
: "terrZ=n/a eyeAbove=n/a";
char root = outdoorRoot ? 'Y' : 'n';
Matrix4x4 vp = viewProjection;
_log.WriteLine(FormattableString.Invariant(
$"[pv-input] outRoot={root} visible={visibleCells.Count} outsideViews={outsideViewCount} eye=({eye.X:F6},{eye.Y:F6},{eye.Z:F6}) player=({player.X:F6},{player.Y:F6},{player.Z:F6}) rawPlayer=({rawPlayer.X:F6},{rawPlayer.Y:F6},{rawPlayer.Z:F6}) yaw={yaw:F8} {terrain} vp=[{vp.M11:F6} {vp.M13:F6} {vp.M22:F6} {vp.M31:F6} {vp.M33:F6} {vp.M41:F6} {vp.M42:F6} {vp.M43:F6}]"));
}
public void EmitRetailPViewDiagnostics(
bool visibilityEnabled,
bool flapEnabled,
RetailPViewFrameResult result,
LoadedCell clipRoot,
uint viewerCellId,
uint playerCellId,
Vector3 cameraPosition,
Vector3 playerPosition,
CameraCellResolution cameraCellResolution)
{
if (visibilityEnabled)
{
AcDream.Core.Rendering.RenderingDiagnostics.EmitVis(
clipRoot.CellId,
result.VisibleCells.OrderBy(static id => id).ToArray(),
result.ClipAssembly.OutsideViewSlices.Length,
result.ClipAssembly.OutsidePlaneCount,
result.ClipAssembly.PerCellPlaneCounts,
result.ClipAssembly.ScissorFallbacks);
}
if (flapEnabled)
{
bool eyeInRoot = CellVisibility.PointInCell(cameraPosition, clipRoot);
bool playerInRoot = CellVisibility.PointInCell(playerPosition, clipRoot);
_log.WriteLine(
$"[flap-cam] root=0x{clipRoot.CellId:X8} "
+ $"viewerCell=0x{viewerCellId:X8} playerCell=0x{playerCellId:X8} "
+ $"res={cameraCellResolution} "
+ $"eyeInRoot={(eyeInRoot ? "Y" : "n")} "
+ $"playerInRoot={(playerInRoot ? "Y" : "n")} "
+ $"eye=({cameraPosition.X:F2},{cameraPosition.Y:F2},{cameraPosition.Z:F2}) "
+ $"player=({playerPosition.X:F2},{playerPosition.Y:F2},{playerPosition.Z:F2}) "
// S3 chunk 4 fix round 2 (L2): the "terrain=" field read
// ClipFrameAssembly.TerrainMode, deleted along with the
// walk's own terrain-clip-mode tracking (the walk draws
// terrain unclipped, one call, matching retail — there is no
// longer a mode to report here).
+ $"outVisible={result.ClipAssembly.OutdoorVisible}");
}
}
public void EmitRenderSignatureIfChanged(
bool enabled,
string branch,
LoadedCell? clipRoot,
LoadedCell? viewerRoot,
LoadedCell? playerRoot,
uint viewerCellId,
uint playerCellId,
bool playerIndoorGate,
bool cameraInsideCell,
bool renderSkyGate,
bool drawSkyThisFrame,
bool terrainDrawn,
TerrainClipMode terrainClipMode,
bool skyDrawn,
bool depthClear,
bool outdoorSceneryDrawn,
int liveDynamicDrawnCount,
string sceneParticles,
IReadOnlySet<uint>? visibleCells,
ClipFrameAssembly? clipAssembly,
IReadOnlySet<uint>? drawableCells,
InteriorEntityPartition.Result? partition,
Vector3 cameraPosition,
Vector3 playerPosition)
{
if (!enabled)
return;
_renderSignatureFrame++;
bool eyeInRoot = clipRoot is not null
&& CellVisibility.PointInCell(cameraPosition, clipRoot);
bool playerInRoot = clipRoot is not null
&& CellVisibility.PointInCell(playerPosition, clipRoot);
var text = new StringBuilder(512);
text.Append("branch=").Append(branch);
text.Append(" root=0x").Append((clipRoot?.CellId ?? 0u).ToString("X8"));
text.Append(" viewerRoot=0x").Append((viewerRoot?.CellId ?? 0u).ToString("X8"));
text.Append(" playerRoot=0x").Append((playerRoot?.CellId ?? 0u).ToString("X8"));
text.Append(" viewerCell=0x").Append(viewerCellId.ToString("X8"));
text.Append(" playerCell=0x").Append(playerCellId.ToString("X8"));
text.Append(" gate=").Append(playerIndoorGate ? "in" : "out");
text.Append(" camIn=").Append(cameraInsideCell ? 'Y' : 'n');
text.Append(" eyeInRoot=").Append(eyeInRoot ? 'Y' : 'n');
text.Append(" playerInRoot=").Append(playerInRoot ? 'Y' : 'n');
text.Append(" eye=").Append(FormatVector(cameraPosition));
text.Append(" player=").Append(FormatVector(playerPosition));
text.Append(" terrain=").Append(terrainClipMode);
text.Append('/').Append(terrainDrawn ? "draw" : "skip");
text.Append(" skyGate=").Append(renderSkyGate ? 'Y' : 'n');
text.Append(" sky=").Append(skyDrawn ? 'Y' : 'n');
text.Append(" skyFrame=").Append(drawSkyThisFrame ? 'Y' : 'n');
text.Append(" zclear=").Append(depthClear ? 'Y' : 'n');
text.Append(" sceneParticles=").Append(sceneParticles);
// S3 chunk 4 fix round 2 (L2): "outMode=" read
// ClipFrameAssembly.TerrainMode, deleted with the walk's own
// terrain-clip-mode tracking.
if (clipAssembly is not null)
{
text.Append(" outSlices=").Append(clipAssembly.OutsideViewSlices.Length);
text.Append(" outPolys=").Append(clipAssembly.OutsideViewSlices.Length);
}
else
{
text.Append(" outSlices=0 outPolys=0");
}
text.Append(" ids=").Append(FormatIds(visibleCells, false));
text.Append(" draw=").Append(FormatIds(drawableCells, false));
text.Append(" miss=").Append(FormatMissingDrawableCells(visibleCells, drawableCells));
text.Append(" obj=").Append(FormatPartitionCounts(partition));
text.Append(" outdoorDoor=").Append(outdoorSceneryDrawn ? 'Y' : 'n');
text.Append(" liveDynDraw=").Append(liveDynamicDrawnCount);
text.Append(" outRoot=").Append(clipRoot is { IsOutdoorNode: true } ? 'Y' : 'n');
if (partition is not null)
{
int totalShells = 0;
int shellsWithMeshes = 0;
foreach (var entity in partition.OutdoorStatic)
{
if (!entity.IsBuildingShell)
continue;
totalShells++;
if (entity.MeshRefs.Count > 0)
shellsWithMeshes++;
}
text.Append(" bshell=").Append(totalShells).Append('/').Append(shellsWithMeshes);
}
string signature = text.ToString();
if (signature == _lastRenderSignature)
{
_renderSignatureStableFrames++;
return;
}
_log.WriteLine(
$"[render-sig] frame={_renderSignatureFrame} "
+ $"stable={_renderSignatureStableFrames} {signature}");
_lastRenderSignature = signature;
_renderSignatureStableFrames = 0;
}
internal static string FormatGlState(RenderGlStateSnapshot state) =>
$"depth={(state.DepthTest ? 1 : 0)} "
+ $"dmask={(state.DepthWrite ? 1 : 0)} "
+ $"dfunc=0x{state.DepthFunction:X} "
+ $"blend={(state.Blend ? 1 : 0)} "
+ $"bsrc=0x{state.BlendSource:X} bdst=0x{state.BlendDestination:X} "
+ $"cull={(state.CullFace ? 1 : 0)} cmode=0x{state.CullMode:X} "
+ $"fface=0x{state.FrontFace:X} "
+ $"scis={(state.Scissor ? 1 : 0)} "
+ $"sbox=({state.ScissorBox.X},{state.ScissorBox.Y},"
+ $"{state.ScissorBox.Width},{state.ScissorBox.Height}) "
+ $"vp=({state.Viewport.X},{state.Viewport.Y},"
+ $"{state.Viewport.Width},{state.Viewport.Height}) "
+ $"fbo={state.DrawFramebuffer} "
+ $"a2c={(state.AlphaToCoverage ? 1 : 0)} "
+ $"stencil={(state.Stencil ? 1 : 0)} "
+ $"clip=0x{state.ClipBits:X2} err=0x{state.Error:X}";
private static string FormatVector(Vector3 value)
{
static float Quantize(float component) => MathF.Round(component * 20f) / 20f;
return $"({Quantize(value.X):F2},{Quantize(value.Y):F2},{Quantize(value.Z):F2})";
}
private static string FormatIds(IEnumerable<uint>? ids, bool preserveOrder)
{
if (ids is null)
return "[]";
var values = new List<uint>(ids);
if (!preserveOrder)
values.Sort();
var text = new StringBuilder(96).Append('[');
const int MaximumIds = 12;
for (int index = 0; index < values.Count && index < MaximumIds; index++)
{
if (index > 0)
text.Append(',');
text.Append("0x").Append(values[index].ToString("X8"));
}
if (values.Count > MaximumIds)
text.Append(",...");
return text.Append(']').ToString();
}
private static string FormatMissingDrawableCells(
IReadOnlySet<uint>? visibleCells,
IReadOnlySet<uint>? drawableCells)
{
if (visibleCells is null || drawableCells is null)
return "[]";
var text = new StringBuilder(96).Append('[');
int written = 0;
const int MaximumCells = 8;
foreach (uint id in visibleCells.OrderBy(static id => id))
{
if (drawableCells.Contains(id))
continue;
if (written > 0)
text.Append(',');
text.Append("0x").Append(id.ToString("X8"));
if (++written >= MaximumCells)
{
text.Append(",...");
break;
}
}
return text.Append(']').ToString();
}
private static string FormatPartitionCounts(InteriorEntityPartition.Result? partition)
{
if (partition is null)
return "cell=[] out=0 live=0";
var keys = new List<uint>(partition.ByCell.Keys);
keys.Sort();
var text = new StringBuilder(128).Append("cell=[");
const int MaximumCells = 10;
for (int index = 0; index < keys.Count && index < MaximumCells; index++)
{
uint id = keys[index];
if (index > 0)
text.Append(',');
text.Append("0x").Append(id.ToString("X8"))
.Append(':').Append(partition.ByCell[id].Count);
}
if (keys.Count > MaximumCells)
text.Append(",...");
return text.Append("] out=").Append(partition.OutdoorStatic.Count)
.Append(" live=").Append(partition.Dynamics.Count)
.ToString();
}
}

View file

@ -718,11 +718,6 @@ internal sealed class RuntimeWorldFrameBuildingSource : IWorldFrameBuildingSourc
if (viewerRoot is null)
outdoorNode = OutdoorCellNode.Build(viewerCellId);
if (RenderingDiagnostics.ProbeFlapEnabled)
{
Console.WriteLine(FormattableString.Invariant(
$"[outdoor-node] cell=0x{viewerCellId:X8} root={(viewerRoot is null ? "OUT" : "IN")} nearbyCells={_scratch.Count} (T2 frustum-gated per-building floods)"));
}
return new WorldBuildingFrame(outdoorNode, _scratch);
}

View file

@ -13,36 +13,6 @@ internal interface IWorldSceneDiagnostics
{
CameraCellResolution CameraCellResolution { get; }
void EmitPViewInput(
IReadOnlySet<uint> visibleCells,
int outsideViewCount,
Matrix4x4 viewProjection,
LoadedCell clipRoot,
Vector3 cameraPosition,
Vector3 playerPosition);
void EmitRenderSignature(
string branch,
LoadedCell? clipRoot,
LoadedCell? viewerRoot,
LoadedCell? playerRoot,
uint viewerCellId,
uint playerCellId,
bool playerIndoorGate,
bool cameraInsideCell,
bool renderSky,
bool drawSkyThisFrame,
bool terrainDrawn,
TerrainClipMode terrainClipMode,
bool skyDrawn,
bool depthClear,
bool outdoorSceneryDrawn,
int liveDynamicDrawnCount,
string sceneParticles,
RetailPViewFrameResult? pviewResult,
Vector3 cameraPosition,
Vector3 playerPosition);
WorldSceneDiagnosticOutcome DrawAndPublish(
in WorldCameraFrame camera,
IReadOnlyList<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax)> bounds);
@ -55,7 +25,6 @@ internal interface IWorldSceneDiagnostics
/// </summary>
internal sealed class WorldSceneDiagnosticsController : IWorldSceneDiagnostics
{
private readonly WorldRenderDiagnostics _diagnostics;
private readonly IWorldScenePViewDiagnosticSource _pview;
private readonly IWorldSceneDebugStateSource _state;
private readonly DebugLineRenderer? _lines;
@ -67,7 +36,6 @@ internal sealed class WorldSceneDiagnosticsController : IWorldSceneDiagnostics
private int _debugDrawLogCount;
public WorldSceneDiagnosticsController(
WorldRenderDiagnostics diagnostics,
IWorldScenePViewDiagnosticSource pview,
IWorldSceneDebugStateSource state,
DebugLineRenderer? lines,
@ -77,7 +45,6 @@ internal sealed class WorldSceneDiagnosticsController : IWorldSceneDiagnostics
DebugVmRenderFactsPublisher debugVm,
bool debugVmConsumerActive)
{
_diagnostics = diagnostics ?? throw new ArgumentNullException(nameof(diagnostics));
_pview = pview ?? throw new ArgumentNullException(nameof(pview));
_state = state ?? throw new ArgumentNullException(nameof(state));
_lines = lines;
@ -90,89 +57,11 @@ internal sealed class WorldSceneDiagnosticsController : IWorldSceneDiagnostics
public CameraCellResolution CameraCellResolution => _pview.CameraCellResolution;
public void EmitPViewInput(
IReadOnlySet<uint> visibleCells,
int outsideViewCount,
Matrix4x4 viewProjection,
LoadedCell clipRoot,
Vector3 cameraPosition,
Vector3 playerPosition)
{
if (!RenderingDiagnostics.ProbePvInputEnabled)
return;
_diagnostics.EmitPViewInput(
enabled: true,
visibleCells,
outsideViewCount,
viewProjection,
clipRoot.IsOutdoorNode,
cameraPosition,
playerPosition,
_pview.RawPlayerPositionOr(playerPosition),
_pview.PlayerYaw,
_pview.SampleTerrainZ(cameraPosition.X, cameraPosition.Y));
}
public void EmitRenderSignature(
string branch,
LoadedCell? clipRoot,
LoadedCell? viewerRoot,
LoadedCell? playerRoot,
uint viewerCellId,
uint playerCellId,
bool playerIndoorGate,
bool cameraInsideCell,
bool renderSky,
bool drawSkyThisFrame,
bool terrainDrawn,
TerrainClipMode terrainClipMode,
bool skyDrawn,
bool depthClear,
bool outdoorSceneryDrawn,
int liveDynamicDrawnCount,
string sceneParticles,
RetailPViewFrameResult? pviewResult,
Vector3 cameraPosition,
Vector3 playerPosition)
{
_diagnostics.EmitRenderSignatureIfChanged(
RenderingDiagnostics.ProbeFlapEnabled,
branch,
clipRoot,
viewerRoot,
playerRoot,
viewerCellId,
playerCellId,
playerIndoorGate,
cameraInsideCell,
renderSky,
drawSkyThisFrame,
terrainDrawn,
terrainClipMode,
skyDrawn,
depthClear,
outdoorSceneryDrawn,
liveDynamicDrawnCount,
sceneParticles,
pviewResult?.VisibleCells,
pviewResult?.ClipAssembly,
pviewResult?.DrawableCells,
pviewResult?.DiagnosticPartition,
cameraPosition,
playerPosition);
}
public WorldSceneDiagnosticOutcome DrawAndPublish(
in WorldCameraFrame camera,
IReadOnlyList<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax)> bounds)
{
ArgumentNullException.ThrowIfNull(bounds);
// Brackets the normal-world phase against the clear-phase tripwire: any
// GL state the world's geometry drew under but the next frame's clear
// no longer sees shows up as a difference between the two lines.
_diagnostics.EmitPostWorldGlStateIfChanged(
AcDream.Core.Rendering.RenderingDiagnostics.ProbeGlStateEnabled);
DrawCollisionWireframes(in camera);
int visible = 0;

View file

@ -29,11 +29,10 @@ internal interface IWorldScenePassExecutor
uint? playerLandblockId,
HashSet<uint> animatedEntityIds);
string DrawPostWorldParticles(
void DrawPostWorldParticles(
LoadedCell? clipRoot,
ClipFrameAssembly? clipAssembly,
in WorldCameraFrame camera,
string currentSignature);
in WorldCameraFrame camera);
void DrawFlatWeather(
in WorldCameraFrame camera,
@ -182,14 +181,13 @@ internal sealed class WorldScenePassExecutor : IWorldScenePassExecutor
visibleCellIds: null,
animatedEntityIds: animatedEntityIds);
public string DrawPostWorldParticles(
public void DrawPostWorldParticles(
LoadedCell? clipRoot,
ClipFrameAssembly? clipAssembly,
in WorldCameraFrame camera,
string currentSignature)
in WorldCameraFrame camera)
{
if (_particles is null || _particleRenderer is null)
return currentSignature;
return;
if (clipRoot is null)
{
@ -202,21 +200,21 @@ internal sealed class WorldScenePassExecutor : IWorldScenePassExecutor
_visibleParticleOwners,
includeUnattached: true,
excludedAttachedOwnerIds: _noExcludedParticleOwners);
return AppendSignature(currentSignature, "filtered");
return;
}
_particleRenderer.Draw(
camera.Camera,
camera.Position,
ParticleRenderPass.Scene);
return AppendSignature(currentSignature, "global");
return;
}
// Every PView root, including the outdoor sentinel, now submits scene
// particles inside LScape::draw. Replaying them here is both a duplicate
// and too late: it occurs after nested building cells, allowing exterior
// waterfall/foliage alpha to repaint an indoor/outdoor transition.
return currentSignature;
return;
}
public void DrawFlatWeather(
@ -291,6 +289,4 @@ internal sealed class WorldScenePassExecutor : IWorldScenePassExecutor
}
}
private static string AppendSignature(string current, string value) =>
current == "none" ? value : current + "+" + value;
}

View file

@ -176,23 +176,15 @@ internal sealed class WorldSceneRenderer : IPreparedWorldSceneFramePhase
WorldCameraFrame camera = world.Camera;
WorldRootFrame roots = world.Roots;
LoadedCell? clipRoot = world.ClipRoot;
bool renderSky = roots.RenderSky;
bool drawSkyThisFrame = false;
bool terrainDrawn = false;
bool skyDrawn = false;
bool depthClear = false;
bool outdoorSceneryDrawn = false;
int liveDynamicDrawnCount = 0;
string sceneParticles = "none";
TerrainClipMode terrainClipMode = TerrainClipMode.Planes;
RetailPViewFrameResult? pviewResult = null;
LoadedCell? clipRoot = world.ClipRoot;
bool renderSky = roots.RenderSky;
bool drawSkyThisFrame = false;
RetailPViewFrameResult? pviewResult = null;
if (clipRoot is null)
{
_passes.PrepareFlatWorldClip();
drawSkyThisFrame = renderSky;
skyDrawn = drawSkyThisFrame;
_passes.PrepareFlatWorldClip();
drawSkyThisFrame = renderSky;
if (drawSkyThisFrame)
{
_passes.DrawFlatSky(
@ -210,12 +202,7 @@ internal sealed class WorldSceneRenderer : IPreparedWorldSceneFramePhase
// waiting frame. One gate computes the frame's visibility;
// this phase only enforces it.
_passes.DrawFlatTerrain(in camera, roots.PlayerLandblockId);
terrainDrawn = true;
}
else
{
terrainClipMode = TerrainClipMode.Skip;
}
}
if (clipRoot is not null)
{
@ -253,30 +240,6 @@ internal sealed class WorldSceneRenderer : IPreparedWorldSceneFramePhase
// VisibleCells remains a diagnostics-only union.
_particleVisibility.MarkVisibleLandscapeCells(
pviewResult.VisibleLandscapeCells);
_diagnostics.EmitPViewInput(
pviewResult.VisibleCells,
pviewResult.ClipAssembly.OutsideViewSlices.Length,
camera.ViewProjection,
clipRoot,
camera.Position,
roots.PlayerViewPosition);
bool hasOutsideSlice = pviewResult.ClipAssembly.OutsideViewSlices.Length > 0;
terrainDrawn = hasOutsideSlice;
skyDrawn = renderSky && hasOutsideSlice;
// This mirrors the established diagnostic meaning, including
// outdoor roots whose PView executor does not issue an interior
// depth clear.
depthClear = hasOutsideSlice;
RenderProjectionCounts sourceCounts =
pviewResult.SourceCounts;
if (sourceCounts.IndoorCellStatic > 0 || hasOutsideSlice)
sceneParticles = "pviewScoped";
outdoorSceneryDrawn =
sourceCounts.OutdoorStatic > 0
&& hasOutsideSlice;
liveDynamicDrawnCount =
sourceCounts.LiveDynamicRoot;
}
else
{
@ -288,43 +251,20 @@ internal sealed class WorldSceneRenderer : IPreparedWorldSceneFramePhase
}
_passes.DisableClipDistances();
sceneParticles = _passes.DrawPostWorldParticles(
clipRoot,
pviewResult?.ClipAssembly,
in camera,
sceneParticles);
_passes.DrawPostWorldParticles(
clipRoot,
pviewResult?.ClipAssembly,
in camera);
if (clipRoot is null && drawSkyThisFrame)
{
skyDrawn = true;
_passes.DrawFlatWeather(
_passes.DrawFlatWeather(
in camera,
in foundation,
_sky.ActiveDayGroup,
_sky.DayFraction);
}
_diagnostics.EmitRenderSignature(
clipRoot is null ? "OutdoorRoot" : "RetailPViewInside",
clipRoot,
roots.ViewerRoot,
roots.PlayerRoot,
roots.ViewerCellId,
roots.PlayerCellId,
roots.PlayerIndoorGate,
roots.CameraInsideCell,
renderSky,
drawSkyThisFrame,
terrainDrawn,
terrainClipMode,
skyDrawn,
depthClear,
outdoorSceneryDrawn,
liveDynamicDrawnCount,
sceneParticles,
pviewResult,
camera.Position,
roots.PlayerViewPosition);
WorldSceneDiagnosticOutcome diagnostic = _diagnostics.DrawAndPublish(
in camera,

View file

@ -188,7 +188,7 @@ public sealed class LightManager
/// registered lights (chase-boom swing churned the eviction boundary), then
/// (2) frame-FLOOD scoping `c500912b` (gaze-dependent: the under-room portal
/// purples entered/left the pool as the camera turned — the seam-floor
/// blink; probe: [seam-blk]/[seam-snap]). Current model: all registered
/// blink). Current model: all registered
/// (=resident) lit lights, then dynamics-first nearest-player, capped here.
/// A later last-frame drawable-cell filter was removed after the Facility Hub
/// zoom trace proved it recreated the same camera-root coupling: the pool
@ -341,10 +341,6 @@ public sealed class LightManager
}
}
// A7.L1 SET-COMPOSITION probe. Inert unless ACDREAM_PROBE_INDOOR_LIGHT=1;
// the flag check keeps it zero-cost off.
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeIndoorLightEnabled)
AcDream.Core.Rendering.RenderingDiagnostics.EmitIndoorLight(_all, _pointSnapshot);
}
private static int CompareRankedLights(

View file

@ -63,7 +63,7 @@ public sealed class LightSource
// cross-cell block-offset distance math. #176 correction (2026-07-06): it is
// NOT a pool filter — retail collects from ALL resident cells
// (CEnvCell::visible_cell_table = the loaded-cell registry, not the flood);
// acdream keeps the tag for probes ([indoor-light]/[seam-*]) + future parity.
// acdream keeps the tag for future parity work.
public bool IsLit = true; // SetLightHook latch
public bool IsDynamic; // #143: true = D3D hardware path (1/d att, range×1.5);
// false = static dat-baked bake (1/d³, range×1.3)

View file

@ -1,612 +1,44 @@
using System.Globalization;
using System;
using System.Collections.Generic;
using System.Text;
namespace AcDream.Core.Rendering;
/// <summary>
/// 2026-05-19 — runtime-toggleable diagnostic flags for the indoor cell
/// rendering pipeline. Initialized from env vars at process start;
/// flippable at runtime by direct assignment. Log call sites read these
/// statics so a change takes effect on the next frame without relaunching.
/// (#434: these used to have a DebugPanel checkbox mirror. That panel has
/// been unreachable since Campaign V slice V11 removed its ImGui host, so
/// every flag here is startup-or-assignment only.)
///
/// <para>
/// Mirrors the L.2a <see cref="AcDream.Core.Physics.PhysicsDiagnostics"/>
/// pattern. The master <see cref="IndoorAll"/> toggle is the user's
/// common case — flipping it cascades to all five probe flags.
/// </para>
///
/// <para>
/// Spec: <c>docs/superpowers/specs/2026-05-19-indoor-cell-rendering-fix-design.md</c>.
/// </para>
/// </summary>
/// <summary>Permanent rendering diagnostics and retail render-route helpers.</summary>
public static class RenderingDiagnostics
{
/// <summary>
/// When true, <c>WbDrawDispatcher.WalkVisibleEntities</c> emits one
/// <c>[indoor-walk]</c> line per visible cell entity per second:
/// entity id, world position, parent cell id, landblock visible flag,
/// AABB-visible flag, "in visible cells" flag, drew flag.
/// Initial state from <c>ACDREAM_PROBE_INDOOR_WALK=1</c>.
/// </summary>
public static bool ProbeIndoorWalkEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_INDOOR_WALK") == "1"
|| Environment.GetEnvironmentVariable("ACDREAM_PROBE_INDOOR_ALL") == "1";
/// <summary>
/// When true, <c>WbDrawDispatcher</c> emits one <c>[indoor-lookup]</c>
/// line per visible cell entity per second: render-data hit/miss,
/// IsSetup flag, SetupParts count, parts-hit / parts-miss tallies.
/// Initial state from <c>ACDREAM_PROBE_INDOOR_LOOKUP=1</c>.
/// </summary>
public static bool ProbeIndoorLookupEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_INDOOR_LOOKUP") == "1"
|| Environment.GetEnvironmentVariable("ACDREAM_PROBE_INDOOR_ALL") == "1";
/// <summary>
/// When true, <c>WbMeshAdapter</c> emits two lines per EnvCell id:
/// <c>[indoor-upload] requested</c> on first IncrementRefCount and
/// <c>[indoor-upload] completed</c> when WB's staged drain produces
/// its <c>ObjectMeshData</c>. Missing "completed" lines indicate WB
/// silently returned null (hypothesis H1).
/// Initial state from <c>ACDREAM_PROBE_INDOOR_UPLOAD=1</c>.
/// </summary>
public static bool ProbeIndoorUploadEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_INDOOR_UPLOAD") == "1"
|| Environment.GetEnvironmentVariable("ACDREAM_PROBE_INDOOR_ALL") == "1";
/// <summary>
/// When true, <c>WbDrawDispatcher</c> emits one <c>[indoor-xform]</c>
/// line per visible cell entity per second: cell-geometry SetupPart's
/// composed world matrix translation. Disambiguates transform
/// double-apply (hypothesis H5).
/// Initial state from <c>ACDREAM_PROBE_INDOOR_XFORM=1</c>.
/// </summary>
public static bool ProbeIndoorXformEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_INDOOR_XFORM") == "1"
|| Environment.GetEnvironmentVariable("ACDREAM_PROBE_INDOOR_ALL") == "1";
/// <summary>
/// When true, <c>WbDrawDispatcher.WalkVisibleEntities</c> emits one
/// <c>[indoor-cull]</c> line per cell entity that gets culled, with
/// the reason (visibleCellIds-miss, frustum, landblock). Disambiguates
/// cull bugs (hypothesis H3).
/// Initial state from <c>ACDREAM_PROBE_INDOOR_CULL=1</c>.
/// </summary>
public static bool ProbeIndoorCullEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_INDOOR_CULL") == "1"
|| Environment.GetEnvironmentVariable("ACDREAM_PROBE_INDOOR_ALL") == "1";
/// <summary>
/// When true, the unified portal-visibility pass emits one <c>[vis]</c>
/// line whenever the camera's root cell CHANGES (see <see cref="EmitVis"/>):
/// root cell id, visible-cell count + ids, the single OutsideView's polygon
/// + plane counts, a per-cell plane-count summary, and the scissor-fallback
/// count for the frame. This is the runtime apparatus #103 lacked — it lets
/// us confirm "OutsideView non-empty and narrowing at the cellar window" off
/// a live launch.log before any GL/visual work.
/// Initial state from <c>ACDREAM_PROBE_VIS=1</c>.
/// <para>
/// Phase U.2d (2026-05-30) repurposed this flag from the abandoned A8
/// two-pipe stencil pass to the Phase U unified pipeline. The env var name
/// is unchanged (its DebugPanel mirror is gone — #434).
/// </para>
/// </summary>
public static bool ProbeVisibilityEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_VIS") == "1";
/// <summary>
/// Temporary Facility Hub staircase discriminator. The frame walk emits
/// camera/root/flood facts and the leaf classifier emits change-only
/// decisions for the authored stair GfxObj (0x010000DE) and local-player
/// setup parts in cells 0x8A02015E/015F/01C1. Output-only; it must never
/// influence admission. Initial state from
/// <c>ACDREAM_PROBE_FACILITY_STAIRS=1</c>.
/// </summary>
public static bool ProbeFacilityStairsEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_FACILITY_STAIRS") == "1";
/// <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
/// <c>[flap]</c> line (root cell's per-portal side-test D + traverse/cull +
/// projection, plus the frame's OutsideView/visible counts) and the call site
/// emits a paired <c>[flap-cam]</c> line (FindCameraCell resolution reason,
/// camera EYE worldpos, player worldpos, eye-in-root-AABB flag). Unlike the
/// cell-change-throttled <see cref="ProbeVisibilityEnabled"/> probe, this fires
/// per-frame so it captures the flicker (the exit cell dropping in/out at a
/// STABLE root). Pinpoints WHY the exit cell drops: side-test cull (eye past an
/// interior portal plane), empty projection, or a stale root (eye outside the
/// cell while FindCameraCell still reports it via cache/grace). Throwaway
/// apparatus — strip once the flap mechanism is confirmed.
/// Initial state from <c>ACDREAM_PROBE_FLAP=1</c>.
/// </summary>
public static bool ProbeFlapEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_FLAP") == "1";
/// <summary>
/// Flap root-cause apparatus (2026-06-07). When true, the indoor render path emits ONE
/// <c>[pv-input]</c> line per frame with the EXACT PortalVisibilityBuilder.Build inputs at HIGH
/// precision (camera eye + player position to 6 dp, plus orientation-sensitive view-projection
/// elements) alongside the resulting flood cell count. The live flap shows the flood set flipping
/// 2↔6 at an eye/player that is identical to cm; this probe answers whether the Build INPUTS differ
/// below cm precision (sub-cm view jitter → robustness fix) or are byte-identical while the output
/// still flips (nondeterminism → surgical bug). Runs WITHOUT the heavy <c>[flap]</c>/<c>[render-sig]</c>
/// spam so the log stays diffable. Throwaway apparatus — strip once the jitter source is pinned.
/// Initial state from <c>ACDREAM_PROBE_PVINPUT=1</c>.
/// </summary>
public static bool ProbePvInputEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_PVINPUT") == "1";
/// <summary>
/// §4 outdoor full-world flap apparatus (2026-06-09). When true, GameWindow snapshots the
/// GL fixed-function state entering the world passes each frame (depth test/mask/func, blend
/// + factors, cull, front-face, scissor + box, viewport, draw-FBO, color mask, glGetError)
/// and emits one <c>[gl-state]</c> line whenever the snapshot CHANGES. Pins or refutes the
/// "leaked GL state" family for the flap (every CPU-side input — matrix, flood, clip planes,
/// scissor box, membership, eye-vs-terrain — is already probe-exonerated). Throwaway
/// apparatus — strip once §4 ships. Initial state from <c>ACDREAM_PROBE_GLSTATE=1</c>.
/// </summary>
public static bool ProbeGlStateEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_GLSTATE") == "1";
/// <summary>
/// §4 outdoor full-world flap apparatus (2026-06-10) — the decisive probe between the two
/// surviving suspects (handoff 2026-06-09 §1): (a) per-instance clip-slot routing under
/// outdoor roots, (b) terrain/sky UBO content at draw time — plus the landscape-pass scissor
/// box as a third ground truth. When true: RetailPViewRenderer.DrawLandscapeThroughOutsideView
/// emits one <c>[clip-route]</c> line (print-on-change) with the outside slice's slot + NDC
/// AABB + planes, the CellIdToSlot routing table, the region-SSBO bytes decoded at the routed
/// slot, and the terrain-UBO head as uploaded. The <c>[clip-route-disp]</c> producer in
/// WbDrawDispatcher.Draw was deleted with the per-instance clip routing at S3 review fix
/// round 1 (the routing had no caller), and the <c>[clip-route-scis]</c> scissor line's
/// landscape-slice producer went with the doorway scissor at S3 chunk 4; only the
/// WorldRenderDiagnostics lines remain. Throwaway apparatus — the S5 cleanup inventory owns
/// its deletion. Initial state from <c>ACDREAM_PROBE_CLIPROUTE=1</c>.
/// </summary>
public static bool ProbeClipRouteEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_CLIPROUTE") == "1";
/// <summary>
/// Bounded-propagation port apparatus (2026-06-08). When true, PortalVisibilityBuilder.Build emits
/// one [portal-churn] summary line per call: per-cell pop count (re-pops = churn), total re-enqueues,
/// max pop count, and — per re-enqueue — the reciprocal-clip pre→post region count + grew flag. Pins
/// whether the flap's churn is redundant reciprocal back-contributions producing non-empty drifted
/// slivers (the hypothesis) vs another source. Throwaway apparatus — strip once the bound ships.
/// Initial state from ACDREAM_PROBE_PORTAL_CHURN=1.
/// </summary>
public static bool ProbePortalChurnEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_PORTAL_CHURN") == "1";
/// <summary>
/// A7.L1 (2026-07-06) light-pool SET-COMPOSITION probe — the apparatus the
/// <c>[light]</c> counts could not provide (the #176/#177 discriminator: the bug
/// lived in set MEMBERSHIP, not counts). When true,
/// <c>LightManager.BuildPointLightSnapshot</c> emits ONE rate-limited
/// <c>[indoor-light]</c> line describing the point-light pool
/// (see <see cref="EmitIndoorLight"/>):
/// <code>
/// [indoor-light] pool=&lt;M&gt; cellLess=&lt;K&gt; registered=&lt;R&gt; capped=&lt;R-M&gt;
/// byCell=[0x&lt;id&gt;:&lt;count&gt;,...]
/// </code>
/// #176 correction (2026-07-06): the pool became retail's RESIDENT-cell
/// collection capped nearest-the-PLAYER — the earlier gaze-coupled scoping
/// (rebuilding the pool from a freshly re-flooded CAMERA-seeded set,
/// <c>c500912b</c>) was the #176 flicker mechanism and was deleted.
/// A7.L1 later added last-frame drawable-cell scoping, but the Facility Hub
/// zoom trace proved that it also couples pool membership to the camera root
/// (five lights became one while the player stood still), so it was removed.
/// <c>byCell</c> now describes the resident pool; <c>cellLess==pool</c> in a
/// fixture-rich room still
/// means cell tagging FAILED (ParentCellId not flowing).
/// Output-only, inert when off. Initial state from <c>ACDREAM_PROBE_INDOOR_LIGHT=1</c>.
/// </summary>
public static bool ProbeIndoorLightEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_INDOOR_LIGHT") == "1";
/// <summary>
/// #176 seam-floor flicker decisive probe (2026-07-06). RenderDoc pixel-history
/// is infeasible on this pipeline (RenderDoc does not support
/// GL_ARB_bindless_texture and hides it from the app → our mandatory-modern
/// startup gate throws), so this is the in-engine equivalent at draw
/// granularity: every draw route that can put geometry at the corridor seam
/// floor reports itself, plus the per-cell light sets ACTUALLY applied.
/// When set, four line families emit (all change-deduped, Console):
/// <list type="bullet">
/// <item><description><c>[seam-cell]</c> — <c>EnvCellRenderer.Render</c>
/// (opaque pass): per target cell — in-filter flag, per-gfx instance count +
/// transform translation (shows the +0.02 shell lift), per-batch
/// cull/translucency, and the cell's 8-light set RESOLVED to identities
/// (owner cell + intensity — raw snapshot indices shuffle when the pool
/// rebuilds, so identities are the stable signature). Two instances of one
/// (cell,gfx) = the runtime double-draw; light identities flipping with the
/// flood = the snapshot-scope mechanism.</description></item>
/// <item><description><c>[seam-snap]</c> — the point-light snapshot's HOT
/// subset (intensity ≥ 50: the portal purples; fixtures are ~12, the viewer
/// fill 2.25) with owner cells, emitted with the block.</description></item>
/// <item><description><c>[seam-ent]</c> — <c>WbDrawDispatcher</c>: any entity
/// parented to a target cell — position, culled/slot, resolved light set. A
/// floor-coincident entity (plate/static) would be the z-fight's second draw;
/// the player entity doubles as the probe's positive control.</description></item>
/// <item><description><c>[seam-mask]</c> — <c>RetailPViewPassExecutor.DrawPortalDepthWrite</c>:
/// every portal depth fan drawn in a target cell. A sealed dungeon must show
/// ZERO (seals fire only for OtherCellId==0xFFFF) — any line is a finding.</description></item>
/// </list>
/// Value <c>1</c> = the default Facility Hub target set (corridor 0x8A020164 +
/// seam neighbors 0165/016E/017A + under-hall 011E + portal-light cells
/// 0118/0119); a comma-separated hex cell-id list overrides it. Throwaway
/// apparatus — strip when #176 closes. Initial state from
/// <c>ACDREAM_PROBE_SEAMDRAW</c>.
/// </summary>
public static bool ProbeSeamDrawEnabled { get; set; } =
!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("ACDREAM_PROBE_SEAMDRAW"));
/// <summary>Target cell ids for the #176 seam-draw probe (see
/// <see cref="ProbeSeamDrawEnabled"/>). Full 32-bit cell ids.</summary>
public static IReadOnlySet<uint> SeamDrawTargetCells { get; } =
ParseSeamDrawTargets(Environment.GetEnvironmentVariable("ACDREAM_PROBE_SEAMDRAW"));
/// <summary>
/// #176 stripe-hunt shader isolation mode (<c>ACDREAM_LIGHT_DEBUG</c>) —
/// throwaway diagnostic, uploaded as <c>uLightDebug</c> by EnvCellRenderer +
/// WbDrawDispatcher each pass. 0 = off; 1 = ambient-only vertex lighting
/// (all point/sun contributions killed); 2 = DYNAMIC point lights killed
/// (the intensity-100 portal purples + the viewer fill off; statics stay);
/// 3 = raw vLit visualization in the fragment shader (texture ignored).
/// Discriminates lighting-driven stripes (gone at 1/2, visible in the field
/// at 3) from texture/per-pixel machinery (survive 1). Settable at
/// runtime by direct assignment.
/// #176 stripe-hunt shader isolation mode. Zero disables the override;
/// values 1-3 retain the established lighting diagnostic modes.
/// </summary>
public static int LightDebugMode { get; set; } =
int.TryParse(
Environment.GetEnvironmentVariable("ACDREAM_LIGHT_DEBUG"),
NumberStyles.Integer,
CultureInfo.InvariantCulture,
out int ldm)
? ldm
out int mode)
? mode
: 0;
// S3 review fix round 1 (F5): ClipDebugNoShellTrim (ACDREAM_CLIP_DEBUG)
// is deleted — the #176 stripe-hunt isolation it toggled between "clip
// shells to their cell's portal-slice region" and "map every instance to
// slot 0" is moot now that EnvCellRenderer's per-cell clip routing is
// deleted outright (its arming method never had a live caller): shells
// always draw WHOLE, unconditionally, the same behavior this flag used
// to force.
/// <summary>Parse ACDREAM_PROBE_SEAMDRAW: "1"/"true"/empty → the default #176
/// Facility Hub set; otherwise a comma-separated hex cell-id list (same forgiving
/// grammar as <see cref="ParseDumpEntityIds"/>). Internal for unit tests.</summary>
internal static IReadOnlySet<uint> ParseSeamDrawTargets(string? raw)
{
// Default: the corridor + the coplanar-sweep seed neighbors + the under-hall
// + the two portal-weenie cells whose intensity-100 purple lights are the
// wedge's source (handoff 2026-07-06-176-seam-floor-zfight-handoff.md).
var defaults = new HashSet<uint>
{
0x8A020164u, 0x8A020165u, 0x8A02016Eu, 0x8A02017Au,
0x8A02011Eu, 0x8A020118u, 0x8A020119u,
};
if (string.IsNullOrWhiteSpace(raw)) return defaults;
var trimmed = raw.Trim();
if (trimmed == "1" || trimmed.Equals("true", StringComparison.OrdinalIgnoreCase))
return defaults;
var parsed = ParseDumpEntityIds(raw);
return parsed.Count > 0 ? parsed : defaults;
}
// Cell-change gate for EmitVis. The probe fires once per distinct root cell
// so launch.log stays readable under motion (the per-frame call is a no-op
// when the root is unchanged). Sentinel 0 = "no root yet" — the first real
// root id always differs and fires. Reset between tests via
// ResetVisibilityProbeForTests so the gate doesn't leak across cases.
private static uint _lastVisRootCellId;
/// <summary>
/// Emit ONE concise, information-dense <c>[vis]</c> line for the portal-
/// visibility frame, but only when <see cref="ProbeVisibilityEnabled"/> is
/// true AND <paramref name="rootCellId"/> differs from the last root the
/// probe reported (cell-change gating). Cheap no-op otherwise.
/// <para>
/// Decoupled by design: the OutsideView is passed as pre-computed
/// <paramref name="outsidePolyCount"/> + <paramref name="outsidePlaneCount"/>
/// primitives rather than the App-layer <c>CellView</c>/<c>ClipPlaneSet</c>
/// types, because this owner lives in <c>AcDream.Core</c> and Core must not
/// reference the App project (Code Structure Rule 2). The U.4a call site
/// supplies <c>OutsideView.Polygons.Count</c> and the OutsideView's
/// <c>ClipPlaneSet.Count</c>.
/// </para>
/// </summary>
/// <param name="rootCellId">The camera's root cell id (the BFS seed).</param>
/// <param name="visibleCells">Ordered visible cell ids for this frame.</param>
/// <param name="outsidePolyCount">Polygon count of the single OutsideView region.</param>
/// <param name="outsidePlaneCount">Clip-plane count the OutsideView reduced to (0 ⇒ scissor/empty).</param>
/// <param name="perCellPlaneCounts">Per-cell clip-plane count (cell id → plane count).</param>
/// <param name="scissorFallbacks">Number of regions that fell back to a scissor AABB this frame.</param>
public static void EmitVis(uint rootCellId,
IReadOnlyList<uint> visibleCells,
int outsidePolyCount,
int outsidePlaneCount,
IReadOnlyDictionary<uint, int> perCellPlaneCounts,
int scissorFallbacks)
{
if (!ProbeVisibilityEnabled) return;
if (rootCellId == _lastVisRootCellId) return; // unchanged root ⇒ suppress
_lastVisRootCellId = rootCellId;
int cellN = visibleCells?.Count ?? 0;
var sb = new StringBuilder(160);
sb.Append("[vis] root=0x").Append(rootCellId.ToString("X8"));
sb.Append(" cells=").Append(cellN);
// Visible cell id list, capped so a wide BFS doesn't blow up the line.
sb.Append(" ids=[");
if (visibleCells is not null)
{
const int MaxIds = 12;
int shown = 0;
foreach (uint id in visibleCells)
{
if (shown >= MaxIds) { sb.Append(",..."); break; }
if (shown > 0) sb.Append(',');
sb.Append("0x").Append(id.ToString("X8"));
shown++;
}
}
sb.Append(']');
sb.Append(" outside(polys=").Append(outsidePolyCount)
.Append(",planes=").Append(outsidePlaneCount).Append(')');
// Per-cell plane-count summary, capped like the id list.
sb.Append(" percell=[");
if (perCellPlaneCounts is not null)
{
const int MaxPerCell = 12;
int shown = 0;
foreach (var kv in perCellPlaneCounts)
{
if (shown >= MaxPerCell) { sb.Append(",..."); break; }
if (shown > 0) sb.Append(',');
sb.Append("0x").Append(kv.Key.ToString("X8")).Append(':').Append(kv.Value);
shown++;
}
}
sb.Append(']');
sb.Append(" fallbacks=").Append(scissorFallbacks);
Console.WriteLine(sb.ToString());
}
/// <summary>
/// Reset the <see cref="EmitVis"/> cell-change gate. Test-only — this is a
/// process-wide static and the gate would otherwise leak across test cases
/// (this codebase has documented static-leak flakiness; keep tests
/// self-contained). Not part of the public runtime surface.
/// </summary>
internal static void ResetVisibilityProbeForTests() => _lastVisRootCellId = 0;
private const long LightEmitIntervalTicks = 10_000_000; // 1 s in 100-ns ticks
// Wall-clock rate-limit gate for EmitIndoorLight (shares the 1 s interval).
private static long _lastIndoorLightEmitTicks;
/// <summary>
/// A7.L1 — emit ONE rate-limited <c>[indoor-light]</c> line describing the
/// point-light pool: the SET COMPOSITION the <c>[light]</c> counts can't show.
/// Cheap no-op when <see cref="ProbeIndoorLightEnabled"/> is false; otherwise
/// fires at most once per second. Called from
/// <c>LightManager.BuildPointLightSnapshot</c> after resident collection and
/// the bounded player-nearest selection.
/// </summary>
/// <param name="allRegistered">Every registered light (<c>LightManager._all</c>).</param>
/// <param name="pointSnapshot">The point-light pool just built.</param>
public static void EmitIndoorLight(
IReadOnlyList<AcDream.Core.Lighting.LightSource> allRegistered,
IReadOnlyList<AcDream.Core.Lighting.LightSource> pointSnapshot)
{
if (!ProbeIndoorLightEnabled) return;
long now = DateTime.UtcNow.Ticks;
if (_lastIndoorLightEmitTicks != 0 && (now - _lastIndoorLightEmitTicks) < LightEmitIntervalTicks)
return;
_lastIndoorLightEmitTicks = now;
int registeredLitPoints = 0;
foreach (var l in allRegistered)
if (l.IsLit && l.Kind != AcDream.Core.Lighting.LightKind.Directional) registeredLitPoints++;
int pool = pointSnapshot.Count;
int cellLess = 0;
var hist = new Dictionary<uint, int>();
foreach (var l in pointSnapshot)
{
if (l.CellId == 0) cellLess++;
hist.TryGetValue(l.CellId, out var c);
hist[l.CellId] = c + 1;
}
var sb = new StringBuilder(220);
sb.Append("[indoor-light] pool=").Append(pool);
sb.Append(" cellLess=").Append(cellLess);
sb.Append(" registered=").Append(registeredLitPoints);
// Lights dropped by the MaxGlobalLights nearest-player cap (0 in Hub-scale
// rooms for dynamics — statics beyond the 128th-nearest are out of range).
sb.Append(" capped=").Append(registeredLitPoints - pool);
sb.Append(" byCell=[");
const int MaxCells = 12;
int shown = 0;
foreach (var kv in hist)
{
if (shown >= MaxCells) { sb.Append(",..."); break; }
if (shown > 0) sb.Append(',');
sb.Append("0x").Append(kv.Key.ToString("X8")).Append(':').Append(kv.Value);
shown++;
}
sb.Append(']');
Console.WriteLine(sb.ToString());
}
private static bool _probeEnvCellEnabled =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_ENVCELL") == "1";
/// <summary>
/// Phase A8 Task 9 (2026-05-28): when true, the indoor EnvCell draw path's
/// <c>[envcells]</c> probe emits one line per indoor frame —
/// CellsRendered / TrianglesDrawn from <c>EnvCellRenderer.Stats</c> +
/// ourBldgs/otherBldgs/filterCnt.
/// Also enabled implicitly when <see cref="ProbeVisibilityEnabled"/> is true.
/// Initial state from <c>ACDREAM_PROBE_ENVCELL=1</c>.
/// (The two-pipe <c>RenderInsideOutAcdream</c> pass that originally owned
/// this probe was removed in Phase U.1; the env var + the
/// <c>EnvCellRenderer.Stats</c> source remain.)
/// </summary>
public static bool ProbeEnvCellEnabled
{
get => _probeEnvCellEnabled || ProbeVisibilityEnabled;
set => _probeEnvCellEnabled = value;
}
/// <summary>
/// Master toggle. Reading reflects the AND of all five flags
/// (true only when every probe is on). Writing cascades — setting
/// to <see langword="true"/> turns ALL five flags on; setting to
/// <see langword="false"/> turns ALL five off.
/// </summary>
public static bool IndoorAll
{
get => ProbeIndoorWalkEnabled
&& ProbeIndoorLookupEnabled
&& ProbeIndoorUploadEnabled
&& ProbeIndoorXformEnabled
&& ProbeIndoorCullEnabled;
set
{
ProbeIndoorWalkEnabled = value;
ProbeIndoorLookupEnabled = value;
ProbeIndoorUploadEnabled = value;
ProbeIndoorXformEnabled = value;
ProbeIndoorCullEnabled = value;
}
}
/// <summary>
/// Helper for probe call sites. Returns <see langword="true"/> when
/// the low 16 bits of <paramref name="id"/> are ≥ 0x0100 — the AC
/// convention for EnvCell (indoor) cells, as opposed to outdoor cells
/// in the 8×8 landblock grid (0x00010x0040).
/// </summary>
/// <summary>Returns true for AC indoor EnvCell ids.</summary>
public static bool IsEnvCellId(ulong id) => (id & 0xFFFFu) >= 0x0100u;
/// <summary>
/// Parse the <c>ACDREAM_DUMP_ENTITY</c> value: comma-separated hex ids,
/// optional 0x prefix, whitespace tolerated, malformed segments ignored
/// (probes are forgiving — a typo'd segment must not take the launch down).
/// Internal for unit tests.
/// </summary>
internal static IReadOnlySet<uint> ParseDumpEntityIds(string? raw)
{
var set = new HashSet<uint>();
if (string.IsNullOrWhiteSpace(raw)) return set;
foreach (var seg in raw.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
{
var s = seg.StartsWith("0x", StringComparison.OrdinalIgnoreCase) ? seg[2..] : seg;
if (uint.TryParse(s, System.Globalization.NumberStyles.HexNumber,
System.Globalization.CultureInfo.InvariantCulture, out var id))
set.Add(id);
}
return set;
}
/// <summary>
/// The top-level render branch: should this frame run the indoor (DrawInside) path?
///
/// <para>Retail <c>SmartBox::RenderNormalMode</c> (0x453aa0, pc:92665) branches
/// DrawInside vs the outdoor <c>LScape::draw</c> on <c>is_player_outside</c> — the
/// <b>PLAYER's</b> cell (<c>(player-&gt;m_position.objcell_id &amp; 0xFFFF) &lt; 0x100</c>,
/// <c>SmartBox::is_player_outside</c> 0x451e80) — NOT the camera/viewer cell. When the
/// player is inside, acdream roots the portal flood at the player's transition-owned
/// physics cell and projects from the camera eye, so the shell around the player remains
/// sealed during chase-camera cell transitions.</para>
///
/// <para>acdream historically branched on the camera cell (a non-null
/// <c>visibility.CameraCell</c>). A 3rd-person chase camera lags the player, so when the
/// player had already stepped outside but the camera still sat in the doorway, the camera
/// branch wrongly chose DrawInside rooted at the doorway cell, where the exit-portal flood
/// degenerates → the whole static world (terrain + shells) gated off → grey screen with
/// only entities (which bypass the gate) showing through. Branching on the player removes it.</para>
///
/// <param name="playerCellId">The player's current cell id (0 if unresolved → outside).</param>
/// <param name="renderRootResolved">Whether the player's indoor render root is loaded and
/// available to DrawInside.</param>
/// Retail chooses the indoor path from the player's cell, provided the
/// transition-owned render root is available.
/// </summary>
public static bool ShouldRenderIndoor(uint playerCellId, bool renderRootResolved)
=> renderRootResolved && IsEnvCellId(playerCellId);
/// <summary>
/// MP0 (2026-07-05) — master toggle for the permanent frame profiler
/// (<c>AcDream.App.Diagnostics.FrameProfiler</c>): CPU frame time
/// (swap-to-swap), whole-frame GPU time, per-stage CPU attribution,
/// per-frame allocation counters, reported as one <c>[frame-prof]</c>
/// line every ~5 s. Permanent apparatus (every MP-track gate reads it) —
/// do NOT strip with session probes. This paragraph previously claimed
/// the whole-frame GPU query self-disables under <c>ACDREAM_WB_DIAG=1</c>;
/// Campaign V slice V11 deleted that self-disable along with the GL query
/// ring it protected, and the two flags are now independent — see
/// <c>FrameProfiler</c>'s own class doc. Every backend reports GPU time
/// through <c>FrameProfiler.RecordGpuSample</c>.
/// Initial state from <c>ACDREAM_FRAME_PROF=1</c>; runtime-toggleable
/// by direct assignment (its DebugPanel mirror is gone — #434).
/// Spec: docs/superpowers/specs/2026-07-05-modern-pipeline-design.md §5.
/// </summary>
/// <summary>Permanent frame-profiler toggle.</summary>
public static bool FrameProfEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_FRAME_PROF") == "1";
/// <summary>
/// 2026-07-24 measurement-tooling review — opt-in per-frame history
/// export path for <c>AcDream.App.Diagnostics.FrameProfiler</c>. When
/// set, the profiler accumulates one record per frame (frame index,
/// wall timestamp, per-stage CPU microseconds, GPU microseconds,
/// frame-thread allocation delta) in memory and writes them as CSV to
/// this path on <c>Dispose</c> — the aggregated 5-second
/// <c>[frame-prof]</c> report is unaffected. History recording only
/// takes effect while <see cref="FrameProfEnabled"/> is also true: it
/// reuses the same per-frame instrumentation rather than duplicating
/// it. Startup-only (not runtime-toggleable) — read once, like every
/// other <c>ACDREAM_*</c> launch flag on this owner.
/// </summary>
/// <summary>Optional per-frame CSV history path for the frame profiler.</summary>
public static string? FrameHistoryPath { get; } =
Environment.GetEnvironmentVariable("ACDREAM_FRAME_HISTORY");
/// <summary>
/// Campaign OVERHAUL S3 chunk 1 (§11.2 B1): print-only walk-transcript
/// emitter. When true, the production frame walk
/// (<c>RetailFrameWalk</c> + <c>WalkFrameDriver</c>,
/// <c>src/AcDream.App/Rendering/Walk/WalkTranscriptDump.cs</c>) prints
/// the OH oracle-trace line kinds — F/P/LS/LC/SC/BLD/DI/DC/EC/OC — to
/// Console at the exact points retail's cdb breakpoints sit
/// (<c>tools/walk-oracle/oh/oh-capture-walk.cdb.template</c>). Print-
/// only: it never gates admission, depth state, or draw order — every
/// print call sits AFTER the walk has already decided to emit the
/// corresponding turn, and every print call bails out before any string
/// work when this is false.
/// <para>
/// This is the "owned by RenderingDiagnostics (rule 5)" half of §11.2
/// B1's design; the sole authoritative read of
/// <c>ACDREAM_DUMP_WALK_TRANSCRIPT</c> lives in
/// <c>RuntimeOptions.DumpWalkTranscript</c> (rule 4) — this property
/// defaults false and is set exactly once, at <c>GameWindow</c>
/// construction, from that typed option (never reads the environment
/// directly itself, unlike this file's other flags), so the deep walk
/// call sites that have no reachable <c>RuntimeOptions</c> reference
/// still get one static, flippable-at-runtime gate to check.
/// </para>
/// Print-only production walk transcript gate. RuntimeOptions owns the
/// sole environment read and assigns this property during construction.
/// </summary>
public static bool DumpWalkTranscriptEnabled { get; set; }
}

View file

@ -1969,7 +1969,7 @@ public sealed class PlayerMovementController
// PhysicsEngine.ResolveWithTransition. That ran for EVERY entity, so a Holtburg NPC
// jump-looping near the cottage doorway clobbered the render root every tick → the render
// rooted at the NPC's tiny connector cell → only its ~8-tri shell drew, rest = GL clear
// color = the cottage doorway "blue-hole" flap (diagnosed 2026-06-03 via [flap-cam]/[shell]).
// color = the cottage doorway "blue-hole" flap (diagnosed 2026-06-03 from a camera capture).
if (publishRenderRoot)
_physics.UpdatePlayerCurrCell(newCellId);
}

View file

@ -325,11 +325,6 @@ public sealed class HostInputCameraCompositionTests
GpuFrameFlightController? frameFlights,
IGpuDevice device) => frameFlights!;
public WorldRenderDiagnostics? CreateWorldRenderDiagnostics(
GameWindowGraphics graphics,
IRenderFrameDiagnosticLog log) =>
new(new GlStateReader(), log);
public SilkKeyboardSource CreateKeyboardSource(
IKeyboard keyboard,
HostQuiescenceGate quiescence) =>
@ -498,12 +493,6 @@ public sealed class HostInputCameraCompositionTests
public void Delete(nint fence) { }
}
private sealed class GlStateReader : IRenderGlStateReader
{
public RenderGlStateSnapshot CaptureState() => default;
public RenderGlScissorSnapshot CaptureScissor() => default;
}
private sealed class DiagnosticLog : IRenderFrameDiagnosticLog
{
public void WriteLine(string message) { }

View file

@ -19,7 +19,7 @@ namespace AcDream.App.Tests.Rendering;
/// each class's <c>finally</c> sufficient.
/// </para>
///
/// <para>Three distinct globals are covered, and every one of them is a shared
/// <para>The shared globals below are covered, and every one of them is an
/// edge between at least two classes in this collection:</para>
/// <list type="bullet">
/// <item><description><c>AcDream.Core.Rendering.CameraDiagnostics</c> —
@ -28,15 +28,12 @@ namespace AcDream.App.Tests.Rendering;
/// <c>RetailChaseCamera.Update</c>, <c>CameraController.Active</c>,
/// <c>CameraFrameController</c>, <c>WorldRenderFrameBuilder</c> and
/// <c>MouseLookController</c>.</description></item>
/// <item><description><c>AcDream.Core.Rendering.RenderingDiagnostics.ProbeFlapEnabled</c>
/// — written by <c>CornerFloodReplayTests</c> and
/// <c>Issue181WallPressEquilibriumTests</c>.</description></item>
/// <item><description><c>AcDream.Core.Rendering.RenderingDiagnostics.DumpWalkTranscriptEnabled</c>
/// (Campaign OVERHAUL S3 chunk 1) — written by
/// <c>WalkFrameDriverTests</c>' transcript-emitter tests
/// (<c>WalkFrameDriverTranscriptTests.cs</c>).</description></item>
/// <item><description><c>System.Console.Out</c> — redirected via
/// <c>Console.SetOut</c> by those same classes to capture probe output.
/// <c>Console.SetOut</c> by the transcript tests to capture output.
/// Interleaved redirection can restore a DISPOSED <c>StringWriter</c> as the
/// process-wide <c>Console.Out</c>, which then throws in unrelated
/// tests.</description></item>

View file

@ -416,7 +416,7 @@ public sealed class ChargenPreviewControllerTests
/// </summary>
private bool TryOpen(out DatCollection? dats, out DatCollectionAdapter? adapter)
{
string? datDir = CornerFloodReplayTests.ResolveDatDir();
string? datDir = InstalledDatTestPath.Resolve();
if (datDir is null)
{
_out.WriteLine("SKIP: dats unavailable");

View file

@ -29,7 +29,7 @@ public sealed class ChargenPreviewEntityBuilderTests
[Fact]
public void TryBuild_AluvianMaleDefaultSelection_ProducesANonEmptyStaticPoseEntity()
{
string? datDir = CornerFloodReplayTests.ResolveDatDir();
string? datDir = InstalledDatTestPath.Resolve();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
@ -87,7 +87,7 @@ public sealed class ChargenPreviewEntityBuilderTests
ChargenPreviewEntityBuilder.PreviewBackdropRenderId,
ChargenPreviewEntityBuilder.SummaryPreviewBackdropRenderId);
string? datDir = CornerFloodReplayTests.ResolveDatDir();
string? datDir = InstalledDatTestPath.Resolve();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
@ -121,7 +121,7 @@ public sealed class ChargenPreviewEntityBuilderTests
[Fact]
public void TryBuild_UnknownSetupId_ReturnsNull()
{
string? datDir = CornerFloodReplayTests.ResolveDatDir();
string? datDir = InstalledDatTestPath.Resolve();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
@ -145,7 +145,7 @@ public sealed class ChargenPreviewEntityBuilderTests
[Fact]
public void TryBuild_OlthoiHeritage_ResolvesADifferentRestPoseDidThanStandardHeritages()
{
string? datDir = CornerFloodReplayTests.ResolveDatDir();
string? datDir = InstalledDatTestPath.Resolve();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
@ -186,7 +186,7 @@ public sealed class ChargenPreviewEntityBuilderTests
[Fact]
public void TryBuildAnimated_AluvianMaleDefaultSelection_ResolvesARealIdleCycle()
{
string? datDir = CornerFloodReplayTests.ResolveDatDir();
string? datDir = InstalledDatTestPath.Resolve();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
@ -232,7 +232,7 @@ public sealed class ChargenPreviewEntityBuilderTests
[Fact]
public void TryBuildAnimated_UnknownSetupId_ReturnsNull()
{
string? datDir = CornerFloodReplayTests.ResolveDatDir();
string? datDir = InstalledDatTestPath.Resolve();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
@ -274,7 +274,7 @@ public sealed class ChargenPreviewEntityBuilderTests
[InlineData(0x10000013u)] // OlthoiAcid's shared idle/rest enum key.
public void OlthoiFamily_SharedIdleRestEnumKey_ResolvesToARealInstalledDid(uint sharedEnumKey)
{
string? datDir = CornerFloodReplayTests.ResolveDatDir();
string? datDir = InstalledDatTestPath.Resolve();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
@ -297,7 +297,7 @@ public sealed class ChargenPreviewEntityBuilderTests
[Fact]
public void TryBuildAnimated_OlthoiHeritage_ResolvesARealIdleAnimationToo()
{
string? datDir = CornerFloodReplayTests.ResolveDatDir();
string? datDir = InstalledDatTestPath.Resolve();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
@ -337,7 +337,7 @@ public sealed class ChargenPreviewEntityBuilderTests
[Fact]
public void TryBuildBackdrop_AluvianHeritage_ResolvesANonEmptyMesh()
{
string? datDir = CornerFloodReplayTests.ResolveDatDir();
string? datDir = InstalledDatTestPath.Resolve();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
@ -376,7 +376,7 @@ public sealed class ChargenPreviewEntityBuilderTests
[Fact]
public void TryBuildBackdrop_ExplicitRenderId_StampsThatIdOnTheEntity()
{
string? datDir = CornerFloodReplayTests.ResolveDatDir();
string? datDir = InstalledDatTestPath.Resolve();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
@ -405,7 +405,7 @@ public sealed class ChargenPreviewEntityBuilderTests
[Fact]
public void TryBuildBackdrop_UnsetEnvironmentSetupId_ReturnsNull()
{
string? datDir = CornerFloodReplayTests.ResolveDatDir();
string? datDir = InstalledDatTestPath.Resolve();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
@ -419,7 +419,7 @@ public sealed class ChargenPreviewEntityBuilderTests
[Fact]
public void TryBuildBackdrop_UnknownSetupId_ReturnsNull()
{
string? datDir = CornerFloodReplayTests.ResolveDatDir();
string? datDir = InstalledDatTestPath.Resolve();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
@ -440,7 +440,7 @@ public sealed class ChargenPreviewEntityBuilderTests
[Fact]
public void TryBuildBackdrop_AllThirteenHeritages_ResolveOrAreReportedByName()
{
string? datDir = CornerFloodReplayTests.ResolveDatDir();
string? datDir = InstalledDatTestPath.Resolve();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);

View file

@ -1,4 +1,3 @@
using System.Collections.Generic;
using System.Numerics;
using AcDream.App.Rendering;
using Xunit;
@ -7,30 +6,12 @@ namespace AcDream.App.Tests.Rendering;
public class ClipFrameAssemblerTests
{
private static ViewPolygon Square(float cx, float cy, float half) => new(new[]
{
new Vector2(cx - half, cy - half),
new Vector2(cx + half, cy - half),
new Vector2(cx + half, cy + half),
new Vector2(cx - half, cy + half),
});
private static CellView ViewOf(params ViewPolygon[] polys)
{
var view = new CellView();
foreach (var p in polys)
view.Add(p);
return view;
}
[Fact]
public void BeginWalkFrame_OutdoorRoot_SeedsOneFullScreenDefaultView()
{
using ClipFrame frame = ClipFrame.NoClip();
ClipFrameAssembly assembly = ClipFrameAssembler.BeginWalkFrame(
frame,
outdoorRoot: true);
ClipFrameAssembly assembly = ClipFrameAssembler.BeginWalkFrame(frame, outdoorRoot: true);
ClipViewSlice slice = Assert.Single(assembly.OutsideViewSlices);
Assert.Equal(0, slice.Slot);
@ -45,9 +26,7 @@ public class ClipFrameAssemblerTests
public void BeginWalkFrame_InteriorRoot_ResetsReusedOutdoorAssemblyToEmpty()
{
using ClipFrame frame = ClipFrame.NoClip();
ClipFrameAssembly reuse = ClipFrameAssembler.BeginWalkFrame(
frame,
outdoorRoot: true);
ClipFrameAssembly reuse = ClipFrameAssembler.BeginWalkFrame(frame, outdoorRoot: true);
ClipFrameAssembly assembly = ClipFrameAssembler.BeginWalkFrame(
frame,
@ -62,270 +41,4 @@ public class ClipFrameAssemblerTests
Assert.Equal(0, assembly.ScissorFallbacks);
Assert.Equal(1, frame.SlotCount);
}
[Fact]
public void TwoVisibleCells_PlusOutsideView_ProducesCorrectSlotMapAndCounts()
{
const uint cellA = 0xA9B40100;
const uint cellB = 0xA9B40101;
var pv = new PortalVisibilityFrame();
pv.CellViews[cellA] = ViewOf(Square(-0.3f, 0f, 0.3f));
pv.CellViews[cellB] = ViewOf(Square(0.3f, 0f, 0.2f));
pv.OrderedVisibleCells.Add(cellA);
pv.OrderedVisibleCells.Add(cellB);
pv.OutsideView.Add(Square(0f, 0.5f, 0.25f));
var asm = ClipFrameAssembler.Assemble(ClipFrame.NoClip(), pv);
Assert.Equal(4, asm.Frame.SlotCount); // slot 0 + two cells + one outside slice
Assert.Contains(cellA, asm.CellIdToSlot.Keys);
Assert.Contains(cellB, asm.CellIdToSlot.Keys);
Assert.NotEqual(0, asm.CellIdToSlot[cellA]);
Assert.NotEqual(0, asm.CellIdToSlot[cellB]);
Assert.NotEqual(asm.CellIdToSlot[cellA], asm.CellIdToSlot[cellB]);
Assert.Equal(4, asm.PerCellPlaneCounts[cellA]);
Assert.Equal(4, asm.PerCellPlaneCounts[cellB]);
Assert.True(asm.OutdoorVisible);
Assert.NotEqual(0, asm.OutdoorSlot);
Assert.Single(asm.OutsideViewSlices);
Assert.Equal(asm.OutdoorSlot, asm.OutsideViewSlices[0].Slot);
Assert.Equal(4, asm.OutsidePlaneCount);
Assert.Equal(0, asm.ScissorFallbacks);
}
[Fact]
public void NothingVisibleCell_IsExcludedFromSlotMap_AndNotAppended()
{
const uint cellA = 0xA9B40100;
const uint cellB = 0xA9B40101;
var pv = new PortalVisibilityFrame();
pv.CellViews[cellA] = ViewOf(Square(0f, 0f, 0.3f));
pv.CellViews[cellB] = new CellView();
pv.OrderedVisibleCells.Add(cellA);
pv.OrderedVisibleCells.Add(cellB);
var asm = ClipFrameAssembler.Assemble(ClipFrame.NoClip(), pv);
Assert.Equal(2, asm.Frame.SlotCount);
Assert.Contains(cellA, asm.CellIdToSlot.Keys);
Assert.DoesNotContain(cellB, asm.CellIdToSlot.Keys);
Assert.False(asm.OutdoorVisible);
Assert.Empty(asm.OutsideViewSlices);
Assert.Equal(0, asm.OutsidePlaneCount);
}
[Fact]
public void OutsideViewMultiPolygon_PreservesRetailSlices()
{
const uint cellA = 0xA9B40100;
var pv = new PortalVisibilityFrame();
pv.CellViews[cellA] = ViewOf(Square(0f, 0f, 0.3f));
pv.OrderedVisibleCells.Add(cellA);
pv.OutsideView.Add(Square(-0.5f, 0f, 0.1f));
pv.OutsideView.Add(Square(0.5f, 0f, 0.1f));
var asm = ClipFrameAssembler.Assemble(ClipFrame.NoClip(), pv);
Assert.True(asm.OutdoorVisible);
Assert.NotEqual(0, asm.OutdoorSlot);
Assert.Equal(2, asm.OutsideViewSlices.Length);
Assert.NotEqual(asm.OutsideViewSlices[0].Slot, asm.OutsideViewSlices[1].Slot);
Assert.Equal(4, asm.OutsidePlaneCount);
Assert.Equal(0, asm.ScissorFallbacks);
Assert.Equal(4, asm.Frame.SlotCount); // slot 0 + cell + two outside slices
}
[Fact]
public void CellMultiPolygonView_PreservesRetailViewSlices()
{
const uint cellA = 0xA9B40100;
var pv = new PortalVisibilityFrame();
pv.CellViews[cellA] = ViewOf(
Square(-0.4f, 0f, 0.1f),
Square(0.4f, 0f, 0.1f));
pv.OrderedVisibleCells.Add(cellA);
pv.OutsideView.Add(Square(0f, 0f, 0.3f));
var asm = ClipFrameAssembler.Assemble(ClipFrame.NoClip(), pv);
Assert.True(asm.CellIdToSlot[cellA] > 0);
Assert.Equal(2, asm.CellIdToViewSlots[cellA].Length);
Assert.Equal(2, asm.CellIdToViewSlices[cellA].Length);
Assert.NotEqual(asm.CellIdToViewSlots[cellA][0], asm.CellIdToViewSlots[cellA][1]);
Assert.Equal(4, asm.PerCellPlaneCounts[cellA]);
Assert.Single(asm.OutsideViewSlices);
Assert.Equal(4, asm.Frame.SlotCount); // slot 0 + two cell slices + outside slice
Assert.Equal(0, asm.ScissorFallbacks);
}
[Fact]
public void Assemble_OutsideViewWithExitPortal_HasOutsideViewTrue_AabbMatchesBounds()
{
var pv = new PortalVisibilityFrame();
pv.OutsideView.Add(Square(-0.3f, 0.2f, 0.25f));
var asm = ClipFrameAssembler.Assemble(ClipFrame.NoClip(), pv);
Assert.True(asm.HasOutsideView);
Assert.Single(asm.OutsideViewSlices);
var expected = new Vector4(
pv.OutsideView.MinX, pv.OutsideView.MinY,
pv.OutsideView.MaxX, pv.OutsideView.MaxY);
Assert.Equal(expected, asm.OutsideViewNdcAabb);
}
[Fact]
public void Assemble_OutsideViewMultiPolygon_PreservesSlicesAndUnionAabb()
{
var pv = new PortalVisibilityFrame();
pv.OutsideView.Add(Square(-0.6f, 0f, 0.15f));
pv.OutsideView.Add(Square(0.6f, 0f, 0.15f));
var asm = ClipFrameAssembler.Assemble(ClipFrame.NoClip(), pv);
Assert.True(asm.HasOutsideView);
Assert.Equal(2, asm.OutsideViewSlices.Length);
var expected = new Vector4(
pv.OutsideView.MinX, pv.OutsideView.MinY,
pv.OutsideView.MaxX, pv.OutsideView.MaxY);
Assert.Equal(expected, asm.OutsideViewNdcAabb);
}
[Fact]
public void Assemble_EmptyOutsideView_HasOutsideViewFalse_AabbZero()
{
const uint cellA = 0xA9B40100;
var pv = new PortalVisibilityFrame();
pv.CellViews[cellA] = ViewOf(Square(0f, 0f, 0.3f));
pv.OrderedVisibleCells.Add(cellA);
var asm = ClipFrameAssembler.Assemble(ClipFrame.NoClip(), pv);
Assert.False(asm.HasOutsideView);
Assert.Equal(Vector4.Zero, asm.OutsideViewNdcAabb);
}
[Fact]
public void Reset_ReusesFrame_NoSlotLeakAcrossAssemblies()
{
var frame = ClipFrame.NoClip();
var pv1 = new PortalVisibilityFrame();
pv1.CellViews[0xA9B40100] = ViewOf(Square(-0.3f, 0f, 0.2f));
pv1.CellViews[0xA9B40101] = ViewOf(Square(0.3f, 0f, 0.2f));
pv1.OrderedVisibleCells.Add(0xA9B40100);
pv1.OrderedVisibleCells.Add(0xA9B40101);
pv1.OutsideView.Add(Square(0f, 0.4f, 0.2f));
var asm1 = ClipFrameAssembler.Assemble(frame, pv1);
Assert.Equal(4, asm1.Frame.SlotCount);
var pv2 = new PortalVisibilityFrame();
pv2.CellViews[0xA9B40200] = ViewOf(Square(0f, 0f, 0.25f));
pv2.OrderedVisibleCells.Add(0xA9B40200);
var asm2 = ClipFrameAssembler.Assemble(frame, pv2);
Assert.Equal(2, asm2.Frame.SlotCount);
Assert.Contains(0xA9B40200, asm2.CellIdToSlot.Keys);
Assert.DoesNotContain(0xA9B40100, asm2.CellIdToSlot.Keys);
Assert.False(asm2.OutdoorVisible);
}
[Fact]
public void Assemble_ReusedAssembly_ClearsStateAndReusesExactLengthArrays()
{
const uint firstCell = 0xA9B40100;
const uint secondCell = 0xA9B40200;
var frame = ClipFrame.NoClip();
var reuse = new ClipFrameAssembly();
var first = new PortalVisibilityFrame();
first.CellViews[firstCell] = ViewOf(
Square(-0.4f, 0f, 0.15f),
Square(0.4f, 0f, 0.15f));
first.OrderedVisibleCells.Add(firstCell);
first.OutsideView.Add(Square(0f, 0.5f, 0.2f));
ClipFrameAssembly firstResult = ClipFrameAssembler.Assemble(frame, first, reuse);
Assert.Same(reuse, firstResult);
int sliceAllocations = reuse.SliceArrayAllocationCount;
int slotAllocations = reuse.SlotArrayAllocationCount;
var second = new PortalVisibilityFrame();
second.CellViews[secondCell] = ViewOf(
Square(-0.4f, 0f, 0.15f),
Square(0.4f, 0f, 0.15f));
second.OrderedVisibleCells.Add(secondCell);
second.OutsideView.Add(Square(0f, 0.5f, 0.2f));
for (int i = 0; i < 12; i++)
{
ClipFrameAssembly actual = ClipFrameAssembler.Assemble(frame, second, reuse);
Assert.Same(reuse, actual);
Assert.DoesNotContain(firstCell, actual.CellIdToViewSlices.Keys);
Assert.Contains(secondCell, actual.CellIdToViewSlices.Keys);
Assert.Equal(2, actual.CellIdToViewSlices[secondCell].Length);
Assert.Equal(2, actual.CellIdToViewSlots[secondCell].Length);
Assert.Single(actual.OutsideViewSlices);
Assert.Equal(sliceAllocations, reuse.SliceArrayAllocationCount);
Assert.Equal(slotAllocations, reuse.SlotArrayAllocationCount);
}
var empty = new PortalVisibilityFrame();
ClipFrameAssembly cleared = ClipFrameAssembler.Assemble(frame, empty, reuse);
Assert.Empty(cleared.CellIdToSlot);
Assert.Empty(cleared.CellIdToViewSlots);
Assert.Empty(cleared.CellIdToViewSlices);
Assert.Empty(cleared.PerCellPlaneCounts);
Assert.Empty(cleared.OutsideViewSlices);
}
[Fact]
public void Assemble_ReusedAssembly_ChangingCardinalityStaysBoundedThenReachesSteadyState()
{
const uint cellId = 0xA9B40100;
var frame = ClipFrame.NoClip();
var reuse = new ClipFrameAssembly();
static PortalVisibilityFrame FrameWithSlices(uint id, int count)
{
var portalFrame = new PortalVisibilityFrame();
var view = new CellView();
for (int i = 0; i < count; i++)
{
// The assembler consumes the retail view_poly list directly. Distinct placement is
// irrelevant to this cache-cardinality stress; avoid CellView dedup collapsing it.
view.Polygons.Add(Square(i * 0.001f, 0f, 0.0004f));
}
portalFrame.CellViews[id] = view;
portalFrame.OrderedVisibleCells.Add(id);
return portalFrame;
}
for (int count = 1; count <= 180; count++)
ClipFrameAssembler.Assemble(frame, FrameWithSlices(cellId, count), reuse);
// Flush the final live arrays into the cache too.
ClipFrameAssembler.Assemble(frame, new PortalVisibilityFrame(), reuse);
Assert.InRange(reuse.RetainedSliceItems, 0, ClipFrameAssembly.MaxRetainedSliceItems);
Assert.InRange(reuse.RetainedSlotItems, 0, ClipFrameAssembly.MaxRetainedSlotItems);
Assert.InRange(reuse.RetainedSliceArrays, 0, ClipFrameAssembly.MaxRetainedArraysPerPool);
Assert.InRange(reuse.RetainedSlotArrays, 0, ClipFrameAssembly.MaxRetainedArraysPerPool);
PortalVisibilityFrame steady = FrameWithSlices(cellId, 73);
ClipFrameAssembler.Assemble(frame, steady, reuse);
int sliceAllocations = reuse.SliceArrayAllocationCount;
int slotAllocations = reuse.SlotArrayAllocationCount;
for (int i = 0; i < 16; i++)
{
ClipFrameAssembler.Assemble(frame, steady, reuse);
Assert.Equal(sliceAllocations, reuse.SliceArrayAllocationCount);
Assert.Equal(slotAllocations, reuse.SlotArrayAllocationCount);
}
}
}

View file

@ -1,685 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Numerics;
using AcDream.App.Rendering;
using DatReaderWriter;
using DatReaderWriter.Options;
using Xunit;
using Xunit.Abstractions;
using DatEnvCell = DatReaderWriter.DBObjs.EnvCell;
using DatEnvironment = DatReaderWriter.DBObjs.Environment;
namespace AcDream.App.Tests.Rendering;
/// <summary>
/// §4 corner-press flood replay (2026-06-10). The §2b camera-collision hypothesis is
/// REFUTED (CameraCornerSealReplayTests, commit b21bb28): the eye legitimately enters
/// the neighbour room through the 0171↔0173↔0172 doorway chain. The user still sees
/// BACKGROUND at certain angles — so the defect is in the FLOOD/CLIP output for those
/// eye positions. This harness drives the REAL <see cref="PortalVisibilityBuilder.Build"/>
/// over the REAL Holtburg building cells (dat-loaded, mirroring EnvCellLandblockBuildBuilder)
/// along the captured corner eye path, and prints which cells the flood keeps/drops per
/// step. The player's room (0172) dropping while the player is visible on screen is the
/// background defect; the step where it happens pins the mechanism.
///
/// Geometry (corner-seal-capture.log + the b21bb28 room map): all cells share origin
/// (161.93, 7.50, 94.00), local = R180z·(worldorigin). The 0171-side doorway plane is
/// local x=4.10 (world x≈157.83), the 0172-side plane local x=3.90 (world x≈158.03);
/// the opening is 1.9 m wide, full height (z 94..96.5). Player dwell position during the
/// corner press: (159.94, 7.70, 94.00) in 0172; captured eyes hover near (157.4..157.5,
/// 7.91, 96.25) — inside 0171, 0.35 m past the doorway plane, near the ceiling.
/// </summary>
[Collection(CameraDiagnosticsCollection.Name)]
public class CornerFloodReplayTests
{
private readonly ITestOutputHelper _out;
public CornerFloodReplayTests(ITestOutputHelper output) => _out = output;
internal const uint Landblock = 0xA9B40000u;
private const uint EnvironmentFilePrefix = 0x0D000000u;
internal static string? ResolveDatDir()
{
var fromEnv = Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR");
if (!string.IsNullOrWhiteSpace(fromEnv) && Directory.Exists(fromEnv))
return fromEnv;
var def = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"Documents", "Asheron's Call");
return Directory.Exists(def) ? def : null;
}
/// <summary>
/// Dat → LoadedCell, mirroring EnvCellLandblockBuildBuilder.
/// field-for-field: portals (with OtherPortalId back-link), clip planes from the
/// portal polygon's first 3 verts + centroid InsideSide, full portal polygons in
/// cell-local space, local AABB, world transform from EnvCell.Position.
/// </summary>
internal static LoadedCell LoadCell(DatCollection dats, uint cellId)
=> LoadCell(dats, cellId, Vector3.Zero);
/// <summary>worldOffset: block offset for multi-landblock fixtures (dat cell
/// positions are landblock-local; a neighbour block needs ±192 per axis).</summary>
internal static LoadedCell LoadCell(DatCollection dats, uint cellId, Vector3 worldOffset)
{
var envCell = dats.Get<DatEnvCell>(cellId)
?? throw new InvalidOperationException($"EnvCell 0x{cellId:X8} not found");
var environment = dats.Get<DatEnvironment>(EnvironmentFilePrefix | envCell.EnvironmentId)
?? throw new InvalidOperationException($"Environment 0x{envCell.EnvironmentId:X8} not found");
if (!environment.Cells.TryGetValue(envCell.CellStructure, out var cellStruct) || cellStruct is null)
throw new InvalidOperationException($"CellStruct {envCell.CellStructure} missing");
var cellTransform =
Matrix4x4.CreateFromQuaternion(envCell.Position.Orientation) *
Matrix4x4.CreateTranslation(envCell.Position.Origin + worldOffset);
Matrix4x4.Invert(cellTransform, out var inverse);
var boundsMin = new Vector3(float.MaxValue);
var boundsMax = new Vector3(float.MinValue);
foreach (var kvp in cellStruct.VertexArray.Vertices)
{
var v = kvp.Value;
var pos = new Vector3(v.Origin.X, v.Origin.Y, v.Origin.Z);
boundsMin = Vector3.Min(boundsMin, pos);
boundsMax = Vector3.Max(boundsMax, pos);
}
if (boundsMin.X == float.MaxValue) { boundsMin = Vector3.Zero; boundsMax = Vector3.Zero; }
var portals = new List<CellPortalInfo>();
var clipPlanes = new List<PortalClipPlane>();
var portalPolygons = new List<Vector3[]>();
foreach (var portal in envCell.CellPortals)
{
portals.Add(new CellPortalInfo(
portal.OtherCellId, portal.PolygonId, (ushort)portal.Flags, portal.OtherPortalId));
if (cellStruct.Polygons.TryGetValue(portal.PolygonId, out var poly)
&& poly.VertexIds.Count >= 3
&& cellStruct.VertexArray.Vertices.TryGetValue((ushort)poly.VertexIds[0], out var v0)
&& cellStruct.VertexArray.Vertices.TryGetValue((ushort)poly.VertexIds[1], out var v1)
&& cellStruct.VertexArray.Vertices.TryGetValue((ushort)poly.VertexIds[2], out var v2))
{
var p0 = new Vector3(v0.Origin.X, v0.Origin.Y, v0.Origin.Z);
var p1 = new Vector3(v1.Origin.X, v1.Origin.Y, v1.Origin.Z);
var p2 = new Vector3(v2.Origin.X, v2.Origin.Y, v2.Origin.Z);
var normal = Vector3.Normalize(Vector3.Cross(p1 - p0, p2 - p0));
float d = -Vector3.Dot(normal, p0);
// InsideSide from the dat PortalSide bit — mirrors the production fix
// (EnvCellLandblockBuildBuilder): retail InitCell (0x005a4b70) + physics
// CellTransit use the dat bit; the old AABB-centroid guess mis-sided thin
// connectors (#186). Kept identical here so this replay stays a faithful mirror.
clipPlanes.Add(new PortalClipPlane
{
Normal = normal, D = d, InsideSide = ((ushort)portal.Flags & 0x2) == 0 ? 1 : 0,
});
}
else
{
clipPlanes.Add(default);
}
Vector3[] polyVerts = Array.Empty<Vector3>();
if (cellStruct.Polygons.TryGetValue(portal.PolygonId, out var portalPoly)
&& portalPoly.VertexIds.Count >= 3)
{
polyVerts = new Vector3[portalPoly.VertexIds.Count];
bool allResolved = true;
for (int vi = 0; vi < portalPoly.VertexIds.Count; vi++)
{
if (cellStruct.VertexArray.Vertices.TryGetValue(
(ushort)portalPoly.VertexIds[vi], out var pv))
polyVerts[vi] = new Vector3(pv.Origin.X, pv.Origin.Y, pv.Origin.Z);
else { allResolved = false; break; }
}
if (!allResolved) polyVerts = Array.Empty<Vector3>();
}
portalPolygons.Add(polyVerts);
}
uint lbPrefix = cellId & 0xFFFF0000u;
var visibleCells = new List<uint>();
if (envCell.VisibleCells is not null)
foreach (var lowId in envCell.VisibleCells)
visibleCells.Add(lbPrefix | lowId);
return new LoadedCell
{
CellId = cellId,
WorldPosition = envCell.Position.Origin + worldOffset,
WorldTransform = cellTransform,
InverseWorldTransform = inverse,
LocalBoundsMin = boundsMin,
LocalBoundsMax = boundsMax,
Portals = portals,
ClipPlanes = clipPlanes,
PortalPolygons = portalPolygons,
VisibleCells = visibleCells,
SeenOutside = envCell.Flags.HasFlag(DatReaderWriter.Enums.EnvCellFlags.SeenOutside),
};
}
internal static Dictionary<uint, LoadedCell> LoadBuilding(DatCollection dats)
{
var cells = new Dictionary<uint, LoadedCell>();
for (uint low = 0x016Fu; low <= 0x0175u; low++)
{
uint id = Landblock | low;
cells[id] = LoadCell(dats, id);
}
return cells;
}
// Production projection: ChaseCamera/FlyCamera use FovY ~1.2, near 1, far 5000,
// 1280x720 (the capture's viewport). The flood's clip is near-independent
// (PortalProjection header), so near/far exactness is not load-bearing.
private static Matrix4x4 ViewProjFor(Vector3 eye, Vector3 lookAt)
{
var view = Matrix4x4.CreateLookAt(eye, lookAt, Vector3.UnitZ);
var proj = Matrix4x4.CreatePerspectiveFieldOfView(1.2f, 1280f / 720f, 1f, 5000f);
return view * proj;
}
// World-x of the doorway planes (local x=4.10 / 3.90 under the R180z transform
// about origin x=161.93).
private const float Plane0171X = 161.93f - 4.10f; // ≈157.83
private const float Plane0172X = 161.93f - 3.90f; // ≈158.03
private static uint RootCellFor(float eyeX) =>
eyeX > Plane0172X ? (Landblock | 0x0172u)
: eyeX > Plane0171X ? (Landblock | 0x0173u)
: (Landblock | 0x0171u);
/// <summary>
/// Diagnostic: full [pv-trace] decision log for one GOOD step (47) vs one GLITCH
/// step (48) of the sweep below — 2 cm apart, root 0171 both, yet 0172 (and its
/// downstream 016F + outside) vanish from the flood at 48. The trace diff pins the
/// exact gate that flips.
/// </summary>
[Fact]
[Trait("Lane", "InstalledDat")]
[Trait("Purpose", "Diagnostic")]
public void Diagnostic_TraceGoodVsGlitchStep()
{
var datDir = ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var cells = LoadBuilding(dats);
Func<uint, LoadedCell?> lookup = id => cells.TryGetValue(id, out var c) ? c : null;
var player = new Vector3(159.936676f, 7.701012f, 94.000000f);
var pivot = player + new Vector3(0f, 0f, 1.5f);
foreach (int i in new[] { 47, 48 })
{
float ex = 158.43f - i * 0.02f;
var eye = new Vector3(ex, 7.912722f, 96.248833f);
uint root = RootCellFor(ex);
var sw = new StringWriter();
var prev = Console.Out;
PortalVisibilityFrame frame;
try
{
Console.SetOut(sw);
AcDream.Core.Rendering.RenderingDiagnostics.ProbeFlapEnabled = true;
frame = PortalVisibilityBuilder.Build(cells[root], eye, lookup, ViewProjFor(eye, pivot));
}
finally
{
AcDream.Core.Rendering.RenderingDiagnostics.ProbeFlapEnabled = false;
Console.SetOut(prev);
}
_out.WriteLine($"########## step {i} (eyeX={ex:F2}) ##########");
foreach (var line in sw.ToString().Split('\n', StringSplitOptions.RemoveEmptyEntries))
_out.WriteLine(line.TrimEnd());
// Per-hop region vertices: where does the sliver come from?
foreach (uint low in new uint[] { 0x0171u, 0x0173u, 0x0172u })
{
if (!frame.CellViews.TryGetValue(Landblock | low, out var view))
{
_out.WriteLine($" view 0x{low:X4}: (absent)");
continue;
}
foreach (var p in view.Polygons)
{
string verts = "";
foreach (var v in p.Vertices)
verts += System.FormattableString.Invariant($" ({v.X:F5},{v.Y:F5})");
_out.WriteLine($" view 0x{low:X4} [{p.Vertices.Length}]:{verts}");
}
if (view.Polygons.Count == 0)
_out.WriteLine($" view 0x{low:X4}: 0 polygons (rejected by Add)");
}
}
}
/// <summary>
/// §4 conformance gate (2026-06-08 handoff §7's pre-gate, realized 2026-06-10):
/// a smooth monotonic eye moving through/near a doorway must produce a STABLE,
/// monotone flood — the full cell chain present on every step, the outside view
/// always reached, and the player-room region never collapsing nor oscillating.
/// Before the homogeneous-reciprocal fix this failed on ~10 of 61 steps (room
/// 0172 vanished entirely or collapsed to a sub-pixel sliver — the corner /
/// transition background strobe). Guards the ApplyReciprocalClip pipeline.
/// </summary>
[Fact]
[Trait("Lane", "InstalledDat")]
public void CornerSweep_FloodIsCompleteAndMonotone()
{
var datDir = ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var cells = LoadBuilding(dats);
Func<uint, LoadedCell?> lookup = id => cells.TryGetValue(id, out var c) ? c : null;
var player = new Vector3(159.936676f, 7.701012f, 94.000000f);
var pivot = player + new Vector3(0f, 0f, 1.5f);
// W=0 port (2026-06-11) — the criterion is retail's, not the rescue's:
// - Cells BEHIND the camera are NOT required (retail polyClipFinish clips an
// all-behind portal to empty; the rescue used to flood them anyway). The eye
// looks AT the player throughout, so the doorway chain is behind the camera
// until the eye recedes through it: require 0173 from root=0173 on, 0171
// from root=0171 on. 0172 (the looked-at room with the player) is required
// at EVERY step — that is THE user-visible §4 invariant.
// - The two KNIFE-EDGE steps (eye exactly ON a doorway plane, the sweep grid
// lands on the plane constants) propagate retail's zero-area degenerate view:
// the chain stays in the draw list (cells draw whole) but the 0172 region is
// legitimately zero-area and the onward flood (016F/outside) legitimately
// dies for that frame — exempt those assertions there.
// - Monotone shrink holds WITHIN a root regime; the root flip is a legitimate
// discontinuity (FullScreen root view -> portal-clipped view).
var failures = new List<string>();
float prevArea = float.MaxValue;
uint prevRoot = 0;
for (int i = 0; i <= 60; i++)
{
float ex = 158.43f - i * 0.02f;
var eye = new Vector3(ex, 7.912722f, 96.248833f);
uint root = RootCellFor(ex);
bool knifeEdge = MathF.Abs(ex - Plane0172X) < 0.005f || MathF.Abs(ex - Plane0171X) < 0.005f;
var frame = PortalVisibilityBuilder.Build(
cells[root], eye, lookup, ViewProjFor(eye, pivot));
// Knife-edge steps: the eye sits EXACTLY on a cell-boundary plane, so the
// root pick itself is ambiguous (production BSP picks either side; a damped
// float eye never lands exactly on the plane). The portal whose plane
// contains the eye is ~perpendicular to the gaze here — genuinely
// off-screen, retail's screen-bounded clip floods nothing through it from
// the far-side root. Require only the root; the neighbouring steps (±2 cm,
// where the real strobe class lived) carry the full criterion.
var required = new List<uint>();
if (knifeEdge)
{
required.Add(root & 0xFFFFu);
}
else
{
required.Add(0x0172u);
if (root != (Landblock | 0x0172u)) required.Add(0x0173u);
if (root == (Landblock | 0x0171u)) required.Add(0x0171u);
required.Add(0x016Fu);
}
foreach (uint low in required)
if (!frame.OrderedVisibleCells.Contains(Landblock | low))
failures.Add($"step {i}: 0x{low:X4} missing from flood (root=0x{root & 0xFFFFu:X4}{(knifeEdge ? ", knife-edge" : "")})");
if (!knifeEdge && frame.OutsideView.Polygons.Count == 0)
failures.Add($"step {i}: outside view empty");
float area = 0f;
if (frame.CellViews.TryGetValue(Landblock | 0x0172u, out var view))
foreach (var p in view.Polygons)
area += MathF.Max(0f, p.MaxX - p.MinX) * MathF.Max(0f, p.MaxY - p.MinY);
if (!knifeEdge && area < 0.5f)
failures.Add(System.FormattableString.Invariant(
$"step {i}: 0172 region collapsed (area={area:F3})"));
// Monotone shrink as the eye recedes — allow float-noise upticks only.
// Reset at root flips + knife-edge frames (legitimate discontinuities).
if (root == prevRoot && !knifeEdge && prevArea != float.MaxValue && area > prevArea + 0.01f)
failures.Add(System.FormattableString.Invariant(
$"step {i}: 0172 region grew {prevArea:F3}->{area:F3} (oscillation)"));
prevArea = knifeEdge ? float.MaxValue : area;
prevRoot = root;
}
Assert.True(failures.Count == 0,
"Flood instability under a monotonic eye sweep (the §4 strobe):\n "
+ string.Join("\n ", failures));
}
/// <summary>
/// #120 repro (T5 gate, 2026-06-11): the T5 launch log fired
/// `[pv-ERROR] in-place propagation tripwire at depth 128` on cottage
/// interior cells 0xA9B40175/0174 (+0162, a different building) while
/// the user walked the doorways — i.e. the eye-on-portal-plane regime.
/// Sweep the eye ACROSS every portal plane of every cell in this
/// building (±6 cm in 5 mm steps, looking through the opening), seeding
/// <see cref="PortalVisibilityBuilder.Build"/> from BOTH sides' cells.
/// The in-place growth's fixpoint invariant must hold at every step —
/// the tripwire count stays 0.
/// </summary>
[Fact]
[Trait("Lane", "InstalledDat")]
public void PortalPlaneCrossings_InPlacePropagationConverges()
{
var datDir = ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var cells = LoadBuilding(dats);
Func<uint, LoadedCell?> lookup = id => cells.TryGetValue(id, out var c) ? c : null;
PortalVisibilityBuilder.ConvergenceTripwireCount = 0;
var firings = new List<string>();
foreach (var cell in cells.Values)
{
for (int i = 0; i < cell.Portals.Count && i < cell.PortalPolygons.Count; i++)
{
var poly = cell.PortalPolygons[i];
if (poly == null || poly.Length < 3) continue;
if (i >= cell.ClipPlanes.Count) continue;
var plane = cell.ClipPlanes[i];
if (plane.Normal.LengthSquared() < 1e-6f) continue;
// Portal centroid + plane normal in WORLD space.
var centroidLocal = Vector3.Zero;
foreach (var v in poly) centroidLocal += v;
centroidLocal /= poly.Length;
var centroidWorld = Vector3.Transform(centroidLocal, cell.WorldTransform);
var normalWorld = Vector3.Normalize(
Vector3.TransformNormal(plane.Normal, cell.WorldTransform));
uint neighbourId = cell.Portals[i].OtherCellId == 0xFFFF
? 0u
: (Landblock | cell.Portals[i].OtherCellId);
var roots = new List<LoadedCell> { cell };
if (neighbourId != 0u && cells.TryGetValue(neighbourId, out var nb))
roots.Add(nb);
for (int step = -12; step <= 12; step++)
{
var eye = centroidWorld + normalWorld * (step * 0.005f);
// Look through the opening (along -normal), slightly down
// — the portal fills the view, maximizing flood activity.
var lookAt = centroidWorld - normalWorld * 2f + new Vector3(0f, 0f, -0.2f);
foreach (var root in roots)
{
int before = PortalVisibilityBuilder.ConvergenceTripwireCount;
PortalVisibilityBuilder.Build(root, eye, lookup, ViewProjFor(eye, lookAt));
int after = PortalVisibilityBuilder.ConvergenceTripwireCount;
if (after != before)
firings.Add(System.FormattableString.Invariant(
$"cell=0x{cell.CellId:X8} portal#{i}->0x{cell.Portals[i].OtherCellId:X4} step={step} root=0x{root.CellId:X8} eye=({eye.X:F4},{eye.Y:F4},{eye.Z:F4})"));
}
}
}
}
Assert.True(firings.Count == 0,
"#120: in-place propagation convergence tripwire fired during the " +
"portal-plane sweep:\n " + string.Join("\n ", firings));
}
/// <summary>
/// #120 repro attempt 2: eyes INSIDE each cell's volume (3×3 XY grid ×
/// 2 Z levels) with full yaw (8) × pitch (3) direction sweeps — the
/// walking-and-turning regime of the T5 session, including the steep
/// pitches of the cellar stairs. Same invariant: the tripwire count
/// stays 0 across every Build.
/// </summary>
[Fact]
[Trait("Lane", "InstalledDat")]
public void InCellDirectionSweep_InPlacePropagationConverges()
{
var datDir = ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var cells = LoadBuilding(dats);
Func<uint, LoadedCell?> lookup = id => cells.TryGetValue(id, out var c) ? c : null;
PortalVisibilityBuilder.ConvergenceTripwireCount = 0;
var firings = new List<string>();
int builds = 0;
foreach (var cell in cells.Values)
{
var lo = cell.LocalBoundsMin;
var hi = cell.LocalBoundsMax;
if (hi.X - lo.X < 0.05f || hi.Y - lo.Y < 0.05f) continue;
for (int gx = 0; gx < 3; gx++)
for (int gy = 0; gy < 3; gy++)
for (int gz = 0; gz < 2; gz++)
{
var local = new Vector3(
lo.X + (hi.X - lo.X) * (0.2f + 0.3f * gx),
lo.Y + (hi.Y - lo.Y) * (0.2f + 0.3f * gy),
lo.Z + (hi.Z - lo.Z) * (gz == 0 ? 0.3f : 0.8f));
var eye = Vector3.Transform(local, cell.WorldTransform);
for (int yaw = 0; yaw < 8; yaw++)
for (int pitch = -1; pitch <= 1; pitch++)
{
float a = yaw * MathF.PI / 4f;
var dir = new Vector3(
MathF.Cos(a), MathF.Sin(a), pitch * 0.9f);
var lookAt = eye + Vector3.Normalize(dir) * 3f;
int before = PortalVisibilityBuilder.ConvergenceTripwireCount;
PortalVisibilityBuilder.Build(cell, eye, lookup, ViewProjFor(eye, lookAt));
builds++;
int after = PortalVisibilityBuilder.ConvergenceTripwireCount;
if (after != before)
firings.Add(System.FormattableString.Invariant(
$"cell=0x{cell.CellId:X8} eye=({eye.X:F3},{eye.Y:F3},{eye.Z:F3}) yaw={yaw} pitch={pitch}"));
}
}
}
_out.WriteLine($"builds={builds} firings={firings.Count}");
Assert.True(firings.Count == 0,
"#120: convergence tripwire fired during the in-cell direction sweep:\n "
+ string.Join("\n ", firings));
}
/// <summary>
/// Diagnostic: microscope on the failing hop. Replays the two portal hops
/// (0171→0173, then 0173→0172) through the PUBLIC PortalProjection APIs for the
/// good step (47) and the glitch step (48), printing the homogeneous subject
/// vertices and full-precision outputs at every stage — the concrete numbers for
/// the collapsing intersection.
/// </summary>
[Fact]
[Trait("Lane", "InstalledDat")]
[Trait("Purpose", "Diagnostic")]
public void Diagnostic_Hop2Microscope()
{
var datDir = ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var cells = LoadBuilding(dats);
var c171 = cells[Landblock | 0x0171u];
var c173 = cells[Landblock | 0x0173u];
var player = new Vector3(159.936676f, 7.701012f, 94.000000f);
var pivot = player + new Vector3(0f, 0f, 1.5f);
// 0171's portal to 0173 is index 1; 0173's portal to 0172 is index 1 (room map).
var poly171to173 = c171.PortalPolygons[1];
var poly173to172 = c173.PortalPolygons[1];
foreach (int i in new[] { 47, 48 })
{
float ex = 158.43f - i * 0.02f;
var eye = new Vector3(ex, 7.912722f, 96.248833f);
var vp = ViewProjFor(eye, pivot);
_out.WriteLine($"########## step {i} (eyeX={ex:F2}) ##########");
// Hop 1: 0171 -> 0173 against the full screen.
var subj1 = PortalProjection.ProjectToClip(poly171to173, c171.WorldTransform, vp);
DumpClip("hop1 subject (0171->0173 portal, clip space)", subj1);
var region173 = PortalProjection.ClipToRegion(subj1, new[]
{
new Vector2(-1f, -1f), new Vector2(1f, -1f), new Vector2(1f, 1f), new Vector2(-1f, 1f),
});
DumpNdc("hop1 out = 0173 region", region173);
if (region173.Length < 3) continue;
// Hop 2: 0173 -> 0172 against the 0173 region.
var subj2 = PortalProjection.ProjectToClip(poly173to172, c173.WorldTransform, vp);
DumpClip("hop2 subject (0173->0172 portal, clip space)", subj2);
var region172 = PortalProjection.ClipToRegion(subj2, region173);
DumpNdc("hop2 out = 0172 region", region172);
}
}
/// <summary>Scratch: the homogeneous reciprocal primitive in isolation, on the
/// synthetic fixture geometry that Build_AppliesReciprocalOtherPortalClip uses.</summary>
[Fact]
[Trait("Purpose", "Diagnostic")]
public void Scratch_ReciprocalPrimitive_SyntheticPair()
{
var view = Matrix4x4.CreateLookAt(Vector3.Zero, new Vector3(0, 0, -1), Vector3.UnitY);
var proj = Matrix4x4.CreatePerspectiveFieldOfView(1.2f, 1.0f, 0.1f, 1000f);
var vp = view * proj;
Vector3[] Quad(float cx, float cy, float hw, float hh, float z) => new[]
{
new Vector3(cx - hw, cy - hh, z), new Vector3(cx + hw, cy - hh, z),
new Vector3(cx + hw, cy + hh, z), new Vector3(cx - hw, cy + hh, z),
};
var wide = Quad(0f, 0f, 0.9f, 0.9f, -3f);
var narrow = Quad(0f, 0f, 0.3f, 0.9f, -3f);
// Near-side region: wide portal clipped against the full screen.
var wideClip = PortalProjection.ProjectToClip(wide, Matrix4x4.Identity, vp);
DumpClip("wide subject", wideClip);
var wideRegion = PortalProjection.ClipToRegion(wideClip, new[]
{
new Vector2(-1f, -1f), new Vector2(1f, -1f), new Vector2(1f, 1f), new Vector2(-1f, 1f),
});
DumpNdc("wide region (near-side)", wideRegion);
// Reciprocal: narrow portal, homogeneous, clipped against the wide region.
var narrowClip = PortalProjection.ProjectToClip(narrow, Matrix4x4.Identity, vp);
DumpClip("narrow subject (reciprocal)", narrowClip);
var tightened = PortalProjection.ClipToRegion(narrowClip, wideRegion);
DumpNdc("tightened = narrow ∩ wide", tightened);
// Now the FULL Build on the same synthetic pair (mirrors
// Build_AppliesReciprocalOtherPortalClip) with output dumps.
var a = new LoadedCell
{
CellId = 0x0001, WorldTransform = Matrix4x4.Identity, InverseWorldTransform = Matrix4x4.Identity,
Portals = new List<CellPortalInfo> { new CellPortalInfo(0x0002, 0, 0, 0) },
};
a.PortalPolygons.Add(wide);
var b = new LoadedCell
{
CellId = 0x0002, WorldTransform = Matrix4x4.Identity, InverseWorldTransform = Matrix4x4.Identity,
Portals = new List<CellPortalInfo> { new CellPortalInfo(0x0001, 0, 0, 0) },
};
b.PortalPolygons.Add(narrow);
var all = new Dictionary<uint, LoadedCell> { [0x0001u] = a, [0x0002u] = b };
var f = PortalVisibilityBuilder.Build(
a, Vector3.Zero, id => all.TryGetValue(id, out var c) ? c : null, vp);
foreach (var kv in f.CellViews)
foreach (var p in kv.Value.Polygons)
{
string s = "";
foreach (var v in p.Vertices)
s += System.FormattableString.Invariant($" ({v.X:F4},{v.Y:F4})");
_out.WriteLine($" Build view 0x{kv.Key:X4} [{p.Vertices.Length}]:{s}");
}
}
private void DumpClip(string label, Vector4[] verts)
{
string s = "";
foreach (var v in verts)
s += System.FormattableString.Invariant($" ({v.X:F4},{v.Y:F4},{v.Z:F4},w={v.W:F4})");
_out.WriteLine($" {label} [{verts.Length}]:{s}");
}
private void DumpNdc(string label, Vector2[] verts)
{
string s = "";
foreach (var v in verts)
s += System.FormattableString.Invariant($" ({v.X:F7},{v.Y:F7})");
_out.WriteLine($" {label} [{verts.Length}]:{s}");
}
/// <summary>
/// Diagnostic (no assertions yet): sweep the eye along the corner-press path —
/// from inside the player's room (0172), across the doorway threshold (0173),
/// into the neighbour room (0171) — looking back at the player throughout, and
/// print the flood result per step. The defect signature: the PLAYER's room
/// (0172) absent from the flood (or its view region empty) while the root is
/// 0171/0173 — the screen area where the player stands then shows background.
/// </summary>
[Fact]
[Trait("Lane", "InstalledDat")]
[Trait("Purpose", "Diagnostic")]
public void Diagnostic_CornerPress_FloodAcrossDoorwayPlane()
{
var datDir = ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var cells = LoadBuilding(dats);
Func<uint, LoadedCell?> lookup = id => cells.TryGetValue(id, out var c) ? c : null;
// The captured corner scenario: player parked in 0172, eye orbiting near the
// doorway plane at head height (z=96.25), looking at the pivot.
var player = new Vector3(159.936676f, 7.701012f, 94.000000f);
var pivot = player + new Vector3(0f, 0f, 1.5f);
_out.WriteLine("step | eyeX (worldX | dPlane0171) | root | flood | 0171/0173/0172 polys | outside");
for (int i = 0; i <= 60; i++)
{
// Sweep world-x 158.43 (inside 0172) -> 157.23 (inside 0171), 2 cm steps,
// crossing the 0172 plane (~158.03) and the 0171 plane (~157.83).
float ex = 158.43f - i * 0.02f;
var eye = new Vector3(ex, 7.912722f, 96.248833f);
uint root = RootCellFor(ex);
var frame = PortalVisibilityBuilder.Build(
cells[root], eye, lookup, ViewProjFor(eye, pivot));
string flood = "";
foreach (uint id in frame.OrderedVisibleCells)
flood += $" {id & 0xFFFFu:X4}";
string PolyInfo(uint low)
{
uint id = Landblock | low;
if (!frame.CellViews.TryGetValue(id, out var view)) return "-";
int n = view.Polygons.Count;
float area = 0f;
foreach (var p in view.Polygons)
area += MathF.Max(0f, p.MaxX - p.MinX) * MathF.Max(0f, p.MaxY - p.MinY);
return System.FormattableString.Invariant($"{n}:{area:F3}");
}
string p71 = PolyInfo(0x0171u);
string p73 = PolyInfo(0x0173u);
string p72 = PolyInfo(0x0172u);
_out.WriteLine(System.FormattableString.Invariant(
$"{i,3} | {ex:F2} ({ex - Plane0171X,6:F3}) | {root & 0xFFFFu:X4} |{flood} | {p71} / {p73} / {p72} | out={frame.OutsideView.Polygons.Count}"));
}
}
}

View file

@ -738,7 +738,6 @@ public sealed class EnvCellAlphaDrawSourceTests
particleRenderer: null,
portalDepthMask: null,
Queue,
(WorldRenderDiagnostics)RuntimeHelpers.GetUninitializedObject(typeof(WorldRenderDiagnostics)),
(TerrainDrawDiagnosticsController)RuntimeHelpers.GetUninitializedObject(
typeof(TerrainDrawDiagnosticsController)));
Leaf = new WalkProductionLeafRenderer(

View file

@ -1,46 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using AcDream.App.Rendering;
using Xunit;
namespace AcDream.App.Tests.Rendering;
public class IndoorDrawPlanTests
{
private static ViewPolygon Quad() => new(new[]
{ new Vector2(-1, -1), new Vector2(1, -1), new Vector2(1, 1), new Vector2(-1, 1) });
private static PortalVisibilityFrame FrameWith(params uint[] orderedCells)
{
var f = new PortalVisibilityFrame();
foreach (var id in orderedCells)
{
f.OrderedVisibleCells.Add(id);
var v = new CellView(); v.Add(Quad());
f.CellViews[id] = v;
}
return f;
}
[Fact]
public void ShellPass_IncludesEveryVisibleCell_NoFilter()
{
// The grey bug: a cell in OrderedVisibleCells must NEVER be dropped from the
// shell pass. (Old code dropped cells lacking a ClipFrameAssembler slot.)
var f = FrameWith(0x01, 0x02, 0x03);
var plan = IndoorDrawPlan.ShellPass(f);
Assert.Equal(new uint[] { 0x03, 0x02, 0x01 }, plan.Select(e => e.CellId).ToArray()); // reverse = far→near
Assert.All(plan, e => Assert.NotEmpty(e.Slices));
}
[Fact]
public void ShellPass_ExcludesEmptyViewCells()
{
var f = FrameWith(0x01);
f.OrderedVisibleCells.Add(0x02); // present in the list…
f.CellViews[0x02] = new CellView(); // …but empty view → not drawable
var plan = IndoorDrawPlan.ShellPass(f);
Assert.Equal(new uint[] { 0x01 }, plan.Select(e => e.CellId).ToArray());
}
}

View file

@ -0,0 +1,17 @@
namespace AcDream.App.Tests.Rendering;
internal static class InstalledDatTestPath
{
internal static string? Resolve()
{
string? configured = Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR");
if (!string.IsNullOrWhiteSpace(configured) && Directory.Exists(configured))
return configured;
string defaultPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"Documents",
"Asheron's Call");
return Directory.Exists(defaultPath) ? defaultPath : null;
}
}

View file

@ -1,243 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Numerics;
using AcDream.App.Rendering;
using DatReaderWriter;
using DatReaderWriter.Options;
using Xunit;
using Xunit.Abstractions;
using DatEnvCell = DatReaderWriter.DBObjs.EnvCell;
using DatEnvironment = DatReaderWriter.DBObjs.Environment;
namespace AcDream.App.Tests.Rendering;
/// <summary>
/// #113 phantom exterior staircase (2026-06-10): the Holtburg meeting hall
/// (AAB3 building[0], model 0x010014C3 at AAB3-local (36,84,116)) shows its
/// INTERIOR stair cells painted across the west exterior wall when viewed from
/// outside. Attribution: retail clips drawn cell geometry to the accumulated
/// portal aperture (Render::set_view portal scissor + polyClipFinish with
/// planeMask=0xffffffff, decomp :343750/:427922); our DrawEnvCellShells routes
/// per-cell clip planes via ClipFrameAssembler — but a slice whose aperture
/// polygon exceeds the 8-plane budget falls back to slot 0 ("the renderer uses
/// scissor for passes that need that fallback", ClipFrameAssembler.cs:13-15)
/// and the shell pass never implemented that scissor → the cell draws fully
/// unclipped. This harness replays the production outdoor per-building flood
/// (PortalVisibilityBuilder.ConstructViewBuilding, the R-A2 path) + the
/// production assembler from the user's click-time viewpoint and pins which
/// hall cells draw and with what clip.
/// </summary>
[Trait("Lane", "InstalledDat")]
public class Issue113MeetingHallFloodTests
{
private readonly ITestOutputHelper _out;
public Issue113MeetingHallFloodTests(ITestOutputHelper output) => _out = output;
private const uint Landblock = 0xAAB30000u;
private const uint EnvironmentFilePrefix = 0x0D000000u;
private static string? ResolveDatDir()
{
var fromEnv = Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR");
if (!string.IsNullOrWhiteSpace(fromEnv) && Directory.Exists(fromEnv))
return fromEnv;
var def = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"Documents", "Asheron's Call");
return Directory.Exists(def) ? def : null;
}
// Mirrors CornerFloodReplayTests.LoadCell (EnvCellLandblockBuildBuilder shape).
private static LoadedCell LoadCell(DatCollection dats, uint cellId)
{
var envCell = dats.Get<DatEnvCell>(cellId)
?? throw new InvalidOperationException($"EnvCell 0x{cellId:X8} not found");
var environment = dats.Get<DatEnvironment>(EnvironmentFilePrefix | envCell.EnvironmentId)
?? throw new InvalidOperationException($"Environment 0x{envCell.EnvironmentId:X8} not found");
if (!environment.Cells.TryGetValue(envCell.CellStructure, out var cellStruct) || cellStruct is null)
throw new InvalidOperationException($"CellStruct {envCell.CellStructure} missing");
var cellTransform =
Matrix4x4.CreateFromQuaternion(envCell.Position.Orientation) *
Matrix4x4.CreateTranslation(envCell.Position.Origin);
Matrix4x4.Invert(cellTransform, out var inverse);
var boundsMin = new Vector3(float.MaxValue);
var boundsMax = new Vector3(float.MinValue);
foreach (var kvp in cellStruct.VertexArray.Vertices)
{
var v = kvp.Value;
var pos = new Vector3(v.Origin.X, v.Origin.Y, v.Origin.Z);
boundsMin = Vector3.Min(boundsMin, pos);
boundsMax = Vector3.Max(boundsMax, pos);
}
if (boundsMin.X == float.MaxValue) { boundsMin = Vector3.Zero; boundsMax = Vector3.Zero; }
var portals = new List<CellPortalInfo>();
var clipPlanes = new List<PortalClipPlane>();
var portalPolygons = new List<Vector3[]>();
foreach (var portal in envCell.CellPortals)
{
portals.Add(new CellPortalInfo(
portal.OtherCellId, portal.PolygonId, (ushort)portal.Flags, portal.OtherPortalId));
if (cellStruct.Polygons.TryGetValue(portal.PolygonId, out var poly)
&& poly.VertexIds.Count >= 3
&& cellStruct.VertexArray.Vertices.TryGetValue((ushort)poly.VertexIds[0], out var v0)
&& cellStruct.VertexArray.Vertices.TryGetValue((ushort)poly.VertexIds[1], out var v1)
&& cellStruct.VertexArray.Vertices.TryGetValue((ushort)poly.VertexIds[2], out var v2))
{
var p0 = new Vector3(v0.Origin.X, v0.Origin.Y, v0.Origin.Z);
var p1 = new Vector3(v1.Origin.X, v1.Origin.Y, v1.Origin.Z);
var p2 = new Vector3(v2.Origin.X, v2.Origin.Y, v2.Origin.Z);
var normal = Vector3.Normalize(Vector3.Cross(p1 - p0, p2 - p0));
float d = -Vector3.Dot(normal, p0);
// InsideSide from the dat PortalSide bit — mirrors the production fix
// (EnvCellLandblockBuildBuilder): retail InitCell (0x005a4b70) + physics
// CellTransit use the dat bit; the old AABB-centroid guess mis-sided thin
// connectors (#186). Kept identical here so this replay stays a faithful mirror.
clipPlanes.Add(new PortalClipPlane
{
Normal = normal, D = d, InsideSide = ((ushort)portal.Flags & 0x2) == 0 ? 1 : 0,
});
}
else
{
clipPlanes.Add(default);
}
Vector3[] polyVerts = Array.Empty<Vector3>();
if (cellStruct.Polygons.TryGetValue(portal.PolygonId, out var portalPoly)
&& portalPoly.VertexIds.Count >= 3)
{
polyVerts = new Vector3[portalPoly.VertexIds.Count];
bool allResolved = true;
for (int vi = 0; vi < portalPoly.VertexIds.Count; vi++)
{
if (cellStruct.VertexArray.Vertices.TryGetValue(
(ushort)portalPoly.VertexIds[vi], out var pv))
polyVerts[vi] = new Vector3(pv.Origin.X, pv.Origin.Y, pv.Origin.Z);
else { allResolved = false; break; }
}
if (!allResolved) polyVerts = Array.Empty<Vector3>();
}
portalPolygons.Add(polyVerts);
}
uint lbPrefix = cellId & 0xFFFF0000u;
var visibleCells = new List<uint>();
if (envCell.VisibleCells is not null)
foreach (var lowId in envCell.VisibleCells)
visibleCells.Add(lbPrefix | lowId);
return new LoadedCell
{
CellId = cellId,
WorldPosition = envCell.Position.Origin,
WorldTransform = cellTransform,
InverseWorldTransform = inverse,
LocalBoundsMin = boundsMin,
LocalBoundsMax = boundsMax,
Portals = portals,
ClipPlanes = clipPlanes,
PortalPolygons = portalPolygons,
VisibleCells = visibleCells,
SeenOutside = envCell.Flags.HasFlag(DatReaderWriter.Enums.EnvCellFlags.SeenOutside),
};
}
private static Dictionary<uint, LoadedCell> LoadHall(DatCollection dats)
{
var cells = new Dictionary<uint, LoadedCell>();
for (uint low = 0x0100u; low <= 0x0106u; low++)
{
uint id = Landblock | low;
cells[id] = LoadCell(dats, id);
}
return cells;
}
private static Matrix4x4 ViewProjFor(Vector3 eye, Vector3 lookAt)
{
var view = Matrix4x4.CreateLookAt(eye, lookAt, Vector3.UnitZ);
var proj = Matrix4x4.CreatePerspectiveFieldOfView(1.2f, 1280f / 720f, 0.1f, 5000f);
return view * proj;
}
/// <summary>
/// Replay the production outdoor flood + assembler from the user's
/// click-time viewpoint (player AAB3-local (24.4, 83.4, 116), chase eye
/// behind at eye height, looking at the hall's west door). Prints every
/// hall cell that enters the drawn set with its slice slot / plane count /
/// NDC AABB. The #113 defect signature: a hall cell drawn with a slice
/// that has Slot==0 AND zero planes (the assembler's scissor-fallback
/// contract) — DrawEnvCellShells draws that slice fully unclipped, so the
/// interior staircase paints across the exterior wall.
/// </summary>
[Fact]
public void WestApproach_HallCellSlices_HaveUsableClip()
{
var datDir = ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var cells = LoadHall(dats);
Func<uint, LoadedCell?> lookup = id => cells.TryGetValue(id, out var c) ? c : null;
// Chase-camera sweep along the west approach: player walked from the
// cottage (west) toward the hall; eye behind the player at z≈117.6,
// looking at the hall's west door (local (29.4, 84.0, 117.3)).
var lookAt = new Vector3(29.4f, 84.0f, 117.3f);
int unclippedDrawn = 0;
var report = new List<string>();
for (int i = 0; i <= 20; i++)
{
float ex = 14f + i * 0.5f; // eye local x 14 → 24
var eye = new Vector3(ex, 83.4f, 117.6f);
var vp = ViewProjFor(eye, lookAt);
var frame = PortalVisibilityBuilder.ConstructViewBuilding(
cells.Values.ToList(), eye, lookup, vp, maxSeedDistance: 48f);
var clipFrame = ClipFrame.NoClip();
var assembly = ClipFrameAssembler.Assemble(clipFrame, frame);
string flood = string.Join(" ", frame.OrderedVisibleCells.Select(c => $"{c & 0xFFFFu:X3}"));
var lineParts = new List<string>();
foreach (uint cellId in frame.OrderedVisibleCells)
{
if (!assembly.CellIdToViewSlices.TryGetValue(cellId, out var slices))
{
// No slices at all → DrawEnvCellShells falls to NoClipSlice → UNCLIPPED.
lineParts.Add($"0x{cellId & 0xFFFFu:X3}:NO-SLICES(unclipped)");
unclippedDrawn++;
continue;
}
foreach (var s in slices)
{
bool unclipped = s.Slot == 0 && s.Planes.Length == 0;
if (unclipped) unclippedDrawn++;
string tag = unclipped ? "(UNCLIPPED)" : "";
lineParts.Add(FormattableString.Invariant(
$"0x{cellId & 0xFFFFu:X3}:slot{s.Slot}/p{s.Planes.Length}[{s.NdcAabb.X:F2},{s.NdcAabb.Y:F2}..{s.NdcAabb.Z:F2},{s.NdcAabb.W:F2}]{tag}"));
}
}
report.Add(FormattableString.Invariant(
$"eyeX={ex,5:F1} flood=[{flood}] {string.Join(" ", lineParts)}"));
}
foreach (var line in report) _out.WriteLine(line);
_out.WriteLine($"unclipped-drawn slices across sweep: {unclippedDrawn}");
// Documents the defect when it fires; flips to 0 once the shell pass
// implements the assembler's scissor-fallback contract (or exact clip).
Assert.True(unclippedDrawn == 0,
$"#113: {unclippedDrawn} hall-cell slices would draw fully unclipped " +
"(slot 0, no planes, no scissor) — interior stair geometry paints " +
"across the exterior wall (the phantom staircase).");
}
}

View file

@ -1,196 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using AcDream.App.Rendering;
using DatReaderWriter;
using DatReaderWriter.Options;
using Xunit;
using Xunit.Abstractions;
using DatLandBlockInfo = DatReaderWriter.DBObjs.LandBlockInfo;
namespace AcDream.App.Tests.Rendering;
/// <summary>
/// #120 reproduction (2026-06-11): the armed tripwire self-attributed on the
/// re-gate launch (regate-118-119-launch.log) — a pure TWO-CELL reciprocal
/// ping-pong, 64 laps each: `chain root=0xA9B4015C eye=(109.995,37.158,96.249)
/// cells: 0xA9B40162x64 0xA9B4015Cx64` and `root=0xA9B3010F
/// eye=(175.771,-107.310,118.814) cells: 0xA9B30103x64 0xA9B3010Fx64`.
///
/// Mechanism: with the eye within PortalSideEpsilon (±1 cm — the T2-refuted-
/// to-tighten root-lag tolerance; retail's is 0.0002) of the portal plane,
/// the in-plane case counts as interior for BOTH cells, so views flow
/// A→B→A…; each lap re-clips through two near-edge-on apertures whose
/// intersection numerics wobble by more than CellView's 1e-3 dedup grid →
/// every lap keys as a NEW polygon → 128-deep in-place recursion. The prior
/// convergence sweeps (CornerFloodReplayTests) could not reproduce because
/// they only load the corner building 0x016F-0x0175 — both firing pairs are
/// OUTSIDE that set.
///
/// The fix class is the handoff's own prediction ("dedup admitting
/// near-duplicates per lap"): a round-trip re-emission is, in exact math, a
/// SUBSET of the region that originated it — CellView.Add must reject
/// contained polygons, which makes union growth strictly area-increasing and
/// the flood convergent regardless of clip-numerics drift.
/// </summary>
[Trait("Lane", "InstalledDat")]
public class Issue120ReciprocalPingPongTests
{
private readonly ITestOutputHelper _out;
public Issue120ReciprocalPingPongTests(ITestOutputHelper output) => _out = output;
internal static Dictionary<uint, LoadedCell> LoadAllInteriorCells(DatCollection dats, uint landblock)
{
var lbi = dats.Get<DatLandBlockInfo>(landblock | 0xFFFEu);
Assert.NotNull(lbi);
var cells = new Dictionary<uint, LoadedCell>();
for (uint low = 0x0100u; low < 0x0100u + lbi!.NumCells; low++)
{
uint id = landblock | low;
try { cells[id] = CornerFloodReplayTests.LoadCell(dats, id); }
catch (InvalidOperationException) { /* sparse cell ids — skip */ }
}
return cells;
}
private static Matrix4x4 ViewProjFor(Vector3 eye, Vector3 lookAt)
{
var view = Matrix4x4.CreateLookAt(eye, lookAt, Vector3.UnitZ);
var proj = Matrix4x4.CreatePerspectiveFieldOfView(1.2f, 1280f / 720f, 0.1f, 5000f);
return view * proj;
}
public static readonly TheoryData<uint, uint, uint, float, float, float> CapturedSites = new()
{
// landblock, cellA (root at firing), cellB, eye x, y, z — straight from the chain dumps
{ 0xA9B40000u, 0x015Cu, 0x0162u, 109.995f, 37.158f, 96.249f },
{ 0xA9B30000u, 0x010Fu, 0x0103u, 175.771f, -107.310f, 118.814f },
};
/// <summary>
/// The captured firing sites, swept over orientations (the dump has no view
/// matrix) and over both cells as root. Invariants: the tripwire stays 0
/// AND no CellView accumulates a pathological polygon pile (the duplicate
/// build-up is the defect even below the depth-128 failsafe).
/// </summary>
[Theory]
[MemberData(nameof(CapturedSites))]
public void CapturedPingPongSites_Converge(uint landblock, uint lowA, uint lowB, float ex, float ey, float ez)
{
var datDir = CornerFloodReplayTests.ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var cells = LoadAllInteriorCells(dats, landblock);
Func<uint, LoadedCell?> lookup = id => cells.TryGetValue(id, out var c) ? c : null;
Assert.True(cells.ContainsKey(landblock | lowA), $"cell 0x{landblock | lowA:X8} not loaded");
Assert.True(cells.ContainsKey(landblock | lowB), $"cell 0x{landblock | lowB:X8} not loaded");
var eye = new Vector3(ex, ey, ez);
var roots = new[] { cells[landblock | lowA], cells[landblock | lowB] };
PortalVisibilityBuilder.ConvergenceTripwireCount = 0;
var failures = new List<string>();
int builds = 0, maxPolys = 0;
foreach (var root in roots)
{
for (int yaw = 0; yaw < 8; yaw++)
{
float a = yaw * MathF.PI / 4f;
foreach (float pitch in new[] { -0.4f, 0f, 0.4f })
{
var dir = Vector3.Normalize(new Vector3(MathF.Cos(a), MathF.Sin(a), pitch));
int before = PortalVisibilityBuilder.ConvergenceTripwireCount;
var frame = PortalVisibilityBuilder.Build(root, eye, lookup, ViewProjFor(eye, eye + dir * 3f));
builds++;
int after = PortalVisibilityBuilder.ConvergenceTripwireCount;
int polys = frame.CellViews.Count == 0 ? 0 : frame.CellViews.Values.Max(v => v.Polygons.Count);
if (polys > maxPolys) maxPolys = polys;
if (after != before || polys > 32)
failures.Add(FormattableString.Invariant(
$"root=0x{root.CellId:X8} yaw={yaw} pitch={pitch} tripwire={after - before} maxCellPolys={polys}"));
}
}
}
_out.WriteLine($"builds={builds} maxCellPolys={maxPolys} failures={failures.Count}");
foreach (var f in failures) _out.WriteLine($" {f}");
Assert.True(failures.Count == 0,
$"{failures.Count}/{builds} builds broke convergence at the captured #120 site (see output)");
}
/// <summary>
/// The geometric trigger, driven directly: eye swept through the
/// both-sides-pass window (±PortalSideEpsilon = ±1 cm) of the reciprocal
/// portal plane between the two captured cells, looking across and along
/// the aperture. Same convergence invariants.
/// </summary>
[Theory]
[MemberData(nameof(CapturedSites))]
public void PortalPlaneWindow_BothSidesPass_Converges(uint landblock, uint lowA, uint lowB, float ex, float ey, float ez)
{
_ = ex; _ = ey; _ = ez; // geometric variant derives its own eyes
var datDir = CornerFloodReplayTests.ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var cells = LoadAllInteriorCells(dats, landblock);
Func<uint, LoadedCell?> lookup = id => cells.TryGetValue(id, out var c) ? c : null;
var cellA = cells[landblock | lowA];
uint cellBId = landblock | lowB;
// the portal from A to B (the ping-pong pair)
int portalIdx = -1;
for (int i = 0; i < cellA.Portals.Count; i++)
if (cellA.Portals[i].OtherCellId == (ushort)lowB) { portalIdx = i; break; }
Assert.True(portalIdx >= 0, $"no portal 0x{lowA:X4}->0x{lowB:X4}");
var poly = cellA.PortalPolygons[portalIdx];
Assert.True(poly is { Length: >= 3 }, "portal polygon degenerate");
var centroidLocal = Vector3.Zero;
foreach (var v in poly!) centroidLocal += v;
centroidLocal /= poly.Length;
var centroidWorld = Vector3.Transform(centroidLocal, cellA.WorldTransform);
var plane = cellA.ClipPlanes[portalIdx];
var normalWorld = Vector3.Normalize(Vector3.TransformNormal(plane.Normal, cellA.WorldTransform));
PortalVisibilityBuilder.ConvergenceTripwireCount = 0;
var failures = new List<string>();
int builds = 0, maxPolys = 0;
foreach (float off in new[] { -0.009f, -0.004f, 0f, 0.004f, 0.009f })
{
var eye = centroidWorld + normalWorld * off;
foreach (var root in new[] { cellA, cells[cellBId] })
{
for (int yaw = 0; yaw < 8; yaw++)
{
float a = yaw * MathF.PI / 4f;
var dir = Vector3.Normalize(new Vector3(MathF.Cos(a), MathF.Sin(a), 0.1f));
int before = PortalVisibilityBuilder.ConvergenceTripwireCount;
var frame = PortalVisibilityBuilder.Build(root, eye, lookup, ViewProjFor(eye, eye + dir * 3f));
builds++;
int after = PortalVisibilityBuilder.ConvergenceTripwireCount;
int polys = frame.CellViews.Count == 0 ? 0 : frame.CellViews.Values.Max(v => v.Polygons.Count);
if (polys > maxPolys) maxPolys = polys;
if (after != before || polys > 32)
failures.Add(FormattableString.Invariant(
$"off={off:F3} root=0x{root.CellId:X8} yaw={yaw} tripwire={after - before} maxCellPolys={polys}"));
}
}
}
_out.WriteLine($"builds={builds} maxCellPolys={maxPolys} failures={failures.Count}");
foreach (var f in failures) _out.WriteLine($" {f}");
Assert.True(failures.Count == 0,
$"{failures.Count}/{builds} builds broke convergence in the ±ε portal-plane window (see output)");
}
}

View file

@ -1,154 +0,0 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using AcDream.App.Rendering;
using DatReaderWriter;
using DatReaderWriter.Options;
using Xunit;
using Xunit.Abstractions;
namespace AcDream.App.Tests.Rendering;
/// <summary>
/// #124 — far-building interiors under an INTERIOR root. Retail seeds the
/// look-in flood by clipping a building's aperture against the CURRENTLY
/// INSTALLED view (PView::GetClip 0x005a4320 inside ConstructView(CBldPortal)
/// 0x005a59a0): full screen outdoors, the accumulated doorway (outside) view
/// when looked into from inside. These tests pin BuildFromExterior's
/// seedRegion parameter — the port of that installed-view clip — against the
/// real Holtburg corner-building door.
/// </summary>
[Trait("Lane", "InstalledDat")]
public class Issue124LookInSeedRegionTests
{
private readonly ITestOutputHelper _out;
public Issue124LookInSeedRegionTests(ITestOutputHelper output) => _out = output;
private const uint ExitCellId = CornerFloodReplayTests.Landblock | 0x0170u;
private static Matrix4x4 ViewProjFor(Vector3 eye, Vector3 lookAt)
{
var view = Matrix4x4.CreateLookAt(eye, lookAt, Vector3.UnitZ);
var proj = Matrix4x4.CreatePerspectiveFieldOfView(1.2f, 1280f / 720f, 1f, 5000f);
return view * proj;
}
private static (Dictionary<uint, LoadedCell> cells, LoadedCell exitCell, int exitIdx,
Vector3 centroid, Vector3 outward) LoadFixture(DatCollection dats)
{
var cells = CornerFloodReplayTests.LoadBuilding(dats);
var exitCell = cells[ExitCellId];
int exitIdx = -1;
for (int i = 0; i < exitCell.Portals.Count; i++)
{
if (exitCell.Portals[i].OtherCellId == 0xFFFF && i < exitCell.PortalPolygons.Count
&& exitCell.PortalPolygons[i].Length >= 3)
{ exitIdx = i; break; }
}
Assert.True(exitIdx >= 0);
var localPoly = exitCell.PortalPolygons[exitIdx];
Vector3 centroid = Vector3.Zero;
foreach (var lp in localPoly)
centroid += Vector3.Transform(lp, exitCell.WorldTransform);
centroid /= localPoly.Length;
var plane = exitCell.ClipPlanes[exitIdx];
var normal = Vector3.TransformNormal(plane.Normal, exitCell.WorldTransform);
var cellCenter = Vector3.Transform(
(exitCell.LocalBoundsMin + exitCell.LocalBoundsMax) * 0.5f, exitCell.WorldTransform);
// outward = away from the cell interior.
if (Vector3.Dot(normal, cellCenter - centroid) > 0)
normal = -normal;
return (cells, exitCell, exitIdx, centroid, Vector3.Normalize(normal));
}
private static Vector4 ApertureNdcAabb(LoadedCell cell, int idx, Matrix4x4 viewProj)
{
float minX = float.MaxValue, minY = float.MaxValue, maxX = float.MinValue, maxY = float.MinValue;
foreach (var lp in cell.PortalPolygons[idx])
{
var w = Vector3.Transform(lp, cell.WorldTransform);
var c = Vector4.Transform(new Vector4(w, 1f), viewProj);
Assert.True(c.W > 0.05f, "fixture eye must keep the aperture fully in front");
minX = MathF.Min(minX, c.X / c.W); maxX = MathF.Max(maxX, c.X / c.W);
minY = MathF.Min(minY, c.Y / c.W); maxY = MathF.Max(maxY, c.Y / c.W);
}
return new Vector4(minX, minY, maxX, maxY);
}
private static ViewPolygon Quad(float minX, float minY, float maxX, float maxY) =>
new(new[]
{
new Vector2(minX, minY), new Vector2(maxX, minY),
new Vector2(maxX, maxY), new Vector2(minX, maxY),
});
[Fact]
public void SeedRegion_ContainingAperture_Floods_DisjointRegion_DoesNot()
{
var datDir = CornerFloodReplayTests.ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var (cells, exitCell, exitIdx, centroid, outward) = LoadFixture(dats);
LoadedCell? Lookup(uint id) => cells.TryGetValue(id, out var c) ? c : null;
// Eye OUTSIDE the building, 3 m in front of the exit door, gaze at it
// — the look-in geometry of a viewer peering at this building through
// some other opening.
var eye = centroid + outward * 3f;
var viewProj = ViewProjFor(eye, centroid);
var ap = ApertureNdcAabb(exitCell, exitIdx, viewProj);
_out.WriteLine(FormattableString.Invariant(
$"aperture ndc=({ap.X:F3},{ap.Y:F3},{ap.Z:F3},{ap.W:F3})"));
// Sanity: the full-screen (outdoor-root) seed floods.
var full = PortalVisibilityBuilder.BuildFromExterior(
cells.Values, eye, Lookup, viewProj);
Assert.True(full.OrderedVisibleCells.Count > 0, "full-screen seed must flood");
// A region containing the aperture floods — and never MORE than the
// full-screen seed (region-restricting can only shrink the flood).
var containing = new[] { Quad(ap.X - 0.05f, ap.Y - 0.05f, ap.Z + 0.05f, ap.W + 0.05f) };
var seeded = PortalVisibilityBuilder.BuildFromExterior(
cells.Values, eye, Lookup, viewProj, float.PositiveInfinity, containing);
Assert.True(seeded.OrderedVisibleCells.Count > 0, "containing region must flood");
Assert.True(seeded.OrderedVisibleCells.Count <= full.OrderedVisibleCells.Count);
// A region strictly disjoint from the aperture must not flood — the
// doorway doesn't show this building, so its interior never builds
// (retail: GetClip vs the installed view returns empty → no look-in).
Assert.True(ap.Z < 0.70f || ap.X > -0.70f, "fixture aperture unexpectedly fills the screen");
var disjoint = ap.Z < 0.70f
? new[] { Quad(0.75f, 0.75f, 0.99f, 0.99f) }
: new[] { Quad(-0.99f, -0.99f, -0.75f, -0.75f) };
var none = PortalVisibilityBuilder.BuildFromExterior(
cells.Values, eye, Lookup, viewProj, float.PositiveInfinity, disjoint);
Assert.True(none.OrderedVisibleCells.Count == 0,
FormattableString.Invariant($"disjoint region flooded {none.OrderedVisibleCells.Count} cells"));
}
[Fact]
public void EyeOnInteriorSide_ExitDoorNeverSeeds()
{
// The root's own doorway must not look-in on itself: the seed eye-side
// test (retail ConstructView's sidedness vs portal_side) excludes any
// aperture the eye is on the interior side of — this is what lets the
// interior-root gather pass ALL nearby buildings including the
// viewer's own without special-casing.
var datDir = CornerFloodReplayTests.ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var (cells, exitCell, _, centroid, outward) = LoadFixture(dats);
LoadedCell? Lookup(uint id) => cells.TryGetValue(id, out var c) ? c : null;
var eye = centroid - outward * 2f; // 2 m INSIDE the doorway
var viewProj = ViewProjFor(eye, centroid);
var frame = PortalVisibilityBuilder.BuildFromExterior(
new[] { exitCell }, eye, Lookup, viewProj);
Assert.True(frame.OrderedVisibleCells.Count == 0,
"an interior-side eye must not seed its own cell's exit portal");
}
}

View file

@ -1,351 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using AcDream.App.Rendering;
using DatReaderWriter;
using DatReaderWriter.Options;
using Xunit;
using Xunit.Abstractions;
using DatLandBlockInfo = DatReaderWriter.DBObjs.LandBlockInfo;
namespace AcDream.App.Tests.Rendering;
/// <summary>
/// #127 — per-building flood admissions are BISTABLE per frame under the
/// outdoor root (the building-flap mechanism: tower roof/edges flap, #123
/// buildings vanishing when running past). Captured live with the [viewer]
/// fwd= probe (flap-fwd-capture2.log): a ~4 cm eye wobble flips the merged
/// flood 24↔25 with a byte-identical gaze:
/// flood=25 eye=(129.051,-15.636,99.512) fwd=(-0.7565,0.5877,-0.2869)
/// flood=24 eye=(129.009,-15.610,99.503) fwd=(same)
/// flood=25 eye=(129.051,-15.642,99.514) fwd=(same)
/// (player parked at pCell=0xA9B40019, Holtburg town, A9B4-anchored world
/// frame — A9B4-local == world; A9B3 cells get a (0,-192,0) block offset.)
///
/// This harness replays the captured (eye, fwd) pairs through the
/// production outdoor pipeline — Build(outdoorNode) + per-building
/// ConstructViewBuilding merged (RetailPViewRenderer.MergeNearbyBuildingFloods
/// shape) — and DIFFS the admitted cell sets: the flipping cell names the
/// bistable admission, and the bisect along the eye segment names the knife
/// edge.
/// </summary>
[Trait("Lane", "InstalledDat")]
public class Issue127FloodFlipReplayTests
{
private readonly ITestOutputHelper _out;
public Issue127FloodFlipReplayTests(ITestOutputHelper output) => _out = output;
// The captured flip pair (A9B4-anchored world frame).
private static readonly Vector3 EyeA = new(129.051f, -15.636f, 99.512f);
private static readonly Vector3 EyeB = new(129.009f, -15.610f, 99.503f);
private static readonly Vector3 Fwd = Vector3.Normalize(new Vector3(-0.7565f, 0.5877f, -0.2869f));
private sealed record World(
Dictionary<uint, LoadedCell> Cells,
List<List<LoadedCell>> BuildingGroups,
Func<uint, LoadedCell?> Lookup);
private static void LoadLandblockBuildings(
DatCollection dats, uint landblock, Vector3 offset,
Dictionary<uint, LoadedCell> cells, List<List<LoadedCell>> groups)
{
var lbi = dats.Get<DatLandBlockInfo>(landblock | 0xFFFEu);
if (lbi is null) return;
// all interior cells (the lookup needs every portal-reachable cell)
for (uint low = 0x0100u; low < 0x0100u + lbi.NumCells; low++)
{
uint id = landblock | low;
try { cells[id] = CornerFloodReplayTests.LoadCell(dats, id, offset); }
catch (InvalidOperationException) { }
}
// per-building groups from the bldPortal stab lists (the dat's own
// building→cells mapping; mirrors BuildingLoader's seed+walk result)
if (lbi.Buildings is null) return;
foreach (var bld in lbi.Buildings)
{
var set = new HashSet<uint>();
if (bld.Portals is not null)
foreach (var bp in bld.Portals)
if (bp.StabList is not null)
foreach (ushort low in bp.StabList)
set.Add(landblock | low);
var group = new List<LoadedCell>();
foreach (uint id in set)
if (cells.TryGetValue(id, out var c))
group.Add(c);
if (group.Count > 0)
groups.Add(group);
}
}
private World? LoadWorld()
{
var datDir = CornerFloodReplayTests.ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); return null; }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var cells = new Dictionary<uint, LoadedCell>();
var groups = new List<List<LoadedCell>>();
// A9B4 = the anchor (world == local); the live gather spans the
// streaming near window, so load a 5x5 block neighbourhood with
// block offsets relative to the anchor.
for (int dx = -2; dx <= 2; dx++)
for (int dy = -2; dy <= 2; dy++)
{
uint lbX = (uint)(0xA9 + dx);
uint lbY = (uint)(0xB4 + dy);
uint lb = (lbX << 24) | (lbY << 16);
LoadLandblockBuildings(dats, lb, new Vector3(dx * 192f, dy * 192f, 0f), cells, groups);
}
_out.WriteLine($"loaded {cells.Count} cells in {groups.Count} building groups");
Func<uint, LoadedCell?> lookup = id => cells.TryGetValue(id, out var c) ? c : null;
return new World(cells, groups, lookup);
}
private static Matrix4x4 ViewProjFor(Vector3 eye, Vector3 fwd, float fovY)
{
var view = Matrix4x4.CreateLookAt(eye, eye + fwd * 3f, Vector3.UnitZ);
var proj = Matrix4x4.CreatePerspectiveFieldOfView(fovY, 1280f / 720f, 0.1f, 5000f);
return view * proj;
}
// PortalBounds per group: the union AABB of the group's exit-portal
// polygons in world space — exactly BuildingLoader's construction
// (BuildingLoader.cs:114-144).
private static (bool Has, Vector3 Min, Vector3 Max) PortalBoundsFor(List<LoadedCell> group)
{
bool has = false;
var min = new Vector3(float.MaxValue);
var max = new Vector3(float.MinValue);
foreach (var cell in group)
{
int n = Math.Min(cell.Portals.Count, cell.PortalPolygons.Count);
for (int i = 0; i < n; i++)
{
if (cell.Portals[i].OtherCellId != 0xFFFF) continue;
var poly = cell.PortalPolygons[i];
if (poly is null) continue;
foreach (var v in poly)
{
var world = Vector3.Transform(v, cell.WorldTransform);
has = true;
min = Vector3.Min(min, world);
max = Vector3.Max(max, world);
}
}
}
return (has, min, max);
}
// The production outdoor pipeline: full-screen outdoor root + per-building
// exterior floods merged as a union (RetailPViewRenderer.DrawInside +
// MergeNearbyBuildingFloods + MergeBuildingFrame), INCLUDING the gather's
// per-building frustum pre-gate on PortalBounds (GameWindow:7542-7545) —
// controlled by <paramref name="withPreGate"/> so the gate itself can be
// implicated or exonerated.
private static HashSet<uint> AdmittedCells(World w, Vector3 eye, Vector3 fwd, float fovY, bool withPreGate)
{
var viewProj = ViewProjFor(eye, fwd, fovY);
var frustum = FrustumPlanes.FromViewProjection(viewProj);
var outdoor = OutdoorCellNode.Build(0xA9B40019u);
var pv = PortalVisibilityBuilder.Build(outdoor, eye, w.Lookup, viewProj);
foreach (var group in w.BuildingGroups)
{
if (withPreGate)
{
var (has, min, max) = PortalBoundsFor(group);
if (has && !FrustumCuller.IsAabbVisible(frustum, min, max))
continue;
}
var bf = PortalVisibilityBuilder.ConstructViewBuilding(group, eye, w.Lookup, viewProj);
foreach (uint cellId in bf.OrderedVisibleCells)
{
if (!bf.CellViews.TryGetValue(cellId, out var srcView)) continue;
if (pv.CellViews.TryGetValue(cellId, out var existing))
{
foreach (var p in srcView.Polygons) existing.Add(p);
continue;
}
pv.CellViews[cellId] = srcView;
pv.OrderedVisibleCells.Add(cellId);
}
}
return new HashSet<uint>(pv.OrderedVisibleCells);
}
/// <summary>
/// Reproduce + name: the captured 4 cm pair must admit the SAME cell set
/// (a stable world does not flap a building interior for a 4 cm eye
/// wobble at identical gaze). The diff names the bistable cell(s); the
/// bisect prints the knife-edge location and transition count.
/// </summary>
[Fact]
public void CapturedFlipPair_AdmissionIsStable()
{
var w = LoadWorld();
if (w is null) return;
foreach (bool preGate in new[] { false, true })
foreach (float fov in new[] { MathF.PI / 3f, 1.2f, MathF.PI / 2f, 0.9f })
{
var a = AdmittedCells(w, EyeA, Fwd, fov, preGate);
var b = AdmittedCells(w, EyeB, Fwd, fov, preGate);
var onlyA = a.Except(b).OrderBy(x => x).ToList();
var onlyB = b.Except(a).OrderBy(x => x).ToList();
_out.WriteLine(FormattableString.Invariant(
$"preGate={preGate} fov={fov:F2}: |A|={a.Count} |B|={b.Count} onlyA=[{string.Join(",", onlyA.Select(x => $"0x{x:X8}"))}] onlyB=[{string.Join(",", onlyB.Select(x => $"0x{x:X8}"))}]"));
if (onlyA.Count == 0 && onlyB.Count == 0) continue;
// bisect the segment: count admission transitions per flipping cell
foreach (uint cell in onlyA.Concat(onlyB))
{
int transitions = 0;
bool? prev = null;
for (int i = 0; i <= 40; i++)
{
var eye = Vector3.Lerp(EyeA, EyeB, i / 40f);
bool admitted = AdmittedCells(w, eye, Fwd, fov, preGate).Contains(cell);
if (prev.HasValue && admitted != prev.Value) transitions++;
prev = admitted;
}
_out.WriteLine(FormattableString.Invariant(
$" cell 0x{cell:X8}: {transitions} admission transition(s) along the 4 cm segment"));
}
Assert.Fail($"flood admission differs across the captured 4 cm pair (preGate={preGate}, fov={fov:F2}) — see output for the flipping cells");
}
}
// Centre of a building group's exit-portal AABB (world space).
private static (bool Has, Vector3 Center) PortalCenterFor(List<LoadedCell> group)
{
var (has, min, max) = PortalBoundsFor(group);
return (has, (min + max) * 0.5f);
}
// Per-building admitted cells (this group only) at one (eye, gaze) — the
// production per-building flood + optional PortalBounds frustum pre-gate.
private static HashSet<uint> BuildingAdmits(
World w, List<LoadedCell> group, Vector3 eye, Matrix4x4 viewProj,
FrustumPlanes frustum, bool withPreGate)
{
var result = new HashSet<uint>();
if (withPreGate)
{
var (has, min, max) = PortalBoundsFor(group);
if (has && !FrustumCuller.IsAabbVisible(frustum, min, max))
return result;
}
var bf = PortalVisibilityBuilder.ConstructViewBuilding(group, eye, w.Lookup, viewProj);
foreach (uint id in bf.OrderedVisibleCells)
result.Add(id);
return result;
}
/// <summary>
/// #127 distant-building churn detector. The captured 4 cm pair is now
/// stable (the near-eye W=0 clip port), but the user symptom is buildings
/// flickering when RUNNING PAST at a distance. This strafes the eye past
/// each loaded building at several distances in 1 mm steps with the gaze
/// fixed forward (the run-past geometry) and counts, per building cell, how
/// many times its admission toggles over the monotone strafe. A stable
/// flood toggles a cell AT MOST ONCE along a monotone eye path (it enters
/// or leaves the view a single time); >=2 toggles is churn — the building
/// flickers. preGate off vs on separates flood-math churn from the
/// PortalBounds frustum pre-gate.
///
/// RESULT (2026-06-12, HEAD post-W=0-clip-port + #120 containment): ZERO
/// churning cases across all 21 building groups x {10,30,60,120,190} m x
/// 100 mm-steps, both preGate states. The near-eye knife-edge class the
/// W=0 polyClipFinish port (987313a) killed was the distant-building
/// flicker too; the user re-gate ("Seems to have been fixed") agrees.
/// Now the REGRESSION PIN — it asserts zero churn.
/// </summary>
[Fact]
public void DistantBuildingStrafe_NoAdmissionChurn()
{
var w = LoadWorld();
if (w is null) return;
const float fovY = MathF.PI / 3f;
const float eyeHeight = 1.8f;
const float strafeSpanM = 0.10f; // 10 cm strafe
const int strafeSteps = 100; // 1 mm/step
var distances = new[] { 10f, 30f, 60f, 120f, 190f };
int totalChurn = 0;
foreach (bool preGate in new[] { false, true })
{
int worstToggles = 0;
string worstDesc = "(none)";
int churningCases = 0;
for (int gi = 0; gi < w.BuildingGroups.Count; gi++)
{
var group = w.BuildingGroups[gi];
var (has, center) = PortalCenterFor(group);
if (!has) continue;
foreach (float dist in distances)
{
// Eye south of the building at eye height, gaze NORTH toward
// the building centre; strafe along world +X (run-past).
var gaze = Vector3.Normalize(new Vector3(0f, 1f, -0.05f));
var strafeDir = Vector3.Normalize(Vector3.Cross(Vector3.UnitZ, gaze)); // ~world +X
var eyeBase = new Vector3(center.X, center.Y - dist, center.Z + eyeHeight)
- strafeDir * (strafeSpanM * 0.5f);
var toggleCount = new Dictionary<uint, int>();
var prevIn = new Dictionary<uint, bool>();
for (int s = 0; s <= strafeSteps; s++)
{
var eye = eyeBase + strafeDir * (strafeSpanM * s / strafeSteps);
var vp = ViewProjFor(eye, gaze, fovY);
var frustum = FrustumPlanes.FromViewProjection(vp);
var admits = BuildingAdmits(w, group, eye, vp, frustum, preGate);
var seen = new HashSet<uint>(admits);
foreach (uint id in seen)
{
bool wasIn = prevIn.TryGetValue(id, out var p) && p;
if (!wasIn && prevIn.ContainsKey(id))
toggleCount[id] = toggleCount.GetValueOrDefault(id) + 1;
prevIn[id] = true;
}
foreach (var id in new List<uint>(prevIn.Keys))
if (!seen.Contains(id))
{
if (prevIn[id])
toggleCount[id] = toggleCount.GetValueOrDefault(id) + 1;
prevIn[id] = false;
}
}
foreach (var (id, toggles) in toggleCount)
{
if (toggles < 2) continue; // <=1 = clean enter/leave
churningCases++;
if (toggles > worstToggles)
{
worstToggles = toggles;
worstDesc = FormattableString.Invariant(
$"group#{gi} dist={dist:F0}m cell=0x{id:X8} toggles={toggles}");
}
}
}
}
_out.WriteLine(FormattableString.Invariant(
$"preGate={preGate}: churningCases={churningCases} worst={worstDesc} (worstToggles={worstToggles})"));
totalChurn += churningCases;
}
Assert.True(totalChurn == 0,
$"{totalChurn} distant-building admission churn case(s) — a building's cells toggle >=2x " +
"over a monotone run-past strafe (the #127 flicker); see output for the worst building/distance/cell");
}
}

View file

@ -1,113 +0,0 @@
using System;
using DatReaderWriter;
using DatReaderWriter.Options;
using Xunit;
using Xunit.Abstractions;
using DatSetup = DatReaderWriter.DBObjs.Setup;
using DatGfxObj = DatReaderWriter.DBObjs.GfxObj;
namespace AcDream.App.Tests.Rendering;
/// <summary>
/// #131 diagnostic (throwaway): identify the Holtburg portal among the
/// outside-stage setup ids captured by the [outstage] probe, by dumping each
/// candidate setup's parts + bounds from the dat. The portal's setup is the
/// translucent swirl; lamp posts / creatures / signs identify by part shape.
/// </summary>
[Trait("Purpose", "Diagnostic")]
[Trait("Lane", "InstalledDat")]
public class Issue131SetupProbeTests
{
private readonly ITestOutputHelper _out;
public Issue131SetupProbeTests(ITestOutputHelper output) => _out = output;
/// <summary>#131: from the captured cottage-interior frame (the user's
/// portal-missing viewpoint), does the look-in flood admit the hall's
/// PORCH cell 0xA9B4017A (the portal's owner cell, pinned by the teleport
/// pCell flip)? If not admitted, no pass can draw the swirl regardless of
/// the emitter plumbing.</summary>
[Fact]
public void Diagnostic_LookInFlood_AdmitsHallPorchFromCottage()
{
var datDir = CornerFloodReplayTests.ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var cells = Issue120ReciprocalPingPongTests.LoadAllInteriorCells(dats, 0xA9B40000u);
_out.WriteLine(FormattableString.Invariant($"loaded {cells.Count} A9B4 interior cells; hasPorch017A={cells.ContainsKey(0xA9B4017Au)}"));
AcDream.App.Rendering.LoadedCell? Lookup(uint id) => cells.TryGetValue(id, out var c) ? c : null;
// The captured frame: [viewer] root=0xA9B40171 eye=(155.255,14.533,96.074)
// fwd=(0.0702,0.9554,-0.2869) (portal-owner-verdicts.log:135118).
var eye = new System.Numerics.Vector3(155.255f, 14.533f, 96.074f);
var fwd = new System.Numerics.Vector3(0.0702f, 0.9554f, -0.2869f);
var view = System.Numerics.Matrix4x4.CreateLookAt(eye, eye + fwd, System.Numerics.Vector3.UnitZ);
var proj = System.Numerics.Matrix4x4.CreatePerspectiveFieldOfView(1.2f, 1280f / 720f, 1f, 5000f);
var viewProj = view * proj;
var root = cells[0xA9B40171u];
var pv = AcDream.App.Rendering.PortalVisibilityBuilder.Build(
root, eye, Lookup, viewProj,
buildingMembership: null);
_out.WriteLine(FormattableString.Invariant(
$"main flood={pv.OrderedVisibleCells.Count} outPolys={pv.OutsideView.Polygons.Count}"));
var lookIn = AcDream.App.Rendering.PortalVisibilityBuilder.BuildFromExterior(
cells.Values, eye, Lookup, viewProj,
float.PositiveInfinity, pv.OutsideView.Polygons);
var sb = new System.Text.StringBuilder("look-in admitted:");
foreach (uint id in lookIn.OrderedVisibleCells)
sb.Append(FormattableString.Invariant($" 0x{id & 0xFFFFu:X4}"));
_out.WriteLine(sb.ToString());
_out.WriteLine(FormattableString.Invariant(
$"porch 0x017A admitted: {lookIn.OrderedVisibleCells.Contains(0xA9B4017Au)}"));
}
[Fact]
public void Diagnostic_DumpOutstageCandidateSetups()
{
var datDir = CornerFloodReplayTests.ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
uint[] candidates =
{
0x020010AC, // 0x7A9B4050 PASS r=11.9 — portal candidate A
0x02000B8E, // 0x7A9B403B PASS r=11.6 — portal candidate B
0x020019FF, // many instances (lamp posts?)
0x02000290,
0x02000001, // baseline (human?)
0x02000E08,
};
foreach (uint setupId in candidates)
{
var setup = dats.Get<DatSetup>(setupId);
if (setup is null)
{
_out.WriteLine(FormattableString.Invariant($"setup 0x{setupId:X8}: NOT FOUND"));
continue;
}
_out.WriteLine(FormattableString.Invariant(
$"setup 0x{setupId:X8}: parts={setup.Parts.Count}"));
int shown = 0;
foreach (uint partId in setup.Parts)
{
if (shown++ >= 4) { _out.WriteLine(" ..."); break; }
var gfx = dats.Get<DatGfxObj>(partId);
if (gfx is null) { _out.WriteLine(FormattableString.Invariant($" part 0x{partId:X8}: not found")); continue; }
var sb = new System.Text.StringBuilder();
sb.Append(FormattableString.Invariant(
$" part 0x{partId:X8}: polys={gfx.Polygons.Count} verts={gfx.VertexArray.Vertices.Count} surfaces=["));
int sShown = 0;
foreach (uint surfId in gfx.Surfaces)
{
if (sShown++ >= 6) { sb.Append(" ..."); break; }
sb.Append(FormattableString.Invariant($" 0x{surfId:X8}"));
}
sb.Append(" ]");
_out.WriteLine(sb.ToString());
}
}
}
}

View file

@ -1,284 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using AcDream.App.Rendering;
using DatReaderWriter;
using DatReaderWriter.Options;
using Xunit;
using Xunit.Abstractions;
namespace AcDream.App.Tests.Rendering;
/// <summary>
/// #176 (purple flashing at corridor seams, camera-angle dependent) + #177
/// (stairs pop in/out) — headless portal-flood replay in the Facility Hub
/// landblock 0x8A02. The unified hypothesis after the dat + draw-path reads:
/// both artifacts are FLOOD ADMISSION instability (a cell dropping out of
/// PortalVisibilityBuilder's admitted set paints the fog-purple clear color
/// where its geometry was; stair cells failing admission = the pop).
///
/// Production-matched inputs: Build(root, eye, lookup, viewProj,
/// buildingMembership: null) — the production call shape (FW3.3 retired drawLiftZ)
/// mirrors RetailPViewRenderer.DrawInside.
///
/// Scenarios:
/// A. #177 approach — stand in corridor 0x0178, look +X at the stair ramp
/// (0x0182) and the lower cell (0x0183): are they admitted?
/// B. #177 descent — eye path down the ramp crossing into 0x0183: does
/// 0x0182 (the ramp geometry's owner) drop near the transit?
/// C. #176 gaze sweep — eye parked in 0x016E near the 0x017A seam, yaw
/// sweep at several pitches: any cell admitted at angle k, gone at k+1,
/// back at k+2 (the bistability signature)?
/// D. #176 walk — eye tracks down the corridor across two seams, gaze
/// locked +X: per-step admitted-set diffs (drop-for-one-step churn).
/// </summary>
[Trait("Lane", "InstalledDat")]
public class Issue176177FacilityHubFloodReplayTests
{
private const uint FacilityHub = 0x8A020000u;
private readonly ITestOutputHelper _out;
public Issue176177FacilityHubFloodReplayTests(ITestOutputHelper output) => _out = output;
private static Matrix4x4 ViewProjFor(Vector3 eye, Vector3 gazeDir)
{
var view = Matrix4x4.CreateLookAt(eye, eye + gazeDir, Vector3.UnitZ);
var proj = Matrix4x4.CreatePerspectiveFieldOfView(1.2f, 1280f / 720f, 0.1f, 5000f);
return view * proj;
}
private static List<uint> Flood(
Dictionary<uint, LoadedCell> cells, uint rootId, Vector3 eye, Vector3 gazeDir)
{
Func<uint, LoadedCell?> lookup = id => cells.TryGetValue(id, out var c) ? c : null;
var frame = PortalVisibilityBuilder.Build(
cells[rootId], eye, lookup, ViewProjFor(eye, gazeDir),
buildingMembership: null);
var result = new List<uint>(frame.OrderedVisibleCells);
result.Sort();
return result;
}
private static string CellSetString(IEnumerable<uint> ids)
=> string.Join(" ", ids.Select(id => $"{id & 0xFFFFu:X4}"));
[Fact]
public void ScenarioA_StairApproach_AdmissionsFromCorridor()
{
var datDir = CornerFloodReplayTests.ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var cells = Issue120ReciprocalPingPongTests.LoadAllInteriorCells(dats, FacilityHub);
Assert.True(cells.ContainsKey(FacilityHub | 0x0178u), "0x0178 not loaded");
// 0x0178 spans x<=95 at z 6..3 (floor 6); the ramp cell 0x0182 runs
// x 95→98.33 descending; 0x0183 continues at z 9 beyond x=98.33.
// Eye at standing height (~1.7 m above the 6 floor), approaching the
// stair portal at x=95, gazing +X with a slight downward pitch (the
// natural look at a descending stair).
foreach (float eyeX in new[] { 88f, 90f, 92f, 94f, 94.9f })
{
var eye = new Vector3(eyeX, -40f, -4.3f);
foreach (var (gaze, label) in new (Vector3, string)[]
{
(new Vector3(1f, 0f, 0f), "level"),
(Vector3.Normalize(new Vector3(1f, 0f, -0.35f)), "pitch-19"),
(Vector3.Normalize(new Vector3(1f, 0f, -0.7f)), "pitch-35"),
})
{
var visible = Flood(cells, FacilityHub | 0x0178u, eye, gaze);
bool ramp = visible.Contains(FacilityHub | 0x0182u);
bool lower = visible.Contains(FacilityHub | 0x0183u);
_out.WriteLine($"eyeX={eyeX,5:F1} gaze={label,-8} flood={visible.Count,2} " +
$"ramp0182={(ramp ? "Y" : "MISSING")} lower0183={(lower ? "Y" : "MISSING")} " +
$"[{CellSetString(visible)}]");
}
}
}
[Fact]
[Trait("Purpose", "Diagnostic")]
public void Diagnostic_ScenarioB_StairDescent_RampCellRetention()
{
var datDir = CornerFloodReplayTests.ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var cells = Issue120ReciprocalPingPongTests.LoadAllInteriorCells(dats, FacilityHub);
// Descend the ramp: floor z goes 6 (x=95) → 9 (x=98.33), then flat.
// Eye rides ~1.7 m above the local floor. Root = the cell containing
// the eye by x-range (0x0178 x<95, ramp 0x0182 95..98.33, 0x0183 after).
// Gaze: forward and slightly down (running down stairs).
var gazeDir = Vector3.Normalize(new Vector3(1f, 0f, -0.4f));
List<uint>? prev = null;
for (float x = 94.0f; x <= 100.5f; x += 0.1f)
{
float floorZ = x < 95f ? -6f
: x < 98.333f ? -6f - 3f * (x - 95f) / 3.333f
: -9f;
var eye = new Vector3(x, -40f, floorZ + 1.7f);
uint rootId = x < 95f ? FacilityHub | 0x0178u
: x < 98.333f ? FacilityHub | 0x0182u
: FacilityHub | 0x0183u;
var visible = Flood(cells, rootId, eye, gazeDir);
bool ramp = visible.Contains(FacilityHub | 0x0182u);
bool upper = visible.Contains(FacilityHub | 0x0178u);
string diff = "";
if (prev is not null)
{
var removed = prev.Except(visible).ToList();
var added = visible.Except(prev).ToList();
if (removed.Count > 0) diff += $" REMOVED=[{CellSetString(removed)}]";
if (added.Count > 0) diff += $" added=[{CellSetString(added)}]";
}
_out.WriteLine($"x={x,6:F1} root={rootId & 0xFFFFu:X4} eyeZ={eye.Z,6:F2} flood={visible.Count,2} " +
$"ramp0182={(ramp ? "Y" : "MISSING")} up0178={(upper ? "Y" : "-")}{diff}");
prev = visible;
}
}
[Fact]
[Trait("Purpose", "Diagnostic")]
public void Diagnostic_ScenarioC_CorridorSeamGazeSweep_Bistability()
{
var datDir = CornerFloodReplayTests.ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var cells = Issue120ReciprocalPingPongTests.LoadAllInteriorCells(dats, FacilityHub);
// Parked eye in 0x016E near the 0x017A seam (x=85), standing height.
var eye = new Vector3(83.5f, -40f, -4.3f);
uint rootId = FacilityHub | 0x016Eu;
int churnEvents = 0;
foreach (float pitchZ in new[] { 0f, -0.35f, -0.75f, -1.2f })
{
List<uint>? prevSet = null;
float prevYaw = 0f;
var perYawSets = new List<(float yaw, List<uint> set)>();
for (float yawDeg = -180f; yawDeg <= 180f; yawDeg += 2f)
{
float rad = yawDeg * MathF.PI / 180f;
var gaze = Vector3.Normalize(new Vector3(MathF.Cos(rad), MathF.Sin(rad), pitchZ));
var visible = Flood(cells, rootId, eye, gaze);
perYawSets.Add((yawDeg, visible));
if (prevSet is not null)
{
var removed = prevSet.Except(visible).ToList();
var added = visible.Except(prevSet).ToList();
if (removed.Count > 0 || added.Count > 0)
{
_out.WriteLine($"pitch={pitchZ,5:F2} yaw {prevYaw,6:F0}->{yawDeg,6:F0}: " +
$"flood {prevSet.Count}->{visible.Count}" +
(removed.Count > 0 ? $" REMOVED=[{CellSetString(removed)}]" : "") +
(added.Count > 0 ? $" added=[{CellSetString(added)}]" : ""));
}
}
prevSet = visible;
prevYaw = yawDeg;
}
// Bistability: a cell present at yaw k, absent at k+1, present at k+2.
for (int i = 2; i < perYawSets.Count; i++)
{
var flicker = perYawSets[i - 2].set
.Intersect(perYawSets[i].set)
.Except(perYawSets[i - 1].set)
.ToList();
if (flicker.Count > 0)
{
churnEvents++;
_out.WriteLine($">>> BISTABLE pitch={pitchZ:F2} yaw={perYawSets[i - 1].yaw:F0}: " +
$"cells [{CellSetString(flicker)}] dropped for ONE 2-degree step");
}
}
}
_out.WriteLine($"bistable one-step drop events: {churnEvents}");
}
/// <summary>
/// THE production lag-window scenario (from launch-137-gate2.log
/// [cell-transit] lines): membership transits fire 0.10.6 m PAST the
/// portal plane in the travel direction (016E→017A at x=85.3385.47 vs
/// the plane at x=85.00; 0182→0183 at 98.5698.64 vs 98.33). The render
/// root (viewer cell, same membership machinery) therefore holds the OLD
/// cell while the camera eye is already beyond the boundary portal's
/// plane. This scenario reproduces exactly that window: root=old cell,
/// eye stepped across and past the plane, gaze forward. If the forward
/// chain (the next corridor cells) drops inside the window, that is #176
/// (purple = fog clear color where the forward cells' geometry was) and
/// #177(a)/(c) at the stair transit.
/// </summary>
[Theory]
[Trait("Purpose", "Diagnostic")]
[InlineData(0x016Eu, 0x017Au, 85.00f, -4.3f)] // corridor seam, plane x=85
[InlineData(0x0182u, 0x0183u, 98.333f, -7.3f)] // stair-bottom transit, plane x=98.33
public void Diagnostic_ScenarioE_RootLagWindow_ForwardChainRetention(
uint rootLow, uint forwardLow, float planeX, float eyeZ)
{
var datDir = CornerFloodReplayTests.ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var cells = Issue120ReciprocalPingPongTests.LoadAllInteriorCells(dats, FacilityHub);
uint rootId = FacilityHub | rootLow;
uint forwardId = FacilityHub | forwardLow;
var gazeDir = Vector3.Normalize(new Vector3(1f, 0f, -0.3f));
_out.WriteLine($"root=0x{rootLow:X4} forward=0x{forwardLow:X4} plane x={planeX:F2} " +
"(eye sweeps across; root HELD at the old cell = the production lag window)");
foreach (float dx in new[] { -0.30f, -0.10f, -0.02f, 0.00f, 0.02f, 0.05f, 0.10f, 0.20f, 0.30f, 0.45f, 0.60f })
{
var eye = new Vector3(planeX + dx, -40f, eyeZ);
var visible = Flood(cells, rootId, eye, gazeDir);
bool fwd = visible.Contains(forwardId);
_out.WriteLine($" eyeX=plane{(dx >= 0 ? "+" : "")}{dx:F2} flood={visible.Count,2} " +
$"forward={(fwd ? "Y" : ">>> DROPPED <<<")} [{CellSetString(visible)}]");
}
}
[Fact]
[Trait("Purpose", "Diagnostic")]
public void Diagnostic_ScenarioD_CorridorWalk_PerStepChurn()
{
var datDir = CornerFloodReplayTests.ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var cells = Issue120ReciprocalPingPongTests.LoadAllInteriorCells(dats, FacilityHub);
// Walk 0x0165 → 0x016E → 0x017A along y=40 (seams at x=75 and x=85),
// gaze locked +X, slight downward pitch (the running view). Root flips
// by x-range at the seams (the camera transits the same portals).
var gazeDir = Vector3.Normalize(new Vector3(1f, 0f, -0.3f));
List<uint>? prev = null;
int churn = 0;
for (float x = 71f; x <= 89f; x += 0.05f)
{
uint rootId = x < 75f ? FacilityHub | 0x0165u
: x < 85f ? FacilityHub | 0x016Eu
: FacilityHub | 0x017Au;
if (!cells.ContainsKey(rootId)) continue;
var eye = new Vector3(x, -40f, -4.3f);
var visible = Flood(cells, rootId, eye, gazeDir);
if (prev is not null)
{
var removed = prev.Except(visible).ToList();
var added = visible.Except(prev).ToList();
if (removed.Count > 0 || added.Count > 0)
{
churn++;
_out.WriteLine($"x={x,6:F2} root={rootId & 0xFFFFu:X4} flood={visible.Count,2}" +
(removed.Count > 0 ? $" REMOVED=[{CellSetString(removed)}]" : "") +
(added.Count > 0 ? $" added=[{CellSetString(added)}]" : ""));
}
}
prev = visible;
}
_out.WriteLine($"admitted-set change events over the walk: {churn}");
}
}

View file

@ -1,727 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Numerics;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Scene;
using AcDream.App.Rendering.Scene.Arch;
using AcDream.App.Rendering.Walk;
using AcDream.Core.Physics;
using AcDream.Core.Rendering;
using AcDream.Core.World;
using AcDream.Core.Meshing;
using DatReaderWriter;
using DatReaderWriter.Options;
using DatEnvCell = DatReaderWriter.DBObjs.EnvCell;
using DatEnvironment = DatReaderWriter.DBObjs.Environment;
using DatGfxObj = DatReaderWriter.DBObjs.GfxObj;
using DatSetup = DatReaderWriter.DBObjs.Setup;
using Xunit;
using Xunit.Abstractions;
namespace AcDream.App.Tests.Rendering;
/// <summary>
/// #177 decisive full-chain replay: drive the REAL <see cref="RetailChaseCamera"/>
/// + REAL <see cref="PhysicsCameraCollisionProbe"/> down the Facility Hub
/// staircase against the REAL BSP, then feed the swept (ViewerCellId, eye,
/// View*Projection) into <see cref="PortalVisibilityBuilder.Build"/> — exactly
/// the production coupling (GameWindow ~9055-9065 + RetailPViewRenderer.DrawInside).
///
/// The earlier Issue176177 replays fed SYNTHETIC (root, eye) pairs: Scenario A
/// admitted the stairs fine from a coherent corridor root, Scenario E collapsed
/// the flood 12->1 but only because it HELD the root at the old cell while the
/// eye crossed a portal plane. This test removes the assumption: the root IS the
/// swept camera cell, the eye IS the swept camera eye, both from the same sweep.
/// If the stairs still collapse, #177 is a production root/eye coupling defect
/// and the printed per-portal side-test D names the failing gate. If they stay
/// admitted, the flood is exonerated and the artifact lives downstream.
/// </summary>
[Collection(CameraDiagnosticsCollection.Name)]
[Trait("Lane", "InstalledDat")]
public class Issue177StairDescentCameraFloodTests
{
private const uint FacilityHub = 0x8A020000u;
private readonly ITestOutputHelper _out;
public Issue177StairDescentCameraFloodTests(ITestOutputHelper output) => _out = output;
private static string? ResolveDatDir()
{
var fromEnv = Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR");
if (!string.IsNullOrWhiteSpace(fromEnv) && Directory.Exists(fromEnv)) return fromEnv;
var def = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"Documents", "Asheron's Call");
return Directory.Exists(def) ? def : null;
}
private static PhysicsEngine BuildHubEngine(DatCollection dats)
{
var cache = new PhysicsDataCache();
var engine = new PhysicsEngine { DataCache = cache };
for (uint low = 0x0100u; low <= 0x01FFu; low++)
{
uint id = FacilityHub | low;
var datCell = dats.Get<DatEnvCell>(id);
if (datCell is null) continue;
var environment = dats.Get<DatEnvironment>(0x0D000000u | datCell.EnvironmentId);
if (environment is null) continue;
if (!environment.Cells.TryGetValue(datCell.CellStructure, out var cellStruct) || cellStruct is null)
continue;
var world = Matrix4x4.CreateFromQuaternion(datCell.Position.Orientation) *
Matrix4x4.CreateTranslation(datCell.Position.Origin);
cache.CacheCellStruct(id, datCell, cellStruct, world);
}
var heights = new byte[81];
var heightTable = new float[256];
for (int i = 0; i < 256; i++) heightTable[i] = -1000f;
engine.AddLandblock(FacilityHub, new TerrainSurface(heights, heightTable),
Array.Empty<CellSurface>(), Array.Empty<PortalPlane>(), 0f, 0f);
return engine;
}
// Analytic ramp floor from Scenario B: flat -6 (x<95), descends to -9 across
// x 95..98.333, then flat -9.
private static float FloorZ(float x)
=> x < 95f ? -6f
: x < 98.333f ? -6f - 3f * (x - 95f) / 3.333f
: -9f;
// Replicated side test (PortalVisibilityBuilder.CameraOnInteriorSide is private).
private const float PortalSideEpsilon = 0.01f;
private static bool EyeInteriorSide(LoadedCell cell, int i, Vector3 eye)
{
if (i >= cell.ClipPlanes.Count) return true;
var pl = cell.ClipPlanes[i];
if (pl.Normal.LengthSquared() < 1e-8f) return true;
var local = Vector3.Transform(eye, cell.InverseWorldTransform);
float dot = Vector3.Dot(pl.Normal, local) + pl.D;
return pl.InsideSide == 0 ? dot >= -PortalSideEpsilon : dot <= PortalSideEpsilon;
}
private static float SideD(LoadedCell cell, int i, Vector3 eye)
{
if (i >= cell.ClipPlanes.Count) return float.NaN;
var pl = cell.ClipPlanes[i];
if (pl.Normal.LengthSquared() < 1e-8f) return float.NaN;
var local = Vector3.Transform(eye, cell.InverseWorldTransform);
return Vector3.Dot(pl.Normal, local) + pl.D;
}
private static string CellSet(IEnumerable<uint> ids)
=> string.Join(" ", ids.Select(id => $"{id & 0xFFFFu:X4}"));
[Fact]
[Trait("Purpose", "Diagnostic")]
public void Diagnostic_StairCellComposition_ShellVsStatics()
{
var datDir = ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
foreach (uint low in new uint[] { 0x015E, 0x015F, 0x01C8, 0x01C4, 0x01C9, 0x0210, 0x020E, 0x01C1, 0x01C0 })
{
uint id = FacilityHub | low;
var envCell = dats.Get<DatEnvCell>(id);
if (envCell is null) { _out.WriteLine($"0x{low:X4}: (no dat cell)"); continue; }
var environment = dats.Get<DatEnvironment>(0x0D000000u | envCell.EnvironmentId);
environment!.Cells.TryGetValue(envCell.CellStructure, out var cs);
int polys = cs?.Polygons.Count ?? -1;
int verts = cs?.VertexArray.Vertices.Count ?? -1;
var mn = new Vector3(float.MaxValue); var mx = new Vector3(float.MinValue);
if (cs is not null)
foreach (var kv in cs.VertexArray.Vertices)
{ var o = kv.Value.Origin; var v = new Vector3(o.X, o.Y, o.Z); mn = Vector3.Min(mn, v); mx = Vector3.Max(mx, v); }
int statics = envCell.StaticObjects?.Count ?? 0;
bool seenOut = (envCell.Flags & DatReaderWriter.Enums.EnvCellFlags.SeenOutside) != 0;
_out.WriteLine(FormattableString.Invariant(
$"0x{low:X4} env=0x{envCell.EnvironmentId:X4} struct={envCell.CellStructure} shellPolys={polys} shellVerts={verts} bounds=[{mn.X:F1},{mn.Y:F1},{mn.Z:F1}]..[{mx.X:F1},{mx.Y:F1},{mx.Z:F1}] portals={envCell.CellPortals.Count} statics={statics} seenOut={seenOut}"));
if (envCell.StaticObjects is not null)
foreach (var stab in envCell.StaticObjects)
{
var o = stab.Frame.Origin;
_out.WriteLine(FormattableString.Invariant(
$" static Id=0x{stab.Id:X8} kind={(stab.Id >> 24 == 0x02 ? "Setup" : stab.Id >> 24 == 0x01 ? "GfxObj" : "?")} pos=({o.X:F2},{o.Y:F2},{o.Z:F2})"));
}
// Portal normals (local space) — horizontal (|Nz|~1) = FLOOR/CEILING portal
// (the edge-on-clip suspect); vertical (|Nz|~0) = wall/doorway portal.
var lc = renderLookup(id);
if (lc is not null)
for (int i = 0; i < lc.Portals.Count; i++)
{
var n = i < lc.ClipPlanes.Count ? lc.ClipPlanes[i].Normal : Vector3.Zero;
string kind = MathF.Abs(n.Z) > 0.7f ? "FLOOR/CEIL" : MathF.Abs(n.Z) < 0.3f ? "wall" : "slope";
_out.WriteLine(FormattableString.Invariant(
$" portal[{i}]->0x{lc.Portals[i].OtherCellId:X4} N=({n.X:F2},{n.Y:F2},{n.Z:F2}) {kind} inside={lc.ClipPlanes[i].InsideSide}"));
}
}
LoadedCell? renderLookup(uint id)
{
try { return CornerFloodReplayTests.LoadCell(dats, id); }
catch (InvalidOperationException) { return null; }
}
}
/// <summary>
/// Campaign OVERHAUL S2 chunk 2: retargeted from the deleted
/// <c>ShadowObjectRegistry.ComputeStaticRenderCells</c> pure-function
/// side channel to the production
/// <see cref="ShadowObjectRegistry.RegisterMultiPart"/> transaction
/// (the SAME BSP-collision + <c>partArray:</c> recipe
/// <c>LandblockPhysicsPublisher.PublishStaticEntity</c> uses, and the
/// same one <c>RetailCellArrayComparatorInstalledDatTests</c>' fixture 1
/// exercises for this exact Setup) plus
/// <see cref="ShadowObjectRegistry.TryGetRetailCellArray"/>. The entity
/// id now MUST match the projection's
/// <see cref="RenderSourceMetadata.LocalEntityId"/> — chunk 2's
/// <see cref="WalkProductionWorldData"/> indoor sweep looks the retail
/// CELLARRAY up BY that id, unlike the deleted method's seed+position
/// pure function, which never carried entity identity. The former
/// "WithoutCollisionRows" name no longer describes production: a part
/// array is always supplied at the SAME registration as the collision
/// shapes now (chunk 1b), so <c>GetOwnerCells</c> for this BSP-bearing
/// Setup reflects that registration — and, per the comparator's fixture-1
/// evidence, equals the retail CELLARRAY exactly.
/// </summary>
[Fact]
public void FacilityStairAssembly_RegisterAcross015FTo015E()
{
string? datDir = ResolveDatDir();
if (datDir is null)
Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md.");
using var dats = new DatCollection(datDir, DatAccessType.Read);
const uint setupId = 0x02000623u;
DatSetup setup = Assert.IsType<DatSetup>(dats.Get<DatSetup>(setupId));
PhysicsEngine engine = BuildHubEngine(dats);
PhysicsDataCache cache = Assert.IsType<PhysicsDataCache>(engine.DataCache);
IReadOnlyList<MeshRef> meshRefs = SetupMesh.Flatten(setup);
foreach (MeshRef meshRef in meshRefs)
{
DatGfxObj gfxObj = Assert.IsType<DatGfxObj>(
dats.Get<DatGfxObj>(meshRef.GfxObjId));
cache.CacheGfxObj(meshRef.GfxObjId, gfxObj);
}
DatEnvCell parent = Assert.IsType<DatEnvCell>(
dats.Get<DatEnvCell>(FacilityHub | 0x015Fu));
var stair = Assert.Single(
parent.StaticObjects,
staticObject =>
staticObject.Id == setupId);
var rootPosition = new Vector3(
stair.Frame.Origin.X,
stair.Frame.Origin.Y,
stair.Frame.Origin.Z);
Quaternion rootRotation = stair.Frame.Orientation;
List<ShadowShape> bspShapes = ShadowShapeBuilder.FromLandblockBspParts(
meshRefs, isBuildingShell: false, cache.GetGfxObj);
List<ShadowShape> parts = ShadowShapeBuilder.FromStaticRenderParts(
meshRefs,
cache.GetGfxObj,
cache.GetVisualBounds,
out bool hasPhysicsBsp);
Assert.True(hasPhysicsBsp);
Assert.NotEmpty(parts);
Assert.NotEmpty(bspShapes);
const uint entityId = 0x48A02000u;
engine.ShadowObjects.RegisterMultiPart(
entityId,
rootPosition,
rootRotation,
bspShapes,
state: 0u,
flags: EntityCollisionFlags.None,
worldOffsetX: 0f,
worldOffsetY: 0f,
landblockId: FacilityHub,
seedCellId: FacilityHub | 0x015Fu,
isStatic: true,
partArray: parts);
Assert.True(engine.ShadowObjects.TryGetRetailCellArray(
entityId, out IReadOnlyList<uint> cells));
ShadowShape part = parts[0];
ShadowPartBox box = ShadowPartBox.FromShape(
part,
rootPosition,
rootRotation);
LoadedCell rootCell = CornerFloodReplayTests.LoadCell(
dats,
FacilityHub | 0x015Fu);
box.RefitToLocal(rootCell.InverseWorldTransform, out Vector3 boxMin, out Vector3 boxMax);
_out.WriteLine(FormattableString.Invariant(
$"stair root=({rootPosition.X:F3},{rootPosition.Y:F3},{rootPosition.Z:F3}) localBox=({boxMin.X:F3},{boxMin.Y:F3},{boxMin.Z:F3})..({boxMax.X:F3},{boxMax.Y:F3},{boxMax.Z:F3}) cells=[{string.Join(',', cells.Select(static id => $"0x{id:X8}"))}]"));
Assert.Contains(FacilityHub | 0x015Fu, cells);
Assert.Contains(FacilityHub | 0x015Eu, cells);
// The registration carries collision shapes at the SAME transaction
// now (production always does — chunk 1b threaded the part array
// through the SAME RegisterMultiPart call the collision dispatch
// uses), so GetOwnerCells reflects that registration instead of
// staying empty; retail == collision for this BSP-bearing Setup
// (RetailCellArrayComparatorInstalledDatTests fixture 1), so the two
// sets agree exactly.
Assert.Equal(
cells.OrderBy(id => id),
engine.ShadowObjects.GetOwnerCells(entityId).OrderBy(id => id));
RenderSceneGeneration generation = RenderSceneGeneration.FromRaw(1);
using var scene = new ArchRenderScene(generation);
RenderProjectionId projectionId = RenderProjectionId.FromRaw(entityId);
RenderTransform transform = RenderTransform.FromRoot(
rootPosition,
rootRotation,
1f);
RenderProjectionRecord projection = new RenderProjectionRecord() with
{
Id = projectionId,
ProjectionClass = RenderProjectionClass.IndoorCellStatic,
OwnerIncarnation = RenderOwnerIncarnation.FromRaw(1),
Transform = transform,
PreviousTransform = new PreviousRenderTransform(transform.LocalToWorld),
Residency = new RenderSpatialResidency(
RenderSpatialBucket.FromRaw(FacilityHub | 0x015Fu),
FacilityHub,
FacilityHub | 0x015Fu),
Flags = RenderProjectionFlags.Draw,
Source = new RenderSourceMetadata() with
{
LocalEntityId = entityId,
SourceId = setupId,
ParentCellId = FacilityHub | 0x015Fu,
},
EntityPayload = new RenderEntityPayload(meshRefs, null, false),
};
scene.Apply([RenderProjectionDelta.Register(generation, 1, projection)]);
var worldData = new WalkProductionWorldData(
new WalkBuildingRegistry(),
engine.ShadowObjects);
worldData.BeginFrame(
scene.OpenQuery(),
FacilityHub,
renderCenterLbX: 0x8A,
renderCenterLbY: 0x02);
Assert.Contains(
worldData.GetCellStatics(FacilityHub | 0x015Eu).Records,
record => record.Id == projectionId);
Assert.Equal(0, worldData.UnregisteredRenderMembershipCount);
}
/// <summary>
/// #177 re-diagnosis (fix#1 failed the visual gate): the vanish flips on a SLIGHT turn +
/// depends on zoom, so it is a gaze/eye knife-edge, not the eye-squarely-in-opening case
/// fix#1 handled. Drive the REAL camera at a fixed player spot on the 015F stairs, sweep
/// yaw FINELY at two zooms, and print the viewer cell + stair-cell admission each step to
/// locate the knife-edge and which cell flips.
/// </summary>
[Fact]
[Trait("Purpose", "Diagnostic")]
public void Diagnostic_RealStaircase_FineYawZoomSweep_FindKnifeEdge()
{
var datDir = ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var renderCells = Issue120ReciprocalPingPongTests.LoadAllInteriorCells(dats, FacilityHub);
var engine = BuildHubEngine(dats);
Func<uint, LoadedCell?> lookup = id => renderCells.TryGetValue(id, out var c) ? c : null;
bool sC = CameraDiagnostics.CollideCamera; bool sA = CameraDiagnostics.AlignToSlope;
try
{
CameraDiagnostics.CollideCamera = true; CameraDiagnostics.AlignToSlope = true;
CameraDiagnostics.TranslationStiffness = 0.45f; CameraDiagnostics.RotationStiffness = 0.45f;
uint[] stair = { 0x015Eu, 0x015Fu, 0x01C1u, 0x01C0u, 0x020Fu };
// Player standing mid-015F stairs, eye near the ceiling boundary (per the sweep char).
foreach (var playerPos in new[] { new Vector3(60.5f, -46f, -2.5f), new Vector3(60.5f, -47f, -3.0f) })
foreach (float dist in new[] { 2.61f, 12.0f })
{
_out.WriteLine(FormattableString.Invariant(
$"=== player=({playerPos.X:F1},{playerPos.Y:F1},{playerPos.Z:F1}) zoom={dist} ==="));
var cam = new RetailChaseCamera
{
CollisionProbe = new PhysicsCameraCollisionProbe(engine),
FovY = MathF.PI / 3f, Aspect = 16f / 9f, Distance = dist,
};
uint prevViewer = 0; string prevAdmit = "";
for (float yawDeg = 0f; yawDeg < 360f; yawDeg += 6f)
{
float yaw = yawDeg * MathF.PI / 180f;
var (pcell, _) = engine.AdjustPosition(FacilityHub | 0x015Fu, playerPos + new Vector3(0, 0, 0.1f));
for (int i = 0; i < 160; i++)
cam.Update(playerPos, yaw, Vector3.Zero, true, Vector3.UnitZ, 1f / 60f, pcell, 0x5000000A);
var eye = cam.Position; uint viewer = cam.ViewerCellId;
string admit = "?";
if (viewer != 0u && renderCells.TryGetValue(viewer, out var rc))
{
var f = PortalVisibilityBuilder.Build(rc, eye, lookup, cam.View * cam.Projection, null);
var s = f.OrderedVisibleCells;
admit = string.Concat(stair.Select(low => (s.Contains(FacilityHub | low) ? "1" : "0")));
}
// Print only when viewer or the admit pattern CHANGES (knife-edges).
if (viewer != prevViewer || admit != prevAdmit)
{
_out.WriteLine(FormattableString.Invariant(
$" yaw={yawDeg,3:F0} eye=({eye.X,6:F2},{eye.Y,6:F2},{eye.Z,6:F2}) viewer=0x{viewer & 0xFFFF:X4} stair[5E,5F,C1,C0,0F]={admit}"));
prevViewer = viewer; prevAdmit = admit;
}
}
}
}
finally { CameraDiagnostics.CollideCamera = sC; CameraDiagnostics.AlignToSlope = sA; }
}
/// <summary>
/// #177 retail-vs-us flood DEPTH: retail's cdb capture shows PView cell_draw_num reaching 26
/// cells from camera cell 015E (avg 14.3) looking down the spiral shaft. Sweep OUR real camera
/// from 015E over many gaze/zoom poses and report the max/avg OrderedVisibleCells — if ours
/// caps well below retail's 26, our flood truncates the spiral (the vanish).
/// </summary>
[Fact]
[Trait("Purpose", "Diagnostic")]
public void Diagnostic_FloodDepthFrom015E_VsRetail26()
{
var datDir = ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var renderCells = Issue120ReciprocalPingPongTests.LoadAllInteriorCells(dats, FacilityHub);
var engine = BuildHubEngine(dats);
Func<uint, LoadedCell?> lookup = id => renderCells.TryGetValue(id, out var c) ? c : null;
bool sC = CameraDiagnostics.CollideCamera; bool sA = CameraDiagnostics.AlignToSlope;
try
{
CameraDiagnostics.CollideCamera = true; CameraDiagnostics.AlignToSlope = true;
CameraDiagnostics.TranslationStiffness = 0.45f; CameraDiagnostics.RotationStiffness = 0.45f;
int maxCells = 0; uint bestViewer = 0; double sum = 0; int n = 0; string bestSet = "";
foreach (var playerPos in new[] { new Vector3(61f, -42f, -4f), new Vector3(60f, -41f, -5f), new Vector3(62f, -43f, -4f) })
foreach (float dist in new[] { 2.61f, 4f, 8f })
foreach (float pitch in new[] { 0.291f, 0.7f, -0.2f })
{
var cam = new RetailChaseCamera { CollisionProbe = new PhysicsCameraCollisionProbe(engine), FovY = MathF.PI / 3f, Aspect = 16f / 9f, Distance = dist, Pitch = pitch };
for (float yawDeg = 0f; yawDeg < 360f; yawDeg += 10f)
{
float yaw = yawDeg * MathF.PI / 180f;
var (pcell, _) = engine.AdjustPosition(FacilityHub | 0x015Eu, playerPos + new Vector3(0, 0, 0.1f));
for (int i = 0; i < 140; i++) cam.Update(playerPos, yaw, Vector3.Zero, true, Vector3.UnitZ, 1f / 60f, pcell, 0x5000000A);
uint viewer = cam.ViewerCellId;
if (viewer == 0u || !renderCells.TryGetValue(viewer, out var rc)) continue;
var f = PortalVisibilityBuilder.Build(rc, cam.Position, lookup, cam.View * cam.Projection, null);
int c = f.OrderedVisibleCells.Count; sum += c; n++;
if (c > maxCells) { maxCells = c; bestViewer = viewer; bestSet = CellSet(f.OrderedVisibleCells.OrderBy(v => v)); }
}
}
_out.WriteLine(FormattableString.Invariant(
$"OUR flood from 015E-area: maxCells={maxCells} (retail max=26) avg={sum / Math.Max(1, n):F1} (retail avg=14.3) bestViewer=0x{bestViewer & 0xFFFF:X4}"));
_out.WriteLine($" best set: [{bestSet}]");
}
finally { CameraDiagnostics.CollideCamera = sC; CameraDiagnostics.AlignToSlope = sA; }
}
private static Matrix4x4 ViewProj(Vector3 eye, Vector3 dir)
=> Matrix4x4.CreateLookAt(eye, eye + Vector3.Normalize(dir), Vector3.UnitZ)
* Matrix4x4.CreatePerspectiveFieldOfView(MathF.PI / 3f, 16f / 9f, 0.1f, 5000f);
// Signed distance of a WORLD point to a render cell's portal[i] plane (local-space plane).
private static float PortalPlaneDistance(LoadedCell cell, int i, Vector3 worldPt)
{
var pl = cell.ClipPlanes[i];
var local = Vector3.Transform(worldPt, cell.InverseWorldTransform);
return Vector3.Dot(pl.Normal, local) + pl.D;
}
/// <summary>
/// #177 THE decisive experiment. Reproduce the captured failing frame — camera in
/// cell 0x015F with the eye sitting IN the ceiling-portal plane (production [flap]:
/// root=015F eye=(60.47,-46.57,-0.00), p->01C1 clip=0) — and confirm the upper
/// stair cell 01C1 drops from the flood. Then move the SAME eye 0.4 m off that
/// plane (into the cell interior) and confirm 01C1 is re-admitted. If the contrast
/// holds, #177's fix is a viewpoint-off-the-portal-plane rule, and the load-bearing
/// clip (#119/#181) is correct as-is.
/// </summary>
[Fact]
public void EdgeOnCeilingPortal_DropsUpperCell_OffPlaneReadmits()
{
var datDir = ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var cells = Issue120ReciprocalPingPongTests.LoadAllInteriorCells(dats, FacilityHub);
Func<uint, LoadedCell?> lookup = id => cells.TryGetValue(id, out var c) ? c : null;
var root = cells[FacilityHub | 0x015Fu];
uint upperId = FacilityHub | 0x01C1u;
// Find 015F's portal to 01C1 (the ceiling/up portal) + report its normal.
int upIdx = -1;
for (int i = 0; i < root.Portals.Count; i++)
if (root.Portals[i].OtherCellId == 0x01C1u) { upIdx = i; break; }
Assert.True(upIdx >= 0, "015F has no portal to 01C1");
var upN = root.ClipPlanes[upIdx].Normal;
_out.WriteLine(FormattableString.Invariant(
$"015F portal[{upIdx}]->01C1 localN=({upN.X:F2},{upN.Y:F2},{upN.Z:F2})"));
// Look UP the stairs so the ceiling portal + 01C1 land on screen.
var gaze = new Vector3(0f, -0.3f, 1f);
// Captured FAILING eye — in the ceiling-portal plane (world z ~0).
var eyeEdgeOn = new Vector3(60.47f, -46.57f, -0.00f);
// Same eye, pulled 0.4 m DOWN (off the ceiling portal, into 015F's interior).
var eyeOffPlane = eyeEdgeOn + new Vector3(0f, 0f, -0.40f);
foreach (var (label, eye) in new[] { ("edge-on(captured)", eyeEdgeOn), ("off-plane(-0.4z)", eyeOffPlane) })
{
float d = PortalPlaneDistance(root, upIdx, eye);
var frame = PortalVisibilityBuilder.Build(
root, eye, lookup, ViewProj(eye, gaze),
buildingMembership: null);
var vis = frame.OrderedVisibleCells.OrderBy(v => v).ToList();
bool up = vis.Contains(upperId);
string admit = up ? "ADMITTED" : "DROPPED";
_out.WriteLine(FormattableString.Invariant(
$"{label,-18} eye=({eye.X,6:F2},{eye.Y,6:F2},{eye.Z,6:F2}) D->01C1={d,6:F3} 01C1={admit} flood={vis.Count} [{CellSet(vis)}]"));
}
// #177 CHARACTERIZATION (fix#1 reverted — this edge-on-in-plane case was NOT the
// production mechanism; the real vanish is a grazing/sliver flood collapse in the
// spiral, root 0x01C8, portals off-screen/sliver, camera NOT collision-jammed).
// Kept as a mechanism pin: an eye exactly in the ceiling-portal plane drops the upper
// cell (edge-on → <3 clip), an eye 0.4 m off admits it via the normal clip.
var fEdge = PortalVisibilityBuilder.Build(root, eyeEdgeOn, lookup, ViewProj(eyeEdgeOn, gaze), null);
var fOff = PortalVisibilityBuilder.Build(root, eyeOffPlane, lookup, ViewProj(eyeOffPlane, gaze), null);
Assert.DoesNotContain(upperId, fEdge.OrderedVisibleCells); // edge-on drops (mechanism)
Assert.Contains(upperId, fOff.OrderedVisibleCells); // off-plane admits (control)
}
/// <summary>
/// #177 sweep characterization: place the player on the 015F staircase and drive the
/// REAL chase camera + collision sweep, reporting where the eye lands relative to the
/// 015F ceiling-portal plane (world z=0). If the eye rests AT the plane (|D|~0) the
/// flood collapses (see EdgeOnCeilingPortal test); this shows whether that is a swept
/// contact leaving the eye on the surface vs free-space boom geometry.
/// </summary>
[Fact]
[Trait("Purpose", "Diagnostic")]
public void Diagnostic_StaircaseSweep_EyeClearanceFromCeilingPortal()
{
var datDir = ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var renderCells = Issue120ReciprocalPingPongTests.LoadAllInteriorCells(dats, FacilityHub);
var engine = BuildHubEngine(dats);
var root015F = renderCells[FacilityHub | 0x015Fu];
int upIdx = 0; // portal[0]->01C1 (ceiling)
bool sC = CameraDiagnostics.CollideCamera; bool sA = CameraDiagnostics.AlignToSlope;
try
{
CameraDiagnostics.CollideCamera = true; CameraDiagnostics.AlignToSlope = true;
CameraDiagnostics.TranslationStiffness = 0.45f; CameraDiagnostics.RotationStiffness = 0.45f;
// Player on the 015F stairs (world y~-46, ceiling portal at world z~0), swept
// at a range of heights + facings (up-stairs = -Y, down-stairs = +Y).
foreach (float pz in new[] { -1.0f, -1.5f, -2.0f, -2.5f, -3.0f, -3.5f })
foreach (var (fLabel, yaw) in new (string, float)[] { ("up(-Y)", -MathF.PI / 2f), ("down(+Y)", MathF.PI / 2f) })
{
var playerPos = new Vector3(60.5f, -46f, pz);
var (pcell, _) = engine.AdjustPosition(FacilityHub | 0x015Fu, playerPos + new Vector3(0, 0, 0.1f));
var cam = new RetailChaseCamera
{
CollisionProbe = new PhysicsCameraCollisionProbe(engine),
FovY = MathF.PI / 3f, Aspect = 16f / 9f,
};
for (int i = 0; i < 300; i++)
cam.Update(playerPos, yaw, Vector3.Zero, true, Vector3.UnitZ, 1f / 60f, pcell, 0x5000000A);
var eye = cam.Position;
uint viewer = cam.ViewerCellId;
// D from the eye to 015F's ceiling-portal plane (only meaningful if viewer==015F).
float dCeil = PortalPlaneDistance(root015F, upIdx, eye);
_out.WriteLine(FormattableString.Invariant(
$"playerZ={pz,5:F1} face={fLabel,-8} pcell=0x{pcell & 0xFFFF:X4} eye=({eye.X,6:F2},{eye.Y,6:F2},{eye.Z,6:F2}) viewer=0x{viewer & 0xFFFF:X4} D->ceilPortal={dCeil,6:F3}"));
}
}
finally { CameraDiagnostics.CollideCamera = sC; CameraDiagnostics.AlignToSlope = sA; }
}
[Theory]
[Trait("Purpose", "Diagnostic")]
// spot label, player x, player cell, description
[InlineData(90.0f, 0x0178u, "TOP corridor 0178 (approach view)")]
[InlineData(100.0f, 0x0183u, "BOTTOM room 0183 (look-back view)")]
public void Diagnostic_ParkedYawZoomSweep_StairAdmission(float px, uint pcell, string label)
{
var datDir = ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var renderCells = Issue120ReciprocalPingPongTests.LoadAllInteriorCells(dats, FacilityHub);
var engine = BuildHubEngine(dats);
Func<uint, LoadedCell?> lookup = id => renderCells.TryGetValue(id, out var c) ? c : null;
bool sC = CameraDiagnostics.CollideCamera; bool sA = CameraDiagnostics.AlignToSlope;
float sT = CameraDiagnostics.TranslationStiffness, sR = CameraDiagnostics.RotationStiffness;
try
{
CameraDiagnostics.CollideCamera = true; CameraDiagnostics.AlignToSlope = true;
CameraDiagnostics.TranslationStiffness = 0.45f; CameraDiagnostics.RotationStiffness = 0.45f;
var playerPos = new Vector3(px, -40f, FloorZ(px));
uint playerCell = FacilityHub | pcell;
_out.WriteLine($"=== {label} @ ({px:F1},-40,{FloorZ(px):F2}) ===");
foreach (float dist in new[] { 2.61f, 8.0f })
{
_out.WriteLine($" -- zoom Distance={dist} --");
var cam = new RetailChaseCamera
{
CollisionProbe = new PhysicsCameraCollisionProbe(engine),
FovY = MathF.PI / 3f, Aspect = 16f / 9f, Distance = dist,
};
for (float yawDeg = 0f; yawDeg < 360f; yawDeg += 30f)
{
float yaw = yawDeg * MathF.PI / 180f;
// Settle at this yaw (static player).
for (int i = 0; i < 200; i++)
cam.Update(playerPos, yaw, Vector3.Zero, true, Vector3.UnitZ, 1f / 60f, playerCell, 0x5000000A);
Vector3 eye = cam.Position; uint viewer = cam.ViewerCellId;
var vp = cam.View * cam.Projection;
if (viewer != 0u && renderCells.TryGetValue(viewer, out var rootCell))
{
var f = PortalVisibilityBuilder.Build(rootCell, eye, lookup, vp, null);
var vis = f.OrderedVisibleCells.OrderBy(v => v).ToList();
string flags = $"0178={(vis.Contains(FacilityHub | 0x0178u) ? "Y" : "-")} 0182={(vis.Contains(FacilityHub | 0x0182u) ? "Y" : "-")} 0183={(vis.Contains(FacilityHub | 0x0183u) ? "Y" : "-")} 0181={(vis.Contains(FacilityHub | 0x0181u) ? "Y" : "-")}";
_out.WriteLine(FormattableString.Invariant(
$" yaw={yawDeg,3:F0} eye=({eye.X,6:F2},{eye.Y,6:F2},{eye.Z,6:F2}) viewer=0x{viewer & 0xFFFF:X4} {flags} flood={vis.Count} [{CellSet(vis)}]"));
}
else
_out.WriteLine(FormattableString.Invariant($" yaw={yawDeg,3:F0} eye=({eye.X,6:F2},{eye.Y,6:F2},{eye.Z,6:F2}) viewer=0x{viewer:X8} ROOT-MISSING"));
}
}
}
finally
{
CameraDiagnostics.CollideCamera = sC; CameraDiagnostics.AlignToSlope = sA;
CameraDiagnostics.TranslationStiffness = sT; CameraDiagnostics.RotationStiffness = sR;
}
}
[Fact]
[Trait("Purpose", "Diagnostic")]
public void Diagnostic_Descent_RealCameraSweep_StairCellRetention()
{
var datDir = ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var renderCells = Issue120ReciprocalPingPongTests.LoadAllInteriorCells(dats, FacilityHub);
var engine = BuildHubEngine(dats);
Func<uint, LoadedCell?> lookup = id => renderCells.TryGetValue(id, out var c) ? c : null;
bool savedColl = CameraDiagnostics.CollideCamera;
bool savedAlign = CameraDiagnostics.AlignToSlope;
float savedT = CameraDiagnostics.TranslationStiffness;
float savedR = CameraDiagnostics.RotationStiffness;
try
{
CameraDiagnostics.CollideCamera = true;
CameraDiagnostics.AlignToSlope = true;
CameraDiagnostics.TranslationStiffness = 0.45f;
CameraDiagnostics.RotationStiffness = 0.45f;
var cam = new RetailChaseCamera
{
CollisionProbe = new PhysicsCameraCollisionProbe(engine),
FovY = MathF.PI / 3f,
Aspect = 16f / 9f,
};
const float yaw = 0f; // facing +X, descending
const float speed = 3.0f; // m/s run
const float dt = 1f / 60f;
float startX = 88f, endX = 101.5f;
// Seat the player + settle the camera at the start pose.
uint playerCell = FacilityHub | 0x0178u;
Vector3 P(float x) => new Vector3(x, -40f, FloorZ(x));
for (int i = 0; i < 400; i++)
cam.Update(P(startX), yaw, Vector3.Zero, true, Vector3.UnitZ, dt, playerCell, 0x5000000A);
_out.WriteLine("x playerCell eye=(x,y,z) viewerCell coh flood stairs rootPortalsPastPlane");
float x = startX;
uint prevViewer = cam.ViewerCellId;
int collapses = 0, incoherentFrames = 0;
while (x <= endX)
{
var playerPos = P(x);
// Track player membership like production: AdjustPosition carry-forward.
var (pc, found) = engine.AdjustPosition(playerCell, playerPos + new Vector3(0, 0, 0.1f));
if (found) playerCell = pc;
cam.Update(playerPos, yaw, new Vector3(speed, 0, 0), true, Vector3.UnitZ, dt, playerCell, 0x5000000A);
Vector3 eye = cam.Position;
uint viewer = cam.ViewerCellId;
var viewProj = cam.View * cam.Projection;
string floodStr, stairs, coh, past;
if (viewer != 0u && renderCells.TryGetValue(viewer, out var rootCell))
{
var frame = PortalVisibilityBuilder.Build(
rootCell, eye, lookup, viewProj,
buildingMembership: null);
var vis = frame.OrderedVisibleCells.OrderBy(v => v).ToList();
floodStr = $"{vis.Count,2} [{CellSet(vis)}]";
bool ramp = vis.Contains(FacilityHub | 0x0182u);
bool lower = vis.Contains(FacilityHub | 0x0183u);
stairs = $"r{(ramp ? "Y" : "-")}l{(lower ? "Y" : "-")}";
// Coherence: is the eye on the interior side of ALL of root's portals?
// If not, the eye is outside root -> the crossed portal's subtree drops.
var pastList = new List<string>();
bool coherent = true;
for (int i = 0; i < rootCell.Portals.Count && i < rootCell.ClipPlanes.Count; i++)
{
if (!EyeInteriorSide(rootCell, i, eye))
{
coherent = false;
pastList.Add($"p{i}->0x{rootCell.Portals[i].OtherCellId:X4}(D={SideD(rootCell, i, eye):F3})");
}
}
coh = coherent ? "IN " : "OUT";
past = pastList.Count == 0 ? "-" : string.Join(",", pastList);
if (!coherent) incoherentFrames++;
if (vis.Count <= 1) collapses++;
}
else
{
floodStr = "root-missing/0"; stairs = "--"; coh = "?"; past = $"viewer=0x{viewer:X8}";
}
bool viewerFlip = viewer != prevViewer;
prevViewer = viewer;
_out.WriteLine(FormattableString.Invariant(
$"{x,6:F2} 0x{playerCell:X8} ({eye.X,7:F3},{eye.Y,7:F3},{eye.Z,7:F3}) 0x{viewer:X8} {coh} {floodStr} {stairs} {(viewerFlip ? "FLIP " : "")}{past}"));
x += speed * dt;
}
_out.WriteLine($"SUMMARY: collapses(flood<=1)={collapses} incoherentFrames(eye-outside-root)={incoherentFrames}");
}
finally
{
CameraDiagnostics.CollideCamera = savedColl;
CameraDiagnostics.AlignToSlope = savedAlign;
CameraDiagnostics.TranslationStiffness = savedT;
CameraDiagnostics.RotationStiffness = savedR;
}
}
}

View file

@ -1,165 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using AcDream.App.Rendering;
using DatReaderWriter;
using DatReaderWriter.Options;
using Xunit;
using Xunit.Abstractions;
namespace AcDream.App.Tests.Rendering;
/// <summary>
/// #181 — the portal-flood admitted set flaps 31↔32 on MICRON eye noise at a
/// parked camera (live: launch-176-leakfix.log, root 0x8A020142, vis flips
/// every ~100200 frames for 517k frames; the swept eye carries ~7 µm
/// float-roundtrip noise). The flapping cell's visibility-scoped lights + the
/// union-AABB scissor rect strobe = the #176 washed-region flicker.
///
/// This replay hunts the knife edge headlessly: at the live parked eye
/// (49.15, 38.62, 3.98), sweep gazes and perturb the eye by ±0.5 mm per
/// axis; ANY admitted-set difference at sub-mm perturbation is the #181
/// instability, and the symmetric difference names the flapping cell.
/// </summary>
[Trait("Lane", "InstalledDat")]
public class Issue181VisFlapReplayTests
{
private const uint FacilityHub = 0x8A020000u;
private const uint LiveRoot = FacilityHub | 0x0142u;
private static readonly Vector3 LiveEye = new(49.15f, -38.62f, -3.98f);
private readonly ITestOutputHelper _out;
public Issue181VisFlapReplayTests(ITestOutputHelper output) => _out = output;
private static Matrix4x4 ViewProjFor(Vector3 eye, Vector3 gazeDir)
{
var view = Matrix4x4.CreateLookAt(eye, eye + gazeDir, Vector3.UnitZ);
var proj = Matrix4x4.CreatePerspectiveFieldOfView(MathF.PI / 3f, 16f / 9f, 0.1f, 5000f);
return view * proj;
}
private static List<uint> Flood(
Dictionary<uint, LoadedCell> cells, uint rootId, Vector3 eye, Vector3 gazeDir)
{
Func<uint, LoadedCell?> lookup = id => cells.TryGetValue(id, out var c) ? c : null;
var frame = PortalVisibilityBuilder.Build(
cells[rootId], eye, lookup, ViewProjFor(eye, gazeDir),
buildingMembership: null);
var result = new List<uint>(frame.OrderedVisibleCells);
result.Sort();
return result;
}
private static string CellSetString(IEnumerable<uint> ids)
=> string.Join(" ", ids.Select(id => $"{id & 0xFFFFu:X4}"));
[Fact]
[Trait("Purpose", "Diagnostic")]
public void Diagnostic_FlappingCellViewRegion_SliverOrLarge()
{
var datDir = CornerFloodReplayTests.ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var cells = Issue120ReciprocalPingPongTests.LoadAllInteriorCells(dats, FacilityHub);
uint flapper = FacilityHub | 0x0181u;
// The most unstable pose from the perturbation sweep.
float yaw = 15f * MathF.PI / 180f, pitch = -20f * MathF.PI / 180f;
var gaze = new Vector3(
MathF.Cos(pitch) * MathF.Cos(yaw),
MathF.Cos(pitch) * MathF.Sin(yaw),
MathF.Sin(pitch));
Func<uint, LoadedCell?> lookup = id => cells.TryGetValue(id, out var c) ? c : null;
foreach (var (label, eye) in new (string, Vector3)[]
{
("base ", LiveEye),
("+0.5mm x ", LiveEye + new Vector3(0.0005f, 0, 0)),
("-0.5mm x ", LiveEye - new Vector3(0.0005f, 0, 0)),
("+5mm x ", LiveEye + new Vector3(0.005f, 0, 0)),
("-5mm x ", LiveEye - new Vector3(0.005f, 0, 0)),
("+2cm z ", LiveEye + new Vector3(0, 0, 0.02f)),
("-2cm z ", LiveEye - new Vector3(0, 0, 0.02f)),
})
{
var frame = PortalVisibilityBuilder.Build(
cells[LiveRoot], eye, lookup, ViewProjFor(eye, gaze),
buildingMembership: null);
if (!frame.CellViews.TryGetValue(flapper, out var view) || view.IsEmpty)
{
_out.WriteLine($"{label}: 0181 NOT ADMITTED");
continue;
}
// NDC shoelace area of the admitted region (screen is 4.0 NDC-units total).
float area = 0f; int polys = 0, verts = 0;
foreach (var vp in view.Polygons)
{
var v = vp.Vertices;
polys++; verts += v.Length;
float a = 0f;
for (int i = 0; i < v.Length; i++)
{
var p = v[i]; var q = v[(i + 1) % v.Length];
a += p.X * q.Y - q.X * p.Y;
}
area += MathF.Abs(a) * 0.5f;
}
_out.WriteLine(FormattableString.Invariant(
$"{label}: 0181 admitted polys={polys} verts={verts} ndcArea={area:F4} ({area / 4f * 100f:F1}% of screen)"));
}
}
[Fact]
[Trait("Purpose", "Diagnostic")]
public void Diagnostic_SubMillimeterEyePerturbation_MustNotChangeAdmission()
{
var datDir = CornerFloodReplayTests.ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var cells = Issue120ReciprocalPingPongTests.LoadAllInteriorCells(dats, FacilityHub);
Assert.True(cells.ContainsKey(LiveRoot), "live root 0x0142 not loaded");
// ±0.5 mm per axis — an order of magnitude above the live µm noise, well
// below anything visually meaningful.
var perturbations = new[]
{
new Vector3( 0.0005f, 0, 0), new Vector3(-0.0005f, 0, 0),
new Vector3(0, 0.0005f, 0), new Vector3(0, -0.0005f, 0),
new Vector3(0, 0, 0.0005f), new Vector3(0, 0, -0.0005f),
};
int unstableGazes = 0;
for (int yawDeg = 0; yawDeg < 360; yawDeg += 15)
{
foreach (float pitchDeg in new[] { 0f, -20f, -35f })
{
float yaw = yawDeg * MathF.PI / 180f, pitch = pitchDeg * MathF.PI / 180f;
var gaze = new Vector3(
MathF.Cos(pitch) * MathF.Cos(yaw),
MathF.Cos(pitch) * MathF.Sin(yaw),
MathF.Sin(pitch));
var baseline = Flood(cells, LiveRoot, LiveEye, gaze);
foreach (var d in perturbations)
{
var perturbed = Flood(cells, LiveRoot, LiveEye + d, gaze);
if (perturbed.SequenceEqual(baseline)) continue;
unstableGazes++;
var removed = baseline.Except(perturbed).ToList();
var added = perturbed.Except(baseline).ToList();
_out.WriteLine(FormattableString.Invariant(
$"UNSTABLE yaw={yawDeg} pitch={pitchDeg} d=({d.X * 1000:F1},{d.Y * 1000:F1},{d.Z * 1000:F1})mm base={baseline.Count} pert={perturbed.Count} removed=[{CellSetString(removed)}] added=[{CellSetString(added)}]"));
}
}
}
_out.WriteLine($"unstable (gaze, perturbation) pairs: {unstableGazes}");
// Diagnostic first: report, don't assert — the fix turns this into a hard pin.
}
}

View file

@ -155,29 +155,12 @@ public class Issue181WallPressEquilibriumTests
_out.WriteLine(FormattableString.Invariant(
$"viewer cells seen: {cells.Count} transitions={cellTransitions} eye=({cam.Position.X:F6},{cam.Position.Y:F6},{cam.Position.Z:F6}) cell=0x{cam.ViewerCellId:X8}"));
// Orbit structure: 16 consecutive frames at 6dp, with the sweep's
// own [flap-sweep] lines captured for the same frames.
bool savedFlap = RenderingDiagnostics.ProbeFlapEnabled;
var savedOut = Console.Out;
try
// Orbit structure: 16 consecutive frames at 6dp.
for (int i = 0; i < 16; i++)
{
RenderingDiagnostics.ProbeFlapEnabled = true;
using var writer = new StringWriter();
Console.SetOut(writer);
for (int i = 0; i < 16; i++)
{
Step();
writer.WriteLine(FormattableString.Invariant(
$"orbit[{i:D2}] eye=({cam.Position.X:F6},{cam.Position.Y:F6},{cam.Position.Z:F6})"));
}
Console.SetOut(savedOut);
foreach (var line in writer.ToString().Split('\n'))
if (line.Length > 1) _out.WriteLine(line.TrimEnd());
}
finally
{
Console.SetOut(savedOut);
RenderingDiagnostics.ProbeFlapEnabled = savedFlap;
Step();
_out.WriteLine(FormattableString.Invariant(
$"orbit[{i:D2}] eye=({cam.Position.X:F6},{cam.Position.Y:F6},{cam.Position.Z:F6})"));
}
}
finally

View file

@ -1,200 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using AcDream.App.Rendering;
using DatReaderWriter;
using DatReaderWriter.Options;
using Xunit;
using Xunit.Abstractions;
using DatLandBlockInfo = DatReaderWriter.DBObjs.LandBlockInfo;
namespace AcDream.App.Tests.Rendering;
/// <summary>
/// #95 MEASUREMENT (2026-06-13): entering the 0x0007 dungeon (Town Network) explodes
/// WB-DIAG to ~9.1M instances/frame. Suspected cause: <see cref="PortalVisibilityBuilder.Build"/>
/// floods the dungeon's portal graph WITHOUT the retail grab_visible_cells stab_list bounding
/// (decomp:311878). A dungeon cell has <c>seen_outside==0</c>; retail's PVS for it is just the
/// cell's <c>stab_list</c> (<see cref="LoadedCell.VisibleCells"/>) — typically a small bounded
/// set. If our flood instead visits ~all cells of the landblock, that is the blowup.
///
/// This is a DIAGNOSTIC, not a fix: it loads the real 0x0007 interior cells, runs the real
/// production flood from representative dungeon-cell roots, and PRINTS the ground-truth numbers —
/// flood visited-cell-set size (<see cref="PortalVisibilityFrame.OrderedVisibleCells"/>) vs the
/// root's stab_list size (<see cref="LoadedCell.VisibleCells"/>), plus how many visited cells
/// cross landblocks. The single assertion just guarantees the test ran; the VALUE is the output.
/// </summary>
[Trait("Lane", "InstalledDat")]
public class Issue95DungeonFloodDiagnosticTests
{
private const uint TownNetwork = 0x00070000u;
private readonly ITestOutputHelper _out;
public Issue95DungeonFloodDiagnosticTests(ITestOutputHelper output) => _out = output;
// Production-ish projection (mirrors the sibling harnesses): FovY ~1.2, 1280x720,
// near 0.1, far 5000. The flood's clip is near-independent, so exactness is not
// load-bearing for cell-count measurement.
private static Matrix4x4 ViewProjFor(Vector3 eye, Vector3 lookAt)
{
var view = Matrix4x4.CreateLookAt(eye, lookAt, Vector3.UnitZ);
var proj = Matrix4x4.CreatePerspectiveFieldOfView(1.2f, 1280f / 720f, 0.1f, 5000f);
return view * proj;
}
[Fact]
[Trait("Purpose", "Diagnostic")]
public void Measure_DungeonFlood_VisibleCellCount()
{
var datDir = CornerFloodReplayTests.ResolveDatDir();
if (datDir is null)
{
_out.WriteLine("SKIP: dat dir did not resolve (ACDREAM_DAT_DIR unset and "
+ "%USERPROFILE%\\Documents\\Asheron's Call absent). No numbers measured.");
// Diagnostic test: do not hard-fail when dats are absent (matches sibling harnesses).
Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md.");
}
_out.WriteLine($"dat dir resolved: {datDir}");
using var dats = new DatCollection(datDir, DatAccessType.Read);
// 1) LandBlockInfo header — NumCells for 0x0007.
var lbi = dats.Get<DatLandBlockInfo>(TownNetwork | 0xFFFEu);
if (lbi is null)
{
_out.WriteLine($"SKIP: LandBlockInfo 0x{TownNetwork | 0xFFFEu:X8} not found in the dat "
+ "(0x0007 may not exist in this client_cell_1.dat).");
return;
}
_out.WriteLine($"=== 0x0007 (Town Network) LandBlockInfo ===");
_out.WriteLine($"NumCells (DatLandBlockInfo.NumCells) = {lbi.NumCells}");
// 2) Load ALL interior cells (sparse ids tolerated — see LoadAllInteriorCells).
var loaded = Issue120ReciprocalPingPongTests.LoadAllInteriorCells(dats, TownNetwork);
_out.WriteLine($"cells actually loaded = {loaded.Count}");
Assert.True(loaded.Count > 0, "no interior cells loaded for 0x0007 — cannot measure");
Func<uint, LoadedCell?> lookup = id => loaded.TryGetValue(id, out var c) ? c : null;
// 3) Per-cell stab_list (VisibleCells) distribution across ALL loaded cells.
// This is the bounded retail PVS size we expect the flood to roughly match.
var stabSizes = loaded.Values.Select(c => c.VisibleCells.Count).ToList();
int seenOutsideCount = loaded.Values.Count(c => c.SeenOutside);
int interiorCount = loaded.Count - seenOutsideCount;
_out.WriteLine("");
_out.WriteLine("=== stab_list (LoadedCell.VisibleCells) distribution over ALL loaded cells ===");
_out.WriteLine($"cells with SeenOutside==true (entrance/exterior-facing) = {seenOutsideCount}");
_out.WriteLine($"cells with SeenOutside==false (interior dungeon) = {interiorCount}");
if (stabSizes.Count > 0)
_out.WriteLine(FormattableString.Invariant(
$"VisibleCells.Count min={stabSizes.Min()} max={stabSizes.Max()} avg={stabSizes.Average():F1} sum={stabSizes.Sum()}"));
int emptyStab = stabSizes.Count(s => s == 0);
_out.WriteLine($"cells with EMPTY stab_list (no dat PVS) = {emptyStab}");
// 4) Pick representative DUNGEON roots: the first interior (SeenOutside==false) cells in
// ascending id order. If none exist, fall back to 0x00070100 and report that.
var interiorRoots = loaded
.Where(kv => !kv.Value.SeenOutside)
.OrderBy(kv => kv.Key)
.Select(kv => kv.Value)
.Take(5)
.ToList();
if (interiorRoots.Count == 0)
{
_out.WriteLine("");
_out.WriteLine("NOTE: NO cell has SeenOutside==false (all cells see the exterior). "
+ "Falling back to root 0x00070100 for the flood measurement.");
if (loaded.TryGetValue(TownNetwork | 0x0100u, out var fallback))
interiorRoots.Add(fallback);
else
{
_out.WriteLine("WARN: 0x00070100 not loaded either; using the lowest-id loaded cell.");
interiorRoots.Add(loaded.OrderBy(kv => kv.Key).First().Value);
}
}
_out.WriteLine("");
_out.WriteLine("=== PER-ROOT FLOOD MEASUREMENT (PortalVisibilityBuilder.Build) ===");
_out.WriteLine("property read for the visited-cell set: PortalVisibilityFrame.OrderedVisibleCells");
_out.WriteLine("root | seenOut | stab(VisibleCells) | flood(OrderedVisibleCells) | crossLB | dir");
var floodSizes = new List<int>();
foreach (var root in interiorRoots)
{
// Eye at the root cell's world origin, looking toward its first portal (or +X if none),
// so the flood actually fires through an opening. Sweep all 6 axis directions and KEEP
// the maximum visited-set — the blowup is a worst-case-over-orientation quantity.
var eye = root.WorldPosition;
int bestFlood = -1;
string bestDir = "?";
int bestCrossLb = -1;
List<uint>? bestVisited = null;
// Direction candidates: toward each portal's polygon centroid (the natural look-through),
// plus the 6 cardinal axes as a fallback sweep.
var lookTargets = new List<(Vector3 target, string label)>();
for (int pi = 0; pi < root.Portals.Count && pi < root.PortalPolygons.Count; pi++)
{
var poly = root.PortalPolygons[pi];
if (poly is { Length: >= 1 })
{
var cl = Vector3.Zero;
foreach (var v in poly) cl += v;
cl /= poly.Length;
lookTargets.Add((Vector3.Transform(cl, root.WorldTransform),
$"portal{pi}->0x{root.Portals[pi].OtherCellId:X4}"));
}
}
foreach (var (d, lbl) in new (Vector3, string)[]
{
(Vector3.UnitX, "+X"), (-Vector3.UnitX, "-X"),
(Vector3.UnitY, "+Y"), (-Vector3.UnitY, "-Y"),
(Vector3.UnitZ, "+Z"), (-Vector3.UnitZ, "-Z"),
})
lookTargets.Add((eye + d * 5f, lbl));
foreach (var (target, label) in lookTargets)
{
if (Vector3.DistanceSquared(target, eye) < 1e-6f) continue;
var frame = PortalVisibilityBuilder.Build(root, eye, lookup, ViewProjFor(eye, target));
int floodN = frame.OrderedVisibleCells.Count;
if (floodN > bestFlood)
{
bestFlood = floodN;
bestDir = label;
bestVisited = frame.OrderedVisibleCells;
bestCrossLb = frame.OrderedVisibleCells.Count(id => (id & 0xFFFF0000u) != TownNetwork);
}
}
floodSizes.Add(bestFlood);
_out.WriteLine(FormattableString.Invariant(
$"0x{root.CellId:X8} | {(root.SeenOutside ? "Y" : "N"),5} | {root.VisibleCells.Count,18} | {bestFlood,26} | {bestCrossLb,7} | {bestDir}"));
// For the FIRST root, also print the actual visited set + stab set for eyeballing.
if (ReferenceEquals(root, interiorRoots[0]) && bestVisited is not null)
{
_out.WriteLine(" first-root visited (OrderedVisibleCells, low ids): "
+ string.Join(" ", bestVisited.Select(id => $"{id & 0xFFFFu:X4}")));
_out.WriteLine(" first-root stab_list (VisibleCells, low ids): "
+ string.Join(" ", root.VisibleCells.Select(id => $"{id & 0xFFFFu:X4}")));
}
}
// 5) Aggregate flood-size stats across the sampled roots — the headline numbers.
_out.WriteLine("");
_out.WriteLine("=== AGGREGATE over sampled roots ===");
if (floodSizes.Count > 0)
_out.WriteLine(FormattableString.Invariant(
$"flood visited-set size (OrderedVisibleCells): min={floodSizes.Min()} max={floodSizes.Max()} avg={floodSizes.Average():F1} (NumCells={lbi.NumCells}, loaded={loaded.Count})"));
var sampledStab = interiorRoots.Select(r => r.VisibleCells.Count).ToList();
if (sampledStab.Count > 0)
_out.WriteLine(FormattableString.Invariant(
$"sampled roots' stab_list size (VisibleCells): min={sampledStab.Min()} max={sampledStab.Max()} avg={sampledStab.Average():F1}"));
_out.WriteLine("");
_out.WriteLine("INTERPRETATION: if flood max ~= loaded.Count (visits ~all cells) while stab "
+ "is small, that is the #95 blowup — the flood is unbounded by the retail stab_list PVS.");
}
}

View file

@ -1,52 +1,21 @@
using System;
using System.Numerics;
using AcDream.App.Rendering;
using Xunit;
namespace AcDream.App.Tests.Rendering;
/// <summary>
/// The OUTDOOR render root (R-A2, 2026-06-08): a portal-less <see cref="LoadedCell"/> whose only job is
/// to root the render outdoors so the flood seeds OutsideView FULL-SCREEN (terrain draws). Buildings are
/// flooded SEPARATELY per-building (<see cref="PortalVisibilityBuilder.ConstructViewBuilding"/>), so the
/// node carries NO reverse portals — the pre-R-A2 reverse-portal unified flood (which oscillated at the
/// doorway) is gone. Its tests moved here; the old <c>UnifiedFloodTests</c> was retired.
/// </summary>
public class OutdoorCellNodeTests
{
[Fact]
public void Build_ReturnsPortallessOutdoorRoot()
{
uint outdoorId = 0xA9B40031;
var node = OutdoorCellNode.Build(outdoorId);
LoadedCell node = OutdoorCellNode.Build(outdoorId);
Assert.Equal(outdoorId, node.CellId);
Assert.True(node.IsOutdoorNode); // the flag PortalVisibilityBuilder keys the full-screen OutsideView on
Assert.True(node.IsOutdoorNode);
Assert.True(node.SeenOutside);
Assert.Equal(Matrix4x4.Identity, node.WorldTransform);
Assert.Empty(node.Portals); // R-A2: no reverse portals into buildings (buildings flood per-building)
}
[Fact]
public void Build_OutdoorRoot_SeedsFullScreenOutsideView_OnlyNodeVisible()
{
// The load-bearing outdoor-terrain behavior: rooting at the IsOutdoorNode node seeds OutsideView
// with the FULL-SCREEN NDC quad, so ClipFrameAssembler yields TerrainMode != Skip and
// DrawLandscapeThroughOutsideView draws terrain/sky/scenery everywhere. The portal-less node
// resolves to exactly {node} — no interior cells flooded from the root (those come per-building).
var node = OutdoorCellNode.Build(0xA9B40031);
var eye = new Vector3(0, -3, 1);
var view = Matrix4x4.CreateLookAt(eye, new Vector3(0, 5, 1), Vector3.UnitZ);
var proj = Matrix4x4.CreatePerspectiveFieldOfView(MathF.PI / 3f, 16f / 9f, 1f, 5000f);
var frame = PortalVisibilityBuilder.Build(node, eye, _ => null, view * proj);
Assert.Equal(new[] { 0xA9B40031u }, frame.OrderedVisibleCells); // only the node
Assert.False(frame.OutsideView.IsEmpty);
Assert.Equal(-1f, frame.OutsideView.MinX, 3);
Assert.Equal(-1f, frame.OutsideView.MinY, 3);
Assert.Equal(1f, frame.OutsideView.MaxX, 3);
Assert.Equal(1f, frame.OutsideView.MaxY, 3);
Assert.Empty(node.Portals);
}
}

View file

@ -1,999 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using AcDream.App.Rendering;
using Xunit;
namespace AcDream.App.Tests.Rendering;
public class PortalVisibilityBuilderTests
{
private static Matrix4x4 ViewProj()
{
var view = Matrix4x4.CreateLookAt(Vector3.Zero, new Vector3(0, 0, -1), Vector3.UnitY);
var proj = Matrix4x4.CreatePerspectiveFieldOfView(1.2f, 1.0f, 0.1f, 1000f);
return view * proj;
}
private static Vector3[] Quad(float cx, float cy, float halfW, float halfH, float z) => new[]
{
new Vector3(cx - halfW, cy - halfH, z), new Vector3(cx + halfW, cy - halfH, z),
new Vector3(cx + halfW, cy + halfH, z), new Vector3(cx - halfW, cy + halfH, z),
};
// Full-height (y in [-0.9, 0.9]) quad spanning [minX, maxX] in X at plane z.
// Used by the multi-back-portal fixture where the X span is the only thing
// distinguishing the LEFT and RIGHT apertures.
private static Vector3[] QuadX(float minX, float maxX, float z) => new[]
{
new Vector3(minX, -0.9f, z), new Vector3(maxX, -0.9f, z),
new Vector3(maxX, 0.9f, z), new Vector3(minX, 0.9f, z),
};
private static LoadedCell Cell(uint id, params CellPortalInfo[] portals) => new LoadedCell
{
CellId = id, WorldTransform = Matrix4x4.Identity, InverseWorldTransform = Matrix4x4.Identity,
Portals = new List<CellPortalInfo>(portals),
};
private static PortalVisibilityFrame Build(LoadedCell cam, Dictionary<uint, LoadedCell> all)
=> PortalVisibilityBuilder.Build(cam, Vector3.Zero, id => all.TryGetValue(id, out var c) ? c : null, ViewProj());
[Fact]
public void Build_ReusedFrame_NestedGraph_MatchesFreshAndStabilizesScratch()
{
var root = Cell(0x0001,
new CellPortalInfo(0x0002, 0, 0, 0),
new CellPortalInfo(0x0003, 1, 0, 0));
root.PortalPolygons.Add(QuadX(-0.8f, -0.05f, -2f));
root.PortalPolygons.Add(QuadX(0.05f, 0.8f, -3f));
var left = Cell(0x0002, new CellPortalInfo(0x0004, 0, 0, 0));
left.PortalPolygons.Add(QuadX(-0.8f, -0.05f, -5f));
var right = Cell(0x0003, new CellPortalInfo(0x0004, 0, 0, 0));
right.PortalPolygons.Add(QuadX(0.05f, 0.8f, -5f));
var exit = Cell(0x0004, new CellPortalInfo(0xFFFF, 0, 0, 0));
exit.PortalPolygons.Add(QuadX(-0.8f, 0.8f, -8f));
var cells = new Dictionary<uint, LoadedCell>
{
[root.CellId] = root,
[left.CellId] = left,
[right.CellId] = right,
[exit.CellId] = exit,
};
LoadedCell? Lookup(uint id) => cells.TryGetValue(id, out LoadedCell? cell) ? cell : null;
PortalVisibilityFrame fresh = PortalVisibilityBuilder.Build(
root, Vector3.Zero, Lookup, ViewProj());
uint[] expectedOrder = fresh.OrderedVisibleCells.ToArray();
uint[] expectedCells = fresh.CellViews.Keys.OrderBy(id => id).ToArray();
int expectedOutsidePolygons = fresh.OutsideView.Polygons.Count;
var reuse = new PortalVisibilityFrame();
PortalVisibilityBuilder.Build(root, Vector3.Zero, Lookup, ViewProj(), reuseFrame: reuse);
int scratchHighWater = reuse.ClipRegionScratchCount;
int vertexArrayHighWater = reuse.PolygonVertexAllocationCount;
Assert.True(scratchHighWater > 0);
Assert.True(vertexArrayHighWater > 0);
for (int frame = 0; frame < 16; frame++)
{
PortalVisibilityFrame actual = PortalVisibilityBuilder.Build(
root, Vector3.Zero, Lookup, ViewProj(), reuseFrame: reuse);
Assert.Same(reuse, actual);
Assert.Equal(expectedOrder, actual.OrderedVisibleCells);
Assert.Equal(expectedCells, actual.CellViews.Keys.OrderBy(id => id).ToArray());
Assert.Equal(expectedOutsidePolygons, actual.OutsideView.Polygons.Count);
Assert.Equal(scratchHighWater, reuse.ClipRegionScratchCount);
Assert.Equal(vertexArrayHighWater, reuse.PolygonVertexAllocationCount);
Assert.Empty(reuse.TodoScratch);
}
}
[Fact]
public void Build_ReusedFrame_AfterLookupException_HasNoStaleWorkOrViews()
{
var root = Cell(0x0001, new CellPortalInfo(0x0002, 0, 0, 0));
root.PortalPolygons.Add(Quad(0f, 0f, 0.5f, 0.5f, -2f));
var reuse = new PortalVisibilityFrame();
Assert.Throws<InvalidOperationException>(() => PortalVisibilityBuilder.Build(
root,
Vector3.Zero,
_ => throw new InvalidOperationException("fixture lookup failure"),
ViewProj(),
reuseFrame: reuse));
var isolatedRoot = Cell(0x0010);
PortalVisibilityFrame recovered = PortalVisibilityBuilder.Build(
isolatedRoot, Vector3.Zero, _ => null, ViewProj(), reuseFrame: reuse);
Assert.Same(reuse, recovered);
Assert.Equal(new[] { isolatedRoot.CellId }, recovered.OrderedVisibleCells);
Assert.Equal(new[] { isolatedRoot.CellId }, recovered.CellViews.Keys);
Assert.Empty(recovered.CrossBuildingViews);
Assert.True(recovered.OutsideView.IsEmpty);
Assert.Empty(reuse.TodoScratch);
}
[Fact]
public void ResetForBuild_DropsPathologicalContainerHighWater_AfterIdleHysteresis()
{
const int pathologicalCount =
PortalVisibilityFrame.MaxRetainedBuildCollectionCapacity * 4;
var frame = new PortalVisibilityFrame();
var placeholder = new LoadedCell();
for (int i = 0; i < pathologicalCount; i++)
{
uint id = (uint)i + 1u;
frame.CellViews.Add(id, new CellView());
frame.CrossBuildingViews.Add(id, new CellView());
frame.OrderedVisibleCells.Add(id);
frame.QueuedScratch.Add(id);
frame.DrawListedScratch.Add(id);
frame.ProcessedViewCountsScratch.Add(id, i);
frame.TodoScratch.Add((placeholder, i));
}
Assert.True(
frame.CellViewMapCapacity
> PortalVisibilityFrame.MaxRetainedBuildCollectionCapacity);
Assert.True(
frame.QueuedCapacity
> PortalVisibilityFrame.MaxRetainedBuildCollectionCapacity);
frame.ResetForBuild();
Assert.Empty(frame.CellViews);
Assert.Empty(frame.CrossBuildingViews);
Assert.Empty(frame.OrderedVisibleCells);
Assert.Empty(frame.QueuedScratch);
Assert.Empty(frame.DrawListedScratch);
Assert.Empty(frame.ProcessedViewCountsScratch);
Assert.Empty(frame.TodoScratch);
Assert.InRange(
frame.RetainedCellViewCount,
0,
PortalVisibilityFrame.MaxRetainedBuildCollectionCapacity);
Assert.True(
frame.CellViewMapCapacity
> PortalVisibilityFrame.MaxRetainedBuildCollectionCapacity);
for (int i = 0; i < PortalVisibilityFrame.CapacityTrimIdleFrames; i++)
frame.ResetForBuild();
Assert.InRange(
frame.CellViewMapCapacity,
0,
PortalVisibilityFrame.MaxRetainedBuildCollectionCapacity);
Assert.InRange(
frame.CrossBuildingViewMapCapacity,
0,
PortalVisibilityFrame.MaxRetainedBuildCollectionCapacity);
Assert.InRange(
frame.QueuedCapacity,
0,
PortalVisibilityFrame.MaxRetainedBuildCollectionCapacity);
Assert.InRange(
frame.DrawListedCapacity,
0,
PortalVisibilityFrame.MaxRetainedBuildCollectionCapacity);
Assert.InRange(
frame.ProcessedViewCountCapacity,
0,
PortalVisibilityFrame.MaxRetainedBuildCollectionCapacity);
Assert.InRange(
frame.OrderedVisibleCells.Capacity,
0,
PortalVisibilityFrame.MaxRetainedBuildCollectionCapacity);
Assert.InRange(
frame.TodoScratch.Capacity,
0,
PortalVisibilityFrame.MaxRetainedBuildCollectionCapacity);
LoadedCell root = Cell(0x1000);
PortalVisibilityFrame rebuilt = PortalVisibilityBuilder.Build(
root,
Vector3.Zero,
_ => null,
ViewProj(),
reuseFrame: frame);
Assert.Same(frame, rebuilt);
Assert.Equal(new[] { root.CellId }, rebuilt.OrderedVisibleCells);
Assert.True(rebuilt.CellViews.ContainsKey(root.CellId));
}
[Fact]
public void ResetForBuild_RecurringLargeFlood_PreservesWarmCapacity()
{
const int recurringCount =
PortalVisibilityFrame.MaxRetainedBuildCollectionCapacity + 163;
var frame = new PortalVisibilityFrame();
for (int iteration = 0;
iteration < PortalVisibilityFrame.CapacityTrimIdleFrames + 5;
iteration++)
{
for (int i = 0; i < recurringCount; i++)
{
uint id = (uint)i + 1u;
frame.QueuedScratch.Add(id);
frame.OrderedVisibleCells.Add(id);
}
int queuedCapacity = frame.QueuedCapacity;
int orderedCapacity = frame.OrderedVisibleCells.Capacity;
frame.ResetForBuild();
Assert.Equal(queuedCapacity, frame.QueuedCapacity);
Assert.Equal(orderedCapacity, frame.OrderedVisibleCells.Capacity);
}
}
[Fact]
public void Builder_Cellar_WindowClippedToStairwell_NotFullWindow()
{
var cam = Cell(0x0001, new CellPortalInfo(0x0002, 0, 0, 0));
cam.PortalPolygons.Add(Quad(0f, 0f, 0.1f, 1.0f, -3f)); // narrow stairwell
var ground = Cell(0x0002, new CellPortalInfo(0xFFFF, 0, 0, 0));
ground.PortalPolygons.Add(Quad(0f, 0f, 1.0f, 1.0f, -6f)); // wide window
var all = new Dictionary<uint, LoadedCell> { [0x0001] = cam, [0x0002] = ground };
var frame = Build(cam, all);
Assert.False(frame.OutsideView.IsEmpty);
float outsideWidth = frame.OutsideView.MaxX - frame.OutsideView.MinX;
float windowOnlyWidth = PortalFrameTestHelper.ProjectedWidth(
new[] { new Vector3(-1, 0, -6), new Vector3(1, 0, -6) }, ViewProj());
Assert.True(outsideWidth < windowOnlyWidth * 0.5f,
$"OutsideView width {outsideWidth} should be a sliver, far less than full window {windowOnlyWidth}");
}
[Fact]
public void Build_IsDeterministic_IdenticalInputsGiveIdenticalVisibleSet()
{
// Flap root-cause apparatus (2026-06-07): the live threshold flap shows OrderedVisibleCells
// flipping 2<->6 at an eye+player identical to cm. Build is a pure static function with
// all-fresh per-call state, so identical inputs MUST yield an identical visible set. If this
// FAILS, the flap is a determinism bug INSIDE Build; if it PASSES (expected), the live flip is
// sub-cm INPUT jitter and the fix must make membership robust, not Build deterministic.
// Exercises the re-enqueue fixpoint via a diamond: 0x0004 is reached from BOTH 0x0002 and 0x0003.
var cam = Cell(0x0001,
new CellPortalInfo(0x0002, 0, 0, 0),
new CellPortalInfo(0x0003, 1, 0, 0));
cam.PortalPolygons.Add(QuadX(-0.6f, -0.05f, -3f)); // left -> 0002
cam.PortalPolygons.Add(QuadX(0.05f, 0.6f, -3f)); // right -> 0003
var left = Cell(0x0002, new CellPortalInfo(0x0004, 0, 0, 0));
left.PortalPolygons.Add(QuadX(-0.6f, -0.05f, -6f));
var right = Cell(0x0003, new CellPortalInfo(0x0004, 0, 0, 0));
right.PortalPolygons.Add(QuadX(0.05f, 0.6f, -6f));
var back = Cell(0x0004, new CellPortalInfo(0xFFFF, 0, 0, 0));
back.PortalPolygons.Add(QuadX(-0.6f, 0.6f, -9f));
var all = new Dictionary<uint, LoadedCell>
{ [0x0001] = cam, [0x0002] = left, [0x0003] = right, [0x0004] = back };
var a = Build(cam, all);
var b = Build(cam, all);
Assert.Equal(a.OrderedVisibleCells, b.OrderedVisibleCells);
Assert.Equal(
a.CellViews.Keys.OrderBy(k => k).ToArray(),
b.CellViews.Keys.OrderBy(k => k).ToArray());
}
[Fact]
public void Build_EyeStandingInInteriorPortal_FloodsNeighbour()
{
// R1 void fix (2026-06-05): when the chase camera roots in a thin doorway cell and the eye is
// STANDING IN an interior portal opening, the live capture showed the vestibule->room portal at
// D=0.16 m projecting to 0 verts (proj=0), so the neighbour was wrongly culled (cells=1) and
// only the thin cell drew -> bluish void. Retail's 3D portal clip imposes no constraint for a
// portal the eye is inside, so the neighbour is fully visible. The flood MUST reach the neighbour.
var cam = Cell(0x0001, new CellPortalInfo(0x0002, 0, 0, 0));
cam.PortalPolygons.Add(Quad(0f, 0f, 0.3f, 0.3f, -0.03f)); // opening 3 cm in front — eye standing in it
var room = Cell(0x0002, new CellPortalInfo(0xFFFF, 0, 0, 0));
room.PortalPolygons.Add(Quad(0f, 0f, 1.0f, 1.0f, -6f));
var all = new Dictionary<uint, LoadedCell> { [0x0001] = cam, [0x0002] = room };
var frame = Build(cam, all);
Assert.True(frame.CellViews.ContainsKey(0x0002),
"eye standing in the doorway must flood the neighbour (degenerate projection was culling it -> void)");
Assert.Contains(0x0002u, frame.OrderedVisibleCells);
}
[Fact]
public void Build_DegeneratePortalToTheSide_NotFlooded_NoOverInclusion()
{
// Guard against the fix over-flooding: a portal whose opening the eye is NOT standing in (3 cm
// in front but 2 m to the SIDE) also projects degenerate, but the eye is OUTSIDE the opening, so
// it must stay culled — otherwise the eye-in-doorway fix would blow up the visible set (#95) by
// flooding every degenerate-projecting portal regardless of where the eye actually is.
var cam = Cell(0x0001, new CellPortalInfo(0x0002, 0, 0, 0));
cam.PortalPolygons.Add(Quad(2.0f, 0f, 0.3f, 0.3f, -0.03f)); // 2 m to the side — eye NOT in it
var room = Cell(0x0002, new CellPortalInfo(0xFFFF, 0, 0, 0));
room.PortalPolygons.Add(Quad(0f, 0f, 1.0f, 1.0f, -6f));
var all = new Dictionary<uint, LoadedCell> { [0x0001] = cam, [0x0002] = room };
var frame = Build(cam, all);
Assert.False(frame.CellViews.ContainsKey(0x0002),
"a degenerate portal the eye is NOT standing in must stay culled (no over-inclusion / #95 blowup)");
}
[Fact]
public void Build_PortalFullyBehindEye_NotFlooded_RetailEmptyClipRule()
{
// W=0 port (2026-06-11): a portal ENTIRELY behind the eye clips to empty in retail
// polyClipFinish (every vertex w < 0 -> <3 survivors -> reject), so the flood does not
// reach its neighbour — the cell is off-screen and drawing nothing through it is correct.
//
// HISTORY: this test used to assert the OPPOSITE (rescue-era pin from a 2026-06-06 cellar
// capture, "0174->0175 must flood at 1.4 m behind the camera"). That pinned the
// EyeInsidePortalOpening rescue — the documented compensation for ProjectToClip's old
// EyePlaneW=1e-4 divergence — not retail. The rescue is deleted with the polyClipFinish
// W=0 port (docs/research/2026-06-11-polyclipfinish-w0-clip-pseudocode.md); the live
// cellar behaviors are re-verified by the dat-backed replay harnesses + the visual gate.
var cam = Cell(0x0001, new CellPortalInfo(0x0002, 0, 0, 0));
cam.PortalPolygons.Add(Quad(0f, 0f, 0.35f, 0.35f, 1.4f)); // entirely behind the eye
var stairs = Cell(0x0002);
var all = new Dictionary<uint, LoadedCell> { [0x0001] = cam, [0x0002] = stairs };
var vp = ViewProj();
Assert.True(PortalProjection.ProjectToClip(cam.PortalPolygons[0], Matrix4x4.Identity, vp).Length < 3,
"a fully-behind portal must clip to empty (polyClipFinish part 1)");
var frame = PortalVisibilityBuilder.Build(
cam, Vector3.Zero, id => all.TryGetValue(id, out var c) ? c : null, vp);
Assert.DoesNotContain(0x0002u, frame.OrderedVisibleCells);
}
[Fact]
public void Build_ViewGrowthAfterDoneCell_PropagatesNewSlicesToExit()
{
// Retail PView tracks update_count vs view_count. If B is processed through a LEFT slice, then
// a later path reaches B through a RIGHT slice, B must propagate that new RIGHT slice to its
// exit portal. Enqueue-once builders flap here: OutsideView stays empty until the camera moves
// enough to discover B in the other order.
const uint A = 0x0001, B = 0x0002, D = 0x0003;
var a = Cell(A,
new CellPortalInfo((ushort)B, PolygonId: 0, Flags: 0, OtherPortalId: 0),
new CellPortalInfo((ushort)D, PolygonId: 1, Flags: 0, OtherPortalId: 0));
a.PortalPolygons.Add(QuadX(-0.9f, -0.3f, -2f)); // nearer LEFT path to B
a.PortalPolygons.Add(QuadX(0.3f, 0.9f, -5f)); // farther RIGHT path to D
var b = Cell(B,
new CellPortalInfo((ushort)A, PolygonId: 0, Flags: 0, OtherPortalId: 0),
new CellPortalInfo(0xFFFF, PolygonId: 1, Flags: 0, OtherPortalId: 0),
new CellPortalInfo((ushort)D, PolygonId: 2, Flags: 0, OtherPortalId: 0));
b.PortalPolygons.Add(QuadX(-0.9f, -0.3f, -2f)); // reciprocal LEFT back to A
b.PortalPolygons.Add(QuadX(0.3f, 0.9f, -3f)); // RIGHT exit, invisible from LEFT slice
b.PortalPolygons.Add(QuadX(0.3f, 0.9f, -5f)); // reciprocal RIGHT back to D
var d = Cell(D, new CellPortalInfo((ushort)B, PolygonId: 0, Flags: 0, OtherPortalId: 2));
d.PortalPolygons.Add(QuadX(0.3f, 0.9f, -5f)); // later RIGHT path into B
var all = new Dictionary<uint, LoadedCell> { [A] = a, [B] = b, [D] = d };
var frame = Build(a, all);
Assert.Contains(B, frame.OrderedVisibleCells);
Assert.Contains(D, frame.OrderedVisibleCells);
Assert.False(frame.OutsideView.IsEmpty);
}
[Fact]
public void Build_ViewGrowthAfterDoneCell_RePopsGrownCell()
{
// Same A->B(near LEFT) + A->D(far RIGHT) + D->B(later) topology as
// Build_ViewGrowthAfterDoneCell_PropagatesNewSlicesToExit: B is popped via LEFT, then D grows B
// through RIGHT after B is done -> B re-pops. This is retail-faithful late growth (kept by the fix).
const uint A = 0x0001, B = 0x0002, D = 0x0003;
var a = Cell(A, new CellPortalInfo((ushort)B, 0, 0, 0), new CellPortalInfo((ushort)D, 1, 0, 0));
a.PortalPolygons.Add(QuadX(-0.9f, -0.3f, -2f));
a.PortalPolygons.Add(QuadX(0.3f, 0.9f, -5f));
var b = Cell(B, new CellPortalInfo((ushort)A, 0, 0, 0),
new CellPortalInfo(0xFFFF, 1, 0, 0), new CellPortalInfo((ushort)D, 2, 0, 0));
b.PortalPolygons.Add(QuadX(-0.9f, -0.3f, -2f));
b.PortalPolygons.Add(QuadX(0.3f, 0.9f, -3f));
b.PortalPolygons.Add(QuadX(0.3f, 0.9f, -5f));
var d = Cell(D, new CellPortalInfo((ushort)B, 0, 0, 2));
d.PortalPolygons.Add(QuadX(0.3f, 0.9f, -5f));
var all = new Dictionary<uint, LoadedCell> { [A] = a, [B] = b, [D] = d };
var frame = Build(a, all);
// Membership (the flap-relevant output) is each cell once, regardless of re-pops.
Assert.Equal(new[] { B }, frame.OrderedVisibleCells.Where(c => c == B).ToArray());
Assert.Contains(D, frame.OrderedVisibleCells);
}
[Fact]
public void Builder_SealedCellar_NoExitPortal_OutsideViewEmpty()
{
var cam = Cell(0x0001, new CellPortalInfo(0x0002, 0, 0, 0));
cam.PortalPolygons.Add(Quad(0, 0, 0.1f, 1f, -3f));
var inner = Cell(0x0002); // no portals at all
var all = new Dictionary<uint, LoadedCell> { [0x0001] = cam, [0x0002] = inner };
Assert.True(Build(cam, all).OutsideView.IsEmpty);
}
[Fact]
public void Builder_CameraCellWithDirectExit_OutsideViewIsFullWindow()
{
var cam = Cell(0x0001, new CellPortalInfo(0xFFFF, 0, 0, 0));
cam.PortalPolygons.Add(Quad(0, 0, 1f, 1f, -6f));
var all = new Dictionary<uint, LoadedCell> { [0x0001] = cam };
var frame = Build(cam, all);
Assert.False(frame.OutsideView.IsEmpty);
Assert.True(frame.OutsideView.MaxX - frame.OutsideView.MinX > 0.3f);
}
[Fact]
public void Builder_BackFacingPortal_NotTraversed()
{
// Portal to 0x0002, but its clip plane puts the camera (origin) on the OUTSIDE.
var cam = Cell(0x0001, new CellPortalInfo(0x0002, 0, 0, 0));
cam.PortalPolygons.Add(Quad(0, 0, 0.5f, 0.5f, -3f));
cam.ClipPlanes.Add(new PortalClipPlane { Normal = new Vector3(0, 0, 1), D = -1f, InsideSide = 0 });
// dot = (0,0,1)·origin + (-1) = -1 < 0; InsideSide==0 requires dot >= -eps → camera OUTSIDE → skip.
var ground = Cell(0x0002, new CellPortalInfo(0xFFFF, 0, 0, 0));
ground.PortalPolygons.Add(Quad(0, 0, 1f, 1f, -6f));
var all = new Dictionary<uint, LoadedCell> { [0x0001] = cam, [0x0002] = ground };
var frame = Build(cam, all);
Assert.False(frame.CellViews.ContainsKey(0x0002)); // neighbour never reached
Assert.True(frame.OutsideView.IsEmpty); // its window never marked
}
[Fact]
public void Build_BackFacingPortal_EyeStandingInOpening_StillCulled()
{
// R-A2b (Option B1, the flap fix): retail PView::InitCell (:432962) culls a back-facing portal by
// the side test REGARDLESS of eye proximity — there is NO eye-in-opening bypass. The live pin
// (flap-sidechk.log, 2026-06-09) showed the back portal 0173->0171 with camInterior=False but
// eyeIn=True (eye within 1.75 m of the shared doorway); the OLD `&& !eyeInsideOpening` side-cull
// bypass let it through -> the 0171<->0173 flood cycle -> re-enqueue churn -> the doorway flap.
// A back-facing portal the eye is STANDING IN must stay culled (the forward-portal clip-empty void
// rescue, tested by Build_EyeStandingInInteriorPortal_FloodsNeighbour, is a separate path and stays).
var cam = Cell(0x0001, new CellPortalInfo(0x0002, 0, 0, 0));
cam.PortalPolygons.Add(Quad(0f, 0f, 0.5f, 0.5f, -1f)); // 1 m in front -> eyeInsideOpening = True
// ClipPlane puts the eye on the EXIT side (camInterior = False), like the back portal of a doorway
// just crossed: Normal·origin + D = 1 > 0, InsideSide==1 wants dot<=eps -> not interior.
cam.ClipPlanes.Add(new PortalClipPlane { Normal = new Vector3(0, 0, 1), D = 1f, InsideSide = 1 });
var ground = Cell(0x0002, new CellPortalInfo(0xFFFF, 0, 0, 0));
ground.PortalPolygons.Add(Quad(0f, 0f, 1f, 1f, -6f));
var all = new Dictionary<uint, LoadedCell> { [0x0001] = cam, [0x0002] = ground };
var frame = Build(cam, all);
Assert.False(frame.CellViews.ContainsKey(0x0002),
"a back-facing portal (camInterior=False) must stay culled even when the eye is standing in its " +
"opening (eyeInsideOpening=True) — retail's side test has no bypass; the bypass WAS the flap cycle");
Assert.DoesNotContain(0x0002u, frame.OrderedVisibleCells);
}
[Fact]
public void Builder_CwWoundExitPortal_OutsideRegionIsCcw()
{
// Exit portal authored CLOCKWISE — the builder must normalize to CCW so downstream stays valid.
var cwQuad = new[]
{
new Vector3(-1, -1, -6), new Vector3(-1, 1, -6), new Vector3(1, 1, -6), new Vector3(1, -1, -6),
};
var cam = Cell(0x0001, new CellPortalInfo(0xFFFF, 0, 0, 0));
cam.PortalPolygons.Add(cwQuad);
var all = new Dictionary<uint, LoadedCell> { [0x0001] = cam };
var frame = Build(cam, all);
Assert.False(frame.OutsideView.IsEmpty);
var p = frame.OutsideView.Polygons[0].Vertices;
float area2 = 0f;
for (int i = 0; i < p.Length; i++) { var a = p[i]; var b = p[(i + 1) % p.Length]; area2 += a.X * b.Y - b.X * a.Y; }
Assert.True(area2 > 0f, "clipped OutsideView region should be CCW after winding normalization");
}
[Fact]
public void Builder_CyclicGraph_TerminatesWithBoundedPolys()
{
// A <-> B cycle; B also has an exit window. Must terminate and not blow up.
var a = Cell(0x0001, new CellPortalInfo(0x0002, 0, 0, 0));
a.PortalPolygons.Add(Quad(0f, 0f, 0.5f, 0.5f, -3f));
var b = Cell(0x0002, new CellPortalInfo(0x0001, 0, 0, 0), new CellPortalInfo(0xFFFF, 1, 0, 0));
b.PortalPolygons.Add(Quad(0f, 0f, 0.5f, 0.5f, -2f)); // back to A
b.PortalPolygons.Add(Quad(0f, 0f, 1.0f, 1.0f, -6f)); // exit window
var all = new Dictionary<uint, LoadedCell> { [0x0001] = a, [0x0002] = b };
var frame = Build(a, all); // must return (no infinite loop)
Assert.False(frame.OutsideView.IsEmpty);
Assert.True(frame.OutsideView.Polygons.Count < 256,
$"OutsideView poly count {frame.OutsideView.Polygons.Count} — termination/dedup regression guard");
}
// -----------------------------------------------------------------------
// Phase U.2a: ordered visible-cell list (closest-first) + grow-watermark
// fixpoint termination (replaces MaxReprocessPerCell hard cap).
// -----------------------------------------------------------------------
// Straight chain A -> B -> C, camera in A looking down -Z. Each onward portal
// is progressively farther in -Z so the camera-to-portal distance is monotonic,
// forcing the priority queue to dequeue A, then B, then C in that order.
private static (LoadedCell[] cells, Dictionary<uint, LoadedCell> lookup) SyntheticChain()
{
const uint A = 0x0001, B = 0x0002, C = 0x0003;
var a = Cell(A, new CellPortalInfo((ushort)B, 0, 0, 0));
a.PortalPolygons.Add(Quad(0f, 0f, 0.6f, 0.6f, -2f)); // portal A->B at z=-2 (nearer)
var b = Cell(B, new CellPortalInfo((ushort)C, 0, 0, 0));
b.PortalPolygons.Add(Quad(0f, 0f, 0.6f, 0.6f, -5f)); // portal B->C at z=-5 (farther)
var c = Cell(C, new CellPortalInfo(0xFFFF, 0, 0, 0));
c.PortalPolygons.Add(Quad(0f, 0f, 0.6f, 0.6f, -8f)); // exit window
var all = new Dictionary<uint, LoadedCell> { [A] = a, [B] = b, [C] = c };
return (new[] { a, b, c }, all);
}
[Fact] // closest-first ordering
public void Build_OrdersVisibleCells_ClosestFirst()
{
var (cells, lookup) = SyntheticChain();
var f = PortalVisibilityBuilder.Build(
cells[0], Vector3.Zero, id => lookup.TryGetValue(id, out var c) ? c : null, ViewProj());
Assert.Equal(new uint[] { 0x0001, 0x0002, 0x0003 }, f.OrderedVisibleCells.ToArray());
}
// Hub cell with 4 rooms, each room portal-linked BACK to the hub (a cycle on
// every spoke). A naive FIFO with no real fixpoint re-enqueues the hub once per
// returning spoke and the rooms once per hub re-process — the watermark must
// converge instead, bounding the visible set to {hub + 4 rooms} with no dupes.
private static (LoadedCell hub, Dictionary<uint, LoadedCell> lookup) SyntheticCyclicHub()
{
const uint HUB = 0x0010;
uint[] rooms = { 0x0011, 0x0012, 0x0013, 0x0014 };
// Hub has one portal to each room; rooms sit at distinct depths so ordering is deterministic.
var hub = Cell(HUB,
new CellPortalInfo((ushort)rooms[0], 0, 0, 0), new CellPortalInfo((ushort)rooms[1], 1, 0, 0),
new CellPortalInfo((ushort)rooms[2], 2, 0, 0), new CellPortalInfo((ushort)rooms[3], 3, 0, 0));
for (int i = 0; i < 4; i++)
hub.PortalPolygons.Add(Quad(0f, 0f, 0.6f, 0.6f, -2f - i)); // -2,-3,-4,-5
var all = new Dictionary<uint, LoadedCell> { [HUB] = hub };
for (int i = 0; i < 4; i++)
{
var room = Cell(rooms[i], new CellPortalInfo((ushort)HUB, 0, 0, 0)); // links back to hub → cycle
room.PortalPolygons.Add(Quad(0f, 0f, 0.6f, 0.6f, -2f - i));
all[rooms[i]] = room;
}
return (hub, all);
}
[Fact] // cyclic graph terminates and bounds the visible set
public void Build_CyclicHub_TerminatesAndBounds()
{
var (hub, lookup) = SyntheticCyclicHub();
var f = PortalVisibilityBuilder.Build(
hub, Vector3.Zero, id => lookup.TryGetValue(id, out var c) ? c : null, ViewProj());
Assert.True(f.OrderedVisibleCells.Count <= 5,
$"hub + 4 rooms expected, got {f.OrderedVisibleCells.Count} — fixpoint failed to converge");
Assert.Equal(f.OrderedVisibleCells.Count, f.OrderedVisibleCells.Distinct().Count()); // no dup cells
}
// -----------------------------------------------------------------------
// Phase U.2b: reciprocal OtherPortalClip (retail PView::OtherPortalClip
// decomp:433524). When a portal leads to a loaded neighbour, the
// propagated region must ALSO be clipped against the neighbour's matching
// (reciprocal) back-portal polygon — the result is the intersection of the
// opening seen from BOTH sides. This can only tighten, never widen.
// -----------------------------------------------------------------------
// Camera in A looking down -Z through a WIDE near-side portal (A->B). B's
// matching back-portal (B->A) is a NARROW opening at the same plane, so the
// reciprocal opening projects to a strictly smaller NDC region. Without the
// reciprocal clip, B's CellView equals the wide near-side projection; with
// it, B's CellView is bounded by the narrow reciprocal opening.
private static (LoadedCell camCell, LoadedCell neighbour, Dictionary<uint, LoadedCell> lookup)
SyntheticReciprocalPair()
{
const uint A = 0x0001, B = 0x0002;
// A's portal into B: wide opening (half-width 0.9) at z = -3. Its
// reciprocal back-portal lives at index 0 in B (OtherPortalId = 0).
var a = Cell(A, new CellPortalInfo((ushort)B, 0, 0, 0));
a.PortalPolygons.Add(Quad(0f, 0f, 0.9f, 0.9f, -3f));
// B's reciprocal portal back to A: NARROW opening (half-width 0.3),
// same height and plane, so it projects fully inside the near-side rect
// but covers only ~1/3 of its width.
var b = Cell(B, new CellPortalInfo((ushort)A, 0, 0, 0));
b.PortalPolygons.Add(Quad(0f, 0f, 0.3f, 0.9f, -3f));
var all = new Dictionary<uint, LoadedCell> { [A] = a, [B] = b };
return (a, b, all);
}
[Fact]
public void Build_AppliesReciprocalOtherPortalClip()
{
var (camCell, neighbour, lookup) = SyntheticReciprocalPair();
var vp = ViewProj();
var f = PortalVisibilityBuilder.Build(
camCell, Vector3.Zero, id => lookup.TryGetValue(id, out var c) ? c : null, vp);
Assert.True(f.CellViews.ContainsKey(0x0002), "neighbour cell view should be populated");
// The reciprocal opening's area in NDC: project B's narrow back-portal
// polygon and take its (CCW-magnitude) shoelace area. This is the
// tightest the neighbour region can be — the propagated region must not
// exceed it once both-sides clipping is applied.
float reciprocalArea = ProjectedPolygonArea(neighbour.PortalPolygons[0], vp);
float area = CellViewArea(f.CellViews[0x0002]);
const float eps = 1e-4f;
Assert.True(area <= reciprocalArea + eps,
$"neighbour CellView area {area} must be clipped to the narrower reciprocal opening " +
$"{reciprocalArea} (without OtherPortalClip it equals the WIDE near-side projection)");
// Falsifiability guard: the near-side projection is genuinely WIDER than
// the reciprocal, so a no-op clip would have failed the assertion above.
float nearSideArea = ProjectedPolygonArea(camCell.PortalPolygons[0], vp);
Assert.True(nearSideArea > reciprocalArea * 1.5f,
$"fixture sanity: near-side area {nearSideArea} must dominate reciprocal {reciprocalArea}");
}
[Fact]
public void Build_ReciprocalClip_DegradesGracefully_WhenNoBackPortal()
{
// Same wide A->B opening, but B has NO portal pointing back to A (data gap).
// The reciprocal clip must no-op, so B's CellView equals the WIDE near-side
// projection — proving degradation never under-includes (and never throws).
const uint A = 0x0001, B = 0x0002;
var a = Cell(A, new CellPortalInfo((ushort)B, 0, 0, 0));
a.PortalPolygons.Add(Quad(0f, 0f, 0.9f, 0.9f, -3f));
var b = Cell(B); // no portals at all → no back-portal to match
var all = new Dictionary<uint, LoadedCell> { [A] = a, [B] = b };
var vp = ViewProj();
var f = PortalVisibilityBuilder.Build(
a, Vector3.Zero, id => all.TryGetValue(id, out var c) ? c : null, vp);
Assert.True(f.CellViews.ContainsKey(B), "neighbour must still be reached when no back-portal exists");
float nearSideArea = ProjectedPolygonArea(a.PortalPolygons[0], vp);
float area = CellViewArea(f.CellViews[B]);
Assert.True(System.MathF.Abs(area - nearSideArea) < 1e-3f,
$"with no reciprocal portal the region must equal the near-side projection " +
$"(got {area}, near-side {nearSideArea}) — degrade must not tighten or expand");
}
// -----------------------------------------------------------------------
// Phase U.2b CRITICAL — multiple portals to the SAME neighbour must each
// resolve their OWN reciprocal back-portal (retail arg2->other_portal_id,
// decomp:433557), NOT the first OtherCellId match. This is the real
// Holtburg-cellar shape: cell 0x148 has two portals to 0x149 (poly 40, 41)
// and 0x149 has two reciprocals back to 0x148. A scan-by-first-match clips
// BOTH near-side openings against the FIRST reciprocal — if the apertures
// are disjoint, the second opening's intersection underflows to empty and
// its geometry is HIDDEN (under-inclusion). Direct-index resolution clips
// each opening against the matching reciprocal, so both survive.
// -----------------------------------------------------------------------
// Camera in A looking down -Z. A has TWO portals to the SAME neighbour B,
// with DISJOINT openings: a LEFT aperture (x in [-0.9,-0.3]) and a RIGHT
// aperture (x in [0.3,0.9]). B has two reciprocals back to A, one matching
// each side. A.Portals[0].OtherPortalId = 0 (B's LEFT reciprocal),
// A.Portals[1].OtherPortalId = 1 (B's RIGHT reciprocal).
//
// Scan-by-first-match resolves BOTH A-portals to B.Portals[0] (LEFT),
// so the RIGHT near-side region (x>0) intersects an x<0 reciprocal → empty
// → B's CellView never reaches the RIGHT aperture. Direct-index keeps it.
private static (LoadedCell camCell, LoadedCell neighbour, Dictionary<uint, LoadedCell> lookup)
SyntheticMultiBackPortalPair()
{
const uint A = 0x0001, B = 0x0002;
// A: portal[0] → B through the LEFT opening, portal[1] → B through the RIGHT opening.
// The OtherPortalId back-links pick the matching reciprocal in B (0 = LEFT, 1 = RIGHT).
var a = Cell(A,
new CellPortalInfo((ushort)B, PolygonId: 0, Flags: 0, OtherPortalId: 0), // LEFT → B recip[0]
new CellPortalInfo((ushort)B, PolygonId: 1, Flags: 0, OtherPortalId: 1)); // RIGHT → B recip[1]
a.PortalPolygons.Add(QuadX(-0.9f, -0.3f, -3f)); // LEFT near-side aperture
a.PortalPolygons.Add(QuadX(0.3f, 0.9f, -3f)); // RIGHT near-side aperture
// B: two reciprocals back to A. Index 0 covers the LEFT opening, index 1
// the RIGHT opening — disjoint, same plane (z=-3) as the near side so each
// reciprocal exactly overlaps its matching A aperture.
var b = Cell(B,
new CellPortalInfo((ushort)A, PolygonId: 0, Flags: 0, OtherPortalId: 0),
new CellPortalInfo((ushort)A, PolygonId: 1, Flags: 0, OtherPortalId: 1));
b.PortalPolygons.Add(QuadX(-0.9f, -0.3f, -3f)); // reciprocal[0] = LEFT
b.PortalPolygons.Add(QuadX(0.3f, 0.9f, -3f)); // reciprocal[1] = RIGHT
var all = new Dictionary<uint, LoadedCell> { [A] = a, [B] = b };
return (a, b, all);
}
[Fact]
public void Build_MultiplePortalsToSameNeighbour_EachResolvesOwnReciprocal()
{
var (camCell, neighbour, lookup) = SyntheticMultiBackPortalPair();
var vp = ViewProj();
var f = PortalVisibilityBuilder.Build(
camCell, Vector3.Zero, id => lookup.TryGetValue(id, out var c) ? c : null, vp);
Assert.True(f.CellViews.ContainsKey(0x0002), "neighbour cell view should be populated");
// The RIGHT reciprocal opening (B.PortalPolygons[1]) projects to a region
// with strictly positive NDC X. Geometry visible through the SECOND opening
// survives ONLY if A.Portals[1] was clipped against B's RIGHT reciprocal
// (index 1) rather than the LEFT one (index 0).
var rightNdc = PortalProjection.ProjectToNdc(neighbour.PortalPolygons[1], Matrix4x4.Identity, vp);
float rightMinX = float.MaxValue, rightMaxX = float.MinValue;
foreach (var v in rightNdc) { if (v.X < rightMinX) rightMinX = v.X; if (v.X > rightMaxX) rightMaxX = v.X; }
Assert.True(rightMinX > 0f, $"fixture sanity: RIGHT reciprocal must project to positive NDC X (got minX {rightMinX})");
// The neighbour CellView must extend into the RIGHT aperture. With the
// scan-by-first-match bug both near-side openings clip against the LEFT
// reciprocal (x<0), so the CellView's MaxX stays well left of rightMinX
// and this assertion FAILS (the RIGHT opening's geometry is hidden).
var bView = f.CellViews[0x0002];
Assert.True(bView.MaxX >= rightMinX - 1e-4f,
$"neighbour CellView MaxX {bView.MaxX} must reach the RIGHT reciprocal opening " +
$"[{rightMinX}, {rightMaxX}] — under scan-by-first-match the second opening is clipped " +
$"against the LEFT reciprocal and HIDDEN (under-inclusion bug #102 M-4).");
// And the LEFT aperture must still be present (the first opening was never
// in question) — guards against a fix that accidentally drops the LEFT side.
var leftNdc = PortalProjection.ProjectToNdc(neighbour.PortalPolygons[0], Matrix4x4.Identity, vp);
float leftMinX = float.MaxValue;
foreach (var v in leftNdc) if (v.X < leftMinX) leftMinX = v.X;
Assert.True(bView.MinX <= leftMinX + 1e-4f,
$"neighbour CellView MinX {bView.MinX} must still cover the LEFT reciprocal opening (minX {leftMinX})");
}
// -----------------------------------------------------------------------
// Phase U.4c: an earlier synthetic flap test (Build_NearBoundaryIntermediatePortal)
// lived here. It modeled the doorway flap as a BUILDER side-test cull dropping the
// exit cell. The live ACDREAM_PROBE_FLAP capture (2026-05-31) DISPROVED that model:
// the real cause is the visibility ROOT being driven by the 3rd-person camera EYE —
// the eye drifts out of the player's cell, FindCameraCell returns a STALE cell for
// its grace frames, and the doorway is then culled as "behind" the eye. The fix is
// at the GameWindow integration level (root visibility at the PLAYER's cell:
// visRootPos), NOT in the builder — the builder's side test is correct and unchanged,
// so the old test asserted a non-bug and was removed rather than left red. The fix is
// validated by the visual gate + the [flap] probe (RenderingDiagnostics.ProbeFlapEnabled);
// see docs/research/2026-05-31-u4c-flap-characterization.md.
// -----------------------------------------------------------------------
// -----------------------------------------------------------------------
// Phase W Stage 4/5: BFS exit-portal + root-cell ordering guarantees
// -----------------------------------------------------------------------
[Fact]
public void Build_ExitPortalVisible_OutsideViewNonEmpty()
{
// A cell with a single exit portal (OtherCellId == 0xFFFF) whose polygon
// projects in front of the camera. The BFS must union the clipped region
// into OutsideView, making it non-empty.
var cam = Cell(0x0001, new CellPortalInfo(0xFFFF, 0, 0, 0));
cam.PortalPolygons.Add(Quad(0f, 0f, 0.5f, 0.5f, -4f)); // in front, in frustum
var all = new Dictionary<uint, LoadedCell> { [0x0001] = cam };
var frame = Build(cam, all);
Assert.True(frame.OutsideView.Polygons.Count > 0,
"An exit portal (OtherCellId=0xFFFF) in front of the camera must populate OutsideView");
}
[Fact]
public void Build_NoExitPortal_OutsideViewEmpty()
{
// A cell whose only portal leads to another interior cell (OtherCellId != 0xFFFF).
// With no exit portal in any reachable cell the OutsideView must remain empty.
var cam = Cell(0x0001, new CellPortalInfo(0x0002, 0, 0, 0));
cam.PortalPolygons.Add(Quad(0f, 0f, 0.5f, 0.5f, -3f));
var inner = Cell(0x0002); // no portals — no exit
var all = new Dictionary<uint, LoadedCell> { [0x0001] = cam, [0x0002] = inner };
var frame = Build(cam, all);
Assert.True(frame.OutsideView.Polygons.Count == 0,
"No exit portal in any reachable cell must leave OutsideView empty");
}
[Fact]
public void BuildFromExterior_SeedsInteriorCellThroughOutsidePortal()
{
var room = Cell(0x0001, new CellPortalInfo(0xFFFF, 0, 0, 0));
room.PortalPolygons.Add(Quad(0f, 0f, 0.5f, 0.5f, -4f));
room.ClipPlanes.Add(new PortalClipPlane
{
Normal = new Vector3(0f, 0f, 1f),
D = 3f,
InsideSide = 1,
});
var frame = PortalVisibilityBuilder.BuildFromExterior(
new[] { room },
Vector3.Zero,
id => id == room.CellId ? room : null,
ViewProj());
Assert.Contains(room.CellId, frame.OrderedVisibleCells);
Assert.True(frame.CellViews.TryGetValue(room.CellId, out var view));
Assert.False(view!.IsEmpty);
Assert.True(view.MaxX - view.MinX < 1.0f,
"exterior seed should be clipped to the door opening, not full-screen");
}
[Fact]
public void BuildFromExterior_ReusedFrame_ClearsPriorOutputAndProducesSameResult()
{
var room = Cell(0x0001, new CellPortalInfo(0xFFFF, 0, 0, 0));
room.PortalPolygons.Add(Quad(0f, 0f, 0.5f, 0.5f, -4f));
room.ClipPlanes.Add(new PortalClipPlane
{
Normal = new Vector3(0f, 0f, 1f),
D = 3f,
InsideSide = 1,
});
var reuse = new PortalVisibilityFrame();
LoadedCell? Lookup(uint id) => id == room.CellId ? room : null;
var first = PortalVisibilityBuilder.BuildFromExterior(
new[] { room }, Vector3.Zero, Lookup, ViewProj(), reuseFrame: reuse);
Assert.Same(reuse, first);
Assert.Equal(new[] { room.CellId }, first.OrderedVisibleCells);
var empty = PortalVisibilityBuilder.BuildFromExterior(
Array.Empty<LoadedCell>(), Vector3.Zero, Lookup, ViewProj(), reuseFrame: reuse);
Assert.Same(reuse, empty);
Assert.Empty(empty.OrderedVisibleCells);
Assert.Empty(empty.CellViews);
Assert.True(empty.OutsideView.IsEmpty);
var second = PortalVisibilityBuilder.BuildFromExterior(
new[] { room }, Vector3.Zero, Lookup, ViewProj(), reuseFrame: reuse);
Assert.Same(reuse, second);
Assert.Equal(new[] { room.CellId }, second.OrderedVisibleCells);
Assert.True(second.CellViews.TryGetValue(room.CellId, out CellView? view));
Assert.False(view!.IsEmpty);
}
[Fact]
public void BuildFromExterior_DoesNotSeedWhenCameraIsOnInteriorSide()
{
var room = Cell(0x0001, new CellPortalInfo(0xFFFF, 0, 0, 0));
room.PortalPolygons.Add(Quad(0f, 0f, 0.5f, 0.5f, -4f));
room.ClipPlanes.Add(new PortalClipPlane
{
Normal = new Vector3(0f, 0f, 1f),
D = -1f,
InsideSide = 1,
});
var frame = PortalVisibilityBuilder.BuildFromExterior(
new[] { room },
Vector3.Zero,
id => id == room.CellId ? room : null,
ViewProj());
Assert.Empty(frame.OrderedVisibleCells);
Assert.Empty(frame.CellViews);
}
[Fact]
public void BuildFromExterior_TraversesDeeperInteriorPortals()
{
var entry = Cell(0x0001,
new CellPortalInfo(0xFFFF, 0, 0, 0),
new CellPortalInfo(0x0002, 1, 0, 0));
entry.PortalPolygons.Add(Quad(0f, 0f, 0.6f, 0.6f, -3f));
entry.PortalPolygons.Add(Quad(0f, 0f, 0.35f, 0.35f, -5f));
entry.ClipPlanes.Add(new PortalClipPlane
{
Normal = new Vector3(0f, 0f, 1f),
D = 2f,
InsideSide = 1,
});
var backRoom = Cell(0x0002);
var all = new Dictionary<uint, LoadedCell>
{
[entry.CellId] = entry,
[backRoom.CellId] = backRoom,
};
var frame = PortalVisibilityBuilder.BuildFromExterior(
new[] { entry },
Vector3.Zero,
id => all.TryGetValue(id, out var c) ? c : null,
ViewProj());
Assert.Equal(new uint[] { 0x0001, 0x0002 }, frame.OrderedVisibleCells.ToArray());
Assert.True(frame.CellViews.ContainsKey(0x0002));
}
[Fact]
public void BuildFromExterior_MaxSeedDistanceSkipsDistantExitPortal()
{
var nearby = Cell(0x0001, new CellPortalInfo(0xFFFF, 0, 0, 0));
nearby.PortalPolygons.Add(Quad(0f, 0f, 0.5f, 0.5f, -4f));
nearby.ClipPlanes.Add(new PortalClipPlane
{
Normal = new Vector3(0f, 0f, 1f),
D = 3f,
InsideSide = 1,
});
var distant = Cell(0x0002, new CellPortalInfo(0xFFFF, 0, 0, 0));
distant.PortalPolygons.Add(Quad(0f, 0f, 0.5f, 0.5f, -200f));
distant.ClipPlanes.Add(new PortalClipPlane
{
Normal = new Vector3(0f, 0f, 1f),
D = 199f,
InsideSide = 1,
});
var frame = PortalVisibilityBuilder.BuildFromExterior(
new[] { nearby, distant },
Vector3.Zero,
id => id == nearby.CellId ? nearby : id == distant.CellId ? distant : null,
ViewProj(),
maxSeedDistance: 48f);
Assert.Contains(nearby.CellId, frame.OrderedVisibleCells);
Assert.DoesNotContain(distant.CellId, frame.OrderedVisibleCells);
}
[Fact]
public void Build_RootCellAlwaysFirstInOrderedVisibleCells()
{
// The camera cell seeds the BFS at distance 0 so it must always pop
// first → OrderedVisibleCells[0] == cameraCell.CellId.
// Use the three-cell chain: A→B→C with exit window on C.
var (cells, lookup) = SyntheticChain();
var f = PortalVisibilityBuilder.Build(
cells[0], Vector3.Zero, id => lookup.TryGetValue(id, out var c) ? c : null, ViewProj());
Assert.NotEmpty(f.OrderedVisibleCells);
Assert.Equal(cells[0].CellId, f.OrderedVisibleCells[0]);
}
// Note: Build_CyclicPortalGraph_Terminates is already covered by
// Builder_CyclicGraph_TerminatesWithBoundedPolys and Build_CyclicHub_TerminatesAndBounds.
// The dedup invariant (each cell appears at most once) is validated by
// Build_CyclicHub_TerminatesAndBounds via OrderedVisibleCells.Distinct().Count().
// Signed-area-magnitude (shoelace) sum over a CellView's polygons in NDC.
private static float CellViewArea(CellView view)
{
float total = 0f;
foreach (var poly in view.Polygons) total += ShoelaceAbs(poly.Vertices);
return total;
}
// Project a cell-local polygon (identity world transform in these fixtures)
// to NDC via the same path the builder uses, then take its area magnitude.
private static float ProjectedPolygonArea(Vector3[] localPoly, Matrix4x4 vp)
{
var ndc = PortalProjection.ProjectToNdc(localPoly, Matrix4x4.Identity, vp);
return ShoelaceAbs(ndc);
}
private static float ShoelaceAbs(Vector2[] poly)
{
if (poly == null || poly.Length < 3) return 0f;
float area2 = 0f;
for (int i = 0; i < poly.Length; i++)
{
var p = poly[i];
var q = poly[(i + 1) % poly.Length];
area2 += p.X * q.Y - q.X * p.Y;
}
return System.MathF.Abs(area2) * 0.5f;
}
}
internal static class PortalFrameTestHelper
{
public static float ProjectedWidth(Vector3[] worldSeg, Matrix4x4 vp)
{
var a = Vector4.Transform(new Vector4(worldSeg[0], 1f), vp);
var b = Vector4.Transform(new Vector4(worldSeg[1], 1f), vp);
return System.MathF.Abs(a.X / a.W - b.X / b.W);
}
}

View file

@ -1,96 +0,0 @@
using System.Collections.Generic;
using System.Numerics;
using AcDream.App.Rendering;
using Xunit;
namespace AcDream.App.Tests.Rendering;
/// <summary>
/// R-A2 conformance: the per-building flood (<see cref="PortalVisibilityBuilder.ConstructViewBuilding"/>)
/// is the robustness mechanism for the doorway flap. These are SOUNDNESS gates against the measured
/// retail values (handoff 2026-06-08 OPTION-A §3.4): a per-building flood touches ≈2 cells and its
/// membership is stable under the eye's ~36 µm rest jitter. They prove the per-building path is built
/// correctly and does not regress; the live doorway is the visual acceptance test for "the flap is
/// gone" (a synthetic fixture cannot reproduce the cottage's exact knife-edge geometry).
/// </summary>
public class PortalVisibilityRobustnessTests
{
private static Matrix4x4 ViewProj()
{
var view = Matrix4x4.CreateLookAt(Vector3.Zero, new Vector3(0, 0, -1), Vector3.UnitY);
var proj = Matrix4x4.CreatePerspectiveFieldOfView(1.2f, 1.0f, 0.1f, 1000f);
return view * proj;
}
private static Vector3[] Quad(float cx, float cy, float halfW, float halfH, float z) => new[]
{
new Vector3(cx - halfW, cy - halfH, z), new Vector3(cx + halfW, cy - halfH, z),
new Vector3(cx + halfW, cy + halfH, z), new Vector3(cx - halfW, cy + halfH, z),
};
private static LoadedCell Cell(uint id, params CellPortalInfo[] portals) => new LoadedCell
{
CellId = id, WorldTransform = Matrix4x4.Identity, InverseWorldTransform = Matrix4x4.Identity,
Portals = new List<CellPortalInfo>(portals),
};
// A realistic 2-cell building viewed from OUTSIDE: a vestibule (0x0170) with an entrance portal to
// the outdoors plus an interior portal to a back room (0x0171). The entrance clip plane puts the
// origin eye on the EXTERIOR side (D=3, InsideSide=1 → dot=3 > ε), so the exterior seed fires —
// exactly the BuildFromExterior seeding contract (mirrors the existing BuildFromExterior_* fixtures).
private static (LoadedCell[] cells, Dictionary<uint, LoadedCell> lookup) TwoCellBuilding()
{
const uint VEST = 0x0170, ROOM = 0x0171;
var vest = Cell(VEST,
new CellPortalInfo(0xFFFF, PolygonId: 0, Flags: 0, OtherPortalId: 0), // entrance to outdoors
new CellPortalInfo((ushort)ROOM, PolygonId: 1, Flags: 0, OtherPortalId: 0)); // interior to room
vest.PortalPolygons.Add(Quad(0f, 0f, 0.5f, 0.5f, -2f)); // entrance opening, in front of eye
vest.PortalPolygons.Add(Quad(0f, 0f, 0.6f, 0.6f, -4f)); // vestibule -> room, deeper
vest.ClipPlanes.Add(new PortalClipPlane { Normal = new Vector3(0, 0, 1), D = 3f, InsideSide = 1 });
var room = Cell(ROOM, new CellPortalInfo((ushort)VEST, PolygonId: 0, Flags: 0, OtherPortalId: 1));
room.PortalPolygons.Add(Quad(0f, 0f, 0.6f, 0.6f, -4f)); // reciprocal back to vestibule
var all = new Dictionary<uint, LoadedCell> { [VEST] = vest, [ROOM] = room };
return (new[] { vest, room }, all);
}
[Fact]
public void ConstructViewBuilding_FloodsTheBuildingFromItsEntrance()
{
var (cells, lookup) = TwoCellBuilding();
var frame = PortalVisibilityBuilder.ConstructViewBuilding(
cells, Vector3.Zero, id => lookup.TryGetValue(id, out var c) ? c : null, ViewProj());
Assert.Contains(0x0170u, frame.OrderedVisibleCells); // entrance cell seeded
Assert.Contains(0x0171u, frame.OrderedVisibleCells); // back room reached through the interior portal
}
[Fact]
public void ConstructViewBuilding_TouchesAboutTwoCells()
{
// Conformance to §3.4: each retail per-building flood has cell_draw_num ≈ 2.
var (cells, lookup) = TwoCellBuilding();
var frame = PortalVisibilityBuilder.ConstructViewBuilding(
cells, Vector3.Zero, id => lookup.TryGetValue(id, out var c) ? c : null, ViewProj());
Assert.InRange(frame.OrderedVisibleCells.Count, 1, 3);
}
[Fact]
public void ConstructViewBuilding_MembershipStableUnderMicrometreEyeJitter()
{
// Conformance to §3.4: retail's per-building membership is stable while the eye jitters ~36 µm
// at rest (measured X≈15 µm, Y≈36 µm, Z≈8 µm). The entrance-bounded seed must return the SAME
// OrderedVisibleCells for the eye and the eye + that per-axis jitter — no flap.
var (cells, lookup) = TwoCellBuilding();
var vp = ViewProj();
System.Func<uint, LoadedCell?> lk = id => lookup.TryGetValue(id, out var c) ? c : null;
var a = PortalVisibilityBuilder.ConstructViewBuilding(cells, Vector3.Zero, lk, vp);
var b = PortalVisibilityBuilder.ConstructViewBuilding(
cells, new Vector3(15e-6f, 36e-6f, 8e-6f), lk, vp);
Assert.Equal(a.OrderedVisibleCells, b.OrderedVisibleCells);
}
}

View file

@ -564,12 +564,8 @@ public sealed class RetailPViewPassExecutorTests
var frames = new GpuDeviceFrameLifetime(device);
var scope = new VulkanWorldPassScope(sampleCount: 1);
using var portalDepthMask = new PortalDepthMaskRenderer(device, frames, scope);
var diagnostics = new WorldRenderDiagnostics(new NeverCalledGlStateReader(), new NeverCalledDiagnosticLog());
// DrawPortalDepthWrite only reads _portalDepthMask, frame.Cells,
// frame.RootCell.IsOutdoorNode, frame.ViewProjection, and
// _diagnostics (short-circuited off by RenderingDiagnostics.
// ProbeSeamDrawEnabled's default-off value) — the same
// frame.RootCell.IsOutdoorNode, and frame.ViewProjection — the same
// constructor-bypass pattern WalkOutsideViewReassemblyTests already
// uses for exercising a real leaf without a full GL/DAT renderer
// graph.
@ -578,10 +574,6 @@ public sealed class RetailPViewPassExecutorTests
typeof(RetailPViewPassExecutor)
.GetField("_portalDepthMask", BindingFlags.NonPublic | BindingFlags.Instance)!
.SetValue(executor, portalDepthMask);
typeof(RetailPViewPassExecutor)
.GetField("_diagnostics", BindingFlags.NonPublic | BindingFlags.Instance)!
.SetValue(executor, diagnostics);
var root = new LoadedCell { CellId = 0xF4180003u, IsOutdoorNode = false };
RetailPViewFrameInput frame = new RetailPViewFrameInput().Reset(
rootCell: root,
@ -625,21 +617,6 @@ public sealed class RetailPViewPassExecutorTests
public LoadedCell? Find(uint cellId) => cellId == cell.CellId ? cell : null;
}
private sealed class NeverCalledGlStateReader : IRenderGlStateReader
{
public RenderGlStateSnapshot CaptureState() =>
throw new InvalidOperationException("EmitSeamMask must short-circuit before reading GL state.");
public RenderGlScissorSnapshot CaptureScissor() =>
throw new InvalidOperationException("EmitSeamMask must short-circuit before reading GL state.");
}
private sealed class NeverCalledDiagnosticLog : IRenderFrameDiagnosticLog
{
public void WriteLine(string message) =>
throw new InvalidOperationException("EmitSeamMask must short-circuit before logging (ProbeSeamDrawEnabled defaults off).");
}
private static int RequiredCallIndex(
IReadOnlyList<CompiledCall> calls,
Type declaringType,

View file

@ -9,7 +9,7 @@ public sealed class TerrainDrawDiagnosticsControllerTests
{
var log = new RecordingLog();
var facts = new RecordingFacts();
var world = new WorldRenderDiagnostics(new NullGlStateReader(), log);
var world = new WorldRenderDiagnostics(log);
var controller = new TerrainDrawDiagnosticsController(false, world, facts, log);
controller.Begin();
@ -25,7 +25,7 @@ public sealed class TerrainDrawDiagnosticsControllerTests
{
var log = new ThrowOnceLog();
var facts = new RecordingFacts();
var world = new WorldRenderDiagnostics(new NullGlStateReader(), log);
var world = new WorldRenderDiagnostics(log);
var controller = new TerrainDrawDiagnosticsController(true, world, facts, log);
controller.Begin();
@ -47,7 +47,7 @@ public sealed class TerrainDrawDiagnosticsControllerTests
{
var log = new ThrowOnFirstFrameLog();
var facts = new RecordingFacts();
var world = new WorldRenderDiagnostics(new NullGlStateReader(), log);
var world = new WorldRenderDiagnostics(log);
var controller = new TerrainDrawDiagnosticsController(true, world, facts, log);
controller.Begin();
@ -70,7 +70,7 @@ public sealed class TerrainDrawDiagnosticsControllerTests
{
var log = new RecordingLog();
var facts = new RecordingFacts();
var world = new WorldRenderDiagnostics(new NullGlStateReader(), log);
var world = new WorldRenderDiagnostics(log);
var controller = new TerrainDrawDiagnosticsController(true, world, facts, log);
controller.Begin();
@ -99,7 +99,7 @@ public sealed class TerrainDrawDiagnosticsControllerTests
{
var log = new RecordingLog();
var facts = new RecordingFacts();
var world = new WorldRenderDiagnostics(new NullGlStateReader(), log);
var world = new WorldRenderDiagnostics(log);
var controller = new TerrainDrawDiagnosticsController(true, world, facts, log);
controller.AccumulateWalkBatch(1_000);
@ -121,7 +121,7 @@ public sealed class TerrainDrawDiagnosticsControllerTests
// not depend on AccumulateWalkBatch ever having been called.
var log = new RecordingLog();
var facts = new RecordingFacts();
var world = new WorldRenderDiagnostics(new NullGlStateReader(), log);
var world = new WorldRenderDiagnostics(log);
var controller = new TerrainDrawDiagnosticsController(true, world, facts, log);
controller.CompleteWalkFrame(10_000);
@ -135,7 +135,7 @@ public sealed class TerrainDrawDiagnosticsControllerTests
{
var log = new RecordingLog();
var facts = new RecordingFacts();
var world = new WorldRenderDiagnostics(new NullGlStateReader(), log);
var world = new WorldRenderDiagnostics(log);
var controller = new TerrainDrawDiagnosticsController(true, world, facts, log);
controller.AccumulateWalkBatch(5_000);
@ -171,12 +171,6 @@ public sealed class TerrainDrawDiagnosticsControllerTests
}
}
private sealed class NullGlStateReader : IRenderGlStateReader
{
public RenderGlStateSnapshot CaptureState() => default;
public RenderGlScissorSnapshot CaptureScissor() => default;
}
private sealed class RecordingLog : IRenderFrameDiagnosticLog
{
public List<string> Messages { get; } = [];

View file

@ -1,429 +0,0 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using AcDream.App.Rendering;
using DatReaderWriter;
using DatReaderWriter.Options;
using Xunit;
using Xunit.Abstractions;
namespace AcDream.App.Tests.Rendering;
/// <summary>
/// #119 residual — the tower-ascent harness (the #118 exit-walk pattern,
/// vertical). User report (post-#120 build): running UP the AAB3 tower
/// (cell 0xAAB30107, building[1] model 0x01001117) the TOP stairs disappear
/// visually but stay walkable, and at the top the roof/edges FLAP in and
/// out. The tower's upper cells are thin landing/roof slabs with EXIT
/// portals (0x0108 z≈121123, 0x0109 z≈112114.5, 0x010A z≈126.8127.2 —
/// the roof lip), so the ascent walks the eye across exit-portal planes —
/// the same decision-stack territory as #118/#120.
///
/// Per step of a helix matching the spiral staircase, this drives the
/// production stack headlessly: viewer-cell resolution (smallest containing
/// AABB; outdoor when none) → PortalVisibilityBuilder.Build (+ the
/// per-building exterior floods on outdoor steps, mirroring
/// RetailPViewRenderer.MergeNearbyBuildingFloods) → ClipFrameAssembler →
/// ViewconeCuller → the DrawCellObjectLists predicate for the staircase
/// static (Setup 0x020003F2, ParentCellId 0x0107). The failing steps pin
/// the vanish; root/visibility instability across adjacent steps pins the
/// flap.
/// </summary>
[Trait("Lane", "InstalledDat")]
public class TowerAscentReplayTests
{
private readonly ITestOutputHelper _out;
public TowerAscentReplayTests(ITestOutputHelper output) => _out = output;
private const uint Landblock = 0xAAB30000u;
private const uint MainCell = Landblock | 0x0107u;
// Tower geometry (Issue119TowerDumpTests dat facts): origin (108,60,112);
// the staircase Setup spans local x,y ∈ [-4,4], z ∈ [0,15.5]. Production
// EntitySphere = AABB center + half-diagonal.
private static readonly Vector3 TowerCenter = new(108f, 60f, 112f);
private static readonly Vector3 StairSphereCenter = new(108f, 60f, 119.75f);
private const float StairSphereRadius = 9.45f;
private sealed record AscentStep(
int Index,
Vector3 Eye,
uint RootCellId, // 0 = outdoor
bool FloodHasMainCell,
bool StairsConeVisible,
int OutsidePolys,
int FloodCells);
private static Matrix4x4 ViewProjFor(Vector3 eye, Vector3 lookAt)
{
var view = Matrix4x4.CreateLookAt(eye, lookAt, Vector3.UnitZ);
var proj = Matrix4x4.CreatePerspectiveFieldOfView(1.2f, 1280f / 720f, 0.1f, 5000f);
return view * proj;
}
private static uint? ResolveInteriorCellByAabb(
Dictionary<uint, LoadedCell> cells, Vector3 worldPoint, float margin = 0.05f)
{
uint? best = null;
float bestVolume = float.MaxValue;
foreach (var (id, cell) in cells)
{
var local = Vector3.Transform(worldPoint, cell.InverseWorldTransform);
var min = cell.LocalBoundsMin - new Vector3(margin);
var max = cell.LocalBoundsMax + new Vector3(margin);
if (local.X < min.X || local.Y < min.Y || local.Z < min.Z
|| local.X > max.X || local.Y > max.Y || local.Z > max.Z)
continue;
var ext = cell.LocalBoundsMax - cell.LocalBoundsMin;
float volume = MathF.Max(ext.X, 1e-3f) * MathF.Max(ext.Y, 1e-3f) * MathF.Max(ext.Z, 1e-3f);
if (volume < bestVolume) { bestVolume = volume; best = id; }
}
return best;
}
// Mirror of RetailPViewRenderer.MergeBuildingFrame (union semantics).
private static void MergeFrame(PortalVisibilityFrame target, PortalVisibilityFrame src)
{
foreach (uint cellId in src.OrderedVisibleCells)
{
if (!src.CellViews.TryGetValue(cellId, out var srcView)) continue;
if (target.CellViews.TryGetValue(cellId, out var existing))
{
foreach (var p in srcView.Polygons) existing.Add(p);
continue;
}
target.CellViews[cellId] = srcView;
target.OrderedVisibleCells.Add(cellId);
}
}
private List<AscentStep> RunAscent()
{
var datDir = RequireDatDir();
using var dats = new DatCollection(datDir, DatAccessType.Read);
var cells = Issue120ReciprocalPingPongTests.LoadAllInteriorCells(dats, Landblock);
Func<uint, LoadedCell?> lookup = id => cells.TryGetValue(id, out var c) ? c : null;
Assert.True(cells.ContainsKey(MainCell), "tower main cell not loaded");
// The tower building's cell list (Issue119TowerDumpTests: building[1]
// stabs = 0x0107..0x010A) — the per-building flood candidates on
// outdoor-root steps, mirroring GameWindow's gather.
var towerCells = new List<LoadedCell>();
for (uint low = 0x0107u; low <= 0x010Au; low++)
if (cells.TryGetValue(Landblock | low, out var c))
towerCells.Add(c);
// Eye helix matching the spiral: two turns, radius 2.5 m, z from head
// height at the base to above the roof lip (cell 0x010A tops at 127.2).
const int Steps = 220;
const float ZStart = 113.6f, ZEnd = 128.6f;
var clipFrame = ClipFrame.NoClip();
var steps = new List<AscentStep>(Steps + 1);
for (int i = 0; i <= Steps; i++)
{
float t = i / (float)Steps;
float theta = t * 4f * MathF.PI;
float z = ZStart + t * (ZEnd - ZStart);
var eye = new Vector3(
TowerCenter.X + 2.5f * MathF.Cos(theta),
TowerCenter.Y + 2.5f * MathF.Sin(theta),
z);
// look AT the staircase (the user's framing while climbing: the
// camera tracks the player on the stairs). An unambiguous pin:
// if the staircase cone-culls while looked at directly, the
// visibility stack is wrong — no gaze excuse.
var gaze = StairSphereCenter - eye;
if (gaze.LengthSquared() < 1e-4f) gaze = new Vector3(0f, 0f, -1f);
var viewProj = ViewProjFor(eye, eye + Vector3.Normalize(gaze) * 3f);
uint rootCellId = 0u;
PortalVisibilityFrame pv;
var contained = ResolveInteriorCellByAabb(cells, eye);
if (contained.HasValue)
{
rootCellId = contained.Value;
pv = PortalVisibilityBuilder.Build(cells[rootCellId], eye, lookup, viewProj);
}
else
{
// outdoor root: full-screen outside view + the per-building
// exterior floods (RetailPViewRenderer.MergeNearbyBuildingFloods)
var outdoor = OutdoorCellNode.Build(Landblock | 0x0001u);
pv = PortalVisibilityBuilder.Build(outdoor, eye, lookup, viewProj);
var bf = PortalVisibilityBuilder.ConstructViewBuilding(
towerCells, eye, lookup, viewProj);
MergeFrame(pv, bf);
}
var asm = ClipFrameAssembler.Assemble(clipFrame, pv);
var cone = ViewconeCuller.Build(asm, viewProj);
bool floodHasMain = pv.CellViews.ContainsKey(MainCell);
// DrawCellObjectLists predicate for a 0x0107 static: sphere vs the
// cell's views (RetailPViewRenderer.cs DrawCellObjectLists / T3).
bool stairsVisible = cone.SphereVisibleInCell(MainCell, StairSphereCenter, StairSphereRadius);
steps.Add(new AscentStep(
i, eye, rootCellId, floodHasMain, stairsVisible,
pv.OutsideView.Polygons.Count, pv.OrderedVisibleCells.Count));
}
return steps;
}
private void DumpSteps(IEnumerable<AscentStep> steps, Func<AscentStep, bool>? filter = null)
{
foreach (var st in steps)
{
if (filter is not null && !filter(st)) continue;
string root = st.RootCellId == 0 ? "OUTDOOR " : FormattableString.Invariant($"0x{st.RootCellId:X8}");
string stairs = st.StairsConeVisible ? "VIS " : "CULL";
_out.WriteLine(FormattableString.Invariant(
$"step={st.Index,3} eye=({st.Eye.X:F2},{st.Eye.Y:F2},{st.Eye.Z:F2}) root={root} floodMain={st.FloodHasMainCell} stairs={stairs} outPolys={st.OutsidePolys} flood={st.FloodCells}"));
}
}
/// <summary>
/// The vanish pin: the staircase static must stay cone-visible on every
/// ascent step (the gaze looks STRAIGHT AT it — no gaze excuse).
///
/// PINNED RED 2026-06-11 (skip carries the defect; un-skip with the fix):
/// steps 195201 (eye z 126.9127.3, the roof-lip band between the main
/// cell's ceiling at 126.8 and the roof aperture plane at ~127.2) resolve
/// OUTDOOR and the per-building exterior flood admits NOTHING (flood=1 =
/// the outdoor node alone) — the eye is above every side aperture's
/// useful view and ON/INSIDE the roof aperture's plane, so
/// BuildFromExterior's seed side-test/in-plane reject refuses every exit
/// portal. The tower interior never floods → the staircase (a 0x0107
/// static) has no views → culled while walkable, and the roof-lip cell
/// geometry flaps as the live eye bobs across the band's edges. Fix
/// needs the retail oracle: does retail keep the viewer cell INTERIOR
/// through this band (curr_cell keep-curr above open-top cells), or can
/// ConstructView(CBldPortal) seed with an in-plane eye?
/// </summary>
[Fact]
[Trait("Status", "KnownFailure")]
public void TowerAscent_StaircaseStaysConeVisible_EveryStep()
{
var steps = RunAscent();
var failures = steps.FindAll(s => !s.StairsConeVisible);
if (failures.Count > 0)
{
_out.WriteLine($"--- {failures.Count} stairs-CULLED steps ---");
DumpSteps(failures);
}
Assert.True(failures.Count == 0,
$"{failures.Count}/{steps.Count} ascent steps cone-cull the staircase (first at step {(failures.Count > 0 ? failures[0].Index : -1)}) — see output");
}
/// <summary>
/// The flap pin: along a smooth monotone ascent, the resolved root may
/// transition between cells/outdoor but must not PING-PONG (A→B→A within
/// three consecutive steps = a knife-edge decision the live camera jitter
/// turns into per-frame flapping).
/// </summary>
[Fact]
public void TowerAscent_RootDoesNotPingPong()
{
var steps = RunAscent();
var flips = new List<int>();
for (int i = 2; i < steps.Count; i++)
{
uint a = steps[i - 2].RootCellId, b = steps[i - 1].RootCellId, c = steps[i].RootCellId;
if (a != b && b != c && a == c)
flips.Add(i - 1);
}
if (flips.Count > 0)
{
_out.WriteLine($"--- {flips.Count} root ping-pong steps ---");
DumpSteps(steps, s => flips.Contains(s.Index) || flips.Contains(s.Index - 1) || flips.Contains(s.Index + 1));
}
Assert.True(flips.Count == 0,
$"{flips.Count} root ping-pongs along a monotone ascent — knife-edge root decisions (see output)");
}
/// <summary>
/// THE captured top-of-stairs vanish (flap-diff-capture.log, live user
/// pan): root=0xAAB3010A (the roof-lip cell), eye 3 mm above its floor
/// portal plane, gazing DOWN at the staircase — and the flood admitted
/// ONLY {010A}: 0x0107 (the whole tower interior, staircase included)
/// dropped while being looked straight at. Exact production inputs:
/// eye=(301.448,-128.448,126.803) fwd=(-0.9361,-0.2459,-0.2514)
/// eye=(301.371,-128.401,126.807) fwd=(-0.8938,-0.3596,-0.2678)
/// The 010A→0107 portal is HORIZONTAL at z≈126.8 — the eye rides within
/// millimetres of its plane at the stair top, knife-edge projection
/// territory (the #120 family's geometry, admission-side).
/// </summary>
[Theory]
[InlineData(301.448f, -128.448f, 126.803f, -0.9361f, -0.2459f, -0.2514f)]
[InlineData(301.371f, -128.401f, 126.807f, -0.8938f, -0.3596f, -0.2678f)]
public void CapturedTopOfStairs_MainCellStaysInFlood(
float ex, float ey, float ez, float fx, float fy, float fz)
{
var datDir = RequireDatDir();
using var dats = new DatCollection(datDir, DatAccessType.Read);
var cells = Issue120ReciprocalPingPongTests.LoadAllInteriorCells(dats, Landblock);
Func<uint, LoadedCell?> lookup = id => cells.TryGetValue(id, out var c) ? c : null;
// The capture is A9B4-anchored world; the loaded cells are AAB3-local
// (LoadCell without offset). AAB3 = A9B4 + (1,-1) blocks → local =
// world - (192,-192,0).
var eye = new Vector3(ex - 192f, ey + 192f, ez);
var fwd = Vector3.Normalize(new Vector3(fx, fy, fz));
// ROOT CAUSE (found via this capture): production fed BuildLoadedCell
// the +0.02 m RENDER-LIFTED transform ("keep the small render lift out
// of physics", GameWindow:5567) — the visibility graph's HORIZONTAL
// portal planes all sat 2 cm high, so an eye standing on the deck
// (3 mm above the TRUE plane) was 17 mm BELOW the lifted plane —
// outside the side test's ±10 mm in-plane window → 010A→0107
// side-culled → the staircase vanished while walkable. FIXED: the
// visibility LoadedCell now gets the PHYSICS (unlifted) transform.
//
// Arm 1 (unlifted = post-fix production): the main cell must be
// admitted. Arm 2 (lifted = the old bug, kept as the mechanism
// canary): the main cell is dropped — if this arm ever starts
// PASSING, the in-plane window has changed enough to absorb a 2 cm
// lift; re-evaluate both this canary and the no-lift rule.
foreach (bool lift in new[] { false, true })
{
var armCells = lift ? new Dictionary<uint, LoadedCell>() : cells;
if (lift)
{
var datDir2 = CornerFloodReplayTests.ResolveDatDir()!;
using var dats2 = new DatCollection(datDir2, DatAccessType.Read);
var lbi = dats2.Get<DatReaderWriter.DBObjs.LandBlockInfo>(Landblock | 0xFFFEu)!;
for (uint low = 0x0100u; low < 0x0100u + lbi.NumCells; low++)
{
try { armCells[Landblock | low] = CornerFloodReplayTests.LoadCell(dats2, Landblock | low, new Vector3(0f, 0f, 0.02f)); }
catch (InvalidOperationException) { }
}
}
Func<uint, LoadedCell?> armLookup = id => armCells.TryGetValue(id, out var c) ? c : null;
var root = armCells[Landblock | 0x010Au];
foreach (float fov in new[] { MathF.PI / 3f, 1.2f, MathF.PI / 2f })
{
var view = Matrix4x4.CreateLookAt(eye, eye + fwd * 3f, Vector3.UnitZ);
var proj = Matrix4x4.CreatePerspectiveFieldOfView(fov, 1280f / 720f, 0.1f, 5000f);
var pv = PortalVisibilityBuilder.Build(root, eye, armLookup, view * proj);
bool hasMain = pv.CellViews.ContainsKey(MainCell);
_out.WriteLine(FormattableString.Invariant(
$"lift={lift} fov={fov:F2}: flood={pv.OrderedVisibleCells.Count} hasMain={hasMain}"));
if (!lift)
Assert.True(hasMain,
$"fov={fov:F2}: the tower main cell 0x{MainCell:X8} is NOT in the flood from the roof-lip root (unlifted = post-fix production)");
else
Assert.False(hasMain,
$"fov={fov:F2}: the LIFTED canary unexpectedly admits the main cell — the in-plane window now absorbs a 2 cm lift; re-evaluate the canary + the no-lift rule");
}
}
}
/// <summary>
/// Gate-by-gate diagnosis of the captured drop: for the 010A→0107 portal
/// at the exact captured eye/gaze, print each admission gate's verdict —
/// side test (CameraOnInteriorSide replica), homogeneous clip vs the
/// full-screen view (ProjectToClip + ClipToRegion, the forward-hop
/// pipeline), and the eye-in-opening rescue inputs (perp distance +
/// footprint containment).
/// </summary>
[Fact]
[Trait("Purpose", "Diagnostic")]
public void Diagnostic_TopOfStairs_GateByGate()
{
var datDir = RequireDatDir();
using var dats = new DatCollection(datDir, DatAccessType.Read);
var cells = Issue120ReciprocalPingPongTests.LoadAllInteriorCells(dats, Landblock);
var root = cells[Landblock | 0x010Au];
// capture eye converted to the cells' AAB3-local frame (world - (192,-192,0))
var eye = new Vector3(301.448f - 192f, -128.448f + 192f, 126.803f);
var fwd = Vector3.Normalize(new Vector3(-0.9361f, -0.2459f, -0.2514f));
var view = Matrix4x4.CreateLookAt(eye, eye + fwd * 3f, Vector3.UnitZ);
var proj = Matrix4x4.CreatePerspectiveFieldOfView(MathF.PI / 3f, 1280f / 720f, 0.1f, 5000f);
var viewProj = view * proj;
for (int i = 0; i < root.Portals.Count; i++)
{
var portal = root.Portals[i];
var poly = i < root.PortalPolygons.Count ? root.PortalPolygons[i] : null;
_out.WriteLine($"portal[{i}] -> 0x{portal.OtherCellId:X4} polyLen={poly?.Length ?? -1}");
if (poly is null || poly.Length < 3) continue;
// gate 1: side test (CameraOnInteriorSide replica, ε=0.01 in-plane-allowed)
string side = "no-plane(allow)";
if (i < root.ClipPlanes.Count && root.ClipPlanes[i].Normal.LengthSquared() >= 1e-8f)
{
var plane = root.ClipPlanes[i];
var localCam = Vector3.Transform(eye, root.InverseWorldTransform);
float dot = Vector3.Dot(plane.Normal, localCam) + plane.D;
bool pass = plane.InsideSide == 0 ? dot >= -0.01f : dot <= 0.01f;
side = FormattableString.Invariant($"dot={dot:F4} insideSide={plane.InsideSide} -> {(pass ? "TRAVERSE" : "CULL")}");
}
_out.WriteLine($" side: {side}");
// gate 2: homogeneous clip vs the full-screen view
var clip = PortalProjection.ProjectToClip(poly, root.WorldTransform, viewProj);
var fullScreen = new[] { new Vector2(-1f, -1f), new Vector2(1f, -1f), new Vector2(1f, 1f), new Vector2(-1f, 1f) };
var region = clip.Length >= 3 ? PortalProjection.ClipToRegion(clip, fullScreen) : Array.Empty<Vector2>();
_out.WriteLine($" clip: projVerts={clip.Length} regionVerts={region.Length}");
// gate 3: rescue inputs — perp distance to the portal plane + the
// perpendicular foot inside the opening's footprint
var centroid = Vector3.Zero;
foreach (var v in poly) centroid += Vector3.Transform(v, root.WorldTransform);
centroid /= poly.Length;
var n0 = Vector3.Transform(poly[0], root.WorldTransform);
var n1 = Vector3.Transform(poly[1], root.WorldTransform);
var n2 = Vector3.Transform(poly[2], root.WorldTransform);
var planeN = Vector3.Normalize(Vector3.Cross(n1 - n0, n2 - n0));
float perp = MathF.Abs(Vector3.Dot(planeN, eye - centroid));
_out.WriteLine(FormattableString.Invariant(
$" rescue: perpDist={perp:F4} centroid=({centroid.X:F2},{centroid.Y:F2},{centroid.Z:F2}) planeN=({planeN.X:F2},{planeN.Y:F2},{planeN.Z:F2})"));
}
// knife-edge sweep: the probe rounds the eye to mm — sweep z within the
// rounding envelope and count clip flips for the 010A→0107 portal. A
// chaotic 6↔0 region inside ±2 mm proves the live drop is the same
// frame at sub-mm precision.
{
var poly = root.PortalPolygons[0];
var fullScreen = new[] { new Vector2(-1f, -1f), new Vector2(1f, -1f), new Vector2(1f, 1f), new Vector2(-1f, 1f) };
int flips = 0;
bool? prev = null;
var results = new System.Text.StringBuilder();
for (int i = -20; i <= 20; i++)
{
var e2 = eye with { Z = eye.Z + i * 0.0002f };
var v2 = Matrix4x4.CreateLookAt(e2, e2 + fwd * 3f, Vector3.UnitZ);
var vp2 = v2 * proj;
var clip2 = PortalProjection.ProjectToClip(poly, root.WorldTransform, vp2);
var region2 = clip2.Length >= 3 ? PortalProjection.ClipToRegion(clip2, fullScreen) : Array.Empty<Vector2>();
bool ok = region2.Length >= 3;
if (prev.HasValue && ok != prev.Value) flips++;
prev = ok;
results.Append(ok ? '1' : '0');
}
_out.WriteLine($" knife-edge sweep (z ±4 mm, 0.2 mm steps): {results} flips={flips}");
}
}
/// <summary>Full per-step table for the investigation record.</summary>
[Fact]
[Trait("Purpose", "Diagnostic")]
public void Diagnostic_TowerAscent_PerStepTable()
{
var steps = RunAscent();
DumpSteps(steps);
}
private static string RequireDatDir()
=> CornerFloodReplayTests.ResolveDatDir()
?? throw new InvalidOperationException(
"Installed client_portal.dat is required for the tower-ascent lane.");
}

View file

@ -1,60 +0,0 @@
using System.Numerics;
using AcDream.App.Rendering;
using Xunit;
namespace AcDream.App.Tests.Rendering;
public sealed class ViewconeCullerReuseTests
{
[Fact]
public void ReusedCuller_ReplacesPriorCellAndOutsideState()
{
const uint firstCell = 0xA9B40100;
const uint secondCell = 0xA9B40101;
var clipFrame = ClipFrame.NoClip();
var assemblyScratch = new ClipFrameAssembly();
var culler = new ViewconeCuller();
var first = Frame(firstCell, includeOutside: true);
ClipFrameAssembly firstAssembly = ClipFrameAssembler.Assemble(
clipFrame,
first,
assemblyScratch);
Assert.Same(culler, ViewconeCuller.Build(firstAssembly, Matrix4x4.Identity, culler));
Assert.True(culler.SphereVisibleInCell(firstCell, Vector3.Zero, 0f));
Assert.True(culler.SphereVisibleOutside(Vector3.Zero, 0f));
var second = Frame(secondCell, includeOutside: false);
ClipFrameAssembly secondAssembly = ClipFrameAssembler.Assemble(
clipFrame,
second,
assemblyScratch);
Assert.Same(culler, ViewconeCuller.Build(secondAssembly, Matrix4x4.Identity, culler));
Assert.False(culler.SphereVisibleInCell(firstCell, Vector3.Zero, 0f));
Assert.True(culler.SphereVisibleInCell(secondCell, Vector3.Zero, 0f));
Assert.False(culler.SphereVisibleInCell(secondCell, new Vector3(2f, 0f, 0f), 0f));
Assert.False(culler.SphereVisibleOutside(Vector3.Zero, 0f));
}
private static PortalVisibilityFrame Frame(uint cellId, bool includeOutside)
{
var result = new PortalVisibilityFrame();
var cellView = new CellView();
cellView.Add(Square(0.5f));
result.CellViews.Add(cellId, cellView);
result.OrderedVisibleCells.Add(cellId);
if (includeOutside)
result.OutsideView.Add(Square(0.5f));
return result;
}
private static ViewPolygon Square(float half)
=> new(new[]
{
new Vector2(-half, -half),
new Vector2(half, -half),
new Vector2(half, half),
new Vector2(-half, half),
});
}

View file

@ -12,7 +12,7 @@ public sealed class WalkBuildingInstalledDatCensusTests
[Fact]
public void CompleteInstalledBuildingCorpusMatchesBoundedSelectionCensus()
{
string? datDir = CornerFloodReplayTests.ResolveDatDir();
string? datDir = InstalledDatTestPath.Resolve();
if (datDir is null)
Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory.");
using var dats = new DatCollection(datDir!, DatAccessType.Read);

View file

@ -23,7 +23,7 @@ public sealed class WalkBuildingJoinDiagnosticTests
public void Traced_lookin_cells_are_reachable_through_the_building_portal_table(
uint buildingCellId, uint[] tracedLookInCells)
{
string? datDir = CornerFloodReplayTests.ResolveDatDir();
string? datDir = InstalledDatTestPath.Resolve();
if (datDir is null)
{
Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory.");

View file

@ -446,7 +446,7 @@ public sealed partial class WalkFrameDriverTests
Assert.Equal(2, mdiCalls.Sum(c => (int)c.DrawCount));
// Exit seals replay through the exact portal_view slices captured by
// this walk. The legacy PortalVisibilityFrame is not consulted.
// this walk; there is no independently reconstructed visibility graph.
Assert.Equal(1, driver.InteriorFloodViewSliceCountAt(0));
Assert.Equal(1, driver.InteriorFloodViewSliceCountAt(1));
Assert.Equal(4, driver.InteriorFloodViewClipPlanesAt(0, 0).Length);

View file

@ -25,8 +25,8 @@ namespace AcDream.App.Tests.Rendering.Walk;
/// <see cref="RenderingDiagnostics.DumpWalkTranscriptEnabled"/> and
/// <see cref="Console.Out"/> are both process-wide mutable statics — joins
/// <see cref="CameraDiagnosticsCollection"/> for the SAME reason
/// <c>CornerFloodReplayTests</c>/<c>Issue181WallPressEquilibriumTests</c> do
/// (that collection's own doc comment, issue #251): interleaved
/// <c>Issue181WallPressEquilibriumTests</c> does (that collection's own doc
/// comment, issue #251): interleaved
/// <c>Console.SetOut</c> redirection across parallel test classes can
/// restore a disposed <see cref="StringWriter"/> process-wide.
/// </para>

View file

@ -31,7 +31,7 @@ public sealed class WalkLookInGateSweepTests
+ "(the doorway-still fixture tests it in isolation).")]
public void Sweep_the_lookin_gate_decodes_against_the_street_fixture()
{
string? datDir = CornerFloodReplayTests.ResolveDatDir();
string? datDir = InstalledDatTestPath.Resolve();
if (datDir is null)
{
Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory.");

View file

@ -20,7 +20,7 @@ public sealed class WalkPortalGateDumpTests
[Fact]
public void Dump_the_gate_inputs_for_a_punching_and_a_silent_building()
{
string? datDir = CornerFloodReplayTests.ResolveDatDir();
string? datDir = InstalledDatTestPath.Resolve();
if (datDir is null)
{
Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory.");
@ -102,7 +102,7 @@ public sealed class WalkPortalGateDumpTests
[Fact]
public void Dump_the_exit_view_look_in_inputs_for_walkout_f2()
{
string? datDir = CornerFloodReplayTests.ResolveDatDir();
string? datDir = InstalledDatTestPath.Resolve();
if (datDir is null)
{
Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory.");
@ -232,7 +232,7 @@ public sealed class WalkPortalGateDumpTests
[Fact]
public void Dump_the_foundry_f67_bsp_traversal_for_0036()
{
string? datDir = CornerFloodReplayTests.ResolveDatDir();
string? datDir = InstalledDatTestPath.Resolve();
if (datDir is null)
{
Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory.");

View file

@ -59,7 +59,7 @@ public sealed class WalkProductionWorldConformanceTests
private static DatCollection OpenDats()
{
string? datDir = CornerFloodReplayTests.ResolveDatDir();
string? datDir = InstalledDatTestPath.Resolve();
if (datDir is null)
{
Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md.");

View file

@ -453,31 +453,6 @@ public sealed class WalkProductionWorldDataTests
Assert.Equal(first.Records.Count, second.Records.Count);
}
[Fact]
public void StaticBucketContains_FindsARegisteredSourceIdInItsRetailCell()
{
const uint entityId = 0x48A02040u;
const uint sourceId = 0x020009A2u;
const uint retailCellId = 0xF4180112u;
var shadows = new ShadowObjectRegistry();
Register(shadows, entityId, retailCellId, landblockId: 0xF4180000u);
RenderSceneGeneration generation = RenderSceneGeneration.FromRaw(1);
using var scene = new ArchRenderScene(generation);
RenderProjectionRecord projection =
IndoorStaticRecord(entityId, sourceId, retailCellId);
scene.Apply([RenderProjectionDelta.Register(generation, 1, projection)]);
var worldData = new WalkProductionWorldData(new WalkBuildingRegistry(), shadows);
worldData.BeginFrame(
scene.OpenQuery(), 0xF4180000u, renderCenterLbX: 0xF4, renderCenterLbY: 0x18);
Assert.True(worldData.StaticBucketContains(retailCellId, sourceId));
Assert.False(worldData.StaticBucketContains(retailCellId, sourceId + 1));
Assert.False(worldData.StaticBucketContains(0xF4180107u, sourceId));
}
private static RenderProjectionRecord Record(uint id, Vector3 position) =>
new RenderProjectionRecord() with
{

View file

@ -0,0 +1,239 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Walk;
using AcDream.App.Rendering.Wb;
using AcDream.Content;
using DatReaderWriter;
namespace AcDream.App.Tests.Rendering.Walk;
public sealed class WalkRendererArchitectureGuardTests
{
private static readonly Assembly AppAssembly = typeof(RetailFrameWalk).Assembly;
private static readonly string[] DeletedGraphTypes =
[
"PortalVisibilityBuilder",
"PortalVisibilityFrame",
"ExteriorPortalSeed",
"IndoorDrawPlan",
"CellDrawEntry",
"ViewconeCuller",
];
private static readonly string[] DeletedProbeSymbols =
[
"ProbeFacilityStairsEnabled",
"ACDREAM_PROBE_FACILITY_STAIRS",
"ProbeIndoorWalkEnabled",
"ProbeIndoorLookupEnabled",
"ProbeIndoorUploadEnabled",
"ProbeIndoorXformEnabled",
"ProbeIndoorCullEnabled",
"ACDREAM_PROBE_INDOOR_WALK",
"ACDREAM_PROBE_INDOOR_LOOKUP",
"ACDREAM_PROBE_INDOOR_UPLOAD",
"ACDREAM_PROBE_INDOOR_XFORM",
"ACDREAM_PROBE_INDOOR_CULL",
"ACDREAM_PROBE_INDOOR_ALL",
"ProbeVisibilityEnabled",
"ACDREAM_PROBE_VIS",
"EmitVis",
"ProbeEnvCellEnabled",
"ACDREAM_PROBE_ENVCELL",
"ProbeFlapEnabled",
"ACDREAM_PROBE_FLAP",
"ProbePvInputEnabled",
"ACDREAM_PROBE_PVINPUT",
"ProbeGlStateEnabled",
"ACDREAM_PROBE_GLSTATE",
"ProbeClipRouteEnabled",
"ACDREAM_PROBE_CLIPROUTE",
"ProbePortalChurnEnabled",
"ACDREAM_PROBE_PORTAL_CHURN",
"ProbeIndoorLightEnabled",
"ACDREAM_PROBE_INDOOR_LIGHT",
"EmitIndoorLight",
"ProbeSeamDrawEnabled",
"ACDREAM_PROBE_SEAMDRAW",
"SeamDrawTargetCells",
];
[Fact]
public void AppAssembly_ContainsNoSupersededPortalGraphTypes()
{
string[] survivors = AppAssembly.GetTypes()
.Where(type => DeletedGraphTypes.Contains(type.Name, StringComparer.Ordinal))
.Select(type => type.FullName ?? type.Name)
.OrderBy(name => name, StringComparer.Ordinal)
.ToArray();
Assert.Empty(survivors);
}
[Fact]
public void WalkFrameOwners_AreUnique()
{
const BindingFlags Fields = BindingFlags.Instance | BindingFlags.Public |
BindingFlags.NonPublic | BindingFlags.DeclaredOnly;
Type[] sourceTypes = AppAssembly.GetTypes()
.Where(type => !type.IsDefined(typeof(CompilerGeneratedAttribute), inherit: false))
.ToArray();
(Type Owner, FieldInfo Field)[] pviewOwners = sourceTypes
.SelectMany(type => type.GetFields(Fields).Select(field => (type, field)))
.Where(pair => pair.field.FieldType == typeof(WalkPView))
.Select(pair => (pair.type, pair.field))
.ToArray();
Assert.All(pviewOwners, pair => Assert.Equal(typeof(RetailFrameWalk), pair.Owner));
Assert.Equal(2, pviewOwners.Length);
(Type Owner, FieldInfo Field)[] frameWalkOwners = sourceTypes
.SelectMany(type => type.GetFields(Fields).Select(field => (type, field)))
.Where(pair => pair.field.FieldType == typeof(RetailFrameWalk))
.Select(pair => (pair.type, pair.field))
.ToArray();
(Type Owner, FieldInfo Field) owner = Assert.Single(frameWalkOwners);
Assert.Equal(typeof(RetailPViewRenderer), owner.Owner);
}
[Fact]
public void FrameTimeWalkOwners_HaveNoRawDatDependency()
{
Type[] owners =
[
typeof(RetailFrameWalk),
typeof(WalkFrameDriver),
typeof(RetailPViewRenderer),
typeof(RetailPViewPassExecutor),
typeof(WalkProductionLeafRenderer),
typeof(WalkStaticStreamPopulator),
typeof(WbDrawDispatcher),
];
var violations = new List<string>();
foreach (Type owner in owners)
{
const BindingFlags Members = BindingFlags.Instance | BindingFlags.Static |
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly;
foreach (FieldInfo field in owner.GetFields(Members))
AddIfRawDat(violations, owner, $"field {field.Name}", field.FieldType);
foreach (PropertyInfo property in owner.GetProperties(Members))
AddIfRawDat(violations, owner, $"property {property.Name}", property.PropertyType);
IEnumerable<MethodBase> methods = owner.GetMethods(Members).Cast<MethodBase>()
.Concat(owner.GetConstructors(Members));
foreach (MethodBase method in methods)
{
foreach (ParameterInfo parameter in method.GetParameters())
{
AddIfRawDat(
violations,
owner,
$"parameter {method.Name}.{parameter.Name}",
parameter.ParameterType);
}
if (method is MethodInfo methodInfo)
AddIfRawDat(violations, owner, $"return {method.Name}", methodInfo.ReturnType);
}
}
Assert.Empty(violations);
}
[Fact]
public void ProductionSource_ContainsNoSecondPortalGraph()
{
string[] sources = ProductionSources();
string[] banned = [.. DeletedGraphTypes, "ClipFrameAssembler.Assemble"];
AssertNoSourceMatches(sources, banned);
}
[Fact]
public void OrderedWalkStream_HasNoCrossStreamReorder()
{
string source = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src", "AcDream.App", "Rendering", "Wb", "WbDrawDispatcher.OrderedStream.cs"));
Assert.DoesNotContain(".Sort(", source, StringComparison.Ordinal);
Assert.DoesNotContain("OrderBy(", source, StringComparison.Ordinal);
Assert.DoesNotContain("OrderByDescending(", source, StringComparison.Ordinal);
}
[Fact]
public void ProductionSource_ContainsNoDeletedRendererProbe_AndRetainsWalkTranscriptProof()
{
AssertNoSourceMatches(ProductionSources(), DeletedProbeSymbols);
string root = FindRepoRoot();
string diagnostics = File.ReadAllText(Path.Combine(
root, "src", "AcDream.Core", "Rendering", "RenderingDiagnostics.cs"));
string transcriptTests = File.ReadAllText(Path.Combine(
root,
"tests", "AcDream.App.Tests", "Rendering", "Walk",
"WalkFrameDriverTranscriptTests.cs"));
Assert.Contains("DumpWalkTranscriptEnabled", diagnostics, StringComparison.Ordinal);
Assert.Contains(
"TranscriptEmitter_FlagOff_EveryPrintMethodIsAZeroCostNoOp",
transcriptTests,
StringComparison.Ordinal);
Assert.Contains(
"Collect_TranscriptFlagOn_PrintedLinesRoundTripThroughTheParser",
transcriptTests,
StringComparison.Ordinal);
}
private static void AddIfRawDat(
ICollection<string> violations,
Type owner,
string member,
Type memberType)
{
if (ContainsRawDat(memberType))
violations.Add($"{owner.FullName} {member}: {memberType}");
}
private static bool ContainsRawDat(Type type)
{
if (type.IsByRef || type.IsArray || type.IsPointer)
return ContainsRawDat(type.GetElementType()!);
if (type == typeof(DatCollection) || type == typeof(IDatReaderWriter))
return true;
return type.IsGenericType && type.GetGenericArguments().Any(ContainsRawDat);
}
private static void AssertNoSourceMatches(IEnumerable<string> sources, IEnumerable<string> banned)
{
string[] violations = sources
.SelectMany(path => banned
.Where(symbol => File.ReadAllText(path).Contains(symbol, StringComparison.Ordinal))
.Select(symbol => $"{Path.GetRelativePath(FindRepoRoot(), path)}: {symbol}"))
.OrderBy(value => value, StringComparer.Ordinal)
.ToArray();
Assert.Empty(violations);
}
private static string[] ProductionSources()
{
string root = Path.Combine(FindRepoRoot(), "src");
return Directory.GetFiles(root, "*.cs", SearchOption.AllDirectories)
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase)
.ToArray();
}
private static string FindRepoRoot()
{
string? directory = AppContext.BaseDirectory;
while (directory is not null)
{
if (File.Exists(Path.Combine(directory, "AcDream.slnx")))
return directory;
directory = Directory.GetParent(directory)?.FullName;
}
throw new DirectoryNotFoundException("Could not locate AcDream.slnx.");
}
}

View file

@ -318,7 +318,7 @@ public sealed class WalkStaticStreamPopulatorTests
Assert.DoesNotContain(
"one stable far-to-near stream keyed", architecture, StringComparison.OrdinalIgnoreCase);
Assert.Contains("active total to **161**", register, StringComparison.Ordinal);
Assert.Contains("current active total to **160**", register, StringComparison.Ordinal);
int apSectionStart = register.IndexOf(
"## 3. Documented approximation (AP)", StringComparison.Ordinal);
int apSectionEnd = register.IndexOf(
@ -342,7 +342,7 @@ public sealed class WalkStaticStreamPopulatorTests
Assert.Single(File.ReadLines(registerPath), static line =>
line.StartsWith("| AP-244 |", StringComparison.Ordinal));
Assert.Equal(
161,
160,
File.ReadLines(registerPath).Count(static line =>
line.StartsWith("| AP-", StringComparison.Ordinal)));
Assert.Contains("greater than 50 m", register, StringComparison.Ordinal);

View file

@ -114,7 +114,7 @@ public sealed partial class WalkTraceConformanceTests
private static DatCollection OpenDats()
{
string? datDir = CornerFloodReplayTests.ResolveDatDir();
string? datDir = InstalledDatTestPath.Resolve();
if (datDir is null)
{
Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md.");

View file

@ -19,7 +19,7 @@ public sealed class WalkWorldDatAdapterTests
{
private static DatCollection OpenDats()
{
string? datDir = CornerFloodReplayTests.ResolveDatDir();
string? datDir = InstalledDatTestPath.Resolve();
if (datDir is null)
{
Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md.");

View file

@ -39,21 +39,13 @@ public class WalkOutsideViewReassemblyTests
return view;
}
private static ClipFrameAssembly AssembledWithOldOutside()
{
var pv = new PortalVisibilityFrame();
pv.OutsideView.Add(new ViewPolygon(new[]
{
new Vector2(-0.9f, -0.9f), new Vector2(0.9f, -0.9f),
new Vector2(0.9f, 0.9f), new Vector2(-0.9f, 0.9f),
}));
return ClipFrameAssembler.Assemble(ClipFrame.NoClip(), pv);
}
private static ClipFrameAssembly BeginAssembly()
=> ClipFrameAssembler.BeginWalkFrame(ClipFrame.NoClip(), outdoorRoot: true);
[Fact]
public void PixelDoorway_MapsToExpectedNdcAabbAndPlanes()
{
ClipFrameAssembly asm = AssembledWithOldOutside();
ClipFrameAssembly asm = BeginAssembly();
// Pixel quad x∈[256,512], y∈[180,360] → NDC x∈[-0.5,0], y∈[0,0.5]
// (yNdc = 1 2·py/H flips the axis: py=180 → +0.5, py=360 → 0).
WalkPortalView walkView = WalkViewOfPixelQuads(new[]
@ -95,7 +87,7 @@ public class WalkOutsideViewReassemblyTests
[Fact]
public void FullViewportWalkQuad_CoversFullNdc()
{
ClipFrameAssembly asm = AssembledWithOldOutside();
ClipFrameAssembly asm = BeginAssembly();
var walkView = new WalkPortalView();
Assert.True(WalkCopyView.AppendFullViewportQuad(
walkView, new StubRays(), Vector3.Zero, W, H));
@ -109,8 +101,8 @@ public class WalkOutsideViewReassemblyTests
[Fact]
public void EmptyWalkView_YieldsSkipMode()
{
ClipFrameAssembly asm = AssembledWithOldOutside();
Assert.True(asm.OutdoorVisible); // the old view was visible pre-cutover
ClipFrameAssembly asm = BeginAssembly();
Assert.True(asm.OutdoorVisible);
ClipFrameAssembler.ReassembleOutsideViewFromWalk(asm, new WalkPortalView(), W, H);
@ -138,7 +130,7 @@ public class WalkOutsideViewReassemblyTests
[Fact]
public void FirstViewCollapses_SecondSurvives_SlicesStayIndexAligned()
{
ClipFrameAssembly asm = AssembledWithOldOutside();
ClipFrameAssembly asm = BeginAssembly();
var walkView = new WalkPortalView();
// View 0: three EXACTLY collinear pixel points -> zero-area triangle,
@ -196,7 +188,7 @@ public class WalkOutsideViewReassemblyTests
[Fact]
public void PunchLeaf_UsesIndexAlignedSlice_DrawsNothingForCollapsed_ThrowsOutOfRange()
{
ClipFrameAssembly asm = AssembledWithOldOutside();
ClipFrameAssembly asm = BeginAssembly();
var walkView = new WalkPortalView();
walkView.View.Vertices.Add(new WalkViewVertex { Point = new Vector2(100f, 100f) });
walkView.View.Vertices.Add(new WalkViewVertex { Point = new Vector2(200f, 100f) });
@ -356,7 +348,7 @@ public class WalkOutsideViewReassemblyTests
[Fact]
public void NineVertexOutsideView_PunchSliceHasZeroPlanes_DrawsUnclipped_NotNothingVisible()
{
ClipFrameAssembly asm = AssembledWithOldOutside();
ClipFrameAssembly asm = BeginAssembly();
const int n = 9;
var verts = new Vector2[n];
@ -387,7 +379,7 @@ public class WalkOutsideViewReassemblyTests
[Fact]
public void TwoWalkViews_ProduceTwoSlicesWithDistinctSlots()
{
ClipFrameAssembly asm = AssembledWithOldOutside();
ClipFrameAssembly asm = BeginAssembly();
WalkPortalView walkView = WalkViewOfPixelQuads(
new[]
{

View file

@ -1,194 +1,29 @@
using System.Numerics;
using System.Reflection;
using AcDream.App.Rendering;
using AcDream.Core.Vfx;
namespace AcDream.App.Tests.Rendering;
public sealed class WorldRenderDiagnosticsTests
{
[Fact]
public void GlTripwire_DisabledPerformsNoGlRead()
{
var gl = new RecordingGlStateReader();
var log = new RecordingLog();
var diagnostics = new WorldRenderDiagnostics(gl, log);
diagnostics.EmitGlStateTripwireIfChanged(enabled: false);
Assert.Equal(0, gl.StateCaptureCount);
Assert.Empty(log.Messages);
}
[Fact]
public void GlTripwire_LogsOnlyChangesAndReportsStableFrameCount()
{
var gl = new RecordingGlStateReader
{
State = State(depthFunction: 0x201),
};
var log = new RecordingLog();
var diagnostics = new WorldRenderDiagnostics(gl, log);
diagnostics.EmitGlStateTripwireIfChanged(enabled: true);
diagnostics.EmitGlStateTripwireIfChanged(enabled: true);
gl.State = State(depthFunction: 0x203);
diagnostics.EmitGlStateTripwireIfChanged(enabled: true);
Assert.Equal(3, gl.StateCaptureCount);
Assert.Collection(
log.Messages,
first =>
{
Assert.StartsWith("[gl-state] frame=1 stable=0", first);
Assert.Contains("dfunc=0x201", first);
},
changed =>
{
Assert.StartsWith("[gl-state] frame=3 stable=1", changed);
Assert.Contains("dfunc=0x203", changed);
});
}
[Fact]
public void ScissorProbe_SequenceAdvancesAcrossSuppressedDuplicates()
{
var gl = new RecordingGlStateReader
{
Scissor = new RenderGlScissorSnapshot(
true,
new IntRenderRectangle(1, 2, 3, 4)),
};
var log = new RecordingLog();
var diagnostics = new WorldRenderDiagnostics(gl, log);
diagnostics.EmitClipRouteScissorProbe(true, true, new Vector4(-1, -1, 1, 1));
diagnostics.EmitClipRouteScissorProbe(true, true, new Vector4(-1, -1, 1, 1));
gl.Scissor = new RenderGlScissorSnapshot(
true,
new IntRenderRectangle(5, 6, 7, 8));
diagnostics.EmitClipRouteScissorProbe(true, true, new Vector4(-1, -1, 1, 1));
Assert.Equal(3, gl.ScissorCaptureCount);
Assert.Collection(
log.Messages,
first => Assert.StartsWith("[clip-route-scis] n=1", first),
changed => Assert.StartsWith("[clip-route-scis] n=3", changed));
}
[Fact]
public void Owner_RetainsNoBorrowedFrameProductsOrBroadRuntimeOwners()
{
FieldInfo[] fields = typeof(WorldRenderDiagnostics).GetFields(
BindingFlags.Instance | BindingFlags.NonPublic);
Type[] forbidden =
[
typeof(GameWindow),
typeof(PortalVisibilityFrame),
typeof(ClipFrameAssembly),
typeof(InteriorEntityPartition.Result),
typeof(AcDream.Core.Physics.PhysicsEngine),
typeof(CameraController),
typeof(CellVisibility),
];
foreach (Type type in forbidden)
Assert.DoesNotContain(fields, field => field.FieldType == type);
Assert.DoesNotContain(
fields,
field => typeof(Delegate).IsAssignableFrom(field.FieldType));
}
// Campaign V slice V11 deleted SilkRenderGlStateReader, the raw-GL
// IRenderGlStateReader implementation this test pinned (it was never
// constructed in production — NullRenderGlStateReader.Instance is the
// sole surviving implementer), so the "consume the entering GL error
// before other state reads" source-order tripwire it protected has
// nothing left to pin.
[Fact]
public void TerrainDiagnostics_RetryFailedPublicationWithoutLosingSamples()
{
var log = new ThrowOnceLog();
var diagnostics = new WorldRenderDiagnostics(new RecordingGlStateReader(), log);
var facts = new TerrainRenderDiagnosticFacts(VisibleSlots: 3, Draws: 3, LoadedSlots: 5, CapacitySlots: 8);
var diagnostics = new WorldRenderDiagnostics(log);
var facts = new TerrainRenderDiagnosticFacts(3, 3, 5, 8);
diagnostics.BeginTerrainDraw();
diagnostics.EndTerrainDraw();
Assert.Throws<InvalidOperationException>(() =>
diagnostics.PublishTerrainDiagnostics(facts));
Assert.Throws<InvalidOperationException>(() => diagnostics.PublishTerrainDiagnostics(facts));
diagnostics.BeginTerrainDraw();
diagnostics.EndTerrainDraw();
diagnostics.PublishTerrainDiagnostics(facts);
Assert.Single(log.Messages);
Assert.Contains("draws=3/frame", log.Messages[0]);
Assert.Contains("visible=3", log.Messages[0]);
Assert.Contains("loaded=5", log.Messages[0]);
Assert.Contains("capacity=8", log.Messages[0]);
}
[Fact]
public void RenderSignature_LogsOnlyChangesAndCarriesStableCount()
{
var log = new RecordingLog();
var diagnostics = new WorldRenderDiagnostics(new RecordingGlStateReader(), log);
EmitMinimalSignature(diagnostics, "flat");
EmitMinimalSignature(diagnostics, "flat");
EmitMinimalSignature(diagnostics, "pview");
Assert.Collection(
log.Messages,
first => Assert.StartsWith("[render-sig] frame=1 stable=0 branch=flat", first),
changed => Assert.StartsWith("[render-sig] frame=3 stable=1 branch=pview", changed));
}
private static RenderGlStateSnapshot State(int depthFunction) => new(
DepthTest: true,
DepthWrite: true,
DepthFunction: depthFunction,
Blend: false,
BlendSource: 1,
BlendDestination: 0,
CullFace: true,
CullMode: 0x405,
FrontFace: 0x900,
Scissor: false,
ScissorBox: new IntRenderRectangle(0, 0, 1280, 720),
Viewport: new IntRenderRectangle(0, 0, 1280, 720),
DrawFramebuffer: 0,
AlphaToCoverage: false,
Stencil: false,
ClipBits: 0,
Error: 0);
private sealed class RecordingGlStateReader : IRenderGlStateReader
{
public RenderGlStateSnapshot State { get; set; } = WorldRenderDiagnosticsTests.State(0x201);
public RenderGlScissorSnapshot Scissor { get; set; }
public int StateCaptureCount { get; private set; }
public int ScissorCaptureCount { get; private set; }
public RenderGlStateSnapshot CaptureState()
{
StateCaptureCount++;
return State;
}
public RenderGlScissorSnapshot CaptureScissor()
{
ScissorCaptureCount++;
return Scissor;
}
}
private sealed class RecordingLog : IRenderFrameDiagnosticLog
{
public List<string> Messages { get; } = [];
public void WriteLine(string message) => Messages.Add(message);
string message = Assert.Single(log.Messages);
Assert.Contains("draws=3/frame", message);
Assert.Contains("visible=3", message);
Assert.Contains("loaded=5", message);
Assert.Contains("capacity=8", message);
}
private sealed class ThrowOnceLog : IRenderFrameDiagnosticLog
@ -207,33 +42,4 @@ public sealed class WorldRenderDiagnosticsTests
Messages.Add(message);
}
}
private static void EmitMinimalSignature(
WorldRenderDiagnostics diagnostics,
string branch) =>
diagnostics.EmitRenderSignatureIfChanged(
enabled: true,
branch: branch,
clipRoot: null,
viewerRoot: null,
playerRoot: null,
viewerCellId: 0,
playerCellId: 0,
playerIndoorGate: false,
cameraInsideCell: false,
renderSkyGate: false,
drawSkyThisFrame: false,
terrainDrawn: false,
terrainClipMode: TerrainClipMode.Skip,
skyDrawn: false,
depthClear: false,
outdoorSceneryDrawn: false,
liveDynamicDrawnCount: 0,
sceneParticles: "none",
visibleCells: null,
clipAssembly: null,
drawableCells: null,
partition: null,
cameraPosition: Vector3.Zero,
playerPosition: Vector3.Zero);
}

View file

@ -198,7 +198,6 @@ public sealed class WorldSceneRendererTests
"passes:disable-clip",
"particles:global",
"flat:weather",
"diagnostics:signature:OutdoorRoot",
"diagnostics:draw",
"alpha:end",
"visibility:complete",
@ -238,10 +237,8 @@ public sealed class WorldSceneRendererTests
"passes:begin",
"pview:draw",
"visibility:mark",
"diagnostics:pview",
"passes:disable-clip",
"particles:none",
"diagnostics:signature:RetailPViewInside",
"particles:skipped",
"diagnostics:draw",
"alpha:end",
"visibility:complete",
@ -307,7 +304,7 @@ public sealed class WorldSceneRendererTests
}
[Fact]
public void OutdoorPView_UsesPViewOwnersForPostWorldParticlesWithoutFlatWeather()
public void OutdoorPView_SkipsPostWorldParticleReplayAndFlatWeather()
{
var root = new LoadedCell
{
@ -319,7 +316,7 @@ public sealed class WorldSceneRendererTests
WorldRenderFrameOutcome result = rig.Renderer.Render(default);
Assert.True(result.NormalWorldDrawn);
Assert.Contains("particles:pviewScoped", rig.Calls);
Assert.Contains("particles:skipped", rig.Calls);
Assert.DoesNotContain("flat:weather", rig.Calls);
}
@ -498,7 +495,6 @@ public sealed class WorldSceneRendererTests
Type[] borrowedTypes =
[
typeof(RetailPViewFrameResult),
typeof(PortalVisibilityFrame),
typeof(ClipFrameAssembly),
];
@ -690,9 +686,7 @@ public sealed class WorldSceneRendererTests
new WbFrustum());
_clipFrame = ClipFrame.NoClip();
var glDiagnostics = new WorldRenderDiagnostics(
new DefaultGlStateReader(),
new NullDiagnosticLog());
var glDiagnostics = new WorldRenderDiagnostics(new NullDiagnosticLog());
var terrainDiagnostics = new TerrainDrawDiagnosticsController(
enabled: false,
glDiagnostics,
@ -710,7 +704,6 @@ public sealed class WorldSceneRendererTests
particleRenderer: null,
portalDepthMask: null,
alpha,
glDiagnostics,
terrainDiagnostics);
const uint landblockId = 0xF4180000u;
@ -859,13 +852,6 @@ public sealed class WorldSceneRendererTests
}
}
private sealed class DefaultGlStateReader : IRenderGlStateReader
{
public RenderGlStateSnapshot CaptureState() => default;
public RenderGlScissorSnapshot CaptureScissor() => default;
}
private sealed class NullDiagnosticLog : IRenderFrameDiagnosticLog
{
public void WriteLine(string message) { }
@ -1093,20 +1079,13 @@ public sealed class WorldSceneRendererTests
uint? playerLandblockId,
HashSet<uint> animatedEntityIds) => calls.Add("flat:entities");
public string DrawPostWorldParticles(
public void DrawPostWorldParticles(
LoadedCell? clipRoot,
ClipFrameAssembly? clipAssembly,
in WorldCameraFrame camera,
string currentSignature)
in WorldCameraFrame camera)
{
string kind = clipRoot switch
{
null => "global",
{ IsOutdoorNode: true } => currentSignature,
_ => currentSignature,
};
string kind = clipRoot is null ? "global" : "skipped";
calls.Add($"particles:{kind}");
return kind;
}
public void DrawFlatWeather(
@ -1135,37 +1114,6 @@ public sealed class WorldSceneRendererTests
{
public CameraCellResolution CameraCellResolution => CameraCellResolution.None;
public void EmitPViewInput(
IReadOnlySet<uint> visibleCells,
int outsideViewCount,
Matrix4x4 viewProjection,
LoadedCell clipRoot,
Vector3 cameraPosition,
Vector3 playerPosition) => calls.Add("diagnostics:pview");
public void EmitRenderSignature(
string branch,
LoadedCell? clipRoot,
LoadedCell? viewerRoot,
LoadedCell? playerRoot,
uint viewerCellId,
uint playerCellId,
bool playerIndoorGate,
bool cameraInsideCell,
bool renderSky,
bool drawSkyThisFrame,
bool terrainDrawn,
TerrainClipMode terrainClipMode,
bool skyDrawn,
bool depthClear,
bool outdoorSceneryDrawn,
int liveDynamicDrawnCount,
string sceneParticles,
RetailPViewFrameResult? pviewResult,
Vector3 cameraPosition,
Vector3 playerPosition) =>
calls.Add($"diagnostics:signature:{branch}");
public WorldSceneDiagnosticOutcome DrawAndPublish(
in WorldCameraFrame camera,
IReadOnlyList<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax)> bounds)

View file

@ -5,175 +5,24 @@ namespace AcDream.Core.Tests.Rendering;
public sealed class RenderingDiagnosticsTests
{
// Each flag-mutating test snapshots the IndoorAll state on entry and
// restores it via try/finally. RenderingDiagnostics is a process-wide
// static (env-var-initialized); without restoration a mutated state
// leaks into other tests + into parallel test runs. Mirrors the
// PhysicsDiagnosticsTests pattern at line 30-49.
[Fact]
public void IndoorAll_True_TurnsAllFlagsOn()
{
bool initial = RenderingDiagnostics.IndoorAll;
try
{
// Reset all flags off first to make the test deterministic
// regardless of env-var state on the test runner.
RenderingDiagnostics.ProbeIndoorWalkEnabled = false;
RenderingDiagnostics.ProbeIndoorLookupEnabled = false;
RenderingDiagnostics.ProbeIndoorUploadEnabled = false;
RenderingDiagnostics.ProbeIndoorXformEnabled = false;
RenderingDiagnostics.ProbeIndoorCullEnabled = false;
RenderingDiagnostics.IndoorAll = true;
Assert.True(RenderingDiagnostics.ProbeIndoorWalkEnabled);
Assert.True(RenderingDiagnostics.ProbeIndoorLookupEnabled);
Assert.True(RenderingDiagnostics.ProbeIndoorUploadEnabled);
Assert.True(RenderingDiagnostics.ProbeIndoorXformEnabled);
Assert.True(RenderingDiagnostics.ProbeIndoorCullEnabled);
Assert.True(RenderingDiagnostics.IndoorAll);
}
finally
{
RenderingDiagnostics.IndoorAll = initial;
}
}
[Fact]
public void IndoorAll_False_TurnsAllFlagsOff()
{
bool initial = RenderingDiagnostics.IndoorAll;
try
{
RenderingDiagnostics.IndoorAll = true; // start from all-on
RenderingDiagnostics.IndoorAll = false;
Assert.False(RenderingDiagnostics.ProbeIndoorWalkEnabled);
Assert.False(RenderingDiagnostics.ProbeIndoorLookupEnabled);
Assert.False(RenderingDiagnostics.ProbeIndoorUploadEnabled);
Assert.False(RenderingDiagnostics.ProbeIndoorXformEnabled);
Assert.False(RenderingDiagnostics.ProbeIndoorCullEnabled);
Assert.False(RenderingDiagnostics.IndoorAll);
}
finally
{
RenderingDiagnostics.IndoorAll = initial;
}
}
[Fact]
public void IndoorAll_OneOff_ReadsAsFalse()
{
bool initial = RenderingDiagnostics.IndoorAll;
try
{
RenderingDiagnostics.IndoorAll = true;
RenderingDiagnostics.ProbeIndoorCullEnabled = false; // flip one off
Assert.False(RenderingDiagnostics.IndoorAll);
}
finally
{
RenderingDiagnostics.IndoorAll = initial;
}
}
[Theory]
[InlineData(0x00000029ul, false)]
[InlineData(0xA9B40029ul, false)]
[InlineData(0x00000100ul, true)]
[InlineData(0xA9B40105ul, true)]
public void IsEnvCellId_DistinguishesOutdoorVsIndoorByLow16Bits(ulong id, bool expected)
=> Assert.Equal(expected, RenderingDiagnostics.IsEnvCellId(id));
[Theory]
[InlineData(0x00000029ul, false)] // outdoor cell 0x29 in 8x8 grid
[InlineData(0xA9B40029ul, false)] // outdoor cell with landblock prefix
[InlineData(0x00000100ul, true)] // indoor cell minimum
[InlineData(0x00000105ul, true)] // typical Holtburg Inn interior
[InlineData(0xA9B40105ul, true)] // indoor with landblock prefix
[InlineData(0xA9B401FFul, true)] // indoor near top of range
public void IsEnvCellId_DistinguishesOutdoorVsIndoorByLow16Bits(ulong id, bool expected)
{
Assert.Equal(expected, RenderingDiagnostics.IsEnvCellId(id));
}
// ── Render inside/outside branch (retail RenderNormalMode is_player_outside) ──
// The top-level render branch decides DrawInside vs DrawOutside. Retail
// (SmartBox::RenderNormalMode 0x453aa0:92665) keys it on is_player_outside (the
// PLAYER's cell, 0x451e80), NOT the camera cell. acdream previously branched on the
// camera cell, so a chase camera lagging in a doorway while the player was already
// outside took the DrawInside path and degenerated to a grey world + entities showing
// through walls. These pin the player-keyed branch and loaded player-root requirement.
[Fact]
public void ShouldRenderIndoor_PlayerOutside_CameraInside_ReturnsFalse()
{
// THE doorway-grey regression: the player stepped onto a landcell (0x...0031) but the
// chase camera still resolves an interior EnvCell. Branch on the PLAYER → outdoor.
Assert.False(RenderingDiagnostics.ShouldRenderIndoor(playerCellId: 0xA9B40031u, renderRootResolved: true));
}
[Fact]
public void ShouldRenderIndoor_PlayerInside_CameraInside_ReturnsTrue()
{
Assert.True(RenderingDiagnostics.ShouldRenderIndoor(playerCellId: 0xA9B40171u, renderRootResolved: true));
}
[Fact]
public void ShouldRenderIndoor_PlayerInside_RootNotLoaded_ReturnsFalse()
{
// Opposite lag (camera pulled outside while the player is inside): no viewer cell to
// root DrawInside at → outdoor. Defensive; matches prior null-CameraCell behavior.
Assert.False(RenderingDiagnostics.ShouldRenderIndoor(playerCellId: 0xA9B40171u, renderRootResolved: false));
}
[Fact]
public void ShouldRenderIndoor_PlayerOutside_CameraOutside_ReturnsFalse()
{
Assert.False(RenderingDiagnostics.ShouldRenderIndoor(playerCellId: 0xA9B40031u, renderRootResolved: false));
}
[Fact]
public void ShouldRenderIndoor_UnknownPlayerCell_TreatedAsOutside_ReturnsFalse()
{
// playerCellId == 0 (unresolved) → treat as outside (safe default: outdoor render).
Assert.False(RenderingDiagnostics.ShouldRenderIndoor(playerCellId: 0u, renderRootResolved: true));
}
[Fact]
public void ProbePortalChurn_DefaultsFalse_WhenEnvUnset()
{
// Env var is absent in the test host, so the flag must default false (inert probe).
Assert.False(AcDream.Core.Rendering.RenderingDiagnostics.ProbePortalChurnEnabled);
}
// ── ACDREAM_DUMP_ENTITY parser (#119 decisive probe) ──────────────────
[Fact]
public void ParseDumpEntityIds_NullOrEmpty_ReturnsEmptySet()
{
Assert.Empty(RenderingDiagnostics.ParseDumpEntityIds(null));
Assert.Empty(RenderingDiagnostics.ParseDumpEntityIds(""));
Assert.Empty(RenderingDiagnostics.ParseDumpEntityIds(" "));
}
[Fact]
public void ParseDumpEntityIds_CommaSeparatedWithPrefixes_ParsesAll()
{
var set = RenderingDiagnostics.ParseDumpEntityIds("0x020003F2,0x020005D8");
Assert.Equal(2, set.Count);
Assert.Contains(0x020003F2u, set);
Assert.Contains(0x020005D8u, set);
}
[Fact]
public void ParseDumpEntityIds_NoPrefixMixedCaseAndWhitespace_Parses()
{
var set = RenderingDiagnostics.ParseDumpEntityIds(" 020003f2 , 0X020005D8 ");
Assert.Equal(2, set.Count);
Assert.Contains(0x020003F2u, set);
Assert.Contains(0x020005D8u, set);
}
[Fact]
public void ParseDumpEntityIds_MalformedSegment_IgnoredNotFatal()
{
// A typo'd segment must not take the launch down — parse what's valid.
var set = RenderingDiagnostics.ParseDumpEntityIds("0x020003F2,zzz,,0x");
Assert.Single(set);
Assert.Contains(0x020003F2u, set);
}
[InlineData(0xA9B40031u, true, false)]
[InlineData(0xA9B40171u, true, true)]
[InlineData(0xA9B40171u, false, false)]
[InlineData(0u, true, false)]
public void ShouldRenderIndoor_UsesPlayerCellAndResolvedRoot(
uint playerCellId,
bool renderRootResolved,
bool expected)
=> Assert.Equal(
expected,
RenderingDiagnostics.ShouldRenderIndoor(playerCellId, renderRootResolved));
}

View file

@ -1,145 +0,0 @@
// Phase A8 — visibility probe flag tests.
// Phase U.2d (2026-05-30) — EmitVis formatter + cell-change gating tests.
using System;
using System.Collections.Generic;
using System.IO;
using AcDream.Core.Rendering;
using Xunit;
namespace AcDream.Core.Tests.Rendering;
public class RenderingDiagnosticsVisibilityTests
{
// RenderingDiagnostics is a process-wide static (env-var-initialized) and
// EmitVis keeps a private last-root-cell field. Both leak between tests +
// parallel runs if not restored, and this codebase has documented
// static-leak flakiness — so every test here snapshots ProbeVisibilityEnabled,
// resets the cell tracker, and restores in finally. Mirrors the
// RenderingDiagnosticsTests / PhysicsDiagnosticsTests pattern.
[Fact]
public void ProbeVisibilityEnabled_CanBeToggled()
{
var prev = RenderingDiagnostics.ProbeVisibilityEnabled;
try
{
RenderingDiagnostics.ProbeVisibilityEnabled = true;
Assert.True(RenderingDiagnostics.ProbeVisibilityEnabled);
RenderingDiagnostics.ProbeVisibilityEnabled = false;
Assert.False(RenderingDiagnostics.ProbeVisibilityEnabled);
}
finally
{
RenderingDiagnostics.ProbeVisibilityEnabled = prev;
}
}
[Fact]
public void ProbeVisibilityEnabled_DefaultsToEnvVarPresence()
{
// The static initializer reads ACDREAM_PROBE_VIS == "1". Reconstruct the
// documented default from the current env and assert the property's
// *defined* default matches it. (We can't re-run the static initializer,
// but the initial value is a pure function of the env var, so this pins
// the documented contract without depending on the runner's env.)
bool expected = Environment.GetEnvironmentVariable("ACDREAM_PROBE_VIS") == "1";
var prev = RenderingDiagnostics.ProbeVisibilityEnabled;
try
{
RenderingDiagnostics.ResetVisibilityProbeForTests();
// Drive the property to the env-derived default explicitly, then read
// it back — this asserts the property faithfully stores the env value
// (the same expression the field initializer uses).
RenderingDiagnostics.ProbeVisibilityEnabled = expected;
Assert.Equal(expected, RenderingDiagnostics.ProbeVisibilityEnabled);
}
finally
{
RenderingDiagnostics.ProbeVisibilityEnabled = prev;
}
}
[Fact]
public void EmitVis_NoOp_WhenProbeDisabled()
{
var prev = RenderingDiagnostics.ProbeVisibilityEnabled;
var prevOut = Console.Out;
try
{
RenderingDiagnostics.ResetVisibilityProbeForTests();
RenderingDiagnostics.ProbeVisibilityEnabled = false;
using var sw = new StringWriter();
Console.SetOut(sw);
RenderingDiagnostics.EmitVis(
rootCellId: 0xA9B40105u,
visibleCells: new uint[] { 0xA9B40105u, 0xA9B40164u },
outsidePolyCount: 1,
outsidePlaneCount: 4,
perCellPlaneCounts: new Dictionary<uint, int> { [0xA9B40105u] = 0, [0xA9B40164u] = 4 },
scissorFallbacks: 1);
Console.SetOut(prevOut);
Assert.Equal(string.Empty, sw.ToString());
}
finally
{
Console.SetOut(prevOut);
RenderingDiagnostics.ProbeVisibilityEnabled = prev;
RenderingDiagnostics.ResetVisibilityProbeForTests();
}
}
[Fact]
public void EmitVis_FiresOnceOnNewRoot_SuppressedOnUnchangedRoot()
{
var prev = RenderingDiagnostics.ProbeVisibilityEnabled;
var prevOut = Console.Out;
try
{
RenderingDiagnostics.ResetVisibilityProbeForTests();
RenderingDiagnostics.ProbeVisibilityEnabled = true;
using var sw = new StringWriter();
Console.SetOut(sw);
var cells = new uint[] { 0xA9B40105u, 0xA9B40164u };
var perCell = new Dictionary<uint, int> { [0xA9B40105u] = 0, [0xA9B40164u] = 4 };
// First call on a fresh root → fires.
RenderingDiagnostics.EmitVis(0xA9B40105u, cells, 1, 4, perCell, 0);
// Same root again → suppressed (no-op).
RenderingDiagnostics.EmitVis(0xA9B40105u, cells, 1, 4, perCell, 0);
// New root → fires again.
RenderingDiagnostics.EmitVis(0xA9B40164u, cells, 2, 8, perCell, 1);
Console.SetOut(prevOut);
string output = sw.ToString();
string[] lines = output.Split('\n', StringSplitOptions.RemoveEmptyEntries);
// Exactly two [vis] lines: one per distinct root transition.
Assert.Equal(2, lines.Length);
Assert.All(lines, l => Assert.StartsWith("[vis]", l.TrimStart()));
Assert.Contains("root=0xA9B40105", lines[0]);
Assert.Contains("root=0xA9B40164", lines[1]);
// Information-density spot checks on the first line.
Assert.Contains("cells=2", lines[0]);
Assert.Contains("0xA9B40105", lines[0]);
Assert.Contains("0xA9B40164", lines[0]);
Assert.Contains("polys=1", lines[0]);
Assert.Contains("planes=4", lines[0]);
Assert.Contains("fallbacks=0", lines[0]);
}
finally
{
Console.SetOut(prevOut);
RenderingDiagnostics.ProbeVisibilityEnabled = prev;
RenderingDiagnostics.ResetVisibilityProbeForTests();
}
}
}