perf(vfx): port retail particle visibility degradation

Resolve DAT-authored particle ranges from the hardware GfxObj, apply retail distance and completed-cell visibility gates, and preserve the exact finite/infinite off-view update semantics. This removes dense-world simulation work without shortening terrain, entity, fog, or streaming distance.

Publish doorway-clipped outdoor cells through a focused frame controller, retain effect cell identity for outdoor statics, reject hidden emitters before particle-slot scans, and offer an explicit opt-in Extended particle range.

Release build succeeds and all 5,857 tests pass with five intentional skips. Retail-conformance, architecture, and adversarial review cycles are clean; connected Aerlinthe visual/performance gate pending.

Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-17 15:27:36 +02:00
parent 82789eea88
commit f1ba147ac5
29 changed files with 1810 additions and 125 deletions

View file

@ -28,15 +28,18 @@ public sealed class ParticleHookSink : IAnimationHookSink
{
private readonly ParticleSystem _system;
private readonly IEntityEffectPoseSource _poses;
private readonly IEntityEffectCellSource? _cells;
private readonly ConcurrentDictionary<(uint EntityId, uint EmitterId), int> _handlesByKey = new();
private readonly ConcurrentDictionary<uint, ConcurrentDictionary<int, byte>> _handlesByEntity = new();
private readonly ConcurrentDictionary<int, EmitterBinding> _bindingsByHandle = new();
private readonly ConcurrentDictionary<uint, ParticleRenderPass> _renderPassByEntity = new();
private readonly ConcurrentDictionary<uint, byte> _examinationOwners = new();
private readonly ConcurrentDictionary<uint, byte> _hiddenPresentationOwners = new();
public ParticleHookSink(ParticleSystem system, IEntityEffectPoseSource poses)
{
_system = system ?? throw new ArgumentNullException(nameof(system));
_poses = poses ?? throw new ArgumentNullException(nameof(poses));
_cells = poses as IEntityEffectCellSource;
_system.EmitterDied += OnEmitterDied;
}
@ -52,6 +55,7 @@ public sealed class ParticleHookSink : IAnimationHookSink
public int LogicalEmitterCount => _handlesByKey.Count;
public int TrackedOwnerCount => _handlesByEntity.Count;
public int RenderPassOwnerCount => _renderPassByEntity.Count;
public int ExaminationOwnerCount => _examinationOwners.Count;
public int HiddenPresentationOwnerCount => _hiddenPresentationOwners.Count;
private void OnEmitterDied(int handle)
@ -115,11 +119,32 @@ public sealed class ParticleHookSink : IAnimationHookSink
}
}
public void SetEntityRenderPass(uint entityId, ParticleRenderPass renderPass) =>
public void SetEntityRenderPass(uint entityId, ParticleRenderPass renderPass)
{
_renderPassByEntity[entityId] = renderPass;
RefreshOwnerVisibilityPolicy(entityId);
}
public void ClearEntityRenderPass(uint entityId) =>
public void ClearEntityRenderPass(uint entityId)
{
_renderPassByEntity.TryRemove(entityId, out _);
RefreshOwnerVisibilityPolicy(entityId);
}
/// <summary>
/// Mirrors retail <c>CPhysicsObj::m_bExaminationObject</c>. Examination
/// previews bypass world distance and cell visibility without conflating
/// that policy with attachment identity.
/// </summary>
public void SetEntityExaminationObject(uint entityId, bool isExaminationObject)
{
if (isExaminationObject)
_examinationOwners[entityId] = 0;
else
_examinationOwners.TryRemove(entityId, out _);
RefreshOwnerVisibilityPolicy(entityId);
}
/// <summary>
/// Mirrors live cell membership without ending emitter lifetime. Retail
@ -158,9 +183,18 @@ public sealed class ParticleHookSink : IAnimationHookSink
bool presentationVisible =
!_hiddenPresentationOwners.ContainsKey(binding.OwnerLocalId);
if (TryResolveAnchor(binding.OwnerLocalId, binding.PartIndex,
binding.HookOffsetOrigin, out Vector3 anchor, out Quaternion rotation))
binding.HookOffsetOrigin,
out Vector3 anchor,
out Quaternion rotation,
out Vector3 ownerPosition))
{
_system.UpdateEmitterAnchor(handle, anchor, rotation);
_system.UpdateEmitterOwnerPosition(handle, ownerPosition);
_system.UpdateEmitterOwnerCell(
handle,
_cells is not null && _cells.TryGetCellId(binding.OwnerLocalId, out uint cellId)
? cellId
: 0u);
// Keep the anchor current while spatially paused; re-entry
// resumes at the authoritative pose without generating the
// absent interval's time- or distance-driven emissions.
@ -193,6 +227,7 @@ public sealed class ParticleHookSink : IAnimationHookSink
_handlesByKey.TryRemove(key, out _);
}
ClearEntityRenderPass(entityId);
_examinationOwners.TryRemove(entityId, out _);
_hiddenPresentationOwners.TryRemove(entityId, out _);
}
@ -236,7 +271,9 @@ public sealed class ParticleHookSink : IAnimationHookSink
Vector3 offsetOrigin = offset?.Origin ?? Vector3.Zero;
Quaternion offsetOrientation = offset?.Orientation ?? Quaternion.Identity;
if (!TryResolveAnchor(ownerLocalId, partIndex, offsetOrigin,
out Vector3 anchor, out Quaternion rotation))
out Vector3 anchor,
out Quaternion rotation,
out Vector3 ownerPosition))
{
DiagnosticSink?.Invoke(
$"No live effect pose for owner 0x{ownerLocalId:X8}, part {partIndex}; " +
@ -247,6 +284,7 @@ public sealed class ParticleHookSink : IAnimationHookSink
ParticleRenderPass renderPass = _renderPassByEntity.TryGetValue(ownerLocalId, out var pass)
? pass
: ParticleRenderPass.Scene;
ParticleVisibilityPolicy visibilityPolicy = ResolveVisibilityPolicy(ownerLocalId);
if (!_system.TrySpawnEmitterById(
emitterInfoId,
anchor,
@ -254,6 +292,7 @@ public sealed class ParticleHookSink : IAnimationHookSink
ownerLocalId,
partIndex,
renderPass,
visibilityPolicy,
out int handle))
{
DiagnosticSink?.Invoke(
@ -261,6 +300,12 @@ public sealed class ParticleHookSink : IAnimationHookSink
$"0x{ownerLocalId:X8} was not found; no fallback was created.");
return;
}
_system.UpdateEmitterOwnerPosition(handle, ownerPosition);
_system.UpdateEmitterOwnerCell(
handle,
_cells is not null && _cells.TryGetCellId(ownerLocalId, out uint cellId)
? cellId
: 0u);
if (_hiddenPresentationOwners.ContainsKey(ownerLocalId))
{
_system.SetEmitterPresentationVisible(handle, false);
@ -282,20 +327,45 @@ public sealed class ParticleHookSink : IAnimationHookSink
_handlesByKey[(ownerLocalId, logicalId)] = handle;
}
private ParticleVisibilityPolicy ResolveVisibilityPolicy(uint ownerLocalId)
{
if (_examinationOwners.ContainsKey(ownerLocalId))
return ParticleVisibilityPolicy.Examination;
return _renderPassByEntity.TryGetValue(ownerLocalId, out ParticleRenderPass pass)
&& pass != ParticleRenderPass.Scene
? ParticleVisibilityPolicy.PassOwned
: ParticleVisibilityPolicy.World;
}
private void RefreshOwnerVisibilityPolicy(uint ownerLocalId)
{
if (!_handlesByEntity.TryGetValue(ownerLocalId, out var handles))
return;
ParticleVisibilityPolicy policy = ResolveVisibilityPolicy(ownerLocalId);
foreach (int handle in handles.Keys)
_system.SetEmitterVisibilityPolicy(handle, policy);
}
private bool TryResolveAnchor(
uint ownerLocalId,
int partIndex,
Vector3 offsetOrigin,
out Vector3 anchor,
out Quaternion rotation)
out Quaternion rotation,
out Vector3 ownerPosition)
{
if (!_poses.TryGetRootPose(ownerLocalId, out Matrix4x4 rootWorld))
{
anchor = default;
rotation = default;
ownerPosition = default;
return false;
}
ownerPosition = new Vector3(rootWorld.M41, rootWorld.M42, rootWorld.M43);
Matrix4x4 ownerFrame = rootWorld;
if (partIndex != -1)
{
@ -303,6 +373,7 @@ public sealed class ParticleHookSink : IAnimationHookSink
{
anchor = default;
rotation = default;
ownerPosition = default;
return false;
}
ownerFrame = partLocal * rootWorld;
@ -313,6 +384,7 @@ public sealed class ParticleHookSink : IAnimationHookSink
{
anchor = default;
rotation = default;
ownerPosition = default;
return false;
}
rotation = Quaternion.Normalize(rotation);