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:
parent
82789eea88
commit
f1ba147ac5
29 changed files with 1810 additions and 125 deletions
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue