Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Resolved SRP Volume API performance

Discussion in 'SRP Dev Blitz Day 2022 - Q&A' started by sabojako, Sep 29, 2022.

  1. sabojako

    sabojako

    Joined:
    Jul 7, 2017
    Posts:
    48
    The Volume API is great. It's flexible enough that I can expose various settings, sometimes not even related to post-processing.
    • However it can be VERY bad on CPU performance. What is the plan regarding this?
    • Are there some next steps for Volumes, not relating to performance?

    In our codebase, on many VolumeComponents we added a [DisabledVolumeComponent] attribute so that they don't register to the Type array and dictionary. That helped a lot. This solution requires code though. Maybe you guys could add some graphics settings to control this instead. I haven't tested latest, but I saw some method GetSupportedVolumeComponents() which seems to head to that direction.

    One of the things I noticed is that when working on VolumeParameters, some methods like Interp() and SetValue() are actually casting VolumeParameter into their respective VolumeParameter<T> type to get the value. Although flexible, this seems highly inefficient, considering the amount of params that can be processed.

    For instance, this happens in the default abstract implementation of VolumeComponent.Override() where all VolumeParameters are interpolated by using casting under the hood. In fact, when manually implementing an override VolumeComponent.Override() and interpolating the params "manually" as their real Type, I saw speed-ups of 75% on VolumeManager.OverrideData() in my benchmarks, in a release build profiled with native CPU tools. In the end, my solution was to run a tool that automatically generates an override VolumeComponent.Override() in every VolumeComponent file.

    For another instance, VolumeManager.ReplaceData() "resets" the stack to the value of another VolumeParameter using VolumeParameter.SetValue() which uses casting underneath. So instead of casting, I thought of adding a VolumeParameter.Reset() method which just sets the value straight to the defaultValue. That showed some gains too. Something like this:

    Code (CSharp):
    1.         [NonSerialized]
    2.         protected T m_DefaultValue;
    3.  
    4.         protected VolumeParameter(T value, bool overrideState)
    5.         {
    6.             // When T is value type, we can store a default value to use fast Reset() later
    7.             // When T is a class, we cannot store a m_DefaultValue because m_DefaultValue and m_Value would point to the same instance
    8.             isValueType = typeof(T).IsValueType;
    9.             if(isValueType)
    10.             {
    11.                 m_DefaultValue = value;
    12.             }
    13.             m_Value = value;
    14.             this.overrideState = overrideState;
    15.         }
    16.  
    17.         internal override void Reset()
    18.         {
    19.             m_OverrideState = false;
    20.             m_Value = m_DefaultValue;
    21.         }
    Code (CSharp):
    1.         void ReplaceData(VolumeStack stack, List<VolumeComponent> components)
    2.         {
    3.             foreach (var component in components)
    4.             {
    5.                 var target = stack.GetComponent(component.GetType());
    6.                 int count = component.parameters.Count;
    7.  
    8.                 for (int i = 0; i < count; i++)
    9.                 {
    10.                     var parameter = target.parameters[i];
    11.                     if (parameter != null)
    12.                     {
    13.                         if (parameter.isValueType)
    14.                         {
    15.                             parameter.Reset();
    16.                         }
    17.                         else
    18.                         {
    19.                             // When T of VolumeParameter<T> is a class, we cannot Reset() because it would replace m_Value with NULL
    20.                             parameter.overrideState = false;
    21.                             parameter.SetValue(component.parameters[i]);
    22.                         }
    23.                     }
    24.                 }
    25.             }
    26.         }
     
    Last edited: Sep 29, 2022
    tmonestudio, florianBrn and sacb0y like this.
  2. arttu_p

    arttu_p

    Unity Technologies

    Joined:
    Jan 15, 2021
    Posts:
    21
    Hi sabojako and thanks for the great question! As it happens, this is exactly what I've been looking at improving this week :) We are aware that VolumeManager runtime performance is suboptimal and hopefully I can improve this. The current plan is to make some minor performance improvements for 2023.1 that should be easily backportable. For 2023.2 there is a tentative plan for a bigger refactor that would allow us to address the performance issues on a more fundamental level. As you rightly point out, the current implementation relying on casting and virtual methods is tricky in terms of performance, and ReplaceData takes way too long relative to the amount of work that it actually does. If the architecture of the system were different (perhaps using value types rather than reference types), ReplaceData might be implementable as a simple memcpy which should be very fast. But design changes on this are still up in the air right now - large changes are tricky as we must maintain compatibility.

    Thanks a lot for the code samples, those are impressive gains. I will look if there are some ideas here that I could take to our code! May I ask if you are working with HDRP or URP? Could you provide me some sense of how much the CPU cost of VolumeManager is for you?

    > Are there some next steps for Volumes, not relating to performance?

    There's a couple of minor UX things I'm addressing right now. In addition we want to unify the default volume approach between HDRP and URP - right now URP doesn't have a configurable default volume like HDRP does. I'd be interested to hear if there are any other things on your mind that you'd like to see improvements on!
     
    florianBrn likes this.
  3. sacb0y

    sacb0y

    Joined:
    May 9, 2016
    Posts:
    776
    Ah i think this is something related to a question I asked elsewhere.
     
  4. sabojako

    sabojako

    Joined:
    Jul 7, 2017
    Posts:
    48
    Hey there! Excellent news!

    Right I heard about that. Well should the default volume profile actually act as a mask to dictate which VolumeComponent are used by the project? Is that a good idea?

    That would be awesome. 95% of params seem to be value types. What about Params that use object value though?
    An idea could be to Burst-ify param interpolations, given most are floats and vectors. But I don't know if the end-user design pattern fits Burst. (blending from A to B to C in specific order.)

    URP. Regarding numbers, it's been a while but initially the cost must have been around 2-3 ms on Switch without optimisations. Right now we stand at 0.4 ms with all those mentioned optims, with ~15/30 VolumeComponent types being culled by our Attribute trick.
    I remember that the parameter.Reset(); trick saved 60% at the time.
    And more recently did the manual Override() benchmark which saved 75%:
    upload_2022-9-29_5-56-5.png


    Regarding UX, properties on VisualEffect component revert to their default values when they are un-overriden. Could be interesting to have that behaviour on VolumeParameters?
     
    tmonestudio likes this.
  5. arttu_p

    arttu_p

    Unity Technologies

    Joined:
    Jan 15, 2021
    Posts:
    21
    I think it is, I've had the same thought. It would just need to be designed carefully so that adding new overrides is still easy in terms of UX. Also ideally I want to solve performance for the worst case (every single component is used) and this wouldn't help with that. But I agree this could be a nice optimization for most projects.

    Yeah, I haven't thought this very far yet. Object/texture parameters tend to be null by default but there may be problems. Using Burst could be an option but I think that's a potential follow-up step - and you're right with every choice I need to make sure adding custom volume components is still easy enough.


    Thank you, this is helpful! I'm surprised the cost is that severe on Switch, I need to make some measurements on that platform. I wonder if you have a lot of custom VolumeComponent types?

    I'm under the impression that current behavior is what we want but perhaps this deserves some more thought...
     
  6. sabojako

    sabojako

    Joined:
    Jul 7, 2017
    Posts:
    48
    I do :D As I said it's a nice framework!
    • 11 custom components
    • 6 URP components (11 are disabled)
    • amounts to 127 params for the ReplaceData()
     
  7. arttu_p

    arttu_p

    Unity Technologies

    Joined:
    Jan 15, 2021
    Posts:
    21
    That's awesome. Thanks for the numbers, I will keep in mind when profiling that we need to expect potentially a lot more components & parameters than what we currently have :)