feat(rendering): contain incremental render scene
Pin Arch behind acdream-owned identities, deltas, queries, generation gates, and memory diagnostics. The new five-archetype store is unbound and non-drawing so static publication can be shadowed without changing production behavior. Co-authored-by: Erik Nilsson <erikn@users.noreply.github.com>
This commit is contained in:
parent
f9b68f8f2a
commit
dbd8318417
7 changed files with 1698 additions and 2 deletions
634
src/AcDream.App/Rendering/Scene/Arch/ArchRenderScene.cs
Normal file
634
src/AcDream.App/Rendering/Scene/Arch/ArchRenderScene.cs
Normal file
|
|
@ -0,0 +1,634 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
using Arch.Core;
|
||||
using ArchWorld = Arch.Core.World;
|
||||
|
||||
namespace AcDream.App.Rendering.Scene.Arch;
|
||||
|
||||
/// <summary>
|
||||
/// Contained Arch-backed implementation of acdream's derived render
|
||||
/// 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.
|
||||
/// </summary>
|
||||
internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
||||
{
|
||||
private const int InitialEntityCapacity = 256;
|
||||
private const int InitialArchetypeCapacity = 8;
|
||||
|
||||
private readonly int _ownerThreadId;
|
||||
private readonly Dictionary<RenderProjectionId, SceneEntry> _entries = [];
|
||||
private ArchWorld _world;
|
||||
private RenderProjectionCounts _counts;
|
||||
private ulong _lastAppliedJournalSequence;
|
||||
private bool _disposed;
|
||||
|
||||
public ArchRenderScene(RenderSceneGeneration initialGeneration)
|
||||
{
|
||||
Generation = initialGeneration;
|
||||
_ownerThreadId = Environment.CurrentManagedThreadId;
|
||||
_world = CreateWorld();
|
||||
}
|
||||
|
||||
public RenderSceneGeneration Generation { get; private set; }
|
||||
|
||||
public RenderProjectionCounts Counts
|
||||
{
|
||||
get
|
||||
{
|
||||
EnsureAvailable();
|
||||
return _counts;
|
||||
}
|
||||
}
|
||||
|
||||
public RenderSceneMemoryAccounting Memory
|
||||
{
|
||||
get
|
||||
{
|
||||
EnsureAvailable();
|
||||
|
||||
int allocatedChunkCount = 0;
|
||||
long estimatedChunkPayloadBytes = 0;
|
||||
foreach (Archetype archetype in _world.Archetypes.AsSpan())
|
||||
{
|
||||
allocatedChunkCount += archetype.ChunkCount;
|
||||
estimatedChunkPayloadBytes +=
|
||||
(long)archetype.ChunkSize * archetype.ChunkCount;
|
||||
}
|
||||
|
||||
int lookupCapacity = _entries.EnsureCapacity(0);
|
||||
long lookupBytes =
|
||||
(long)lookupCapacity * Unsafe.SizeOf<ProjectionLookupSlotEstimate>();
|
||||
|
||||
return new RenderSceneMemoryAccounting(
|
||||
EntityCount: _world.Size,
|
||||
ArchEntityCapacity: _world.Capacity,
|
||||
ArchetypeCount: _world.Archetypes.Count,
|
||||
AllocatedChunkCount: allocatedChunkCount,
|
||||
EstimatedChunkPayloadBytes: estimatedChunkPayloadBytes,
|
||||
ProjectionLookupCapacity: lookupCapacity,
|
||||
EstimatedProjectionLookupBytes: lookupBytes,
|
||||
EstimatedIndexBytes: 0,
|
||||
EstimatedJournalBufferBytes: 0,
|
||||
EstimatedSynchronizationSourceBytes: 0);
|
||||
}
|
||||
}
|
||||
|
||||
public RenderDeltaApplyResult Apply(
|
||||
ReadOnlySpan<RenderProjectionDelta> deltas)
|
||||
{
|
||||
EnsureMutationThread();
|
||||
var result = new ApplyResultBuilder();
|
||||
|
||||
foreach (ref readonly RenderProjectionDelta delta in deltas)
|
||||
{
|
||||
if (delta.Generation != Generation)
|
||||
{
|
||||
result.RejectedGeneration++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (delta.JournalSequence == 0
|
||||
|| delta.JournalSequence <= _lastAppliedJournalSequence)
|
||||
{
|
||||
result.RejectedOutOfOrderSequence++;
|
||||
continue;
|
||||
}
|
||||
|
||||
_lastAppliedJournalSequence = delta.JournalSequence;
|
||||
RenderProjectionRecord record = delta.Record;
|
||||
|
||||
switch (delta.Kind)
|
||||
{
|
||||
case RenderProjectionDeltaKind.Register:
|
||||
ApplyRegister(in record, ref result);
|
||||
break;
|
||||
case RenderProjectionDeltaKind.UpdateTransform:
|
||||
case RenderProjectionDeltaKind.UpdateAppearance:
|
||||
case RenderProjectionDeltaKind.UpdateFlags:
|
||||
case RenderProjectionDeltaKind.Rebucket:
|
||||
ApplyUpdate(delta.Kind, in record, ref result);
|
||||
break;
|
||||
case RenderProjectionDeltaKind.Unregister:
|
||||
ApplyUnregister(in record, ref result);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(delta.Kind),
|
||||
delta.Kind,
|
||||
null);
|
||||
}
|
||||
}
|
||||
|
||||
return result.Build();
|
||||
}
|
||||
|
||||
public void SynchronizeDynamicSources(
|
||||
in DynamicProjectionSyncInput input)
|
||||
{
|
||||
EnsureMutationThread();
|
||||
if (input.Generation != Generation)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Dynamic synchronization belongs to {input.Generation}, "
|
||||
+ $"but the scene is {Generation}.");
|
||||
}
|
||||
|
||||
foreach (ref readonly DynamicProjectionUpdate update in input.Updates)
|
||||
{
|
||||
if (!_entries.TryGetValue(update.Id, out SceneEntry entry)
|
||||
|| entry.OwnerIncarnation != update.OwnerIncarnation)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ref RenderTransform current =
|
||||
ref _world.Get<RenderTransform>(entry.Entity);
|
||||
ref RenderWorldBounds bounds =
|
||||
ref _world.Get<RenderWorldBounds>(entry.Entity);
|
||||
if (current == update.Transform && bounds == update.Bounds)
|
||||
continue;
|
||||
|
||||
_world.Set(
|
||||
entry.Entity,
|
||||
new PreviousRenderTransform(current.LocalToWorld));
|
||||
_world.Set(entry.Entity, update.Transform);
|
||||
_world.Set(entry.Entity, update.Bounds);
|
||||
|
||||
ref RenderDirtyMask dirty =
|
||||
ref _world.Get<RenderDirtyMask>(entry.Entity);
|
||||
dirty |= RenderDirtyMask.Transform | RenderDirtyMask.WorldBounds;
|
||||
}
|
||||
}
|
||||
|
||||
public RenderSceneDigest BuildDigest(RenderSceneDigestBuffer reuse)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(reuse);
|
||||
EnsureAvailable();
|
||||
|
||||
List<RenderProjectionRecord> records = reuse.Records;
|
||||
records.Clear();
|
||||
if (records.Capacity < _entries.Count)
|
||||
records.Capacity = _entries.Count;
|
||||
|
||||
foreach (SceneEntry entry in _entries.Values)
|
||||
records.Add(ReadRecord(in entry));
|
||||
|
||||
records.Sort(RenderProjectionRecordComparer.Instance);
|
||||
|
||||
StableRenderHash128 hash = StableRenderHash128.Create();
|
||||
hash.Add(Generation.RawValue);
|
||||
hash.Add(records.Count);
|
||||
foreach (RenderProjectionRecord record in records)
|
||||
AddRecord(ref hash, in record);
|
||||
|
||||
return new RenderSceneDigest(Generation, _counts, hash.Finish());
|
||||
}
|
||||
|
||||
public RenderSceneQuery OpenQuery()
|
||||
{
|
||||
EnsureAvailable();
|
||||
return new RenderSceneQuery(this, Generation);
|
||||
}
|
||||
|
||||
public void Clear(RenderSceneGeneration replacementGeneration)
|
||||
{
|
||||
EnsureMutationThread();
|
||||
if (replacementGeneration.CompareTo(Generation) <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(replacementGeneration),
|
||||
replacementGeneration,
|
||||
"A replacement render-scene generation must advance.");
|
||||
}
|
||||
|
||||
ArchWorld.Destroy(_world);
|
||||
_world = CreateWorld();
|
||||
_entries.Clear();
|
||||
_counts = default;
|
||||
_lastAppliedJournalSequence = 0;
|
||||
Generation = replacementGeneration;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
EnsureMutationThread();
|
||||
ArchWorld.Destroy(_world);
|
||||
_entries.Clear();
|
||||
_counts = default;
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
RenderProjectionCounts IRenderSceneQuerySource.GetCounts(
|
||||
RenderSceneGeneration generation)
|
||||
{
|
||||
EnsureQueryGeneration(generation);
|
||||
return _counts;
|
||||
}
|
||||
|
||||
bool IRenderSceneQuerySource.TryGet(
|
||||
RenderSceneGeneration generation,
|
||||
RenderProjectionId id,
|
||||
out RenderProjectionRecord record)
|
||||
{
|
||||
EnsureQueryGeneration(generation);
|
||||
if (_entries.TryGetValue(id, out SceneEntry entry))
|
||||
{
|
||||
record = ReadRecord(in entry);
|
||||
return true;
|
||||
}
|
||||
|
||||
record = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
int IRenderSceneQuerySource.CopyTo(
|
||||
RenderSceneGeneration generation,
|
||||
RenderProjectionClass? projectionClass,
|
||||
Span<RenderProjectionRecord> destination)
|
||||
{
|
||||
EnsureQueryGeneration(generation);
|
||||
int required = projectionClass.HasValue
|
||||
? _counts.For(projectionClass.Value)
|
||||
: _counts.Total;
|
||||
if (destination.Length < required)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"Destination holds {destination.Length} records; {required} required.",
|
||||
nameof(destination));
|
||||
}
|
||||
|
||||
int count = 0;
|
||||
foreach (SceneEntry entry in _entries.Values)
|
||||
{
|
||||
if (projectionClass.HasValue
|
||||
&& entry.ProjectionClass != projectionClass.Value)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
destination[count++] = ReadRecord(in entry);
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
private static ArchWorld CreateWorld() =>
|
||||
ArchWorld.Create(
|
||||
archetypeCapacity: InitialArchetypeCapacity,
|
||||
entityCapacity: InitialEntityCapacity);
|
||||
|
||||
private void ApplyRegister(
|
||||
in RenderProjectionRecord record,
|
||||
ref ApplyResultBuilder result)
|
||||
{
|
||||
if (_entries.TryGetValue(record.Id, out SceneEntry existing))
|
||||
{
|
||||
int incarnationOrder =
|
||||
record.OwnerIncarnation.CompareTo(existing.OwnerIncarnation);
|
||||
if (incarnationOrder < 0)
|
||||
{
|
||||
result.RejectedStaleIncarnation++;
|
||||
return;
|
||||
}
|
||||
|
||||
if (incarnationOrder == 0
|
||||
&& record.ProjectionClass == existing.ProjectionClass)
|
||||
{
|
||||
WriteRecord(existing.Entity, in record);
|
||||
result.Applied++;
|
||||
result.Updated++;
|
||||
return;
|
||||
}
|
||||
|
||||
Destroy(in existing);
|
||||
result.Replaced++;
|
||||
}
|
||||
|
||||
Entity entity = CreateEntity(in record);
|
||||
_entries[record.Id] = new SceneEntry(
|
||||
entity,
|
||||
record.OwnerIncarnation,
|
||||
record.ProjectionClass);
|
||||
IncrementCount(record.ProjectionClass);
|
||||
result.Applied++;
|
||||
result.Registered++;
|
||||
}
|
||||
|
||||
private void ApplyUpdate(
|
||||
RenderProjectionDeltaKind kind,
|
||||
in RenderProjectionRecord record,
|
||||
ref ApplyResultBuilder result)
|
||||
{
|
||||
if (!TryGetCurrent(in record, ref result, out SceneEntry entry))
|
||||
return;
|
||||
|
||||
switch (kind)
|
||||
{
|
||||
case RenderProjectionDeltaKind.UpdateTransform:
|
||||
_world.Set(entry.Entity, record.PreviousTransform);
|
||||
_world.Set(entry.Entity, record.Transform);
|
||||
_world.Set(entry.Entity, record.Bounds);
|
||||
_world.Set(entry.Entity, record.SortKey);
|
||||
OrDirty(
|
||||
entry.Entity,
|
||||
RenderDirtyMask.Transform
|
||||
| RenderDirtyMask.WorldBounds
|
||||
| RenderDirtyMask.SortKey);
|
||||
break;
|
||||
case RenderProjectionDeltaKind.UpdateAppearance:
|
||||
_world.Set(entry.Entity, record.MeshSet);
|
||||
_world.Set(entry.Entity, record.Material);
|
||||
_world.Set(entry.Entity, record.DegradeState);
|
||||
OrDirty(entry.Entity, RenderDirtyMask.Appearance);
|
||||
break;
|
||||
case RenderProjectionDeltaKind.UpdateFlags:
|
||||
_world.Set(entry.Entity, record.Flags);
|
||||
OrDirty(entry.Entity, RenderDirtyMask.Flags);
|
||||
break;
|
||||
case RenderProjectionDeltaKind.Rebucket:
|
||||
_world.Set(entry.Entity, record.Residency);
|
||||
OrDirty(entry.Entity, RenderDirtyMask.SpatialResidency);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(kind), kind, null);
|
||||
}
|
||||
|
||||
result.Applied++;
|
||||
result.Updated++;
|
||||
}
|
||||
|
||||
private void ApplyUnregister(
|
||||
in RenderProjectionRecord record,
|
||||
ref ApplyResultBuilder result)
|
||||
{
|
||||
if (!TryGetCurrent(in record, ref result, out SceneEntry entry))
|
||||
return;
|
||||
|
||||
Destroy(in entry);
|
||||
_entries.Remove(record.Id);
|
||||
result.Applied++;
|
||||
result.Unregistered++;
|
||||
}
|
||||
|
||||
private bool TryGetCurrent(
|
||||
in RenderProjectionRecord record,
|
||||
ref ApplyResultBuilder result,
|
||||
out SceneEntry entry)
|
||||
{
|
||||
if (!_entries.TryGetValue(record.Id, out entry))
|
||||
{
|
||||
result.RejectedMissing++;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (record.OwnerIncarnation != entry.OwnerIncarnation)
|
||||
{
|
||||
result.RejectedStaleIncarnation++;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private Entity CreateEntity(in RenderProjectionRecord record) =>
|
||||
record.ProjectionClass switch
|
||||
{
|
||||
RenderProjectionClass.OutdoorStatic =>
|
||||
CreateEntity(in record, new OutdoorStaticTag()),
|
||||
RenderProjectionClass.IndoorCellStatic =>
|
||||
CreateEntity(in record, new IndoorCellStaticTag()),
|
||||
RenderProjectionClass.LiveDynamicRoot =>
|
||||
CreateEntity(in record, new LiveDynamicRootTag()),
|
||||
RenderProjectionClass.ActiveAnimatedStatic =>
|
||||
CreateEntity(in record, new ActiveAnimatedStaticTag()),
|
||||
RenderProjectionClass.EquippedChild =>
|
||||
CreateEntity(in record, new EquippedChildTag()),
|
||||
_ => throw new ArgumentOutOfRangeException(
|
||||
nameof(record.ProjectionClass),
|
||||
record.ProjectionClass,
|
||||
null),
|
||||
};
|
||||
|
||||
private Entity CreateEntity<TTag>(
|
||||
in RenderProjectionRecord record,
|
||||
TTag tag)
|
||||
where TTag : struct =>
|
||||
_world.Create(
|
||||
new ProjectionIdentity(record.Id),
|
||||
record.Transform,
|
||||
record.PreviousTransform,
|
||||
record.MeshSet,
|
||||
record.Material,
|
||||
record.Residency,
|
||||
record.Bounds,
|
||||
record.Flags,
|
||||
record.DegradeState,
|
||||
record.SortKey,
|
||||
record.OwnerIncarnation,
|
||||
record.DirtyMask,
|
||||
tag);
|
||||
|
||||
private void WriteRecord(
|
||||
Entity entity,
|
||||
in RenderProjectionRecord record)
|
||||
{
|
||||
_world.Set(entity, record.Transform);
|
||||
_world.Set(entity, record.PreviousTransform);
|
||||
_world.Set(entity, record.MeshSet);
|
||||
_world.Set(entity, record.Material);
|
||||
_world.Set(entity, record.Residency);
|
||||
_world.Set(entity, record.Bounds);
|
||||
_world.Set(entity, record.Flags);
|
||||
_world.Set(entity, record.DegradeState);
|
||||
_world.Set(entity, record.SortKey);
|
||||
_world.Set(entity, record.OwnerIncarnation);
|
||||
_world.Set(entity, record.DirtyMask);
|
||||
}
|
||||
|
||||
private RenderProjectionRecord ReadRecord(in SceneEntry entry) =>
|
||||
new(
|
||||
_world.Get<ProjectionIdentity>(entry.Entity).Id,
|
||||
entry.ProjectionClass,
|
||||
_world.Get<RenderOwnerIncarnation>(entry.Entity),
|
||||
_world.Get<RenderTransform>(entry.Entity),
|
||||
_world.Get<PreviousRenderTransform>(entry.Entity),
|
||||
_world.Get<RenderMeshSet>(entry.Entity),
|
||||
_world.Get<RenderMaterialVariant>(entry.Entity),
|
||||
_world.Get<RenderSpatialResidency>(entry.Entity),
|
||||
_world.Get<RenderWorldBounds>(entry.Entity),
|
||||
_world.Get<RenderProjectionFlags>(entry.Entity),
|
||||
_world.Get<RenderDegradeState>(entry.Entity),
|
||||
_world.Get<RenderSortKey>(entry.Entity),
|
||||
_world.Get<RenderDirtyMask>(entry.Entity));
|
||||
|
||||
private void Destroy(in SceneEntry entry)
|
||||
{
|
||||
_world.Destroy(entry.Entity);
|
||||
DecrementCount(entry.ProjectionClass);
|
||||
}
|
||||
|
||||
private void OrDirty(Entity entity, RenderDirtyMask value)
|
||||
{
|
||||
ref RenderDirtyMask dirty = ref _world.Get<RenderDirtyMask>(entity);
|
||||
dirty |= value;
|
||||
}
|
||||
|
||||
private void IncrementCount(RenderProjectionClass projectionClass)
|
||||
{
|
||||
_counts = WithClassCount(
|
||||
_counts,
|
||||
projectionClass,
|
||||
_counts.For(projectionClass) + 1,
|
||||
_counts.Total + 1);
|
||||
}
|
||||
|
||||
private void DecrementCount(RenderProjectionClass projectionClass)
|
||||
{
|
||||
_counts = WithClassCount(
|
||||
_counts,
|
||||
projectionClass,
|
||||
_counts.For(projectionClass) - 1,
|
||||
_counts.Total - 1);
|
||||
}
|
||||
|
||||
private static RenderProjectionCounts WithClassCount(
|
||||
RenderProjectionCounts counts,
|
||||
RenderProjectionClass projectionClass,
|
||||
int classCount,
|
||||
int total) =>
|
||||
projectionClass switch
|
||||
{
|
||||
RenderProjectionClass.OutdoorStatic =>
|
||||
counts with { Total = total, OutdoorStatic = classCount },
|
||||
RenderProjectionClass.IndoorCellStatic =>
|
||||
counts with { Total = total, IndoorCellStatic = classCount },
|
||||
RenderProjectionClass.LiveDynamicRoot =>
|
||||
counts with { Total = total, LiveDynamicRoot = classCount },
|
||||
RenderProjectionClass.ActiveAnimatedStatic =>
|
||||
counts with { Total = total, ActiveAnimatedStatic = classCount },
|
||||
RenderProjectionClass.EquippedChild =>
|
||||
counts with { Total = total, EquippedChild = classCount },
|
||||
_ => throw new ArgumentOutOfRangeException(
|
||||
nameof(projectionClass),
|
||||
projectionClass,
|
||||
null),
|
||||
};
|
||||
|
||||
private void EnsureMutationThread()
|
||||
{
|
||||
EnsureAvailable();
|
||||
if (Environment.CurrentManagedThreadId != _ownerThreadId)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Render-scene mutation must remain on its owning update thread.");
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureQueryGeneration(RenderSceneGeneration generation)
|
||||
{
|
||||
EnsureAvailable();
|
||||
if (generation != Generation)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Borrowed render-scene query belongs to {generation}; "
|
||||
+ $"the current scene is {Generation}.");
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureAvailable() =>
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
|
||||
private static void AddRecord(
|
||||
ref StableRenderHash128 hash,
|
||||
in RenderProjectionRecord record)
|
||||
{
|
||||
hash.Add(record.Id.RawValue);
|
||||
hash.Add((byte)record.ProjectionClass);
|
||||
hash.Add(record.OwnerIncarnation.RawValue);
|
||||
hash.Add(record.Transform.LocalToWorld);
|
||||
hash.Add(record.PreviousTransform.LocalToWorld);
|
||||
hash.Add(record.MeshSet.Handle.RawValue);
|
||||
hash.Add(record.MeshSet.MeshCount);
|
||||
hash.Add(record.MeshSet.Revision);
|
||||
hash.Add(record.Material.PaletteKey);
|
||||
hash.Add(record.Material.TextureReplacementKey);
|
||||
hash.Add(record.Material.Opacity);
|
||||
hash.Add(record.Residency.Bucket.RawValue);
|
||||
hash.Add(record.Residency.OwnerLandblockId);
|
||||
hash.Add(record.Residency.FullCellId);
|
||||
hash.Add(record.Bounds.Minimum);
|
||||
hash.Add(record.Bounds.Maximum);
|
||||
hash.Add((uint)record.Flags);
|
||||
hash.Add(record.DegradeState.Level);
|
||||
hash.Add(record.DegradeState.Revision);
|
||||
hash.Add(record.SortKey.Value);
|
||||
}
|
||||
|
||||
private readonly record struct ProjectionIdentity(RenderProjectionId Id);
|
||||
|
||||
private readonly record struct OutdoorStaticTag;
|
||||
|
||||
private readonly record struct IndoorCellStaticTag;
|
||||
|
||||
private readonly record struct LiveDynamicRootTag;
|
||||
|
||||
private readonly record struct ActiveAnimatedStaticTag;
|
||||
|
||||
private readonly record struct EquippedChildTag;
|
||||
|
||||
private readonly record struct SceneEntry(
|
||||
Entity Entity,
|
||||
RenderOwnerIncarnation OwnerIncarnation,
|
||||
RenderProjectionClass ProjectionClass);
|
||||
|
||||
private readonly record struct ProjectionLookupSlotEstimate(
|
||||
int HashCode,
|
||||
int Next,
|
||||
RenderProjectionId Key,
|
||||
SceneEntry Entry);
|
||||
|
||||
private struct ApplyResultBuilder
|
||||
{
|
||||
public int Applied;
|
||||
public int Registered;
|
||||
public int Updated;
|
||||
public int Replaced;
|
||||
public int Unregistered;
|
||||
public int RejectedGeneration;
|
||||
public int RejectedOutOfOrderSequence;
|
||||
public int RejectedStaleIncarnation;
|
||||
public int RejectedMissing;
|
||||
|
||||
public readonly RenderDeltaApplyResult Build() =>
|
||||
new(
|
||||
Applied,
|
||||
Registered,
|
||||
Updated,
|
||||
Replaced,
|
||||
Unregistered,
|
||||
RejectedGeneration,
|
||||
RejectedOutOfOrderSequence,
|
||||
RejectedStaleIncarnation,
|
||||
RejectedMissing);
|
||||
}
|
||||
|
||||
private sealed class RenderProjectionRecordComparer :
|
||||
IComparer<RenderProjectionRecord>
|
||||
{
|
||||
public static RenderProjectionRecordComparer Instance { get; } = new();
|
||||
|
||||
public int Compare(
|
||||
RenderProjectionRecord x,
|
||||
RenderProjectionRecord y)
|
||||
{
|
||||
int value = x.Id.CompareTo(y.Id);
|
||||
return value != 0
|
||||
? value
|
||||
: x.ProjectionClass.CompareTo(y.ProjectionClass);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue