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

This reverts commit 2c848d4167.
This commit is contained in:
Erik 2026-07-25 08:36:11 +02:00
parent 823936ec31
commit 20f9fadb12
13 changed files with 1333 additions and 267 deletions

View file

@ -10,7 +10,8 @@ namespace AcDream.App.Rendering.Scene.Arch;
/// projection. Arch identities never leave this namespace; callers only see
/// generation-checked acdream value types.
///
/// Slice F1 deliberately leaves this owner unbound and non-drawing.
/// Slice F introduced this owner in non-drawing shadow mode; Slice G supplies
/// its borrowed frame product to the production renderer.
/// </summary>
internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
{
@ -35,6 +36,7 @@ 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)
@ -225,6 +227,7 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
_counts = default;
_lastAppliedJournalSequence = 0;
Generation = replacementGeneration;
AdvanceIndexRevision();
}
public void Dispose()
@ -274,6 +277,13 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
_dirty.Count);
}
ulong IRenderSceneQuerySource.GetIndexRevision(
RenderSceneGeneration generation)
{
EnsureQueryGeneration(generation);
return _indexRevision;
}
bool IRenderSceneQuerySource.TryGet(
RenderSceneGeneration generation,
RenderProjectionId id,
@ -591,6 +601,12 @@ 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);
}
@ -635,6 +651,7 @@ 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)
@ -650,6 +667,46 @@ 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,6 +455,7 @@ internal interface IRenderSceneQuerySource
{
RenderProjectionCounts GetCounts(RenderSceneGeneration generation);
RenderSceneIndexCounts GetIndexCounts(RenderSceneGeneration generation);
ulong GetIndexRevision(RenderSceneGeneration generation);
bool TryGet(
RenderSceneGeneration generation,
@ -503,6 +504,9 @@ 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,16 +113,15 @@ internal interface IRenderFrameProductSnapshotSource
}
/// <summary>
/// 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.
/// 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.
/// </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();
@ -155,12 +154,12 @@ internal sealed class RenderScenePViewFrameProductController :
public RenderScenePViewFrameProductController(
RenderSceneShadowRuntime shadow,
CurrentRenderSceneOracle current,
CurrentRenderSceneOracle? current = null,
Action<string>? log = null,
WbDrawDispatcher? dispatcher = null)
{
_shadow = shadow ?? throw new ArgumentNullException(nameof(shadow));
_current = current ?? throw new ArgumentNullException(nameof(current));
_current = current;
_log = log;
_dispatcher = dispatcher;
Snapshot = RenderFrameProductComparisonSnapshot.Disabled with
@ -171,6 +170,56 @@ 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,
@ -183,18 +232,7 @@ internal sealed class RenderScenePViewFrameProductController :
bool rootIsOutdoor,
Vector3 cameraWorldPosition = default)
{
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(),
RenderFrameView view = BuildAndBorrow(
portalFrame,
clipAssembly,
viewcone,
@ -203,10 +241,6 @@ internal sealed class RenderScenePViewFrameProductController :
cells,
animatedEntityIds,
rootIsOutdoor);
_builder.Build(_exchange, frameSequence, in input);
RenderFrameView view = _exchange.BorrowLatest(
scene.Generation,
frameSequence);
try
{
Compare(
@ -216,26 +250,92 @@ internal sealed class RenderScenePViewFrameProductController :
}
finally
{
_exchange.Release(in view);
Release(in view);
}
}
private void Compare(
in RenderFrameView view,
uint tupleLandblockId,
Vector3 cameraWorldPosition)
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)
{
_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 i = 0; i < expected.Count; i++)
for (int index = 0; index < expected.Count; index++)
{
CurrentRenderPViewCandidateFingerprint candidate = expected[i];
CurrentRenderPViewCandidateFingerprint candidate =
expected[index];
CurrentRenderProjectionFingerprint projection =
candidate.Projection;
_expected.Add(new CandidateKey(
@ -245,11 +345,15 @@ 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);
@ -285,10 +389,32 @@ 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 =
@ -312,7 +438,7 @@ internal sealed class RenderScenePViewFrameProductController :
Enabled: true,
ProductFrameSequence: view.FrameSequence,
CurrentPViewFrameSequence:
_current.Snapshot.CompletedPViewFrameSequence,
Current.Snapshot.CompletedPViewFrameSequence,
ComparisonCount: _comparisonCount,
SuccessfulComparisonCount: _successfulComparisonCount,
ExpectedCandidateCount: _expected.Count,
@ -395,7 +521,7 @@ internal sealed class RenderScenePViewFrameProductController :
_packedClassification =
dispatcher.PackedClassificationSnapshot;
IReadOnlyList<CurrentRenderDispatcherSubmission> expected =
_current.DispatcherSubmissions;
Current.DispatcherSubmissions;
IReadOnlyList<CurrentRenderDispatcherSubmission> actual =
dispatcher.PackedDispatcherSubmissions;
RenderSceneHash128 expectedDigest =
@ -426,7 +552,7 @@ internal sealed class RenderScenePViewFrameProductController :
}
IReadOnlyList<CurrentRenderSelectionFingerprint>
expectedSelection = _current.SelectionParts;
expectedSelection = Current.SelectionParts;
IReadOnlyList<CurrentRenderSelectionFingerprint>
actualSelection = dispatcher.PackedSelectionParts;
BuildSelectionKeys(
@ -628,7 +754,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++)
@ -750,10 +876,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)
@ -891,6 +1017,11 @@ 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,
@ -993,11 +1124,19 @@ 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,
@ -1023,6 +1162,7 @@ internal sealed class RenderScenePViewFrameBuilder
BuildCellStaticRoute(writer, in input);
BuildDynamicLastRoute(writer, in input);
writer.Publish();
AcknowledgeCachedDirtyRecords();
}
catch
{
@ -1034,21 +1174,97 @@ internal sealed class RenderScenePViewFrameBuilder
private void LoadSceneIndices(RenderSceneQuery scene)
{
RenderSceneIndexCounts counts = scene.IndexCounts;
EnsureCapacity(ref _outdoor, counts.OutdoorStatic);
_outdoorCount = scene.CopyIndexTo(
RenderSceneIndex.OutdoorStatic,
_outdoor);
_outdoorCount = CompactAndSort(
_outdoor,
_outdoorCount);
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 _dynamics, counts.Dynamic);
_dynamicCount = scene.CopyIndexTo(
RenderSceneIndex.Dynamic,
_dynamics);
_dynamicCount = CompactAndSort(
_dynamics,
_dynamicCount);
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);
}
private void BuildOutdoorRoutes(

View file

@ -50,10 +50,12 @@ internal interface IRenderSceneShadowSnapshotSource
}
/// <summary>
/// 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.
/// 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.
/// </summary>
internal sealed class RenderSceneShadowRuntime : IDisposable
{