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

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using AcDream.Core.Physics;
using AcDream.Core.World;
using AcDream.Runtime.Entities;
namespace AcDream.App.Rendering.Wb;
@ -48,18 +49,20 @@ public sealed class EntitySpawnAdapter
private readonly Func<WorldEntity, AnimationSequencer> _sequencerFactory;
private readonly IWbMeshAdapter? _meshAdapter;
// One logical owner per server GUID. Animated state survives projection
// One logical owner per exact Runtime identity. Animated state survives projection
// suspension, while the resident bit controls the shorter GPU-presentation
// lifetime. The exact WorldEntity reference makes delayed visibility edges
// from a displaced GUID generation harmless.
// Single-threaded: called only from the render thread (same as GpuWorldState).
private readonly Dictionary<uint, Owner> _ownersByGuid = new();
private readonly Dictionary<RuntimeEntityKey, Owner> _ownersByKey = [];
private sealed class Owner(
RuntimeEntityKey key,
WorldEntity entity,
AnimatedEntityState state,
HashSet<ulong> meshIds)
{
public RuntimeEntityKey Key { get; } = key;
public WorldEntity Entity { get; } = entity;
public AnimatedEntityState State { get; } = state;
public HashSet<ulong> MeshIds { get; set; } = meshIds;
@ -137,12 +140,25 @@ public sealed class EntitySpawnAdapter
/// <paramref name="entity"/> is atlas-tier (<c>ServerGuid == 0</c>).
/// </summary>
public AnimatedEntityState? OnCreate(WorldEntity entity)
=> OnCreate(new RuntimeEntityKey(entity.Id, 0), entity);
/// <summary>
/// Creates one exact Runtime-owned presentation resource set.
/// </summary>
public AnimatedEntityState? OnCreate(
RuntimeEntityKey key,
WorldEntity entity)
{
ArgumentNullException.ThrowIfNull(entity);
// Atlas-tier entities (procedural / dat-hydrated, ServerGuid == 0)
// are handled by LandblockSpawnAdapter, not here.
if (entity.ServerGuid == 0) return null;
if (key.LocalEntityId == 0 || key.LocalEntityId != entity.Id)
{
throw new InvalidOperationException(
"The exact Runtime projection key must match the WorldEntity local ID.");
}
// A.5 T18: populate cached AABB so WalkEntities reads from the cache
// rather than recomputing Position±5 per frame. Called here because
@ -177,8 +193,8 @@ public sealed class EntitySpawnAdapter
// valid. Retirement is also completed before replacement publication:
// a failed texture or mesh release therefore leaves the prior owner in
// the dictionary, with its per-resource progress available to retry.
var replacementOwner = new Owner(entity, state, meshIds);
if (_ownersByGuid.TryGetValue(entity.ServerGuid, out Owner? displacedOwner))
var replacementOwner = new Owner(key, entity, state, meshIds);
if (_ownersByKey.TryGetValue(key, out Owner? displacedOwner))
{
if (displacedOwner.RemovalPending)
{
@ -200,11 +216,11 @@ public sealed class EntitySpawnAdapter
}
}
_ownersByGuid[entity.ServerGuid] = replacementOwner;
_ownersByKey[key] = replacementOwner;
}
else
{
_ownersByGuid.Add(entity.ServerGuid, replacementOwner);
_ownersByKey.Add(key, replacementOwner);
}
return state;
@ -223,8 +239,7 @@ public sealed class EntitySpawnAdapter
public bool SetPresentationResident(WorldEntity entity, bool resident)
{
ArgumentNullException.ThrowIfNull(entity);
if (!_ownersByGuid.TryGetValue(entity.ServerGuid, out Owner? owner)
|| !ReferenceEquals(owner.Entity, entity)
if (!TryFindOwner(entity, out _, out Owner owner)
|| owner.RemovalPending
|| (resident ? owner.IsFullyResident : owner.IsFullySuspended))
{
@ -270,8 +285,7 @@ public sealed class EntitySpawnAdapter
ArgumentNullException.ThrowIfNull(partOverrides);
ArgumentNullException.ThrowIfNull(publishAppearance);
if (!_ownersByGuid.TryGetValue(entity.ServerGuid, out Owner? owner)
|| !ReferenceEquals(owner.Entity, entity)
if (!TryFindOwner(entity, out _, out Owner owner)
|| owner.RemovalPending
|| owner.Transition != PresentationTransition.None)
{
@ -340,19 +354,31 @@ public sealed class EntitySpawnAdapter
/// and propagates the exception; a later call resumes its unfinished releases.
/// </summary>
public void OnRemove(uint serverGuid)
=> _ = TryRemove(serverGuid, expectedEntity: null);
private bool TryRemove(uint serverGuid, WorldEntity? expectedEntity)
{
if (!_ownersByGuid.TryGetValue(serverGuid, out Owner? owner)
|| (expectedEntity is not null && !ReferenceEquals(owner.Entity, expectedEntity)))
foreach ((RuntimeEntityKey key, Owner owner) in _ownersByKey)
{
if (owner.Entity.ServerGuid == serverGuid)
{
_ = TryRemove(key, owner.Entity);
return;
}
}
}
private bool TryRemove(
RuntimeEntityKey key,
WorldEntity expectedEntity)
{
if (!_ownersByKey.TryGetValue(key, out Owner? owner)
|| !ReferenceEquals(owner.Entity, expectedEntity))
{
return false;
}
owner.RemovalPending = true;
if (owner.Transition != PresentationTransition.None)
throw new EntityPresentationRemovalDeferredException(serverGuid);
throw new EntityPresentationRemovalDeferredException(
owner.Entity.ServerGuid);
// A resident owner still holds both mesh and potentially-lazy texture
// resources. A suspended owner released them at the visibility edge,
@ -363,10 +389,10 @@ public sealed class EntitySpawnAdapter
if (owner.HasPresentationResources && !SuspendPresentation(owner))
return false;
if (_ownersByGuid.TryGetValue(serverGuid, out Owner? current)
if (_ownersByKey.TryGetValue(key, out Owner? current)
&& ReferenceEquals(current, owner))
{
_ownersByGuid.Remove(serverGuid);
_ownersByKey.Remove(key);
return true;
}
@ -384,7 +410,8 @@ public sealed class EntitySpawnAdapter
public bool OnRemove(WorldEntity entity)
{
ArgumentNullException.ThrowIfNull(entity);
return TryRemove(entity.ServerGuid, entity);
return TryFindOwner(entity, out RuntimeEntityKey key, out _)
&& TryRemove(key, entity);
}
/// <summary>
@ -393,9 +420,35 @@ public sealed class EntitySpawnAdapter
/// been removed.
/// </summary>
public AnimatedEntityState? GetState(uint serverGuid)
=> _ownersByGuid.TryGetValue(serverGuid, out Owner? owner)
? owner.State
: null;
{
foreach (Owner owner in _ownersByKey.Values)
{
if (owner.Entity.ServerGuid == serverGuid)
return owner.State;
}
return null;
}
private bool TryFindOwner(
WorldEntity entity,
out RuntimeEntityKey key,
out Owner owner)
{
foreach ((RuntimeEntityKey candidateKey, Owner candidate) in _ownersByKey)
{
if (ReferenceEquals(candidate.Entity, entity))
{
key = candidateKey;
owner = candidate;
return true;
}
}
key = default;
owner = null!;
return false;
}
private bool ResumePresentation(Owner owner)
{