refactor(app): key live projections by runtime identity

Move materialized live-object sidecars and presentation worksets to exact RuntimeEntityKey ownership. Runtime remains the only GUID/incarnation/local-ID authority while hydration, animation, effects, lights, equipped children, renderer resources, visibility, liveness, and teardown resolve exact projection identities. Preserve synchronous callbacks, local-ID allocation order, and current rendering behavior.
This commit is contained in:
Erik 2026-07-25 21:50:58 +02:00
parent e18df84437
commit 420e5eea70
73 changed files with 2939 additions and 1715 deletions

View file

@ -1,6 +1,7 @@
using AcDream.App.Update;
using AcDream.App.World;
using AcDream.Core.World;
using AcDream.Runtime.Entities;
namespace AcDream.App.Rendering.Scene;
@ -9,7 +10,7 @@ internal interface ILiveRenderProjectionSink : IRenderProjectionSyncPhase
bool OnEntityReady(LiveEntityReadyCandidate candidate);
void OnProjectionVisibilityChanged(LiveEntityRecord record, bool visible);
void OnProjectionPoseReady(uint serverGuid);
void OnProjectionRemoved(uint localEntityId);
void OnProjectionRemoved(LiveEntityRecord record);
void OnResourceUnregister(WorldEntity entity);
}
@ -25,7 +26,7 @@ internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
private readonly LiveEntityRuntime _runtime;
private readonly RenderProjectionJournal _journal;
private readonly IRenderTraversalOrderSource _traversalOrder;
private readonly Dictionary<uint, TrackedProjection> _byLocalId = [];
private readonly Dictionary<RuntimeEntityKey, TrackedProjection> _byKey = [];
private readonly List<LiveEntityRecord> _activeRootScratch = [];
private readonly List<TrackedProjection> _activeScratch = [];
@ -40,7 +41,7 @@ internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
?? throw new ArgumentNullException(nameof(traversalOrder));
}
public int ProjectionCount => _byLocalId.Count;
public int ProjectionCount => _byKey.Count;
public bool OnEntityReady(LiveEntityReadyCandidate candidate)
{
@ -62,7 +63,8 @@ internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
ArgumentNullException.ThrowIfNull(record);
if (!_runtime.IsCurrentRecord(record)
|| record.WorldEntity is not { } entity
|| !_byLocalId.ContainsKey(entity.Id))
|| record.ProjectionKey is not { } key
|| !_byKey.ContainsKey(key))
{
return;
}
@ -87,10 +89,12 @@ internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
Upsert(record, entity, record.IsSpatiallyVisible);
}
public void OnProjectionRemoved(uint localEntityId)
public void OnProjectionRemoved(LiveEntityRecord record)
{
if (_byLocalId.TryGetValue(
localEntityId,
ArgumentNullException.ThrowIfNull(record);
if (record.ProjectionKey is { } key
&& _byKey.TryGetValue(
key,
out TrackedProjection? tracked)
&& tracked.Projection.ProjectionClass is
RenderProjectionClass.EquippedChild)
@ -116,11 +120,17 @@ internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
public void OnResourceUnregister(WorldEntity entity)
{
ArgumentNullException.ThrowIfNull(entity);
if (_byLocalId.TryGetValue(entity.Id, out TrackedProjection? tracked)
&& ReferenceEquals(tracked.Entity, entity))
TrackedProjection? tracked = null;
foreach (TrackedProjection candidate in _byKey.Values)
{
Remove(tracked);
if (ReferenceEquals(candidate.Entity, entity))
{
tracked = candidate;
break;
}
}
if (tracked is not null)
Remove(tracked);
}
/// <summary>
@ -153,7 +163,7 @@ internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
// synchronization only refreshes children whose attachment projection
// is still retained by this derived scene.
_activeScratch.Clear();
foreach (TrackedProjection tracked in _byLocalId.Values)
foreach (TrackedProjection tracked in _byKey.Values)
{
if (tracked.Record.IsSpatiallyProjected
&& tracked.Record.ProjectionKind is
@ -166,8 +176,9 @@ internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
TrackedProjection tracked = _activeScratch[i];
if (!_runtime.IsCurrentRecord(tracked.Record)
|| !ReferenceEquals(tracked.Record.WorldEntity, tracked.Entity)
|| !_byLocalId.TryGetValue(
tracked.Entity.Id,
|| tracked.Record.ProjectionKey is not { } key
|| !_byKey.TryGetValue(
key,
out TrackedProjection? current)
|| !ReferenceEquals(current, tracked))
{
@ -183,7 +194,7 @@ internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
public void ResetTracking()
{
_byLocalId.Clear();
_byKey.Clear();
_activeRootScratch.Clear();
_activeScratch.Clear();
}
@ -192,9 +203,11 @@ internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
uint localEntityId,
out RenderProjectionRecord record)
{
if (_byLocalId.TryGetValue(
if (_runtime.TryGetRecordByLocalEntityId(
localEntityId,
out TrackedProjection? tracked))
out LiveEntityRecord owner)
&& owner.ProjectionKey is { } key
&& _byKey.TryGetValue(key, out TrackedProjection? tracked))
{
record = tracked.Projection;
return true;
@ -217,11 +230,12 @@ internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
record,
entity,
spatiallyVisible);
if (!_byLocalId.TryGetValue(entity.Id, out TrackedProjection? tracked))
RuntimeEntityKey key = RequireProjectionKey(record);
if (!_byKey.TryGetValue(key, out TrackedProjection? tracked))
{
_journal.Register(in projected);
_byLocalId.Add(
entity.Id,
_byKey.Add(
key,
new TrackedProjection(record, entity, projected));
return;
}
@ -234,7 +248,7 @@ internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
!= projected.ProjectionClass)
{
_journal.Register(in projected);
_byLocalId[entity.Id] =
_byKey[key] =
new TrackedProjection(record, entity, projected);
return;
}
@ -283,8 +297,9 @@ internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
// Spatial withdrawal is not logical destruction. Retain the last
// resident order while the projection is inactive so a portal/rebucket
// edge does not manufacture an unrelated ordering mutation.
return _byLocalId.TryGetValue(
entity.Id,
RuntimeEntityKey key = RequireProjectionKey(record);
return _byKey.TryGetValue(
key,
out TrackedProjection? retained)
? projected with { SortKey = retained.Projection.SortKey }
: projected;
@ -292,7 +307,8 @@ internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
private void Remove(TrackedProjection tracked)
{
if (!_byLocalId.Remove(tracked.Entity.Id))
RuntimeEntityKey key = RequireProjectionKey(tracked.Record);
if (!_byKey.Remove(key))
return;
_journal.Unregister(
tracked.Projection.Id,
@ -302,6 +318,13 @@ internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
private static uint Canonicalize(uint cellOrLandblockId) =>
(cellOrLandblockId & 0xFFFF0000u) | 0xFFFFu;
private static RuntimeEntityKey RequireProjectionKey(
LiveEntityRecord record) =>
record.ProjectionKey
?? throw new InvalidOperationException(
$"Live entity 0x{record.ServerGuid:X8}/{record.Generation} " +
"has no exact projection key.");
private sealed class TrackedProjection(
LiveEntityRecord record,
WorldEntity entity,