You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
o3de/Gems/AudioSystem/Code/Source/Engine/FileCacheManager.cpp

909 lines
40 KiB
C++

/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include <FileCacheManager.h>
#include <AzCore/Debug/Profiler.h>
#include <AzCore/IO/FileIO.h>
#include <AzCore/IO/IStreamer.h>
#include <AzCore/IO/Path/Path.h>
#include <AzCore/std/string/conversions.h>
#include <AzCore/std/parallel/binary_semaphore.h>
#include <AzCore/StringFunc/StringFunc.h>
#include <AudioAllocators.h>
#include <AudioInternalInterfaces.h>
#include <IAudioSystemImplementation.h>
#include <SoundCVars.h>
#include <AudioSystem_Traits_Platform.h>
#include <IRenderAuxGeom.h>
namespace Audio
{
extern CAudioLogger g_audioLogger;
///////////////////////////////////////////////////////////////////////////////////////////////
CFileCacheManager::CFileCacheManager(TATLPreloadRequestLookup& preloadRequests)
: m_preloadRequests(preloadRequests)
, m_currentByteTotal(0)
, m_maxByteTotal(0)
{
}
///////////////////////////////////////////////////////////////////////////////////////////////
CFileCacheManager::~CFileCacheManager()
{
}
///////////////////////////////////////////////////////////////////////////////////////////////
void CFileCacheManager::Initialize()
{
AllocateHeap(static_cast<size_t>(Audio::CVars::s_FileCacheManagerMemorySize), "AudioFileCacheManager");
AudioFileCacheManagerNotficationBus::Handler::BusConnect();
}
///////////////////////////////////////////////////////////////////////////////////////////////
void CFileCacheManager::Release()
{
AudioFileCacheManagerNotficationBus::Handler::BusDisconnect();
// Should we check here for any lingering files?
// ATL unloads everything before getting here, but a stop-gap could be safer.
}
///////////////////////////////////////////////////////////////////////////////////////////////
void CFileCacheManager::Update()
{
AZ_PROFILE_FUNCTION(Audio);
AudioFileCacheManagerNotficationBus::ExecuteQueuedEvents();
UpdatePreloadRequestsStatus();
}
///////////////////////////////////////////////////////////////////////////////////////////////
void CFileCacheManager::AllocateHeap(const size_t size, [[maybe_unused]] const char* const usage)
{
if (size > 0)
{
m_maxByteTotal = size << 10;
}
}
///////////////////////////////////////////////////////////////////////////////////////////////
TAudioFileEntryID CFileCacheManager::TryAddFileCacheEntry(const AZ::rapidxml::xml_node<char>* fileXmlNode, const EATLDataScope dataScope, bool autoLoad)
{
TAudioFileEntryID fileEntryId = INVALID_AUDIO_FILE_ENTRY_ID;
SATLAudioFileEntryInfo fileEntryInfo;
EAudioRequestStatus result = eARS_FAILURE;
AudioSystemImplementationRequestBus::BroadcastResult(result, &AudioSystemImplementationRequestBus::Events::ParseAudioFileEntry, fileXmlNode, &fileEntryInfo);
if (result == eARS_SUCCESS)
{
const char* fileLocation = nullptr;
AudioSystemImplementationRequestBus::BroadcastResult(fileLocation, &AudioSystemImplementationRequestBus::Events::GetAudioFileLocation, &fileEntryInfo);
AZStd::string filePath;
AZ::StringFunc::AssetDatabasePath::Join(fileLocation, fileEntryInfo.sFileName, filePath);
auto newAudioFileEntry = azcreate(CATLAudioFileEntry, (filePath.c_str(), fileEntryInfo.pImplData), Audio::AudioSystemAllocator, "ATLAudioFileEntry");
if (newAudioFileEntry)
{
newAudioFileEntry->m_memoryBlockAlignment = fileEntryInfo.nMemoryBlockAlignment;
if (fileEntryInfo.bLocalized)
{
newAudioFileEntry->m_flags.AddFlags(eAFF_LOCALIZED);
}
fileEntryId = AudioStringToID<TAudioFileEntryID>(newAudioFileEntry->m_filePath.c_str());
auto it = m_audioFileEntries.find(fileEntryId);
if (it == m_audioFileEntries.end())
{
if (!autoLoad)
{
// Can now be ref-counted and therefore manually unloaded.
newAudioFileEntry->m_flags.AddFlags(eAFF_USE_COUNTED);
}
newAudioFileEntry->m_dataScope = dataScope;
AZStd::to_lower(newAudioFileEntry->m_filePath.begin(), newAudioFileEntry->m_filePath.end());
auto fileIO = AZ::IO::FileIOBase::GetInstance();
if (AZ::u64 fileSize = 0;
fileIO->Size(newAudioFileEntry->m_filePath.c_str(), fileSize) && fileSize != 0)
{
newAudioFileEntry->m_fileSize = fileSize;
newAudioFileEntry->m_flags.ClearFlags(eAFF_NOTFOUND);
}
m_audioFileEntries[fileEntryId] = newAudioFileEntry;
}
else
{
if (autoLoad && it->second->m_flags.AreAnyFlagsActive(eAFF_USE_COUNTED))
{
// This file entry is upgraded from "manual loading" to "auto loading" but needs a reset to "manual loading" again!
it->second->m_flags.AddFlags(eAFF_NEEDS_RESET_TO_MANUAL_LOADING);
it->second->m_flags.ClearFlags(eAFF_USE_COUNTED);
g_audioLogger.Log(eALT_COMMENT, "Upgraded file entry from 'Manual' to 'Auto' loading: %s", it->second->m_filePath.c_str());
}
// Entry already exists, free the memory!
AudioSystemImplementationRequestBus::Broadcast(&AudioSystemImplementationRequestBus::Events::DeleteAudioFileEntryData, newAudioFileEntry->m_implData);
azdestroy(newAudioFileEntry, Audio::AudioSystemAllocator);
}
}
}
return fileEntryId;
}
///////////////////////////////////////////////////////////////////////////////////////////////
bool CFileCacheManager::TryRemoveFileCacheEntry(const TAudioFileEntryID audioFileID, const EATLDataScope dataScope)
{
bool success = false;
const TAudioFileEntries::iterator iter(m_audioFileEntries.find(audioFileID));
if (iter != m_audioFileEntries.end())
{
CATLAudioFileEntry* const audioFileEntry = iter->second;
if (audioFileEntry->m_dataScope == dataScope)
{
UncacheFileCacheEntryInternal(audioFileEntry, true, true);
AudioSystemImplementationRequestBus::Broadcast(&AudioSystemImplementationRequestBus::Events::DeleteAudioFileEntryData, audioFileEntry->m_implData);
azdestroy(audioFileEntry, Audio::AudioSystemAllocator);
m_audioFileEntries.erase(iter);
}
else if (dataScope == eADS_LEVEL_SPECIFIC && audioFileEntry->m_flags.AreAnyFlagsActive(eAFF_NEEDS_RESET_TO_MANUAL_LOADING))
{
audioFileEntry->m_flags.AddFlags(eAFF_USE_COUNTED);
audioFileEntry->m_flags.ClearFlags(eAFF_NEEDS_RESET_TO_MANUAL_LOADING);
g_audioLogger.Log(eALT_COMMENT, "Downgraded file entry from 'Auto' to 'Manual' loading: %s", audioFileEntry->m_filePath.c_str());
}
}
return success;
}
///////////////////////////////////////////////////////////////////////////////////////////////
void CFileCacheManager::UpdateLocalizedFileCacheEntries()
{
for (auto& audioFileEntryPair : m_audioFileEntries)
{
CATLAudioFileEntry* const audioFileEntry = audioFileEntryPair.second;
if (audioFileEntry && audioFileEntry->m_flags.AreAnyFlagsActive(eAFF_LOCALIZED))
{
if (audioFileEntry->m_flags.AreAnyFlagsActive(eAFF_CACHED | eAFF_LOADING))
{
// The file needs to be unloaded first.
const size_t useCount = audioFileEntry->m_useCount;
audioFileEntry->m_useCount = 0;// Needed to uncache without an error.
UncacheFile(audioFileEntry);
UpdateLocalizedFileEntryData(audioFileEntry);
TryCacheFileCacheEntryInternal(audioFileEntry, audioFileEntryPair.first, true, true, useCount);
}
else
{
// The file is not cached or loading, it is safe to update the corresponding CATLAudioFileEntry data.
UpdateLocalizedFileEntryData(audioFileEntry);
}
}
}
}
///////////////////////////////////////////////////////////////////////////////////////////////
EAudioRequestStatus CFileCacheManager::TryLoadRequest(const TAudioPreloadRequestID preloadRequestID, const bool loadSynchronously, const bool autoLoadOnly)
{
bool fullSuccess = false;
bool fullFailure = true;
auto itPreload = m_preloadRequests.find(preloadRequestID);
if (itPreload != m_preloadRequests.end())
{
CATLPreloadRequest* const preloadRequest = itPreload->second;
if (!preloadRequest->m_cFileEntryIDs.empty() && (!autoLoadOnly || (autoLoadOnly && preloadRequest->m_bAutoLoad)))
{
fullSuccess = true;
for (auto fileId : preloadRequest->m_cFileEntryIDs)
{
auto itFileEntry = m_audioFileEntries.find(fileId);
if (itFileEntry != m_audioFileEntries.end())
{
const bool tempResult = TryCacheFileCacheEntryInternal(itFileEntry->second, fileId, loadSynchronously);
fullSuccess = (fullSuccess && tempResult);
fullFailure = (fullFailure && !tempResult);
}
}
}
if (fullSuccess && preloadRequest->m_allLoaded)
{
// Notify to handlers that the preload is already loaded/cached.
AudioPreloadNotificationBus::Event(preloadRequestID, &AudioPreloadNotificationBus::Events::OnAudioPreloadCached);
}
}
return (fullSuccess ? eARS_SUCCESS : (fullFailure ? eARS_FAILURE : eARS_PARTIAL_SUCCESS));
}
///////////////////////////////////////////////////////////////////////////////////////////////
EAudioRequestStatus CFileCacheManager::TryUnloadRequest(const TAudioPreloadRequestID preloadRequestID)
{
bool fullSuccess = false;
bool fullFailure = true;
auto itPreload = m_preloadRequests.find(preloadRequestID);
if (itPreload != m_preloadRequests.end())
{
CATLPreloadRequest* const preloadRequest = itPreload->second;
if (!preloadRequest->m_cFileEntryIDs.empty())
{
fullSuccess = true;
for (auto fileId : preloadRequest->m_cFileEntryIDs)
{
auto itFileEntry = m_audioFileEntries.find(fileId);
if (itFileEntry != m_audioFileEntries.end())
{
const bool tempResult = UncacheFileCacheEntryInternal(itFileEntry->second, true);
fullSuccess = (fullSuccess && tempResult);
fullFailure = (fullFailure && !tempResult);
}
}
}
if (fullSuccess && !preloadRequest->m_allLoaded)
{
// Notify to handlers the the preload is already unloaded.
AudioPreloadNotificationBus::Event(preloadRequestID, &AudioPreloadNotificationBus::Events::OnAudioPreloadUncached);
}
}
return (fullSuccess ? eARS_SUCCESS : (fullFailure ? eARS_FAILURE : eARS_PARTIAL_SUCCESS));
}
///////////////////////////////////////////////////////////////////////////////////////////////
EAudioRequestStatus CFileCacheManager::UnloadDataByScope(const EATLDataScope dataScope)
{
for (auto it = m_audioFileEntries.begin(); it != m_audioFileEntries.end(); )
{
CATLAudioFileEntry* const audioFileEntry = it->second;
if (audioFileEntry && audioFileEntry->m_dataScope == dataScope)
{
if (UncacheFileCacheEntryInternal(audioFileEntry, true, true))
{
it = m_audioFileEntries.erase(it);
continue;
}
}
++it;
}
return eARS_SUCCESS;
}
///////////////////////////////////////////////////////////////////////////////////////////////
bool CFileCacheManager::UncacheFileCacheEntryInternal(CATLAudioFileEntry* const audioFileEntry, const bool now, const bool ignoreUsedCount /* = false */)
{
bool success = false;
// In any case decrement the used count.
if (audioFileEntry->m_useCount > 0)
{
--audioFileEntry->m_useCount;
}
if (audioFileEntry->m_useCount < 1 || ignoreUsedCount)
{
// Must be cached to proceed.
if (audioFileEntry->m_flags.AreAnyFlagsActive(eAFF_CACHED))
{
// Only "use-counted" files can become removable!
if (audioFileEntry->m_flags.AreAnyFlagsActive(eAFF_USE_COUNTED))
{
audioFileEntry->m_flags.AddFlags(eAFF_REMOVABLE);
}
if (now || ignoreUsedCount)
{
UncacheFile(audioFileEntry);
}
}
else if (audioFileEntry->m_flags.AreAnyFlagsActive(eAFF_LOADING | eAFF_MEMALLOCFAIL))
{
g_audioLogger.Log(eALT_ALWAYS, "FileCacheManager - Trying to remove a loading or mem-failed file cache entry '%s'", audioFileEntry->m_filePath.c_str());
// Reset the entry in case it's still loading or was a memory allocation fail.
UncacheFile(audioFileEntry);
}
// The file was either properly uncached, queued for uncache or not cached at all.
success = true;
}
return success;
}
#if !defined(AUDIO_RELEASE)
///////////////////////////////////////////////////////////////////////////////////////////////
void CFileCacheManager::DrawDebugInfo(IRenderAuxGeom& auxGeom, const float posX, const float posY)
{
if (CVars::s_debugDrawOptions.AreAllFlagsActive(DebugDraw::Options::FileCacheInfo))
{
EATLDataScope dataScope = eADS_ALL;
if (CVars::s_fcmDrawOptions.AreAllFlagsActive(FileCacheManagerDebugDraw::Options::Global))
{
dataScope = eADS_GLOBAL;
}
else if (CVars::s_fcmDrawOptions.AreAllFlagsActive(FileCacheManagerDebugDraw::Options::LevelSpecific))
{
dataScope = eADS_LEVEL_SPECIFIC;
}
const auto frameTime = AZStd::chrono::system_clock::now();
const float entryDrawSize = 1.5f;
const float entryStepSize = 15.0f;
float positionY = posY + 20.0f;
float positionX = posX + 20.0f;
float time = 0.0f;
float ratio = 0.0f;
float originalAlpha = 0.7f;
float* color = nullptr;
// The colors.
float white[4] = { 1.0f, 1.0f, 1.0f, originalAlpha };
float cyan[4] = { 0.0f, 1.0f, 1.0f, originalAlpha };
float orange[4] = { 1.0f, 0.5f, 0.0f, originalAlpha };
float green[4] = { 0.0f, 1.0f, 0.0f, originalAlpha };
float red[4] = { 1.0f, 0.0f, 0.0f, originalAlpha };
float redish[4] = { 0.7f, 0.0f, 0.0f, originalAlpha };
float blue[4] = { 0.1f, 0.2f, 0.8f, originalAlpha };
float yellow[4] = { 1.0f, 1.0f, 0.0f, originalAlpha };
float darkish[4] = { 0.3f, 0.3f, 0.3f, originalAlpha };
auxGeom.Draw2dLabel(posX, positionY, 1.6f, orange, false,
"FileCacheManager (%zu of %zu KiB) [Entries: %zu]", m_currentByteTotal >> 10, m_maxByteTotal >> 10, m_audioFileEntries.size());
positionY += 15.0f;
const bool displayAll = CVars::s_fcmDrawOptions.GetRawFlags() == 0;
const bool displayGlobals = CVars::s_fcmDrawOptions.AreAllFlagsActive(FileCacheManagerDebugDraw::Options::Global);
const bool displayLevels = CVars::s_fcmDrawOptions.AreAllFlagsActive(FileCacheManagerDebugDraw::Options::LevelSpecific);
const bool displayUseCounted = CVars::s_fcmDrawOptions.AreAllFlagsActive(FileCacheManagerDebugDraw::Options::UseCounted);
const bool displayLoaded = CVars::s_fcmDrawOptions.AreAllFlagsActive(FileCacheManagerDebugDraw::Options::Loaded);
for (auto& audioFileEntryPair : m_audioFileEntries)
{
CATLAudioFileEntry* const audioFileEntry = audioFileEntryPair.second;
bool isGlobal = (audioFileEntry->m_dataScope == eADS_GLOBAL);
bool isLevel = (audioFileEntry->m_dataScope == eADS_LEVEL_SPECIFIC);
bool isUseCounted = audioFileEntry->m_flags.AreAnyFlagsActive(eAFF_USE_COUNTED);
bool isLoaded = audioFileEntry->m_flags.AreAnyFlagsActive(eAFF_CACHED);
if (displayAll || (displayGlobals && isGlobal) || (displayLevels && isLevel) || (displayUseCounted && isUseCounted) || (displayLoaded && isLoaded))
{
if (audioFileEntry->m_flags.AreAnyFlagsActive(eAFF_LOADING))
{
color = blue;
}
else if (audioFileEntry->m_flags.AreAnyFlagsActive(eAFF_MEMALLOCFAIL))
{
color = red;
}
else if (audioFileEntry->m_flags.AreAnyFlagsActive(eAFF_REMOVABLE))
{
color = green;
}
else if (!audioFileEntry->m_flags.AreAnyFlagsActive(eAFF_CACHED))
{
color = darkish;
}
else if (audioFileEntry->m_flags.AreAnyFlagsActive(eAFF_NOTFOUND))
{
color = redish;
}
else if (isGlobal)
{
color = cyan;
}
else if (isLevel)
{
color = yellow;
}
else // isUseCounted
{
color = white;
}
using duration_sec = AZStd::chrono::duration<float>;
time = AZStd::chrono::duration_cast<duration_sec>(frameTime - audioFileEntry->m_timeCached).count();
ratio = time / 5.0f;
originalAlpha = color[3];
color[3] *= AZ::GetClamp(ratio, 0.2f, 1.0f);
bool kiloBytes = false;
size_t fileSize = audioFileEntry->m_fileSize;
if (fileSize >= 1024)
{
fileSize >>= 10;
kiloBytes = true;
}
// Format: "relative/path/filename.ext (230 KiB) [2]"
auxGeom.Draw2dLabel(positionX, positionY, entryDrawSize, color, false,
"%s (%zu %s) [%zu]",
audioFileEntry->m_filePath.c_str(),
fileSize,
kiloBytes ? "KiB" : "Bytes",
audioFileEntry->m_useCount);
color[3] = originalAlpha;
positionY += entryStepSize;
}
}
}
}
#endif // !AUDIO_RELEASE
///////////////////////////////////////////////////////////////////////////////////////////////
bool CFileCacheManager::DoesRequestFitInternal(const size_t requestSize)
{
// Make sure these unsigned values don't flip around.
AZ_Assert(m_currentByteTotal <= m_maxByteTotal, "FileCacheManager DoesRequestFitInternal - Unsigned wraparound detected!");
bool success = false;
if (requestSize <= (m_maxByteTotal - m_currentByteTotal))
{
// Here the requested size is available without the need of first cleaning up.
success = true;
}
else
{
// Determine how much memory would get freed if all eAFF_REMOVABLE files get thrown out.
// We however skip files that are already queued for unload. The request will get queued up in that case.
size_t possibleMemoryGain = 0;
// Check the single file entries for removability.
for (auto& audioFileEntryPair : m_audioFileEntries)
{
CATLAudioFileEntry* const audioFileEntry = audioFileEntryPair.second;
if (audioFileEntry && audioFileEntry->m_flags.AreAllFlagsActive(eAFF_CACHED | eAFF_REMOVABLE))
{
possibleMemoryGain += audioFileEntry->m_fileSize;
}
}
const size_t maxAvailableSize = (m_maxByteTotal - (m_currentByteTotal - possibleMemoryGain));
if (requestSize <= maxAvailableSize)
{
// Here we need to cleanup first before allowing the new request to be allocated.
TryToUncacheFiles();
// We should only indicate success if there's actually really enough room for the new entry!
success = (m_maxByteTotal - m_currentByteTotal) >= requestSize;
}
}
return success;
}
///////////////////////////////////////////////////////////////////////////////////////////////
void CFileCacheManager::FinishAsyncStreamRequest(AZ::IO::FileRequestHandle request)
{
auto streamer = AZ::Interface<AZ::IO::IStreamer>::Get();
AZ_Assert(streamer, "FileCacheManager - AZ::IO::Streamer is not available.")
// Find the file entry that matches the request handle...
auto fileEntryIter = AZStd::find_if(m_audioFileEntries.begin(), m_audioFileEntries.end(),
[&request] (const AZStd::pair<TAudioFileEntryID, CATLAudioFileEntry*>& data) -> bool
{
return (data.second->m_asyncStreamRequest == request);
}
);
// If found, we finish processing the async file load request...
if (fileEntryIter != m_audioFileEntries.end())
{
void* buffer{};
AZ::u64 numBytesRead{};
[[maybe_unused]] bool result = streamer->GetReadRequestResult(request, buffer, numBytesRead);
AZ_Assert(result, "FileCacheManager - Unable to retrieve read information from the file request. "
"This can happen if the callback was assigned to a request that didn't read.");
CATLAudioFileEntry* audioFileEntry = fileEntryIter->second;
AZ_Assert(audioFileEntry, "FileCacheManager - Audio file entry is null!");
AZ_Assert(buffer == audioFileEntry->m_memoryBlock, "FileCacheManager - The memory buffer doesn't match the file entry memory block!");
FinishCachingFileInternal(audioFileEntry, numBytesRead, streamer->GetRequestStatus(request));
}
}
///////////////////////////////////////////////////////////////////////////////////////////////
bool CFileCacheManager::FinishCachingFileInternal(CATLAudioFileEntry* const audioFileEntry, [[maybe_unused]] AZ::IO::SizeType bytesRead,
AZ::IO::IStreamerTypes::RequestStatus requestState)
{
AZ_PROFILE_FUNCTION(Audio);
bool success = false;
audioFileEntry->m_asyncStreamRequest.reset();
switch (requestState)
{
case AZ::IO::IStreamerTypes::RequestStatus::Completed:
{
AZ_Assert(
bytesRead == audioFileEntry->m_fileSize,
"FileCacheManager - Sync Streamed Read completed, but bytes read does not match file size!");
if (audioFileEntry->m_flags.AreAnyFlagsActive(eAFF_LOADING))
{
audioFileEntry->m_flags.AddFlags(eAFF_CACHED);
audioFileEntry->m_flags.ClearFlags(eAFF_LOADING);
#if !defined(AUDIO_RELEASE)
audioFileEntry->m_timeCached = AZStd::chrono::system_clock::now();
#endif // !AUDIO_RELEASE
SATLAudioFileEntryInfo fileEntryInfo;
fileEntryInfo.nMemoryBlockAlignment = audioFileEntry->m_memoryBlockAlignment;
fileEntryInfo.pFileData = audioFileEntry->m_memoryBlock;
fileEntryInfo.nSize = audioFileEntry->m_fileSize;
fileEntryInfo.pImplData = audioFileEntry->m_implData;
AZ::IO::PathView filePath{ audioFileEntry->m_filePath };
fileEntryInfo.sFileName = filePath.Filename().Native().data();
AudioSystemImplementationRequestBus::Broadcast(&AudioSystemImplementationRequestBus::Events::RegisterInMemoryFile, &fileEntryInfo);
success = true;
g_audioLogger.Log(eALT_COMMENT, "FileCacheManager - File Cached: '%s'\n", fileEntryInfo.sFileName);
}
break;
}
case AZ::IO::IStreamerTypes::RequestStatus::Failed:
{
AZ_Error("FileCacheManager", false, "FileCacheManager - Async file stream '%s' failed during operation!", audioFileEntry->m_filePath.c_str());
UncacheFileCacheEntryInternal(audioFileEntry, true, true);
break;
}
case AZ::IO::IStreamerTypes::RequestStatus::Canceled:
{
g_audioLogger.Log(eALT_COMMENT, "FileCacheManager - Async file stream '%s' was canceled by user!", audioFileEntry->m_filePath.c_str());
UncacheFileCacheEntryInternal(audioFileEntry, true, true);
break;
}
default:
{
break;
}
}
return success;
}
///////////////////////////////////////////////////////////////////////////////////////////////
void CFileCacheManager::UpdatePreloadRequestsStatus()
{
// Run through the list of preload requests and their fileEntryIDs.
// Check the fileEntries for the CACHED flags and accumulate the 'allLoaded' and 'anyLoaded' status of each preload request.
// If the result is different than what is stored on the preload request, update it and send a notification of
// either cached or uncached.
for (auto& preloadPair : m_preloadRequests)
{
CATLPreloadRequest* preloadRequest = preloadPair.second;
bool wasLoaded = preloadRequest->m_allLoaded;
bool allLoaded = !preloadRequest->m_cFileEntryIDs.empty();
bool anyLoaded = false;
for (auto fileId : preloadRequest->m_cFileEntryIDs)
{
bool cached = false;
auto iter = m_audioFileEntries.find(fileId);
if (iter != m_audioFileEntries.end())
{
cached = iter->second->m_flags.AreAnyFlagsActive(eAFF_CACHED);
}
allLoaded = (allLoaded && cached);
anyLoaded = (anyLoaded || cached);
}
if (allLoaded != wasLoaded && allLoaded)
{
// Loaded now...
preloadRequest->m_allLoaded = allLoaded;
AudioPreloadNotificationBus::Event(preloadPair.first, &AudioPreloadNotificationBus::Events::OnAudioPreloadCached);
}
if (anyLoaded != wasLoaded && !anyLoaded)
{
// Unloaded now...
preloadRequest->m_allLoaded = anyLoaded;
AudioPreloadNotificationBus::Event(preloadPair.first, &AudioPreloadNotificationBus::Events::OnAudioPreloadUncached);
}
}
}
///////////////////////////////////////////////////////////////////////////////////////////////
bool CFileCacheManager::AllocateMemoryBlockInternal(CATLAudioFileEntry* const audioFileEntry)
{
AZ_PROFILE_FUNCTION(Audio);
// Must not have valid memory yet.
AZ_Assert(!audioFileEntry->m_memoryBlock, "FileCacheManager AllocateMemoryBlockInternal - Memory appears to be set already!");
audioFileEntry->m_memoryBlock = AZ::AllocatorInstance<AudioBankAllocator>::Get().Allocate(
audioFileEntry->m_fileSize,
audioFileEntry->m_memoryBlockAlignment,
0,
audioFileEntry->m_filePath.c_str(),
__FILE__, __LINE__);
if (!audioFileEntry->m_memoryBlock)
{
// Memory block is either full or too fragmented, let's try to throw everything out that can be removed and allocate again.
TryToUncacheFiles();
// And try again
audioFileEntry->m_memoryBlock = AZ::AllocatorInstance<AudioBankAllocator>::Get().Allocate(
audioFileEntry->m_fileSize,
audioFileEntry->m_memoryBlockAlignment,
0,
audioFileEntry->m_filePath.c_str(),
__FILE__, __LINE__);
}
return (audioFileEntry->m_memoryBlock != nullptr);
}
///////////////////////////////////////////////////////////////////////////////////////////////
void CFileCacheManager::UncacheFile(CATLAudioFileEntry* const audioFileEntry)
{
if (audioFileEntry->m_asyncStreamRequest)
{
auto streamer = AZ::Interface<AZ::IO::IStreamer>::Get();
AZ::IO::FileRequestPtr request = streamer->Cancel(audioFileEntry->m_asyncStreamRequest);
AZStd::binary_semaphore wait;
streamer->SetRequestCompleteCallback(
request,
[&wait](AZ::IO::FileRequestHandle)
{
wait.release();
});
streamer->QueueRequest(request);
wait.acquire();
audioFileEntry->m_asyncStreamRequest.reset();
}
if (audioFileEntry->m_memoryBlock)
{
SATLAudioFileEntryInfo fileEntryInfo;
fileEntryInfo.nMemoryBlockAlignment = audioFileEntry->m_memoryBlockAlignment;
fileEntryInfo.pFileData = audioFileEntry->m_memoryBlock;
fileEntryInfo.nSize = audioFileEntry->m_fileSize;
fileEntryInfo.pImplData = audioFileEntry->m_implData;
AZ::IO::PathView filePath{ audioFileEntry->m_filePath };
fileEntryInfo.sFileName = filePath.Filename().Native().data();
EAudioRequestStatus result = eARS_SUCCESS;
AudioSystemImplementationRequestBus::BroadcastResult(result, &AudioSystemImplementationRequestBus::Events::UnregisterInMemoryFile, &fileEntryInfo);
if (result == eARS_SUCCESS)
{
g_audioLogger.Log(eALT_COMMENT, "FileCacheManager - File Uncached: '%s'\n", fileEntryInfo.sFileName);
}
else
{
g_audioLogger.Log(eALT_COMMENT, "FileCacheManager - Unable to uncache file '%s'\n", fileEntryInfo.sFileName);
return;
}
}
AZ::AllocatorInstance<AudioBankAllocator>::Get().DeAllocate(
audioFileEntry->m_memoryBlock,
audioFileEntry->m_fileSize,
audioFileEntry->m_memoryBlockAlignment
);
audioFileEntry->m_flags.ClearFlags(eAFF_CACHED | eAFF_REMOVABLE);
m_currentByteTotal -= audioFileEntry->m_fileSize;
AZ_Warning("FileCacheManager", audioFileEntry->m_useCount == 0, "Use-count of file '%s' is non-zero while uncaching it! Use Count: %d", audioFileEntry->m_filePath.c_str(), audioFileEntry->m_useCount);
audioFileEntry->m_useCount = 0;
#if !defined(AUDIO_RELEASE)
audioFileEntry->m_timeCached = AZStd::chrono::system_clock::time_point();
#endif // !AUDIO_RELEASE
}
///////////////////////////////////////////////////////////////////////////////////////////////
void CFileCacheManager::TryToUncacheFiles()
{
for (auto& audioFileEntryPair : m_audioFileEntries)
{
CATLAudioFileEntry* const audioFileEntry = audioFileEntryPair.second;
if (audioFileEntry && audioFileEntry->m_flags.AreAllFlagsActive(eAFF_CACHED | eAFF_REMOVABLE))
{
UncacheFileCacheEntryInternal(audioFileEntry, true);
}
}
}
///////////////////////////////////////////////////////////////////////////////////////////////
void CFileCacheManager::UpdateLocalizedFileEntryData(CATLAudioFileEntry* const audioFileEntry)
{
static SATLAudioFileEntryInfo fileEntryInfo;
fileEntryInfo.bLocalized = true;
fileEntryInfo.nSize = 0;
fileEntryInfo.pFileData = nullptr;
fileEntryInfo.nMemoryBlockAlignment = 0;
AZ::IO::FixedMaxPath filePath{ audioFileEntry->m_filePath };
AZStd::string_view fileName{ filePath.Filename().Native() };
fileEntryInfo.pImplData = audioFileEntry->m_implData;
fileEntryInfo.sFileName = fileName.data();
const char* fileLocation = nullptr;
AudioSystemImplementationRequestBus::BroadcastResult(fileLocation, &AudioSystemImplementationRequestBus::Events::GetAudioFileLocation, &fileEntryInfo);
if (fileLocation && fileLocation[0] != '\0')
{
audioFileEntry->m_filePath.assign(fileLocation);
audioFileEntry->m_filePath.append(fileName.data(), fileName.size());
}
else
{
AZ_WarningOnce("FileCacheManager", fileLocation != nullptr, "GetAudioFileLocation returned null when getting a localized file path! Path will not be changed.");
}
AZStd::to_lower(audioFileEntry->m_filePath.begin(), audioFileEntry->m_filePath.end());
AZ::u64 fileSize = 0;
auto fileIO = AZ::IO::FileIOBase::GetInstance();
fileIO->Size(audioFileEntry->m_filePath.c_str(), fileSize);
audioFileEntry->m_fileSize = fileSize;
AZ_Assert(audioFileEntry->m_fileSize != 0, "FileCacheManager - UpdateLocalizedFileEntryData expected file size to be greater than zero!");
}
///////////////////////////////////////////////////////////////////////////////////////////////
bool CFileCacheManager::TryCacheFileCacheEntryInternal(
CATLAudioFileEntry* const audioFileEntry,
[[maybe_unused]] const TAudioFileEntryID fileEntryId,
[[maybe_unused]] const bool loadSynchronously,
const bool overrideUseCount /* = false */,
const size_t useCount /* = 0 */)
{
AZ_PROFILE_FUNCTION(Audio);
bool success = false;
if (!audioFileEntry->m_filePath.empty() && !audioFileEntry->m_flags.AreAnyFlagsActive(eAFF_CACHED | eAFF_LOADING))
{
if (DoesRequestFitInternal(audioFileEntry->m_fileSize) && AllocateMemoryBlockInternal(audioFileEntry))
{
auto streamer = AZ::Interface<AZ::IO::IStreamer>::Get();
AZ_Assert(streamer, "FileCacheManager - Streamer should be ready!");
audioFileEntry->m_flags.AddFlags(eAFF_LOADING);
if (loadSynchronously)
{
AZ::IO::FileRequestPtr request = streamer->Read(
audioFileEntry->m_filePath.c_str(),
audioFileEntry->m_memoryBlock,
audioFileEntry->m_fileSize,
audioFileEntry->m_fileSize,
AZ::IO::IStreamerTypes::s_deadlineNow,
AZ::IO::IStreamerTypes::s_priorityHigh);
AZStd::binary_semaphore wait;
streamer->SetRequestCompleteCallback(
request,
[&wait](AZ::IO::FileRequestHandle)
{
wait.release();
});
streamer->QueueRequest(request);
wait.acquire();
AZ::IO::IStreamerTypes::RequestStatus status = streamer->GetRequestStatus(request);
if (FinishCachingFileInternal(audioFileEntry, audioFileEntry->m_fileSize, status))
{
m_currentByteTotal += audioFileEntry->m_fileSize;
success = true;
}
}
else
{
streamer->Read(
audioFileEntry->m_asyncStreamRequest,
audioFileEntry->m_filePath.c_str(),
audioFileEntry->m_memoryBlock,
audioFileEntry->m_fileSize,
audioFileEntry->m_fileSize,
AZ::IO::IStreamerTypes::s_noDeadline,
AZ::IO::IStreamerTypes::s_priorityHigh);
streamer->SetRequestCompleteCallback(
audioFileEntry->m_asyncStreamRequest,
[](AZ::IO::FileRequestHandle request)
{
AZ_PROFILE_FUNCTION(Audio);
AudioFileCacheManagerNotficationBus::QueueBroadcast(
&AudioFileCacheManagerNotficationBus::Events::FinishAsyncStreamRequest,
request);
});
streamer->QueueRequest(audioFileEntry->m_asyncStreamRequest);
// Increase total size even though async request is processing...
m_currentByteTotal += audioFileEntry->m_fileSize;
success = true;
}
}
else
{
// Cannot have a valid memory block!
AZ_Assert(audioFileEntry->m_memoryBlock == nullptr,
"FileCacheManager - Memory block should be null after memory allocation failure!");
// This unfortunately is a total memory allocation fail.
audioFileEntry->m_flags.AddFlags(eAFF_MEMALLOCFAIL);
// The user should be made aware of it.
g_audioLogger.Log(eALT_ERROR, "FileCacheManager - Could not cache '%s' - out of memory or fragmented memory!", audioFileEntry->m_filePath.c_str());
}
}
else if (audioFileEntry->m_flags.AreAnyFlagsActive(eAFF_CACHED | eAFF_LOADING))
{
g_audioLogger.Log(eALT_COMMENT, "FileCacheManager - Skipping '%s' - it's either already loaded or currently loading!", audioFileEntry->m_filePath.c_str());
success = true;
}
else if (audioFileEntry->m_flags.AreAnyFlagsActive(eAFF_NOTFOUND))
{
g_audioLogger.Log(eALT_WARNING, "FileCacheManager - Could not cache '%s' - file was not found at that location!", audioFileEntry->m_filePath.c_str());
}
// Increment the used count on manually-loaded files.
if (audioFileEntry->m_flags.AreAnyFlagsActive(eAFF_USE_COUNTED) && audioFileEntry->m_flags.AreAnyFlagsActive(eAFF_CACHED | eAFF_LOADING))
{
if (overrideUseCount)
{
audioFileEntry->m_useCount = useCount;
}
else
{
++audioFileEntry->m_useCount;
}
// Make sure to handle the eAFCS_REMOVABLE flag according to the m_useCount count.
if (audioFileEntry->m_useCount != 0)
{
audioFileEntry->m_flags.ClearFlags(eAFF_REMOVABLE);
}
else
{
audioFileEntry->m_flags.AddFlags(eAFF_REMOVABLE);
}
}
return success;
}
} // namespace Audio