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
|
|
@ -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" />
|
||||
|
|
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
381
src/AcDream.App/Rendering/Scene/RenderSceneContracts.cs
Normal file
381
src/AcDream.App/Rendering/Scene/RenderSceneContracts.cs
Normal 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);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue