Files
UnrealEngine/Engine/Source/Runtime/AudioMixer/Private/AudioRenderScheduler.cpp
Brandyn / Techy fcc1b09210 init
2026-04-04 15:40:51 -05:00

411 lines
12 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "AudioRenderScheduler.h"
#include "AudioMixerDevice.h"
#include "AudioMixerTrace.h"
#include "IAudioMixerRenderStep.h"
#include "Logging/StructuredLog.h"
#include "Tasks/Task.h"
DEFINE_LOG_CATEGORY(LogAudioRenderScheduler);
// If MixerDevice is null, don't check the thread. This is used in unit tests.
#define AUDIO_RENDER_SCHEDULER_CHECK_THREAD() check(MixerDevice == nullptr || MixerDevice->IsAudioRenderingThread())
namespace Audio
{
FAudioRenderStepId FAudioRenderStepId::FromTransmitterID(const uint64 TransmitterID)
{
return TransmitterID;
}
FAudioRenderStepId FAudioRenderStepId::FromAudioBusKey(const FAudioBusKey BusKey)
{
// BusKey.InstanceId is usually -1. We add 1 just to make this equal to the object id in that usual case.
return (uint64(uint32(BusKey.InstanceId + 1)) << 32) | BusKey.ObjectId;
}
FAudioRenderScheduler::FAudioRenderScheduler(FMixerDevice* InMixerDevice) :
MixerDevice(InMixerDevice)
{
#if !WITH_DEV_AUTOMATION_TESTS
// A null mixer device is only allowed in unit tests.
check(MixerDevice != nullptr);
#endif
}
FAudioRenderScheduler::FInternalIndex FAudioRenderScheduler::GetInternalIndex(const FAudioRenderStepId Id)
{
FInternalIndex* IndexInMap = IndexMap.Find(Id);
if (IndexInMap)
{
return *IndexInMap;
}
FInternalIndex Index;
if (FirstFreeIndex != INDEX_NONE)
{
// Pop a step entry off the free list if available
Index = FirstFreeIndex;
FirstFreeIndex = Steps[Index].NextFreeIndex;
}
else
{
// Otherwise grow the steps array
check(Steps.Num() < TNumericLimits<FInternalIndex>::Max());
Steps.AddDefaulted(1);
Index = Steps.Num() - 1;
}
Steps[Index].StepId = Id;
IndexMap.Add(Id, Index);
ActiveSteps.Add(Index);
return Index;
}
void FAudioRenderScheduler::RemoveReference(FAudioRenderScheduler::FInternalIndex Index)
{
FStepEntry& Entry = Steps[Index];
check(Entry.ReferenceCount >= 1 && Entry.StepId.IsValid());
if (--Entry.ReferenceCount == 0)
{
IndexMap.Remove(Entry.StepId);
ActiveSteps.Remove(Index);
Entry = FStepEntry{};
Entry.NextFreeIndex = FirstFreeIndex;
FirstFreeIndex = Index;
}
}
void FAudioRenderScheduler::AddStep(const FAudioRenderStepId Id, IAudioMixerRenderStep* Step)
{
AUDIO_RENDER_SCHEDULER_CHECK_THREAD();
check(Id.IsValid() && Step != nullptr);
FInternalIndex Index = GetInternalIndex(Id);
FStepEntry& Entry = Steps[Index];
if (ensureMsgf(Entry.StepInterface == nullptr, TEXT("Duplicate render step added to audio render scheduler")))
{
UE_LOGFMT(LogAudioRenderScheduler, Verbose, "Adding render step '{Name}' with id {Id}", Step->GetRenderStepName(), Id.GetRawValue());
Entry.StepInterface = Step;
Entry.ReferenceCount++;
bDirty = true;
}
}
void FAudioRenderScheduler::RemoveStep(const FAudioRenderStepId Id)
{
AUDIO_RENDER_SCHEDULER_CHECK_THREAD();
check(Id.IsValid());
FInternalIndex Index = GetInternalIndex(Id);
FStepEntry& Entry = Steps[Index];
if (ensureMsgf(Entry.StepInterface != nullptr, TEXT("Unknown render step removed from audio render scheduler")))
{
UE_LOGFMT(LogAudioRenderScheduler, Verbose, "Removing render step '{Name}' with id {Id}", Entry.StepInterface->GetRenderStepName(), Id.GetRawValue());
Entry.StepInterface = nullptr;
RemoveReference(Index);
bDirty = true;
}
}
void FAudioRenderScheduler::AddDependency(const FAudioRenderStepId FirstStep, const FAudioRenderStepId SecondStep)
{
AUDIO_RENDER_SCHEDULER_CHECK_THREAD();
check(FirstStep.IsValid() && SecondStep.IsValid());
if (FirstStep == SecondStep)
{
// We allow this, but ignore it because this dependency will always need to be broken anyway.
return;
}
const FInternalIndex FirstIndex = GetInternalIndex(FirstStep);
const FInternalIndex SecondIndex = GetInternalIndex(SecondStep);
Steps[SecondIndex].Prerequisites.Add(FirstIndex);
Steps[SecondIndex].ReferenceCount++;
Steps[FirstIndex].ReferenceCount++;
bDirty = true;
}
void FAudioRenderScheduler::RemoveDependency(const FAudioRenderStepId FirstStep, const FAudioRenderStepId SecondStep)
{
AUDIO_RENDER_SCHEDULER_CHECK_THREAD();
check(FirstStep.IsValid() && SecondStep.IsValid());
if (FirstStep == SecondStep)
{
return;
}
const FInternalIndex FirstIndex = GetInternalIndex(FirstStep);
const FInternalIndex SecondIndex = GetInternalIndex(SecondStep);
if (ensureMsgf(Steps[SecondIndex].Prerequisites.RemoveSingleSwap(FirstIndex), TEXT("Unknown dependency removed from audio render scheduler")))
{
RemoveReference(FirstIndex);
RemoveReference(SecondIndex);
}
bDirty = true;
}
void FAudioRenderScheduler::RenderBlock(const bool bSingleThreaded)
{
AUDIO_RENDER_SCHEDULER_CHECK_THREAD();
AUDIO_MIXER_TRACE_CPUPROFILER_EVENT_SCOPE(FAudioRenderScheduler::RenderBlock);
if (bSingleThreaded)
{
ScheduleAll(true);
}
else
{
UE::Tasks::FTask Task = UE::Tasks::Launch(
TEXT("Scheduled audio render steps"),
[this]() { ScheduleAll(false); },
LowLevelTasks::ETaskPriority::High);
Task.Wait();
}
}
void FAudioRenderScheduler::ScheduleAll(const bool bSingleThreaded)
{
AUDIO_MIXER_TRACE_CPUPROFILER_EVENT_SCOPE(FAudioRenderScheduler::ScheduleAll);
TArray<FInternalIndex> OrderedSteps;
OrderedSteps.Reserve(ActiveSteps.Num());
TArray<UE::Tasks::FTask> PrerequisiteHandles;
// If we need multiple passes, we avoid having to start from the beginning each time.
int FirstPendingStepIndex = 0;
// Check if any of the previously broken links need to be broken again
CheckBrokenLinks();
while (OrderedSteps.Num() < ActiveSteps.Num())
{
bool bStuck = true;
// Go through the steps array, launching any steps that have no prerequisites or prerequisites that have already been launched.
for (int ActiveStepIndex = FirstPendingStepIndex; ActiveStepIndex < ActiveSteps.Num(); ++ActiveStepIndex)
{
FStepEntry& Entry = Steps[ActiveSteps[ActiveStepIndex]];
if (Entry.bLaunched)
{
if (ActiveStepIndex == FirstPendingStepIndex)
{
++FirstPendingStepIndex;
}
continue;
}
bool bCanLaunch = true;
if (bDirty)
{
for (FInternalIndex PrerequisiteIndex : Entry.Prerequisites)
{
if (!Steps[PrerequisiteIndex].bLaunched)
{
bCanLaunch = false;
break;
}
}
}
if (bCanLaunch)
{
if (bSingleThreaded)
{
if (Entry.StepInterface != nullptr)
{
Entry.StepInterface->DoRenderStep();
}
}
else
{
// Gather prerequisite task handles
PrerequisiteHandles.Reset();
for (FInternalIndex PrerequisiteIndex : Entry.Prerequisites)
{
check(Steps[PrerequisiteIndex].TaskHandle.IsValid());
PrerequisiteHandles.Add(Steps[PrerequisiteIndex].TaskHandle);
}
if (Entry.bCheckBrokenLinks)
{
// If this step was one end of a broken link, we prevent it from running concurrently with the other
// end of the link.
for (const TPair<FInternalIndex, FInternalIndex>& BrokenLink : BrokenLinks)
{
if (BrokenLink.Get<0>() == ActiveSteps[ActiveStepIndex] && Steps[BrokenLink.Get<1>()].bLaunched)
{
PrerequisiteHandles.Add(Steps[BrokenLink.Get<1>()].TaskHandle);
}
if (BrokenLink.Get<1>() == ActiveSteps[ActiveStepIndex] && Steps[BrokenLink.Get<0>()].bLaunched)
{
PrerequisiteHandles.Add(Steps[BrokenLink.Get<0>()].TaskHandle);
}
}
}
auto TaskLambda = [&Entry]()
{
if (Entry.StepInterface != nullptr)
{
Entry.StepInterface->DoRenderStep();
}
};
// Launch the task
Entry.TaskHandle = UE::Tasks::Launch(
Entry.StepInterface ? Entry.StepInterface->GetRenderStepName() : TEXT("Empty audio render step"),
MoveTemp(TaskLambda),
PrerequisiteHandles,
LowLevelTasks::ETaskPriority::High);
UE::Tasks::AddNested(Entry.TaskHandle);
}
OrderedSteps.Add(ActiveSteps[ActiveStepIndex]);
if (ActiveStepIndex == FirstPendingStepIndex)
{
++FirstPendingStepIndex;
}
Entry.bLaunched = true;
bStuck = false;
}
}
if (bStuck)
{
// If we go through the whole list and find no steps that are launchable, it means there must be a cycle that needs to be broken.
BreakCycle(FirstPendingStepIndex);
}
}
// After scheduling everything, restore the broken links
for (TPair<FInternalIndex, FInternalIndex> Link : BrokenLinks)
{
Steps[Link.Get<1>()].Prerequisites.Add(Link.Get<0>());
}
// Reset the transient state for all steps
for (int i = 0; i < Steps.Num(); ++i)
{
FStepEntry& Entry = Steps[i];
Entry.TaskHandle = {};
Entry.bLaunched = false;
Entry.bCheckBrokenLinks = false;
}
// Remember the ordering of steps for next block
ActiveSteps = MoveTemp(OrderedSteps);
// If no changes happen before next block, we can skip lots of the checks.
bDirty = false;
}
void FAudioRenderScheduler::BreakCycle(const int FirstPendingStepIndex)
{
FInternalIndex CurrentStep = ActiveSteps[FirstPendingStepIndex];
TArray<FInternalIndex> VisitedIndices;
VisitedIndices.Add(CurrentStep);
while (true)
{
FStepEntry& Entry = Steps[CurrentStep];
check(!Entry.bLaunched);
FInternalIndex PrerequisiteStep = INDEX_NONE;
// Find a pending step that is a dependency of CurrentStep
for (FInternalIndex PrerequisiteIndex : Entry.Prerequisites)
{
if (!Steps[PrerequisiteIndex].bLaunched)
{
PrerequisiteStep = PrerequisiteIndex;
break;
}
}
// If this fails, BreakCycle() was called unnecessarily (there was a step with no pending prerequisites.)
check(PrerequisiteStep != INDEX_NONE);
if (VisitedIndices.Contains(PrerequisiteStep))
{
// The link between CurrentStep and PrerequisiteStep is part of a cycle, so we'll break it.
UE_LOGFMT(LogAudioRenderScheduler, Verbose, "Breaking cycle between step {Id1} and step {Id2}", Steps[CurrentStep].StepId.GetRawValue(), Steps[PrerequisiteStep].StepId.GetRawValue());
BreakLink(PrerequisiteStep, CurrentStep);
BrokenLinks.Add(TPair<FInternalIndex, FInternalIndex>{PrerequisiteStep, CurrentStep});
return;
}
CurrentStep = PrerequisiteStep;
VisitedIndices.Add(CurrentStep);
}
// It shouldn't be possible to get here; each time through the loop we extend VisitedIndices and check for duplicates.
checkNoEntry();
}
void FAudioRenderScheduler::CheckBrokenLinks()
{
for (int LinkIndex = 0; LinkIndex < BrokenLinks.Num(); )
{
const TPair<FInternalIndex, FInternalIndex>& Link = BrokenLinks[LinkIndex];
if (!bDirty || (Steps[Link.Get<1>()].Prerequisites.Contains(Link.Get<0>()) && FindIndirectLink(Link.Get<1>(), Link.Get<0>())))
{
// This link is still part of a cycle; break it again and leave it in BrokenLinks
BreakLink(Link.Get<0>(), Link.Get<1>());
++LinkIndex;
}
else
{
// This link is no longer part of a cycle; remove it and continue.
BrokenLinks.RemoveAt(LinkIndex);
}
}
}
void FAudioRenderScheduler::BreakLink(FInternalIndex FirstStep, FInternalIndex SecondStep)
{
Steps[SecondStep].Prerequisites.RemoveSingleSwap(FirstStep);
Steps[FirstStep].bCheckBrokenLinks = true;
Steps[SecondStep].bCheckBrokenLinks = true;
}
bool FAudioRenderScheduler::FindIndirectLink(FInternalIndex FirstStep, FInternalIndex SecondStep)
{
TArray<FInternalIndex> StepsToSkip;
return FindIndirectLinkSkipping(FirstStep, SecondStep, StepsToSkip);
}
bool FAudioRenderScheduler::FindIndirectLinkSkipping(FInternalIndex FirstStep, FInternalIndex SecondStep, TArray<FInternalIndex>& StepsToSkip)
{
StepsToSkip.Add(SecondStep);
for (FInternalIndex Prereq : Steps[SecondStep].Prerequisites)
{
if (Prereq == FirstStep)
{
return true;
}
if (StepsToSkip.Contains(Prereq))
{
continue;
}
if (FindIndirectLinkSkipping(FirstStep, Prereq, StepsToSkip))
{
return true;
}
}
return false;
}
}