checkpoint: preserve cathedral look-in investigation state

This commit is contained in:
Erik 2026-08-30 23:29:28 +02:00
parent c287db8651
commit 572de1ec30
11 changed files with 757 additions and 197 deletions

View file

@ -315,7 +315,7 @@ issue is closed, the strip was missed; delete both.
| `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_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-disp]` / `[clip-route-scis]` lines: outside-slice clip routing, region-SSBO bytes, terrain-UBO head, actual GL/RHI scissor state | print-only | `RenderingDiagnostics.ProbeClipRouteEnabled` | | `ACDREAM_PROBE_CLIPROUTE` | "throwaway apparatus — strip once §4 ships" | `=1` | print-on-change `[clip-route]` / `[clip-route-disp]` / `[clip-route-scis]` lines: outside-slice clip routing, region-SSBO bytes, terrain-UBO head, actual GL/RHI scissor state | print-only | `RenderingDiagnostics.ProbeClipRouteEnabled` |
| `ACDREAM_PROBE_WALK_SHADOW` | Campaign FW3.2b-2 flip apparatus — dies with the flip commit | `=1` | runs the PRODUCTION retail frame walk per frame in SHADOW (no draws change) and prints one `[walk-shadow]` line per frame whose visited cell/building sets diverge from the old visibility path (the I5 dual-shadow pattern applied to the FW3 static cutover) | print-only; adds the per-frame CPU cost of the shadow walk while set | `RenderingDiagnostics.ProbeWalkShadowEnabled` | | `ACDREAM_PROBE_WALK_SHADOW` | Campaign FW3.2b-2 flip apparatus — dies with the flip commit | `=1` | runs the PRODUCTION retail frame walk per frame in SHADOW (no draws change) and prints one `[walk-shadow]` line per frame whose visited cell/building sets diverge from the old visibility path (the I5 dual-shadow pattern applied to the FW3 static cutover) | print-only; adds the per-frame CPU cost of the shadow walk while set | `RenderingDiagnostics.ProbeWalkShadowEnabled` |
| `ACDREAM_PROBE_WALK_ROOT` | the cathedral Phase I instrumentation (synthesis doc `docs/research/2026-08-30-cathedral-synthesis.md`) — dies with that investigation | `=1` | prints `[walk-root]` rooting lines every ~30th frame AND for 8 frames after each interior/outdoor root flip (viewer/root cell, camera-cell resolution, slice count, the walk's OWN exit-view count, the TRUE root flood separated from the visited union, eye + forward); `[walk-cam]` replayable camera dumps (eye, viewport, full view-projection) every ~300 frames and at flips; `[falls]` lines on EVERY particle submission carrying a real falls owner (0xCF418000-13) with route + phase; `[falls-adm]` admission verdicts for F418 emitters (~every 120th ApplyRetailView); plus the earlier family — `[walk-part]` per-route submissions, `[walk-alpha]` drains, `[walk-dyn]` outside-stage verdicts (~120th product frame), `[walk-portal]` per-portal side/clip decisions (~90th frame), and `[dyn-route]` per-entity ON-CHANGE DynamicLast admission traces (parent cell, look-in exclusion, viewcone verdict, the cell's lifted-plane shape — "0" marks a pass-all zero-plane slice) | print-only; chatty near particle-heavy scenes while set | `RenderingDiagnostics.ProbeWalkRootEnabled` | | `ACDREAM_PROBE_WALK_ROOT` | the cathedral Phase I/FW4 instrumentation (synthesis doc `docs/research/2026-08-30-cathedral-synthesis.md`) — dies with that investigation | `=1` | prints `[walk-root]` rooting lines every ~30th frame AND for 8 frames after each interior/outdoor root flip (viewer/root cell, camera-cell resolution, slice count, the walk's OWN exit-view count, the TRUE root flood separated from the visited union, eye + forward); `[walk-cam]` replayable camera dumps (eye, viewport, full view-projection) every ~300 frames and at flips; `[lookin-frame]` ON-CHANGE/~30-frame comparisons of diagnostic-only legacy look-in frames against the walk's exact look-in cells, explicit 0xF4180112 membership, and walk-vs-legacy punch counts; `[lookin-draw]` identifies the walk-turn route index/cell actually submitted; `[lookin-punch]` fires if the legacy punch routine executes; `[falls]` lines on EVERY particle submission carrying a real falls owner (0xCF418000-13) with route + phase; `[falls-adm]` admission verdicts for F418 emitters (~every 120th ApplyRetailView); plus the earlier family — `[walk-part]` per-route submissions, `[walk-alpha]` drains, `[walk-dyn]` outside-stage verdicts (~120th product frame), `[walk-portal]` per-portal side/clip decisions (~90th frame), and `[dyn-route]` per-entity ON-CHANGE DynamicLast admission traces (parent cell, look-in exclusion, viewcone verdict, the cell's lifted-plane shape — "0" marks a pass-all zero-plane slice) | print-only; chatty near particle-heavy scenes while set | `RenderingDiagnostics.ProbeWalkRootEnabled` |
| `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_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_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_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` |

View file

@ -1,4 +1,5 @@
using System.Numerics; using System.Numerics;
using AcDream.App.Rendering.Scene;
using AcDream.App.Rendering.Walk; using AcDream.App.Rendering.Walk;
using AcDream.Core.Vfx; using AcDream.Core.Vfx;
@ -151,6 +152,8 @@ internal sealed partial class RetailPViewPassExecutor
int count = Math.Min(vertices.Length, world.Length); int count = Math.Min(vertices.Length, world.Length);
for (int vertex = 0; vertex < count; vertex++) for (int vertex = 0; vertex < count; vertex++)
world[vertex] = vertices[vertex]; world[vertex] = vertices[vertex];
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeWalkRootEnabled)
WalkLookInPunchCountThisFrame++;
_portalDepthMask.DrawDepthFan( _portalDepthMask.DrawDepthFan(
world[..count], world[..count],
frame.ViewProjection, frame.ViewProjection,
@ -201,6 +204,9 @@ internal sealed class WalkProductionLeafRenderer : IWalkFrameLeafRenderer
private readonly Action _drawExitSeals; private readonly Action _drawExitSeals;
private readonly HashSet<uint> _singleCellScratch = new(); private readonly HashSet<uint> _singleCellScratch = new();
private readonly List<uint> _singleCellListScratch = new(); private readonly List<uint> _singleCellListScratch = new();
private readonly HashSet<uint> _lookInParticleOwnerScratch = new();
private RenderFrameView _entityFrame;
private bool _entityFrameBound;
internal WalkProductionLeafRenderer( internal WalkProductionLeafRenderer(
RetailPViewPassExecutor passes, RetailPViewPassExecutor passes,
@ -242,6 +248,55 @@ internal sealed class WalkProductionLeafRenderer : IWalkFrameLeafRenderer
_passes.DrawLandscapeStaticParticles( _passes.DrawLandscapeStaticParticles(
_frame, new RetailPViewLandscapeStaticParticleContext(ownerIds)); _frame, new RetailPViewLandscapeStaticParticleContext(ownerIds));
/// <summary>Binds the packed frame product built after the walk's Collect
/// pass and before Replay. The walk records route indices during Collect;
/// Replay consumes the matching product ranges through this exact view.</summary>
internal void BindEntityFrame(in RenderFrameView view)
{
_entityFrame = view;
_entityFrameBound = true;
}
public void DrawLookInDynamics(
uint cellId,
int routeIndex,
IReadOnlySet<uint> staticParticleOwnerIds)
{
if (!_entityFrameBound)
{
throw new InvalidOperationException(
"Walk look-in dynamics replayed before the packed entity frame was bound.");
}
_passes.UseIndoorMembershipOnlyRouting();
_passes.DrawEntityRoute(
_frame.Camera,
in _entityFrame,
RenderFrameCandidateRoute.LookInObject,
routeIndex,
cellId,
_frame.PlayerLandblockId ?? 0u);
_lookInParticleOwnerScratch.Clear();
_lookInParticleOwnerScratch.UnionWith(staticParticleOwnerIds);
RenderFrameRouteOwnerSelector.Union(
_lookInParticleOwnerScratch,
in _entityFrame,
RenderFrameCandidateRoute.LookInObject,
routeIndex,
cellId);
if (_lookInParticleOwnerScratch.Count > 0)
{
_passes.DrawCellParticles(
_frame,
new RetailPViewCellSliceContext(
cellId,
default,
_lookInParticleOwnerScratch));
}
}
public void DrawExitSeals() => _drawExitSeals(); public void DrawExitSeals() => _drawExitSeals();
public void DrawPunchFan(WalkPolygon worldPolygon, int activeViewIndex) => public void DrawPunchFan(WalkPolygon worldPolygon, int activeViewIndex) =>

View file

@ -138,6 +138,13 @@ internal sealed partial class RetailPViewPassExecutor :
private readonly Dictionary<uint, int> _singleCellClipRouting = new(1); private readonly Dictionary<uint, int> _singleCellClipRouting = new(1);
private readonly Dictionary<uint, int> _noCellClipRouting = new(0); private readonly Dictionary<uint, int> _noCellClipRouting = new(0);
// ACDREAM_PROBE_WALK_ROOT / cathedral FW4: observation-only counters.
// They distinguish the walk-owned far-Z portal punches from the retired
// legacy look-in punch route. RetailPViewRenderer samples them for the
// diagnostic comparison after the walk-turn look-in routes have replayed.
internal int WalkLookInPunchCountThisFrame { get; private set; }
internal int LegacyLookInPunchCountThisFrame { get; private set; }
/// <summary> /// <summary>
/// Borrowed until the next late landscape pass. The outdoor-root post-world /// Borrowed until the next late landscape pass. The outdoor-root post-world
/// particle pass consumes this synchronously before another PView frame. /// particle pass consumes this synchronously before another PView frame.
@ -181,7 +188,11 @@ internal sealed partial class RetailPViewPassExecutor :
{ {
_particleClassifications.BeginFrame(); _particleClassifications.BeginFrame();
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeWalkRootEnabled) if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeWalkRootEnabled)
{
AcDream.Core.Rendering.RenderingDiagnostics.WalkRootPhase = "pre"; AcDream.Core.Rendering.RenderingDiagnostics.WalkRootPhase = "pre";
WalkLookInPunchCountThisFrame = 0;
LegacyLookInPunchCountThisFrame = 0;
}
} }
/// <summary>Campaign FW3.2b-2: the shared dispatcher, for /// <summary>Campaign FW3.2b-2: the shared dispatcher, for
@ -596,8 +607,19 @@ internal sealed partial class RetailPViewPassExecutor :
public void DrawLookInPortalPunch( public void DrawLookInPortalPunch(
RetailPViewFrameInput frame, RetailPViewFrameInput frame,
RetailPViewCellSliceContext context, RetailPViewCellSliceContext context,
int portalIndex) => int portalIndex)
{
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeWalkRootEnabled)
{
LegacyLookInPunchCountThisFrame++;
Console.WriteLine(
$"[lookin-punch] source=legacy viewer=0x{frame.ViewerCellId:X8} "
+ $"root=0x{frame.RootCell.CellId:X8} cell=0x{context.CellId:X8} "
+ $"portal={portalIndex} planes={context.Slice.Planes.Length} "
+ $"phase={AcDream.Core.Rendering.RenderingDiagnostics.WalkRootPhase}");
}
DrawPortalDepthWrite(context, frame, forceFarZ: true, portalIndex); DrawPortalDepthWrite(context, frame, forceFarZ: true, portalIndex);
}
public void DrawUnattachedSceneParticles( public void DrawUnattachedSceneParticles(
RetailPViewFrameInput frame, RetailPViewFrameInput frame,

View file

@ -110,6 +110,12 @@ public sealed class RetailPViewRenderer
private bool? _probeWalkRootPrevOutdoor; private bool? _probeWalkRootPrevOutdoor;
private int _probeWalkRootFramesLeft; private int _probeWalkRootFramesLeft;
private ulong _probeWalkRootFrame; private ulong _probeWalkRootFrame;
private string? _probeLookInState;
// The parked remote player's authoritative parent cell for the current
// cathedral FW4 investigation. This is diagnostic scope only: it changes
// no visibility or draw decision.
private const uint ProbeCathedralRemoteCellId = 0xF4180112u;
// Campaign FW3.2b-2: the walk's production world-data registries // Campaign FW3.2b-2: the walk's production world-data registries
// (published/retired by LandblockRenderPublisher) plus the per-frame // (published/retired by LandblockRenderPublisher) plus the per-frame
@ -263,6 +269,7 @@ public sealed class RetailPViewRenderer
// runs at append time, not at Replay time), so the world data must // runs at append time, not at Replay time), so the world data must
// already be rebuilt for this frame before the walk starts. // already be rebuilt for this frame before the walk starts.
Walk.WalkFrameDriver? walkDriver = null; Walk.WalkFrameDriver? walkDriver = null;
WalkProductionLeafRenderer? walkLeafRenderer = null;
if (walkActive) if (walkActive)
{ {
Matrix4x4 view = ctx.CameraView; Matrix4x4 view = ctx.CameraView;
@ -343,9 +350,12 @@ public sealed class RetailPViewRenderer
Action drawExitSeals = () => Action drawExitSeals = () =>
DrawWalkExitPortalMasks(ctx, passes, clipAssembly, walkDriver!); DrawWalkExitPortalMasks(ctx, passes, clipAssembly, walkDriver!);
var leafRenderer = new WalkProductionLeafRenderer( walkLeafRenderer = new WalkProductionLeafRenderer(
walkExecutor!, ctx, clipAssembly, clearInteriorDepth, drawExitSeals); walkExecutor!, ctx, clipAssembly, clearInteriorDepth, drawExitSeals);
walkDriver = new Walk.WalkFrameDriver(walkExecutor!.Dispatcher, leafRenderer, _walkWorldData); walkDriver = new Walk.WalkFrameDriver(
walkExecutor!.Dispatcher,
walkLeafRenderer,
_walkWorldData);
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeWalkRootEnabled) if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeWalkRootEnabled)
{ {
@ -527,7 +537,8 @@ public sealed class RetailPViewRenderer
pvFrame, pvFrame,
clipAssembly, clipAssembly,
viewcone, viewcone,
_lookInFrames, walkActive ? walkDriver!.LookInCellTurns : [],
walkActive ? walkDriver : null,
outsideStageFlood, outsideStageFlood,
ctx.Cells, ctx.Cells,
ctx.AnimatedEntityIds, ctx.AnimatedEntityIds,
@ -539,6 +550,8 @@ public sealed class RetailPViewRenderer
frameViewBorrowed = true; frameViewBorrowed = true;
frameEntityPasses!.BeginEntityFrame(in frameView); frameEntityPasses!.BeginEntityFrame(in frameView);
entityFrameOpen = true; entityFrameOpen = true;
if (walkActive)
walkLeafRenderer!.BindEntityFrame(in frameView);
} }
// The retained scene product is the production object source. // The retained scene product is the production object source.
@ -651,6 +664,7 @@ public sealed class RetailPViewRenderer
clipAssembly, clipAssembly,
capturedPartition, capturedPartition,
capturedViewcone, capturedViewcone,
capturedDriver,
capturedPasses, capturedPasses,
in capturedView); in capturedView);
}; };
@ -666,6 +680,7 @@ public sealed class RetailPViewRenderer
clipAssembly, clipAssembly,
partition, partition,
viewcone, viewcone,
walkDriver!,
frameEntityPasses, frameEntityPasses,
in frameView); in frameView);
} }
@ -1404,7 +1419,7 @@ public sealed class RetailPViewRenderer
_cellParticleOwnerScratch.Clear(); _cellParticleOwnerScratch.Clear();
foreach (uint cellId in driver.VisitedCells) foreach (uint cellId in driver.VisitedCells)
{ {
if (_lookInCellIds.Contains(cellId)) if (driver.LookInCells.Contains(cellId))
continue; continue;
UnionRecordOwners(_walkWorldData!.GetCellStatics(cellId), _cellParticleOwnerScratch); UnionRecordOwners(_walkWorldData!.GetCellStatics(cellId), _cellParticleOwnerScratch);
} }
@ -1431,15 +1446,16 @@ public sealed class RetailPViewRenderer
/// split — the walk now draws every STATIC route (see /// split — the walk now draws every STATIC route (see
/// <see cref="DrawWalkDrivenStatics"/>); this method keeps ONLY what /// <see cref="DrawWalkDrivenStatics"/>); this method keeps ONLY what
/// stays on the OLD visibility pipeline per the plan's dual-compute /// stays on the OLD visibility pipeline per the plan's dual-compute
/// split: outdoor-cell unattached particles, LookInObject dynamics + their /// split: outdoor-cell unattached particles, the late per-slice outside-
/// per-cell particles, the late per-slice outside-dynamics/weather loop, /// dynamics/weather loop, and the late particle union submission. Look-in
/// and the late particle union submission.</summary> /// dynamics and their cell particles are now walk-turn-owned.</summary>
private void DrawLandscapeDynamicsPhase( private void DrawLandscapeDynamicsPhase(
RetailPViewFrameInput ctx, RetailPViewFrameInput ctx,
IRetailPViewPassExecutor passes, IRetailPViewPassExecutor passes,
ClipFrameAssembly clipAssembly, ClipFrameAssembly clipAssembly,
InteriorEntityPartition.Result? partition, InteriorEntityPartition.Result? partition,
ViewconeCuller viewcone, ViewconeCuller viewcone,
Walk.WalkFrameDriver walkDriver,
IRenderFrameEntityPassExecutor? frameEntityPasses, IRenderFrameEntityPassExecutor? frameEntityPasses,
in RenderFrameView frameView) in RenderFrameView frameView)
{ {
@ -1452,8 +1468,10 @@ public sealed class RetailPViewRenderer
// (the walk owns its own alpha barriers — WalkFrameDriver.OnBuildingTurn). // (the walk owns its own alpha barriers — WalkFrameDriver.OnBuildingTurn).
passes.DrawUnattachedSceneParticles(ctx, outdoorCells: true); passes.DrawUnattachedSceneParticles(ctx, outdoorCells: true);
DrawBuildingLookInDynamics( // Trace-only now. Look-in dynamics no longer have a late production
ctx, passes, clipAssembly, partition, frameEntityPasses, in frameView); // phase: WalkFrameDriver replays each packed route at the walk's own
// per-cell DrawCells turn, before the enclosing building shell.
ProbeBuildingLookInFrames(ctx, passes, walkDriver);
// LATE phase (per slice): outside-stage dynamics' meshes + weather — // LATE phase (per slice): outside-stage dynamics' meshes + weather —
// unchanged from DrawLandscapeThroughOutsideView's own late loop. // unchanged from DrawLandscapeThroughOutsideView's own late loop.
@ -1527,129 +1545,72 @@ public sealed class RetailPViewRenderer
passes.UseIndoorMembershipOnlyRouting(); passes.UseIndoorMembershipOnlyRouting();
} }
/// <summary>Campaign FW3.2b-2: the DYNAMICS-only remainder of the old /// <summary>ACDREAM_PROBE_WALK_ROOT companion for FW4's surviving
/// <see cref="DrawBuildingLookIns"/> — punches, shells, and look-in cell /// through-wall dynamic. It compares the now-diagnostic-only legacy
/// STATICS are now walk-owned (<see cref="Walk.WalkFrameDriver"/>'s /// look-in frames with the production walk's exact look-in set and reports
/// Building/BuildingShell/LookInStatic turns); this method keeps ONLY the /// the two punch producers separately. Print-only; never participates in
/// LookInObject route (now dynamic-classified — see /// admission.</summary>
/// <c>RenderScenePViewFrameBuilder.BuildLookInRoutes</c>) and the per-cell private void ProbeBuildingLookInFrames(
/// particle union that route's owners feed, unioned with the walk's
/// static owners for that SAME cell (plan §FW3 item 4 — GetCellStatics
/// fills the gap the retired CellStatic-route particle submission left
/// for look-in cells specifically).</summary>
private void DrawBuildingLookInDynamics(
RetailPViewFrameInput ctx, RetailPViewFrameInput ctx,
IRetailPViewPassExecutor passes, IRetailPViewPassExecutor passes,
ClipFrameAssembly clipAssembly, Walk.WalkFrameDriver walkDriver)
InteriorEntityPartition.Result? partition,
IRenderFrameEntityPassExecutor? frameEntityPasses,
in RenderFrameView frameView)
{ {
if (_lookInFrames.Count == 0) if (!AcDream.Core.Rendering.RenderingDiagnostics.ProbeWalkRootEnabled)
return; return;
int lookInRouteIndex = 0; uint[] walkLookInCells = walkDriver.LookInCells
for (int frameIndex = 0; frameIndex < _lookInFrames.Count; frameIndex++) .OrderBy(cellId => cellId)
.ToArray();
bool legacyHasTarget = _lookInFrames.Any(
frame => frame.OrderedVisibleCells.Contains(ProbeCathedralRemoteCellId));
bool walkHasTarget = Array.IndexOf(
walkLookInCells, ProbeCathedralRemoteCellId) >= 0;
string legacyShape = string.Join(
";",
_lookInFrames.Select((frame, index) =>
$"{index}:b{frame.SourceBuildingKey:x8}/lb{frame.SourceBuildingLandblockId:x8}"
+ $"/s{frame.ExteriorSeedPortals.Count}/c["
+ string.Join(",", frame.OrderedVisibleCells.Select(cell => cell.ToString("x8")))
+ "]"));
string walkShape = string.Join(",", walkLookInCells.Select(cell => cell.ToString("x8")));
int walkPunches = passes is RetailPViewPassExecutor concrete
? concrete.WalkLookInPunchCountThisFrame
: -1;
int legacyPunches = passes is RetailPViewPassExecutor concreteLegacy
? concreteLegacy.LegacyLookInPunchCountThisFrame
: -1;
string state =
$"viewer={ctx.ViewerCellId:x8}|root={ctx.RootCell.CellId:x8}"
+ $"|phase={AcDream.Core.Rendering.RenderingDiagnostics.WalkRootPhase}"
+ $"|legacy={legacyShape}|walk={walkShape}"
+ $"|p={walkPunches}/{legacyPunches}";
bool emit = state != _probeLookInState || _probeWalkRootFrame % 30 == 0;
_probeLookInState = state;
if (!emit)
return;
Console.WriteLine(
$"[lookin-frame] f={_probeWalkRootFrame} "
+ $"viewer=0x{ctx.ViewerCellId:X8} root=0x{ctx.RootCell.CellId:X8} "
+ $"phase={AcDream.Core.Rendering.RenderingDiagnostics.WalkRootPhase} "
+ $"legacyFrames={_lookInFrames.Count} legacy112={(legacyHasTarget ? 1 : 0)} "
+ $"walkLookIn={walkLookInCells.Length} walk112={(walkHasTarget ? 1 : 0)} "
+ $"punches=walk:{walkPunches},legacy:{legacyPunches}");
for (int index = 0; index < _lookInFrames.Count; index++)
{ {
PortalVisibilityFrame frame = _lookInFrames[frameIndex]; PortalVisibilityFrame frame = _lookInFrames[index];
for (int i = frame.OrderedVisibleCells.Count - 1; i >= 0; i--) Console.WriteLine(
{ $"[lookin-frame] f={_probeWalkRootFrame} index={index} "
uint cellId = frame.OrderedVisibleCells[i]; + $"building=0x{frame.SourceBuildingKey:X8} "
var clipKey = new LookInClipCell(frameIndex, cellId); + $"landblock=0x{frame.SourceBuildingLandblockId:X8} "
if (!clipAssembly.LookInCellToViewSlices.TryGetValue( + $"seeds={frame.ExteriorSeedPortals.Count} "
clipKey, + $"has112={(frame.OrderedVisibleCells.Contains(ProbeCathedralRemoteCellId) ? 1 : 0)} "
out ClipViewSlice[]? cellSlices) + $"cells=[{string.Join(",", frame.OrderedVisibleCells.Select(cell => $"0x{cell:X8}"))}]");
|| cellSlices.Length == 0)
{
continue;
}
_cellStaticScratch.Clear();
if (partition is not null)
{
foreach (var e in partition.Dynamics)
if (e.ParentCellId == cellId)
_cellStaticScratch.Add(e);
}
bool cellDrewObjects = false;
_cellParticleUnionScratch.Clear();
foreach (ClipViewSlice slice in cellSlices)
{
int routeIndex = lookInRouteIndex++;
passes.UseCellPortalViewRouting(cellId, slice);
if (frameEntityPasses is not null)
{
RenderFrameRouteOwnerSelector.Replace(
_cellParticleOwnerScratch,
in frameView,
RenderFrameCandidateRoute.LookInObject,
routeIndex,
cellId);
}
else
{
ReplaceOwnerIds(
_cellParticleOwnerScratch,
_cellStaticScratch);
}
if (frameEntityPasses is not null
|| _cellStaticScratch.Count > 0)
{
_candidateObserver?.ObservePViewBucket(
CurrentRenderPViewRoute.LookInObject,
routeIndex,
cellId,
_cellStaticScratch);
_oneCell.Clear();
_oneCell.Add(cellId);
DrawEntityRouteOrLegacy(
ctx,
passes,
frameEntityPasses,
in frameView,
RenderFrameCandidateRoute.LookInObject,
routeIndex,
cellId,
_cellStaticScratch,
_oneCell);
cellDrewObjects = true;
_cellParticleUnionScratch.UnionWith(
_cellParticleOwnerScratch);
}
}
// The walk already drew this cell's STATIC content
// (WalkFrameDriver's LookInStatic turn) but never submits
// particles for it — GetCellStatics fills that gap, unioned
// with the dynamic route's own owners so ONE
// DrawCellParticles call covers both.
if (_walkWorldData is not null)
{
Walk.WalkFrameStaticRecords statics =
_walkWorldData.GetCellStatics(cellId);
foreach (RenderProjectionRecord record in statics.Records)
{
if (record.Source.LocalEntityId != 0)
{
_cellParticleUnionScratch.Add(record.Source.LocalEntityId);
cellDrewObjects = true;
}
}
}
if (cellDrewObjects)
{
passes.DrawCellParticles(ctx, new RetailPViewCellSliceContext(
cellId, NoClipSlice, _cellParticleUnionScratch));
}
}
passes.UseIndoorMembershipOnlyRouting();
} }
Console.WriteLine(
$"[lookin-frame] f={_probeWalkRootFrame} walkCells=["
+ string.Join(",", walkLookInCells.Select(cell => $"0x{cell:X8}"))
+ "]");
} }
private void DrawLandscapeThroughOutsideView( private void DrawLandscapeThroughOutsideView(

View file

@ -1,6 +1,7 @@
using System.Numerics; using System.Numerics;
using AcDream.Core.Selection; using AcDream.Core.Selection;
using AcDream.Core.World; using AcDream.Core.World;
using AcDream.App.Rendering.Walk;
namespace AcDream.App.Rendering.Scene; namespace AcDream.App.Rendering.Scene;
@ -173,6 +174,13 @@ internal readonly struct RenderFrameView
public ClipFrameAssembly? ClipAssembly => Arena.ClipAssembly; public ClipFrameAssembly? ClipAssembly => Arena.ClipAssembly;
/// <summary>
/// Exact portal-view cones captured by the retail frame walk for each
/// building look-in turn. The packed dispatcher consumes these at
/// per-GfxObj granularity, matching RenderDeviceD3D::DrawMesh.
/// </summary>
public IWalkLookInViewSource? WalkLookInViews => Arena.WalkLookInViews;
public RenderFrameDiagnosticCounts DiagnosticCounts => public RenderFrameDiagnosticCounts DiagnosticCounts =>
Arena.DiagnosticCounts; Arena.DiagnosticCounts;
@ -247,6 +255,8 @@ internal sealed class RenderFrameArena
public ClipFrameAssembly? ClipAssembly { get; private set; } public ClipFrameAssembly? ClipAssembly { get; private set; }
public IWalkLookInViewSource? WalkLookInViews { get; private set; }
public RenderSceneDigest SourceDigest { get; private set; } public RenderSceneDigest SourceDigest { get; private set; }
public RenderFrameDiagnosticCounts DiagnosticCounts => public RenderFrameDiagnosticCounts DiagnosticCounts =>
@ -328,6 +338,7 @@ internal sealed class RenderFrameArena
_alphaClassificationCount = 0; _alphaClassificationCount = 0;
PortalFrame = null; PortalFrame = null;
ClipAssembly = null; ClipAssembly = null;
WalkLookInViews = null;
SourceDigest = default; SourceDigest = default;
_sourceDigestSet = false; _sourceDigestSet = false;
Generation = generation; Generation = generation;
@ -341,11 +352,13 @@ internal sealed class RenderFrameArena
internal void SetBorrowedProducts( internal void SetBorrowedProducts(
ulong epoch, ulong epoch,
PortalVisibilityFrame? portalFrame, PortalVisibilityFrame? portalFrame,
ClipFrameAssembly? clipAssembly) ClipFrameAssembly? clipAssembly,
IWalkLookInViewSource? walkLookInViews)
{ {
EnsureBuilding(epoch); EnsureBuilding(epoch);
PortalFrame = portalFrame; PortalFrame = portalFrame;
ClipAssembly = clipAssembly; ClipAssembly = clipAssembly;
WalkLookInViews = walkLookInViews;
} }
internal void SetSourceDigest(ulong epoch, in RenderSceneDigest digest) internal void SetSourceDigest(ulong epoch, in RenderSceneDigest digest)
@ -549,6 +562,7 @@ internal sealed class RenderFrameArena
ClearReferenceStorage(); ClearReferenceStorage();
PortalFrame = null; PortalFrame = null;
ClipAssembly = null; ClipAssembly = null;
WalkLookInViews = null;
SourceDigest = default; SourceDigest = default;
_sourceDigestSet = false; _sourceDigestSet = false;
_state = ArenaState.Available; _state = ArenaState.Available;
@ -655,8 +669,13 @@ internal readonly struct RenderFrameWriter
public void SetBorrowedProducts( public void SetBorrowedProducts(
PortalVisibilityFrame? portalFrame, PortalVisibilityFrame? portalFrame,
ClipFrameAssembly? clipAssembly) => ClipFrameAssembly? clipAssembly,
Arena.SetBorrowedProducts(_epoch, portalFrame, clipAssembly); IWalkLookInViewSource? walkLookInViews = null) =>
Arena.SetBorrowedProducts(
_epoch,
portalFrame,
clipAssembly,
walkLookInViews);
public void SetSourceDigest(in RenderSceneDigest digest) => public void SetSourceDigest(in RenderSceneDigest digest) =>
Arena.SetSourceDigest(_epoch, in digest); Arena.SetSourceDigest(_epoch, in digest);

View file

@ -1,5 +1,6 @@
using System.Numerics; using System.Numerics;
using AcDream.App.Rendering.Wb; using AcDream.App.Rendering.Wb;
using AcDream.App.Rendering.Walk;
namespace AcDream.App.Rendering.Scene; namespace AcDream.App.Rendering.Scene;
@ -9,7 +10,8 @@ internal readonly record struct RenderScenePViewBuildInput(
PortalVisibilityFrame PortalFrame, PortalVisibilityFrame PortalFrame,
ClipFrameAssembly ClipAssembly, ClipFrameAssembly ClipAssembly,
ViewconeCuller Viewcone, ViewconeCuller Viewcone,
IReadOnlyList<PortalVisibilityFrame> LookInFrames, IReadOnlyList<uint> LookInCellTurns,
IWalkLookInViewSource? WalkLookInViews,
HashSet<uint> DrawableCells, HashSet<uint> DrawableCells,
IRetailPViewCellSource Cells, IRetailPViewCellSource Cells,
HashSet<uint>? AnimatedEntityIds, HashSet<uint>? AnimatedEntityIds,
@ -185,7 +187,8 @@ internal sealed class RenderScenePViewFrameProductController :
PortalVisibilityFrame portalFrame, PortalVisibilityFrame portalFrame,
ClipFrameAssembly clipAssembly, ClipFrameAssembly clipAssembly,
ViewconeCuller viewcone, ViewconeCuller viewcone,
IReadOnlyList<PortalVisibilityFrame> lookInFrames, IReadOnlyList<uint> lookInCellTurns,
IWalkLookInViewSource? walkLookInViews,
HashSet<uint> drawableCells, HashSet<uint> drawableCells,
IRetailPViewCellSource cells, IRetailPViewCellSource cells,
HashSet<uint>? animatedEntityIds, HashSet<uint>? animatedEntityIds,
@ -195,7 +198,7 @@ internal sealed class RenderScenePViewFrameProductController :
ArgumentNullException.ThrowIfNull(portalFrame); ArgumentNullException.ThrowIfNull(portalFrame);
ArgumentNullException.ThrowIfNull(clipAssembly); ArgumentNullException.ThrowIfNull(clipAssembly);
ArgumentNullException.ThrowIfNull(viewcone); ArgumentNullException.ThrowIfNull(viewcone);
ArgumentNullException.ThrowIfNull(lookInFrames); ArgumentNullException.ThrowIfNull(lookInCellTurns);
ArgumentNullException.ThrowIfNull(drawableCells); ArgumentNullException.ThrowIfNull(drawableCells);
ArgumentNullException.ThrowIfNull(cells); ArgumentNullException.ThrowIfNull(cells);
@ -213,7 +216,8 @@ internal sealed class RenderScenePViewFrameProductController :
portalFrame, portalFrame,
clipAssembly, clipAssembly,
viewcone, viewcone,
lookInFrames, lookInCellTurns,
walkLookInViews,
drawableCells, drawableCells,
cells, cells,
animatedEntityIds, animatedEntityIds,
@ -248,7 +252,8 @@ internal sealed class RenderScenePViewFrameProductController :
portalFrame, portalFrame,
clipAssembly, clipAssembly,
viewcone, viewcone,
lookInFrames, FlattenLegacyLookInCells(lookInFrames),
walkLookInViews: null,
drawableCells, drawableCells,
cells, cells,
animatedEntityIds, animatedEntityIds,
@ -267,6 +272,19 @@ internal sealed class RenderScenePViewFrameProductController :
} }
} }
private static uint[] FlattenLegacyLookInCells(
IReadOnlyList<PortalVisibilityFrame> lookInFrames)
{
var cells = new List<uint>();
for (int frameIndex = 0; frameIndex < lookInFrames.Count; frameIndex++)
{
PortalVisibilityFrame frame = lookInFrames[frameIndex];
for (int index = frame.OrderedVisibleCells.Count - 1; index >= 0; index--)
cells.Add(frame.OrderedVisibleCells[index]);
}
return cells.ToArray();
}
public void CompleteProduction(in RenderFrameView view) public void CompleteProduction(in RenderFrameView view)
{ {
_packedClassification = _packedClassification =
@ -1171,7 +1189,8 @@ internal sealed class RenderScenePViewFrameBuilder
{ {
writer.SetBorrowedProducts( writer.SetBorrowedProducts(
input.PortalFrame, input.PortalFrame,
input.ClipAssembly); input.ClipAssembly,
input.WalkLookInViews);
RenderSceneDigest digest = input.SourceDigest; RenderSceneDigest digest = input.SourceDigest;
writer.SetSourceDigest(in digest); writer.SetSourceDigest(in digest);
_projectionIds.Clear(); _projectionIds.Clear();
@ -1182,19 +1201,11 @@ internal sealed class RenderScenePViewFrameBuilder
// outdoor static, building shell, and cell static (including // outdoor static, building shell, and cell static (including
// look-in cell statics) directly through OrderedDrawStream (plan // look-in cell statics) directly through OrderedDrawStream (plan
// §FW3 "FW3.2b-2 — the production rooting", item 3). LookInObject // §FW3 "FW3.2b-2 — the production rooting", item 3). LookInObject
// keeps emitting, but BuildLookInRoutes below is now filtered to // keeps the packed classifier for live animation/fades, but its
// DYNAMIC candidates only — the walk owns that route's statics. // route ranges are keyed to THE WALK'S own look-in cell turns.
int lookInRouteIndex = 0; // The legacy PortalVisibilityFrame list no longer has a production
for (int frameIndex = 0; // admission or ordering role.
frameIndex < input.LookInFrames.Count; BuildLookInRoutes(writer, in input);
frameIndex++)
{
BuildLookInRoutes(
writer,
in input,
frameIndex,
ref lookInRouteIndex);
}
BuildOutsideDynamicRoutes(writer, in input); BuildOutsideDynamicRoutes(writer, in input);
BuildDynamicLastRoute(writer, in input); BuildDynamicLastRoute(writer, in input);
writer.Publish(); writer.Publish();
@ -1284,45 +1295,37 @@ internal sealed class RenderScenePViewFrameBuilder
/// item 3).</summary> /// item 3).</summary>
private void BuildLookInRoutes( private void BuildLookInRoutes(
RenderFrameWriter writer, RenderFrameWriter writer,
in RenderScenePViewBuildInput input, in RenderScenePViewBuildInput input)
int frameIndex,
ref int routeIndex)
{ {
PortalVisibilityFrame frame = input.LookInFrames[frameIndex]; for (int routeIndex = 0; routeIndex < input.LookInCellTurns.Count; routeIndex++)
for (int i = frame.OrderedVisibleCells.Count - 1; i >= 0; i--)
{ {
uint cellId = frame.OrderedVisibleCells[i]; uint cellId = input.LookInCellTurns[routeIndex];
var clipKey = new LookInClipCell(frameIndex, cellId);
if (!input.ClipAssembly.LookInCellToViewSlices.TryGetValue(
clipKey,
out ClipViewSlice[]? slices)
|| slices.Length == 0)
{
continue;
}
int count = LoadCell( int count = LoadCell(
input.Scene, input.Scene,
cellId, cellId,
includeStatics: false, includeStatics: false,
includeDynamics: true); includeDynamics: true);
for (int sliceIndex = 0; sliceIndex < slices.Length; sliceIndex++) if (count == 0)
{ continue;
int currentRouteIndex = routeIndex++;
if (count == 0)
continue;
for (int index = 0; index < count; index++) int survivorCount = 0;
AddProjection( EnsureCapacity(ref _survivors, count);
writer, for (int index = 0; index < count; index++)
in _cell[index], {
input.AnimatedEntityIds); RenderProjectionRecord record = _cell[index];
writer.AddRouteRange( _survivors[survivorCount++] = record;
RenderFrameCandidateRoute.LookInObject, AddProjection(
currentRouteIndex, writer,
cellId, in record,
_cell.AsSpan(0, count)); input.AnimatedEntityIds);
} }
if (survivorCount == 0)
continue;
writer.AddRouteRange(
RenderFrameCandidateRoute.LookInObject,
routeIndex,
cellId,
_survivors.AsSpan(0, survivorCount));
} }
} }
@ -1384,14 +1387,8 @@ internal sealed class RenderScenePViewFrameBuilder
in RenderScenePViewBuildInput input) in RenderScenePViewBuildInput input)
{ {
_lookInCellScratch.Clear(); _lookInCellScratch.Clear();
for (int frameIndex = 0; for (int index = 0; index < input.LookInCellTurns.Count; index++)
frameIndex < input.LookInFrames.Count; _lookInCellScratch.Add(input.LookInCellTurns[index]);
frameIndex++)
{
PortalVisibilityFrame frame = input.LookInFrames[frameIndex];
for (int i = 0; i < frame.OrderedVisibleCells.Count; i++)
_lookInCellScratch.Add(frame.OrderedVisibleCells[i]);
}
int count = 0; int count = 0;
EnsureCapacity(ref _survivors, _dynamicCount); EnsureCapacity(ref _survivors, _dynamicCount);

View file

@ -1,4 +1,5 @@
using System.Numerics; using System.Numerics;
using System.Runtime.InteropServices;
using AcDream.App.Rendering; using AcDream.App.Rendering;
using AcDream.App.Rendering.Gpu; using AcDream.App.Rendering.Gpu;
using AcDream.App.Rendering.Scene; using AcDream.App.Rendering.Scene;
@ -118,6 +119,17 @@ internal interface IWalkFrameLeafRenderer
/// positional invariant this carries (the #132 falls containment).</summary> /// positional invariant this carries (the #132 falls containment).</summary>
void DrawStaticParticles(IReadOnlySet<uint> ownerIds); void DrawStaticParticles(IReadOnlySet<uint> ownerIds);
/// <summary>Draws the packed dynamic occupants of one building look-in
/// cell at that cell's OWN <c>PView::DrawCells</c> turn, then submits the
/// cell's static + dynamic particle owners at the same turn. The route
/// index is assigned in the exact order Collect encountered look-in cell
/// turns and therefore matches the frame product's walk-keyed
/// <c>LookInObject</c> ranges.</summary>
void DrawLookInDynamics(
uint cellId,
int routeIndex,
IReadOnlySet<uint> staticParticleOwnerIds);
/// <summary><c>PView::DrawCells</c> @0x005a4840's gated full depth clear /// <summary><c>PView::DrawCells</c> @0x005a4840's gated full depth clear
/// (pc:432731-432732) between the outside stage and the interior root's /// (pc:432731-432732) between the outside stage and the interior root's
/// own flood — production maps this to <c>IWorldPassScope.ClearInteriorDepth</c> /// own flood — production maps this to <c>IWorldPassScope.ClearInteriorDepth</c>
@ -259,8 +271,37 @@ internal enum WalkFrameEventKind : byte
/// cathedral bleed; the old pipeline's user-verified #132 fix /// cathedral bleed; the old pipeline's user-verified #132 fix
/// `e102fb36` encoded the same invariant).</summary> /// `e102fb36` encoded the same invariant).</summary>
StaticParticles, StaticParticles,
/// <summary><see cref="IWalkFrameLeafRenderer.DrawLookInDynamics"/> —
/// <see cref="WalkFrameEvent.CellId"/> is the look-in cell and
/// <see cref="WalkFrameEvent.IntArg"/> is its walk-ordered packed route
/// index.</summary>
LookInDynamics,
} }
/// <summary>
/// The exact portal-view cones installed at the walk's building look-in
/// <c>DrawCells</c> turns. Retail <c>RenderDeviceD3D::DrawMesh</c>
/// @0x005A0860 tests each object's drawing sphere against these views before
/// drawing the mesh whole; cell membership alone is not an admission rule.
/// </summary>
internal interface IWalkLookInViewSource
{
IReadOnlyList<uint> LookInCellTurns { get; }
bool SphereVisibleInLookInTurn(
int routeIndex,
in Vector3 center,
float radius);
}
internal readonly record struct WalkLookInSlice(int PlaneStart, int PlaneCount);
internal readonly record struct WalkLookInTurn(
uint CellId,
int SliceStart,
int SliceCount);
/// <summary>See <see cref="WalkFrameEventKind"/> for what each field means per /// <summary>See <see cref="WalkFrameEventKind"/> for what each field means per
/// kind. A single struct (rather than a kind hierarchy) keeps Collect's /// kind. A single struct (rather than a kind hierarchy) keeps Collect's
/// per-turn list a flat, allocation-cheap <c>List&lt;WalkFrameEvent&gt;</c> — /// per-turn list a flat, allocation-cheap <c>List&lt;WalkFrameEvent&gt;</c> —
@ -320,6 +361,9 @@ internal readonly struct WalkFrameEvent
internal static WalkFrameEvent BuildingShellParticles(WalkBuilding building) => internal static WalkFrameEvent BuildingShellParticles(WalkBuilding building) =>
new(WalkFrameEventKind.StaticParticles, 0, 0, 0f, null, building); new(WalkFrameEventKind.StaticParticles, 0, 0, 0f, null, building);
internal static WalkFrameEvent LookInDynamics(uint cellId, int routeIndex) =>
new(WalkFrameEventKind.LookInDynamics, routeIndex, cellId, 0f, null);
internal static WalkFrameEvent ClearInteriorDepth() => internal static WalkFrameEvent ClearInteriorDepth() =>
new(WalkFrameEventKind.ClearInteriorDepth, 0, 0, 0f, null); new(WalkFrameEventKind.ClearInteriorDepth, 0, 0, 0f, null);
@ -404,7 +448,7 @@ internal readonly struct WalkFrameEvent
/// @0x0059f2a0 (the <c>part-&gt;gfxobj[deg_level]!=0</c> gate @0x0059f2d3 /// @0x0059f2a0 (the <c>part-&gt;gfxobj[deg_level]!=0</c> gate @0x0059f2d3
/// and the alpha-barrier → portal-pass → shell order @0x0059f30b0x0059f345).</para> /// and the alpha-barrier → portal-pass → shell order @0x0059f30b0x0059f345).</para>
/// </summary> /// </summary>
internal sealed class WalkFrameDriver : IWalkEventSink internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
{ {
private readonly WbDrawDispatcher _dispatcher; private readonly WbDrawDispatcher _dispatcher;
private readonly WalkStaticStreamPopulator _populator; private readonly WalkStaticStreamPopulator _populator;
@ -422,6 +466,23 @@ internal sealed class WalkFrameDriver : IWalkEventSink
// walk pass dedicated to nothing but set-gathering. // walk pass dedicated to nothing but set-gathering.
internal HashSet<uint> VisitedCells { get; } = new(); internal HashSet<uint> VisitedCells { get; } = new();
/// <summary>Building look-in cells in the exact order the walk encountered
/// their <c>DrawCells</c> turns. Duplicates are intentional: two authored
/// portal views can independently visit the same cell and therefore own
/// distinct packed route indices.</summary>
internal List<uint> LookInCellTurns { get; } = new();
private readonly List<WalkLookInTurn> _lookInTurns = new();
private readonly List<WalkLookInSlice> _lookInSlices = new();
private readonly List<WalkPlane> _lookInPlanes = new();
private WalkPlane _lookInCyPlane;
IReadOnlyList<uint> IWalkLookInViewSource.LookInCellTurns => LookInCellTurns;
/// <summary>The set form of <see cref="LookInCellTurns"/>, for drawn-once
/// exclusion and root-flood particle bookkeeping.</summary>
internal HashSet<uint> LookInCells { get; } = new();
/// <summary>FW4 slice 2: the interior root's ORDERED flood cell list, /// <summary>FW4 slice 2: the interior root's ORDERED flood cell list,
/// exactly as retail's <c>PView::DrawCells</c> iterates it for the /// exactly as retail's <c>PView::DrawCells</c> iterates it for the
/// exit-portal seals (pc:432785-432786) — captured at /// exit-portal seals (pc:432785-432786) — captured at
@ -459,6 +520,7 @@ internal sealed class WalkFrameDriver : IWalkEventSink
private bool _skyDrawnThisFrame; private bool _skyDrawnThisFrame;
private WalkDrawStage? _currentDcStage; private WalkDrawStage? _currentDcStage;
private bool _readyToReplay; private bool _readyToReplay;
private int _lookInRouteIndex;
internal WalkFrameDriver( internal WalkFrameDriver(
WbDrawDispatcher dispatcher, WbDrawDispatcher dispatcher,
@ -534,7 +596,7 @@ internal sealed class WalkFrameDriver : IWalkEventSink
/// GPU work — see this type's own doc comment. /// GPU work — see this type's own doc comment.
/// </summary> /// </summary>
internal void BeginFrame( internal void BeginFrame(
IWalkBuildingFrameContext ctx, IRetailFrameWalkContext ctx,
Matrix4x4 viewProjection, Matrix4x4 viewProjection,
Vector3 cameraWorldPosition) Vector3 cameraWorldPosition)
{ {
@ -557,9 +619,16 @@ internal sealed class WalkFrameDriver : IWalkEventSink
_events.Clear(); _events.Clear();
_markPositions.Clear(); _markPositions.Clear();
VisitedCells.Clear(); VisitedCells.Clear();
LookInCellTurns.Clear();
_lookInTurns.Clear();
_lookInSlices.Clear();
_lookInPlanes.Clear();
_lookInCyPlane = ctx.CyPlane;
LookInCells.Clear();
VisitedBuildings.Clear(); VisitedBuildings.Clear();
VisitedLandscapeCellIds.Clear(); VisitedLandscapeCellIds.Clear();
InteriorFloodCells.Clear(); InteriorFloodCells.Clear();
_lookInRouteIndex = 0;
} }
/// <summary>Records the final segment mark (plan §FW3.2b-1's "at frame /// <summary>Records the final segment mark (plan §FW3.2b-1's "at frame
@ -655,6 +724,16 @@ internal sealed class WalkFrameDriver : IWalkEventSink
if (_staticParticleOwnerScratch.Count > 0) if (_staticParticleOwnerScratch.Count > 0)
_leafRenderer.DrawStaticParticles(_staticParticleOwnerScratch); _leafRenderer.DrawStaticParticles(_staticParticleOwnerScratch);
break; break;
case WalkFrameEventKind.LookInDynamics:
_staticParticleOwnerScratch.Clear();
UnionOwners(
_worldData.GetCellStatics(e.CellId),
_staticParticleOwnerScratch);
_leafRenderer.DrawLookInDynamics(
e.CellId,
e.IntArg,
_staticParticleOwnerScratch);
break;
} }
} }
@ -883,6 +962,76 @@ internal sealed class WalkFrameDriver : IWalkEventSink
_populator.PopulateCell( _populator.PopulateCell(
_stream, stage, cellId, records.Records, records.TupleLandblockId, _stream, stage, cellId, records.Records, records.TupleLandblockId,
_cameraWorldPosition, _viewProjection); _cameraWorldPosition, _viewProjection);
if (stage == WalkDrawStage.LookInStatic)
{
// Retail draws a look-in cell's complete object list at this
// re-entrant DrawCells turn. The packed dynamic route used to run
// much later at the pre-clear boundary, after nearer building
// shells, which let the cathedral's 0x112 remote player overpaint
// opaque walls. Keep animation/fade in the packed route, but replay
// it here between this cell's content and the building shell.
MarkIfGrown();
int routeIndex = _lookInRouteIndex++;
LookInCellTurns.Add(cellId);
LookInCells.Add(cellId);
CaptureLookInViews(cellId);
_events.Add(WalkFrameEvent.LookInDynamics(cellId, routeIndex));
}
}
private void CaptureLookInViews(uint cellId)
{
IWalkBuildingFrameContext ctx = RequireOpenFrame();
WalkCell? cell = ctx.GetVisible(cellId);
if (cell is null || cell.NumView <= 0)
{
_lookInTurns.Add(new WalkLookInTurn(
cellId, _lookInSlices.Count, 0));
return;
}
WalkPortalView portalView = cell.TopView;
int sliceStart = _lookInSlices.Count;
for (int sliceIndex = 0; sliceIndex < portalView.ViewCount; sliceIndex++)
{
WalkViewPoly poly = portalView.View.Polys[sliceIndex];
int planeStart = _lookInPlanes.Count;
for (int edge = 0; edge < poly.VertexCount; edge++)
{
_lookInPlanes.Add(
portalView.View.Vertices[poly.VertexIndex + edge].Plane);
}
_lookInSlices.Add(new WalkLookInSlice(
planeStart, poly.VertexCount));
}
_lookInTurns.Add(new WalkLookInTurn(
cellId, sliceStart, _lookInSlices.Count - sliceStart));
}
public bool SphereVisibleInLookInTurn(
int routeIndex,
in Vector3 center,
float radius)
{
if ((uint)routeIndex >= (uint)_lookInTurns.Count)
return false;
WalkLookInTurn turn = _lookInTurns[routeIndex];
for (int sliceOffset = 0; sliceOffset < turn.SliceCount; sliceOffset++)
{
WalkLookInSlice slice = _lookInSlices[turn.SliceStart + sliceOffset];
if (WalkVisibilityMath.ViewconeCheck(
center,
radius,
_lookInCyPlane,
CollectionsMarshal.AsSpan(_lookInPlanes).Slice(
slice.PlaneStart,
slice.PlaneCount)) != WalkBoundingType.Outside)
{
return true;
}
}
return false;
} }
/// <summary>Campaign FW3.4a: the collect-time analogue of the old /// <summary>Campaign FW3.4a: the collect-time analogue of the old

View file

@ -6,6 +6,7 @@ using AcDream.Core.Lighting;
using AcDream.Core.Meshing; using AcDream.Core.Meshing;
using AcDream.Core.Selection; using AcDream.Core.Selection;
using AcDream.Core.World; using AcDream.Core.World;
using AcDream.App.Rendering.Walk;
using DatReaderWriter.Enums; using DatReaderWriter.Enums;
namespace AcDream.App.Rendering.Wb; namespace AcDream.App.Rendering.Wb;
@ -40,6 +41,8 @@ public sealed unsafe partial class WbDrawDispatcher
private RenderSceneGeneration _packedProductionGeneration; private RenderSceneGeneration _packedProductionGeneration;
private ulong _packedProductionFrameSequence; private ulong _packedProductionFrameSequence;
private int _packedProductionNextRange; private int _packedProductionNextRange;
private readonly Dictionary<(int RouteIndex, uint LocalEntityId, int PartIndex, uint GfxObjId), string>
_probeLookInPartStates = [];
internal IReadOnlyList<CurrentRenderDispatcherSubmission> internal IReadOnlyList<CurrentRenderDispatcherSubmission>
PackedDispatcherSubmissions => _packedSubmissions; PackedDispatcherSubmissions => _packedSubmissions;
@ -295,7 +298,12 @@ public sealed unsafe partial class WbDrawDispatcher
source.MeshPartOffset, source.MeshPartOffset,
source.MeshPartCount), source.MeshPartCount),
ref anyVao, ref anyVao,
publishSelection); publishSelection,
range.Route is RenderFrameCandidateRoute.LookInObject
? view.WalkLookInViews
: null,
range.RouteIndex,
range.CellId);
} }
return new PackedRangeClassification( return new PackedRangeClassification(
@ -334,7 +342,10 @@ public sealed unsafe partial class WbDrawDispatcher
in RenderInstanceCandidate entity, in RenderInstanceCandidate entity,
ReadOnlySpan<RenderFrameMeshPart> meshParts, ReadOnlySpan<RenderFrameMeshPart> meshParts,
ref uint anyVao, ref uint anyVao,
bool publishSelection) bool publishSelection,
IWalkLookInViewSource? lookInViews,
int lookInRouteIndex,
uint lookInCellId)
{ {
(uint slot, bool culled) = ResolveSlotForFrame( (uint slot, bool culled) = ResolveSlotForFrame(
_clipRoutingActive, _clipRoutingActive,
@ -361,7 +372,8 @@ public sealed unsafe partial class WbDrawDispatcher
: new Vector2(0f, 1f); : new Vector2(0f, 1f);
PackedProjectionClassificationEntry? cacheEntry = null; PackedProjectionClassificationEntry? cacheEntry = null;
if (!entity.Animated) bool lookInConeActive = lookInViews is not null;
if (!entity.Animated && !lookInConeActive)
{ {
PackedClassificationIdentity identity = PackedClassificationIdentity identity =
PackedClassificationIdentity.From(in projection); PackedClassificationIdentity.From(in projection);
@ -394,7 +406,7 @@ public sealed unsafe partial class WbDrawDispatcher
entity.ProjectionId, entity.ProjectionId,
in identity); in identity);
} }
else else if (entity.Animated)
{ {
_packedClassificationCache.RecordAnimatedClassification(); _packedClassificationCache.RecordAnimatedClassification();
} }
@ -485,6 +497,21 @@ public sealed unsafe partial class WbDrawDispatcher
partTransform * meshRef.PartTransform; partTransform * meshRef.PartTransform;
Matrix4x4 model = Matrix4x4 model =
restPose * entity.RootWorld; restPose * entity.RootWorld;
int selectionPartIndex = unchecked(
(partIndex << 16)
| (setupPartIndex & 0xFFFF));
if (!PartVisibleInLookInTurn(
lookInViews,
lookInRouteIndex,
lookInCellId,
in entity,
selectionPartIndex,
(uint)gfxObjId,
partData,
model))
{
continue;
}
if (!ClassifyPackedBatches( if (!ClassifyPackedBatches(
partData, partData,
restPose, restPose,
@ -502,9 +529,6 @@ public sealed unsafe partial class WbDrawDispatcher
{ {
reusableAcrossFrames = false; reusableAcrossFrames = false;
} }
int selectionPartIndex = unchecked(
(partIndex << 16)
| (setupPartIndex & 0xFFFF));
cacheEntry?.SelectionParts.Add( cacheEntry?.SelectionParts.Add(
new PackedClassifiedSelectionPart( new PackedClassifiedSelectionPart(
selectionPartIndex, selectionPartIndex,
@ -538,6 +562,18 @@ public sealed unsafe partial class WbDrawDispatcher
Matrix4x4 restPose = meshRef.PartTransform; Matrix4x4 restPose = meshRef.PartTransform;
Matrix4x4 model = restPose * entity.RootWorld; Matrix4x4 model = restPose * entity.RootWorld;
if (!PartVisibleInLookInTurn(
lookInViews,
lookInRouteIndex,
lookInCellId,
in entity,
partIndex,
meshRef.GfxObjId,
renderData,
model))
{
continue;
}
if (!ClassifyPackedBatches( if (!ClassifyPackedBatches(
renderData, renderData,
restPose, restPose,
@ -576,6 +612,97 @@ public sealed unsafe partial class WbDrawDispatcher
} }
} }
/// <summary>
/// Retail RenderDeviceD3D::DrawMesh @0x005A0860 admits each CGfxObj
/// independently by transforming its drawing sphere into the active
/// portal view. A whole-entity sphere is not equivalent for multipart
/// creatures: the union can be roughly ten metres wide and intersect an
/// aperture while every actual body/equipment part is behind its wall.
/// </summary>
private bool PartVisibleInLookInTurn(
IWalkLookInViewSource? lookInViews,
int routeIndex,
uint cellId,
in RenderInstanceCandidate entity,
int partIndex,
uint gfxObjId,
ObjectRenderData renderData,
Matrix4x4 localToWorld)
{
if (lookInViews is null)
return true;
// ObjectRenderData.SelectionSphere is retained per GfxObj by the
// prepared mesh payload. It is never the entity-union sphere and is
// therefore the correct granularity for DrawMesh admission. The
// current package stores a conservative vertex-derived sphere here;
// this preserves whole-mesh drawing while avoiding the invalid
// aggregate-character admission that caused the cathedral bleed.
if (renderData.SelectionSphere is not { Radius: > 0f } sphere)
return true;
bool visible = LookInDrawingSphereVisible(
lookInViews,
routeIndex,
sphere,
localToWorld,
out Vector3 center,
out float radius);
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeWalkRootEnabled
&& cellId == 0xF4180112u)
{
var key = (routeIndex, entity.LocalEntityId, partIndex, gfxObjId);
string state =
$"visible={(visible ? 1 : 0)} "
+ $"center=({center.X:F2},{center.Y:F2},{center.Z:F2}) "
+ $"r={radius:F2}";
if (!_probeLookInPartStates.TryGetValue(key, out string? previous)
|| previous != state)
{
_probeLookInPartStates[key] = state;
Console.WriteLine(
$"[lookin-part] route={routeIndex} cell=0x{cellId:X8} "
+ $"id={entity.LocalEntityId:x} part={partIndex} "
+ $"gfx=0x{gfxObjId:X8} {state}");
}
}
return visible;
}
internal static bool LookInDrawingSphereVisible(
IWalkLookInViewSource lookInViews,
int routeIndex,
DatReaderWriter.Types.Sphere sphere,
Matrix4x4 localToWorld,
out Vector3 center,
out float radius)
{
ArgumentNullException.ThrowIfNull(lookInViews);
ArgumentNullException.ThrowIfNull(sphere);
center = Vector3.Transform(sphere.Origin, localToWorld);
float scaleX = new Vector3(
localToWorld.M11,
localToWorld.M12,
localToWorld.M13).Length();
float scaleY = new Vector3(
localToWorld.M21,
localToWorld.M22,
localToWorld.M23).Length();
float scaleZ = new Vector3(
localToWorld.M31,
localToWorld.M32,
localToWorld.M33).Length();
radius = sphere.Radius
* MathF.Max(scaleX, MathF.Max(scaleY, scaleZ));
return lookInViews.SphereVisibleInLookInTurn(
routeIndex,
in center,
radius);
}
/// <summary> /// <summary>
/// Mirrors the production dispatcher's no-VAO early return. That return /// Mirrors the production dispatcher's no-VAO early return. That return
/// records an empty submission with transparent deferral disabled even /// records an empty submission with transparent deferral disabled even

View file

@ -3,12 +3,23 @@ using AcDream.App.Rendering;
using AcDream.App.Rendering.Scene; using AcDream.App.Rendering.Scene;
using AcDream.App.Rendering.Scene.Arch; using AcDream.App.Rendering.Scene.Arch;
using AcDream.App.Rendering.Wb; using AcDream.App.Rendering.Wb;
using AcDream.App.Rendering.Walk;
using AcDream.Core.World; using AcDream.Core.World;
namespace AcDream.App.Tests.Rendering; namespace AcDream.App.Tests.Rendering;
public sealed class RenderScenePViewFrameProductTests public sealed class RenderScenePViewFrameProductTests
{ {
private sealed class RejectAllLookInViews(uint cellId) : IWalkLookInViewSource
{
public IReadOnlyList<uint> LookInCellTurns { get; } = [cellId];
public bool SphereVisibleInLookInTurn(
int routeIndex,
in Vector3 center,
float radius) => false;
}
private const uint Landblock = 0xA9B4FFFF; private const uint Landblock = 0xA9B4FFFF;
private const uint Cell = 0xA9B40170; private const uint Cell = 0xA9B40170;
private static readonly RenderSceneGeneration Generation = private static readonly RenderSceneGeneration Generation =
@ -159,6 +170,7 @@ public sealed class RenderScenePViewFrameProductTests
clip, clip,
viewcone, viewcone,
[], [],
null,
[Cell], [Cell],
EmptyCellSource.Instance, EmptyCellSource.Instance,
[], [],
@ -168,6 +180,134 @@ public sealed class RenderScenePViewFrameProductTests
} }
} }
[Fact]
public void Builder_KeysLookInDynamicsToWalkTurnsAndExcludesThemFromDynamicLast()
{
const uint lookInCell = 0xA9B4_0171;
using var scene = new ArchRenderScene(Generation);
RenderProjectionRecord rootDynamic = Record(
0x0100_0000_0000_0021,
RenderProjectionClass.LiveDynamicRoot,
parentCell: Cell);
RenderProjectionRecord lookInDynamic = Record(
0x0100_0000_0000_0022,
RenderProjectionClass.LiveDynamicRoot,
parentCell: lookInCell);
scene.Apply(
[
RenderProjectionDelta.Register(Generation, 1, rootDynamic),
RenderProjectionDelta.Register(Generation, 2, lookInDynamic),
]);
PortalVisibilityFrame portal = Portal(Cell);
ClipFrameAssembly clip = FullScreenClip(Cell);
ViewconeCuller viewcone = ViewconeCuller.Build(clip, Matrix4x4.Identity);
RenderSceneQuery query = scene.OpenQuery();
var input = new RenderScenePViewBuildInput(
query,
new RenderSceneDigest(query.Generation, query.Counts, default),
portal,
clip,
viewcone,
LookInCellTurns: [lookInCell],
WalkLookInViews: null,
DrawableCells: [Cell, lookInCell],
Cells: EmptyCellSource.Instance,
AnimatedEntityIds: [],
RootIsOutdoor: false,
RootFloodCells: new HashSet<uint> { Cell });
var exchange = new RenderFrameExchange();
var builder = new RenderScenePViewFrameBuilder();
builder.Build(exchange, frameSequence: 1, in input);
RenderFrameView frame = exchange.BorrowLatest(Generation, 1);
try
{
RenderFrameCandidateRange lookInRange = Assert.Single(
frame.RouteRanges.ToArray(),
range => range.Route == RenderFrameCandidateRoute.LookInObject);
Assert.Equal(0, lookInRange.RouteIndex);
Assert.Equal(lookInCell, lookInRange.CellId);
Assert.Equal(
[lookInDynamic.Id],
frame.RouteCandidates
.Slice(lookInRange.Offset, lookInRange.Count)
.ToArray()
.Select(static record => record.Id));
RenderFrameCandidateRange dynamicLast = Assert.Single(
frame.RouteRanges.ToArray(),
range => range.Route == RenderFrameCandidateRoute.DynamicLast);
RenderProjectionId[] lastIds = frame.RouteCandidates
.Slice(dynamicLast.Offset, dynamicLast.Count)
.ToArray()
.Select(static record => record.Id)
.ToArray();
Assert.Contains(rootDynamic.Id, lastIds);
Assert.DoesNotContain(lookInDynamic.Id, lastIds);
}
finally
{
exchange.Release(in frame);
}
}
[Fact]
public void Builder_CarriesExactWalkConeForPerPartDispatcherAdmission()
{
const uint lookInCell = 0xA9B4_0171;
using var scene = new ArchRenderScene(Generation);
RenderProjectionRecord lookInDynamic = Record(
0x0100_0000_0000_0022,
RenderProjectionClass.LiveDynamicRoot,
parentCell: lookInCell);
scene.Apply([RenderProjectionDelta.Register(Generation, 1, lookInDynamic)]);
PortalVisibilityFrame portal = Portal(Cell);
ClipFrameAssembly clip = FullScreenClip(Cell);
ViewconeCuller viewcone = ViewconeCuller.Build(clip, Matrix4x4.Identity);
RenderSceneQuery query = scene.OpenQuery();
var input = new RenderScenePViewBuildInput(
query,
new RenderSceneDigest(query.Generation, query.Counts, default),
portal,
clip,
viewcone,
LookInCellTurns: [lookInCell],
WalkLookInViews: new RejectAllLookInViews(lookInCell),
DrawableCells: [Cell, lookInCell],
Cells: EmptyCellSource.Instance,
AnimatedEntityIds: [],
RootIsOutdoor: false,
RootFloodCells: new HashSet<uint> { Cell });
var exchange = new RenderFrameExchange();
var builder = new RenderScenePViewFrameBuilder();
builder.Build(exchange, frameSequence: 1, in input);
RenderFrameView frame = exchange.BorrowLatest(Generation, 1);
try
{
RenderFrameCandidateRange range = Assert.Single(
frame.RouteRanges.ToArray(),
candidate => candidate.Route == RenderFrameCandidateRoute.LookInObject);
Assert.Equal(
[lookInDynamic.Id],
frame.RouteCandidates
.Slice(range.Offset, range.Count)
.ToArray()
.Select(static record => record.Id));
Assert.NotNull(frame.WalkLookInViews);
Assert.False(frame.WalkLookInViews!.SphereVisibleInLookInTurn(
routeIndex: 0,
center: Vector3.Zero,
radius: 1f));
}
finally
{
exchange.Release(in frame);
}
}
[Fact] [Fact]
public void Controller_MatchesCurrentPViewThenNamesAWithdrawnCandidate() public void Controller_MatchesCurrentPViewThenNamesAWithdrawnCandidate()
{ {

View file

@ -67,6 +67,18 @@ public sealed class WalkFrameDriverTests
sorted.Sort(); sorted.Sort();
log.Add($"PARTICLES:{string.Join(",", sorted.ConvertAll(o => o.ToString("x")))}"); log.Add($"PARTICLES:{string.Join(",", sorted.ConvertAll(o => o.ToString("x")))}");
} }
public void DrawLookInDynamics(
uint cellId,
int routeIndex,
IReadOnlySet<uint> staticParticleOwnerIds)
{
var sorted = new List<uint>(staticParticleOwnerIds);
sorted.Sort();
log.Add(
$"LOOKIN:{cellId:x8}:{routeIndex}:"
+ string.Join(",", sorted.ConvertAll(o => o.ToString("x"))));
}
} }
private sealed class RecordingTrace(List<string> log) : IWalkFrameDriverTrace private sealed class RecordingTrace(List<string> log) : IWalkFrameDriverTrace
@ -417,10 +429,28 @@ public sealed class WalkFrameDriverTests
new[] new[]
{ {
"ALPHA:12.50", "PUNCH:4@v0", "SHELL:00000104", "ALPHA:12.50", "PUNCH:4@v0", "SHELL:00000104",
"FLUSH:1:LookInStatic", "FLUSH:1:BuildingShell", "PARTICLES:c9", "FLUSH:1:LookInStatic", "LOOKIN:00000104:0:ca",
"FLUSH:1:BuildingShell", "PARTICLES:c9",
}, },
log); log);
Assert.Equal([0x104u], driver.LookInCellTurns);
Assert.Equal([0x104u], driver.LookInCells);
WalkPortalView capturedView = ctx.Cells[0x104].PortalViews[0];
WalkViewPoly capturedPoly = Assert.Single(capturedView.View.Polys);
Vector2 capturedCenter = Vector2.Zero;
for (int edge = 0; edge < capturedPoly.VertexCount; edge++)
{
capturedCenter += capturedView.View.Vertices[
capturedPoly.VertexIndex + edge].Point;
}
capturedCenter /= capturedPoly.VertexCount;
Vector3 insideCone = ctx.Rays.RayThrough(capturedCenter.X, capturedCenter.Y);
Assert.True(driver.SphereVisibleInLookInTurn(
0, in insideCone, 0.1f));
Assert.False(driver.SphereVisibleInLookInTurn(
0, new Vector3(10_000f, 0f, 10f), 0.1f));
// The punch polygon reached the leaf renderer in WORLD space: the // The punch polygon reached the leaf renderer in WORLD space: the
// building-local Quad(-2f) vertex (-0.5,-0.5,-2) translates by // building-local Quad(-2f) vertex (-0.5,-0.5,-2) translates by
// (10,0,0) under the caller-supplied building world transform. // (10,0,0) under the caller-supplied building world transform.

View file

@ -0,0 +1,60 @@
using System.Numerics;
using AcDream.App.Rendering.Walk;
using AcDream.App.Rendering.Wb;
using DatReaderWriter.Types;
namespace AcDream.App.Tests.Rendering.Wb;
public sealed class WbDrawDispatcherLookInConeTests
{
[Fact]
public void DrawingSphereAdmissionUsesTheIndividualPartTransformAndLargestScale()
{
var cone = new RecordingLookInViews();
var sphere = new Sphere
{
Origin = new Vector3(1f, 2f, 3f),
Radius = 0.5f,
};
Matrix4x4 transform =
Matrix4x4.CreateScale(2f, 3f, 4f)
* Matrix4x4.CreateTranslation(10f, 20f, 30f);
bool visible = WbDrawDispatcher.LookInDrawingSphereVisible(
cone,
routeIndex: 7,
sphere,
transform,
out Vector3 center,
out float radius);
Assert.True(visible);
Assert.Equal(7, cone.RouteIndex);
Assert.Equal(new Vector3(12f, 26f, 42f), center);
Assert.Equal(center, cone.Center);
Assert.Equal(2f, radius);
Assert.Equal(radius, cone.Radius);
}
private sealed class RecordingLookInViews : IWalkLookInViewSource
{
public IReadOnlyList<uint> LookInCellTurns { get; } = [0xF4180112u];
public int RouteIndex { get; private set; }
public Vector3 Center { get; private set; }
public float Radius { get; private set; }
public bool SphereVisibleInLookInTurn(
int routeIndex,
in Vector3 center,
float radius)
{
RouteIndex = routeIndex;
Center = center;
Radius = radius;
return true;
}
}
}