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:
parent
e18df84437
commit
420e5eea70
73 changed files with 2939 additions and 1715 deletions
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue