Files
sbox-public/engine/Sandbox.Engine/Scene/GameObjectSystems/SceneAnimationSystem.cs
Lorenz Junglas 4a050a9ab9 Animation optimizations v3 (#3635)
* Cleanup SkinnedModelRendererSetBoneMerge

* Proper bookkeeping for SkinnedModelRenderer Hierarchy

* Use ConcurrentQueue instead of Channel to reduce lock contention

* Limit animation update parallelism to Environment.ProcessorCount - 1

* BoneMerge in parallel

* Speed up native anim decompression using (lock-free) LRU posecache

* Remove some unused debug counters
2025-12-18 17:02:20 +01:00

116 lines
3.2 KiB
C#

using Sandbox.Utility;
using System.Collections.Concurrent;
using System.Threading.Channels;
namespace Sandbox;
[Expose]
public sealed class SceneAnimationSystem : GameObjectSystem<SceneAnimationSystem>
{
private HashSetEx<SkinnedModelRenderer> SkinnedRenderers { get; } = new();
internal void AddRenderer( SkinnedModelRenderer renderer )
{
SkinnedRenderers.Add( renderer );
}
internal void RemoveRenderer( SkinnedModelRenderer renderer )
{
SkinnedRenderers.Remove( renderer );
}
private ConcurrentQueue<GameTransform> ChangedTransforms { get; } = new();
private static int _animThreadCount = Math.Max( 1, Environment.ProcessorCount - 1 );
private static ParallelOptions _animParallelOptions = new()
{
MaxDegreeOfParallelism = _animThreadCount
};
private object _decodeCacheLock = new();
public SceneAnimationSystem( Scene scene ) : base( scene )
{
Listen( Stage.UpdateBones, 0, UpdateAnimation, "UpdateAnimation" );
Listen( Stage.FinishUpdate, 0, FinishUpdate, "FinishUpdate" );
Listen( Stage.PhysicsStep, 0, PhysicsStep, "PhysicsStep" );
}
void UpdateAnimation()
{
using ( PerformanceStats.Timings.Animation.Scope() )
{
var rootRenderers = SkinnedRenderers.EnumerateLocked().Where( x => x.IsRootRenderer );
// We hold the lock in managed because we want it to be as coarse as possible to avoid contention with the cache maintenance
lock ( _decodeCacheLock )
{
// Skip out if we have a parent that is a skinned model, because we need to move relative to that
// and their bones haven't been worked out yet. They will get worked out after our parent is.
System.Threading.Tasks.Parallel.ForEach( rootRenderers, _animParallelOptions, ProcessRenderer );
}
// This is a good time to maintain decode caches
// Will copy local caches to the global cache and handle LRU eviction
// Can do this in a background task as nothing is touching these caches until next frame
Task.Run
(
() =>
{
lock ( _decodeCacheLock )
{
g_pAnimationSystemUtils.MaintainDecodeCaches();
}
}
);
// Now merge any descendants
System.Threading.Tasks.Parallel.ForEach( rootRenderers.Where( x => !x.BoneMergeTarget.IsValid() ), _animParallelOptions, x => x.MergeDescendants( ChangedTransforms.Enqueue ) );
while ( ChangedTransforms.TryDequeue( out var tx ) )
{
tx.TransformChanged( true );
}
//
// Run events in the main thread
//
foreach ( var x in SkinnedRenderers.EnumerateLocked() )
{
x.DispatchEvents();
}
}
}
void ProcessRenderer( SkinnedModelRenderer renderer )
{
if ( !renderer.IsValid() || !renderer.Enabled )
return;
if ( renderer.AnimationUpdate() )
{
ChangedTransforms.Enqueue( renderer.Transform );
}
foreach ( var child in renderer.SkinnedChildren )
{
ProcessRenderer( child );
}
}
void FinishUpdate()
{
foreach ( var renderer in SkinnedRenderers.EnumerateLocked() )
{
renderer.FinishUpdate();
}
}
void PhysicsStep()
{
var physRenderers = SkinnedRenderers.EnumerateLocked().Where( x => x.Physics != null );
System.Threading.Tasks.Parallel.ForEach( physRenderers, _animParallelOptions, renderer => renderer.Physics.Step() );
}
}