feat(rendering): journal live scene projections

Mirror exact live-root and equipped-child lifetimes behind the non-drawing render-scene boundary. Ready, visibility, final-pose, rebucket, removal, and resource teardown edges retain canonical LiveEntityRuntime identity while active-only synchronization avoids resident-static scans.

Co-authored-by: Erik Nilsson <erikn@users.noreply.github.com>
This commit is contained in:
Erik 2026-07-24 22:05:42 +02:00
parent 5d19c56d15
commit 58e7c2eb99
10 changed files with 949 additions and 148 deletions

View file

@ -0,0 +1,266 @@
using AcDream.App.World;
using AcDream.Core.World;
namespace AcDream.App.Rendering.Scene;
internal interface ILiveRenderProjectionSink
{
bool OnEntityReady(LiveEntityReadyCandidate candidate);
void OnProjectionVisibilityChanged(LiveEntityRecord record, bool visible);
void OnProjectionPoseReady(uint serverGuid);
void OnProjectionRemoved(uint localEntityId);
void OnResourceUnregister(WorldEntity entity);
void SynchronizeActiveSources();
}
/// <summary>
/// Change-driven render projection for canonical live records. It retains
/// exact record references and local presentation IDs only; server GUID lookup
/// remains exclusively in <see cref="LiveEntityRuntime"/>.
/// </summary>
internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
{
private const byte LiveProjectionDomain = 3;
private readonly LiveEntityRuntime _runtime;
private readonly RenderProjectionJournal _journal;
private readonly Dictionary<uint, TrackedProjection> _byLocalId = [];
private readonly List<TrackedProjection> _activeScratch = [];
public LiveRenderProjectionJournal(
LiveEntityRuntime runtime,
RenderProjectionJournal journal)
{
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
_journal = journal ?? throw new ArgumentNullException(nameof(journal));
}
public int ProjectionCount => _byLocalId.Count;
public bool OnEntityReady(LiveEntityReadyCandidate candidate)
{
if (!candidate.IsCurrent(_runtime)
|| candidate.WorldEntity is not { } entity
|| !candidate.Record.ResourcesRegistered)
{
return false;
}
Upsert(candidate.Record, entity, candidate.Record.IsSpatiallyVisible);
return candidate.IsCurrent(_runtime);
}
public void OnProjectionVisibilityChanged(
LiveEntityRecord record,
bool visible)
{
ArgumentNullException.ThrowIfNull(record);
if (!_runtime.IsCurrentRecord(record)
|| record.WorldEntity is not { } entity
|| !_byLocalId.ContainsKey(entity.Id))
{
return;
}
Upsert(record, entity, visible);
}
public void OnProjectionPoseReady(uint serverGuid)
{
if (!_runtime.TryGetRecord(serverGuid, out LiveEntityRecord record)
|| record.WorldEntity is not { } entity
|| !_byLocalId.ContainsKey(entity.Id)
|| record.ProjectionKind is not LiveEntityProjectionKind.Attached)
{
return;
}
Upsert(record, entity, record.IsSpatiallyVisible);
}
public void OnProjectionRemoved(uint localEntityId)
{
if (_byLocalId.TryGetValue(
localEntityId,
out TrackedProjection? tracked)
&& tracked.Projection.ProjectionClass is
RenderProjectionClass.EquippedChild)
{
Remove(tracked);
}
}
public void OnResourceUnregister(WorldEntity entity)
{
ArgumentNullException.ThrowIfNull(entity);
if (_byLocalId.TryGetValue(entity.Id, out TrackedProjection? tracked)
&& ReferenceEquals(tracked.Entity, entity))
{
Remove(tracked);
}
}
/// <summary>
/// Mirrors only registered live/attached sources after simulation,
/// animation, and attachment pose composition have committed. Resident
/// statics are never scanned.
/// </summary>
public void SynchronizeActiveSources()
{
_activeScratch.Clear();
foreach (TrackedProjection tracked in _byLocalId.Values)
{
if (tracked.Record.IsSpatiallyProjected)
_activeScratch.Add(tracked);
}
for (int i = 0; i < _activeScratch.Count; i++)
{
TrackedProjection tracked = _activeScratch[i];
if (!_runtime.IsCurrentRecord(tracked.Record)
|| !ReferenceEquals(tracked.Record.WorldEntity, tracked.Entity)
|| !_byLocalId.TryGetValue(
tracked.Entity.Id,
out TrackedProjection? current)
|| !ReferenceEquals(current, tracked))
{
continue;
}
Upsert(
tracked.Record,
tracked.Entity,
tracked.Record.IsSpatiallyVisible);
}
}
public void ResetTracking()
{
_byLocalId.Clear();
_activeScratch.Clear();
}
internal bool TryGet(
uint localEntityId,
out RenderProjectionRecord record)
{
if (_byLocalId.TryGetValue(
localEntityId,
out TrackedProjection? tracked))
{
record = tracked.Projection;
return true;
}
record = default;
return false;
}
internal static RenderProjectionId ProjectionId(uint localEntityId) =>
RenderProjectionId.FromRaw(
((ulong)LiveProjectionDomain << 56) | localEntityId);
private void Upsert(
LiveEntityRecord record,
WorldEntity entity,
bool spatiallyVisible)
{
RenderProjectionRecord projected = Project(
record,
entity,
spatiallyVisible);
if (!_byLocalId.TryGetValue(entity.Id, out TrackedProjection? tracked))
{
_journal.Register(in projected);
_byLocalId.Add(
entity.Id,
new TrackedProjection(record, entity, projected));
return;
}
if (!ReferenceEquals(tracked.Record, record)
|| !ReferenceEquals(tracked.Entity, entity)
|| tracked.Projection.OwnerIncarnation
!= projected.OwnerIncarnation
|| tracked.Projection.ProjectionClass
!= projected.ProjectionClass)
{
_journal.Register(in projected);
_byLocalId[entity.Id] =
new TrackedProjection(record, entity, projected);
return;
}
projected = projected with
{
PreviousTransform = new PreviousRenderTransform(
tracked.Projection.Transform.LocalToWorld),
};
_journal.AppendDifference(tracked.Projection, projected);
tracked.Projection = projected;
}
private RenderProjectionRecord Project(
LiveEntityRecord record,
WorldEntity entity,
bool spatiallyVisible)
{
uint fullCellId = record.FullCellId != 0
? record.FullCellId
: entity.ParentCellId ?? 0;
uint ownerLandblockId = record.CanonicalLandblockId != 0
? record.CanonicalLandblockId
: fullCellId == 0
? 0
: Canonicalize(fullCellId);
return RenderProjectionRecordFactory.ProjectEntity(
ProjectionId(entity.Id),
record.ProjectionKind is LiveEntityProjectionKind.Attached
? RenderProjectionClass.EquippedChild
: RenderProjectionClass.LiveDynamicRoot,
RenderOwnerIncarnation.FromRaw(
((ulong)record.Generation << 32) | entity.Id),
ownerLandblockId,
fullCellId,
entity,
spatiallyVisible);
}
private void Remove(TrackedProjection tracked)
{
if (!_byLocalId.Remove(tracked.Entity.Id))
return;
_journal.Unregister(
tracked.Projection.Id,
tracked.Projection.OwnerIncarnation);
}
private static uint Canonicalize(uint cellOrLandblockId) =>
(cellOrLandblockId & 0xFFFF0000u) | 0xFFFFu;
private sealed class TrackedProjection(
LiveEntityRecord record,
WorldEntity entity,
RenderProjectionRecord projection)
{
public LiveEntityRecord Record { get; } = record;
public WorldEntity Entity { get; } = entity;
public RenderProjectionRecord Projection { get; set; } = projection;
}
}
internal sealed class LiveRenderProjectionResourceLifecycle(
ILiveRenderProjectionSink sink) : ILiveEntityResourceLifecycle
{
private readonly ILiveRenderProjectionSink _sink =
sink ?? throw new ArgumentNullException(nameof(sink));
public void Register(WorldEntity entity)
{
// The exact EntityReady edge follows all resource registration and is
// the first point at which the projection may enter the journal.
}
public void Unregister(WorldEntity entity) =>
_sink.OnResourceUnregister(entity);
}