refactor(render): extract typed retail pview passes
This commit is contained in:
parent
6d6e5b5fa5
commit
85239fb373
13 changed files with 1888 additions and 841 deletions
|
|
@ -1,9 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using AcDream.Core.World;
|
||||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
|
|
@ -14,10 +12,6 @@ namespace AcDream.App.Rendering;
|
|||
/// </summary>
|
||||
public sealed class RetailPViewRenderer
|
||||
{
|
||||
private readonly GL _gl;
|
||||
private readonly ClipFrame _clipFrame;
|
||||
private readonly EnvCellRenderer _envCells;
|
||||
private readonly WbDrawDispatcher _entities;
|
||||
private readonly PortalVisibilityFrame _mainPortalFrameScratch = new();
|
||||
private readonly ClipFrameAssembly _clipAssemblyScratch = new();
|
||||
private readonly ViewconeCuller _viewconeScratch = new();
|
||||
|
|
@ -76,28 +70,20 @@ public sealed class RetailPViewRenderer
|
|||
// gather); seeds themselves are unbounded.
|
||||
private const float OutdoorBuildingSeedDistance = float.PositiveInfinity;
|
||||
|
||||
public RetailPViewRenderer(
|
||||
GL gl,
|
||||
ClipFrame clipFrame,
|
||||
EnvCellRenderer envCells,
|
||||
WbDrawDispatcher entities)
|
||||
{
|
||||
_gl = gl;
|
||||
_clipFrame = clipFrame;
|
||||
_envCells = envCells;
|
||||
_entities = entities;
|
||||
}
|
||||
|
||||
public RetailPViewFrameResult DrawInside(RetailPViewDrawContext ctx)
|
||||
public RetailPViewFrameResult DrawInside(
|
||||
RetailPViewFrameInput ctx,
|
||||
IRetailPViewPassExecutor passes)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(ctx);
|
||||
ArgumentNullException.ThrowIfNull(passes);
|
||||
passes.BeginFrame();
|
||||
RecycleLookInFrames();
|
||||
ResetBuildingGroups();
|
||||
|
||||
var pvFrame = PortalVisibilityBuilder.Build(
|
||||
ctx.RootCell,
|
||||
ctx.ViewerEyePos,
|
||||
ctx.CellLookup,
|
||||
ctx.Cells.Find,
|
||||
ctx.ViewProjection,
|
||||
buildingMembership: null,
|
||||
drawLiftZ: PortalVisibilityBuilder.ShellDrawLiftZ,
|
||||
|
|
@ -131,12 +117,11 @@ public sealed class RetailPViewRenderer
|
|||
&& pvFrame.OutsideView.Polygons.Count > 0)
|
||||
BuildInteriorRootLookIns(ctx, pvFrame);
|
||||
|
||||
var clipAssembly = ClipFrameAssembler.Assemble(
|
||||
_clipFrame,
|
||||
var clipAssembly = passes.AssembleClipFrame(
|
||||
pvFrame,
|
||||
_clipAssemblyScratch);
|
||||
int terrainUploadCount = checked(1 + clipAssembly.OutsideViewSlices.Length * 2);
|
||||
PrepareClipFrame(ctx.SetTerrainClipUbo, terrainUploadCount);
|
||||
passes.PrepareClipFrame(terrainUploadCount);
|
||||
|
||||
// R1: draw EVERY visible cell (retail cell_draw_list), not only the cells the
|
||||
// assembler handed a clip-slot. This feeds the Prepare filter + entity partition,
|
||||
|
|
@ -146,7 +131,7 @@ public sealed class RetailPViewRenderer
|
|||
_drawableCellsScratch.Clear();
|
||||
_drawableCellsScratch.UnionWith(pvFrame.OrderedVisibleCells);
|
||||
var drawableCells = _drawableCellsScratch;
|
||||
UseIndoorMembershipOnlyRouting();
|
||||
passes.UseIndoorMembershipOnlyRouting();
|
||||
|
||||
// #124: look-in cells need prepared shell batches + their statics routed
|
||||
// into partition.ByCell (consumed ONLY by DrawBuildingLookIns — the main
|
||||
|
|
@ -169,13 +154,7 @@ public sealed class RetailPViewRenderer
|
|||
// the RESIDENT-cell registry, not the frame flood — and is deleted. The pool
|
||||
// is built once per frame in GameWindow, player-anchored.)
|
||||
|
||||
_envCells.PrepareRenderBatches(
|
||||
ctx.ViewProjection,
|
||||
ctx.CameraWorldPosition,
|
||||
filter: prepareCells,
|
||||
centerLbX: ctx.RenderCenterLbX,
|
||||
centerLbY: ctx.RenderCenterLbY,
|
||||
renderRadius: ctx.RenderRadius);
|
||||
passes.PrepareCellBatches(ctx, prepareCells);
|
||||
|
||||
InteriorEntityPartition.Partition(_partitionResult, prepareCells, ctx.LandblockEntries);
|
||||
var partition = _partitionResult;
|
||||
|
|
@ -185,7 +164,7 @@ public sealed class RetailPViewRenderer
|
|||
drawableCells,
|
||||
partition);
|
||||
|
||||
ctx.EmitDiagnostics?.Invoke(result);
|
||||
passes.EmitDiagnostics(ctx, result);
|
||||
|
||||
// T1 (fused BR-2/3): retail's frame order — static world, then the
|
||||
// aperture depth writes, then interior cells WHOLE far→near, then
|
||||
|
|
@ -226,17 +205,17 @@ public sealed class RetailPViewRenderer
|
|||
foreach (var e in partition.Dynamics)
|
||||
{
|
||||
EntitySphere(e, out var c, out float r);
|
||||
if (DynamicDrawsInOutsideStage(e.ParentCellId, c, r, drawableCells, ctx.CellLookup))
|
||||
if (DynamicDrawsInOutsideStage(e.ParentCellId, c, r, drawableCells, ctx.Cells))
|
||||
_outsideStageDynamics.Add(e);
|
||||
}
|
||||
}
|
||||
|
||||
DrawLandscapeThroughOutsideView(ctx, clipAssembly, partition, viewcone);
|
||||
UseIndoorMembershipOnlyRouting();
|
||||
DrawExitPortalMasks(ctx, pvFrame, clipAssembly, drawableCells);
|
||||
DrawEnvCellShells(pvFrame);
|
||||
DrawCellObjectLists(ctx, pvFrame, clipAssembly, drawableCells, partition, viewcone);
|
||||
DrawDynamicsLast(ctx, partition, viewcone, ctx.RootCell.IsOutdoorNode);
|
||||
DrawLandscapeThroughOutsideView(ctx, passes, clipAssembly, partition, viewcone);
|
||||
passes.UseIndoorMembershipOnlyRouting();
|
||||
DrawExitPortalMasks(ctx, passes, pvFrame, clipAssembly, drawableCells);
|
||||
DrawEnvCellShells(passes, pvFrame);
|
||||
DrawCellObjectLists(ctx, passes, pvFrame, clipAssembly, drawableCells, partition, viewcone);
|
||||
DrawDynamicsLast(ctx, passes, partition, viewcone, ctx.RootCell.IsOutdoorNode);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
|
@ -244,7 +223,7 @@ public sealed class RetailPViewRenderer
|
|||
// R-A2: group the nearby building cells by BuildingId and run one per-building flood per group
|
||||
// (retail's per-building ConstructView(CBldPortal)), merging each small view into the frame. The
|
||||
// grouping dict contains only this frame's keys; lists are pooled across frames.
|
||||
private void MergeNearbyBuildingFloods(RetailPViewDrawContext ctx, PortalVisibilityFrame pvFrame)
|
||||
private void MergeNearbyBuildingFloods(RetailPViewFrameInput ctx, PortalVisibilityFrame pvFrame)
|
||||
{
|
||||
RebuildBuildingGroups(ctx.NearbyBuildingCells!);
|
||||
|
||||
|
|
@ -255,7 +234,7 @@ public sealed class RetailPViewRenderer
|
|||
var buildingFrame = PortalVisibilityBuilder.ConstructViewBuilding(
|
||||
group,
|
||||
ctx.ViewerEyePos,
|
||||
ctx.CellLookup,
|
||||
ctx.Cells.Find,
|
||||
ctx.ViewProjection,
|
||||
OutdoorBuildingSeedDistance,
|
||||
reuseFrame: _outdoorBuildingFrameScratch);
|
||||
|
|
@ -304,7 +283,7 @@ public sealed class RetailPViewRenderer
|
|||
// doorway, ConstructView(CBldPortal) 0x005a59a0 via PView::GetClip
|
||||
// 0x005a4320). Same grouping as MergeNearbyBuildingFloods; the root's own
|
||||
// building self-excludes via the seed eye-side test.
|
||||
private void BuildInteriorRootLookIns(RetailPViewDrawContext ctx, PortalVisibilityFrame pvFrame)
|
||||
private void BuildInteriorRootLookIns(RetailPViewFrameInput ctx, PortalVisibilityFrame pvFrame)
|
||||
{
|
||||
RebuildBuildingGroups(ctx.NearbyBuildingCells!);
|
||||
|
||||
|
|
@ -316,7 +295,7 @@ public sealed class RetailPViewRenderer
|
|||
? _lookInFramePool.Pop()
|
||||
: new PortalVisibilityFrame();
|
||||
var frame = PortalVisibilityBuilder.ConstructViewBuilding(
|
||||
group, ctx.ViewerEyePos, ctx.CellLookup, ctx.ViewProjection,
|
||||
group, ctx.ViewerEyePos, ctx.Cells.Find, ctx.ViewProjection,
|
||||
OutdoorBuildingSeedDistance, pvFrame.OutsideView.Polygons,
|
||||
reuseFrame: frameScratch);
|
||||
if (frame.OrderedVisibleCells.Count > 0)
|
||||
|
|
@ -365,7 +344,8 @@ public sealed class RetailPViewRenderer
|
|||
// here is color-safe; statics draw whole (the main viewcone has no entry
|
||||
// for look-in cells; over-include is the safe direction).
|
||||
private void DrawBuildingLookIns(
|
||||
RetailPViewDrawContext ctx,
|
||||
RetailPViewFrameInput ctx,
|
||||
IRetailPViewPassExecutor passes,
|
||||
ClipFrameAssembly clipAssembly,
|
||||
InteriorEntityPartition.Result partition)
|
||||
{
|
||||
|
|
@ -375,30 +355,27 @@ public sealed class RetailPViewRenderer
|
|||
foreach (var frame in _lookInFrames)
|
||||
{
|
||||
// Pass 1: far-Z punch every aperture of this building.
|
||||
if (ctx.DrawLookInPortalPunch is not null)
|
||||
foreach (uint cellId in frame.OrderedVisibleCells)
|
||||
{
|
||||
foreach (uint cellId in frame.OrderedVisibleCells)
|
||||
if (!frame.CellViews.TryGetValue(cellId, out var view))
|
||||
continue;
|
||||
foreach (var poly in view.Polygons)
|
||||
{
|
||||
if (!frame.CellViews.TryGetValue(cellId, out var view))
|
||||
var cps = ClipPlaneSet.From(poly);
|
||||
if (cps.IsNothingVisible)
|
||||
continue;
|
||||
foreach (var poly in view.Polygons)
|
||||
{
|
||||
var cps = ClipPlaneSet.From(poly);
|
||||
if (cps.IsNothingVisible)
|
||||
continue;
|
||||
ctx.DrawLookInPortalPunch(new RetailPViewCellSliceContext(
|
||||
cellId,
|
||||
new ClipViewSlice(
|
||||
0,
|
||||
new Vector4(poly.MinX, poly.MinY, poly.MaxX, poly.MaxY),
|
||||
cps.PlaneArray),
|
||||
Array.Empty<WorldEntity>()));
|
||||
}
|
||||
passes.DrawLookInPortalPunch(ctx, new RetailPViewCellSliceContext(
|
||||
cellId,
|
||||
new ClipViewSlice(
|
||||
0,
|
||||
new Vector4(poly.MinX, poly.MinY, poly.MaxX, poly.MaxY),
|
||||
cps.PlaneArray),
|
||||
Array.Empty<WorldEntity>()));
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 2: shells + statics, far→near.
|
||||
UseIndoorMembershipOnlyRouting();
|
||||
passes.UseIndoorMembershipOnlyRouting();
|
||||
|
||||
// Opaque shells batched per building into ONE Render (this building's
|
||||
// aperture punches above already ran; z-buffer handles order and
|
||||
|
|
@ -408,7 +385,7 @@ public sealed class RetailPViewRenderer
|
|||
foreach (uint cid in frame.OrderedVisibleCells)
|
||||
_shellBatch.Add(cid);
|
||||
if (_shellBatch.Count > 0)
|
||||
_envCells.Render(WbRenderPass.Opaque, _shellBatch);
|
||||
passes.DrawOpaqueCellShells(_shellBatch);
|
||||
|
||||
for (int i = frame.OrderedVisibleCells.Count - 1; i >= 0; i--)
|
||||
{
|
||||
|
|
@ -417,8 +394,8 @@ public sealed class RetailPViewRenderer
|
|||
_oneCell.Add(cellId);
|
||||
// Opaque shell batched above. Transparent stays per-cell (far→near)
|
||||
// for correct compositing; skipped for opaque-only cells.
|
||||
if (_envCells.CellHasTransparent(cellId))
|
||||
_envCells.Render(WbRenderPass.Transparent, _oneCell);
|
||||
if (passes.CellHasTransparentShell(cellId))
|
||||
passes.DrawTransparentCellShells(_oneCell);
|
||||
|
||||
_cellStaticScratch.Clear();
|
||||
if (partition.ByCell.TryGetValue(cellId, out var bucket))
|
||||
|
|
@ -443,12 +420,12 @@ public sealed class RetailPViewRenderer
|
|||
|
||||
if (_cellStaticScratch.Count > 0)
|
||||
{
|
||||
DrawEntityBucket(ctx, _cellStaticScratch, _oneCell);
|
||||
passes.DrawEntityBucket(ctx, _cellStaticScratch, _oneCell);
|
||||
|
||||
// The cell-particles pass for look-in cells — retail's
|
||||
// nested DrawCells draws objects WITH their emitters.
|
||||
foreach (var slice in GetCellSlicesOrNoClip(clipAssembly, cellId))
|
||||
ctx.DrawCellParticles?.Invoke(new RetailPViewCellSliceContext(
|
||||
passes.DrawCellParticles(ctx, new RetailPViewCellSliceContext(
|
||||
cellId, slice, _cellStaticScratch));
|
||||
}
|
||||
}
|
||||
|
|
@ -456,7 +433,8 @@ public sealed class RetailPViewRenderer
|
|||
}
|
||||
|
||||
private void DrawLandscapeThroughOutsideView(
|
||||
RetailPViewDrawContext ctx,
|
||||
RetailPViewFrameInput ctx,
|
||||
IRetailPViewPassExecutor passes,
|
||||
ClipFrameAssembly clipAssembly,
|
||||
InteriorEntityPartition.Result partition,
|
||||
ViewconeCuller viewcone)
|
||||
|
|
@ -479,16 +457,15 @@ public sealed class RetailPViewRenderer
|
|||
int probeSliceIndex = 0;
|
||||
foreach (var slice in clipAssembly.OutsideViewSlices)
|
||||
{
|
||||
_clipFrame.SetTerrainClip(slice.Planes);
|
||||
UploadTerrainClip(ctx.SetTerrainClipUbo);
|
||||
passes.SetTerrainClip(slice.Planes);
|
||||
// T3 (BR-5): entities are never hard-clipped — retail viewcone-
|
||||
// CHECKS each mesh's sphere against the view (Ghidra 0x0054c250)
|
||||
// and draws it whole. The old per-slice entity clip routing
|
||||
// (gl_ClipDistance via SetClipRouting) is replaced by the sphere
|
||||
// pre-filter below; terrain/sky keep their per-slice plane clip.
|
||||
_entities.ClearClipRouting();
|
||||
passes.ClearClipRouting();
|
||||
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeClipRouteEnabled)
|
||||
EmitClipRouteProbe(clipAssembly, slice, probeSliceIndex);
|
||||
passes.EmitClipRouteProbe(clipAssembly, slice, probeSliceIndex);
|
||||
|
||||
_outdoorStaticScratch.Clear();
|
||||
foreach (var e in partition.OutdoorStatic)
|
||||
|
|
@ -498,14 +475,14 @@ public sealed class RetailPViewRenderer
|
|||
_outdoorStaticScratch.Add(e);
|
||||
}
|
||||
probeSliceIndex++;
|
||||
ctx.DrawLandscapeSlice(new RetailPViewLandscapeSliceContext(slice, _outdoorStaticScratch));
|
||||
passes.DrawLandscapeSlice(ctx, new RetailPViewLandscapeSliceContext(slice, _outdoorStaticScratch));
|
||||
}
|
||||
|
||||
// #124: far-building look-ins draw HERE — still inside the landscape
|
||||
// stage (their punches mark against the terrain/exterior depth just
|
||||
// drawn), strictly BEFORE the depth clear + seals below, matching
|
||||
// retail's LScape::draw placement (DrawCells pc:432719 vs 432732/432785).
|
||||
DrawBuildingLookIns(ctx, clipAssembly, partition);
|
||||
DrawBuildingLookIns(ctx, passes, clipAssembly, partition);
|
||||
|
||||
// LATE phase (per slice): outside-stage dynamics' meshes (#118 — drawn
|
||||
// pre-clear so the seal protects their aperture pixels; AFTER the
|
||||
|
|
@ -515,9 +492,8 @@ public sealed class RetailPViewRenderer
|
|||
probeSliceIndex = 0;
|
||||
foreach (var slice in clipAssembly.OutsideViewSlices)
|
||||
{
|
||||
_clipFrame.SetTerrainClip(slice.Planes);
|
||||
UploadTerrainClip(ctx.SetTerrainClipUbo);
|
||||
_entities.ClearClipRouting();
|
||||
passes.SetTerrainClip(slice.Planes);
|
||||
passes.ClearClipRouting();
|
||||
|
||||
_outdoorStaticScratch.Clear(); // late: dynamics survivors
|
||||
_lateParticleOwnerScratch.Clear(); // late: statics + dynamics survivors
|
||||
|
|
@ -530,14 +506,12 @@ public sealed class RetailPViewRenderer
|
|||
// #131 owner watchlist (throwaway): ACDREAM_DUMP_ENTITY ids
|
||||
// double as an ENTITY-id watchlist here — one line per watched
|
||||
// outdoor-static owner per CHANGE of its cone verdict.
|
||||
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeOutStageEnabled
|
||||
&& AcDream.Core.Rendering.RenderingDiagnostics.DumpEntitySourceIds.Contains(e.Id)
|
||||
&& (!_outStageOwnerVerdicts.TryGetValue(e.Id, out bool prev) || prev != ownerPass))
|
||||
{
|
||||
_outStageOwnerVerdicts[e.Id] = ownerPass;
|
||||
Console.WriteLine(System.FormattableString.Invariant(
|
||||
$"[outstage-own] id=0x{e.Id:X8} src=0x{e.SourceGfxObjOrSetupId:X8} pos=({e.Position.X:F1},{e.Position.Y:F1},{e.Position.Z:F1}) c=({c.X:F1},{c.Y:F1},{c.Z:F1}) r={r:F1} slice={probeSliceIndex} {(ownerPass ? "PASS" : "CULL")}"));
|
||||
}
|
||||
passes.EmitOutStageOwner(
|
||||
e,
|
||||
c,
|
||||
r,
|
||||
probeSliceIndex,
|
||||
ownerPass);
|
||||
}
|
||||
foreach (var e in _outsideStageDynamics)
|
||||
{
|
||||
|
|
@ -548,10 +522,12 @@ public sealed class RetailPViewRenderer
|
|||
_lateParticleOwnerScratch.Add(e);
|
||||
}
|
||||
}
|
||||
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeOutStageEnabled)
|
||||
EmitOutStageProbe(probeSliceIndex, viewcone);
|
||||
passes.EmitOutStageRouting(
|
||||
probeSliceIndex,
|
||||
_outsideStageDynamics,
|
||||
viewcone);
|
||||
probeSliceIndex++;
|
||||
ctx.DrawLandscapeSliceLate?.Invoke(new RetailPViewLandscapeLateSliceContext(
|
||||
passes.DrawLandscapeSliceLate(ctx, new RetailPViewLandscapeLateSliceContext(
|
||||
slice, _outdoorStaticScratch, _lateParticleOwnerScratch));
|
||||
}
|
||||
|
||||
|
|
@ -563,12 +539,12 @@ public sealed class RetailPViewRenderer
|
|||
// must not double-draw, the #121 lesson), at the END of the landscape
|
||||
// stage: after the clear they would z-fail against the doorway seal.
|
||||
if (!ctx.RootCell.IsOutdoorNode)
|
||||
ctx.DrawUnattachedSceneParticles?.Invoke();
|
||||
passes.DrawUnattachedSceneParticles(ctx);
|
||||
|
||||
// Retail PView::DrawCells 0x005A4872 drains the landscape alpha list
|
||||
// immediately after LScape::draw and before the optional depth clear.
|
||||
// The queue remains active for the post-clear/final-world scope.
|
||||
ctx.FlushLandscapeAlpha?.Invoke();
|
||||
passes.FlushLandscapeAlpha();
|
||||
|
||||
// T1: retail clears the FULL depth buffer ONCE between the outside
|
||||
// stage and the interior stage (PView::DrawCells, Ghidra 0x005a4840 —
|
||||
|
|
@ -577,133 +553,19 @@ public sealed class RetailPViewRenderer
|
|||
// every outside-leading portal's TRUE depth (the seals,
|
||||
// DrawExitPortalMasks). Replaces the old per-slice scissored AABB
|
||||
// clear (wrong shape, no seal after it).
|
||||
if (clipAssembly.OutsideViewSlices.Length > 0)
|
||||
ctx.ClearDepthForInterior?.Invoke();
|
||||
if (clipAssembly.OutsideViewSlices.Length > 0 && !ctx.RootCell.IsOutdoorNode)
|
||||
passes.ClearInteriorDepth();
|
||||
|
||||
UseIndoorMembershipOnlyRouting();
|
||||
}
|
||||
|
||||
// #131 [outstage] probe state (2026-06-12, throwaway): print-on-change —
|
||||
// which outdoor dynamics were routed to the outside stage and which
|
||||
// survived the slice viewcone. Strip with the probe when #131 closes.
|
||||
private string? _lastOutStageSig;
|
||||
private readonly Dictionary<uint, bool> _outStageOwnerVerdicts = new();
|
||||
|
||||
private void EmitOutStageProbe(int sliceIndex, ViewconeCuller viewcone)
|
||||
{
|
||||
var sb = new System.Text.StringBuilder(192);
|
||||
sb.Append("slice=").Append(sliceIndex)
|
||||
.Append(" outStage=").Append(_outsideStageDynamics.Count).Append(" [");
|
||||
for (int i = 0; i < _outsideStageDynamics.Count; i++)
|
||||
{
|
||||
var e = _outsideStageDynamics[i];
|
||||
EntitySphere(e, out var c, out float r);
|
||||
bool pass = viewcone.SphereVisibleInOutsideSlice(sliceIndex, c, r);
|
||||
if (i > 0) sb.Append(' ');
|
||||
sb.Append(System.FormattableString.Invariant(
|
||||
$"0x{(e.ServerGuid != 0 ? e.ServerGuid : e.Id):X8}(s{e.SourceGfxObjOrSetupId:X8}):{(pass ? "PASS" : "CULL")}:r={r:F1}"));
|
||||
}
|
||||
sb.Append(']');
|
||||
string sig = sb.ToString();
|
||||
if (sig == _lastOutStageSig) return;
|
||||
_lastOutStageSig = sig;
|
||||
Console.WriteLine("[outstage] " + sig);
|
||||
}
|
||||
|
||||
// §4 flap [clip-route] probe state (2026-06-10, throwaway): print-on-change signature +
|
||||
// monotonic sequence so held-flap vs healthy frames diff cleanly in one capture.
|
||||
private string? _lastClipRouteSig;
|
||||
private long _clipRouteSeq;
|
||||
private readonly List<uint> _clipRouteCellKeys = new();
|
||||
|
||||
// §4 flap apparatus (2026-06-10): the decisive probe between the surviving suspects
|
||||
// (handoff 2026-06-09 §1). Emits the EXACT clip inputs the landscape pass draws under:
|
||||
// the outside slice's slot + NDC AABB + planes (CPU side), the region-SSBO bytes decoded
|
||||
// at that slot (what mesh_modern.vert reads for routed instances), the terrain-UBO head
|
||||
// (what terrain/sky gate against), and the CellIdToSlot routing table. Fires AFTER
|
||||
// SetTerrainClip + UploadTerrainClip + SetClipRouting, BEFORE DrawLandscapeSlice — so the
|
||||
// printed bytes are exactly what this slice's draws consume.
|
||||
private void EmitClipRouteProbe(ClipFrameAssembly clipAssembly, ClipViewSlice slice, int sliceIndex)
|
||||
{
|
||||
var sb = new System.Text.StringBuilder(256);
|
||||
sb.Append(System.FormattableString.Invariant(
|
||||
$"slice={sliceIndex}/{clipAssembly.OutsideViewSlices.Length} slot={slice.Slot}"));
|
||||
sb.Append(System.FormattableString.Invariant(
|
||||
$" ndc=({slice.NdcAabb.X:F3},{slice.NdcAabb.Y:F3},{slice.NdcAabb.Z:F3},{slice.NdcAabb.W:F3})"));
|
||||
sb.Append(System.FormattableString.Invariant($" planes={slice.Planes.Length}["));
|
||||
for (int i = 0; i < slice.Planes.Length; i++)
|
||||
{
|
||||
var p = slice.Planes[i];
|
||||
if (i > 0) sb.Append(' ');
|
||||
sb.Append(System.FormattableString.Invariant($"({p.X:F3},{p.Y:F3},{p.Z:F3},{p.W:F3})"));
|
||||
}
|
||||
|
||||
// CellIdToSlot sorted by cell id so dictionary enumeration order can't fake a change.
|
||||
sb.Append("] cells={");
|
||||
_clipRouteCellKeys.Clear();
|
||||
foreach (uint key in clipAssembly.CellIdToSlot.Keys)
|
||||
_clipRouteCellKeys.Add(key);
|
||||
_clipRouteCellKeys.Sort();
|
||||
for (int i = 0; i < _clipRouteCellKeys.Count; i++)
|
||||
{
|
||||
if (i > 0) sb.Append(',');
|
||||
sb.Append(System.FormattableString.Invariant(
|
||||
$"0x{_clipRouteCellKeys[i]:X8}:{clipAssembly.CellIdToSlot[_clipRouteCellKeys[i]]}"));
|
||||
}
|
||||
sb.Append('}');
|
||||
|
||||
// Region-SSBO content decoded at the routed slot, from the packed bytes UploadRegions
|
||||
// just uploaded — slot stride 144: count uint at +0, planes[8] at +16.
|
||||
var rb = _clipFrame.RegionBytesForTest;
|
||||
int off = slice.Slot * ClipFrame.CellClipStrideBytes;
|
||||
if (off >= 0 && off + ClipFrame.CellClipStrideBytes <= rb.Length)
|
||||
{
|
||||
uint ssboCount = System.BitConverter.ToUInt32(rb.Slice(off, 4));
|
||||
sb.Append(System.FormattableString.Invariant($" ssbo[{slice.Slot}]: n={ssboCount}"));
|
||||
int planeN = (int)System.Math.Min(ssboCount, (uint)ClipFrame.MaxPlanes);
|
||||
for (int i = 0; i < planeN; i++)
|
||||
{
|
||||
int po = off + ClipFrame.CellClipPlanesOffset + i * 16;
|
||||
float px = System.BitConverter.ToSingle(rb.Slice(po, 4));
|
||||
float py = System.BitConverter.ToSingle(rb.Slice(po + 4, 4));
|
||||
float pz = System.BitConverter.ToSingle(rb.Slice(po + 8, 4));
|
||||
float pw = System.BitConverter.ToSingle(rb.Slice(po + 12, 4));
|
||||
sb.Append(System.FormattableString.Invariant($" ({px:F3},{py:F3},{pz:F3},{pw:F3})"));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append(System.FormattableString.Invariant(
|
||||
$" ssbo[{slice.Slot}]: OUT-OF-RANGE len={rb.Length}"));
|
||||
}
|
||||
|
||||
// Terrain-UBO head as uploaded (std140: int count at +0, planes[8] at +16).
|
||||
var tb = _clipFrame.TerrainBytesForTest;
|
||||
int uboCount = System.BitConverter.ToInt32(tb.Slice(0, 4));
|
||||
float u0 = System.BitConverter.ToSingle(tb.Slice(16, 4));
|
||||
float u1 = System.BitConverter.ToSingle(tb.Slice(20, 4));
|
||||
float u2 = System.BitConverter.ToSingle(tb.Slice(24, 4));
|
||||
float u3 = System.BitConverter.ToSingle(tb.Slice(28, 4));
|
||||
sb.Append(System.FormattableString.Invariant(
|
||||
$" ubo: n={uboCount} p0=({u0:F3},{u1:F3},{u2:F3},{u3:F3})"));
|
||||
|
||||
string sig = sb.ToString();
|
||||
_clipRouteSeq++;
|
||||
if (sig == _lastClipRouteSig)
|
||||
return;
|
||||
_lastClipRouteSig = sig;
|
||||
Console.WriteLine($"[clip-route] n={_clipRouteSeq} {sig}");
|
||||
passes.UseIndoorMembershipOnlyRouting();
|
||||
}
|
||||
|
||||
private void DrawExitPortalMasks(
|
||||
IRetailPViewCellDrawCallbacks ctx,
|
||||
RetailPViewFrameInput ctx,
|
||||
IRetailPViewPassExecutor passes,
|
||||
PortalVisibilityFrame pvFrame,
|
||||
ClipFrameAssembly clipAssembly,
|
||||
HashSet<uint> drawableCells)
|
||||
{
|
||||
if (ctx.DrawExitPortalMasks is null)
|
||||
return;
|
||||
|
||||
for (int i = pvFrame.OrderedVisibleCells.Count - 1; i >= 0; i--)
|
||||
{
|
||||
uint cellId = pvFrame.OrderedVisibleCells[i];
|
||||
|
|
@ -711,11 +573,15 @@ public sealed class RetailPViewRenderer
|
|||
continue;
|
||||
|
||||
foreach (var slice in GetCellSlicesOrNoClip(clipAssembly, cellId))
|
||||
ctx.DrawExitPortalMasks(new RetailPViewCellSliceContext(cellId, slice, Array.Empty<WorldEntity>()));
|
||||
passes.DrawExitPortalMask(
|
||||
ctx,
|
||||
new RetailPViewCellSliceContext(cellId, slice, Array.Empty<WorldEntity>()));
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawEnvCellShells(PortalVisibilityFrame pvFrame)
|
||||
private void DrawEnvCellShells(
|
||||
IRetailPViewPassExecutor passes,
|
||||
PortalVisibilityFrame pvFrame)
|
||||
{
|
||||
// T1 (fused BR-2/3): retail DrawCells Loop 2 — every visible cell's
|
||||
// shell drawn WHOLE, reverse cell_draw_list (far→near), drawn once.
|
||||
|
|
@ -727,7 +593,7 @@ public sealed class RetailPViewRenderer
|
|||
// (927fd8f/9ce335e, #114) is deleted with this rewrite.
|
||||
// Per-cell opaque+transparent keeps the far→near transparent
|
||||
// compositing the per-cell loop already provided.
|
||||
UseIndoorMembershipOnlyRouting();
|
||||
passes.UseIndoorMembershipOnlyRouting();
|
||||
|
||||
// Opaque: ONE batched Render for all shell cells (was one heavy per-frame
|
||||
// Render call PER cell — the dense-town FPS sink, ~94 calls/24.75ms at
|
||||
|
|
@ -739,7 +605,7 @@ public sealed class RetailPViewRenderer
|
|||
foreach (var entry in IndoorDrawPlan.ShellPass(pvFrame))
|
||||
_shellBatch.Add(entry.CellId);
|
||||
if (_shellBatch.Count > 0)
|
||||
_envCells.Render(WbRenderPass.Opaque, _shellBatch);
|
||||
passes.DrawOpaqueCellShells(_shellBatch);
|
||||
|
||||
// Transparent: far-to-near order matters for compositing. The ordered
|
||||
// list retains ShellPass cell boundaries while EnvCellRenderer shares
|
||||
|
|
@ -747,11 +613,11 @@ public sealed class RetailPViewRenderer
|
|||
_orderedTransparentShellCells.Clear();
|
||||
foreach (var entry in IndoorDrawPlan.ShellPass(pvFrame))
|
||||
{
|
||||
if (_envCells.CellHasTransparent(entry.CellId))
|
||||
if (passes.CellHasTransparentShell(entry.CellId))
|
||||
_orderedTransparentShellCells.Add(entry.CellId);
|
||||
}
|
||||
if (_orderedTransparentShellCells.Count > 0)
|
||||
_envCells.RenderTransparentOrdered(_orderedTransparentShellCells);
|
||||
passes.DrawTransparentCellShellsOrdered(_orderedTransparentShellCells);
|
||||
}
|
||||
|
||||
// T1: the frame's single LAST entity pass — ALL server-spawned dynamics
|
||||
|
|
@ -767,7 +633,8 @@ public sealed class RetailPViewRenderer
|
|||
// the draw list; the partition keeps routing it so the CULL (not the
|
||||
// visibility set) drops it, exactly retail's shape.
|
||||
private void DrawDynamicsLast(
|
||||
IRetailPViewCellDrawContext ctx,
|
||||
RetailPViewFrameInput ctx,
|
||||
IRetailPViewPassExecutor passes,
|
||||
InteriorEntityPartition.Result partition,
|
||||
ViewconeCuller viewcone,
|
||||
bool rootIsOutdoor)
|
||||
|
|
@ -811,8 +678,8 @@ public sealed class RetailPViewRenderer
|
|||
if (_dynamicsScratch.Count == 0)
|
||||
return;
|
||||
|
||||
UseIndoorMembershipOnlyRouting();
|
||||
DrawEntityBucket(ctx, _dynamicsScratch, visibleCellIds: null);
|
||||
passes.UseIndoorMembershipOnlyRouting();
|
||||
passes.DrawEntityBucket(ctx, _dynamicsScratch, visibleCellIds: null);
|
||||
|
||||
// #121: dynamics' attached emitters (portal swirls, creature effects)
|
||||
// gate through the SAME cone-surviving owner set as their meshes —
|
||||
|
|
@ -824,19 +691,17 @@ public sealed class RetailPViewRenderer
|
|||
// portals went invisible. Outside-stage dynamics are excluded here:
|
||||
// their emitters already drew in the landscape slice (alpha-blended
|
||||
// particles must not double-draw, unlike the depth-idempotent meshes).
|
||||
if (ctx.DrawDynamicsParticles is not null)
|
||||
{
|
||||
_dynamicsParticleScratch.Clear();
|
||||
foreach (var e in _dynamicsScratch)
|
||||
if (!_outsideStageDynamics.Contains(e))
|
||||
_dynamicsParticleScratch.Add(e);
|
||||
if (_dynamicsParticleScratch.Count > 0)
|
||||
ctx.DrawDynamicsParticles(_dynamicsParticleScratch);
|
||||
}
|
||||
_dynamicsParticleScratch.Clear();
|
||||
foreach (var e in _dynamicsScratch)
|
||||
if (!_outsideStageDynamics.Contains(e))
|
||||
_dynamicsParticleScratch.Add(e);
|
||||
if (_dynamicsParticleScratch.Count > 0)
|
||||
passes.DrawDynamicsParticles(ctx, _dynamicsParticleScratch);
|
||||
}
|
||||
|
||||
private void DrawCellObjectLists(
|
||||
IRetailPViewCellDrawContext ctx,
|
||||
RetailPViewFrameInput ctx,
|
||||
IRetailPViewPassExecutor passes,
|
||||
PortalVisibilityFrame pvFrame,
|
||||
ClipFrameAssembly clipAssembly,
|
||||
HashSet<uint> drawableCells,
|
||||
|
|
@ -889,8 +754,7 @@ public sealed class RetailPViewRenderer
|
|||
_cellObjCells.Add(cellId);
|
||||
|
||||
// BR-2 phantom-site probe (T3-updated): post-viewcone survivors.
|
||||
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbePhantomEnabled)
|
||||
EmitPhantomObjsProbe(cellId, survivors);
|
||||
passes.EmitPhantomObjects(cellId, survivors);
|
||||
}
|
||||
|
||||
// ONE batched static-object draw for every visible cell (was N per-cell
|
||||
|
|
@ -900,26 +764,27 @@ public sealed class RetailPViewRenderer
|
|||
// cull; only the draw is batched).
|
||||
if (_allCellStatics.Count > 0)
|
||||
{
|
||||
UseIndoorMembershipOnlyRouting();
|
||||
DrawEntityBucket(ctx, _allCellStatics, _cellObjCells);
|
||||
passes.UseIndoorMembershipOnlyRouting();
|
||||
passes.DrawEntityBucket(ctx, _allCellStatics, _cellObjCells);
|
||||
}
|
||||
|
||||
// Cell-particle pass — consolidated across ALL visible cells into ONE
|
||||
// draw. Was per-cell, and each call re-walked the ENTIRE live particle set
|
||||
// (DrawRetailPViewCellParticles → ParticleRenderer.Draw enumerates every
|
||||
// (RetailPViewPassExecutor.DrawCellParticles → ParticleRenderer.Draw enumerates every
|
||||
// live emitter), i.e. O(cells × particles) — the dense-town cellobjects
|
||||
// sink (~5 ms at Arwic). Static owners are disjoint per cell, so the UNION
|
||||
// (= _allCellStatics, already accumulated above for the batched draw) draws
|
||||
// EXACTLY the same emitters: the callback gates on owner id (the cone-
|
||||
// surviving set), the renderer sorts globally back-to-front, and the per-
|
||||
// cell slice was never used for clipping (the scissor gate was deleted in
|
||||
// T3 — DrawRetailPViewCellParticles disables clip distances). Runs after
|
||||
// T3 — RetailPViewPassExecutor.DrawCellParticles disables clip distances). Runs after
|
||||
// the batched static draw so emitters depth-test against the statics now in
|
||||
// the buffer (the statics-before-particles order). cellId/slice are unused
|
||||
// by the particle pass — pass NoClipSlice + the union owner list. This also
|
||||
// drops the per-cell BuildDrawList allocations (N → 1).
|
||||
if (_allCellStatics.Count > 0)
|
||||
ctx.DrawCellParticles?.Invoke(
|
||||
passes.DrawCellParticles(
|
||||
ctx,
|
||||
new RetailPViewCellSliceContext(0u, NoClipSlice, _allCellStatics));
|
||||
}
|
||||
|
||||
|
|
@ -960,7 +825,7 @@ public sealed class RetailPViewRenderer
|
|||
Vector3 sphereCenter,
|
||||
float sphereRadius,
|
||||
HashSet<uint> drawableCells,
|
||||
Func<uint, LoadedCell?> cellLookup)
|
||||
IRetailPViewCellSource cells)
|
||||
{
|
||||
if (!InteriorEntityPartition.IsIndoorCellId(parentCellId))
|
||||
return true;
|
||||
|
|
@ -968,7 +833,7 @@ public sealed class RetailPViewRenderer
|
|||
uint cellId = parentCellId!.Value;
|
||||
if (!drawableCells.Contains(cellId))
|
||||
return false; // not in the flood — the last-pass cone cull owns it
|
||||
var cell = cellLookup(cellId);
|
||||
var cell = cells.Find(cellId);
|
||||
if (cell is null)
|
||||
return false;
|
||||
|
||||
|
|
@ -998,20 +863,6 @@ public sealed class RetailPViewRenderer
|
|||
radius = (e.AabbMax - e.AabbMin).Length() * 0.5f;
|
||||
}
|
||||
|
||||
// BR-2 phantom-site probe state: print-on-change per cell so the log stays
|
||||
// diffable while the condition persists. Throwaway apparatus — strip when
|
||||
// the #113 phantom residual closes. (The [phantom-shell] half died with
|
||||
// the T1 chop deletion — shells draw whole, there is no slice state left
|
||||
// to report.)
|
||||
private readonly Dictionary<uint, int> _phantomObjsSig = new();
|
||||
|
||||
private void EmitPhantomObjsProbe(uint cellId, int bucketCount)
|
||||
{
|
||||
if (_phantomObjsSig.TryGetValue(cellId, out var prev) && prev == bucketCount) return;
|
||||
_phantomObjsSig[cellId] = bucketCount;
|
||||
Console.WriteLine($"[phantom-objs] cell=0x{cellId:X8} entities={bucketCount} (drawn unclipped, no viewcone)");
|
||||
}
|
||||
|
||||
private static ClipViewSlice[] GetCellSlicesOrNoClip(
|
||||
ClipFrameAssembly clipAssembly,
|
||||
uint cellId)
|
||||
|
|
@ -1023,80 +874,65 @@ public sealed class RetailPViewRenderer
|
|||
return NoClipSlices;
|
||||
}
|
||||
|
||||
private void UseIndoorMembershipOnlyRouting()
|
||||
{
|
||||
// T1: NOTHING in the world passes hard-clips geometry anymore — retail
|
||||
// viewcone-CHECKS meshes (sphere vs view planes, T3) and never clips
|
||||
// cell shells (DrawEnvCell draws the whole prebuilt mesh, pc:427905).
|
||||
// This clears any clip routing left by the landscape slices.
|
||||
_envCells.SetClipRouting(null);
|
||||
_entities.ClearClipRouting();
|
||||
}
|
||||
|
||||
private void DrawEntityBucket(
|
||||
IRetailPViewCellDrawContext ctx,
|
||||
IReadOnlyList<WorldEntity> bucket,
|
||||
HashSet<uint>? visibleCellIds)
|
||||
{
|
||||
uint lbId = ctx.PlayerLandblockId ?? 0u;
|
||||
var entry = (lbId, Vector3.Zero, Vector3.Zero,
|
||||
(IReadOnlyList<WorldEntity>)bucket,
|
||||
(IReadOnlyDictionary<uint, WorldEntity>?)null);
|
||||
|
||||
_entities.Draw(
|
||||
ctx.Camera,
|
||||
new[] { entry },
|
||||
ctx.Frustum,
|
||||
neverCullLandblockId: ctx.PlayerLandblockId,
|
||||
visibleCellIds: visibleCellIds,
|
||||
animatedEntityIds: ctx.AnimatedEntityIds);
|
||||
}
|
||||
|
||||
private void PrepareClipFrame(
|
||||
Action<TerrainClipBufferBinding> setTerrainClipUbo,
|
||||
int terrainUploadCount)
|
||||
{
|
||||
// Allocate every terrain record before issuing the first draw. BufferData
|
||||
// therefore never replaces the arena's backing store while an earlier
|
||||
// slice can still reference it. The large region table is immutable for
|
||||
// this assembled PView frame and is uploaded exactly once.
|
||||
_clipFrame.ReserveTerrainUploads(_gl, terrainUploadCount);
|
||||
_clipFrame.UploadRegions(_gl);
|
||||
_entities.SetClipRegionSsbo(_clipFrame.RegionSsbo);
|
||||
_envCells.SetClipRegionSsbo(_clipFrame.RegionSsbo);
|
||||
UploadTerrainClip(setTerrainClipUbo);
|
||||
}
|
||||
|
||||
private void UploadTerrainClip(Action<TerrainClipBufferBinding> setTerrainClipUbo)
|
||||
{
|
||||
TerrainClipBufferBinding binding = _clipFrame.UploadTerrainClip(_gl);
|
||||
setTerrainClipUbo(binding);
|
||||
}
|
||||
}
|
||||
|
||||
public interface IRetailPViewCellDrawCallbacks
|
||||
public interface IRetailPViewCellSource
|
||||
{
|
||||
public Action<RetailPViewCellSliceContext>? DrawExitPortalMasks { get; }
|
||||
public Action<RetailPViewCellSliceContext>? DrawCellParticles { get; }
|
||||
|
||||
/// <summary>#124: far-Z punch one look-in aperture (a clipped view polygon
|
||||
/// of a looked-into building cell) — always the PUNCH variant regardless
|
||||
/// of root kind (retail maxZ1; the root-keyed forceFarZ selector only
|
||||
/// governs the MAIN frame's exit-portal masks).</summary>
|
||||
public Action<RetailPViewCellSliceContext>? DrawLookInPortalPunch { get; }
|
||||
LoadedCell? Find(uint cellId);
|
||||
}
|
||||
|
||||
public interface IRetailPViewCellDrawContext : IRetailPViewCellDrawCallbacks
|
||||
/// <summary>
|
||||
/// Typed execution seam for the GL passes ordered by
|
||||
/// <see cref="RetailPViewRenderer"/>. Implementations execute the requested
|
||||
/// pass only; visibility construction and draw ordering remain renderer-owned.
|
||||
/// All frame inputs and results are borrowed for the duration of the call.
|
||||
/// </summary>
|
||||
public interface IRetailPViewPassExecutor
|
||||
{
|
||||
public ICamera Camera { get; }
|
||||
public FrustumPlanes? Frustum { get; }
|
||||
public uint? PlayerLandblockId { get; }
|
||||
public HashSet<uint>? AnimatedEntityIds { get; }
|
||||
|
||||
/// <summary>#121: draw the Scene-pass emitters attached to the frame's
|
||||
/// cone-surviving dynamics (portal swirls, creature effects). Invoked once
|
||||
/// per frame after the last entity pass with the survivor list.</summary>
|
||||
public Action<IReadOnlyList<WorldEntity>>? DrawDynamicsParticles { get; }
|
||||
void BeginFrame();
|
||||
ClipFrameAssembly AssembleClipFrame(
|
||||
PortalVisibilityFrame portalFrame,
|
||||
ClipFrameAssembly reuseAssembly);
|
||||
void PrepareClipFrame(int terrainUploadCount);
|
||||
void SetTerrainClip(ReadOnlySpan<Vector4> planes);
|
||||
void ClearClipRouting();
|
||||
void UseIndoorMembershipOnlyRouting();
|
||||
void PrepareCellBatches(
|
||||
RetailPViewFrameInput frame,
|
||||
HashSet<uint> visibleCellIds);
|
||||
void DrawOpaqueCellShells(HashSet<uint> cellIds);
|
||||
bool CellHasTransparentShell(uint cellId);
|
||||
void DrawTransparentCellShells(HashSet<uint> cellIds);
|
||||
void DrawTransparentCellShellsOrdered(IReadOnlyList<uint> cellIds);
|
||||
void DrawEntityBucket(
|
||||
RetailPViewFrameInput frame,
|
||||
IReadOnlyList<WorldEntity> entities,
|
||||
HashSet<uint>? visibleCellIds);
|
||||
void EmitClipRouteProbe(
|
||||
ClipFrameAssembly clipAssembly,
|
||||
ClipViewSlice slice,
|
||||
int sliceIndex);
|
||||
void EmitOutStageOwner(
|
||||
WorldEntity entity,
|
||||
Vector3 sphereCenter,
|
||||
float sphereRadius,
|
||||
int sliceIndex,
|
||||
bool passed);
|
||||
void EmitOutStageRouting(
|
||||
int sliceIndex,
|
||||
IReadOnlyList<WorldEntity> entities,
|
||||
ViewconeCuller viewcone);
|
||||
void EmitPhantomObjects(uint cellId, int survivorCount);
|
||||
void DrawLandscapeSlice(RetailPViewFrameInput frame, RetailPViewLandscapeSliceContext context);
|
||||
void DrawLandscapeSliceLate(RetailPViewFrameInput frame, RetailPViewLandscapeLateSliceContext context);
|
||||
void ClearInteriorDepth();
|
||||
void DrawExitPortalMask(RetailPViewFrameInput frame, RetailPViewCellSliceContext context);
|
||||
void DrawLookInPortalPunch(RetailPViewFrameInput frame, RetailPViewCellSliceContext context);
|
||||
void DrawUnattachedSceneParticles(RetailPViewFrameInput frame);
|
||||
void FlushLandscapeAlpha();
|
||||
void DrawCellParticles(RetailPViewFrameInput frame, RetailPViewCellSliceContext context);
|
||||
void DrawDynamicsParticles(RetailPViewFrameInput frame, IReadOnlyList<WorldEntity> survivors);
|
||||
void EmitDiagnostics(RetailPViewFrameInput frame, RetailPViewFrameResult result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1261,7 +1097,7 @@ internal sealed class BuildingGroupScratch
|
|||
}
|
||||
}
|
||||
|
||||
public sealed class RetailPViewDrawContext : IRetailPViewCellDrawContext
|
||||
public sealed class RetailPViewFrameInput
|
||||
{
|
||||
public required LoadedCell RootCell { get; init; }
|
||||
|
||||
|
|
@ -1271,7 +1107,7 @@ public sealed class RetailPViewDrawContext : IRetailPViewCellDrawContext
|
|||
|
||||
public required Vector3 ViewerEyePos { get; init; }
|
||||
public required Matrix4x4 ViewProjection { get; init; }
|
||||
public required Func<uint, LoadedCell?> CellLookup { get; init; }
|
||||
public required IRetailPViewCellSource Cells { get; init; }
|
||||
public required ICamera Camera { get; init; }
|
||||
public required Vector3 CameraWorldPosition { get; init; }
|
||||
public required FrustumPlanes? Frustum { get; init; }
|
||||
|
|
@ -1283,37 +1119,20 @@ public sealed class RetailPViewDrawContext : IRetailPViewCellDrawContext
|
|||
public required IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax,
|
||||
IReadOnlyList<WorldEntity> Entities,
|
||||
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> LandblockEntries { get; init; }
|
||||
public required Action<TerrainClipBufferBinding> SetTerrainClipUbo { get; init; }
|
||||
public required Action<RetailPViewLandscapeSliceContext> DrawLandscapeSlice { get; init; }
|
||||
|
||||
/// <summary>#131/#132: the LATE landscape phase, per slice, after the #124
|
||||
/// look-ins — outside-stage dynamics' meshes + all scene particles +
|
||||
/// weather (the FlushAlphaList deferral; see DrawLandscapeThroughOutsideView).</summary>
|
||||
public Action<RetailPViewLandscapeLateSliceContext>? DrawLandscapeSliceLate { get; init; }
|
||||
/// <summary>T1: one full-buffer depth clear between the outside stage and the
|
||||
/// interior stage (retail PView::DrawCells, Ghidra 0x005a4840). Null for outdoor
|
||||
/// roots — outdoors the interiors must depth-test against terrain + exteriors and
|
||||
/// appear only through punched apertures.</summary>
|
||||
public Action? ClearDepthForInterior { get; init; }
|
||||
public Action<RetailPViewCellSliceContext>? DrawExitPortalMasks { get; init; }
|
||||
public Action<RetailPViewCellSliceContext>? DrawCellParticles { get; init; }
|
||||
public Action<RetailPViewCellSliceContext>? DrawLookInPortalPunch { get; init; }
|
||||
|
||||
/// <summary>#131: Scene-pass draw of UNATTACHED emitters
|
||||
/// (AttachedObjectId == 0) for interior-root frames — invoked once at the
|
||||
/// end of the landscape stage (pre-clear). Outdoor roots draw them via
|
||||
/// GameWindow's dedicated post-frame pass instead.</summary>
|
||||
public Action? DrawUnattachedSceneParticles { get; init; }
|
||||
/// <summary>Drains retail's delayed landscape alpha list before the
|
||||
/// optional outside-to-inside depth clear. The same frame-scoped queue
|
||||
/// remains open for the final world alpha scope.</summary>
|
||||
public Action? FlushLandscapeAlpha { get; init; }
|
||||
public Action<IReadOnlyList<WorldEntity>>? DrawDynamicsParticles { get; init; }
|
||||
/// <summary>
|
||||
/// Synchronous observation of the borrowed current-frame result. The
|
||||
/// callback must copy anything it needs to retain beyond this invocation.
|
||||
/// </summary>
|
||||
public Action<RetailPViewFrameResult>? EmitDiagnostics { get; init; }
|
||||
// Pass-presentation and diagnostic values consumed synchronously by the
|
||||
// typed executor. This input is data-only and is never retained.
|
||||
public required bool RenderSky { get; init; }
|
||||
public required bool RenderWeather { get; init; }
|
||||
public required float DayFraction { get; init; }
|
||||
public required DayGroupData? ActiveDayGroup { get; init; }
|
||||
public required SkyKeyframe SkyKeyframe { get; init; }
|
||||
public required bool EnvironOverrideActive { get; init; }
|
||||
public required uint ViewerCellId { get; init; }
|
||||
public required uint PlayerCellId { get; init; }
|
||||
public required Vector3 PlayerViewPosition { get; init; }
|
||||
public required Matrix4x4 CameraView { get; init; }
|
||||
public required CameraCellResolution CameraCellResolution { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue