Version
menu_open
Wwise SDK 2022.1.11
Optimizing CPU usage

Audio Rendering Thread

By default, the Wwise Sound Engine does all its command processing and audio rendering in a dedicated thread named AK::EventManager, controlled by the AkPlatformInitSettings::threadLEngine parameters. Calling AK::SoundEngine::RenderAudio signals the end of a game frame and allows the thread to consume all API commands received since the previous call to RenderAudio.

Setting AkInitSettings::bUseLEngineThread to false disables this thread and causes RenderAudio to run synchronously: processing commands and, if needed, rendering audio. The actual rate of audio output remains controlled by the audio endpoint. If the RenderAudio call interval is shorter than the buffer period determined by AkInitSettings::uNumSamplesPerFrame and the output sample rate, some calls to RenderAudio will skip the audio rendering portion. Conversely, if the RenderAudio call interval is longer than the output buffer period, RenderAudio may process more than one buffer at a time, causing a CPU usage spike, and may eventually cause the audio to stutter.

Enabling offline rendering disables asynchronous command processing and audio rendering from the audio thread. The amount of audio rendered per call to RenderAudio is determined by a positive non-zero value sent to AK::SoundEngine::SetOfflineRenderingFrameTime. A zero or negative value will force RenderAudio to process precisely one audio buffer.

Caution: With the audio rendering thread disabled or with offline rendering enabled, synchronous AK::SoundEngine::LoadBank and AK::SoundEngine::UnloadBank API calls must not be made from the same thread as the caller of RenderAudio. This is because these calls may block until an audio buffer is rendered to complete Stop operations and free SoundBank media, which won't happen without a concurrent call to RenderAudio.

On Microsoft platforms, due to the use of a single-threaded apartment (STA) concurrency model, CoInitializeEx() must be called from the same thread that calls AK::SoundEngine::RenderAudio when setting AkInitSettings::bUseLEngineThread to false.

Using Query API Functions

Certain AK::SoundEngine::Query functions can cause CPU spikes. To minimize wasted CPU time and ensure optimal performance, we recommend that you follow these guidelines:

  • Use the Query functions in development builds and avoid using them in production builds.
  • If it is necessary to use Query functions, use them inside SoundEngine global callbacks (see AkGlobalCallbackLocation). For example, if you have to read RTPC values, put the code in AkGlobalCallbackLocation_BeginRender or AkGlobalCallbackLocation_EndRender.

Leveraging the Job Manager for Concurrent Execution of Audio Rendering Jobs

By default, the Wwise Sound Engine executes its various audio rendering tasks, or "jobs", on the audio rendering thread in a sequential fashion. These jobs include, but are not limited to, bus and voice processing tasks.

Concurrent execution of these audio jobs can be enabled by specifying a callback that allows the Wwise Sound Engine to request CPU time on game-managed threads. When the game provides an implementation for this callback via AkJobMgrSettings::fnRequestJobWorker, concurrent execution is enabled in the Wwise Sound Engine.

Note: When enabling concurrent job execution, some AK::SoundEngine callbacks will be generated from concurrent job worker threads. Additionally, some plug-ins may not be compatible with concurrent job execution.

To understand how the Sound Engine's Job Manager works, it is important to learn the difference between two important callbacks:

  1. The worker request function is defined by the game engine and is called by the Wwise Sound Engine.
  2. The worker function is defined by the Wwise Sound Engine and is called by the game engine.
Note: The Wwise Sound Engine expects exactly one call to the worker function for each worker request. For example, if the worker request function is called once to request three workers of type AkJobType_AudioProcessing, and then called again to request two workers of type AkJobType_Generic, then the Sound Engine will expect the game to call the worker function three times with AkJobType_AudioProcessing, and two times with AkJobType_Generic. The calls may be issued in any order, sequentially or concurrently from different threads.

When the worker request function is defined, the audio rendering thread will behave as follows:

Figure: Sequence Diagram of Worker Execution
  1. Audio rendering thread identifies jobs that can be performed on multiple threads, such as voice or bus processing.
  2. Audio rendering thread calls the game's worker request function to request CPU time on another thread. In this step, the game engine receives the address of the worker function to call.
  3. The game engine schedules the requests on its thread pool. A number of worker threads are woken up accordingly.
  4. The game's worker threads call the worker functions provided by the Sound Engine in step 2.
  5. Each call to the worker function executes at least one audio rendering job, but can execute more if they are available.
  6. The audio rendering thread resumes processing as soon as all required jobs have finished executing.

This process can repeat multiple times throughout one audio rendering pass, and the audio rendering thread will attempt to pipeline work as much as possible. For example, when rendering the bus graph, processing time for a given bus will be requested as soon as all inputs have finished processing, and may run independently of other busses. In this way, the Job Manager will always attempt to maximize throughput.

Note: The worker request function can be called from any thread executing Sound Engine code, and must be implemented in a thread-safe manner.

Integrating with a Game Engine's Existing Job Scheduler

The Wwise Sound Engine's Job Manager is designed to work in tandem with existing job schedulers to achieve co-operative multi-tasking. Game engines that already have a job scheduler should implement the worker request function in a way that schedules the execution of the worker function within their existing job system.

When calling the worker function, the game engine's job scheduler can specify a timeout in microseconds. This is to prevent the Sound Engine from taking too much CPU time on the calling thread. Past this timeout, the worker function will stop and request an additional worker if more jobs were available for execution. This allows the execution of other, possibly higher-priority game engine work on this thread.

Note: Care must be taken when delaying the execution of the worker function or limiting the execution time of Sound Engine work, as this can lead to voice starvation. When integrating Sound Engine jobs into an existing job scheduler, it is recommended to treat audio rendering jobs as high-priority work.

For game engines that don't already have a job scheduler, a sample implementation of such a scheduler is provided in the SDK samples under SDK/samples/SoundEngine/Common/AkJobWorkerMgr.[h,cpp]. This sample provides a great starting point for concurrent execution of audio rendering jobs. Additionally, IntegrationDemo provides code demonstrating how this sample implementation can be integrated in an actual end-user application.

Best Practices When Using the Job Manager

Here are a few recommendations on how best to utilize the Job Manager.

  1. Do not create threads on-demand when new worker requests come in. Creating new threads is an expensive operation on most systems. Instead, pre-allocate a pre-determined number of threads before the Sound Engine is initialized, and distribute worker requests among those threads.
  2. Do not use more worker threads than are necessary to achieve sufficient parallelization of Sound Engine jobs. In some cases, the overhead of requesting workers may result in lost CPU time that would be more effectively used for other tasks. As well, increasing the number of workers may increase the total amount of memory that the Sound Engine requests, due to the use of thread-local caches in memory allocators or other systems in the Sound Engine.
  3. On game-engine worker threads that support execution of jobs, it is recommended to call AK::MemoryMgr::InitForThread and AK::MemoryMgr::TermForThread when initializing and terminating the threads, in order to ensure proper initialization and termination of thread-local memory resources. It is also recommended to call AK::MemoryMgr::TrimForThread when entering a period of inactivity after running the worker function, in order to free up any thread-local memory resources which may not be utilized again in the near future.
  4. On platforms where the number of CPU cores is fixed and known in advance, set worker thread affinities so each thread stays on the same CPU core. Making sure that audio work does not move across CPU cores during execution is desirable to avoid refreshing CPU caches when a thread migrates to a different core.
  5. On systems with multiple Clusters or Core Complexes (CCX), set thread affinities so worker threads and the audio rendering thread all run on the same CCX in order to improve cache coherency across cores.
  6. On systems with Simultaneous Multi-Threading (SMT), set thread affinities so worker threads run on separate physical cores, instead of sharing the same core, to reduce competition on CPU resources.
  7. If you wish to change the maximum number of workers that the sound engine may request without reinitializing the sound engine, you can do so with AK::SoundEngine::SetJobMgrMaxActiveWorkers. This can be useful to dynamically respond to changes in your title's operating conditions, or to more easily experiment and profile different configurations for multi-threaded work.

The recommendations above must be weighed against the other needs of the game. The Job Manager remains a good way to increase overall throughput of the Sound Engine even if some of these recommendations are not followed.

Optimizing Job Manager Memory Usage

Running out of memory when allocating a job is considered a critical failure, as the logical flow of audio rendering would be interrupted and unable to resume. This would lead to undefined results and leaks of resources.

To prevent this, the Job Manager allocates slabs of memory that will be kept around and re-used until the Sound Engine is terminated. Most of these slabs are pre-allocated during Sound Engine initialization, but others may be allocated as needed.

You can control the size of the memory slabs, as well as how many are pre-allocated at initialization time, via AkInitSettings::settingsJobManager.

Note: If AK::MemoryMgr::Malloc fails to allocate a new memory slab during rendering, the Job Manager will keep re-trying the allocation until it succeeds. If you observe the Sound Engine hang during low-memory situations, increase the number of pre-allocated slabs in the initialization settings.

Was this page helpful?

Need Support?

Questions? Problems? Need more info? Contact us, and we can help!

Visit our Support page

Tell us about your project. We're here to help.

Register your project and we'll help you get started with no strings attached!

Get started with Wwise