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:
parent
c7d7848dd2
commit
ef1d263337
13 changed files with 1333 additions and 267 deletions
|
|
@ -374,12 +374,6 @@ internal sealed class FrameRootCompositionPhase
|
||||||
&& d.Options.AutomationArtifactDirectory is not null
|
&& d.Options.AutomationArtifactDirectory is not null
|
||||||
? new CurrentRenderSceneOracle()
|
? new CurrentRenderSceneOracle()
|
||||||
: null;
|
: null;
|
||||||
if ((currentRenderSceneOracle is null)
|
|
||||||
!= (live.RenderSceneShadow is null))
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException(
|
|
||||||
"Lifecycle automation must compose the current-path referee and shadow render scene together.");
|
|
||||||
}
|
|
||||||
RenderSceneShadowComparisonController? renderSceneShadowComparison =
|
RenderSceneShadowComparisonController? renderSceneShadowComparison =
|
||||||
currentRenderSceneOracle is not null
|
currentRenderSceneOracle is not null
|
||||||
&& live.RenderSceneShadow is not null
|
&& live.RenderSceneShadow is not null
|
||||||
|
|
@ -390,18 +384,18 @@ internal sealed class FrameRootCompositionPhase
|
||||||
acknowledgeDirty: false)
|
acknowledgeDirty: false)
|
||||||
: null;
|
: null;
|
||||||
RenderScenePViewFrameProductController? renderFrameProduct =
|
RenderScenePViewFrameProductController? renderFrameProduct =
|
||||||
currentRenderSceneOracle is not null
|
live.RenderSceneShadow is not null
|
||||||
&& live.RenderSceneShadow is not null
|
|
||||||
? new RenderScenePViewFrameProductController(
|
? new RenderScenePViewFrameProductController(
|
||||||
live.RenderSceneShadow,
|
live.RenderSceneShadow,
|
||||||
currentRenderSceneOracle,
|
currentRenderSceneOracle,
|
||||||
message => d.Log("[UI-PROBE] " + message),
|
message => d.Log("[UI-PROBE] " + message),
|
||||||
live.DrawDispatcher)
|
live.DrawDispatcher)
|
||||||
: null;
|
: null;
|
||||||
live.DrawDispatcher.SetCurrentRenderSceneObserver(
|
// After G4 the retained product is the production object source.
|
||||||
currentRenderSceneOracle);
|
// The old dispatcher/selection observer is intentionally detached;
|
||||||
live.SelectionScene.SetCurrentRenderSceneObserver(
|
// automation still compares the independently built PView route list.
|
||||||
currentRenderSceneOracle);
|
live.DrawDispatcher.SetCurrentRenderSceneObserver(null);
|
||||||
|
live.SelectionScene.SetCurrentRenderSceneObserver(null);
|
||||||
var worldSceneRenderer = new WorldSceneRenderer(
|
var worldSceneRenderer = new WorldSceneRenderer(
|
||||||
renderFrameResources,
|
renderFrameResources,
|
||||||
renderLoginState,
|
renderLoginState,
|
||||||
|
|
|
||||||
|
|
@ -200,15 +200,11 @@ internal sealed class LivePresentationCompositionPhase
|
||||||
{
|
{
|
||||||
CompositionAcquisitionScope.CompositionAcquisitionLease<
|
CompositionAcquisitionScope.CompositionAcquisitionLease<
|
||||||
RenderSceneShadowRuntime>? renderSceneShadowLease = null;
|
RenderSceneShadowRuntime>? renderSceneShadowLease = null;
|
||||||
if (interaction.RetainedUi?.Screenshots is not null
|
renderSceneShadowLease = scope.Acquire(
|
||||||
&& d.Options.AutomationArtifactDirectory is not null)
|
"render scene",
|
||||||
{
|
() => new RenderSceneShadowRuntime(
|
||||||
renderSceneShadowLease = scope.Acquire(
|
RenderSceneGeneration.FromRaw(1)),
|
||||||
"shadow render scene",
|
static value => value.Dispose());
|
||||||
() => new RenderSceneShadowRuntime(
|
|
||||||
RenderSceneGeneration.FromRaw(1)),
|
|
||||||
static value => value.Dispose());
|
|
||||||
}
|
|
||||||
RenderSceneShadowRuntime? renderSceneShadow =
|
RenderSceneShadowRuntime? renderSceneShadow =
|
||||||
renderSceneShadowLease?.Resource;
|
renderSceneShadowLease?.Resource;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
using AcDream.App.Rendering.Scene;
|
||||||
using AcDream.App.Rendering.Sky;
|
using AcDream.App.Rendering.Sky;
|
||||||
using AcDream.App.Rendering.Wb;
|
using AcDream.App.Rendering.Wb;
|
||||||
using AcDream.Core.Rendering;
|
using AcDream.Core.Rendering;
|
||||||
|
|
@ -79,8 +80,33 @@ internal interface IOutdoorSceneParticleOwnerSource
|
||||||
IReadOnlySet<uint> OutdoorSceneParticleEntityIds { get; }
|
IReadOnlySet<uint> OutdoorSceneParticleEntityIds { get; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal readonly record struct RenderFrameEntityDrawRequest(
|
||||||
|
RenderFrameView View,
|
||||||
|
RenderFrameCandidateRoute Route,
|
||||||
|
int RouteIndex,
|
||||||
|
uint CellId,
|
||||||
|
uint TupleLandblockId);
|
||||||
|
|
||||||
|
internal interface IRenderFrameEntityPassExecutor
|
||||||
|
{
|
||||||
|
void BeginEntityFrame(in RenderFrameView view);
|
||||||
|
|
||||||
|
bool DrawEntityRoute(
|
||||||
|
ICamera camera,
|
||||||
|
in RenderFrameView view,
|
||||||
|
RenderFrameCandidateRoute route,
|
||||||
|
int routeIndex,
|
||||||
|
uint cellId,
|
||||||
|
uint tupleLandblockId);
|
||||||
|
|
||||||
|
void CompleteEntityFrame(in RenderFrameView view);
|
||||||
|
|
||||||
|
void AbortEntityFrame();
|
||||||
|
}
|
||||||
|
|
||||||
internal sealed class RetailPViewPassExecutor :
|
internal sealed class RetailPViewPassExecutor :
|
||||||
IRetailPViewPassExecutor,
|
IRetailPViewPassExecutor,
|
||||||
|
IRenderFrameEntityPassExecutor,
|
||||||
IOutdoorSceneParticleOwnerSource
|
IOutdoorSceneParticleOwnerSource
|
||||||
{
|
{
|
||||||
private readonly GL _gl;
|
private readonly GL _gl;
|
||||||
|
|
@ -147,6 +173,30 @@ internal sealed class RetailPViewPassExecutor :
|
||||||
_particleClassifications.BeginFrame();
|
_particleClassifications.BeginFrame();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void BeginEntityFrame(in RenderFrameView view) =>
|
||||||
|
_entities.BeginPackedProductionFrame(in view);
|
||||||
|
|
||||||
|
public bool DrawEntityRoute(
|
||||||
|
ICamera camera,
|
||||||
|
in RenderFrameView view,
|
||||||
|
RenderFrameCandidateRoute route,
|
||||||
|
int routeIndex,
|
||||||
|
uint cellId,
|
||||||
|
uint tupleLandblockId) =>
|
||||||
|
_entities.DrawPackedProductionRoute(
|
||||||
|
camera,
|
||||||
|
in view,
|
||||||
|
route,
|
||||||
|
routeIndex,
|
||||||
|
cellId,
|
||||||
|
tupleLandblockId);
|
||||||
|
|
||||||
|
public void CompleteEntityFrame(in RenderFrameView view) =>
|
||||||
|
_entities.CompletePackedProductionFrame(in view);
|
||||||
|
|
||||||
|
public void AbortEntityFrame() =>
|
||||||
|
_entities.AbortPackedProductionFrame();
|
||||||
|
|
||||||
public void AbortFrame()
|
public void AbortFrame()
|
||||||
{
|
{
|
||||||
List<Exception>? failures = null;
|
List<Exception>? failures = null;
|
||||||
|
|
@ -332,7 +382,18 @@ internal sealed class RetailPViewPassExecutor :
|
||||||
_terrainDiagnostics.Complete();
|
_terrainDiagnostics.Complete();
|
||||||
|
|
||||||
DisableClipDistances();
|
DisableClipDistances();
|
||||||
if (context.OutdoorEntities.Count > 0)
|
if (context.EntityDraw is RenderFrameEntityDrawRequest request)
|
||||||
|
{
|
||||||
|
RenderFrameView drawView = request.View;
|
||||||
|
_entities.DrawPackedProductionRoute(
|
||||||
|
frame.Camera,
|
||||||
|
in drawView,
|
||||||
|
request.Route,
|
||||||
|
request.RouteIndex,
|
||||||
|
request.CellId,
|
||||||
|
request.TupleLandblockId);
|
||||||
|
}
|
||||||
|
else if (context.OutdoorEntities.Count > 0)
|
||||||
{
|
{
|
||||||
var sceneryEntry = (
|
var sceneryEntry = (
|
||||||
frame.PlayerLandblockId ?? 0u,
|
frame.PlayerLandblockId ?? 0u,
|
||||||
|
|
@ -363,7 +424,18 @@ internal sealed class RetailPViewPassExecutor :
|
||||||
_clipFrame.BindTerrainClip(_gl);
|
_clipFrame.BindTerrainClip(_gl);
|
||||||
|
|
||||||
DisableClipDistances();
|
DisableClipDistances();
|
||||||
if (context.Dynamics.Count > 0)
|
if (context.EntityDraw is RenderFrameEntityDrawRequest request)
|
||||||
|
{
|
||||||
|
RenderFrameView drawView = request.View;
|
||||||
|
_entities.DrawPackedProductionRoute(
|
||||||
|
frame.Camera,
|
||||||
|
in drawView,
|
||||||
|
request.Route,
|
||||||
|
request.RouteIndex,
|
||||||
|
request.CellId,
|
||||||
|
request.TupleLandblockId);
|
||||||
|
}
|
||||||
|
else if (context.Dynamics.Count > 0)
|
||||||
{
|
{
|
||||||
var dynamicsEntry = (
|
var dynamicsEntry = (
|
||||||
frame.PlayerLandblockId ?? 0u,
|
frame.PlayerLandblockId ?? 0u,
|
||||||
|
|
|
||||||
|
|
@ -76,13 +76,6 @@ public sealed class RetailPViewRenderer
|
||||||
InteriorEntityPartition.IObserver? partitionObserver,
|
InteriorEntityPartition.IObserver? partitionObserver,
|
||||||
RenderScenePViewFrameProductController? sceneFrameProduct = null)
|
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;
|
_partitionObserver = partitionObserver;
|
||||||
_candidateObserver = partitionObserver as ICurrentRenderPViewObserver;
|
_candidateObserver = partitionObserver as ICurrentRenderPViewObserver;
|
||||||
_sceneFrameProduct = sceneFrameProduct;
|
_sceneFrameProduct = sceneFrameProduct;
|
||||||
|
|
@ -212,6 +205,16 @@ public sealed class RetailPViewRenderer
|
||||||
ctx.ViewProjection,
|
ctx.ViewProjection,
|
||||||
_viewconeScratch);
|
_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();
|
_candidateObserver?.BeginPViewFrame();
|
||||||
try
|
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();
|
passes.UseIndoorMembershipOnlyRouting();
|
||||||
DrawExitPortalMasks(ctx, passes, pvFrame, clipAssembly, drawableCells);
|
DrawExitPortalMasks(ctx, passes, pvFrame, clipAssembly, drawableCells);
|
||||||
DrawEnvCellShells(passes, pvFrame);
|
DrawEnvCellShells(passes, pvFrame);
|
||||||
DrawCellObjectLists(ctx, passes, pvFrame, clipAssembly, drawableCells, partition, viewcone);
|
DrawCellObjectLists(
|
||||||
DrawDynamicsLast(ctx, passes, partition, viewcone, ctx.RootCell.IsOutdoorNode);
|
ctx,
|
||||||
|
passes,
|
||||||
_candidateObserver?.CompletePViewFrame();
|
|
||||||
_sceneFrameProduct?.BuildAndCompare(
|
|
||||||
pvFrame,
|
pvFrame,
|
||||||
clipAssembly,
|
clipAssembly,
|
||||||
viewcone,
|
|
||||||
_lookInFrames,
|
|
||||||
drawableCells,
|
drawableCells,
|
||||||
ctx.Cells,
|
partition,
|
||||||
ctx.AnimatedEntityIds,
|
viewcone,
|
||||||
ctx.PlayerLandblockId ?? 0,
|
frameEntityPasses,
|
||||||
|
in frameView);
|
||||||
|
DrawDynamicsLast(
|
||||||
|
ctx,
|
||||||
|
passes,
|
||||||
|
partition,
|
||||||
|
viewcone,
|
||||||
ctx.RootCell.IsOutdoorNode,
|
ctx.RootCell.IsOutdoorNode,
|
||||||
ctx.CameraWorldPosition);
|
frameEntityPasses,
|
||||||
|
in frameView);
|
||||||
|
|
||||||
|
if (entityFrameOpen)
|
||||||
|
{
|
||||||
|
frameEntityPasses!.CompleteEntityFrame(in frameView);
|
||||||
|
entityFrameOpen = false;
|
||||||
|
}
|
||||||
|
_candidateObserver?.CompletePViewFrame();
|
||||||
|
_sceneFrameProduct?.CompleteProduction(in frameView);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
if (entityFrameOpen)
|
||||||
|
frameEntityPasses!.AbortEntityFrame();
|
||||||
_candidateObserver?.AbortPViewFrame();
|
_candidateObserver?.AbortPViewFrame();
|
||||||
throw;
|
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
|
// 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,
|
RetailPViewFrameInput ctx,
|
||||||
IRetailPViewPassExecutor passes,
|
IRetailPViewPassExecutor passes,
|
||||||
ClipFrameAssembly clipAssembly,
|
ClipFrameAssembly clipAssembly,
|
||||||
InteriorEntityPartition.Result partition)
|
InteriorEntityPartition.Result partition,
|
||||||
|
IRenderFrameEntityPassExecutor? frameEntityPasses,
|
||||||
|
in RenderFrameView frameView)
|
||||||
{
|
{
|
||||||
if (_lookInFrames.Count == 0)
|
if (_lookInFrames.Count == 0)
|
||||||
return;
|
return;
|
||||||
|
|
@ -475,7 +521,16 @@ public sealed class RetailPViewRenderer
|
||||||
i,
|
i,
|
||||||
cellId,
|
cellId,
|
||||||
_cellStaticScratch);
|
_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
|
// The cell-particles pass for look-in cells — retail's
|
||||||
// nested DrawCells draws objects WITH their emitters.
|
// nested DrawCells draws objects WITH their emitters.
|
||||||
|
|
@ -492,7 +547,9 @@ public sealed class RetailPViewRenderer
|
||||||
IRetailPViewPassExecutor passes,
|
IRetailPViewPassExecutor passes,
|
||||||
ClipFrameAssembly clipAssembly,
|
ClipFrameAssembly clipAssembly,
|
||||||
InteriorEntityPartition.Result partition,
|
InteriorEntityPartition.Result partition,
|
||||||
ViewconeCuller viewcone)
|
ViewconeCuller viewcone,
|
||||||
|
IRenderFrameEntityPassExecutor? frameEntityPasses,
|
||||||
|
in RenderFrameView frameView)
|
||||||
{
|
{
|
||||||
if (clipAssembly.OutsideViewSlices.Length == 0)
|
if (clipAssembly.OutsideViewSlices.Length == 0)
|
||||||
return;
|
return;
|
||||||
|
|
@ -534,15 +591,37 @@ public sealed class RetailPViewRenderer
|
||||||
probeSliceIndex,
|
probeSliceIndex,
|
||||||
0,
|
0,
|
||||||
_outdoorStaticScratch);
|
_outdoorStaticScratch);
|
||||||
|
RenderFrameEntityDrawRequest? entityDraw =
|
||||||
|
frameEntityPasses is null
|
||||||
|
? null
|
||||||
|
: new RenderFrameEntityDrawRequest(
|
||||||
|
frameView,
|
||||||
|
RenderFrameCandidateRoute.LandscapeOutdoorStatic,
|
||||||
|
probeSliceIndex,
|
||||||
|
0,
|
||||||
|
ctx.PlayerLandblockId ?? 0);
|
||||||
probeSliceIndex++;
|
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
|
// #124: far-building look-ins draw HERE — still inside the landscape
|
||||||
// stage (their punches mark against the terrain/exterior depth just
|
// stage (their punches mark against the terrain/exterior depth just
|
||||||
// drawn), strictly BEFORE the depth clear + seals below, matching
|
// drawn), strictly BEFORE the depth clear + seals below, matching
|
||||||
// retail's LScape::draw placement (DrawCells pc:432719 vs 432732/432785).
|
// 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
|
// LATE phase (per slice): outside-stage dynamics' meshes (#118 — drawn
|
||||||
// pre-clear so the seal protects their aperture pixels; AFTER the
|
// pre-clear so the seal protects their aperture pixels; AFTER the
|
||||||
|
|
@ -591,9 +670,25 @@ public sealed class RetailPViewRenderer
|
||||||
probeSliceIndex,
|
probeSliceIndex,
|
||||||
0,
|
0,
|
||||||
_outdoorStaticScratch);
|
_outdoorStaticScratch);
|
||||||
|
RenderFrameEntityDrawRequest? entityDraw =
|
||||||
|
frameEntityPasses is null
|
||||||
|
? null
|
||||||
|
: new RenderFrameEntityDrawRequest(
|
||||||
|
frameView,
|
||||||
|
RenderFrameCandidateRoute.LandscapeOutsideDynamic,
|
||||||
|
probeSliceIndex,
|
||||||
|
0,
|
||||||
|
ctx.PlayerLandblockId ?? 0);
|
||||||
probeSliceIndex++;
|
probeSliceIndex++;
|
||||||
passes.DrawLandscapeSliceLate(ctx, new RetailPViewLandscapeLateSliceContext(
|
passes.DrawLandscapeSliceLate(
|
||||||
slice, _outdoorStaticScratch, _lateParticleOwnerScratch));
|
ctx,
|
||||||
|
new RetailPViewLandscapeLateSliceContext(
|
||||||
|
slice,
|
||||||
|
_outdoorStaticScratch,
|
||||||
|
_lateParticleOwnerScratch)
|
||||||
|
{
|
||||||
|
EntityDraw = entityDraw,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// #131: UNATTACHED emitters (AttachedObjectId == 0 — portal swirls,
|
// #131: UNATTACHED emitters (AttachedObjectId == 0 — portal swirls,
|
||||||
|
|
@ -702,7 +797,9 @@ public sealed class RetailPViewRenderer
|
||||||
IRetailPViewPassExecutor passes,
|
IRetailPViewPassExecutor passes,
|
||||||
InteriorEntityPartition.Result partition,
|
InteriorEntityPartition.Result partition,
|
||||||
ViewconeCuller viewcone,
|
ViewconeCuller viewcone,
|
||||||
bool rootIsOutdoor)
|
bool rootIsOutdoor,
|
||||||
|
IRenderFrameEntityPassExecutor? frameEntityPasses,
|
||||||
|
in RenderFrameView frameView)
|
||||||
{
|
{
|
||||||
if (partition.Dynamics.Count == 0)
|
if (partition.Dynamics.Count == 0)
|
||||||
return;
|
return;
|
||||||
|
|
@ -749,7 +846,16 @@ public sealed class RetailPViewRenderer
|
||||||
0,
|
0,
|
||||||
_dynamicsScratch);
|
_dynamicsScratch);
|
||||||
passes.UseIndoorMembershipOnlyRouting();
|
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)
|
// #121: dynamics' attached emitters (portal swirls, creature effects)
|
||||||
// gate through the SAME cone-surviving owner set as their meshes —
|
// gate through the SAME cone-surviving owner set as their meshes —
|
||||||
|
|
@ -776,7 +882,9 @@ public sealed class RetailPViewRenderer
|
||||||
ClipFrameAssembly clipAssembly,
|
ClipFrameAssembly clipAssembly,
|
||||||
HashSet<uint> drawableCells,
|
HashSet<uint> drawableCells,
|
||||||
InteriorEntityPartition.Result partition,
|
InteriorEntityPartition.Result partition,
|
||||||
ViewconeCuller viewcone)
|
ViewconeCuller viewcone,
|
||||||
|
IRenderFrameEntityPassExecutor? frameEntityPasses,
|
||||||
|
in RenderFrameView frameView)
|
||||||
{
|
{
|
||||||
// T1: per-cell STATIC object lists only (dat-baked 0x40 statics) —
|
// T1: per-cell STATIC object lists only (dat-baked 0x40 statics) —
|
||||||
// dynamics moved to DrawDynamicsLast. Far→near with the cells, after
|
// dynamics moved to DrawDynamicsLast. Far→near with the cells, after
|
||||||
|
|
@ -840,7 +948,16 @@ public sealed class RetailPViewRenderer
|
||||||
0,
|
0,
|
||||||
_allCellStatics);
|
_allCellStatics);
|
||||||
passes.UseIndoorMembershipOnlyRouting();
|
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
|
// Cell-particle pass — consolidated across ALL visible cells into ONE
|
||||||
|
|
@ -863,6 +980,35 @@ public sealed class RetailPViewRenderer
|
||||||
new RetailPViewCellSliceContext(0u, NoClipSlice, _allCellStatics));
|
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).
|
// T3 scratch lists (render thread only; cleared per use).
|
||||||
private readonly List<WorldEntity> _outdoorStaticScratch = new();
|
private readonly List<WorldEntity> _outdoorStaticScratch = new();
|
||||||
private readonly List<WorldEntity> _cellStaticScratch = new();
|
private readonly List<WorldEntity> _cellStaticScratch = new();
|
||||||
|
|
@ -1241,7 +1387,10 @@ public sealed class RetailPViewFrameResult
|
||||||
|
|
||||||
public readonly record struct RetailPViewLandscapeSliceContext(
|
public readonly record struct RetailPViewLandscapeSliceContext(
|
||||||
ClipViewSlice Slice,
|
ClipViewSlice Slice,
|
||||||
IReadOnlyList<WorldEntity> OutdoorEntities);
|
IReadOnlyList<WorldEntity> OutdoorEntities)
|
||||||
|
{
|
||||||
|
internal RenderFrameEntityDrawRequest? EntityDraw { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>#131/#132: the late landscape phase's per-slice payload —
|
/// <summary>#131/#132: the late landscape phase's per-slice payload —
|
||||||
/// outside-stage dynamics to mesh-draw, plus the full scene-particle owner
|
/// 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(
|
public readonly record struct RetailPViewLandscapeLateSliceContext(
|
||||||
ClipViewSlice Slice,
|
ClipViewSlice Slice,
|
||||||
IReadOnlyList<WorldEntity> Dynamics,
|
IReadOnlyList<WorldEntity> Dynamics,
|
||||||
IReadOnlyList<WorldEntity> ParticleOwners);
|
IReadOnlyList<WorldEntity> ParticleOwners)
|
||||||
|
{
|
||||||
|
internal RenderFrameEntityDrawRequest? EntityDraw { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
public readonly record struct RetailPViewCellSliceContext(
|
public readonly record struct RetailPViewCellSliceContext(
|
||||||
uint CellId,
|
uint CellId,
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,8 @@ namespace AcDream.App.Rendering.Scene.Arch;
|
||||||
/// projection. Arch identities never leave this namespace; callers only see
|
/// projection. Arch identities never leave this namespace; callers only see
|
||||||
/// generation-checked acdream value types.
|
/// generation-checked acdream value types.
|
||||||
///
|
///
|
||||||
/// Slice F1 deliberately leaves this owner unbound and non-drawing.
|
/// Slice F introduced this owner in non-drawing shadow mode; Slice G supplies
|
||||||
|
/// its borrowed frame product to the production renderer.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
||||||
{
|
{
|
||||||
|
|
@ -35,6 +36,7 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
||||||
private ArchWorld _world;
|
private ArchWorld _world;
|
||||||
private RenderProjectionCounts _counts;
|
private RenderProjectionCounts _counts;
|
||||||
private ulong _lastAppliedJournalSequence;
|
private ulong _lastAppliedJournalSequence;
|
||||||
|
private ulong _indexRevision = 1;
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
|
||||||
public ArchRenderScene(RenderSceneGeneration initialGeneration)
|
public ArchRenderScene(RenderSceneGeneration initialGeneration)
|
||||||
|
|
@ -225,6 +227,7 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
||||||
_counts = default;
|
_counts = default;
|
||||||
_lastAppliedJournalSequence = 0;
|
_lastAppliedJournalSequence = 0;
|
||||||
Generation = replacementGeneration;
|
Generation = replacementGeneration;
|
||||||
|
AdvanceIndexRevision();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
|
@ -274,6 +277,13 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
||||||
_dirty.Count);
|
_dirty.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ulong IRenderSceneQuerySource.GetIndexRevision(
|
||||||
|
RenderSceneGeneration generation)
|
||||||
|
{
|
||||||
|
EnsureQueryGeneration(generation);
|
||||||
|
return _indexRevision;
|
||||||
|
}
|
||||||
|
|
||||||
bool IRenderSceneQuerySource.TryGet(
|
bool IRenderSceneQuerySource.TryGet(
|
||||||
RenderSceneGeneration generation,
|
RenderSceneGeneration generation,
|
||||||
RenderProjectionId id,
|
RenderProjectionId id,
|
||||||
|
|
@ -591,6 +601,12 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
||||||
in RenderProjectionRecord prior,
|
in RenderProjectionRecord prior,
|
||||||
in RenderProjectionRecord current)
|
in RenderProjectionRecord current)
|
||||||
{
|
{
|
||||||
|
if (IndexMembershipEquals(in prior, in current))
|
||||||
|
{
|
||||||
|
SynchronizeDirtyIndex(in current);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
RemoveFromIndices(in prior);
|
RemoveFromIndices(in prior);
|
||||||
AddToIndices(in current);
|
AddToIndices(in current);
|
||||||
}
|
}
|
||||||
|
|
@ -635,6 +651,7 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
||||||
_lightCandidates.Add(record.Id);
|
_lightCandidates.Add(record.Id);
|
||||||
if (record.DirtyMask != RenderDirtyMask.None)
|
if (record.DirtyMask != RenderDirtyMask.None)
|
||||||
_dirty.Add(record.Id);
|
_dirty.Add(record.Id);
|
||||||
|
AdvanceIndexRevision();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RemoveFromIndices(in RenderProjectionRecord record)
|
private void RemoveFromIndices(in RenderProjectionRecord record)
|
||||||
|
|
@ -650,6 +667,46 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
||||||
_dirty.Remove(record.Id);
|
_dirty.Remove(record.Id);
|
||||||
RemoveCell(_cellStatics, record.Residency.FullCellId, record.Id);
|
RemoveCell(_cellStatics, record.Residency.FullCellId, record.Id);
|
||||||
RemoveCell(_cellDynamics, record.Residency.FullCellId, record.Id);
|
RemoveCell(_cellDynamics, record.Residency.FullCellId, record.Id);
|
||||||
|
AdvanceIndexRevision();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IndexMembershipEquals(
|
||||||
|
in RenderProjectionRecord left,
|
||||||
|
in RenderProjectionRecord right)
|
||||||
|
{
|
||||||
|
const RenderProjectionFlags indexedFlags =
|
||||||
|
RenderProjectionFlags.Translucent
|
||||||
|
| RenderProjectionFlags.Selectable
|
||||||
|
| RenderProjectionFlags.LightCandidate
|
||||||
|
| RenderProjectionFlags.PortalStraddling
|
||||||
|
| RenderProjectionFlags.SpatiallyResident;
|
||||||
|
|
||||||
|
return left.ProjectionClass == right.ProjectionClass
|
||||||
|
&& left.Source.ParentCellId == right.Source.ParentCellId
|
||||||
|
&& left.Residency.FullCellId == right.Residency.FullCellId
|
||||||
|
&& (left.Flags & indexedFlags) == (right.Flags & indexedFlags)
|
||||||
|
&& left.MeshSet.MeshCount == right.MeshSet.MeshCount
|
||||||
|
&& left.SortKey == right.SortKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SynchronizeDirtyIndex(
|
||||||
|
in RenderProjectionRecord record)
|
||||||
|
{
|
||||||
|
if (record.DirtyMask == RenderDirtyMask.None)
|
||||||
|
_dirty.Remove(record.Id);
|
||||||
|
else
|
||||||
|
_dirty.Add(record.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AdvanceIndexRevision()
|
||||||
|
{
|
||||||
|
if (_indexRevision == ulong.MaxValue)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Render-scene index revision space was exhausted.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_indexRevision++;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsDynamic(RenderProjectionClass projectionClass) =>
|
private static bool IsDynamic(RenderProjectionClass projectionClass) =>
|
||||||
|
|
|
||||||
|
|
@ -455,6 +455,7 @@ internal interface IRenderSceneQuerySource
|
||||||
{
|
{
|
||||||
RenderProjectionCounts GetCounts(RenderSceneGeneration generation);
|
RenderProjectionCounts GetCounts(RenderSceneGeneration generation);
|
||||||
RenderSceneIndexCounts GetIndexCounts(RenderSceneGeneration generation);
|
RenderSceneIndexCounts GetIndexCounts(RenderSceneGeneration generation);
|
||||||
|
ulong GetIndexRevision(RenderSceneGeneration generation);
|
||||||
|
|
||||||
bool TryGet(
|
bool TryGet(
|
||||||
RenderSceneGeneration generation,
|
RenderSceneGeneration generation,
|
||||||
|
|
@ -503,6 +504,9 @@ internal readonly struct RenderSceneQuery
|
||||||
public RenderSceneIndexCounts IndexCounts =>
|
public RenderSceneIndexCounts IndexCounts =>
|
||||||
Source.GetIndexCounts(Generation);
|
Source.GetIndexCounts(Generation);
|
||||||
|
|
||||||
|
public ulong IndexRevision =>
|
||||||
|
Source.GetIndexRevision(Generation);
|
||||||
|
|
||||||
public bool TryGet(
|
public bool TryGet(
|
||||||
RenderProjectionId id,
|
RenderProjectionId id,
|
||||||
out RenderProjectionRecord record) =>
|
out RenderProjectionRecord record) =>
|
||||||
|
|
|
||||||
|
|
@ -113,16 +113,15 @@ internal interface IRenderFrameProductSnapshotSource
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// G1 compare-only owner. It builds the candidate frame product directly from
|
/// Owns the same-frame PView product. Diagnostic composition may compare it
|
||||||
/// the incremental scene after the accepted PView traversal, borrows it in the
|
/// with the retired current-path observer; production borrows the exact same
|
||||||
/// same render frame, and compares route membership against the current-path
|
/// arena and supplies its ordered ranges directly to the dispatcher.
|
||||||
/// observer. It never supplies a production draw input.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class RenderScenePViewFrameProductController :
|
internal sealed class RenderScenePViewFrameProductController :
|
||||||
IRenderFrameProductSnapshotSource
|
IRenderFrameProductSnapshotSource
|
||||||
{
|
{
|
||||||
private readonly RenderSceneShadowRuntime _shadow;
|
private readonly RenderSceneShadowRuntime _shadow;
|
||||||
private readonly CurrentRenderSceneOracle _current;
|
private readonly CurrentRenderSceneOracle? _current;
|
||||||
private readonly WbDrawDispatcher? _dispatcher;
|
private readonly WbDrawDispatcher? _dispatcher;
|
||||||
private readonly RenderScenePViewFrameBuilder _builder = new();
|
private readonly RenderScenePViewFrameBuilder _builder = new();
|
||||||
private readonly RenderFrameExchange _exchange = new();
|
private readonly RenderFrameExchange _exchange = new();
|
||||||
|
|
@ -155,12 +154,12 @@ internal sealed class RenderScenePViewFrameProductController :
|
||||||
|
|
||||||
public RenderScenePViewFrameProductController(
|
public RenderScenePViewFrameProductController(
|
||||||
RenderSceneShadowRuntime shadow,
|
RenderSceneShadowRuntime shadow,
|
||||||
CurrentRenderSceneOracle current,
|
CurrentRenderSceneOracle? current = null,
|
||||||
Action<string>? log = null,
|
Action<string>? log = null,
|
||||||
WbDrawDispatcher? dispatcher = null)
|
WbDrawDispatcher? dispatcher = null)
|
||||||
{
|
{
|
||||||
_shadow = shadow ?? throw new ArgumentNullException(nameof(shadow));
|
_shadow = shadow ?? throw new ArgumentNullException(nameof(shadow));
|
||||||
_current = current ?? throw new ArgumentNullException(nameof(current));
|
_current = current;
|
||||||
_log = log;
|
_log = log;
|
||||||
_dispatcher = dispatcher;
|
_dispatcher = dispatcher;
|
||||||
Snapshot = RenderFrameProductComparisonSnapshot.Disabled with
|
Snapshot = RenderFrameProductComparisonSnapshot.Disabled with
|
||||||
|
|
@ -171,6 +170,56 @@ internal sealed class RenderScenePViewFrameProductController :
|
||||||
|
|
||||||
public RenderFrameProductComparisonSnapshot Snapshot { get; private set; }
|
public RenderFrameProductComparisonSnapshot Snapshot { get; private set; }
|
||||||
|
|
||||||
|
private CurrentRenderSceneOracle Current =>
|
||||||
|
_current
|
||||||
|
?? throw new InvalidOperationException(
|
||||||
|
"Current-path comparison was requested without a referee.");
|
||||||
|
|
||||||
|
public RenderFrameView BuildAndBorrow(
|
||||||
|
PortalVisibilityFrame portalFrame,
|
||||||
|
ClipFrameAssembly clipAssembly,
|
||||||
|
ViewconeCuller viewcone,
|
||||||
|
IReadOnlyList<PortalVisibilityFrame> lookInFrames,
|
||||||
|
HashSet<uint> drawableCells,
|
||||||
|
IRetailPViewCellSource cells,
|
||||||
|
HashSet<uint>? animatedEntityIds,
|
||||||
|
bool rootIsOutdoor)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(portalFrame);
|
||||||
|
ArgumentNullException.ThrowIfNull(clipAssembly);
|
||||||
|
ArgumentNullException.ThrowIfNull(viewcone);
|
||||||
|
ArgumentNullException.ThrowIfNull(lookInFrames);
|
||||||
|
ArgumentNullException.ThrowIfNull(drawableCells);
|
||||||
|
ArgumentNullException.ThrowIfNull(cells);
|
||||||
|
|
||||||
|
ulong frameSequence = checked(++_productFrameSequence);
|
||||||
|
RenderSceneQuery scene = _shadow.Query;
|
||||||
|
RenderSceneDigest sourceDigest = _current is null
|
||||||
|
? new RenderSceneDigest(
|
||||||
|
scene.Generation,
|
||||||
|
scene.Counts,
|
||||||
|
default)
|
||||||
|
: _shadow.BuildDigest();
|
||||||
|
var input = new RenderScenePViewBuildInput(
|
||||||
|
scene,
|
||||||
|
sourceDigest,
|
||||||
|
portalFrame,
|
||||||
|
clipAssembly,
|
||||||
|
viewcone,
|
||||||
|
lookInFrames,
|
||||||
|
drawableCells,
|
||||||
|
cells,
|
||||||
|
animatedEntityIds,
|
||||||
|
rootIsOutdoor);
|
||||||
|
_builder.Build(_exchange, frameSequence, in input);
|
||||||
|
return _exchange.BorrowLatest(
|
||||||
|
scene.Generation,
|
||||||
|
frameSequence);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Release(in RenderFrameView view) =>
|
||||||
|
_exchange.Release(in view);
|
||||||
|
|
||||||
public void BuildAndCompare(
|
public void BuildAndCompare(
|
||||||
PortalVisibilityFrame portalFrame,
|
PortalVisibilityFrame portalFrame,
|
||||||
ClipFrameAssembly clipAssembly,
|
ClipFrameAssembly clipAssembly,
|
||||||
|
|
@ -183,18 +232,7 @@ internal sealed class RenderScenePViewFrameProductController :
|
||||||
bool rootIsOutdoor,
|
bool rootIsOutdoor,
|
||||||
Vector3 cameraWorldPosition = default)
|
Vector3 cameraWorldPosition = default)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(portalFrame);
|
RenderFrameView view = BuildAndBorrow(
|
||||||
ArgumentNullException.ThrowIfNull(clipAssembly);
|
|
||||||
ArgumentNullException.ThrowIfNull(viewcone);
|
|
||||||
ArgumentNullException.ThrowIfNull(lookInFrames);
|
|
||||||
ArgumentNullException.ThrowIfNull(drawableCells);
|
|
||||||
ArgumentNullException.ThrowIfNull(cells);
|
|
||||||
|
|
||||||
ulong frameSequence = checked(++_productFrameSequence);
|
|
||||||
RenderSceneQuery scene = _shadow.Query;
|
|
||||||
var input = new RenderScenePViewBuildInput(
|
|
||||||
scene,
|
|
||||||
_shadow.BuildDigest(),
|
|
||||||
portalFrame,
|
portalFrame,
|
||||||
clipAssembly,
|
clipAssembly,
|
||||||
viewcone,
|
viewcone,
|
||||||
|
|
@ -203,10 +241,6 @@ internal sealed class RenderScenePViewFrameProductController :
|
||||||
cells,
|
cells,
|
||||||
animatedEntityIds,
|
animatedEntityIds,
|
||||||
rootIsOutdoor);
|
rootIsOutdoor);
|
||||||
_builder.Build(_exchange, frameSequence, in input);
|
|
||||||
RenderFrameView view = _exchange.BorrowLatest(
|
|
||||||
scene.Generation,
|
|
||||||
frameSequence);
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Compare(
|
Compare(
|
||||||
|
|
@ -216,26 +250,92 @@ internal sealed class RenderScenePViewFrameProductController :
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_exchange.Release(in view);
|
Release(in view);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Compare(
|
public void CompleteProduction(in RenderFrameView view)
|
||||||
in RenderFrameView view,
|
{
|
||||||
uint tupleLandblockId,
|
_packedClassification =
|
||||||
Vector3 cameraWorldPosition)
|
_dispatcher?.PackedClassificationSnapshot ?? default;
|
||||||
|
if (_current is null)
|
||||||
|
{
|
||||||
|
_shadow.ClearDirty();
|
||||||
|
Snapshot = Snapshot with
|
||||||
|
{
|
||||||
|
Enabled = true,
|
||||||
|
ProductFrameSequence = view.FrameSequence,
|
||||||
|
ActualCandidateCount = view.RouteCandidates.Length,
|
||||||
|
PackedClassificationProjectionCount =
|
||||||
|
_packedClassification.ProjectionCount,
|
||||||
|
PackedClassificationStaticRebuildCount =
|
||||||
|
_packedClassification.StaticRebuildCount,
|
||||||
|
PackedClassificationSameFrameReuseCount =
|
||||||
|
_packedClassification.SameFrameReuseCount,
|
||||||
|
PackedClassificationCrossFrameReuseCount =
|
||||||
|
_packedClassification.CrossFrameReuseCount,
|
||||||
|
PackedClassificationAnimatedCount =
|
||||||
|
_packedClassification.AnimatedClassificationCount,
|
||||||
|
PackedClassificationRetiredCount =
|
||||||
|
_packedClassification.RetiredProjectionCount,
|
||||||
|
PackedClassificationRetainedPayloadBytes =
|
||||||
|
_packedClassification.RetainedPayloadBytes,
|
||||||
|
ProductCounts = view.DiagnosticCounts,
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CandidateComparison candidates =
|
||||||
|
CompareCandidateMembership(in view);
|
||||||
|
if (candidates.Successful)
|
||||||
|
_shadow.ClearDirty();
|
||||||
|
Snapshot = Snapshot with
|
||||||
|
{
|
||||||
|
Enabled = true,
|
||||||
|
ProductFrameSequence = view.FrameSequence,
|
||||||
|
CurrentPViewFrameSequence =
|
||||||
|
Current.Snapshot.CompletedPViewFrameSequence,
|
||||||
|
ComparisonCount = _comparisonCount,
|
||||||
|
SuccessfulComparisonCount = _successfulComparisonCount,
|
||||||
|
ExpectedCandidateCount = _expected.Count,
|
||||||
|
ActualCandidateCount = _actual.Count,
|
||||||
|
MismatchCount = _mismatchCount,
|
||||||
|
FirstMismatch = _firstMismatch,
|
||||||
|
ExpectedDigest = candidates.ExpectedDigest,
|
||||||
|
ActualDigest = candidates.ActualDigest,
|
||||||
|
PackedClassificationProjectionCount =
|
||||||
|
_packedClassification.ProjectionCount,
|
||||||
|
PackedClassificationStaticRebuildCount =
|
||||||
|
_packedClassification.StaticRebuildCount,
|
||||||
|
PackedClassificationSameFrameReuseCount =
|
||||||
|
_packedClassification.SameFrameReuseCount,
|
||||||
|
PackedClassificationCrossFrameReuseCount =
|
||||||
|
_packedClassification.CrossFrameReuseCount,
|
||||||
|
PackedClassificationAnimatedCount =
|
||||||
|
_packedClassification.AnimatedClassificationCount,
|
||||||
|
PackedClassificationRetiredCount =
|
||||||
|
_packedClassification.RetiredProjectionCount,
|
||||||
|
PackedClassificationRetainedPayloadBytes =
|
||||||
|
_packedClassification.RetainedPayloadBytes,
|
||||||
|
ProductCounts = view.DiagnosticCounts,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private CandidateComparison CompareCandidateMembership(
|
||||||
|
in RenderFrameView view)
|
||||||
{
|
{
|
||||||
_comparisonCount = checked(_comparisonCount + 1);
|
_comparisonCount = checked(_comparisonCount + 1);
|
||||||
_expected.Clear();
|
_expected.Clear();
|
||||||
_actual.Clear();
|
_actual.Clear();
|
||||||
|
|
||||||
IReadOnlyList<CurrentRenderPViewCandidateFingerprint> expected =
|
IReadOnlyList<CurrentRenderPViewCandidateFingerprint> expected =
|
||||||
_current.PViewCandidates;
|
Current.PViewCandidates;
|
||||||
if (_expected.Capacity < expected.Count)
|
if (_expected.Capacity < expected.Count)
|
||||||
_expected.Capacity = expected.Count;
|
_expected.Capacity = expected.Count;
|
||||||
for (int i = 0; i < expected.Count; i++)
|
for (int index = 0; index < expected.Count; index++)
|
||||||
{
|
{
|
||||||
CurrentRenderPViewCandidateFingerprint candidate = expected[i];
|
CurrentRenderPViewCandidateFingerprint candidate =
|
||||||
|
expected[index];
|
||||||
CurrentRenderProjectionFingerprint projection =
|
CurrentRenderProjectionFingerprint projection =
|
||||||
candidate.Projection;
|
candidate.Projection;
|
||||||
_expected.Add(new CandidateKey(
|
_expected.Add(new CandidateKey(
|
||||||
|
|
@ -245,11 +345,15 @@ internal sealed class RenderScenePViewFrameProductController :
|
||||||
ExpectedId(in projection)));
|
ExpectedId(in projection)));
|
||||||
}
|
}
|
||||||
|
|
||||||
ReadOnlySpan<RenderFrameCandidateRange> ranges = view.RouteRanges;
|
ReadOnlySpan<RenderFrameCandidateRange> ranges =
|
||||||
ReadOnlySpan<RenderProjectionRecord> records = view.RouteCandidates;
|
view.RouteRanges;
|
||||||
|
ReadOnlySpan<RenderProjectionRecord> records =
|
||||||
|
view.RouteCandidates;
|
||||||
if (_actual.Capacity < records.Length)
|
if (_actual.Capacity < records.Length)
|
||||||
_actual.Capacity = records.Length;
|
_actual.Capacity = records.Length;
|
||||||
for (int rangeIndex = 0; rangeIndex < ranges.Length; rangeIndex++)
|
for (int rangeIndex = 0;
|
||||||
|
rangeIndex < ranges.Length;
|
||||||
|
rangeIndex++)
|
||||||
{
|
{
|
||||||
RenderFrameCandidateRange range = ranges[rangeIndex];
|
RenderFrameCandidateRange range = ranges[rangeIndex];
|
||||||
int end = checked(range.Offset + range.Count);
|
int end = checked(range.Offset + range.Count);
|
||||||
|
|
@ -285,10 +389,32 @@ internal sealed class RenderScenePViewFrameProductController :
|
||||||
{
|
{
|
||||||
_firstMismatch = mismatch;
|
_firstMismatch = mismatch;
|
||||||
_log?.Invoke(
|
_log?.Invoke(
|
||||||
"[render-frame-product] first mismatch: " + mismatch);
|
"[render-frame-product] first mismatch: "
|
||||||
|
+ mismatch);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return new CandidateComparison(
|
||||||
|
mismatch is null,
|
||||||
|
expectedDigest,
|
||||||
|
actualDigest);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Compare(
|
||||||
|
in RenderFrameView view,
|
||||||
|
uint tupleLandblockId,
|
||||||
|
Vector3 cameraWorldPosition)
|
||||||
|
{
|
||||||
|
CandidateComparison candidates =
|
||||||
|
CompareCandidateMembership(in view);
|
||||||
|
RenderSceneHash128 expectedDigest =
|
||||||
|
candidates.ExpectedDigest;
|
||||||
|
RenderSceneHash128 actualDigest =
|
||||||
|
candidates.ActualDigest;
|
||||||
|
string? mismatch = candidates.Successful
|
||||||
|
? null
|
||||||
|
: _firstMismatch;
|
||||||
|
|
||||||
PackedInputComparison packed =
|
PackedInputComparison packed =
|
||||||
ComparePackedInput(in view, tupleLandblockId);
|
ComparePackedInput(in view, tupleLandblockId);
|
||||||
ClassifiedOutputComparison classified =
|
ClassifiedOutputComparison classified =
|
||||||
|
|
@ -312,7 +438,7 @@ internal sealed class RenderScenePViewFrameProductController :
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
ProductFrameSequence: view.FrameSequence,
|
ProductFrameSequence: view.FrameSequence,
|
||||||
CurrentPViewFrameSequence:
|
CurrentPViewFrameSequence:
|
||||||
_current.Snapshot.CompletedPViewFrameSequence,
|
Current.Snapshot.CompletedPViewFrameSequence,
|
||||||
ComparisonCount: _comparisonCount,
|
ComparisonCount: _comparisonCount,
|
||||||
SuccessfulComparisonCount: _successfulComparisonCount,
|
SuccessfulComparisonCount: _successfulComparisonCount,
|
||||||
ExpectedCandidateCount: _expected.Count,
|
ExpectedCandidateCount: _expected.Count,
|
||||||
|
|
@ -395,7 +521,7 @@ internal sealed class RenderScenePViewFrameProductController :
|
||||||
_packedClassification =
|
_packedClassification =
|
||||||
dispatcher.PackedClassificationSnapshot;
|
dispatcher.PackedClassificationSnapshot;
|
||||||
IReadOnlyList<CurrentRenderDispatcherSubmission> expected =
|
IReadOnlyList<CurrentRenderDispatcherSubmission> expected =
|
||||||
_current.DispatcherSubmissions;
|
Current.DispatcherSubmissions;
|
||||||
IReadOnlyList<CurrentRenderDispatcherSubmission> actual =
|
IReadOnlyList<CurrentRenderDispatcherSubmission> actual =
|
||||||
dispatcher.PackedDispatcherSubmissions;
|
dispatcher.PackedDispatcherSubmissions;
|
||||||
RenderSceneHash128 expectedDigest =
|
RenderSceneHash128 expectedDigest =
|
||||||
|
|
@ -426,7 +552,7 @@ internal sealed class RenderScenePViewFrameProductController :
|
||||||
}
|
}
|
||||||
|
|
||||||
IReadOnlyList<CurrentRenderSelectionFingerprint>
|
IReadOnlyList<CurrentRenderSelectionFingerprint>
|
||||||
expectedSelection = _current.SelectionParts;
|
expectedSelection = Current.SelectionParts;
|
||||||
IReadOnlyList<CurrentRenderSelectionFingerprint>
|
IReadOnlyList<CurrentRenderSelectionFingerprint>
|
||||||
actualSelection = dispatcher.PackedSelectionParts;
|
actualSelection = dispatcher.PackedSelectionParts;
|
||||||
BuildSelectionKeys(
|
BuildSelectionKeys(
|
||||||
|
|
@ -628,7 +754,7 @@ internal sealed class RenderScenePViewFrameProductController :
|
||||||
_packedActual.Clear();
|
_packedActual.Clear();
|
||||||
|
|
||||||
IReadOnlyList<CurrentRenderDispatcherFingerprint> expected =
|
IReadOnlyList<CurrentRenderDispatcherFingerprint> expected =
|
||||||
_current.DispatcherCandidates;
|
Current.DispatcherCandidates;
|
||||||
if (_packedExpected.Capacity < expected.Count)
|
if (_packedExpected.Capacity < expected.Count)
|
||||||
_packedExpected.Capacity = expected.Count;
|
_packedExpected.Capacity = expected.Count;
|
||||||
for (int index = 0; index < expected.Count; index++)
|
for (int index = 0; index < expected.Count; index++)
|
||||||
|
|
@ -750,10 +876,10 @@ internal sealed class RenderScenePViewFrameProductController :
|
||||||
|
|
||||||
private string? ComparePackedInputKeys(int actualDrawCount)
|
private string? ComparePackedInputKeys(int actualDrawCount)
|
||||||
{
|
{
|
||||||
if (_current.Snapshot.DispatcherDrawCount != actualDrawCount)
|
if (Current.Snapshot.DispatcherDrawCount != actualDrawCount)
|
||||||
{
|
{
|
||||||
return $"field=drawCount expected="
|
return $"field=drawCount expected="
|
||||||
+ $"{_current.Snapshot.DispatcherDrawCount} "
|
+ $"{Current.Snapshot.DispatcherDrawCount} "
|
||||||
+ $"actual={actualDrawCount}";
|
+ $"actual={actualDrawCount}";
|
||||||
}
|
}
|
||||||
if (_packedExpected.Count != _packedActual.Count)
|
if (_packedExpected.Count != _packedActual.Count)
|
||||||
|
|
@ -891,6 +1017,11 @@ internal sealed class RenderScenePViewFrameProductController :
|
||||||
RenderSceneHash128 ExpectedDigest,
|
RenderSceneHash128 ExpectedDigest,
|
||||||
RenderSceneHash128 ActualDigest);
|
RenderSceneHash128 ActualDigest);
|
||||||
|
|
||||||
|
private readonly record struct CandidateComparison(
|
||||||
|
bool Successful,
|
||||||
|
RenderSceneHash128 ExpectedDigest,
|
||||||
|
RenderSceneHash128 ActualDigest);
|
||||||
|
|
||||||
private readonly record struct ClassifiedOutputComparison(
|
private readonly record struct ClassifiedOutputComparison(
|
||||||
bool Successful,
|
bool Successful,
|
||||||
int ExpectedDrawCount,
|
int ExpectedDrawCount,
|
||||||
|
|
@ -993,11 +1124,19 @@ internal sealed class RenderScenePViewFrameBuilder
|
||||||
private RenderProjectionRecord[] _outdoor = [];
|
private RenderProjectionRecord[] _outdoor = [];
|
||||||
private RenderProjectionRecord[] _dynamics = [];
|
private RenderProjectionRecord[] _dynamics = [];
|
||||||
private RenderProjectionRecord[] _cell = [];
|
private RenderProjectionRecord[] _cell = [];
|
||||||
|
private RenderProjectionRecord[] _dirty = [];
|
||||||
private RenderProjectionRecord[] _survivors = [];
|
private RenderProjectionRecord[] _survivors = [];
|
||||||
private RenderProjectionRecord[] _cellRoute = [];
|
private RenderProjectionRecord[] _cellRoute = [];
|
||||||
|
private readonly Dictionary<RenderProjectionId, int>
|
||||||
|
_outdoorPositions = [];
|
||||||
|
private readonly Dictionary<RenderProjectionId, int>
|
||||||
|
_dynamicPositions = [];
|
||||||
private int _outdoorCount;
|
private int _outdoorCount;
|
||||||
private int _dynamicCount;
|
private int _dynamicCount;
|
||||||
private int _cellRouteCount;
|
private int _cellRouteCount;
|
||||||
|
private int _dirtyCount;
|
||||||
|
private RenderSceneGeneration _indexGeneration;
|
||||||
|
private ulong _indexRevision;
|
||||||
|
|
||||||
public void Build(
|
public void Build(
|
||||||
RenderFrameExchange exchange,
|
RenderFrameExchange exchange,
|
||||||
|
|
@ -1023,6 +1162,7 @@ internal sealed class RenderScenePViewFrameBuilder
|
||||||
BuildCellStaticRoute(writer, in input);
|
BuildCellStaticRoute(writer, in input);
|
||||||
BuildDynamicLastRoute(writer, in input);
|
BuildDynamicLastRoute(writer, in input);
|
||||||
writer.Publish();
|
writer.Publish();
|
||||||
|
AcknowledgeCachedDirtyRecords();
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
|
@ -1034,21 +1174,97 @@ internal sealed class RenderScenePViewFrameBuilder
|
||||||
private void LoadSceneIndices(RenderSceneQuery scene)
|
private void LoadSceneIndices(RenderSceneQuery scene)
|
||||||
{
|
{
|
||||||
RenderSceneIndexCounts counts = scene.IndexCounts;
|
RenderSceneIndexCounts counts = scene.IndexCounts;
|
||||||
EnsureCapacity(ref _outdoor, counts.OutdoorStatic);
|
ulong revision = scene.IndexRevision;
|
||||||
_outdoorCount = scene.CopyIndexTo(
|
if (scene.Generation != _indexGeneration
|
||||||
RenderSceneIndex.OutdoorStatic,
|
|| revision != _indexRevision)
|
||||||
_outdoor);
|
{
|
||||||
_outdoorCount = CompactAndSort(
|
EnsureCapacity(ref _outdoor, counts.OutdoorStatic);
|
||||||
_outdoor,
|
_outdoorCount = scene.CopyIndexTo(
|
||||||
_outdoorCount);
|
RenderSceneIndex.OutdoorStatic,
|
||||||
|
_outdoor);
|
||||||
|
_outdoorCount = CompactAndSort(
|
||||||
|
_outdoor,
|
||||||
|
_outdoorCount);
|
||||||
|
BuildPositionIndex(
|
||||||
|
_outdoor,
|
||||||
|
_outdoorCount,
|
||||||
|
_outdoorPositions);
|
||||||
|
|
||||||
EnsureCapacity(ref _dynamics, counts.Dynamic);
|
EnsureCapacity(ref _dynamics, counts.Dynamic);
|
||||||
_dynamicCount = scene.CopyIndexTo(
|
_dynamicCount = scene.CopyIndexTo(
|
||||||
RenderSceneIndex.Dynamic,
|
RenderSceneIndex.Dynamic,
|
||||||
_dynamics);
|
_dynamics);
|
||||||
_dynamicCount = CompactAndSort(
|
_dynamicCount = CompactAndSort(
|
||||||
_dynamics,
|
_dynamics,
|
||||||
_dynamicCount);
|
_dynamicCount);
|
||||||
|
BuildPositionIndex(
|
||||||
|
_dynamics,
|
||||||
|
_dynamicCount,
|
||||||
|
_dynamicPositions);
|
||||||
|
|
||||||
|
_indexGeneration = scene.Generation;
|
||||||
|
_indexRevision = revision;
|
||||||
|
}
|
||||||
|
|
||||||
|
EnsureCapacity(ref _dirty, counts.Dirty);
|
||||||
|
_dirtyCount = scene.CopyIndexTo(
|
||||||
|
RenderSceneIndex.Dirty,
|
||||||
|
_dirty);
|
||||||
|
for (int index = 0; index < _dirtyCount; index++)
|
||||||
|
{
|
||||||
|
RenderProjectionRecord record = _dirty[index];
|
||||||
|
if (_outdoorPositions.TryGetValue(
|
||||||
|
record.Id,
|
||||||
|
out int outdoorPosition))
|
||||||
|
{
|
||||||
|
_outdoor[outdoorPosition] = record;
|
||||||
|
}
|
||||||
|
if (_dynamicPositions.TryGetValue(
|
||||||
|
record.Id,
|
||||||
|
out int dynamicPosition))
|
||||||
|
{
|
||||||
|
_dynamics[dynamicPosition] = record;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AcknowledgeCachedDirtyRecords()
|
||||||
|
{
|
||||||
|
for (int index = 0; index < _dirtyCount; index++)
|
||||||
|
{
|
||||||
|
RenderProjectionId id = _dirty[index].Id;
|
||||||
|
if (_outdoorPositions.TryGetValue(
|
||||||
|
id,
|
||||||
|
out int outdoorPosition))
|
||||||
|
{
|
||||||
|
_outdoor[outdoorPosition] =
|
||||||
|
_outdoor[outdoorPosition] with
|
||||||
|
{
|
||||||
|
DirtyMask = RenderDirtyMask.None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (_dynamicPositions.TryGetValue(
|
||||||
|
id,
|
||||||
|
out int dynamicPosition))
|
||||||
|
{
|
||||||
|
_dynamics[dynamicPosition] =
|
||||||
|
_dynamics[dynamicPosition] with
|
||||||
|
{
|
||||||
|
DirtyMask = RenderDirtyMask.None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void BuildPositionIndex(
|
||||||
|
RenderProjectionRecord[] records,
|
||||||
|
int count,
|
||||||
|
Dictionary<RenderProjectionId, int> positions)
|
||||||
|
{
|
||||||
|
positions.Clear();
|
||||||
|
positions.EnsureCapacity(count);
|
||||||
|
for (int index = 0; index < count; index++)
|
||||||
|
positions.Add(records[index].Id, index);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void BuildOutdoorRoutes(
|
private void BuildOutdoorRoutes(
|
||||||
|
|
|
||||||
|
|
@ -50,10 +50,12 @@ internal interface IRenderSceneShadowSnapshotSource
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Diagnostic-only owner of Slice F's non-drawing render projection. The
|
/// Owner of the derived render projection. Slice F introduced it as a
|
||||||
/// normal runtime never constructs this type. Accepted world/live lifecycle
|
/// diagnostic shadow; Slice G keeps the same generation/incarnation journal
|
||||||
/// edges append to one ordered journal, and the update thread drains it only
|
/// boundary while lending its frame product to production drawing. Accepted
|
||||||
/// after current-frame animation, network, and attachment reconciliation.
|
/// world/live lifecycle edges append to one ordered journal, and the update
|
||||||
|
/// thread drains it only after current-frame animation, network, and
|
||||||
|
/// attachment reconciliation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class RenderSceneShadowRuntime : IDisposable
|
internal sealed class RenderSceneShadowRuntime : IDisposable
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
using AcDream.Core.Rendering;
|
||||||
using AcDream.App.Rendering.Scene;
|
using AcDream.App.Rendering.Scene;
|
||||||
using AcDream.App.Rendering.Selection;
|
using AcDream.App.Rendering.Selection;
|
||||||
using AcDream.Core.Lighting;
|
using AcDream.Core.Lighting;
|
||||||
|
|
@ -10,10 +11,10 @@ using DatReaderWriter.Enums;
|
||||||
namespace AcDream.App.Rendering.Wb;
|
namespace AcDream.App.Rendering.Wb;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// G2 compare-only classification of the scene-built frame product. This file
|
/// Scene-frame classification and production submission. The retained group
|
||||||
/// deliberately shares the production dispatcher's exact mesh, texture,
|
/// storage is also consumed by the G2/G3 compare oracle, while production
|
||||||
/// light, translucency, selection, grouping, and ordering owners while keeping
|
/// routes reuse the dispatcher's exact mesh, texture, light, translucency,
|
||||||
/// its group storage separate. It never uploads or draws.
|
/// selection, upload, and draw owners.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed unsafe partial class WbDrawDispatcher
|
public sealed unsafe partial class WbDrawDispatcher
|
||||||
{
|
{
|
||||||
|
|
@ -35,6 +36,10 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
private long _packedGroupFrame;
|
private long _packedGroupFrame;
|
||||||
private long _nextPackedGroupRegistration = 1;
|
private long _nextPackedGroupRegistration = 1;
|
||||||
private int _nextPackedInstanceSubmissionOrder;
|
private int _nextPackedInstanceSubmissionOrder;
|
||||||
|
private bool _packedProductionFrameOpen;
|
||||||
|
private RenderSceneGeneration _packedProductionGeneration;
|
||||||
|
private ulong _packedProductionFrameSequence;
|
||||||
|
private int _packedProductionNextRange;
|
||||||
|
|
||||||
internal IReadOnlyList<CurrentRenderDispatcherSubmission>
|
internal IReadOnlyList<CurrentRenderDispatcherSubmission>
|
||||||
PackedDispatcherSubmissions => _packedSubmissions;
|
PackedDispatcherSubmissions => _packedSubmissions;
|
||||||
|
|
@ -51,104 +56,28 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
uint tupleLandblockId,
|
uint tupleLandblockId,
|
||||||
Vector3 cameraWorldPosition)
|
Vector3 cameraWorldPosition)
|
||||||
{
|
{
|
||||||
if (_packedGroupFrame == long.MaxValue)
|
if (_packedProductionFrameOpen)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
"Packed dispatcher frame identity was exhausted.");
|
"The packed dispatcher oracle cannot replace an active production frame.");
|
||||||
}
|
}
|
||||||
|
|
||||||
_packedGroupFrame++;
|
BeginPackedFrameStorage(in view);
|
||||||
PruneInstanceGroupsUnusedBeforeFrame(
|
|
||||||
_packedGroups,
|
|
||||||
_packedRetiredGroupKeys,
|
|
||||||
_packedGroupFrame - 1);
|
|
||||||
_packedEntityById.Clear();
|
|
||||||
_packedSubmissions.Clear();
|
|
||||||
_packedSelectionParts.Clear();
|
|
||||||
_packedSelectionKeys.Clear();
|
|
||||||
_packedClassificationCache.BeginFrame(view.Generation);
|
|
||||||
|
|
||||||
ReadOnlySpan<RenderFrameEntityCandidate> entities =
|
|
||||||
view.EntityCandidates;
|
|
||||||
for (int i = 0; i < entities.Length; i++)
|
|
||||||
{
|
|
||||||
if (!_packedEntityById.TryAdd(
|
|
||||||
entities[i].Projection.Id,
|
|
||||||
entities[i]))
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException(
|
|
||||||
$"Packed dispatcher received duplicate projection "
|
|
||||||
+ $"{entities[i].Projection.Id}.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ReadOnlySpan<RenderFrameMeshPart> meshParts = view.MeshParts;
|
|
||||||
ReadOnlySpan<RenderProjectionRecord> routeCandidates =
|
|
||||||
view.RouteCandidates;
|
|
||||||
ReadOnlySpan<RenderFrameCandidateRange> ranges =
|
ReadOnlySpan<RenderFrameCandidateRange> ranges =
|
||||||
view.RouteRanges;
|
view.RouteRanges;
|
||||||
for (int rangeIndex = 0;
|
for (int rangeIndex = 0;
|
||||||
rangeIndex < ranges.Length;
|
rangeIndex < ranges.Length;
|
||||||
rangeIndex++)
|
rangeIndex++)
|
||||||
{
|
{
|
||||||
_nextPackedInstanceSubmissionOrder = 0;
|
PackedRangeClassification classified =
|
||||||
uint anyVao = 0;
|
ClassifyPackedRange(
|
||||||
foreach (InstanceGroup group in _packedGroups.Values)
|
in view,
|
||||||
group.ClearPerInstanceData();
|
rangeIndex,
|
||||||
|
tupleLandblockId,
|
||||||
RenderFrameCandidateRange range = ranges[rangeIndex];
|
publishSelection: false);
|
||||||
int end = checked(range.Offset + range.Count);
|
|
||||||
if ((uint)range.Offset > (uint)routeCandidates.Length
|
|
||||||
|| (uint)end > (uint)routeCandidates.Length)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException(
|
|
||||||
$"Packed dispatcher route {rangeIndex} exceeds "
|
|
||||||
+ "candidate storage.");
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int routeIndex = range.Offset;
|
|
||||||
routeIndex < end;
|
|
||||||
routeIndex++)
|
|
||||||
{
|
|
||||||
RenderProjectionRecord projection =
|
|
||||||
routeCandidates[routeIndex];
|
|
||||||
if ((projection.Flags & RenderProjectionFlags.Draw) == 0)
|
|
||||||
continue;
|
|
||||||
if (!_packedEntityById.TryGetValue(
|
|
||||||
projection.Id,
|
|
||||||
out RenderFrameEntityCandidate source))
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException(
|
|
||||||
$"Packed route references absent entity "
|
|
||||||
+ $"{projection.Id}.");
|
|
||||||
}
|
|
||||||
|
|
||||||
int meshEnd = checked(
|
|
||||||
source.MeshPartOffset + source.MeshPartCount);
|
|
||||||
if ((uint)source.MeshPartOffset
|
|
||||||
> (uint)meshParts.Length
|
|
||||||
|| (uint)meshEnd > (uint)meshParts.Length)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException(
|
|
||||||
$"Packed entity {projection.Id} exceeds mesh "
|
|
||||||
+ "part storage.");
|
|
||||||
}
|
|
||||||
|
|
||||||
RenderInstanceCandidate candidate =
|
|
||||||
RenderInstanceCandidate.FromFrame(
|
|
||||||
in source,
|
|
||||||
tupleLandblockId);
|
|
||||||
ClassifyPackedEntity(
|
|
||||||
in projection,
|
|
||||||
in candidate,
|
|
||||||
meshParts.Slice(
|
|
||||||
source.MeshPartOffset,
|
|
||||||
source.MeshPartCount),
|
|
||||||
ref anyVao);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool deferTransparent = ShouldDeferPackedTransparent(
|
bool deferTransparent = ShouldDeferPackedTransparent(
|
||||||
anyVao,
|
classified.AnyVao,
|
||||||
_alphaQueue?.IsCollecting == true);
|
_alphaQueue?.IsCollecting == true);
|
||||||
InstanceLayoutCounts counts = PartitionInstanceGroups(
|
InstanceLayoutCounts counts = PartitionInstanceGroups(
|
||||||
_packedGroups.Values,
|
_packedGroups.Values,
|
||||||
|
|
@ -176,11 +105,236 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
_packedClassificationCache.EndFrame();
|
_packedClassificationCache.EndFrame();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal void BeginPackedProductionFrame(
|
||||||
|
in RenderFrameView view)
|
||||||
|
{
|
||||||
|
if (_packedProductionFrameOpen)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"The packed dispatcher cannot begin a second production frame.");
|
||||||
|
}
|
||||||
|
|
||||||
|
BeginPackedFrameStorage(in view);
|
||||||
|
_packedProductionFrameOpen = true;
|
||||||
|
_packedProductionGeneration = view.Generation;
|
||||||
|
_packedProductionFrameSequence = view.FrameSequence;
|
||||||
|
_packedProductionNextRange = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal bool DrawPackedProductionRoute(
|
||||||
|
ICamera camera,
|
||||||
|
in RenderFrameView view,
|
||||||
|
RenderFrameCandidateRoute route,
|
||||||
|
int routeIndex,
|
||||||
|
uint cellId,
|
||||||
|
uint tupleLandblockId)
|
||||||
|
{
|
||||||
|
ValidatePackedProductionView(in view);
|
||||||
|
ReadOnlySpan<RenderFrameCandidateRange> ranges =
|
||||||
|
view.RouteRanges;
|
||||||
|
if (_packedProductionNextRange >= ranges.Length)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
RenderFrameCandidateRange range =
|
||||||
|
ranges[_packedProductionNextRange];
|
||||||
|
if (range.Route != route
|
||||||
|
|| range.RouteIndex != routeIndex
|
||||||
|
|| range.CellId != cellId)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int rangePosition = _packedProductionNextRange++;
|
||||||
|
bool diagnosticsEnabled = BeginEntityDispatch(
|
||||||
|
camera,
|
||||||
|
out Matrix4x4 viewProjection,
|
||||||
|
out Vector3 cameraWorldPosition);
|
||||||
|
PackedRangeClassification classified =
|
||||||
|
ClassifyPackedRange(
|
||||||
|
in view,
|
||||||
|
rangePosition,
|
||||||
|
tupleLandblockId,
|
||||||
|
publishSelection: true);
|
||||||
|
ExecuteClassifiedGroups(
|
||||||
|
viewProjection,
|
||||||
|
cameraWorldPosition,
|
||||||
|
classified.AnyVao,
|
||||||
|
_packedGroups.Values,
|
||||||
|
EntitySet.All,
|
||||||
|
classified.CandidateCount,
|
||||||
|
classified.MeshPartCount,
|
||||||
|
diagnosticsEnabled,
|
||||||
|
observeCurrentPath: false);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void CompletePackedProductionFrame(
|
||||||
|
in RenderFrameView view)
|
||||||
|
{
|
||||||
|
ValidatePackedProductionView(in view);
|
||||||
|
int expected = view.RouteRanges.Length;
|
||||||
|
if (_packedProductionNextRange != expected)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"The packed dispatcher did not consume the complete route stream: "
|
||||||
|
+ $"consumed={_packedProductionNextRange} expected={expected}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_packedClassificationCache.EndFrame();
|
||||||
|
ResetPackedProductionFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void AbortPackedProductionFrame()
|
||||||
|
{
|
||||||
|
if (!_packedProductionFrameOpen)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_packedClassificationCache.EndFrame();
|
||||||
|
ResetPackedProductionFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BeginPackedFrameStorage(
|
||||||
|
in RenderFrameView view)
|
||||||
|
{
|
||||||
|
if (_packedGroupFrame == long.MaxValue)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Packed dispatcher frame identity was exhausted.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_packedGroupFrame++;
|
||||||
|
PruneInstanceGroupsUnusedBeforeFrame(
|
||||||
|
_packedGroups,
|
||||||
|
_packedRetiredGroupKeys,
|
||||||
|
_packedGroupFrame - 1);
|
||||||
|
_packedEntityById.Clear();
|
||||||
|
_packedSubmissions.Clear();
|
||||||
|
_packedSelectionParts.Clear();
|
||||||
|
_packedSelectionKeys.Clear();
|
||||||
|
_packedClassificationCache.BeginFrame(view.Generation);
|
||||||
|
|
||||||
|
ReadOnlySpan<RenderFrameEntityCandidate> entities =
|
||||||
|
view.EntityCandidates;
|
||||||
|
for (int index = 0; index < entities.Length; index++)
|
||||||
|
{
|
||||||
|
if (!_packedEntityById.TryAdd(
|
||||||
|
entities[index].Projection.Id,
|
||||||
|
entities[index]))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Packed dispatcher received duplicate projection "
|
||||||
|
+ $"{entities[index].Projection.Id}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private PackedRangeClassification ClassifyPackedRange(
|
||||||
|
in RenderFrameView view,
|
||||||
|
int rangeIndex,
|
||||||
|
uint tupleLandblockId,
|
||||||
|
bool publishSelection)
|
||||||
|
{
|
||||||
|
ReadOnlySpan<RenderFrameCandidateRange> ranges =
|
||||||
|
view.RouteRanges;
|
||||||
|
if ((uint)rangeIndex >= (uint)ranges.Length)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(rangeIndex));
|
||||||
|
|
||||||
|
_nextPackedInstanceSubmissionOrder = 0;
|
||||||
|
foreach (InstanceGroup group in _packedGroups.Values)
|
||||||
|
group.ClearPerInstanceData();
|
||||||
|
|
||||||
|
uint anyVao = 0;
|
||||||
|
int meshPartCount = 0;
|
||||||
|
RenderFrameCandidateRange range = ranges[rangeIndex];
|
||||||
|
ReadOnlySpan<RenderProjectionRecord> routeCandidates =
|
||||||
|
view.RouteCandidates;
|
||||||
|
ReadOnlySpan<RenderFrameMeshPart> meshParts = view.MeshParts;
|
||||||
|
int end = checked(range.Offset + range.Count);
|
||||||
|
if ((uint)range.Offset > (uint)routeCandidates.Length
|
||||||
|
|| (uint)end > (uint)routeCandidates.Length)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Packed dispatcher route {rangeIndex} exceeds candidate storage.");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int candidateIndex = range.Offset;
|
||||||
|
candidateIndex < end;
|
||||||
|
candidateIndex++)
|
||||||
|
{
|
||||||
|
RenderProjectionRecord projection =
|
||||||
|
routeCandidates[candidateIndex];
|
||||||
|
if ((projection.Flags & RenderProjectionFlags.Draw) == 0)
|
||||||
|
continue;
|
||||||
|
if (!_packedEntityById.TryGetValue(
|
||||||
|
projection.Id,
|
||||||
|
out RenderFrameEntityCandidate source))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Packed route references absent entity {projection.Id}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
int meshEnd = checked(
|
||||||
|
source.MeshPartOffset + source.MeshPartCount);
|
||||||
|
if ((uint)source.MeshPartOffset > (uint)meshParts.Length
|
||||||
|
|| (uint)meshEnd > (uint)meshParts.Length)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Packed entity {projection.Id} exceeds mesh part storage.");
|
||||||
|
}
|
||||||
|
|
||||||
|
meshPartCount = checked(
|
||||||
|
meshPartCount + source.MeshPartCount);
|
||||||
|
RenderInstanceCandidate candidate =
|
||||||
|
RenderInstanceCandidate.FromFrame(
|
||||||
|
in source,
|
||||||
|
tupleLandblockId);
|
||||||
|
ClassifyPackedEntity(
|
||||||
|
in projection,
|
||||||
|
in candidate,
|
||||||
|
meshParts.Slice(
|
||||||
|
source.MeshPartOffset,
|
||||||
|
source.MeshPartCount),
|
||||||
|
ref anyVao,
|
||||||
|
publishSelection);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PackedRangeClassification(
|
||||||
|
anyVao,
|
||||||
|
range.Count,
|
||||||
|
meshPartCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ValidatePackedProductionView(
|
||||||
|
in RenderFrameView view)
|
||||||
|
{
|
||||||
|
if (!_packedProductionFrameOpen
|
||||||
|
|| view.Generation != _packedProductionGeneration
|
||||||
|
|| view.FrameSequence != _packedProductionFrameSequence)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"The packed dispatcher received a stale or foreign production frame.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ResetPackedProductionFrame()
|
||||||
|
{
|
||||||
|
_packedProductionFrameOpen = false;
|
||||||
|
_packedProductionGeneration = default;
|
||||||
|
_packedProductionFrameSequence = 0;
|
||||||
|
_packedProductionNextRange = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly record struct PackedRangeClassification(
|
||||||
|
uint AnyVao,
|
||||||
|
int CandidateCount,
|
||||||
|
int MeshPartCount);
|
||||||
|
|
||||||
private void ClassifyPackedEntity(
|
private void ClassifyPackedEntity(
|
||||||
in RenderProjectionRecord projection,
|
in RenderProjectionRecord projection,
|
||||||
in RenderInstanceCandidate entity,
|
in RenderInstanceCandidate entity,
|
||||||
ReadOnlySpan<RenderFrameMeshPart> meshParts,
|
ReadOnlySpan<RenderFrameMeshPart> meshParts,
|
||||||
ref uint anyVao)
|
ref uint anyVao,
|
||||||
|
bool publishSelection)
|
||||||
{
|
{
|
||||||
(uint slot, bool culled) = ResolveSlotForFrame(
|
(uint slot, bool culled) = ResolveSlotForFrame(
|
||||||
_clipRoutingActive,
|
_clipRoutingActive,
|
||||||
|
|
@ -231,7 +385,8 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
slot,
|
slot,
|
||||||
lights,
|
lights,
|
||||||
indoor,
|
indoor,
|
||||||
selectionLighting);
|
selectionLighting,
|
||||||
|
publishSelection);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -272,6 +427,8 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
if (renderData is null)
|
if (renderData is null)
|
||||||
{
|
{
|
||||||
reusableAcrossFrames = false;
|
reusableAcrossFrames = false;
|
||||||
|
if (_missRequested.Add(meshRef.GfxObjId))
|
||||||
|
_meshAdapter.EnsureLoaded(meshRef.GfxObjId);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (anyVao == 0)
|
if (anyVao == 0)
|
||||||
|
|
@ -291,6 +448,8 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
if (partData is null)
|
if (partData is null)
|
||||||
{
|
{
|
||||||
reusableAcrossFrames = false;
|
reusableAcrossFrames = false;
|
||||||
|
if (_missRequested.Add(gfxObjId))
|
||||||
|
_meshAdapter.EnsureLoaded(gfxObjId);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -334,7 +493,8 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
in entity,
|
in entity,
|
||||||
selectionPartIndex,
|
selectionPartIndex,
|
||||||
(uint)gfxObjId,
|
(uint)gfxObjId,
|
||||||
model);
|
model,
|
||||||
|
publishSelection);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
@ -374,7 +534,8 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
in entity,
|
in entity,
|
||||||
partIndex,
|
partIndex,
|
||||||
meshRef.GfxObjId,
|
meshRef.GfxObjId,
|
||||||
model);
|
model,
|
||||||
|
publishSelection);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -481,7 +642,8 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
uint slot,
|
uint slot,
|
||||||
InstanceLightSet lights,
|
InstanceLightSet lights,
|
||||||
bool indoor,
|
bool indoor,
|
||||||
Vector2 selectionLighting)
|
Vector2 selectionLighting,
|
||||||
|
bool publishSelection)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < entry.Batches.Count; i++)
|
for (int i = 0; i < entry.Batches.Count; i++)
|
||||||
{
|
{
|
||||||
|
|
@ -504,7 +666,8 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
in entity,
|
in entity,
|
||||||
part.PartIndex,
|
part.PartIndex,
|
||||||
part.GfxObjId,
|
part.GfxObjId,
|
||||||
part.RestPose * entity.RootWorld);
|
part.RestPose * entity.RootWorld,
|
||||||
|
publishSelection);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -593,13 +756,29 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
in RenderInstanceCandidate entity,
|
in RenderInstanceCandidate entity,
|
||||||
int partIndex,
|
int partIndex,
|
||||||
uint gfxObjId,
|
uint gfxObjId,
|
||||||
Matrix4x4 localToWorld)
|
Matrix4x4 localToWorld,
|
||||||
|
bool publishSelection)
|
||||||
{
|
{
|
||||||
if (_selectionSink is not IRetailSelectionRenderOracle oracle
|
if (!_packedSelectionKeys.Add(new PackedSelectionKey(
|
||||||
|| !_packedSelectionKeys.Add(new PackedSelectionKey(
|
|
||||||
entity.LocalEntityId,
|
entity.LocalEntityId,
|
||||||
partIndex,
|
partIndex,
|
||||||
gfxObjId))
|
gfxObjId)))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (publishSelection)
|
||||||
|
{
|
||||||
|
_selectionSink?.AddVisiblePart(
|
||||||
|
entity.ServerGuid,
|
||||||
|
entity.LocalEntityId,
|
||||||
|
partIndex,
|
||||||
|
gfxObjId,
|
||||||
|
localToWorld);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_selectionSink is not IRetailSelectionRenderOracle oracle
|
||||||
|| !oracle.TryCreateVisiblePart(
|
|| !oracle.TryCreateVisiblePart(
|
||||||
entity.ServerGuid,
|
entity.ServerGuid,
|
||||||
entity.LocalEntityId,
|
entity.LocalEntityId,
|
||||||
|
|
|
||||||
|
|
@ -1466,41 +1466,10 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
|
||||||
HashSet<uint>? animatedEntityIds = null,
|
HashSet<uint>? animatedEntityIds = null,
|
||||||
EntitySet set = EntitySet.All)
|
EntitySet set = EntitySet.All)
|
||||||
{
|
{
|
||||||
_shader.Use();
|
bool diag = BeginEntityDispatch(
|
||||||
_selectionLighting?.TickLighting();
|
camera,
|
||||||
_indoorProbeFrameCounter++;
|
out Matrix4x4 vp,
|
||||||
var vp = camera.View * camera.Projection;
|
out Vector3 camPos);
|
||||||
_shader.SetMatrix4("uViewProjection", vp);
|
|
||||||
// A7 Fix D D-3/D-4: object path — plain Lambert points + sun. MUST set
|
|
||||||
// explicitly (shared GL uniform; EnvCellRenderer sets it to 1).
|
|
||||||
_shader.SetInt("uLightingMode", 0);
|
|
||||||
// #176 stripe-hunt isolation (ACDREAM_LIGHT_DEBUG) — throwaway diagnostic.
|
|
||||||
_shader.SetInt("uLightDebug", AcDream.Core.Rendering.RenderingDiagnostics.LightDebugMode);
|
|
||||||
|
|
||||||
// #128 self-heal: fresh re-request dedup per Draw pass.
|
|
||||||
_missRequested.Clear();
|
|
||||||
|
|
||||||
bool diag = string.Equals(Environment.GetEnvironmentVariable("ACDREAM_WB_DIAG"), "1", StringComparison.Ordinal);
|
|
||||||
|
|
||||||
if (diag && !_gpuQueriesInitialized)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < GpuQueryRingDepth; i++)
|
|
||||||
{
|
|
||||||
_gpuQueryOpaque[i] = _gl.GenQuery();
|
|
||||||
_gpuQueryTransparent[i] = _gl.GenQuery();
|
|
||||||
}
|
|
||||||
_gpuQueriesInitialized = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always run the CPU stopwatch — cheap; only logged under diag.
|
|
||||||
_cpuStopwatch.Restart();
|
|
||||||
|
|
||||||
// Camera world-space position for front-to-back sort (perf #2). The view
|
|
||||||
// matrix is the inverse of the camera's world transform, so the world
|
|
||||||
// translation lives in the inverse's translation row.
|
|
||||||
Vector3 camPos = Vector3.Zero;
|
|
||||||
if (Matrix4x4.Invert(camera.View, out var invView))
|
|
||||||
camPos = invView.Translation;
|
|
||||||
|
|
||||||
// ── Phase 1: clear groups, walk entities, build groups ──────────────
|
// ── Phase 1: clear groups, walk entities, build groups ──────────────
|
||||||
// Draw is invoked several times per frame (landscape slices, late
|
// Draw is invoked several times per frame (landscape slices, late
|
||||||
|
|
@ -2041,11 +2010,71 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
|
||||||
if (RenderingDiagnostics.ProbeClipRouteEnabled && _clipRoutingActive)
|
if (RenderingDiagnostics.ProbeClipRouteEnabled && _clipRoutingActive)
|
||||||
EmitClipRouteDispatchProbe(probeCulledEntities);
|
EmitClipRouteDispatchProbe(probeCulledEntities);
|
||||||
|
|
||||||
|
ExecuteClassifiedGroups(
|
||||||
|
vp,
|
||||||
|
camPos,
|
||||||
|
anyVao,
|
||||||
|
_groups.Values,
|
||||||
|
set,
|
||||||
|
walkResult.EntitiesWalked,
|
||||||
|
_walkScratch.Count,
|
||||||
|
diag,
|
||||||
|
observeCurrentPath: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool BeginEntityDispatch(
|
||||||
|
ICamera camera,
|
||||||
|
out Matrix4x4 viewProjection,
|
||||||
|
out Vector3 cameraWorldPosition)
|
||||||
|
{
|
||||||
|
_shader.Use();
|
||||||
|
_selectionLighting?.TickLighting();
|
||||||
|
_indoorProbeFrameCounter++;
|
||||||
|
viewProjection = camera.View * camera.Projection;
|
||||||
|
_shader.SetMatrix4("uViewProjection", viewProjection);
|
||||||
|
_shader.SetInt("uLightingMode", 0);
|
||||||
|
_shader.SetInt(
|
||||||
|
"uLightDebug",
|
||||||
|
RenderingDiagnostics.LightDebugMode);
|
||||||
|
_missRequested.Clear();
|
||||||
|
|
||||||
|
bool diagnosticsEnabled = string.Equals(
|
||||||
|
Environment.GetEnvironmentVariable("ACDREAM_WB_DIAG"),
|
||||||
|
"1",
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
if (diagnosticsEnabled && !_gpuQueriesInitialized)
|
||||||
|
{
|
||||||
|
for (int index = 0; index < GpuQueryRingDepth; index++)
|
||||||
|
{
|
||||||
|
_gpuQueryOpaque[index] = _gl.GenQuery();
|
||||||
|
_gpuQueryTransparent[index] = _gl.GenQuery();
|
||||||
|
}
|
||||||
|
_gpuQueriesInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_cpuStopwatch.Restart();
|
||||||
|
cameraWorldPosition = Vector3.Zero;
|
||||||
|
if (Matrix4x4.Invert(camera.View, out Matrix4x4 inverseView))
|
||||||
|
cameraWorldPosition = inverseView.Translation;
|
||||||
|
return diagnosticsEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ExecuteClassifiedGroups(
|
||||||
|
Matrix4x4 vp,
|
||||||
|
Vector3 camPos,
|
||||||
|
uint anyVao,
|
||||||
|
IEnumerable<InstanceGroup> groups,
|
||||||
|
EntitySet set,
|
||||||
|
int entitiesWalked,
|
||||||
|
int tupleCount,
|
||||||
|
bool diag,
|
||||||
|
bool observeCurrentPath)
|
||||||
|
{
|
||||||
// Nothing visible — skip the GL pass entirely.
|
// Nothing visible — skip the GL pass entirely.
|
||||||
if (anyVao == 0)
|
if (anyVao == 0)
|
||||||
{
|
{
|
||||||
LastDrawStats = new DrawStats(set, walkResult.EntitiesWalked, _walkScratch.Count, 0, 0, 0, 0, 0, 0);
|
LastDrawStats = new DrawStats(set, entitiesWalked, tupleCount, 0, 0, 0, 0, 0, 0);
|
||||||
ObserveCurrentDispatcherSubmission(
|
ObserveClassifiedDispatcherSubmission(observeCurrentPath,
|
||||||
visibleInstanceCount: 0,
|
visibleInstanceCount: 0,
|
||||||
immediateInstanceCount: 0,
|
immediateInstanceCount: 0,
|
||||||
deferTransparent: false,
|
deferTransparent: false,
|
||||||
|
|
@ -2058,7 +2087,7 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
|
||||||
// ── Phase 3: assign FirstInstance per group, lay matrices contiguously, sort opaque ──
|
// ── Phase 3: assign FirstInstance per group, lay matrices contiguously, sort opaque ──
|
||||||
bool deferTransparent = _alphaQueue?.IsCollecting == true;
|
bool deferTransparent = _alphaQueue?.IsCollecting == true;
|
||||||
var instanceCounts = PartitionInstanceGroups(
|
var instanceCounts = PartitionInstanceGroups(
|
||||||
_groups.Values,
|
groups,
|
||||||
deferTransparent,
|
deferTransparent,
|
||||||
camPos,
|
camPos,
|
||||||
_opaqueDraws,
|
_opaqueDraws,
|
||||||
|
|
@ -2067,8 +2096,8 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
|
||||||
int immediateInstances = instanceCounts.ImmediateInstances;
|
int immediateInstances = instanceCounts.ImmediateInstances;
|
||||||
if (totalInstances == 0)
|
if (totalInstances == 0)
|
||||||
{
|
{
|
||||||
LastDrawStats = new DrawStats(set, walkResult.EntitiesWalked, _walkScratch.Count, 0, 0, 0, 0, 0, 0);
|
LastDrawStats = new DrawStats(set, entitiesWalked, tupleCount, 0, 0, 0, 0, 0, 0);
|
||||||
ObserveCurrentDispatcherSubmission(
|
ObserveClassifiedDispatcherSubmission(observeCurrentPath,
|
||||||
visibleInstanceCount: 0,
|
visibleInstanceCount: 0,
|
||||||
immediateInstanceCount: 0,
|
immediateInstanceCount: 0,
|
||||||
deferTransparent,
|
deferTransparent,
|
||||||
|
|
@ -2185,15 +2214,15 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
|
||||||
_transparentByteOffset = layout.TransparentByteOffset;
|
_transparentByteOffset = layout.TransparentByteOffset;
|
||||||
LastDrawStats = new DrawStats(
|
LastDrawStats = new DrawStats(
|
||||||
set,
|
set,
|
||||||
walkResult.EntitiesWalked,
|
entitiesWalked,
|
||||||
_walkScratch.Count,
|
tupleCount,
|
||||||
totalInstances,
|
totalInstances,
|
||||||
totalDraws,
|
totalDraws,
|
||||||
cullRuns,
|
cullRuns,
|
||||||
_opaqueDrawCount,
|
_opaqueDrawCount,
|
||||||
_transparentDrawCount,
|
_transparentDrawCount,
|
||||||
totalTriangles);
|
totalTriangles);
|
||||||
ObserveCurrentDispatcherSubmission(
|
ObserveClassifiedDispatcherSubmission(observeCurrentPath,
|
||||||
totalInstances,
|
totalInstances,
|
||||||
immediateInstances,
|
immediateInstances,
|
||||||
deferTransparent,
|
deferTransparent,
|
||||||
|
|
@ -2404,6 +2433,7 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Phase A8 RR5 (2026-05-26): per-building draw overload. Walks only
|
/// Phase A8 RR5 (2026-05-26): per-building draw overload. Walks only
|
||||||
/// entities whose ParentCellId is in <paramref name="cellIds"/>, plus
|
/// entities whose ParentCellId is in <paramref name="cellIds"/>, plus
|
||||||
|
|
@ -2559,6 +2589,23 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
|
||||||
observer.ObserveDispatcherSubmission(in submission);
|
observer.ObserveDispatcherSubmission(in submission);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ObserveClassifiedDispatcherSubmission(
|
||||||
|
bool observeCurrentPath,
|
||||||
|
int visibleInstanceCount,
|
||||||
|
int immediateInstanceCount,
|
||||||
|
bool deferTransparent,
|
||||||
|
Vector3 cameraWorldPosition)
|
||||||
|
{
|
||||||
|
if (observeCurrentPath)
|
||||||
|
{
|
||||||
|
ObserveCurrentDispatcherSubmission(
|
||||||
|
visibleInstanceCount,
|
||||||
|
immediateInstanceCount,
|
||||||
|
deferTransparent,
|
||||||
|
cameraWorldPosition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal static CurrentRenderDispatcherSubmission
|
internal static CurrentRenderDispatcherSubmission
|
||||||
CreateDispatcherSubmission(
|
CreateDispatcherSubmission(
|
||||||
int visibleInstanceCount,
|
int visibleInstanceCount,
|
||||||
|
|
|
||||||
|
|
@ -575,6 +575,62 @@ public sealed class ArchRenderSceneTests
|
||||||
Assert.True(scene.Memory.EstimatedIndexBytes > 0);
|
Assert.True(scene.Memory.EstimatedIndexBytes > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IndexRevision_AdvancesOnlyWhenOrderedMembershipCanChange()
|
||||||
|
{
|
||||||
|
RenderSceneGeneration generation = Generation(18);
|
||||||
|
using var scene = new ArchRenderScene(generation);
|
||||||
|
RenderProjectionRecord original = Record(
|
||||||
|
18,
|
||||||
|
1,
|
||||||
|
RenderProjectionClass.OutdoorStatic);
|
||||||
|
scene.Apply(
|
||||||
|
[RenderProjectionDelta.Register(generation, 1, original)]);
|
||||||
|
scene.ClearDirty();
|
||||||
|
ulong registeredRevision = scene.OpenQuery().IndexRevision;
|
||||||
|
|
||||||
|
Matrix4x4 movedTransform =
|
||||||
|
Matrix4x4.CreateTranslation(4, 5, 6);
|
||||||
|
RenderProjectionRecord moved = original with
|
||||||
|
{
|
||||||
|
Transform = new RenderTransform(movedTransform),
|
||||||
|
PreviousTransform =
|
||||||
|
new PreviousRenderTransform(
|
||||||
|
original.Transform.LocalToWorld),
|
||||||
|
Bounds = new RenderWorldBounds(
|
||||||
|
new Vector3(3, 4, 5),
|
||||||
|
new Vector3(5, 6, 7)),
|
||||||
|
};
|
||||||
|
scene.Apply(
|
||||||
|
[
|
||||||
|
RenderProjectionDelta.Update(
|
||||||
|
RenderProjectionDeltaKind.UpdateTransform,
|
||||||
|
generation,
|
||||||
|
2,
|
||||||
|
moved),
|
||||||
|
]);
|
||||||
|
|
||||||
|
RenderSceneQuery movedQuery = scene.OpenQuery();
|
||||||
|
Assert.Equal(registeredRevision, movedQuery.IndexRevision);
|
||||||
|
Assert.Equal(1, movedQuery.IndexCounts.Dirty);
|
||||||
|
|
||||||
|
RenderProjectionRecord reordered = moved with
|
||||||
|
{
|
||||||
|
SortKey = new RenderSortKey(moved.SortKey.Value + 1),
|
||||||
|
};
|
||||||
|
scene.Apply(
|
||||||
|
[
|
||||||
|
RenderProjectionDelta.Update(
|
||||||
|
RenderProjectionDeltaKind.UpdateTransform,
|
||||||
|
generation,
|
||||||
|
3,
|
||||||
|
reordered),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Assert.True(
|
||||||
|
scene.OpenQuery().IndexRevision > registeredRevision);
|
||||||
|
}
|
||||||
|
|
||||||
private static RenderProjectionRecord Record(
|
private static RenderProjectionRecord Record(
|
||||||
ulong id,
|
ulong id,
|
||||||
ulong incarnation,
|
ulong incarnation,
|
||||||
|
|
|
||||||
|
|
@ -133,6 +133,142 @@ public sealed class RenderScenePViewFrameProductTests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Builder_ReusesOrderedIndicesWhileRefreshingDirtyRecords()
|
||||||
|
{
|
||||||
|
using var scene = new ArchRenderScene(Generation);
|
||||||
|
RenderProjectionRecord later = Record(
|
||||||
|
0x0100_0000_0000_0012,
|
||||||
|
RenderProjectionClass.OutdoorStatic);
|
||||||
|
RenderProjectionRecord earlier = Record(
|
||||||
|
0x0100_0000_0000_0011,
|
||||||
|
RenderProjectionClass.OutdoorStatic);
|
||||||
|
scene.Apply(
|
||||||
|
[
|
||||||
|
RenderProjectionDelta.Register(Generation, 1, later),
|
||||||
|
RenderProjectionDelta.Register(Generation, 2, earlier),
|
||||||
|
]);
|
||||||
|
|
||||||
|
PortalVisibilityFrame portal = Portal(Cell);
|
||||||
|
ClipFrameAssembly clip = FullScreenClip(Cell);
|
||||||
|
ViewconeCuller viewcone =
|
||||||
|
ViewconeCuller.Build(clip, Matrix4x4.Identity);
|
||||||
|
var exchange = new RenderFrameExchange();
|
||||||
|
var builder = new RenderScenePViewFrameBuilder();
|
||||||
|
|
||||||
|
Build(frameSequence: 1);
|
||||||
|
RenderFrameView first = exchange.BorrowLatest(Generation, 1);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Assert.Equal(
|
||||||
|
[earlier.Id, later.Id],
|
||||||
|
first.OutdoorStaticCandidates.ToArray()
|
||||||
|
.Select(static record => record.Id));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
exchange.Release(in first);
|
||||||
|
}
|
||||||
|
|
||||||
|
scene.ClearDirty();
|
||||||
|
ulong retainedRevision = scene.OpenQuery().IndexRevision;
|
||||||
|
Matrix4x4 movedTransform =
|
||||||
|
Matrix4x4.CreateTranslation(0.25f, 0, 0);
|
||||||
|
RenderProjectionRecord moved = later with
|
||||||
|
{
|
||||||
|
Transform = new RenderTransform(movedTransform),
|
||||||
|
PreviousTransform =
|
||||||
|
new PreviousRenderTransform(
|
||||||
|
later.Transform.LocalToWorld),
|
||||||
|
Bounds = new RenderWorldBounds(
|
||||||
|
new Vector3(-0.75f, -1, -1),
|
||||||
|
new Vector3(1.25f, 1, 1)),
|
||||||
|
};
|
||||||
|
scene.Apply(
|
||||||
|
[
|
||||||
|
RenderProjectionDelta.Update(
|
||||||
|
RenderProjectionDeltaKind.UpdateTransform,
|
||||||
|
Generation,
|
||||||
|
3,
|
||||||
|
moved),
|
||||||
|
]);
|
||||||
|
Assert.Equal(
|
||||||
|
retainedRevision,
|
||||||
|
scene.OpenQuery().IndexRevision);
|
||||||
|
|
||||||
|
Build(frameSequence: 2);
|
||||||
|
RenderFrameView second = exchange.BorrowLatest(Generation, 2);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
RenderProjectionRecord projected =
|
||||||
|
second.EntityCandidates.ToArray()
|
||||||
|
.Single(candidate =>
|
||||||
|
candidate.Projection.Id == later.Id)
|
||||||
|
.Projection;
|
||||||
|
Assert.Equal(
|
||||||
|
movedTransform,
|
||||||
|
projected.Transform.LocalToWorld);
|
||||||
|
Assert.Equal(
|
||||||
|
[earlier.Id, later.Id],
|
||||||
|
second.OutdoorStaticCandidates.ToArray()
|
||||||
|
.Select(static record => record.Id));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
exchange.Release(in second);
|
||||||
|
}
|
||||||
|
|
||||||
|
scene.ClearDirty();
|
||||||
|
RenderProjectionRecord reordered = moved with
|
||||||
|
{
|
||||||
|
SortKey = new RenderSortKey(0),
|
||||||
|
};
|
||||||
|
scene.Apply(
|
||||||
|
[
|
||||||
|
RenderProjectionDelta.Update(
|
||||||
|
RenderProjectionDeltaKind.UpdateTransform,
|
||||||
|
Generation,
|
||||||
|
4,
|
||||||
|
reordered),
|
||||||
|
]);
|
||||||
|
Assert.True(
|
||||||
|
scene.OpenQuery().IndexRevision > retainedRevision);
|
||||||
|
|
||||||
|
Build(frameSequence: 3);
|
||||||
|
RenderFrameView third = exchange.BorrowLatest(Generation, 3);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Assert.Equal(
|
||||||
|
[later.Id, earlier.Id],
|
||||||
|
third.OutdoorStaticCandidates.ToArray()
|
||||||
|
.Select(static record => record.Id));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
exchange.Release(in third);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Build(ulong frameSequence)
|
||||||
|
{
|
||||||
|
RenderSceneQuery query = scene.OpenQuery();
|
||||||
|
var input = new RenderScenePViewBuildInput(
|
||||||
|
query,
|
||||||
|
new RenderSceneDigest(
|
||||||
|
query.Generation,
|
||||||
|
query.Counts,
|
||||||
|
default),
|
||||||
|
portal,
|
||||||
|
clip,
|
||||||
|
viewcone,
|
||||||
|
[],
|
||||||
|
[Cell],
|
||||||
|
EmptyCellSource.Instance,
|
||||||
|
[],
|
||||||
|
RootIsOutdoor: true);
|
||||||
|
builder.Build(exchange, frameSequence, in input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Controller_MatchesCurrentPViewThenNamesAWithdrawnCandidate()
|
public void Controller_MatchesCurrentPViewThenNamesAWithdrawnCandidate()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -158,6 +158,103 @@ public sealed class RetailPViewPassExecutorTests
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DrawInside_production_product_supplies_the_ordered_entity_routes()
|
||||||
|
{
|
||||||
|
LoadedCell interior = InteriorWithExit(0xA9B40100u);
|
||||||
|
WorldEntity cellStatic = Entity(
|
||||||
|
30u,
|
||||||
|
parentCellId: interior.CellId);
|
||||||
|
WorldEntity dynamic = Entity(
|
||||||
|
31u,
|
||||||
|
serverGuid: 0x80000031u,
|
||||||
|
parentCellId: interior.CellId);
|
||||||
|
uint landblockId = interior.CellId & 0xFFFF0000u;
|
||||||
|
var oracle = new CurrentRenderSceneOracle();
|
||||||
|
using var shadow = new RenderSceneShadowRuntime(
|
||||||
|
RenderSceneGeneration.FromRaw(1));
|
||||||
|
RenderProjectionRecord staticRecord =
|
||||||
|
RenderProjectionRecordFactory.ProjectEntity(
|
||||||
|
StaticRenderProjectionJournal.StaticEntityId(
|
||||||
|
landblockId,
|
||||||
|
cellStatic.Id),
|
||||||
|
RenderProjectionClass.IndoorCellStatic,
|
||||||
|
RenderOwnerIncarnation.FromRaw(cellStatic.Id),
|
||||||
|
landblockId,
|
||||||
|
interior.CellId,
|
||||||
|
cellStatic,
|
||||||
|
spatiallyVisible: true);
|
||||||
|
RenderProjectionRecord dynamicRecord =
|
||||||
|
RenderProjectionRecordFactory.ProjectEntity(
|
||||||
|
LiveRenderProjectionJournal.ProjectionId(dynamic.Id),
|
||||||
|
RenderProjectionClass.LiveDynamicRoot,
|
||||||
|
RenderOwnerIncarnation.FromRaw(dynamic.Id),
|
||||||
|
landblockId,
|
||||||
|
interior.CellId,
|
||||||
|
dynamic,
|
||||||
|
spatiallyVisible: true);
|
||||||
|
shadow.Journal.Register(in staticRecord);
|
||||||
|
shadow.Journal.Register(in dynamicRecord);
|
||||||
|
shadow.DrainUpdateBoundary();
|
||||||
|
var product = new RenderScenePViewFrameProductController(
|
||||||
|
shadow,
|
||||||
|
oracle);
|
||||||
|
var renderer = new RetailPViewRenderer(oracle, product);
|
||||||
|
using var executor = new RecordingExecutor();
|
||||||
|
|
||||||
|
renderer.DrawInside(
|
||||||
|
Frame(interior, [cellStatic, dynamic]),
|
||||||
|
executor);
|
||||||
|
|
||||||
|
AssertAppearsInOrder(
|
||||||
|
string.Join('|', executor.Operations),
|
||||||
|
"entity-frame-begin",
|
||||||
|
"entity-route:CellStatic:0:00000000",
|
||||||
|
"entity-route:DynamicLast:0:00000000",
|
||||||
|
"entity-frame-complete");
|
||||||
|
Assert.Equal(0, product.Snapshot.MismatchCount);
|
||||||
|
Assert.Equal(
|
||||||
|
product.Snapshot.ExpectedDigest,
|
||||||
|
product.Snapshot.ActualDigest);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DrawInside_production_product_does_not_require_compare_only_referee()
|
||||||
|
{
|
||||||
|
LoadedCell interior = InteriorWithExit(0xA9B40100u);
|
||||||
|
WorldEntity dynamic = Entity(
|
||||||
|
32u,
|
||||||
|
serverGuid: 0x80000032u,
|
||||||
|
parentCellId: interior.CellId);
|
||||||
|
uint landblockId = interior.CellId & 0xFFFF0000u;
|
||||||
|
using var shadow = new RenderSceneShadowRuntime(
|
||||||
|
RenderSceneGeneration.FromRaw(1));
|
||||||
|
RenderProjectionRecord dynamicRecord =
|
||||||
|
RenderProjectionRecordFactory.ProjectEntity(
|
||||||
|
LiveRenderProjectionJournal.ProjectionId(dynamic.Id),
|
||||||
|
RenderProjectionClass.LiveDynamicRoot,
|
||||||
|
RenderOwnerIncarnation.FromRaw(dynamic.Id),
|
||||||
|
landblockId,
|
||||||
|
interior.CellId,
|
||||||
|
dynamic,
|
||||||
|
spatiallyVisible: true);
|
||||||
|
shadow.Journal.Register(in dynamicRecord);
|
||||||
|
shadow.DrainUpdateBoundary();
|
||||||
|
var product = new RenderScenePViewFrameProductController(shadow);
|
||||||
|
var renderer = new RetailPViewRenderer(null, product);
|
||||||
|
using var executor = new RecordingExecutor();
|
||||||
|
|
||||||
|
renderer.DrawInside(Frame(interior, [dynamic]), executor);
|
||||||
|
|
||||||
|
AssertAppearsInOrder(
|
||||||
|
string.Join('|', executor.Operations),
|
||||||
|
"entity-frame-begin",
|
||||||
|
"entity-route:DynamicLast:0:00000000",
|
||||||
|
"entity-frame-complete");
|
||||||
|
Assert.Equal(0uL, product.Snapshot.ComparisonCount);
|
||||||
|
Assert.Equal(1, product.Snapshot.ActualCandidateCount);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void DrawInside_interior_root_executes_the_nearby_building_look_in_punch()
|
public void DrawInside_interior_root_executes_the_nearby_building_look_in_punch()
|
||||||
{
|
{
|
||||||
|
|
@ -186,11 +283,11 @@ public sealed class RetailPViewPassExecutorTests
|
||||||
drawInside,
|
drawInside,
|
||||||
"passes.BeginFrame();",
|
"passes.BeginFrame();",
|
||||||
"passes.EmitDiagnostics(ctx, result);",
|
"passes.EmitDiagnostics(ctx, result);",
|
||||||
"DrawLandscapeThroughOutsideView(ctx, passes",
|
"DrawLandscapeThroughOutsideView(",
|
||||||
"DrawExitPortalMasks(ctx, passes",
|
"DrawExitPortalMasks(ctx, passes",
|
||||||
"DrawEnvCellShells(passes, pvFrame);",
|
"DrawEnvCellShells(passes, pvFrame);",
|
||||||
"DrawCellObjectLists(ctx, passes",
|
"DrawCellObjectLists(",
|
||||||
"DrawDynamicsLast(ctx, passes");
|
"DrawDynamicsLast(");
|
||||||
|
|
||||||
string landscape = MethodBody(
|
string landscape = MethodBody(
|
||||||
source,
|
source,
|
||||||
|
|
@ -198,9 +295,9 @@ public sealed class RetailPViewPassExecutorTests
|
||||||
AssertAppearsInOrder(
|
AssertAppearsInOrder(
|
||||||
landscape,
|
landscape,
|
||||||
"passes.SetTerrainClip(slice.Planes);",
|
"passes.SetTerrainClip(slice.Planes);",
|
||||||
"passes.DrawLandscapeSlice(ctx",
|
"passes.DrawLandscapeSlice(",
|
||||||
"DrawBuildingLookIns(ctx, passes",
|
"DrawBuildingLookIns(",
|
||||||
"passes.DrawLandscapeSliceLate(ctx",
|
"passes.DrawLandscapeSliceLate(",
|
||||||
"passes.DrawUnattachedSceneParticles(ctx);",
|
"passes.DrawUnattachedSceneParticles(ctx);",
|
||||||
"passes.FlushLandscapeAlpha();",
|
"passes.FlushLandscapeAlpha();",
|
||||||
"passes.ClearInteriorDepth();");
|
"passes.ClearInteriorDepth();");
|
||||||
|
|
@ -506,7 +603,10 @@ public sealed class RetailPViewPassExecutorTests
|
||||||
public float Aspect { get; set; } = 1f;
|
public float Aspect { get; set; } = 1f;
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class RecordingExecutor : IRetailPViewPassExecutor, IDisposable
|
private sealed class RecordingExecutor :
|
||||||
|
IRetailPViewPassExecutor,
|
||||||
|
IRenderFrameEntityPassExecutor,
|
||||||
|
IDisposable
|
||||||
{
|
{
|
||||||
public void AbortFrame() => Operations.Add("abort");
|
public void AbortFrame() => Operations.Add("abort");
|
||||||
|
|
||||||
|
|
@ -559,8 +659,41 @@ public sealed class RetailPViewPassExecutorTests
|
||||||
ViewconeCuller viewcone) => Operations.Add("outstage-routing");
|
ViewconeCuller viewcone) => Operations.Add("outstage-routing");
|
||||||
public void EmitPhantomObjects(uint cellId, int survivorCount) =>
|
public void EmitPhantomObjects(uint cellId, int survivorCount) =>
|
||||||
Operations.Add("phantom-objects");
|
Operations.Add("phantom-objects");
|
||||||
public void DrawLandscapeSlice(RetailPViewFrameInput frame, RetailPViewLandscapeSliceContext context) => Operations.Add("landscape-early");
|
public void DrawLandscapeSlice(
|
||||||
public void DrawLandscapeSliceLate(RetailPViewFrameInput frame, RetailPViewLandscapeLateSliceContext context) => Operations.Add("landscape-late");
|
RetailPViewFrameInput frame,
|
||||||
|
RetailPViewLandscapeSliceContext context)
|
||||||
|
{
|
||||||
|
Operations.Add("landscape-early");
|
||||||
|
if (context.EntityDraw is RenderFrameEntityDrawRequest request)
|
||||||
|
{
|
||||||
|
RenderFrameView view = request.View;
|
||||||
|
DrawEntityRoute(
|
||||||
|
frame.Camera,
|
||||||
|
in view,
|
||||||
|
request.Route,
|
||||||
|
request.RouteIndex,
|
||||||
|
request.CellId,
|
||||||
|
request.TupleLandblockId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DrawLandscapeSliceLate(
|
||||||
|
RetailPViewFrameInput frame,
|
||||||
|
RetailPViewLandscapeLateSliceContext context)
|
||||||
|
{
|
||||||
|
Operations.Add("landscape-late");
|
||||||
|
if (context.EntityDraw is RenderFrameEntityDrawRequest request)
|
||||||
|
{
|
||||||
|
RenderFrameView view = request.View;
|
||||||
|
DrawEntityRoute(
|
||||||
|
frame.Camera,
|
||||||
|
in view,
|
||||||
|
request.Route,
|
||||||
|
request.RouteIndex,
|
||||||
|
request.CellId,
|
||||||
|
request.TupleLandblockId);
|
||||||
|
}
|
||||||
|
}
|
||||||
public void ClearInteriorDepth() => Operations.Add("interior-depth-clear");
|
public void ClearInteriorDepth() => Operations.Add("interior-depth-clear");
|
||||||
public void DrawExitPortalMask(RetailPViewFrameInput frame, RetailPViewCellSliceContext context) => Operations.Add("exit-mask");
|
public void DrawExitPortalMask(RetailPViewFrameInput frame, RetailPViewCellSliceContext context) => Operations.Add("exit-mask");
|
||||||
public void DrawLookInPortalPunch(RetailPViewFrameInput frame, RetailPViewCellSliceContext context) => Operations.Add("look-in-punch");
|
public void DrawLookInPortalPunch(RetailPViewFrameInput frame, RetailPViewCellSliceContext context) => Operations.Add("look-in-punch");
|
||||||
|
|
@ -570,6 +703,28 @@ public sealed class RetailPViewPassExecutorTests
|
||||||
public void DrawDynamicsParticles(RetailPViewFrameInput frame, IReadOnlyList<WorldEntity> survivors) => Operations.Add("dynamics-particles");
|
public void DrawDynamicsParticles(RetailPViewFrameInput frame, IReadOnlyList<WorldEntity> survivors) => Operations.Add("dynamics-particles");
|
||||||
public void EmitDiagnostics(RetailPViewFrameInput frame, RetailPViewFrameResult result) => Operations.Add("diagnostics");
|
public void EmitDiagnostics(RetailPViewFrameInput frame, RetailPViewFrameResult result) => Operations.Add("diagnostics");
|
||||||
|
|
||||||
|
public void BeginEntityFrame(in RenderFrameView view) =>
|
||||||
|
Operations.Add("entity-frame-begin");
|
||||||
|
|
||||||
|
public bool DrawEntityRoute(
|
||||||
|
ICamera camera,
|
||||||
|
in RenderFrameView view,
|
||||||
|
RenderFrameCandidateRoute route,
|
||||||
|
int routeIndex,
|
||||||
|
uint cellId,
|
||||||
|
uint tupleLandblockId)
|
||||||
|
{
|
||||||
|
Operations.Add(
|
||||||
|
$"entity-route:{route}:{routeIndex}:{cellId:X8}");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CompleteEntityFrame(in RenderFrameView view) =>
|
||||||
|
Operations.Add("entity-frame-complete");
|
||||||
|
|
||||||
|
public void AbortEntityFrame() =>
|
||||||
|
Operations.Add("entity-frame-abort");
|
||||||
|
|
||||||
public void Dispose() => _clipFrame.Dispose();
|
public void Dispose() => _clipFrame.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue