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:
Erik 2026-07-24 21:42:42 +02:00
parent f9b68f8f2a
commit dbd8318417
7 changed files with 1698 additions and 2 deletions

View file

@ -23,6 +23,9 @@
ObjectMeshManager.cs (extracted in T2). -->
<PackageReference Include="BCnEncoder.Net.ImageSharp" Version="1.1.2" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
<!-- Slice F: contained render-projection storage. Arch types are confined to
Rendering/Scene/Arch and never cross an acdream-owned interface. -->
<PackageReference Include="Arch" Version="2.1.0" />
<PackageReference Include="Silk.NET.OpenGL" Version="2.23.0" />
<PackageReference Include="Silk.NET.OpenGL.Extensions.ARB" Version="2.23.0" />
<PackageReference Include="Silk.NET.Windowing" Version="2.23.0" />

View 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);
}
}
}

View file

@ -0,0 +1,381 @@
using System.Numerics;
namespace AcDream.App.Rendering.Scene;
internal readonly record struct RenderProjectionId
{
private readonly ulong _value;
private RenderProjectionId(ulong value) => _value = value;
internal static RenderProjectionId FromRaw(ulong value) => new(value);
internal ulong RawValue => _value;
public int CompareTo(RenderProjectionId other) =>
_value.CompareTo(other._value);
public override string ToString() => $"projection:{_value:X16}";
}
internal readonly record struct RenderOwnerIncarnation
{
private readonly ulong _value;
private RenderOwnerIncarnation(ulong value) => _value = value;
internal static RenderOwnerIncarnation FromRaw(ulong value) => new(value);
internal ulong RawValue => _value;
public int CompareTo(RenderOwnerIncarnation other) =>
_value.CompareTo(other._value);
public override string ToString() => $"incarnation:{_value}";
}
internal readonly record struct RenderSceneGeneration
{
private readonly ulong _value;
private RenderSceneGeneration(ulong value) => _value = value;
internal static RenderSceneGeneration FromRaw(ulong value) => new(value);
internal ulong RawValue => _value;
public int CompareTo(RenderSceneGeneration other) =>
_value.CompareTo(other._value);
public override string ToString() => $"generation:{_value}";
}
internal readonly record struct RenderSpatialBucket
{
private readonly ulong _value;
private RenderSpatialBucket(ulong value) => _value = value;
internal static RenderSpatialBucket FromRaw(ulong value) => new(value);
internal ulong RawValue => _value;
public int CompareTo(RenderSpatialBucket other) =>
_value.CompareTo(other._value);
public override string ToString() => $"bucket:{_value:X16}";
}
internal readonly record struct RenderAssetHandle
{
private readonly ulong _value;
private RenderAssetHandle(ulong value) => _value = value;
internal static RenderAssetHandle FromRaw(ulong value) => new(value);
internal ulong RawValue => _value;
public int CompareTo(RenderAssetHandle other) =>
_value.CompareTo(other._value);
public override string ToString() => $"asset:{_value:X16}";
}
internal enum RenderProjectionClass : byte
{
OutdoorStatic,
IndoorCellStatic,
LiveDynamicRoot,
ActiveAnimatedStatic,
EquippedChild,
}
[Flags]
internal enum RenderProjectionFlags : uint
{
None = 0,
Draw = 1 << 0,
Hidden = 1 << 1,
AncestorHidden = 1 << 2,
Translucent = 1 << 3,
Selectable = 1 << 4,
LightCandidate = 1 << 5,
PortalStraddling = 1 << 6,
}
[Flags]
internal enum RenderDirtyMask : ushort
{
None = 0,
Transform = 1 << 0,
Appearance = 1 << 1,
Flags = 1 << 2,
SpatialResidency = 1 << 3,
WorldBounds = 1 << 4,
SortKey = 1 << 5,
All = ushort.MaxValue,
}
internal readonly record struct RenderTransform(Matrix4x4 LocalToWorld);
internal readonly record struct PreviousRenderTransform(Matrix4x4 LocalToWorld);
internal readonly record struct RenderMeshSet(
RenderAssetHandle Handle,
int MeshCount,
ulong Revision);
internal readonly record struct RenderMaterialVariant(
ulong PaletteKey,
ulong TextureReplacementKey,
float Opacity);
internal readonly record struct RenderSpatialResidency(
RenderSpatialBucket Bucket,
uint OwnerLandblockId,
uint FullCellId);
internal readonly record struct RenderWorldBounds(Vector3 Minimum, Vector3 Maximum);
internal readonly record struct RenderDegradeState(byte Level, uint Revision);
internal readonly record struct RenderSortKey(ulong Value);
internal readonly record struct RenderProjectionRecord(
RenderProjectionId Id,
RenderProjectionClass ProjectionClass,
RenderOwnerIncarnation OwnerIncarnation,
RenderTransform Transform,
PreviousRenderTransform PreviousTransform,
RenderMeshSet MeshSet,
RenderMaterialVariant Material,
RenderSpatialResidency Residency,
RenderWorldBounds Bounds,
RenderProjectionFlags Flags,
RenderDegradeState DegradeState,
RenderSortKey SortKey,
RenderDirtyMask DirtyMask);
internal enum RenderProjectionDeltaKind : byte
{
Register,
UpdateTransform,
UpdateAppearance,
UpdateFlags,
Rebucket,
Unregister,
}
internal readonly record struct RenderProjectionDelta(
RenderProjectionDeltaKind Kind,
RenderSceneGeneration Generation,
ulong JournalSequence,
RenderProjectionRecord Record)
{
public static RenderProjectionDelta Register(
RenderSceneGeneration generation,
ulong journalSequence,
in RenderProjectionRecord record) =>
new(
RenderProjectionDeltaKind.Register,
generation,
journalSequence,
record);
public static RenderProjectionDelta Update(
RenderProjectionDeltaKind kind,
RenderSceneGeneration generation,
ulong journalSequence,
in RenderProjectionRecord record)
{
if (kind is RenderProjectionDeltaKind.Register
or RenderProjectionDeltaKind.Unregister)
{
throw new ArgumentOutOfRangeException(
nameof(kind),
kind,
"Use Register or Unregister for structural deltas.");
}
return new RenderProjectionDelta(kind, generation, journalSequence, record);
}
public static RenderProjectionDelta Unregister(
RenderSceneGeneration generation,
ulong journalSequence,
RenderProjectionId id,
RenderOwnerIncarnation ownerIncarnation) =>
new(
RenderProjectionDeltaKind.Unregister,
generation,
journalSequence,
new RenderProjectionRecord() with
{
Id = id,
OwnerIncarnation = ownerIncarnation,
});
}
internal readonly record struct DynamicProjectionUpdate(
RenderProjectionId Id,
RenderOwnerIncarnation OwnerIncarnation,
RenderTransform Transform,
RenderWorldBounds Bounds);
internal readonly ref struct DynamicProjectionSyncInput
{
public DynamicProjectionSyncInput(
RenderSceneGeneration generation,
ReadOnlySpan<DynamicProjectionUpdate> updates)
{
Generation = generation;
Updates = updates;
}
public RenderSceneGeneration Generation { get; }
public ReadOnlySpan<DynamicProjectionUpdate> Updates { get; }
}
internal readonly record struct RenderProjectionCounts(
int Total,
int OutdoorStatic,
int IndoorCellStatic,
int LiveDynamicRoot,
int ActiveAnimatedStatic,
int EquippedChild)
{
public int For(RenderProjectionClass projectionClass) =>
projectionClass switch
{
RenderProjectionClass.OutdoorStatic => OutdoorStatic,
RenderProjectionClass.IndoorCellStatic => IndoorCellStatic,
RenderProjectionClass.LiveDynamicRoot => LiveDynamicRoot,
RenderProjectionClass.ActiveAnimatedStatic => ActiveAnimatedStatic,
RenderProjectionClass.EquippedChild => EquippedChild,
_ => throw new ArgumentOutOfRangeException(
nameof(projectionClass),
projectionClass,
null),
};
}
internal readonly record struct RenderDeltaApplyResult(
int Applied,
int Registered,
int Updated,
int Replaced,
int Unregistered,
int RejectedGeneration,
int RejectedOutOfOrderSequence,
int RejectedStaleIncarnation,
int RejectedMissing)
{
public int Rejected =>
RejectedGeneration
+ RejectedOutOfOrderSequence
+ RejectedStaleIncarnation
+ RejectedMissing;
}
internal readonly record struct RenderSceneMemoryAccounting(
int EntityCount,
int ArchEntityCapacity,
int ArchetypeCount,
int AllocatedChunkCount,
long EstimatedChunkPayloadBytes,
int ProjectionLookupCapacity,
long EstimatedProjectionLookupBytes,
long EstimatedIndexBytes,
long EstimatedJournalBufferBytes,
long EstimatedSynchronizationSourceBytes)
{
public long TotalEstimatedBytes =>
EstimatedChunkPayloadBytes
+ EstimatedProjectionLookupBytes
+ EstimatedIndexBytes
+ EstimatedJournalBufferBytes
+ EstimatedSynchronizationSourceBytes;
}
internal readonly record struct RenderSceneDigest(
RenderSceneGeneration Generation,
RenderProjectionCounts Counts,
RenderSceneHash128 Hash);
internal sealed class RenderSceneDigestBuffer
{
internal List<RenderProjectionRecord> Records { get; } = [];
internal int Capacity => Records.Capacity;
}
internal interface IRenderSceneQuerySource
{
RenderProjectionCounts GetCounts(RenderSceneGeneration generation);
bool TryGet(
RenderSceneGeneration generation,
RenderProjectionId id,
out RenderProjectionRecord record);
int CopyTo(
RenderSceneGeneration generation,
RenderProjectionClass? projectionClass,
Span<RenderProjectionRecord> destination);
}
internal readonly struct RenderSceneQuery
{
private readonly IRenderSceneQuerySource? _source;
internal RenderSceneQuery(
IRenderSceneQuerySource source,
RenderSceneGeneration generation)
{
_source = source;
Generation = generation;
}
public RenderSceneGeneration Generation { get; }
public RenderProjectionCounts Counts =>
Source.GetCounts(Generation);
public bool TryGet(
RenderProjectionId id,
out RenderProjectionRecord record) =>
Source.TryGet(Generation, id, out record);
public int CopyTo(Span<RenderProjectionRecord> destination) =>
Source.CopyTo(Generation, null, destination);
public int CopyTo(
RenderProjectionClass projectionClass,
Span<RenderProjectionRecord> destination) =>
Source.CopyTo(Generation, projectionClass, destination);
private IRenderSceneQuerySource Source =>
_source
?? throw new InvalidOperationException("The render-scene query is uninitialized.");
}
internal interface IRenderScene : IDisposable
{
RenderSceneGeneration Generation { get; }
RenderProjectionCounts Counts { get; }
RenderSceneMemoryAccounting Memory { get; }
RenderDeltaApplyResult Apply(ReadOnlySpan<RenderProjectionDelta> deltas);
void SynchronizeDynamicSources(in DynamicProjectionSyncInput input);
RenderSceneDigest BuildDigest(RenderSceneDigestBuffer reuse);
RenderSceneQuery OpenQuery();
void Clear(RenderSceneGeneration replacementGeneration);
}