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

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