namespace AcDream.App.Rendering.Scene;
///
/// Update-thread-owned structural journal between accepted world mutation and
/// the disposable render projection. Structural edges are never coalesced.
/// A successful frame publication drains the complete retained buffer.
///
internal sealed class RenderProjectionJournal
{
private readonly List _deltas = [];
private ulong _nextSequence = 1;
public RenderProjectionJournal(RenderSceneGeneration generation)
{
Generation = generation;
}
public RenderSceneGeneration Generation { get; private set; }
public int Count => _deltas.Count;
public int Capacity => _deltas.Capacity;
public void Register(in RenderProjectionRecord record) =>
_deltas.Add(RenderProjectionDelta.Register(
Generation,
NextSequence(),
in record));
public void Update(
RenderProjectionDeltaKind kind,
in RenderProjectionRecord record) =>
_deltas.Add(RenderProjectionDelta.Update(
kind,
Generation,
NextSequence(),
in record));
public void Unregister(
RenderProjectionId id,
RenderOwnerIncarnation incarnation) =>
_deltas.Add(RenderProjectionDelta.Unregister(
Generation,
NextSequence(),
id,
incarnation));
public void AppendDifference(
in RenderProjectionRecord prior,
in RenderProjectionRecord current)
{
if (prior.Id != current.Id
|| prior.OwnerIncarnation != current.OwnerIncarnation
|| prior.ProjectionClass != current.ProjectionClass)
{
throw new ArgumentException(
"A channel update cannot change projection identity, incarnation, or class.",
nameof(current));
}
if (prior.Transform != current.Transform
|| prior.Bounds != current.Bounds
|| prior.SortKey != current.SortKey
|| prior.Source.TransformFingerprint
!= current.Source.TransformFingerprint)
{
Update(RenderProjectionDeltaKind.UpdateTransform, in current);
}
if (prior.MeshSet != current.MeshSet
|| prior.Material != current.Material
|| prior.DegradeState != current.DegradeState
|| prior.EntityPayload != current.EntityPayload
|| prior.Source.GeometryFingerprint
!= current.Source.GeometryFingerprint
|| prior.Source.AppearanceFingerprint
!= current.Source.AppearanceFingerprint)
{
Update(RenderProjectionDeltaKind.UpdateAppearance, in current);
}
if (prior.Residency != current.Residency)
Update(RenderProjectionDeltaKind.Rebucket, in current);
if (prior.Flags != current.Flags)
Update(RenderProjectionDeltaKind.UpdateFlags, in current);
}
public RenderDeltaApplyResult DrainTo(IRenderScene scene)
{
ArgumentNullException.ThrowIfNull(scene);
if (scene.Generation != Generation)
{
throw new InvalidOperationException(
$"Journal {Generation} cannot drain into scene {scene.Generation}.");
}
RenderDeltaApplyResult result =
scene.Apply(System.Runtime.InteropServices.CollectionsMarshal.AsSpan(_deltas));
_deltas.Clear();
return result;
}
public void Clear(RenderSceneGeneration replacementGeneration)
{
if (replacementGeneration.CompareTo(Generation) <= 0)
{
throw new ArgumentOutOfRangeException(
nameof(replacementGeneration),
replacementGeneration,
"A replacement journal generation must advance.");
}
_deltas.Clear();
Generation = replacementGeneration;
}
internal ReadOnlySpan Pending =>
System.Runtime.InteropServices.CollectionsMarshal.AsSpan(_deltas);
private ulong NextSequence()
{
if (_nextSequence == ulong.MaxValue)
{
throw new InvalidOperationException(
"Render projection journal sequence exhausted.");
}
return _nextSequence++;
}
}