perf(rendering): draw retained frame product

Make the incremental render scene the production entity source at the existing retail PView stages while retaining the accepted dispatcher upload and draw executor. Keep diagnostics consumer-gated, retain ordered indices across unchanged frames, refresh only dirty records, and preserve exact mesh-load, selection, alpha, lighting, and route lifecycle semantics.
This commit is contained in:
Erik 2026-07-25 04:12:23 +02:00
parent c7d7848dd2
commit ef1d263337
13 changed files with 1333 additions and 267 deletions

View file

@ -76,13 +76,6 @@ public sealed class RetailPViewRenderer
InteriorEntityPartition.IObserver? partitionObserver,
RenderScenePViewFrameProductController? sceneFrameProduct = null)
{
if (sceneFrameProduct is not null
&& partitionObserver is not ICurrentRenderPViewObserver)
{
throw new ArgumentException(
"The scene frame-product referee requires the current PView observer.",
nameof(partitionObserver));
}
_partitionObserver = partitionObserver;
_candidateObserver = partitionObserver as ICurrentRenderPViewObserver;
_sceneFrameProduct = sceneFrameProduct;
@ -212,6 +205,16 @@ public sealed class RetailPViewRenderer
ctx.ViewProjection,
_viewconeScratch);
IRenderFrameEntityPassExecutor? frameEntityPasses = null;
if (_sceneFrameProduct is not null)
{
frameEntityPasses = passes as IRenderFrameEntityPassExecutor
?? throw new InvalidOperationException(
"The production frame product requires a packed entity-pass executor.");
}
RenderFrameView frameView = default;
bool frameViewBorrowed = false;
bool entityFrameOpen = false;
_candidateObserver?.BeginPViewFrame();
try
{
@ -242,32 +245,73 @@ public sealed class RetailPViewRenderer
}
}
DrawLandscapeThroughOutsideView(ctx, passes, clipAssembly, partition, viewcone);
if (_sceneFrameProduct is not null)
{
frameView = _sceneFrameProduct.BuildAndBorrow(
pvFrame,
clipAssembly,
viewcone,
_lookInFrames,
drawableCells,
ctx.Cells,
ctx.AnimatedEntityIds,
ctx.RootCell.IsOutdoorNode);
frameViewBorrowed = true;
frameEntityPasses!.BeginEntityFrame(in frameView);
entityFrameOpen = true;
}
DrawLandscapeThroughOutsideView(
ctx,
passes,
clipAssembly,
partition,
viewcone,
frameEntityPasses,
in frameView);
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);
_candidateObserver?.CompletePViewFrame();
_sceneFrameProduct?.BuildAndCompare(
DrawCellObjectLists(
ctx,
passes,
pvFrame,
clipAssembly,
viewcone,
_lookInFrames,
drawableCells,
ctx.Cells,
ctx.AnimatedEntityIds,
ctx.PlayerLandblockId ?? 0,
partition,
viewcone,
frameEntityPasses,
in frameView);
DrawDynamicsLast(
ctx,
passes,
partition,
viewcone,
ctx.RootCell.IsOutdoorNode,
ctx.CameraWorldPosition);
frameEntityPasses,
in frameView);
if (entityFrameOpen)
{
frameEntityPasses!.CompleteEntityFrame(in frameView);
entityFrameOpen = false;
}
_candidateObserver?.CompletePViewFrame();
_sceneFrameProduct?.CompleteProduction(in frameView);
return result;
}
catch
{
if (entityFrameOpen)
frameEntityPasses!.AbortEntityFrame();
_candidateObserver?.AbortPViewFrame();
throw;
}
finally
{
if (frameViewBorrowed)
_sceneFrameProduct!.Release(in frameView);
}
}
// R-A2: group the nearby building cells by BuildingId and run one per-building flood per group
@ -397,7 +441,9 @@ public sealed class RetailPViewRenderer
RetailPViewFrameInput ctx,
IRetailPViewPassExecutor passes,
ClipFrameAssembly clipAssembly,
InteriorEntityPartition.Result partition)
InteriorEntityPartition.Result partition,
IRenderFrameEntityPassExecutor? frameEntityPasses,
in RenderFrameView frameView)
{
if (_lookInFrames.Count == 0)
return;
@ -475,7 +521,16 @@ public sealed class RetailPViewRenderer
i,
cellId,
_cellStaticScratch);
passes.DrawEntityBucket(ctx, _cellStaticScratch, _oneCell);
DrawEntityRouteOrLegacy(
ctx,
passes,
frameEntityPasses,
in frameView,
RenderFrameCandidateRoute.LookInObject,
i,
cellId,
_cellStaticScratch,
_oneCell);
// The cell-particles pass for look-in cells — retail's
// nested DrawCells draws objects WITH their emitters.
@ -492,7 +547,9 @@ public sealed class RetailPViewRenderer
IRetailPViewPassExecutor passes,
ClipFrameAssembly clipAssembly,
InteriorEntityPartition.Result partition,
ViewconeCuller viewcone)
ViewconeCuller viewcone,
IRenderFrameEntityPassExecutor? frameEntityPasses,
in RenderFrameView frameView)
{
if (clipAssembly.OutsideViewSlices.Length == 0)
return;
@ -534,15 +591,37 @@ public sealed class RetailPViewRenderer
probeSliceIndex,
0,
_outdoorStaticScratch);
RenderFrameEntityDrawRequest? entityDraw =
frameEntityPasses is null
? null
: new RenderFrameEntityDrawRequest(
frameView,
RenderFrameCandidateRoute.LandscapeOutdoorStatic,
probeSliceIndex,
0,
ctx.PlayerLandblockId ?? 0);
probeSliceIndex++;
passes.DrawLandscapeSlice(ctx, new RetailPViewLandscapeSliceContext(slice, _outdoorStaticScratch));
passes.DrawLandscapeSlice(
ctx,
new RetailPViewLandscapeSliceContext(
slice,
_outdoorStaticScratch)
{
EntityDraw = entityDraw,
});
}
// #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, passes, clipAssembly, partition);
DrawBuildingLookIns(
ctx,
passes,
clipAssembly,
partition,
frameEntityPasses,
in frameView);
// LATE phase (per slice): outside-stage dynamics' meshes (#118 — drawn
// pre-clear so the seal protects their aperture pixels; AFTER the
@ -591,9 +670,25 @@ public sealed class RetailPViewRenderer
probeSliceIndex,
0,
_outdoorStaticScratch);
RenderFrameEntityDrawRequest? entityDraw =
frameEntityPasses is null
? null
: new RenderFrameEntityDrawRequest(
frameView,
RenderFrameCandidateRoute.LandscapeOutsideDynamic,
probeSliceIndex,
0,
ctx.PlayerLandblockId ?? 0);
probeSliceIndex++;
passes.DrawLandscapeSliceLate(ctx, new RetailPViewLandscapeLateSliceContext(
slice, _outdoorStaticScratch, _lateParticleOwnerScratch));
passes.DrawLandscapeSliceLate(
ctx,
new RetailPViewLandscapeLateSliceContext(
slice,
_outdoorStaticScratch,
_lateParticleOwnerScratch)
{
EntityDraw = entityDraw,
});
}
// #131: UNATTACHED emitters (AttachedObjectId == 0 — portal swirls,
@ -702,7 +797,9 @@ public sealed class RetailPViewRenderer
IRetailPViewPassExecutor passes,
InteriorEntityPartition.Result partition,
ViewconeCuller viewcone,
bool rootIsOutdoor)
bool rootIsOutdoor,
IRenderFrameEntityPassExecutor? frameEntityPasses,
in RenderFrameView frameView)
{
if (partition.Dynamics.Count == 0)
return;
@ -749,7 +846,16 @@ public sealed class RetailPViewRenderer
0,
_dynamicsScratch);
passes.UseIndoorMembershipOnlyRouting();
passes.DrawEntityBucket(ctx, _dynamicsScratch, visibleCellIds: null);
DrawEntityRouteOrLegacy(
ctx,
passes,
frameEntityPasses,
in frameView,
RenderFrameCandidateRoute.DynamicLast,
0,
0,
_dynamicsScratch,
visibleCellIds: null);
// #121: dynamics' attached emitters (portal swirls, creature effects)
// gate through the SAME cone-surviving owner set as their meshes —
@ -776,7 +882,9 @@ public sealed class RetailPViewRenderer
ClipFrameAssembly clipAssembly,
HashSet<uint> drawableCells,
InteriorEntityPartition.Result partition,
ViewconeCuller viewcone)
ViewconeCuller viewcone,
IRenderFrameEntityPassExecutor? frameEntityPasses,
in RenderFrameView frameView)
{
// T1: per-cell STATIC object lists only (dat-baked 0x40 statics) —
// dynamics moved to DrawDynamicsLast. Far→near with the cells, after
@ -840,7 +948,16 @@ public sealed class RetailPViewRenderer
0,
_allCellStatics);
passes.UseIndoorMembershipOnlyRouting();
passes.DrawEntityBucket(ctx, _allCellStatics, _cellObjCells);
DrawEntityRouteOrLegacy(
ctx,
passes,
frameEntityPasses,
in frameView,
RenderFrameCandidateRoute.CellStatic,
0,
0,
_allCellStatics,
_cellObjCells);
}
// Cell-particle pass — consolidated across ALL visible cells into ONE
@ -863,6 +980,35 @@ public sealed class RetailPViewRenderer
new RetailPViewCellSliceContext(0u, NoClipSlice, _allCellStatics));
}
private static void DrawEntityRouteOrLegacy(
RetailPViewFrameInput frame,
IRetailPViewPassExecutor passes,
IRenderFrameEntityPassExecutor? frameEntityPasses,
in RenderFrameView frameView,
RenderFrameCandidateRoute route,
int routeIndex,
uint cellId,
IReadOnlyList<WorldEntity> legacyEntities,
HashSet<uint>? visibleCellIds)
{
if (frameEntityPasses is not null)
{
frameEntityPasses.DrawEntityRoute(
frame.Camera,
in frameView,
route,
routeIndex,
cellId,
frame.PlayerLandblockId ?? 0);
return;
}
passes.DrawEntityBucket(
frame,
legacyEntities,
visibleCellIds);
}
// T3 scratch lists (render thread only; cleared per use).
private readonly List<WorldEntity> _outdoorStaticScratch = new();
private readonly List<WorldEntity> _cellStaticScratch = new();
@ -1241,7 +1387,10 @@ public sealed class RetailPViewFrameResult
public readonly record struct RetailPViewLandscapeSliceContext(
ClipViewSlice Slice,
IReadOnlyList<WorldEntity> OutdoorEntities);
IReadOnlyList<WorldEntity> OutdoorEntities)
{
internal RenderFrameEntityDrawRequest? EntityDraw { get; init; }
}
/// <summary>#131/#132: the late landscape phase's per-slice payload —
/// outside-stage dynamics to mesh-draw, plus the full scene-particle owner
@ -1249,7 +1398,10 @@ public readonly record struct RetailPViewLandscapeSliceContext(
public readonly record struct RetailPViewLandscapeLateSliceContext(
ClipViewSlice Slice,
IReadOnlyList<WorldEntity> Dynamics,
IReadOnlyList<WorldEntity> ParticleOwners);
IReadOnlyList<WorldEntity> ParticleOwners)
{
internal RenderFrameEntityDrawRequest? EntityDraw { get; init; }
}
public readonly record struct RetailPViewCellSliceContext(
uint CellId,