checkpoint: preserve cathedral look-in investigation state
This commit is contained in:
parent
c287db8651
commit
572de1ec30
11 changed files with 757 additions and 197 deletions
|
|
@ -1,4 +1,5 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Rendering.Scene;
|
||||
using AcDream.App.Rendering.Walk;
|
||||
using AcDream.Core.Vfx;
|
||||
|
||||
|
|
@ -151,6 +152,8 @@ internal sealed partial class RetailPViewPassExecutor
|
|||
int count = Math.Min(vertices.Length, world.Length);
|
||||
for (int vertex = 0; vertex < count; vertex++)
|
||||
world[vertex] = vertices[vertex];
|
||||
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeWalkRootEnabled)
|
||||
WalkLookInPunchCountThisFrame++;
|
||||
_portalDepthMask.DrawDepthFan(
|
||||
world[..count],
|
||||
frame.ViewProjection,
|
||||
|
|
@ -201,6 +204,9 @@ internal sealed class WalkProductionLeafRenderer : IWalkFrameLeafRenderer
|
|||
private readonly Action _drawExitSeals;
|
||||
private readonly HashSet<uint> _singleCellScratch = new();
|
||||
private readonly List<uint> _singleCellListScratch = new();
|
||||
private readonly HashSet<uint> _lookInParticleOwnerScratch = new();
|
||||
private RenderFrameView _entityFrame;
|
||||
private bool _entityFrameBound;
|
||||
|
||||
internal WalkProductionLeafRenderer(
|
||||
RetailPViewPassExecutor passes,
|
||||
|
|
@ -242,6 +248,55 @@ internal sealed class WalkProductionLeafRenderer : IWalkFrameLeafRenderer
|
|||
_passes.DrawLandscapeStaticParticles(
|
||||
_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 DrawPunchFan(WalkPolygon worldPolygon, int activeViewIndex) =>
|
||||
|
|
|
|||
|
|
@ -138,6 +138,13 @@ internal sealed partial class RetailPViewPassExecutor :
|
|||
private readonly Dictionary<uint, int> _singleCellClipRouting = new(1);
|
||||
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>
|
||||
/// Borrowed until the next late landscape pass. The outdoor-root post-world
|
||||
/// particle pass consumes this synchronously before another PView frame.
|
||||
|
|
@ -181,7 +188,11 @@ internal sealed partial class RetailPViewPassExecutor :
|
|||
{
|
||||
_particleClassifications.BeginFrame();
|
||||
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeWalkRootEnabled)
|
||||
{
|
||||
AcDream.Core.Rendering.RenderingDiagnostics.WalkRootPhase = "pre";
|
||||
WalkLookInPunchCountThisFrame = 0;
|
||||
LegacyLookInPunchCountThisFrame = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Campaign FW3.2b-2: the shared dispatcher, for
|
||||
|
|
@ -596,8 +607,19 @@ internal sealed partial class RetailPViewPassExecutor :
|
|||
public void DrawLookInPortalPunch(
|
||||
RetailPViewFrameInput frame,
|
||||
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);
|
||||
}
|
||||
|
||||
public void DrawUnattachedSceneParticles(
|
||||
RetailPViewFrameInput frame,
|
||||
|
|
|
|||
|
|
@ -110,6 +110,12 @@ public sealed class RetailPViewRenderer
|
|||
private bool? _probeWalkRootPrevOutdoor;
|
||||
private int _probeWalkRootFramesLeft;
|
||||
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
|
||||
// (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
|
||||
// already be rebuilt for this frame before the walk starts.
|
||||
Walk.WalkFrameDriver? walkDriver = null;
|
||||
WalkProductionLeafRenderer? walkLeafRenderer = null;
|
||||
if (walkActive)
|
||||
{
|
||||
Matrix4x4 view = ctx.CameraView;
|
||||
|
|
@ -343,9 +350,12 @@ public sealed class RetailPViewRenderer
|
|||
Action drawExitSeals = () =>
|
||||
DrawWalkExitPortalMasks(ctx, passes, clipAssembly, walkDriver!);
|
||||
|
||||
var leafRenderer = new WalkProductionLeafRenderer(
|
||||
walkLeafRenderer = new WalkProductionLeafRenderer(
|
||||
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)
|
||||
{
|
||||
|
|
@ -527,7 +537,8 @@ public sealed class RetailPViewRenderer
|
|||
pvFrame,
|
||||
clipAssembly,
|
||||
viewcone,
|
||||
_lookInFrames,
|
||||
walkActive ? walkDriver!.LookInCellTurns : [],
|
||||
walkActive ? walkDriver : null,
|
||||
outsideStageFlood,
|
||||
ctx.Cells,
|
||||
ctx.AnimatedEntityIds,
|
||||
|
|
@ -539,6 +550,8 @@ public sealed class RetailPViewRenderer
|
|||
frameViewBorrowed = true;
|
||||
frameEntityPasses!.BeginEntityFrame(in frameView);
|
||||
entityFrameOpen = true;
|
||||
if (walkActive)
|
||||
walkLeafRenderer!.BindEntityFrame(in frameView);
|
||||
}
|
||||
|
||||
// The retained scene product is the production object source.
|
||||
|
|
@ -651,6 +664,7 @@ public sealed class RetailPViewRenderer
|
|||
clipAssembly,
|
||||
capturedPartition,
|
||||
capturedViewcone,
|
||||
capturedDriver,
|
||||
capturedPasses,
|
||||
in capturedView);
|
||||
};
|
||||
|
|
@ -666,6 +680,7 @@ public sealed class RetailPViewRenderer
|
|||
clipAssembly,
|
||||
partition,
|
||||
viewcone,
|
||||
walkDriver!,
|
||||
frameEntityPasses,
|
||||
in frameView);
|
||||
}
|
||||
|
|
@ -1404,7 +1419,7 @@ public sealed class RetailPViewRenderer
|
|||
_cellParticleOwnerScratch.Clear();
|
||||
foreach (uint cellId in driver.VisitedCells)
|
||||
{
|
||||
if (_lookInCellIds.Contains(cellId))
|
||||
if (driver.LookInCells.Contains(cellId))
|
||||
continue;
|
||||
UnionRecordOwners(_walkWorldData!.GetCellStatics(cellId), _cellParticleOwnerScratch);
|
||||
}
|
||||
|
|
@ -1431,15 +1446,16 @@ public sealed class RetailPViewRenderer
|
|||
/// split — the walk now draws every STATIC route (see
|
||||
/// <see cref="DrawWalkDrivenStatics"/>); this method keeps ONLY what
|
||||
/// stays on the OLD visibility pipeline per the plan's dual-compute
|
||||
/// split: outdoor-cell unattached particles, LookInObject dynamics + their
|
||||
/// per-cell particles, the late per-slice outside-dynamics/weather loop,
|
||||
/// and the late particle union submission.</summary>
|
||||
/// split: outdoor-cell unattached particles, the late per-slice outside-
|
||||
/// dynamics/weather loop, and the late particle union submission. Look-in
|
||||
/// dynamics and their cell particles are now walk-turn-owned.</summary>
|
||||
private void DrawLandscapeDynamicsPhase(
|
||||
RetailPViewFrameInput ctx,
|
||||
IRetailPViewPassExecutor passes,
|
||||
ClipFrameAssembly clipAssembly,
|
||||
InteriorEntityPartition.Result? partition,
|
||||
ViewconeCuller viewcone,
|
||||
Walk.WalkFrameDriver walkDriver,
|
||||
IRenderFrameEntityPassExecutor? frameEntityPasses,
|
||||
in RenderFrameView frameView)
|
||||
{
|
||||
|
|
@ -1452,8 +1468,10 @@ public sealed class RetailPViewRenderer
|
|||
// (the walk owns its own alpha barriers — WalkFrameDriver.OnBuildingTurn).
|
||||
passes.DrawUnattachedSceneParticles(ctx, outdoorCells: true);
|
||||
|
||||
DrawBuildingLookInDynamics(
|
||||
ctx, passes, clipAssembly, partition, frameEntityPasses, in frameView);
|
||||
// Trace-only now. Look-in dynamics no longer have a late production
|
||||
// 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 —
|
||||
// unchanged from DrawLandscapeThroughOutsideView's own late loop.
|
||||
|
|
@ -1527,129 +1545,72 @@ public sealed class RetailPViewRenderer
|
|||
passes.UseIndoorMembershipOnlyRouting();
|
||||
}
|
||||
|
||||
/// <summary>Campaign FW3.2b-2: the DYNAMICS-only remainder of the old
|
||||
/// <see cref="DrawBuildingLookIns"/> — punches, shells, and look-in cell
|
||||
/// STATICS are now walk-owned (<see cref="Walk.WalkFrameDriver"/>'s
|
||||
/// Building/BuildingShell/LookInStatic turns); this method keeps ONLY the
|
||||
/// LookInObject route (now dynamic-classified — see
|
||||
/// <c>RenderScenePViewFrameBuilder.BuildLookInRoutes</c>) and the per-cell
|
||||
/// 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(
|
||||
/// <summary>ACDREAM_PROBE_WALK_ROOT companion for FW4's surviving
|
||||
/// through-wall dynamic. It compares the now-diagnostic-only legacy
|
||||
/// look-in frames with the production walk's exact look-in set and reports
|
||||
/// the two punch producers separately. Print-only; never participates in
|
||||
/// admission.</summary>
|
||||
private void ProbeBuildingLookInFrames(
|
||||
RetailPViewFrameInput ctx,
|
||||
IRetailPViewPassExecutor passes,
|
||||
ClipFrameAssembly clipAssembly,
|
||||
InteriorEntityPartition.Result? partition,
|
||||
IRenderFrameEntityPassExecutor? frameEntityPasses,
|
||||
in RenderFrameView frameView)
|
||||
Walk.WalkFrameDriver walkDriver)
|
||||
{
|
||||
if (_lookInFrames.Count == 0)
|
||||
if (!AcDream.Core.Rendering.RenderingDiagnostics.ProbeWalkRootEnabled)
|
||||
return;
|
||||
|
||||
int lookInRouteIndex = 0;
|
||||
for (int frameIndex = 0; frameIndex < _lookInFrames.Count; frameIndex++)
|
||||
uint[] walkLookInCells = walkDriver.LookInCells
|
||||
.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];
|
||||
for (int i = frame.OrderedVisibleCells.Count - 1; i >= 0; i--)
|
||||
{
|
||||
uint cellId = frame.OrderedVisibleCells[i];
|
||||
var clipKey = new LookInClipCell(frameIndex, cellId);
|
||||
if (!clipAssembly.LookInCellToViewSlices.TryGetValue(
|
||||
clipKey,
|
||||
out ClipViewSlice[]? cellSlices)
|
||||
|| 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();
|
||||
PortalVisibilityFrame frame = _lookInFrames[index];
|
||||
Console.WriteLine(
|
||||
$"[lookin-frame] f={_probeWalkRootFrame} index={index} "
|
||||
+ $"building=0x{frame.SourceBuildingKey:X8} "
|
||||
+ $"landblock=0x{frame.SourceBuildingLandblockId:X8} "
|
||||
+ $"seeds={frame.ExteriorSeedPortals.Count} "
|
||||
+ $"has112={(frame.OrderedVisibleCells.Contains(ProbeCathedralRemoteCellId) ? 1 : 0)} "
|
||||
+ $"cells=[{string.Join(",", frame.OrderedVisibleCells.Select(cell => $"0x{cell:X8}"))}]");
|
||||
}
|
||||
Console.WriteLine(
|
||||
$"[lookin-frame] f={_probeWalkRootFrame} walkCells=["
|
||||
+ string.Join(",", walkLookInCells.Select(cell => $"0x{cell:X8}"))
|
||||
+ "]");
|
||||
}
|
||||
|
||||
private void DrawLandscapeThroughOutsideView(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System.Numerics;
|
||||
using AcDream.Core.Selection;
|
||||
using AcDream.Core.World;
|
||||
using AcDream.App.Rendering.Walk;
|
||||
|
||||
namespace AcDream.App.Rendering.Scene;
|
||||
|
||||
|
|
@ -173,6 +174,13 @@ internal readonly struct RenderFrameView
|
|||
|
||||
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 =>
|
||||
Arena.DiagnosticCounts;
|
||||
|
||||
|
|
@ -247,6 +255,8 @@ internal sealed class RenderFrameArena
|
|||
|
||||
public ClipFrameAssembly? ClipAssembly { get; private set; }
|
||||
|
||||
public IWalkLookInViewSource? WalkLookInViews { get; private set; }
|
||||
|
||||
public RenderSceneDigest SourceDigest { get; private set; }
|
||||
|
||||
public RenderFrameDiagnosticCounts DiagnosticCounts =>
|
||||
|
|
@ -328,6 +338,7 @@ internal sealed class RenderFrameArena
|
|||
_alphaClassificationCount = 0;
|
||||
PortalFrame = null;
|
||||
ClipAssembly = null;
|
||||
WalkLookInViews = null;
|
||||
SourceDigest = default;
|
||||
_sourceDigestSet = false;
|
||||
Generation = generation;
|
||||
|
|
@ -341,11 +352,13 @@ internal sealed class RenderFrameArena
|
|||
internal void SetBorrowedProducts(
|
||||
ulong epoch,
|
||||
PortalVisibilityFrame? portalFrame,
|
||||
ClipFrameAssembly? clipAssembly)
|
||||
ClipFrameAssembly? clipAssembly,
|
||||
IWalkLookInViewSource? walkLookInViews)
|
||||
{
|
||||
EnsureBuilding(epoch);
|
||||
PortalFrame = portalFrame;
|
||||
ClipAssembly = clipAssembly;
|
||||
WalkLookInViews = walkLookInViews;
|
||||
}
|
||||
|
||||
internal void SetSourceDigest(ulong epoch, in RenderSceneDigest digest)
|
||||
|
|
@ -549,6 +562,7 @@ internal sealed class RenderFrameArena
|
|||
ClearReferenceStorage();
|
||||
PortalFrame = null;
|
||||
ClipAssembly = null;
|
||||
WalkLookInViews = null;
|
||||
SourceDigest = default;
|
||||
_sourceDigestSet = false;
|
||||
_state = ArenaState.Available;
|
||||
|
|
@ -655,8 +669,13 @@ internal readonly struct RenderFrameWriter
|
|||
|
||||
public void SetBorrowedProducts(
|
||||
PortalVisibilityFrame? portalFrame,
|
||||
ClipFrameAssembly? clipAssembly) =>
|
||||
Arena.SetBorrowedProducts(_epoch, portalFrame, clipAssembly);
|
||||
ClipFrameAssembly? clipAssembly,
|
||||
IWalkLookInViewSource? walkLookInViews = null) =>
|
||||
Arena.SetBorrowedProducts(
|
||||
_epoch,
|
||||
portalFrame,
|
||||
clipAssembly,
|
||||
walkLookInViews);
|
||||
|
||||
public void SetSourceDigest(in RenderSceneDigest digest) =>
|
||||
Arena.SetSourceDigest(_epoch, in digest);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using AcDream.App.Rendering.Walk;
|
||||
|
||||
namespace AcDream.App.Rendering.Scene;
|
||||
|
||||
|
|
@ -9,7 +10,8 @@ internal readonly record struct RenderScenePViewBuildInput(
|
|||
PortalVisibilityFrame PortalFrame,
|
||||
ClipFrameAssembly ClipAssembly,
|
||||
ViewconeCuller Viewcone,
|
||||
IReadOnlyList<PortalVisibilityFrame> LookInFrames,
|
||||
IReadOnlyList<uint> LookInCellTurns,
|
||||
IWalkLookInViewSource? WalkLookInViews,
|
||||
HashSet<uint> DrawableCells,
|
||||
IRetailPViewCellSource Cells,
|
||||
HashSet<uint>? AnimatedEntityIds,
|
||||
|
|
@ -185,7 +187,8 @@ internal sealed class RenderScenePViewFrameProductController :
|
|||
PortalVisibilityFrame portalFrame,
|
||||
ClipFrameAssembly clipAssembly,
|
||||
ViewconeCuller viewcone,
|
||||
IReadOnlyList<PortalVisibilityFrame> lookInFrames,
|
||||
IReadOnlyList<uint> lookInCellTurns,
|
||||
IWalkLookInViewSource? walkLookInViews,
|
||||
HashSet<uint> drawableCells,
|
||||
IRetailPViewCellSource cells,
|
||||
HashSet<uint>? animatedEntityIds,
|
||||
|
|
@ -195,7 +198,7 @@ internal sealed class RenderScenePViewFrameProductController :
|
|||
ArgumentNullException.ThrowIfNull(portalFrame);
|
||||
ArgumentNullException.ThrowIfNull(clipAssembly);
|
||||
ArgumentNullException.ThrowIfNull(viewcone);
|
||||
ArgumentNullException.ThrowIfNull(lookInFrames);
|
||||
ArgumentNullException.ThrowIfNull(lookInCellTurns);
|
||||
ArgumentNullException.ThrowIfNull(drawableCells);
|
||||
ArgumentNullException.ThrowIfNull(cells);
|
||||
|
||||
|
|
@ -213,7 +216,8 @@ internal sealed class RenderScenePViewFrameProductController :
|
|||
portalFrame,
|
||||
clipAssembly,
|
||||
viewcone,
|
||||
lookInFrames,
|
||||
lookInCellTurns,
|
||||
walkLookInViews,
|
||||
drawableCells,
|
||||
cells,
|
||||
animatedEntityIds,
|
||||
|
|
@ -248,7 +252,8 @@ internal sealed class RenderScenePViewFrameProductController :
|
|||
portalFrame,
|
||||
clipAssembly,
|
||||
viewcone,
|
||||
lookInFrames,
|
||||
FlattenLegacyLookInCells(lookInFrames),
|
||||
walkLookInViews: null,
|
||||
drawableCells,
|
||||
cells,
|
||||
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)
|
||||
{
|
||||
_packedClassification =
|
||||
|
|
@ -1171,7 +1189,8 @@ internal sealed class RenderScenePViewFrameBuilder
|
|||
{
|
||||
writer.SetBorrowedProducts(
|
||||
input.PortalFrame,
|
||||
input.ClipAssembly);
|
||||
input.ClipAssembly,
|
||||
input.WalkLookInViews);
|
||||
RenderSceneDigest digest = input.SourceDigest;
|
||||
writer.SetSourceDigest(in digest);
|
||||
_projectionIds.Clear();
|
||||
|
|
@ -1182,19 +1201,11 @@ internal sealed class RenderScenePViewFrameBuilder
|
|||
// outdoor static, building shell, and cell static (including
|
||||
// look-in cell statics) directly through OrderedDrawStream (plan
|
||||
// §FW3 "FW3.2b-2 — the production rooting", item 3). LookInObject
|
||||
// keeps emitting, but BuildLookInRoutes below is now filtered to
|
||||
// DYNAMIC candidates only — the walk owns that route's statics.
|
||||
int lookInRouteIndex = 0;
|
||||
for (int frameIndex = 0;
|
||||
frameIndex < input.LookInFrames.Count;
|
||||
frameIndex++)
|
||||
{
|
||||
BuildLookInRoutes(
|
||||
writer,
|
||||
in input,
|
||||
frameIndex,
|
||||
ref lookInRouteIndex);
|
||||
}
|
||||
// keeps the packed classifier for live animation/fades, but its
|
||||
// route ranges are keyed to THE WALK'S own look-in cell turns.
|
||||
// The legacy PortalVisibilityFrame list no longer has a production
|
||||
// admission or ordering role.
|
||||
BuildLookInRoutes(writer, in input);
|
||||
BuildOutsideDynamicRoutes(writer, in input);
|
||||
BuildDynamicLastRoute(writer, in input);
|
||||
writer.Publish();
|
||||
|
|
@ -1284,45 +1295,37 @@ internal sealed class RenderScenePViewFrameBuilder
|
|||
/// item 3).</summary>
|
||||
private void BuildLookInRoutes(
|
||||
RenderFrameWriter writer,
|
||||
in RenderScenePViewBuildInput input,
|
||||
int frameIndex,
|
||||
ref int routeIndex)
|
||||
in RenderScenePViewBuildInput input)
|
||||
{
|
||||
PortalVisibilityFrame frame = input.LookInFrames[frameIndex];
|
||||
for (int i = frame.OrderedVisibleCells.Count - 1; i >= 0; i--)
|
||||
for (int routeIndex = 0; routeIndex < input.LookInCellTurns.Count; routeIndex++)
|
||||
{
|
||||
uint cellId = frame.OrderedVisibleCells[i];
|
||||
var clipKey = new LookInClipCell(frameIndex, cellId);
|
||||
if (!input.ClipAssembly.LookInCellToViewSlices.TryGetValue(
|
||||
clipKey,
|
||||
out ClipViewSlice[]? slices)
|
||||
|| slices.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
uint cellId = input.LookInCellTurns[routeIndex];
|
||||
int count = LoadCell(
|
||||
input.Scene,
|
||||
cellId,
|
||||
includeStatics: false,
|
||||
includeDynamics: true);
|
||||
for (int sliceIndex = 0; sliceIndex < slices.Length; sliceIndex++)
|
||||
{
|
||||
int currentRouteIndex = routeIndex++;
|
||||
if (count == 0)
|
||||
continue;
|
||||
if (count == 0)
|
||||
continue;
|
||||
|
||||
for (int index = 0; index < count; index++)
|
||||
AddProjection(
|
||||
writer,
|
||||
in _cell[index],
|
||||
input.AnimatedEntityIds);
|
||||
writer.AddRouteRange(
|
||||
RenderFrameCandidateRoute.LookInObject,
|
||||
currentRouteIndex,
|
||||
cellId,
|
||||
_cell.AsSpan(0, count));
|
||||
int survivorCount = 0;
|
||||
EnsureCapacity(ref _survivors, count);
|
||||
for (int index = 0; index < count; index++)
|
||||
{
|
||||
RenderProjectionRecord record = _cell[index];
|
||||
_survivors[survivorCount++] = record;
|
||||
AddProjection(
|
||||
writer,
|
||||
in record,
|
||||
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)
|
||||
{
|
||||
_lookInCellScratch.Clear();
|
||||
for (int frameIndex = 0;
|
||||
frameIndex < input.LookInFrames.Count;
|
||||
frameIndex++)
|
||||
{
|
||||
PortalVisibilityFrame frame = input.LookInFrames[frameIndex];
|
||||
for (int i = 0; i < frame.OrderedVisibleCells.Count; i++)
|
||||
_lookInCellScratch.Add(frame.OrderedVisibleCells[i]);
|
||||
}
|
||||
for (int index = 0; index < input.LookInCellTurns.Count; index++)
|
||||
_lookInCellScratch.Add(input.LookInCellTurns[index]);
|
||||
|
||||
int count = 0;
|
||||
EnsureCapacity(ref _survivors, _dynamicCount);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Gpu;
|
||||
using AcDream.App.Rendering.Scene;
|
||||
|
|
@ -118,6 +119,17 @@ internal interface IWalkFrameLeafRenderer
|
|||
/// positional invariant this carries (the #132 falls containment).</summary>
|
||||
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
|
||||
/// (pc:432731-432732) between the outside stage and the interior root's
|
||||
/// 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
|
||||
/// `e102fb36` encoded the same invariant).</summary>
|
||||
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
|
||||
/// kind. A single struct (rather than a kind hierarchy) keeps Collect's
|
||||
/// per-turn list a flat, allocation-cheap <c>List<WalkFrameEvent></c> —
|
||||
|
|
@ -320,6 +361,9 @@ internal readonly struct WalkFrameEvent
|
|||
internal static WalkFrameEvent BuildingShellParticles(WalkBuilding 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() =>
|
||||
new(WalkFrameEventKind.ClearInteriorDepth, 0, 0, 0f, null);
|
||||
|
||||
|
|
@ -404,7 +448,7 @@ internal readonly struct WalkFrameEvent
|
|||
/// @0x0059f2a0 (the <c>part->gfxobj[deg_level]!=0</c> gate @0x0059f2d3
|
||||
/// and the alpha-barrier → portal-pass → shell order @0x0059f30b–0x0059f345).</para>
|
||||
/// </summary>
|
||||
internal sealed class WalkFrameDriver : IWalkEventSink
|
||||
internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
||||
{
|
||||
private readonly WbDrawDispatcher _dispatcher;
|
||||
private readonly WalkStaticStreamPopulator _populator;
|
||||
|
|
@ -422,6 +466,23 @@ internal sealed class WalkFrameDriver : IWalkEventSink
|
|||
// walk pass dedicated to nothing but set-gathering.
|
||||
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,
|
||||
/// exactly as retail's <c>PView::DrawCells</c> iterates it for the
|
||||
/// exit-portal seals (pc:432785-432786) — captured at
|
||||
|
|
@ -459,6 +520,7 @@ internal sealed class WalkFrameDriver : IWalkEventSink
|
|||
private bool _skyDrawnThisFrame;
|
||||
private WalkDrawStage? _currentDcStage;
|
||||
private bool _readyToReplay;
|
||||
private int _lookInRouteIndex;
|
||||
|
||||
internal WalkFrameDriver(
|
||||
WbDrawDispatcher dispatcher,
|
||||
|
|
@ -534,7 +596,7 @@ internal sealed class WalkFrameDriver : IWalkEventSink
|
|||
/// GPU work — see this type's own doc comment.
|
||||
/// </summary>
|
||||
internal void BeginFrame(
|
||||
IWalkBuildingFrameContext ctx,
|
||||
IRetailFrameWalkContext ctx,
|
||||
Matrix4x4 viewProjection,
|
||||
Vector3 cameraWorldPosition)
|
||||
{
|
||||
|
|
@ -557,9 +619,16 @@ internal sealed class WalkFrameDriver : IWalkEventSink
|
|||
_events.Clear();
|
||||
_markPositions.Clear();
|
||||
VisitedCells.Clear();
|
||||
LookInCellTurns.Clear();
|
||||
_lookInTurns.Clear();
|
||||
_lookInSlices.Clear();
|
||||
_lookInPlanes.Clear();
|
||||
_lookInCyPlane = ctx.CyPlane;
|
||||
LookInCells.Clear();
|
||||
VisitedBuildings.Clear();
|
||||
VisitedLandscapeCellIds.Clear();
|
||||
InteriorFloodCells.Clear();
|
||||
_lookInRouteIndex = 0;
|
||||
}
|
||||
|
||||
/// <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)
|
||||
_leafRenderer.DrawStaticParticles(_staticParticleOwnerScratch);
|
||||
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(
|
||||
_stream, stage, cellId, records.Records, records.TupleLandblockId,
|
||||
_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
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ using AcDream.Core.Lighting;
|
|||
using AcDream.Core.Meshing;
|
||||
using AcDream.Core.Selection;
|
||||
using AcDream.Core.World;
|
||||
using AcDream.App.Rendering.Walk;
|
||||
using DatReaderWriter.Enums;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb;
|
||||
|
|
@ -40,6 +41,8 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
private RenderSceneGeneration _packedProductionGeneration;
|
||||
private ulong _packedProductionFrameSequence;
|
||||
private int _packedProductionNextRange;
|
||||
private readonly Dictionary<(int RouteIndex, uint LocalEntityId, int PartIndex, uint GfxObjId), string>
|
||||
_probeLookInPartStates = [];
|
||||
|
||||
internal IReadOnlyList<CurrentRenderDispatcherSubmission>
|
||||
PackedDispatcherSubmissions => _packedSubmissions;
|
||||
|
|
@ -295,7 +298,12 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
source.MeshPartOffset,
|
||||
source.MeshPartCount),
|
||||
ref anyVao,
|
||||
publishSelection);
|
||||
publishSelection,
|
||||
range.Route is RenderFrameCandidateRoute.LookInObject
|
||||
? view.WalkLookInViews
|
||||
: null,
|
||||
range.RouteIndex,
|
||||
range.CellId);
|
||||
}
|
||||
|
||||
return new PackedRangeClassification(
|
||||
|
|
@ -334,7 +342,10 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
in RenderInstanceCandidate entity,
|
||||
ReadOnlySpan<RenderFrameMeshPart> meshParts,
|
||||
ref uint anyVao,
|
||||
bool publishSelection)
|
||||
bool publishSelection,
|
||||
IWalkLookInViewSource? lookInViews,
|
||||
int lookInRouteIndex,
|
||||
uint lookInCellId)
|
||||
{
|
||||
(uint slot, bool culled) = ResolveSlotForFrame(
|
||||
_clipRoutingActive,
|
||||
|
|
@ -361,7 +372,8 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
: new Vector2(0f, 1f);
|
||||
|
||||
PackedProjectionClassificationEntry? cacheEntry = null;
|
||||
if (!entity.Animated)
|
||||
bool lookInConeActive = lookInViews is not null;
|
||||
if (!entity.Animated && !lookInConeActive)
|
||||
{
|
||||
PackedClassificationIdentity identity =
|
||||
PackedClassificationIdentity.From(in projection);
|
||||
|
|
@ -394,7 +406,7 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
entity.ProjectionId,
|
||||
in identity);
|
||||
}
|
||||
else
|
||||
else if (entity.Animated)
|
||||
{
|
||||
_packedClassificationCache.RecordAnimatedClassification();
|
||||
}
|
||||
|
|
@ -485,6 +497,21 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
partTransform * meshRef.PartTransform;
|
||||
Matrix4x4 model =
|
||||
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(
|
||||
partData,
|
||||
restPose,
|
||||
|
|
@ -502,9 +529,6 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
{
|
||||
reusableAcrossFrames = false;
|
||||
}
|
||||
int selectionPartIndex = unchecked(
|
||||
(partIndex << 16)
|
||||
| (setupPartIndex & 0xFFFF));
|
||||
cacheEntry?.SelectionParts.Add(
|
||||
new PackedClassifiedSelectionPart(
|
||||
selectionPartIndex,
|
||||
|
|
@ -538,6 +562,18 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
|
||||
Matrix4x4 restPose = meshRef.PartTransform;
|
||||
Matrix4x4 model = restPose * entity.RootWorld;
|
||||
if (!PartVisibleInLookInTurn(
|
||||
lookInViews,
|
||||
lookInRouteIndex,
|
||||
lookInCellId,
|
||||
in entity,
|
||||
partIndex,
|
||||
meshRef.GfxObjId,
|
||||
renderData,
|
||||
model))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!ClassifyPackedBatches(
|
||||
renderData,
|
||||
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>
|
||||
/// Mirrors the production dispatcher's no-VAO early return. That return
|
||||
/// records an empty submission with transparent deferral disabled even
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue