animancer icon indicating copy to clipboard operation
animancer copied to clipboard

threaded update rate example

Open JohnManna opened this issue 2 years ago • 4 comments

Use Case

Just want to point out if you follow the guide to update rate here https://kybernetik.com.au/animancer/docs/examples/fine-control/update-rate/ it will no longer process parts of the graph on worker threads and will force it to the main thread potentially causing worse performance then if letting Unity schedule.

Solution

I found the by toggling the graph running state and modifying its speed can work around this issue to increase performance while still allowing Unity to schedule across job threads. Here is an example script:

public class AnimationLODTest : MonoBehaviour {
    private AnimancerComponent _animancer;

    private void Start() {
        _animancer = GetComponent<AnimancerComponent>();
    }

    private float _lastUpdateTime;
    private float UpdatesPerSecond {
        get {
            switch (_lod) {
                case LOD.LOD1: return 45;
                case LOD.LOD2: return 30;
                case LOD.LOD3: return 10;
                case LOD.LOD4: return 2;
                default: return float.MaxValue;
            }
        }
    }

    public enum LOD {
        FullQuality,
        LOD1,
        LOD2,
        LOD3,
        LOD4,
    }

    private LOD _lod;

    public LOD Lod {
        get => _lod;
        set {
            if (_lod != value) {
                _lod = value;
                if (_lod == LOD.FullQuality) {
                    _animancer.Playable.Speed = 1f;
                    _animancer.Playable.UnpauseGraph();
                }
                else {
                    _animancer.Playable.PauseGraph();
                }
            }
        }
    }

    public void UpdateAnimations() {
        if (Lod == LOD.FullQuality) {
            _lastUpdateTime = Time.time;
            return;
        }

        UnityEngine.Profiling.Profiler.BeginSample("UpdateAnimations");
        var time = Time.time;
        var timeSinceLastUpdate = time - _lastUpdateTime;
        if (timeSinceLastUpdate > 1 / UpdatesPerSecond) {
                
            // performance concern, this will evaluate single threaded
            //_animancer.Evaluate(timeSinceLastUpdate); 
                
            // workaround, this should use job system
            _animancer.Playable.UnpauseGraph();
            _animancer.Playable.Speed = timeSinceLastUpdate / Time.unscaledDeltaTime;
            _lastUpdateTime = time;
        }
        else {
            _animancer.Playable.PauseGraph();
        }
        UnityEngine.Profiling.Profiler.EndSample();
    }
}

JohnManna avatar Dec 22 '22 20:12 JohnManna

That's a cool idea, I'll see if I can turn that into a utility script to do it more easily for the next version.

KybernetikGames avatar Dec 22 '22 23:12 KybernetikGames

How did you measure the performance difference? I did some quick tests by waiting for 1 second for the engine startup to stabilize then count the number of Updates that occur in 10 seconds with 100 characters set to Animate Always and the camera disabled and I can't get a consistent difference between your approach and mine, it just fluctuates about 5% either way.

KybernetikGames avatar Dec 23 '22 05:12 KybernetikGames

That is a good approach but might be missing some details.

In my test I have 129 characters with a idle clip and mixers for walk/run. To check performance I simply used the profiler and observed total ms. I didn't disable the camera other active scripts (there is barely anything in this prototype so far).

With the main thread method there are spikes as the graphs are evaluated. In this profiling sample I am updating animations at a rate of 45 fps. It will actually cause total ms time to increase on the main thread. You can see when the lod kicks in on the right the total time is higher and the spikes. image

With the alternative approach, these spikes are removed and performance remains consistent. The work that would of been done inside the Animancer LOD UpdateAnimations is spread out over different methods and threads so is not super clear but overall ms is down at this animation update rate resulting in a 20fps boost rather than a loss. image

JohnManna avatar Dec 23 '22 18:12 JohnManna

I can see an improvement from your approach in the profiler too, but that means the results are being affected by the overhead of the editor and the profiler. Since your approach means offloading the cost of animations out of the scripting loop, I'm worried that the profiler isn't properly counting it so doing it in a runtime build and calculating the average frame rate with no camera rendering overhead either should give more useful results. And it does show a massive improvement with either low frame rate approach compared to running normally, but nothing significant when comparing the two approaches. I'm assuming there's something I'm doing wrong since I would expect your approach to be better, but I just can't figure out what that might be.

KybernetikGames avatar Dec 23 '22 23:12 KybernetikGames