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

View file

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

View file

@ -1,5 +1,4 @@
using System.Numerics;
using AcDream.App.Rendering.Scene;
using AcDream.App.Rendering.Sky;
using AcDream.App.Rendering.Wb;
using AcDream.Core.Rendering;
@ -80,33 +79,8 @@ internal interface IOutdoorSceneParticleOwnerSource
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 :
IRetailPViewPassExecutor,
IRenderFrameEntityPassExecutor,
IOutdoorSceneParticleOwnerSource
{
private readonly GL _gl;
@ -173,30 +147,6 @@ internal sealed class RetailPViewPassExecutor :
_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()
{
List<Exception>? failures = null;
@ -382,18 +332,7 @@ internal sealed class RetailPViewPassExecutor :
_terrainDiagnostics.Complete();
DisableClipDistances();
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)
if (context.OutdoorEntities.Count > 0)
{
var sceneryEntry = (
frame.PlayerLandblockId ?? 0u,
@ -424,18 +363,7 @@ internal sealed class RetailPViewPassExecutor :
_clipFrame.BindTerrainClip(_gl);
DisableClipDistances();
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)
if (context.Dynamics.Count > 0)
{
var dynamicsEntry = (
frame.PlayerLandblockId ?? 0u,

View file

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

View file

@ -10,8 +10,7 @@ namespace AcDream.App.Rendering.Scene.Arch;
/// projection. Arch identities never leave this namespace; callers only see
/// generation-checked acdream value types.
///
/// Slice F introduced this owner in non-drawing shadow mode; Slice G supplies
/// its borrowed frame product to the production renderer.
/// Slice F1 deliberately leaves this owner unbound and non-drawing.
/// </summary>
internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
{
@ -36,7 +35,6 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
private ArchWorld _world;
private RenderProjectionCounts _counts;
private ulong _lastAppliedJournalSequence;
private ulong _indexRevision = 1;
private bool _disposed;
public ArchRenderScene(RenderSceneGeneration initialGeneration)
@ -227,7 +225,6 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
_counts = default;
_lastAppliedJournalSequence = 0;
Generation = replacementGeneration;
AdvanceIndexRevision();
}
public void Dispose()
@ -277,13 +274,6 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
_dirty.Count);
}
ulong IRenderSceneQuerySource.GetIndexRevision(
RenderSceneGeneration generation)
{
EnsureQueryGeneration(generation);
return _indexRevision;
}
bool IRenderSceneQuerySource.TryGet(
RenderSceneGeneration generation,
RenderProjectionId id,
@ -601,12 +591,6 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
in RenderProjectionRecord prior,
in RenderProjectionRecord current)
{
if (IndexMembershipEquals(in prior, in current))
{
SynchronizeDirtyIndex(in current);
return;
}
RemoveFromIndices(in prior);
AddToIndices(in current);
}
@ -651,7 +635,6 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
_lightCandidates.Add(record.Id);
if (record.DirtyMask != RenderDirtyMask.None)
_dirty.Add(record.Id);
AdvanceIndexRevision();
}
private void RemoveFromIndices(in RenderProjectionRecord record)
@ -667,46 +650,6 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
_dirty.Remove(record.Id);
RemoveCell(_cellStatics, 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) =>

View file

@ -455,7 +455,6 @@ internal interface IRenderSceneQuerySource
{
RenderProjectionCounts GetCounts(RenderSceneGeneration generation);
RenderSceneIndexCounts GetIndexCounts(RenderSceneGeneration generation);
ulong GetIndexRevision(RenderSceneGeneration generation);
bool TryGet(
RenderSceneGeneration generation,
@ -504,9 +503,6 @@ internal readonly struct RenderSceneQuery
public RenderSceneIndexCounts IndexCounts =>
Source.GetIndexCounts(Generation);
public ulong IndexRevision =>
Source.GetIndexRevision(Generation);
public bool TryGet(
RenderProjectionId id,
out RenderProjectionRecord record) =>

View file

@ -113,15 +113,16 @@ internal interface IRenderFrameProductSnapshotSource
}
/// <summary>
/// Owns the same-frame PView product. Diagnostic composition may compare it
/// with the retired current-path observer; production borrows the exact same
/// arena and supplies its ordered ranges directly to the dispatcher.
/// G1 compare-only owner. It builds the candidate frame product directly from
/// the incremental scene after the accepted PView traversal, borrows it in the
/// same render frame, and compares route membership against the current-path
/// observer. It never supplies a production draw input.
/// </summary>
internal sealed class RenderScenePViewFrameProductController :
IRenderFrameProductSnapshotSource
{
private readonly RenderSceneShadowRuntime _shadow;
private readonly CurrentRenderSceneOracle? _current;
private readonly CurrentRenderSceneOracle _current;
private readonly WbDrawDispatcher? _dispatcher;
private readonly RenderScenePViewFrameBuilder _builder = new();
private readonly RenderFrameExchange _exchange = new();
@ -154,12 +155,12 @@ internal sealed class RenderScenePViewFrameProductController :
public RenderScenePViewFrameProductController(
RenderSceneShadowRuntime shadow,
CurrentRenderSceneOracle? current = null,
CurrentRenderSceneOracle current,
Action<string>? log = null,
WbDrawDispatcher? dispatcher = null)
{
_shadow = shadow ?? throw new ArgumentNullException(nameof(shadow));
_current = current;
_current = current ?? throw new ArgumentNullException(nameof(current));
_log = log;
_dispatcher = dispatcher;
Snapshot = RenderFrameProductComparisonSnapshot.Disabled with
@ -170,56 +171,6 @@ internal sealed class RenderScenePViewFrameProductController :
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(
PortalVisibilityFrame portalFrame,
ClipFrameAssembly clipAssembly,
@ -232,7 +183,18 @@ internal sealed class RenderScenePViewFrameProductController :
bool rootIsOutdoor,
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,
clipAssembly,
viewcone,
@ -241,6 +203,10 @@ internal sealed class RenderScenePViewFrameProductController :
cells,
animatedEntityIds,
rootIsOutdoor);
_builder.Build(_exchange, frameSequence, in input);
RenderFrameView view = _exchange.BorrowLatest(
scene.Generation,
frameSequence);
try
{
Compare(
@ -250,92 +216,26 @@ internal sealed class RenderScenePViewFrameProductController :
}
finally
{
Release(in view);
_exchange.Release(in view);
}
}
public void CompleteProduction(in RenderFrameView view)
{
_packedClassification =
_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)
private void Compare(
in RenderFrameView view,
uint tupleLandblockId,
Vector3 cameraWorldPosition)
{
_comparisonCount = checked(_comparisonCount + 1);
_expected.Clear();
_actual.Clear();
IReadOnlyList<CurrentRenderPViewCandidateFingerprint> expected =
Current.PViewCandidates;
_current.PViewCandidates;
if (_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 =
expected[index];
CurrentRenderPViewCandidateFingerprint candidate = expected[i];
CurrentRenderProjectionFingerprint projection =
candidate.Projection;
_expected.Add(new CandidateKey(
@ -345,15 +245,11 @@ internal sealed class RenderScenePViewFrameProductController :
ExpectedId(in projection)));
}
ReadOnlySpan<RenderFrameCandidateRange> ranges =
view.RouteRanges;
ReadOnlySpan<RenderProjectionRecord> records =
view.RouteCandidates;
ReadOnlySpan<RenderFrameCandidateRange> ranges = view.RouteRanges;
ReadOnlySpan<RenderProjectionRecord> records = view.RouteCandidates;
if (_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];
int end = checked(range.Offset + range.Count);
@ -389,32 +285,10 @@ internal sealed class RenderScenePViewFrameProductController :
{
_firstMismatch = mismatch;
_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 =
ComparePackedInput(in view, tupleLandblockId);
ClassifiedOutputComparison classified =
@ -438,7 +312,7 @@ internal sealed class RenderScenePViewFrameProductController :
Enabled: true,
ProductFrameSequence: view.FrameSequence,
CurrentPViewFrameSequence:
Current.Snapshot.CompletedPViewFrameSequence,
_current.Snapshot.CompletedPViewFrameSequence,
ComparisonCount: _comparisonCount,
SuccessfulComparisonCount: _successfulComparisonCount,
ExpectedCandidateCount: _expected.Count,
@ -521,7 +395,7 @@ internal sealed class RenderScenePViewFrameProductController :
_packedClassification =
dispatcher.PackedClassificationSnapshot;
IReadOnlyList<CurrentRenderDispatcherSubmission> expected =
Current.DispatcherSubmissions;
_current.DispatcherSubmissions;
IReadOnlyList<CurrentRenderDispatcherSubmission> actual =
dispatcher.PackedDispatcherSubmissions;
RenderSceneHash128 expectedDigest =
@ -552,7 +426,7 @@ internal sealed class RenderScenePViewFrameProductController :
}
IReadOnlyList<CurrentRenderSelectionFingerprint>
expectedSelection = Current.SelectionParts;
expectedSelection = _current.SelectionParts;
IReadOnlyList<CurrentRenderSelectionFingerprint>
actualSelection = dispatcher.PackedSelectionParts;
BuildSelectionKeys(
@ -754,7 +628,7 @@ internal sealed class RenderScenePViewFrameProductController :
_packedActual.Clear();
IReadOnlyList<CurrentRenderDispatcherFingerprint> expected =
Current.DispatcherCandidates;
_current.DispatcherCandidates;
if (_packedExpected.Capacity < expected.Count)
_packedExpected.Capacity = expected.Count;
for (int index = 0; index < expected.Count; index++)
@ -876,10 +750,10 @@ internal sealed class RenderScenePViewFrameProductController :
private string? ComparePackedInputKeys(int actualDrawCount)
{
if (Current.Snapshot.DispatcherDrawCount != actualDrawCount)
if (_current.Snapshot.DispatcherDrawCount != actualDrawCount)
{
return $"field=drawCount expected="
+ $"{Current.Snapshot.DispatcherDrawCount} "
+ $"{_current.Snapshot.DispatcherDrawCount} "
+ $"actual={actualDrawCount}";
}
if (_packedExpected.Count != _packedActual.Count)
@ -1017,11 +891,6 @@ internal sealed class RenderScenePViewFrameProductController :
RenderSceneHash128 ExpectedDigest,
RenderSceneHash128 ActualDigest);
private readonly record struct CandidateComparison(
bool Successful,
RenderSceneHash128 ExpectedDigest,
RenderSceneHash128 ActualDigest);
private readonly record struct ClassifiedOutputComparison(
bool Successful,
int ExpectedDrawCount,
@ -1124,19 +993,11 @@ internal sealed class RenderScenePViewFrameBuilder
private RenderProjectionRecord[] _outdoor = [];
private RenderProjectionRecord[] _dynamics = [];
private RenderProjectionRecord[] _cell = [];
private RenderProjectionRecord[] _dirty = [];
private RenderProjectionRecord[] _survivors = [];
private RenderProjectionRecord[] _cellRoute = [];
private readonly Dictionary<RenderProjectionId, int>
_outdoorPositions = [];
private readonly Dictionary<RenderProjectionId, int>
_dynamicPositions = [];
private int _outdoorCount;
private int _dynamicCount;
private int _cellRouteCount;
private int _dirtyCount;
private RenderSceneGeneration _indexGeneration;
private ulong _indexRevision;
public void Build(
RenderFrameExchange exchange,
@ -1162,7 +1023,6 @@ internal sealed class RenderScenePViewFrameBuilder
BuildCellStaticRoute(writer, in input);
BuildDynamicLastRoute(writer, in input);
writer.Publish();
AcknowledgeCachedDirtyRecords();
}
catch
{
@ -1174,97 +1034,21 @@ internal sealed class RenderScenePViewFrameBuilder
private void LoadSceneIndices(RenderSceneQuery scene)
{
RenderSceneIndexCounts counts = scene.IndexCounts;
ulong revision = scene.IndexRevision;
if (scene.Generation != _indexGeneration
|| revision != _indexRevision)
{
EnsureCapacity(ref _outdoor, counts.OutdoorStatic);
_outdoorCount = scene.CopyIndexTo(
RenderSceneIndex.OutdoorStatic,
_outdoor);
_outdoorCount = CompactAndSort(
_outdoor,
_outdoorCount);
BuildPositionIndex(
_outdoor,
_outdoorCount,
_outdoorPositions);
EnsureCapacity(ref _outdoor, counts.OutdoorStatic);
_outdoorCount = scene.CopyIndexTo(
RenderSceneIndex.OutdoorStatic,
_outdoor);
_outdoorCount = CompactAndSort(
_outdoor,
_outdoorCount);
EnsureCapacity(ref _dynamics, counts.Dynamic);
_dynamicCount = scene.CopyIndexTo(
RenderSceneIndex.Dynamic,
_dynamics);
_dynamicCount = CompactAndSort(
_dynamics,
_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);
EnsureCapacity(ref _dynamics, counts.Dynamic);
_dynamicCount = scene.CopyIndexTo(
RenderSceneIndex.Dynamic,
_dynamics);
_dynamicCount = CompactAndSort(
_dynamics,
_dynamicCount);
}
private void BuildOutdoorRoutes(

View file

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

View file

@ -1,5 +1,4 @@
using System.Numerics;
using AcDream.Core.Rendering;
using AcDream.App.Rendering.Scene;
using AcDream.App.Rendering.Selection;
using AcDream.Core.Lighting;
@ -11,10 +10,10 @@ using DatReaderWriter.Enums;
namespace AcDream.App.Rendering.Wb;
/// <summary>
/// Scene-frame classification and production submission. The retained group
/// storage is also consumed by the G2/G3 compare oracle, while production
/// routes reuse the dispatcher's exact mesh, texture, light, translucency,
/// selection, upload, and draw owners.
/// G2 compare-only classification of the scene-built frame product. This file
/// deliberately shares the production dispatcher's exact mesh, texture,
/// light, translucency, selection, grouping, and ordering owners while keeping
/// its group storage separate. It never uploads or draws.
/// </summary>
public sealed unsafe partial class WbDrawDispatcher
{
@ -36,10 +35,6 @@ public sealed unsafe partial class WbDrawDispatcher
private long _packedGroupFrame;
private long _nextPackedGroupRegistration = 1;
private int _nextPackedInstanceSubmissionOrder;
private bool _packedProductionFrameOpen;
private RenderSceneGeneration _packedProductionGeneration;
private ulong _packedProductionFrameSequence;
private int _packedProductionNextRange;
internal IReadOnlyList<CurrentRenderDispatcherSubmission>
PackedDispatcherSubmissions => _packedSubmissions;
@ -56,28 +51,104 @@ public sealed unsafe partial class WbDrawDispatcher
uint tupleLandblockId,
Vector3 cameraWorldPosition)
{
if (_packedProductionFrameOpen)
if (_packedGroupFrame == long.MaxValue)
{
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 =
view.RouteRanges;
for (int rangeIndex = 0;
rangeIndex < ranges.Length;
rangeIndex++)
{
PackedRangeClassification classified =
ClassifyPackedRange(
in view,
rangeIndex,
tupleLandblockId,
publishSelection: false);
_nextPackedInstanceSubmissionOrder = 0;
uint anyVao = 0;
foreach (InstanceGroup group in _packedGroups.Values)
group.ClearPerInstanceData();
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(
classified.AnyVao,
anyVao,
_alphaQueue?.IsCollecting == true);
InstanceLayoutCounts counts = PartitionInstanceGroups(
_packedGroups.Values,
@ -105,236 +176,11 @@ public sealed unsafe partial class WbDrawDispatcher
_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(
in RenderProjectionRecord projection,
in RenderInstanceCandidate entity,
ReadOnlySpan<RenderFrameMeshPart> meshParts,
ref uint anyVao,
bool publishSelection)
ref uint anyVao)
{
(uint slot, bool culled) = ResolveSlotForFrame(
_clipRoutingActive,
@ -385,8 +231,7 @@ public sealed unsafe partial class WbDrawDispatcher
slot,
lights,
indoor,
selectionLighting,
publishSelection);
selectionLighting);
return;
}
@ -427,8 +272,6 @@ public sealed unsafe partial class WbDrawDispatcher
if (renderData is null)
{
reusableAcrossFrames = false;
if (_missRequested.Add(meshRef.GfxObjId))
_meshAdapter.EnsureLoaded(meshRef.GfxObjId);
continue;
}
if (anyVao == 0)
@ -448,8 +291,6 @@ public sealed unsafe partial class WbDrawDispatcher
if (partData is null)
{
reusableAcrossFrames = false;
if (_missRequested.Add(gfxObjId))
_meshAdapter.EnsureLoaded(gfxObjId);
continue;
}
@ -493,8 +334,7 @@ public sealed unsafe partial class WbDrawDispatcher
in entity,
selectionPartIndex,
(uint)gfxObjId,
model,
publishSelection);
model);
}
}
else
@ -534,8 +374,7 @@ public sealed unsafe partial class WbDrawDispatcher
in entity,
partIndex,
meshRef.GfxObjId,
model,
publishSelection);
model);
}
}
@ -642,8 +481,7 @@ public sealed unsafe partial class WbDrawDispatcher
uint slot,
InstanceLightSet lights,
bool indoor,
Vector2 selectionLighting,
bool publishSelection)
Vector2 selectionLighting)
{
for (int i = 0; i < entry.Batches.Count; i++)
{
@ -666,8 +504,7 @@ public sealed unsafe partial class WbDrawDispatcher
in entity,
part.PartIndex,
part.GfxObjId,
part.RestPose * entity.RootWorld,
publishSelection);
part.RestPose * entity.RootWorld);
}
}
@ -756,29 +593,13 @@ public sealed unsafe partial class WbDrawDispatcher
in RenderInstanceCandidate entity,
int partIndex,
uint gfxObjId,
Matrix4x4 localToWorld,
bool publishSelection)
Matrix4x4 localToWorld)
{
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
|| !_packedSelectionKeys.Add(new PackedSelectionKey(
entity.LocalEntityId,
partIndex,
gfxObjId))
|| !oracle.TryCreateVisiblePart(
entity.ServerGuid,
entity.LocalEntityId,

View file

@ -1483,10 +1483,41 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
HashSet<uint>? animatedEntityIds = null,
EntitySet set = EntitySet.All)
{
bool diag = BeginEntityDispatch(
camera,
out Matrix4x4 vp,
out Vector3 camPos);
_shader.Use();
_selectionLighting?.TickLighting();
_indoorProbeFrameCounter++;
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 ──────────────
// 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)
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.
if (anyVao == 0)
{
LastDrawStats = new DrawStats(set, entitiesWalked, tupleCount, 0, 0, 0, 0, 0, 0);
ObserveClassifiedDispatcherSubmission(observeCurrentPath,
LastDrawStats = new DrawStats(set, walkResult.EntitiesWalked, _walkScratch.Count, 0, 0, 0, 0, 0, 0);
ObserveCurrentDispatcherSubmission(
visibleInstanceCount: 0,
immediateInstanceCount: 0,
deferTransparent: false,
@ -2104,7 +2075,7 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
// ── Phase 3: assign FirstInstance per group, lay matrices contiguously, sort opaque ──
bool deferTransparent = _alphaQueue?.IsCollecting == true;
var instanceCounts = PartitionInstanceGroups(
groups,
_groups.Values,
deferTransparent,
camPos,
_opaqueDraws,
@ -2113,8 +2084,8 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
int immediateInstances = instanceCounts.ImmediateInstances;
if (totalInstances == 0)
{
LastDrawStats = new DrawStats(set, entitiesWalked, tupleCount, 0, 0, 0, 0, 0, 0);
ObserveClassifiedDispatcherSubmission(observeCurrentPath,
LastDrawStats = new DrawStats(set, walkResult.EntitiesWalked, _walkScratch.Count, 0, 0, 0, 0, 0, 0);
ObserveCurrentDispatcherSubmission(
visibleInstanceCount: 0,
immediateInstanceCount: 0,
deferTransparent,
@ -2231,15 +2202,15 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
_transparentByteOffset = layout.TransparentByteOffset;
LastDrawStats = new DrawStats(
set,
entitiesWalked,
tupleCount,
walkResult.EntitiesWalked,
_walkScratch.Count,
totalInstances,
totalDraws,
cullRuns,
_opaqueDrawCount,
_transparentDrawCount,
totalTriangles);
ObserveClassifiedDispatcherSubmission(observeCurrentPath,
ObserveCurrentDispatcherSubmission(
totalInstances,
immediateInstances,
deferTransparent,
@ -2450,7 +2421,6 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
}
}
/// <summary>
/// Phase A8 RR5 (2026-05-26): per-building draw overload. Walks only
/// entities whose ParentCellId is in <paramref name="cellIds"/>, plus
@ -2606,23 +2576,6 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
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
CreateDispatcherSubmission(
int visibleInstanceCount,

View file

@ -575,62 +575,6 @@ public sealed class ArchRenderSceneTests
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(
ulong id,
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]
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]
public void DrawInside_interior_root_executes_the_nearby_building_look_in_punch()
{
@ -283,11 +186,11 @@ public sealed class RetailPViewPassExecutorTests
drawInside,
"passes.BeginFrame();",
"passes.EmitDiagnostics(ctx, result);",
"DrawLandscapeThroughOutsideView(",
"DrawLandscapeThroughOutsideView(ctx, passes",
"DrawExitPortalMasks(ctx, passes",
"DrawEnvCellShells(passes, pvFrame);",
"DrawCellObjectLists(",
"DrawDynamicsLast(");
"DrawCellObjectLists(ctx, passes",
"DrawDynamicsLast(ctx, passes");
string landscape = MethodBody(
source,
@ -295,9 +198,9 @@ public sealed class RetailPViewPassExecutorTests
AssertAppearsInOrder(
landscape,
"passes.SetTerrainClip(slice.Planes);",
"passes.DrawLandscapeSlice(",
"DrawBuildingLookIns(",
"passes.DrawLandscapeSliceLate(",
"passes.DrawLandscapeSlice(ctx",
"DrawBuildingLookIns(ctx, passes",
"passes.DrawLandscapeSliceLate(ctx",
"passes.DrawUnattachedSceneParticles(ctx);",
"passes.FlushLandscapeAlpha();",
"passes.ClearInteriorDepth();");
@ -601,10 +504,7 @@ public sealed class RetailPViewPassExecutorTests
public float Aspect { get; set; } = 1f;
}
private sealed class RecordingExecutor :
IRetailPViewPassExecutor,
IRenderFrameEntityPassExecutor,
IDisposable
private sealed class RecordingExecutor : IRetailPViewPassExecutor, IDisposable
{
public void AbortFrame() => Operations.Add("abort");
@ -657,41 +557,8 @@ public sealed class RetailPViewPassExecutorTests
ViewconeCuller viewcone) => Operations.Add("outstage-routing");
public void EmitPhantomObjects(uint cellId, int survivorCount) =>
Operations.Add("phantom-objects");
public void DrawLandscapeSlice(
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 DrawLandscapeSlice(RetailPViewFrameInput frame, RetailPViewLandscapeSliceContext context) => Operations.Add("landscape-early");
public void DrawLandscapeSliceLate(RetailPViewFrameInput frame, RetailPViewLandscapeLateSliceContext context) => Operations.Add("landscape-late");
public void ClearInteriorDepth() => Operations.Add("interior-depth-clear");
public void DrawExitPortalMask(RetailPViewFrameInput frame, RetailPViewCellSliceContext context) => Operations.Add("exit-mask");
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 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();
}
}