Revert "perf(rendering): draw retained frame product"

This reverts commit ef1d263337.
This commit is contained in:
Erik 2026-07-25 06:28:31 +02:00
parent 624e1119ca
commit 2c848d4167
13 changed files with 268 additions and 1334 deletions

View file

@ -374,6 +374,12 @@ 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
@ -384,18 +390,18 @@ internal sealed class FrameRootCompositionPhase
acknowledgeDirty: false) acknowledgeDirty: false)
: null; : null;
RenderScenePViewFrameProductController? renderFrameProduct = RenderScenePViewFrameProductController? renderFrameProduct =
live.RenderSceneShadow is not null currentRenderSceneOracle 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;
// After G4 the retained product is the production object source. live.DrawDispatcher.SetCurrentRenderSceneObserver(
// The old dispatcher/selection observer is intentionally detached; currentRenderSceneOracle);
// automation still compares the independently built PView route list. live.SelectionScene.SetCurrentRenderSceneObserver(
live.DrawDispatcher.SetCurrentRenderSceneObserver(null); currentRenderSceneOracle);
live.SelectionScene.SetCurrentRenderSceneObserver(null);
var worldSceneRenderer = new WorldSceneRenderer( var worldSceneRenderer = new WorldSceneRenderer(
renderFrameResources, renderFrameResources,
renderLoginState, renderLoginState,

View file

@ -200,11 +200,15 @@ internal sealed class LivePresentationCompositionPhase
{ {
CompositionAcquisitionScope.CompositionAcquisitionLease< CompositionAcquisitionScope.CompositionAcquisitionLease<
RenderSceneShadowRuntime>? renderSceneShadowLease = null; RenderSceneShadowRuntime>? renderSceneShadowLease = null;
renderSceneShadowLease = scope.Acquire( if (interaction.RetainedUi?.Screenshots is not null
"render scene", && d.Options.AutomationArtifactDirectory is not null)
() => new RenderSceneShadowRuntime( {
RenderSceneGeneration.FromRaw(1)), renderSceneShadowLease = scope.Acquire(
static value => value.Dispose()); "shadow render scene",
() => new RenderSceneShadowRuntime(
RenderSceneGeneration.FromRaw(1)),
static value => value.Dispose());
}
RenderSceneShadowRuntime? renderSceneShadow = RenderSceneShadowRuntime? renderSceneShadow =
renderSceneShadowLease?.Resource; renderSceneShadowLease?.Resource;

View file

@ -1,5 +1,4 @@
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;
@ -80,33 +79,8 @@ 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;
@ -173,30 +147,6 @@ 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;
@ -382,18 +332,7 @@ internal sealed class RetailPViewPassExecutor :
_terrainDiagnostics.Complete(); _terrainDiagnostics.Complete();
DisableClipDistances(); DisableClipDistances();
if (context.EntityDraw is RenderFrameEntityDrawRequest request) if (context.OutdoorEntities.Count > 0)
{
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,
@ -424,18 +363,7 @@ internal sealed class RetailPViewPassExecutor :
_clipFrame.BindTerrainClip(_gl); _clipFrame.BindTerrainClip(_gl);
DisableClipDistances(); DisableClipDistances();
if (context.EntityDraw is RenderFrameEntityDrawRequest request) if (context.Dynamics.Count > 0)
{
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,

View file

@ -76,6 +76,13 @@ 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;
@ -205,16 +212,6 @@ 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
{ {
@ -245,73 +242,32 @@ public sealed class RetailPViewRenderer
} }
} }
if (_sceneFrameProduct is not null) DrawLandscapeThroughOutsideView(ctx, passes, clipAssembly, partition, viewcone);
{
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( DrawCellObjectLists(ctx, passes, pvFrame, clipAssembly, drawableCells, partition, viewcone);
ctx, DrawDynamicsLast(ctx, passes, partition, viewcone, ctx.RootCell.IsOutdoorNode);
passes,
_candidateObserver?.CompletePViewFrame();
_sceneFrameProduct?.BuildAndCompare(
pvFrame, pvFrame,
clipAssembly, clipAssembly,
viewcone,
_lookInFrames,
drawableCells, drawableCells,
partition, ctx.Cells,
viewcone, ctx.AnimatedEntityIds,
frameEntityPasses, ctx.PlayerLandblockId ?? 0,
in frameView);
DrawDynamicsLast(
ctx,
passes,
partition,
viewcone,
ctx.RootCell.IsOutdoorNode, ctx.RootCell.IsOutdoorNode,
frameEntityPasses, ctx.CameraWorldPosition);
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
@ -441,9 +397,7 @@ 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;
@ -521,16 +475,7 @@ public sealed class RetailPViewRenderer
i, i,
cellId, cellId,
_cellStaticScratch); _cellStaticScratch);
DrawEntityRouteOrLegacy( passes.DrawEntityBucket(ctx, _cellStaticScratch, _oneCell);
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.
@ -547,9 +492,7 @@ 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;
@ -591,37 +534,15 @@ 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( passes.DrawLandscapeSlice(ctx, new RetailPViewLandscapeSliceContext(slice, _outdoorStaticScratch));
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( DrawBuildingLookIns(ctx, passes, clipAssembly, partition);
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
@ -670,25 +591,9 @@ 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( passes.DrawLandscapeSliceLate(ctx, new RetailPViewLandscapeLateSliceContext(
ctx, slice, _outdoorStaticScratch, _lateParticleOwnerScratch));
new RetailPViewLandscapeLateSliceContext(
slice,
_outdoorStaticScratch,
_lateParticleOwnerScratch)
{
EntityDraw = entityDraw,
});
} }
// #131: UNATTACHED emitters (AttachedObjectId == 0 — portal swirls, // #131: UNATTACHED emitters (AttachedObjectId == 0 — portal swirls,
@ -797,9 +702,7 @@ 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;
@ -846,16 +749,7 @@ public sealed class RetailPViewRenderer
0, 0,
_dynamicsScratch); _dynamicsScratch);
passes.UseIndoorMembershipOnlyRouting(); passes.UseIndoorMembershipOnlyRouting();
DrawEntityRouteOrLegacy( passes.DrawEntityBucket(ctx, _dynamicsScratch, visibleCellIds: null);
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 —
@ -882,9 +776,7 @@ 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
@ -948,16 +840,7 @@ public sealed class RetailPViewRenderer
0, 0,
_allCellStatics); _allCellStatics);
passes.UseIndoorMembershipOnlyRouting(); passes.UseIndoorMembershipOnlyRouting();
DrawEntityRouteOrLegacy( passes.DrawEntityBucket(ctx, _allCellStatics, _cellObjCells);
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
@ -980,35 +863,6 @@ 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();
@ -1446,10 +1300,7 @@ 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
@ -1457,10 +1308,7 @@ 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,

View file

@ -10,8 +10,7 @@ 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 F introduced this owner in non-drawing shadow mode; Slice G supplies /// Slice F1 deliberately leaves this owner unbound and non-drawing.
/// its borrowed frame product to the production renderer.
/// </summary> /// </summary>
internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
{ {
@ -36,7 +35,6 @@ 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)
@ -227,7 +225,6 @@ 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()
@ -277,13 +274,6 @@ 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,
@ -601,12 +591,6 @@ 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);
} }
@ -651,7 +635,6 @@ 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)
@ -667,46 +650,6 @@ 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) =>

View file

@ -455,7 +455,6 @@ 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,
@ -504,9 +503,6 @@ 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) =>

View file

@ -113,15 +113,16 @@ internal interface IRenderFrameProductSnapshotSource
} }
/// <summary> /// <summary>
/// Owns the same-frame PView product. Diagnostic composition may compare it /// G1 compare-only owner. It builds the candidate frame product directly from
/// with the retired current-path observer; production borrows the exact same /// the incremental scene after the accepted PView traversal, borrows it in the
/// arena and supplies its ordered ranges directly to the dispatcher. /// same render frame, and compares route membership against the current-path
/// 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();
@ -154,12 +155,12 @@ internal sealed class RenderScenePViewFrameProductController :
public RenderScenePViewFrameProductController( public RenderScenePViewFrameProductController(
RenderSceneShadowRuntime shadow, RenderSceneShadowRuntime shadow,
CurrentRenderSceneOracle? current = null, CurrentRenderSceneOracle current,
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; _current = current ?? throw new ArgumentNullException(nameof(current));
_log = log; _log = log;
_dispatcher = dispatcher; _dispatcher = dispatcher;
Snapshot = RenderFrameProductComparisonSnapshot.Disabled with Snapshot = RenderFrameProductComparisonSnapshot.Disabled with
@ -170,56 +171,6 @@ 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,
@ -232,7 +183,18 @@ internal sealed class RenderScenePViewFrameProductController :
bool rootIsOutdoor, bool rootIsOutdoor,
Vector3 cameraWorldPosition = default) Vector3 cameraWorldPosition = default)
{ {
RenderFrameView view = BuildAndBorrow( 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;
var input = new RenderScenePViewBuildInput(
scene,
_shadow.BuildDigest(),
portalFrame, portalFrame,
clipAssembly, clipAssembly,
viewcone, viewcone,
@ -241,6 +203,10 @@ 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(
@ -250,92 +216,26 @@ internal sealed class RenderScenePViewFrameProductController :
} }
finally finally
{ {
Release(in view); _exchange.Release(in view);
} }
} }
public void CompleteProduction(in RenderFrameView view) private void Compare(
{ in RenderFrameView view,
_packedClassification = uint tupleLandblockId,
_dispatcher?.PackedClassificationSnapshot ?? default; Vector3 cameraWorldPosition)
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 index = 0; index < expected.Count; index++) for (int i = 0; i < expected.Count; i++)
{ {
CurrentRenderPViewCandidateFingerprint candidate = CurrentRenderPViewCandidateFingerprint candidate = expected[i];
expected[index];
CurrentRenderProjectionFingerprint projection = CurrentRenderProjectionFingerprint projection =
candidate.Projection; candidate.Projection;
_expected.Add(new CandidateKey( _expected.Add(new CandidateKey(
@ -345,15 +245,11 @@ internal sealed class RenderScenePViewFrameProductController :
ExpectedId(in projection))); ExpectedId(in projection)));
} }
ReadOnlySpan<RenderFrameCandidateRange> ranges = ReadOnlySpan<RenderFrameCandidateRange> ranges = view.RouteRanges;
view.RouteRanges; ReadOnlySpan<RenderProjectionRecord> records = view.RouteCandidates;
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; for (int rangeIndex = 0; rangeIndex < ranges.Length; rangeIndex++)
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);
@ -389,32 +285,10 @@ internal sealed class RenderScenePViewFrameProductController :
{ {
_firstMismatch = mismatch; _firstMismatch = mismatch;
_log?.Invoke( _log?.Invoke(
"[render-frame-product] first mismatch: " "[render-frame-product] first mismatch: " + 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 =
@ -438,7 +312,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,
@ -521,7 +395,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 =
@ -552,7 +426,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(
@ -754,7 +628,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++)
@ -876,10 +750,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)
@ -1017,11 +891,6 @@ 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,
@ -1124,19 +993,11 @@ 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,
@ -1162,7 +1023,6 @@ 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
{ {
@ -1174,97 +1034,21 @@ internal sealed class RenderScenePViewFrameBuilder
private void LoadSceneIndices(RenderSceneQuery scene) private void LoadSceneIndices(RenderSceneQuery scene)
{ {
RenderSceneIndexCounts counts = scene.IndexCounts; RenderSceneIndexCounts counts = scene.IndexCounts;
ulong revision = scene.IndexRevision; EnsureCapacity(ref _outdoor, counts.OutdoorStatic);
if (scene.Generation != _indexGeneration _outdoorCount = scene.CopyIndexTo(
|| revision != _indexRevision) RenderSceneIndex.OutdoorStatic,
{ _outdoor);
EnsureCapacity(ref _outdoor, counts.OutdoorStatic); _outdoorCount = CompactAndSort(
_outdoorCount = scene.CopyIndexTo( _outdoor,
RenderSceneIndex.OutdoorStatic, _outdoorCount);
_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(

View file

@ -50,12 +50,10 @@ internal interface IRenderSceneShadowSnapshotSource
} }
/// <summary> /// <summary>
/// Owner of the derived render projection. Slice F introduced it as a /// Diagnostic-only owner of Slice F's non-drawing render projection. The
/// diagnostic shadow; Slice G keeps the same generation/incarnation journal /// normal runtime never constructs this type. Accepted world/live lifecycle
/// boundary while lending its frame product to production drawing. Accepted /// edges append to one ordered journal, and the update thread drains it only
/// world/live lifecycle edges append to one ordered journal, and the update /// after current-frame animation, network, and attachment reconciliation.
/// thread drains it only after current-frame animation, network, and
/// attachment reconciliation.
/// </summary> /// </summary>
internal sealed class RenderSceneShadowRuntime : IDisposable internal sealed class RenderSceneShadowRuntime : IDisposable
{ {

View file

@ -1,5 +1,4 @@
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;
@ -11,10 +10,10 @@ using DatReaderWriter.Enums;
namespace AcDream.App.Rendering.Wb; namespace AcDream.App.Rendering.Wb;
/// <summary> /// <summary>
/// Scene-frame classification and production submission. The retained group /// G2 compare-only classification of the scene-built frame product. This file
/// storage is also consumed by the G2/G3 compare oracle, while production /// deliberately shares the production dispatcher's exact mesh, texture,
/// routes reuse the dispatcher's exact mesh, texture, light, translucency, /// light, translucency, selection, grouping, and ordering owners while keeping
/// selection, upload, and draw owners. /// its group storage separate. It never uploads or draws.
/// </summary> /// </summary>
public sealed unsafe partial class WbDrawDispatcher public sealed unsafe partial class WbDrawDispatcher
{ {
@ -36,10 +35,6 @@ 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;
@ -56,28 +51,104 @@ public sealed unsafe partial class WbDrawDispatcher
uint tupleLandblockId, uint tupleLandblockId,
Vector3 cameraWorldPosition) Vector3 cameraWorldPosition)
{ {
if (_packedProductionFrameOpen) if (_packedGroupFrame == long.MaxValue)
{ {
throw new InvalidOperationException( throw new InvalidOperationException(
"The packed dispatcher oracle cannot replace an active production frame."); "Packed dispatcher frame identity was exhausted.");
} }
BeginPackedFrameStorage(in view); _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 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++)
{ {
PackedRangeClassification classified = _nextPackedInstanceSubmissionOrder = 0;
ClassifyPackedRange( uint anyVao = 0;
in view, foreach (InstanceGroup group in _packedGroups.Values)
rangeIndex, group.ClearPerInstanceData();
tupleLandblockId,
publishSelection: false); RenderFrameCandidateRange range = ranges[rangeIndex];
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(
classified.AnyVao, anyVao,
_alphaQueue?.IsCollecting == true); _alphaQueue?.IsCollecting == true);
InstanceLayoutCounts counts = PartitionInstanceGroups( InstanceLayoutCounts counts = PartitionInstanceGroups(
_packedGroups.Values, _packedGroups.Values,
@ -105,236 +176,11 @@ 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,
@ -385,8 +231,7 @@ public sealed unsafe partial class WbDrawDispatcher
slot, slot,
lights, lights,
indoor, indoor,
selectionLighting, selectionLighting);
publishSelection);
return; return;
} }
@ -427,8 +272,6 @@ 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)
@ -448,8 +291,6 @@ 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;
} }
@ -493,8 +334,7 @@ public sealed unsafe partial class WbDrawDispatcher
in entity, in entity,
selectionPartIndex, selectionPartIndex,
(uint)gfxObjId, (uint)gfxObjId,
model, model);
publishSelection);
} }
} }
else else
@ -534,8 +374,7 @@ public sealed unsafe partial class WbDrawDispatcher
in entity, in entity,
partIndex, partIndex,
meshRef.GfxObjId, meshRef.GfxObjId,
model, model);
publishSelection);
} }
} }
@ -642,8 +481,7 @@ 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++)
{ {
@ -666,8 +504,7 @@ 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);
} }
} }
@ -756,29 +593,13 @@ 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 (!_packedSelectionKeys.Add(new PackedSelectionKey(
entity.LocalEntityId,
partIndex,
gfxObjId)))
{
return;
}
if (publishSelection)
{
_selectionSink?.AddVisiblePart(
entity.ServerGuid,
entity.LocalEntityId,
partIndex,
gfxObjId,
localToWorld);
return;
}
if (_selectionSink is not IRetailSelectionRenderOracle oracle if (_selectionSink is not IRetailSelectionRenderOracle oracle
|| !_packedSelectionKeys.Add(new PackedSelectionKey(
entity.LocalEntityId,
partIndex,
gfxObjId))
|| !oracle.TryCreateVisiblePart( || !oracle.TryCreateVisiblePart(
entity.ServerGuid, entity.ServerGuid,
entity.LocalEntityId, entity.LocalEntityId,

View file

@ -1483,10 +1483,41 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
HashSet<uint>? animatedEntityIds = null, HashSet<uint>? animatedEntityIds = null,
EntitySet set = EntitySet.All) EntitySet set = EntitySet.All)
{ {
bool diag = BeginEntityDispatch( _shader.Use();
camera, _selectionLighting?.TickLighting();
out Matrix4x4 vp, _indoorProbeFrameCounter++;
out Vector3 camPos); var vp = camera.View * camera.Projection;
_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
@ -2027,71 +2058,11 @@ 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, entitiesWalked, tupleCount, 0, 0, 0, 0, 0, 0); LastDrawStats = new DrawStats(set, walkResult.EntitiesWalked, _walkScratch.Count, 0, 0, 0, 0, 0, 0);
ObserveClassifiedDispatcherSubmission(observeCurrentPath, ObserveCurrentDispatcherSubmission(
visibleInstanceCount: 0, visibleInstanceCount: 0,
immediateInstanceCount: 0, immediateInstanceCount: 0,
deferTransparent: false, deferTransparent: false,
@ -2104,7 +2075,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, _groups.Values,
deferTransparent, deferTransparent,
camPos, camPos,
_opaqueDraws, _opaqueDraws,
@ -2113,8 +2084,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, entitiesWalked, tupleCount, 0, 0, 0, 0, 0, 0); LastDrawStats = new DrawStats(set, walkResult.EntitiesWalked, _walkScratch.Count, 0, 0, 0, 0, 0, 0);
ObserveClassifiedDispatcherSubmission(observeCurrentPath, ObserveCurrentDispatcherSubmission(
visibleInstanceCount: 0, visibleInstanceCount: 0,
immediateInstanceCount: 0, immediateInstanceCount: 0,
deferTransparent, deferTransparent,
@ -2231,15 +2202,15 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
_transparentByteOffset = layout.TransparentByteOffset; _transparentByteOffset = layout.TransparentByteOffset;
LastDrawStats = new DrawStats( LastDrawStats = new DrawStats(
set, set,
entitiesWalked, walkResult.EntitiesWalked,
tupleCount, _walkScratch.Count,
totalInstances, totalInstances,
totalDraws, totalDraws,
cullRuns, cullRuns,
_opaqueDrawCount, _opaqueDrawCount,
_transparentDrawCount, _transparentDrawCount,
totalTriangles); totalTriangles);
ObserveClassifiedDispatcherSubmission(observeCurrentPath, ObserveCurrentDispatcherSubmission(
totalInstances, totalInstances,
immediateInstances, immediateInstances,
deferTransparent, deferTransparent,
@ -2450,7 +2421,6 @@ 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
@ -2606,23 +2576,6 @@ 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,

View file

@ -575,62 +575,6 @@ 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,

View file

@ -133,142 +133,6 @@ 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()
{ {

View file

@ -158,103 +158,6 @@ 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()
{ {
@ -283,11 +186,11 @@ public sealed class RetailPViewPassExecutorTests
drawInside, drawInside,
"passes.BeginFrame();", "passes.BeginFrame();",
"passes.EmitDiagnostics(ctx, result);", "passes.EmitDiagnostics(ctx, result);",
"DrawLandscapeThroughOutsideView(", "DrawLandscapeThroughOutsideView(ctx, passes",
"DrawExitPortalMasks(ctx, passes", "DrawExitPortalMasks(ctx, passes",
"DrawEnvCellShells(passes, pvFrame);", "DrawEnvCellShells(passes, pvFrame);",
"DrawCellObjectLists(", "DrawCellObjectLists(ctx, passes",
"DrawDynamicsLast("); "DrawDynamicsLast(ctx, passes");
string landscape = MethodBody( string landscape = MethodBody(
source, source,
@ -295,9 +198,9 @@ public sealed class RetailPViewPassExecutorTests
AssertAppearsInOrder( AssertAppearsInOrder(
landscape, landscape,
"passes.SetTerrainClip(slice.Planes);", "passes.SetTerrainClip(slice.Planes);",
"passes.DrawLandscapeSlice(", "passes.DrawLandscapeSlice(ctx",
"DrawBuildingLookIns(", "DrawBuildingLookIns(ctx, passes",
"passes.DrawLandscapeSliceLate(", "passes.DrawLandscapeSliceLate(ctx",
"passes.DrawUnattachedSceneParticles(ctx);", "passes.DrawUnattachedSceneParticles(ctx);",
"passes.FlushLandscapeAlpha();", "passes.FlushLandscapeAlpha();",
"passes.ClearInteriorDepth();"); "passes.ClearInteriorDepth();");
@ -601,10 +504,7 @@ public sealed class RetailPViewPassExecutorTests
public float Aspect { get; set; } = 1f; public float Aspect { get; set; } = 1f;
} }
private sealed class RecordingExecutor : private sealed class RecordingExecutor : IRetailPViewPassExecutor, IDisposable
IRetailPViewPassExecutor,
IRenderFrameEntityPassExecutor,
IDisposable
{ {
public void AbortFrame() => Operations.Add("abort"); public void AbortFrame() => Operations.Add("abort");
@ -657,41 +557,8 @@ 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( public void DrawLandscapeSlice(RetailPViewFrameInput frame, RetailPViewLandscapeSliceContext context) => Operations.Add("landscape-early");
RetailPViewFrameInput frame, public void DrawLandscapeSliceLate(RetailPViewFrameInput frame, RetailPViewLandscapeLateSliceContext context) => Operations.Add("landscape-late");
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");
@ -701,28 +568,6 @@ 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();
} }
} }