ValveSoftware / steam-audio

Steam Audio
https://valvesoftware.github.io/steam-audio/
Apache License 2.0
2.26k stars 158 forks source link

[C API] iplSourceGetOutputs - Safe to access while simulation is running in another thread? #256

Open saturnian-tides opened 1 year ago

saturnian-tides commented 1 year ago

Hi @lakulish , I've seen some notes in the documentation that many aspects of the API are thread-safe, and I was curious about this. I have a main game/physics thread that runs the Direct simulation and a worker thread that runs the Reflections/Pathing simulation in parallel (reflections/pathing simulation work is submitted by the main thread). This setup is very similar to the provided integrations to Unreal/Unity. However, I need to get the simulation output for Reflections (the reflection effect params, including the embedded impulse response) to the audio rendering thread. Is it safe to execute iplSourceGetOutputs in this audio thread while the simulation is running in the other worker thread - ie. are their guarantees that the output of iplSourceGetOutputs will be protected until the simulation finishes?

If not I suppose I can deepcopy the reflection effect params somewhere for the audio thread to pick up (I was doing this for direct effect) but it's not clear to me how to deepcopy the .ir (impusle response) component. Is the size just numChannels*irSize*sizeof(float)?

lakulish commented 1 year ago

It is not safe to call iplSourceGetOutputs with IPL_SIMULATIONFLAGS_REFLECTIONS while the reflection simulation is still running. Our Unity/Unreal integrations call this from the main update thread, after the reflection simulation completes. The output data is then passed to the audio thread via the Steam Audio Source component (you should be able to do something similar).

As for what this data is, the struct just contains an opaque pointer in the .ir data member. As long as you can atomically copy the pointer over to the audio thread, you should be fine. (We may, in the future, provide additional API to inspect and manipulate the impulse response data, but there are no concrete plans at this time.) In fact, if the .ir pointer doesn't change, you only need to copy it the first time, subsequently Steam Audio will internally handle synchronization between the new data written by the simulation thread and the data read by the audio thread.

Let me know if this helps, and feel free to reach out with any other questions.

saturnian-tides commented 1 year ago

Thanks, that cleared up a lot. It's good to know that the audio thread can access the .ir data member and not worry about synchronization issues so long as the handle is copied atomically at least once. About the output data being passed to the audio thread in the Unreal/Unity integrations - I was looking at these examples and I'm not quite familiar with the internals of these engines, but it seems that the data is passed via an audio callback (which I assume runs in the audio thread) as an IPLSource and the outputs are accessed later during the application of the Effect (which I also assumed was in the audio thread). From Unity for example:

UNITY_AUDIODSP_RESULT UNITY_AUDIODSP_CALLBACK setParam(UnityAudioEffectState* state,
                                                       int index,
                                                       float value)
{
...
    case SIMULATION_OUTPUTS_HANDLE:
        if (gSourceManager)
        {
            setSource(state, gSourceManager->getSource(static_cast<int>(value)));
        }
        break;
    }

    return UNITY_AUDIODSP_OK;
}

UNITY_AUDIODSP_RESULT UNITY_AUDIODSP_CALLBACK process(UnityAudioEffectState* state,
                                                      float* in,
                                                      float* out,
                                                      unsigned int numSamples,
                                                      int numChannelsIn,
                                                      int numChannelsOut)
{
...

    if (effect->simulationSource[0])
    {
        IPLSimulationOutputs simulationOutputs{};
        iplSourceGetOutputs(effect->simulationSource[0], static_cast<IPLSimulationFlags>(IPL_SIMULATIONFLAGS_REFLECTIONS | IPL_SIMULATIONFLAGS_PATHING), &simulationOutputs);

Based on your comments I suppose at least the iplSourceGetOutputs function is actually being called in the main update thread rather than the audio thread.

lakulish commented 1 year ago

Actually, in the Unity integration example above, the iplSourceGetOutputs function is called from the audio thread. (The process function is called by Unity's audio engine in the audio thread.)

Basically, the C++ code assumes that the C# code will call AudioSource.SetSpatializerFloat or whatever in the main thread, when a simulation is not running, which will end up calling the setParam function (still in the main thread). The IPLSimulationOutputs struct is then retrieved in the audio thread so that various parameters can be passed to the iplXyzEffectApply functions. Synchronization between the main thread and audio thread here is achieved with a double buffer (simulationSource[0] and simulationSource[1]) and an std::atomic<bool> flag.