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:
parent
5d19c56d15
commit
58e7c2eb99
10 changed files with 949 additions and 148 deletions
266
src/AcDream.App/Rendering/Scene/LiveRenderProjectionJournal.cs
Normal file
266
src/AcDream.App/Rendering/Scene/LiveRenderProjectionJournal.cs
Normal 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);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue