diff options
| author | FluorescentCIAAfricanAmerican <[email protected]> | 2020-04-22 12:56:21 -0400 |
|---|---|---|
| committer | FluorescentCIAAfricanAmerican <[email protected]> | 2020-04-22 12:56:21 -0400 |
| commit | 3bf9df6b2785fa6d951086978a3e66f49427166a (patch) | |
| tree | 2c0f1f0c63c4832882bc93814ebd2c2b1c6224e5 /filesystem/QueuedLoader.cpp | |
| download | archived-source-engine-2018-hl2-src-master.tar.xz archived-source-engine-2018-hl2-src-master.zip | |
Diffstat (limited to 'filesystem/QueuedLoader.cpp')
| -rw-r--r-- | filesystem/QueuedLoader.cpp | 1978 |
1 files changed, 1978 insertions, 0 deletions
diff --git a/filesystem/QueuedLoader.cpp b/filesystem/QueuedLoader.cpp new file mode 100644 index 0000000..15ab20f --- /dev/null +++ b/filesystem/QueuedLoader.cpp @@ -0,0 +1,1978 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Queued Loading of map resources. !!!!Specifically!!! designed for the map loading process. +// +// Not designed for application startup or gameplay time. Layered on top of async i/o. +// Queued loading is allowed during the map load process until full connection only, +// but can complete the remaining low priority jobs during the game render. +// The normal loading path can run in units of seconds if it does not have to do I/O, +// which is why this system runs first and gets all the data in memory unhindered +// by dependency blocking. The I/O delivery process achieves its speed by having all the I/O +// requests at once, performing the I/O, and handing the actual consumption +// of the I/O buffer to another available core/thread (via job pool) for computation work. +// The I/O (should be all unbuffered) is then only throttled by physical transfer rates. +// +// The Load process is broken into three phases. The first phase build up I/O requests. +// The second phase fulfills only the high priority I/O requests. This gets the critical +// data in memory, that has to be there for the normal load path to query, or the renderer +// to run (i.e. models and shaders). The third phase is the normal load process. +// The low priority jobs run concurrently with the normal load process. Low priority jobs +// are those that have been specially built such that the game or loading can operate unblocked +// without the actual data (i.e. d3d texture bits). +// +// Phase 1: The reslist is parsed into seperate lists based on handled extensions. Each list +// call its own loader which in turn generates its own dictionaries and I/O requests through +// "AddJob". A single reslist entry could cause a laoder to request multiple jobs. ( i.e. models ) +// A loader marks its jobs as high or low priority. +// Phase 2: The I/O requests are sorted (which achieves seek offset order) and +// async i/o commences. Phase 2 does not end until all the high priority jobs +// are complete. This ensures critical data is resident. +// Phase 3: The !!!NORMAL!!! loading path can commence. The legacy loading path then +// is not expected to do I/O (it can, but that's a hole in the reslist), as all of the data +// that it queries, should be resident. +// +// Late added jobs are non-optimal (should have been in reslist), warned, but handled. +// +//===========================================================================// + +#include "basefilesystem.h" + +#include "tier0/vprof.h" +#include "tier0/tslist.h" +#include "tier1/utlbuffer.h" +#include "tier1/convar.h" +#include "tier1/KeyValues.h" +#include "tier1/utllinkedlist.h" +#include "tier1/utlstring.h" +#include "tier1/UtlSortVector.h" +#include "tier1/utldict.h" +#include "basefilesystem.h" +#include "tier0/icommandline.h" +#include "vstdlib/jobthread.h" +#include "filesystem/IQueuedLoader.h" +#include "tier2/tier2.h" +#include "characterset.h" +#if !defined( _X360 ) +#include "xbox/xboxstubs.h" +#endif + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + + + + +#define PRIORITY_HIGH 1 +#define PRIORITY_NORMAL 0 +#define PRIORITY_LOW -1 + +// main thread has reason to block and wait for thread pool to finish jobs +#define MAIN_THREAD_YIELD_TIME 20 + +// discrete stages in the preload process to tick the progress bar +#define PROGRESS_START 0.10f +#define PROGRESS_GOTRESLIST 0.12f +#define PROGRESS_PARSEDRESLIST 0.15f +#define PROGRESS_CREATEDRESOURCES 0.20f +#define PROGRESS_PREPURGE 0.22f +#define PROGRESS_IO 0.25f // up to 1.0 + +struct FileJob_t +{ + FileJob_t() + { + Q_memset( this, 0, sizeof( FileJob_t ) ); + } + + FileNameHandle_t m_hFilename; + QueuedLoaderCallback_t m_pCallback; + FSAsyncControl_t m_hAsyncControl; + void *m_pContext; + void *m_pContext2; + void *m_pTargetData; + int m_nBytesToRead; + unsigned int m_nStartOffset; + LoaderPriority_t m_Priority; + + unsigned int m_SubmitTime; + unsigned int m_FinishTime; + int m_SubmitTag; + int m_nActualBytesRead; + LoaderError_t m_LoaderError; + unsigned int m_ThreadId; + + unsigned int m_bFinished : 1; + unsigned int m_bFreeTargetAfterIO : 1; + unsigned int m_bFileExists : 1; + unsigned int m_bClaimed : 1; +}; + +// dummy stubbed progress interface +class CDummyProgress : public ILoaderProgress +{ + void BeginProgress() {} + void UpdateProgress( float progress ) {} + void EndProgress() {} +}; +static CDummyProgress s_DummyProgress; + +class CQueuedLoader : public CTier2AppSystem< IQueuedLoader > +{ + typedef CTier2AppSystem< IQueuedLoader > BaseClass; + +public: + CQueuedLoader(); + virtual ~CQueuedLoader(); + + // Inherited from IAppSystem + virtual InitReturnVal_t Init(); + virtual void Shutdown(); + + // IQueuedLoader + virtual void InstallLoader( ResourcePreload_t type, IResourcePreload *pLoader ); + virtual void InstallProgress( ILoaderProgress *pProgress ); + // Set bOptimizeReload if you want appropriate data (such as static prop lighting) + // to persist - rather than being purged and reloaded - when going from map A to map A. + virtual bool BeginMapLoading( const char *pMapName, bool bLoadForHDR, bool bOptimizeMapReload ); + virtual void EndMapLoading( bool bAbort ); + virtual bool AddJob( const LoaderJob_t *pLoaderJob ); + virtual void AddMapResource( const char *pFilename ); + virtual void DynamicLoadMapResource( const char *pFilename, DynamicResourceCallback_t pCallback, void *pContext, void *pContext2 ); + virtual void QueueDynamicLoadFunctor( CFunctor* pFunctor ); + virtual bool CompleteDynamicLoad(); + virtual void QueueCleanupDynamicLoadFunctor( CFunctor* pFunctor ); + virtual bool CleanupDynamicLoad(); + virtual bool ClaimAnonymousJob( const char *pFilename, QueuedLoaderCallback_t pCallback, void *pContext, void *pContext2 ); + virtual bool ClaimAnonymousJob( const char *pFilename, void **pData, int *pDataSize, LoaderError_t *pError ); + virtual bool IsMapLoading() const; + virtual bool IsSameMapLoading() const; + virtual bool IsFinished() const; + virtual bool IsBatching() const; + virtual bool IsDynamic() const; + virtual int GetSpewDetail() const; + + char *GetFilename( const FileNameHandle_t hFilename, char *pBuff, int nBuffSize ); + FileNameHandle_t FindFilename( const char *pFilename ); + void SpewInfo(); + + // submit any queued jobs to the async loader, called by main or async thread to get more work + void SubmitPendingJobs(); + + void PurgeAll(); + + +private: + + class CFileJobsLessFunc + { + public: + int GetLayoutOrderForFilename( const char *pFilename ); + bool Less( FileJob_t* const &pFileJobLHS, FileJob_t* const &pFileJobRHS, void *pCtx ); + }; + + class CResourceNameLessFunc + { + public: + bool Less( const FileNameHandle_t &hFilenameLHS, const FileNameHandle_t &hFilenameRHS, void *pCtx ); + }; + typedef CUtlSortVector< FileNameHandle_t, CResourceNameLessFunc > ResourceList_t; + + static void BuildResources( IResourcePreload *pLoader, ResourceList_t *pList, float *pBuildTime ); + static void BuildMaterialResources( IResourcePreload *pLoader, ResourceList_t *pList, float *pBuildTime ); + + void PurgeQueue(); + void CleanQueue(); + void SubmitBatchedJobs(); + void SubmitBatchedJobsAndWait(); + void ParseResourceList( CUtlBuffer &resourceList ); + void GetJobRequests(); + void PurgeUnreferencedResources(); + void AddResourceToTable( const char *pFilename ); + + bool m_bStarted; + bool m_bActive; + bool m_bBatching; + bool m_bDynamic; + bool m_bCanBatch; + bool m_bLoadForHDR; + bool m_bDoProgress; + bool m_bSameMap; + int m_nSubmitCount; + unsigned int m_StartTime; + unsigned int m_EndTime; + char m_szMapNameToCompareSame[MAX_PATH]; + + DynamicResourceCallback_t m_pfnDynamicCallback; + CUtlString m_DynamicFileName; + void* m_pDynamicContext; + void* m_pDynamicContext2; + CThreadFastMutex m_FunctorQueueMutex; + CUtlVector< CFunctor* > m_FunctorQueue; + CUtlVector< CFunctor* > m_CleanupFunctorQueue; + + CUtlFilenameSymbolTable m_Filenames; + CTSList< FileJob_t* > m_PendingJobs; + CTSList< FileJob_t* > m_BatchedJobs; + CUtlLinkedList< FileJob_t* > m_SubmittedJobs; + CUtlDict< FileJob_t*, int > m_AnonymousJobs; + CUtlSymbolTable m_AdditionalResources; + + CUtlSortVector< FileNameHandle_t, CResourceNameLessFunc > m_ResourceNames[RESOURCEPRELOAD_COUNT]; + IResourcePreload *m_pLoaders[RESOURCEPRELOAD_COUNT]; + float m_LoaderTimes[RESOURCEPRELOAD_COUNT]; + ILoaderProgress *m_pProgress; + CThreadFastMutex m_Mutex; +}; +static CQueuedLoader g_QueuedLoader; +EXPOSE_SINGLE_INTERFACE_GLOBALVAR( CQueuedLoader, IQueuedLoader, QUEUEDLOADER_INTERFACE_VERSION, g_QueuedLoader ); + + +class CResourcePreloadAnonymous : public IResourcePreload +{ + virtual bool CreateResource( const char *pName ) + { + // create an anonymous job to get the data in memory, claimed during load, or auto-freed + LoaderJob_t loaderJob; + loaderJob.m_pFilename = pName; + loaderJob.m_pPathID = "GAME"; + loaderJob.m_Priority = LOADERPRIORITY_DURINGPRELOAD; + g_QueuedLoader.AddJob( &loaderJob ); + return true; + } + + virtual void PurgeUnreferencedResources() {} + virtual void OnEndMapLoading( bool bAbort ) {} + virtual void PurgeAll() {} +}; +static CResourcePreloadAnonymous s_ResourcePreloadAnonymous; + +const char *g_ResourceLoaderNames[RESOURCEPRELOAD_COUNT] = +{ + "???", // RESOURCEPRELOAD_UNKNOWN + "Sounds", // RESOURCEPRELOAD_SOUND + "Materials", // RESOURCEPRELOAD_MATERIAL + "Models", // RESOURCEPRELOAD_MODEL + "Cubemaps", // RESOURCEPRELOAD_CUBEMAP + "PropLighting", // RESOURCEPRELOAD_STATICPROPLIGHTING + "Anonymous", // RESOURCEPRELOAD_ANONYMOUS +}; + +static CInterlockedInt g_nActiveJobs; +static CInterlockedInt g_nQueuedJobs; +static CInterlockedInt g_nHighPriorityJobs; // tracks jobs that must finish during preload +static CInterlockedInt g_nJobsToFinishBeforePlay; // tracks jobs that must finish before gameplay +static CInterlockedInt g_nIOMemory; // tracks I/O data from async delivery until consumed +static CInterlockedInt g_nAnonymousIOMemory; // tracks anonymous I/O data from async delivery until consumed +static CInterlockedInt g_SuspendIO; // used to throttle the I/O +static int g_nIOMemoryPeak; +static int g_nAnonymousIOMemoryPeak; +static int g_nHighIOSuspensionMark; +static int g_nLowIOSuspensionMark; + +ConVar loader_spew_info( "loader_spew_info", "0", 0, "0:Off, 1:Timing, 2:Completions, 3:Late Completions, 4:Purges, -1:All " ); + +// Kyle says: this is here only to change the DLL size to force clients to update! This should be removed +// by whoever sees this comment after we've shipped a DLL using it! +ConVar loader_sped_info_ex( "loader_spew_info_ex", "0", 0, "(internal)" ); + +CON_COMMAND( loader_dump_table, "" ) +{ + g_QueuedLoader.SpewInfo(); +} + +//----------------------------------------------------------------------------- +// Constructor +//----------------------------------------------------------------------------- +CQueuedLoader::CQueuedLoader() : BaseClass( false ) +{ + m_bStarted = false; + m_bActive = false; + m_bBatching = false; + m_bDynamic = false; + m_bCanBatch = false; + m_bLoadForHDR = false; + m_bDoProgress = false; + m_bSameMap = false; + + m_nSubmitCount = 0; + + m_pfnDynamicCallback = NULL; + m_pDynamicContext = NULL; + m_pDynamicContext2 = NULL; + + m_szMapNameToCompareSame[0] = '\0'; + + m_pProgress = &s_DummyProgress; + V_memset( m_pLoaders, 0, sizeof( m_pLoaders ) ); + + // set resource dictionaries sort context + for ( int i = 0; i < RESOURCEPRELOAD_COUNT; i++ ) + { + m_ResourceNames[i].SetLessContext( (void *)i ); + } + + InstallLoader( RESOURCEPRELOAD_ANONYMOUS, &s_ResourcePreloadAnonymous ); +} + +//----------------------------------------------------------------------------- +// Destructor +//----------------------------------------------------------------------------- +CQueuedLoader::~CQueuedLoader() +{ +} + +//----------------------------------------------------------------------------- +// Computation job to build out objects +//----------------------------------------------------------------------------- +void CQueuedLoader::BuildResources( IResourcePreload *pLoader, ResourceList_t *pList, float *pBuildTime ) +{ + float t0 = Plat_FloatTime(); + + if ( pLoader ) + { + pList->RedoSort(); + + for ( int i = 0; i < pList->Count(); i++ ) + { + char szFilename[MAX_PATH]; + g_QueuedLoader.GetFilename( pList->Element( i ), szFilename, sizeof( szFilename ) ); + if ( szFilename[0] ) + { + if ( !pLoader->CreateResource( szFilename ) ) + { + Warning( "QueuedLoader: Failed to create resource %s\n", szFilename ); + } + } + } + } + + // finished with list + pList->Purge(); + + *pBuildTime = Plat_FloatTime() - t0; +} + +//----------------------------------------------------------------------------- +// Computation job to build out material objects +//----------------------------------------------------------------------------- +void CQueuedLoader::BuildMaterialResources( IResourcePreload *pLoader, ResourceList_t *pList, float *pBuildTime ) +{ + float t0 = Plat_FloatTime(); + + char szLastFilename[MAX_PATH]; + szLastFilename[0] = '\0'; + + // ensure cubemaps are first + pList->RedoSort(); + + // run a clean operation to cull the non-patched env_cubemap materials, which are not built directly + for ( int i = 0; i < pList->Count(); i++ ) + { + char szFilename[MAX_PATH]; + char *pFilename = g_QueuedLoader.GetFilename( pList->Element( i ), szFilename, sizeof( szFilename ) ); + if ( !V_stristr( pFilename, "maps\\" ) ) + { + // list is sorted, first non-cubemap marks end of relevant list + break; + } + + // skip past maps/mapname/ + pFilename += 5; + pFilename = strchr( pFilename, '\\' ) + 1; + // back up until end of material name is found, need to strip off _%d_%d_%d.vmt + char *pEndFilename = V_stristr( pFilename, ".vmt" ); + if ( !pEndFilename ) + { + pEndFilename = pFilename + strlen( pFilename ); + } + int numUnderscores = 3; + while ( pEndFilename != pFilename && numUnderscores > 0 ) + { + pEndFilename--; + if ( pEndFilename[0] == '_' ) + { + numUnderscores--; + } + } + if ( numUnderscores == 0 ) + { + *pEndFilename = '\0'; + if ( !V_strcmp( szLastFilename, pFilename ) ) + { + // same cubemap material base already processed, skip it + continue; + } + V_strncpy( szLastFilename, pFilename, sizeof( szLastFilename ) ); + + strcat( pFilename, ".vmt" ); + FileNameHandle_t hFilename = g_QueuedLoader.FindFilename( pFilename ); + if ( hFilename ) + { + pList->Remove( hFilename ); + } + } + } + + // process clean list + BuildResources( pLoader, pList, pBuildTime ); + + *pBuildTime = Plat_FloatTime() - t0; +} + +//----------------------------------------------------------------------------- +// Called by multiple worker threads. Throttle the I/O to ensure too many +// buffers don't flood the work queue. Anonymous I/O is allowed to grow unbounded. +//----------------------------------------------------------------------------- +void AdjustAsyncIOSpeed() +{ + if ( g_QueuedLoader.IsDynamic() == true ) + { + return; + } + + // throttle back the I/O to keep the pending buffers from exhausting memory + if ( g_SuspendIO == 0 ) + { + if ( g_nIOMemory >= g_nHighIOSuspensionMark && g_nActiveJobs != 0 ) + { + // protect against another worker thread + if ( g_SuspendIO.AssignIf( 0, 1 ) ) + { + if ( g_QueuedLoader.GetSpewDetail() ) + { + Msg( "QueuedLoader: Suspending I/O at %.2f MB\n", (float)g_nIOMemory / ( 1024.0f * 1024.0f ) ); + } + g_pFullFileSystem->AsyncSuspend(); + } + } + } + else if ( g_SuspendIO == 1 ) + { + if ( g_nIOMemory <= g_nLowIOSuspensionMark ) + { + // protect against another worker thread + if ( g_SuspendIO.AssignIf( 1, 0 ) ) + { + if ( g_QueuedLoader.GetSpewDetail() ) + { + Msg( "QueuedLoader: Resuming I/O at %.2f MB\n", (float)g_nIOMemory / ( 1024.0f * 1024.0f ) ); + } + g_pFullFileSystem->AsyncResume(); + } + } + } +} + +//----------------------------------------------------------------------------- +// Computation job to do work after IO, runs callback +//----------------------------------------------------------------------------- +void IOComputationJob( FileJob_t *pFileJob, void *pData, int nSize, LoaderError_t loaderError ) +{ + int spewDetail = g_QueuedLoader.GetSpewDetail(); + if ( spewDetail & ( LOADER_DETAIL_COMPLETIONS|LOADER_DETAIL_LATECOMPLETIONS ) ) + { + const char *pLateString = ""; + if ( !g_QueuedLoader.IsMapLoading() ) + { + // completed outside of load process + pLateString = "(Late) "; + } + + if ( ( spewDetail & LOADER_DETAIL_COMPLETIONS ) || ( ( spewDetail & LOADER_DETAIL_LATECOMPLETIONS ) && pLateString[0] ) ) + { + char szFilename[MAX_PATH]; + g_QueuedLoader.GetFilename( pFileJob->m_hFilename, szFilename, sizeof( szFilename ) ); + Msg( "QueuedLoader: Computation:%8.8x, Size:%7d %s%s\n", ThreadGetCurrentId(), nSize, pLateString, szFilename ); + } + } + + if ( loaderError != LOADERERROR_NONE && pFileJob->m_bFileExists ) + { + char szFilename[MAX_PATH]; + g_QueuedLoader.GetFilename( pFileJob->m_hFilename, szFilename, sizeof( szFilename ) ); + Warning( "QueuedLoader:: I/O Error on %s\n", szFilename ); + } + + pFileJob->m_nActualBytesRead = nSize; + pFileJob->m_LoaderError = loaderError; + + if ( !pFileJob->m_pCallback ) + { + // absent callback means resource loader want this system to delay buffer until ready for it + if ( !pFileJob->m_pTargetData ) + { + // track it for later, unclaimed buffers will get freed + pFileJob->m_pTargetData = pData; + } + } + else + { + // regardless of error, call job callback so caller can do cleanup of their context + pFileJob->m_pCallback( pFileJob->m_pContext, pFileJob->m_pContext2, pData, nSize, loaderError ); + if ( pFileJob->m_bFreeTargetAfterIO && pData ) + { + // free our data only + g_pFullFileSystem->FreeOptimalReadBuffer( pData ); + } + + // memory has been consumed + g_nIOMemory -= nSize; + } + + // mark as completed + pFileJob->m_bFinished = true; + pFileJob->m_FinishTime = Plat_MSTime(); + pFileJob->m_ThreadId = ThreadGetCurrentId(); + + if ( pFileJob->m_Priority == LOADERPRIORITY_DURINGPRELOAD ) + { + g_nHighPriorityJobs--; + } + else if ( pFileJob->m_Priority == LOADERPRIORITY_BEFOREPLAY ) + { + g_nJobsToFinishBeforePlay--; + } + + g_nQueuedJobs--; + + if ( g_nQueuedJobs == 0 && ( spewDetail & LOADER_DETAIL_TIMING ) ) + { + Msg( "QueuedLoader: Finished I/O of all queued jobs!\n" ); + } + + AdjustAsyncIOSpeed(); +} + +//----------------------------------------------------------------------------- +// Computation job to do work after anonymous job was asynchronously claimed, runs callback. +//----------------------------------------------------------------------------- +void FinishAnonymousJob( FileJob_t *pFileJob, QueuedLoaderCallback_t pCallback, void *pContext, void *pContext2 ) +{ + // regardless of error, call job callback so caller can do cleanup of their context + pCallback( pContext, pContext2, pFileJob->m_pTargetData, pFileJob->m_nActualBytesRead, pFileJob->m_LoaderError ); + if ( pFileJob->m_bFreeTargetAfterIO && pFileJob->m_pTargetData ) + { + // free our data only + g_pFullFileSystem->FreeOptimalReadBuffer( pFileJob->m_pTargetData ); + pFileJob->m_pTargetData = NULL; + } + + pFileJob->m_bClaimed = true; + + // memory has been consumed + g_nAnonymousIOMemory -= pFileJob->m_nActualBytesRead; +} + +//----------------------------------------------------------------------------- +// Callback from I/O job thread. Purposely lightweight as possible to keep i/o from stalling. +//----------------------------------------------------------------------------- +void IOAsyncCallback( const FileAsyncRequest_t &asyncRequest, int numReadBytes, FSAsyncStatus_t asyncStatus ) +{ + FileJob_t *pFileJob = (FileJob_t *)asyncRequest.pContext; + + // interpret the async error + LoaderError_t loaderError; + switch ( asyncStatus ) + { + case FSASYNC_OK: + loaderError = LOADERERROR_NONE; + break; + case FSASYNC_ERR_FILEOPEN: + loaderError = LOADERERROR_FILEOPEN; + break; + default: + loaderError = LOADERERROR_READING; + } + + // track how much i/o data is in flight, consumption will decrement + if ( !pFileJob->m_pCallback ) + { + // anonymous io memory is tracked seperatley + g_nAnonymousIOMemory += numReadBytes; + if ( g_nAnonymousIOMemory > g_nAnonymousIOMemoryPeak ) + { + g_nAnonymousIOMemoryPeak = g_nAnonymousIOMemory; + } + } + else + { + g_nIOMemory += numReadBytes; + if ( g_nIOMemory > g_nIOMemoryPeak ) + { + g_nIOMemoryPeak = g_nIOMemory; + } + } + + // have data or error, do callback as a computation job + if ( !g_QueuedLoader.IsDynamic() ) + { + g_pThreadPool->QueueCall( IOComputationJob, pFileJob, asyncRequest.pData, numReadBytes, loaderError )->Release(); + } + else + { + g_QueuedLoader.QueueDynamicLoadFunctor( CreateFunctor( IOComputationJob, pFileJob, asyncRequest.pData, numReadBytes, loaderError ) ); + } + + // don't let the i/o starve, possibly get some more work from the pending queue + g_QueuedLoader.SubmitPendingJobs(); + + // possibly goes to zero atomically, AFTER submission + // prevents contention between main thread + --g_nActiveJobs; +} + +//----------------------------------------------------------------------------- +// Public method to filename dictionary +//----------------------------------------------------------------------------- +char *CQueuedLoader::GetFilename( const FileNameHandle_t hFilename, char *pBuff, int nBuffSize ) +{ + m_Filenames.String( hFilename, pBuff, nBuffSize ); + return pBuff; +} + +//----------------------------------------------------------------------------- +// Public method to filename dictionary +//----------------------------------------------------------------------------- +FileNameHandle_t CQueuedLoader::FindFilename( const char *pFilename ) +{ + return m_Filenames.FindFileName( pFilename ); +} + +//----------------------------------------------------------------------------- +// Sort function for resource names. +//----------------------------------------------------------------------------- +bool CQueuedLoader::CResourceNameLessFunc::Less( const FileNameHandle_t &hFilenameLHS, const FileNameHandle_t &hFilenameRHS, void *pCtx ) +{ + switch ( (int)pCtx ) + { + case RESOURCEPRELOAD_MATERIAL: + { + // Cubemap materials are expected to be at top of list + char szNameLHS[MAX_PATH]; + char szNameRHS[MAX_PATH]; + + const char *pNameLHS = g_QueuedLoader.GetFilename( hFilenameLHS, szNameLHS, sizeof( szNameLHS ) ); + const char *pNameRHS = g_QueuedLoader.GetFilename( hFilenameRHS, szNameRHS, sizeof( szNameRHS ) ); + + bool bIsCubemapLHS = V_stristr( pNameLHS, "maps\\" ) != NULL; + bool bIsCubemapRHS = V_stristr( pNameRHS, "maps\\" ) != NULL; + if ( bIsCubemapLHS != bIsCubemapRHS ) + { + return ( bIsCubemapLHS == true && bIsCubemapRHS == false ); + } + return ( V_stricmp( pNameLHS, pNameRHS ) < 0 ); + } + break; + + default: + // sort not really needed, just use numeric handles + return ( hFilenameLHS < hFilenameRHS ); + } +} + +//----------------------------------------------------------------------------- +// Resolve filenames to expected disc layout order as... +// bsp, graphs, platform, hl2, episodic, ep2, tf, portal, non-zip +// see XGD layout. +//----------------------------------------------------------------------------- +int CQueuedLoader::CFileJobsLessFunc::GetLayoutOrderForFilename( const char *pFilename ) +{ + bool bIsLocalizedZip = false; + if ( XBX_IsLocalized() ) + { + if ( V_stristr( pFilename, "\\zip" ) && V_stristr( pFilename, XBX_GetLanguageString() ) ) + { + bIsLocalizedZip = true; + } + } + + int order; + if ( V_stristr( pFilename, "\\maps\\" ) ) + { + // bsp's and graphs on the opposite layer, these must be topmost + // the queued loader is expecting to do these first, all at once + // this allows for a single layer switch + if ( V_stristr( pFilename, "\\graphs\\" ) ) + { + order = 1; + } + else + { + order = 0; + } + } + else if ( V_stristr( pFilename, "\\platform\\zip" ) ) + { + order = 2; + } + else if ( V_stristr( pFilename, "\\hl2\\zip" ) ) + { + order = 3; + } + else if ( V_stristr( pFilename, "\\episodic\\zip" ) ) + { + order = 4; + } + else if ( V_stristr( pFilename, "\\ep2\\zip" ) ) + { + order = 5; + } + else if ( V_stristr( pFilename, "\\tf\\zip" ) ) + { + order = 6; + } + else if ( V_stristr( pFilename, "\\portal\\zip" ) ) + { + order = 7; + } + else + { + // other + order = 8; + } + + // localized zips have same relative sort order, but after all other zips + return bIsLocalizedZip ? 10*order : order; +} + +//----------------------------------------------------------------------------- +// Sort function, high priority jobs sort first, then offset, then zip +//----------------------------------------------------------------------------- +bool CQueuedLoader::CFileJobsLessFunc::Less( FileJob_t* const &pFileJobLHS, FileJob_t* const &pFileJobRHS, void *pCtx ) +{ + if ( pFileJobLHS->m_Priority != pFileJobRHS->m_Priority ) + { + // higher priorities sort to top + return ( pFileJobLHS->m_Priority > pFileJobRHS->m_Priority ); + } + + if ( pFileJobLHS->m_hFilename == pFileJobRHS->m_hFilename ) + { + // same file (zip), sort by offset + return pFileJobLHS->m_nStartOffset < pFileJobRHS->m_nStartOffset; + } + + char szFilenameLHS[MAX_PATH]; + char szFilenameRHS[MAX_PATH]; + g_QueuedLoader.GetFilename( pFileJobLHS->m_hFilename, szFilenameLHS, sizeof( szFilenameLHS ) ); + g_QueuedLoader.GetFilename( pFileJobRHS->m_hFilename, szFilenameRHS, sizeof( szFilenameRHS ) ); + + // resolve filename to match disk layout of zips + int layoutLHS = GetLayoutOrderForFilename( szFilenameLHS ); + int layoutRHS = GetLayoutOrderForFilename( szFilenameRHS ); + if ( layoutLHS != layoutRHS ) + { + return layoutLHS < layoutRHS; + } + + return CaselessStringLessThan( szFilenameLHS, szFilenameRHS ); +} + +//----------------------------------------------------------------------------- +// Dump the queue contents to the file system. +//----------------------------------------------------------------------------- +void CQueuedLoader::SubmitPendingJobs() +{ + // prevents contention between I/O and main thread attempting to submit + if ( ThreadInMainThread() && g_nActiveJobs != 0 && m_bDynamic == false ) + { + // main thread can only kick start work if the I/O is idle + // once the I/O is kicked off, the I/O thread is responsible for continual draining + return; + } + else if ( !ThreadInMainThread() && g_nActiveJobs != 1 && m_bDynamic == false ) + { + // I/O thread requests more work, but will only fall through and get some when it expects to go idle + // I/O thread still has jobs and doesn't need any more yet + return; + } + + CTSList<FileJob_t *>::Node_t *pNode = m_PendingJobs.Detach(); + if ( !pNode ) + { + return; + } + + // used by spew to indicate submission blocks + m_nSubmitCount++; + + // sort entries + CUtlSortVector< FileJob_t*, CFileJobsLessFunc > sortedFiles( 0, 128 ); + while ( pNode ) + { + FileJob_t *pFileJob = pNode->elem; + + sortedFiles.InsertNoSort( pFileJob ); + + CTSList<FileJob_t *>::Node_t *pNext = (CTSList<FileJob_t *>::Node_t*)pNode->Next; + delete pNode; + pNode = pNext; + } + sortedFiles.RedoSort(); + + FileAsyncRequest_t asyncRequest; + asyncRequest.pfnCallback = IOAsyncCallback; + + char szFilename[MAX_PATH]; + for ( int i = 0; i<sortedFiles.Count(); i++ ) + { + FileJob_t *pFileJob = sortedFiles[i]; + + pFileJob->m_SubmitTag = m_nSubmitCount; + pFileJob->m_SubmitTime = Plat_MSTime(); + + m_SubmittedJobs.AddToTail( pFileJob ); + + // build an async request + if ( pFileJob->m_Priority == LOADERPRIORITY_DURINGPRELOAD ) + { + // must finish during preload + asyncRequest.priority = PRIORITY_HIGH; + g_nHighPriorityJobs++; + } + else if ( pFileJob->m_Priority == LOADERPRIORITY_BEFOREPLAY ) + { + // must finish before gameplay + asyncRequest.priority = PRIORITY_NORMAL; + g_nJobsToFinishBeforePlay++; + } + else + { + // can finish during gameplay, normal priority + asyncRequest.priority = PRIORITY_NORMAL; + } + + // async will allocate unless caller provided a target + // loader always takes ownership of buffer + asyncRequest.pData = pFileJob->m_pTargetData; + asyncRequest.flags = pFileJob->m_pTargetData ? 0 : FSASYNC_FLAGS_ALLOCNOFREE; + asyncRequest.nOffset = pFileJob->m_nStartOffset; + asyncRequest.nBytes = pFileJob->m_nBytesToRead; + asyncRequest.pszFilename = GetFilename( pFileJob->m_hFilename, szFilename, sizeof( szFilename ) ); + asyncRequest.pContext = (void *)pFileJob; + + if ( pFileJob->m_bFileExists ) + { + // start the valid async request + g_nActiveJobs++; + g_pFullFileSystem->AsyncRead( asyncRequest, &pFileJob->m_hAsyncControl ); + } + else + { + // prevent dragging the i/o system down for known failures + // still need to do callback so subsystems can do the right thing based on file absence + if ( IsDynamic() ) + QueueDynamicLoadFunctor( CreateFunctor( IOComputationJob, pFileJob, pFileJob->m_pTargetData, 0, LOADERERROR_FILEOPEN ) ); + else + g_pThreadPool->QueueCall( IOComputationJob, pFileJob, pFileJob->m_pTargetData, 0, LOADERERROR_FILEOPEN )->Release(); + } + } +} + +//----------------------------------------------------------------------------- +// Add to queue +//----------------------------------------------------------------------------- +bool CQueuedLoader::AddJob( const LoaderJob_t *pLoaderJob ) +{ + if ( !m_bActive ) + { + return false; + } + + Assert( pLoaderJob && pLoaderJob->m_pFilename ); + + if ( m_bCanBatch && !m_bBatching ) + { + // should have been part of pre-load batch + DevWarning( "QueuedLoader: Late Queued Job: %s\n", pLoaderJob->m_pFilename ); + } + + // anonymous jobs lack callbacks and are heavily restricted to ensure their stability + // the caller is expected to claim these before load ends (which auto-purges them) + if ( !pLoaderJob->m_pCallback && pLoaderJob->m_Priority == LOADERPRIORITY_ANYTIME ) + { + Assert( 0 ); + DevWarning( "QueuedLoader: Ignoring Anonymous Job: %s\n", pLoaderJob->m_pFilename ); + return false; + } + + MEM_ALLOC_CREDIT(); + + // all bsp based files get forced to a higher priority in order to achieve a clustered sort + // the bsp files are not going to be anywhere near the zips, thus we don't want head thrashing + bool bFileIsFromBSP; + bool bExists = false; + + char *pFullPath; + char szFullPath[MAX_PATH]; + if ( V_IsAbsolutePath( pLoaderJob->m_pFilename ) ) + { + // an absolute path is trusted, take as is + pFullPath = (char *)pLoaderJob->m_pFilename; + bFileIsFromBSP = V_stristr( pFullPath, ".bsp" ) != NULL; + bExists = true; + } + else + { + // must resolve now, all submitted paths must be absolute for proper sort which achieves seek linearization + // a resolved absolute file ensures its existence + PathTypeFilter_t pathFilter = FILTER_NONE; + if ( IsX360() && ( g_pFullFileSystem->GetDVDMode() == DVDMODE_STRICT ) ) + { + if ( V_stristr( pLoaderJob->m_pFilename, ".bsp" ) || V_stristr( pLoaderJob->m_pFilename, ".ain" ) ) + { + // only the bsp/ain are allowed to be external + pathFilter = FILTER_CULLPACK; + } + else + { + // all files are expected to be in zip + pathFilter = FILTER_CULLNONPACK; + } + } + + PathTypeQuery_t pathType; + g_pFullFileSystem->RelativePathToFullPath( pLoaderJob->m_pFilename, pLoaderJob->m_pPathID, szFullPath, sizeof( szFullPath ), pathFilter, &pathType ); + bExists = V_IsAbsolutePath( szFullPath ); + pFullPath = szFullPath; + bFileIsFromBSP = ( (pathType & PATH_IS_MAPPACKFILE) != 0 ); + } + + // create a file job + FileJob_t *pFileJob = new FileJob_t; + + pFileJob->m_hFilename = m_Filenames.FindOrAddFileName( pFullPath ); + pFileJob->m_bFileExists = bExists; + pFileJob->m_pCallback = pLoaderJob->m_pCallback; + pFileJob->m_pContext = pLoaderJob->m_pContext; + pFileJob->m_pContext2 = pLoaderJob->m_pContext2; + pFileJob->m_pTargetData = pLoaderJob->m_pTargetData; + pFileJob->m_nBytesToRead = pLoaderJob->m_nBytesToRead; + pFileJob->m_nStartOffset = pLoaderJob->m_nStartOffset; + pFileJob->m_Priority = bFileIsFromBSP ? LOADERPRIORITY_DURINGPRELOAD : pLoaderJob->m_Priority; + + if ( pLoaderJob->m_pTargetData ) + { + // never free caller's buffer, if they provide, they have to free it + pFileJob->m_bFreeTargetAfterIO = false; + } + else + { + // caller can take over ownership, otherwise it gets freed after I/O + pFileJob->m_bFreeTargetAfterIO = ( pLoaderJob->m_bPersistTargetData == false ); + } + + if ( !pLoaderJob->m_pCallback ) + { + // track anonymous jobs + AUTO_LOCK( m_Mutex ); + char szFixedName[MAX_PATH]; + V_strncpy( szFixedName, pLoaderJob->m_pFilename, sizeof( szFixedName ) ); + V_FixSlashes( szFixedName ); + m_AnonymousJobs.Insert( szFixedName, pFileJob ); + } + + g_nQueuedJobs++; + + if ( m_bBatching ) + { + m_BatchedJobs.PushItem( pFileJob ); + } + else + { + m_PendingJobs.PushItem( pFileJob ); + SubmitPendingJobs(); + } + + return true; +} + +//----------------------------------------------------------------------------- +// Allows an external system to append to a map's reslist. The next map load +// will append these specified files. Unhandled resources will just get +// quietly discarded. An external system could use this to patch a hole +// or prevent a purge. +//----------------------------------------------------------------------------- +void CQueuedLoader::AddMapResource( const char *pFilename ) +{ + if ( !pFilename || !pFilename[0] ) + { + // pointless + return; + } + + // normalize the provided name as a filename + char szFilename[MAX_PATH]; + V_strncpy( szFilename, pFilename, sizeof( szFilename ) ); + V_FixSlashes( szFilename ); + V_strlower( szFilename ); + + if ( m_AdditionalResources.Find( szFilename ) != UTL_INVAL_SYMBOL ) + { + // already added + return; + } + + m_AdditionalResources.AddString( szFilename ); +} + +//----------------------------------------------------------------------------- +// Asynchronous claim for an anonymous job. +// This allows loaders with deep dependencies to get their data in flight, and then claim it +// when the they are in a state to consume it. +//----------------------------------------------------------------------------- +bool CQueuedLoader::ClaimAnonymousJob( const char *pFilename, QueuedLoaderCallback_t pCallback, void *pContext, void *pContext2 ) +{ + Assert( ThreadInMainThread() ); + Assert( pFilename && pCallback && !m_bBatching ); + + char szFixedName[MAX_PATH]; + V_strncpy( szFixedName, pFilename, sizeof( szFixedName ) ); + V_FixSlashes( szFixedName ); + pFilename = szFixedName; + + int iIndex = m_AnonymousJobs.Find( pFilename ); + if ( iIndex == m_AnonymousJobs.InvalidIndex() ) + { + // unknown + DevWarning( "QueuedLoader: Anonymous Job '%s' not found\n", pFilename ); + return false; + } + + // caller is claiming + FileJob_t *pFileJob = m_AnonymousJobs[iIndex]; + if ( !pFileJob->m_bFinished ) + { + // unfinished shouldn't happen and caller can't have it + // anonymous jobs and their claims are very restrictive in such a way to provide stability + // this dead job will get auto-cleaned at end of map loading + Assert( 0 ); + return false; + } + + m_AnonymousJobs.RemoveAt( iIndex ); + g_pThreadPool->QueueCall( FinishAnonymousJob, pFileJob, pCallback, pContext, pContext2 )->Release(); + + return true; +} + +//----------------------------------------------------------------------------- +// Synchronous claim for an anonymous job. This allows loaders +// with deep dependencies to get their data in flight, and then claim it +// when the they are in a state to consume it. +//----------------------------------------------------------------------------- +bool CQueuedLoader::ClaimAnonymousJob( const char *pFilename, void **pData, int *pDataSize, LoaderError_t *pError ) +{ + Assert( ThreadInMainThread() ); + Assert( pFilename && !m_bBatching ); + + char szFixedName[MAX_PATH]; + V_strncpy( szFixedName, pFilename, sizeof( szFixedName ) ); + V_FixSlashes( szFixedName ); + pFilename = szFixedName; + + int iIndex = m_AnonymousJobs.Find( pFilename ); + if ( iIndex == m_AnonymousJobs.InvalidIndex() ) + { + // unknown + DevWarning( "QueuedLoader: Anonymous Job '%s' not found\n", pFilename ); + return false; + } + + // caller is claiming + FileJob_t *pFileJob = m_AnonymousJobs[iIndex]; + if ( !pFileJob->m_bFinished ) + { + // unfinished shouldn't happen and caller can't have it + // anonymous jobs and their claims are very restrictive in such a way to provide stability + // this dead job will get auto-cleaned at end of map loading + Assert( 0 ); + return false; + } + + pFileJob->m_bClaimed = true; + + m_AnonymousJobs.RemoveAt( iIndex ); + + *pData = pFileJob->m_pTargetData; + *pDataSize = pFileJob->m_LoaderError == LOADERERROR_NONE ? pFileJob->m_nActualBytesRead : 0; + if ( pError ) + { + *pError = pFileJob->m_LoaderError; + } + + // caller owns the data, regardless of how the job was setup + pFileJob->m_pTargetData = NULL; + + // memory has been consumed + g_nAnonymousIOMemory -= pFileJob->m_nActualBytesRead; + + return true; +} + +//----------------------------------------------------------------------------- +// End of batching. Moves jobs into pending queue and submits but does not wait +//----------------------------------------------------------------------------- +void CQueuedLoader::SubmitBatchedJobs() +{ + // end of batching + m_bBatching = false; + + CTSList<FileJob_t *>::Node_t *pNode = m_BatchedJobs.Detach(); + if ( !pNode ) + { + return; + } + + // must wait for any initial i/o to finish + // i/o thread must stop in order to submit all the batched jobs atomically + // and get an accurate accounting of high priority jobs + while ( g_nActiveJobs != 0 ) + { + g_pThreadPool->Yield( MAIN_THREAD_YIELD_TIME ); + } + + // dump batched jobs to pending jobs + while ( pNode ) + { + FileJob_t *pFileJob = pNode->elem; + + m_PendingJobs.PushItem( pFileJob ); + + CTSList<FileJob_t *>::Node_t *pNext = (CTSList<FileJob_t *>::Node_t*)pNode->Next; + delete pNode; + pNode = pNext; + } + + SubmitPendingJobs(); + + if ( GetSpewDetail() ) + { + Msg( "QueuedLoader: High Priority Jobs: %d\n", (int)g_nHighPriorityJobs ); + } +} + +//----------------------------------------------------------------------------- +// End of batching. High priority jobs are guaranteed completed before function returns. +//----------------------------------------------------------------------------- +void CQueuedLoader::SubmitBatchedJobsAndWait() +{ + SubmitBatchedJobs(); + + // finish only the high priority jobs + // high priority jobs are expected to be complete at the conclusion of batching + int total = g_nHighPriorityJobs; + while ( g_nHighPriorityJobs != 0 ) + { + float t = (float)( total - g_nHighPriorityJobs ) / (float)total; + m_pProgress->UpdateProgress( PROGRESS_IO + t * ( 1.0f - PROGRESS_IO ) ); + + // yield some time + g_pThreadPool->Yield( MAIN_THREAD_YIELD_TIME ); + } +} + +//----------------------------------------------------------------------------- +// Clean queue of stale entries. Active entries are skipped. +//----------------------------------------------------------------------------- +void CQueuedLoader::CleanQueue() +{ + for ( int i = 0; i<RESOURCEPRELOAD_COUNT; i++ ) + { + m_ResourceNames[i].Purge(); + } + + m_BatchedJobs.Purge(); + + int iIndex = m_SubmittedJobs.Head(); + while ( iIndex != m_SubmittedJobs.InvalidIndex() ) + { + int iNext = m_SubmittedJobs.Next( iIndex ); + + FileJob_t *pFileJob = m_SubmittedJobs[iIndex]; + if ( pFileJob->m_bFinished ) + { + // job is complete, safe to free + m_SubmittedJobs.Free( iIndex ); + g_pFullFileSystem->AsyncRelease( pFileJob->m_hAsyncControl ); + delete pFileJob; + } + iIndex = iNext; + } + + m_Filenames.RemoveAll(); +} + +//----------------------------------------------------------------------------- +// Abandon queue +//----------------------------------------------------------------------------- +void CQueuedLoader::PurgeQueue() +{ +} + +//----------------------------------------------------------------------------- +// Spew info abut queued load +//----------------------------------------------------------------------------- +void CQueuedLoader::SpewInfo() +{ + Msg( "Queued Loader:\n\n" ); + + int totalClaimed = 0; + int totalUnclaimed = 0; + + if ( IsFinished() || m_bDynamic == true ) + { + // can only access submitted jobs safely when io thread complete + int lastPriority = -1; + int iIndex = m_SubmittedJobs.Head(); + while ( iIndex != m_SubmittedJobs.InvalidIndex() ) + { + FileJob_t *pFileJob = m_SubmittedJobs[iIndex]; + + int asyncDuration = -1; + if ( pFileJob->m_FinishTime ) + { + asyncDuration = pFileJob->m_FinishTime - pFileJob->m_SubmitTime; + } + + if ( pFileJob->m_Priority != lastPriority ) + { + switch ( pFileJob->m_Priority ) + { + case LOADERPRIORITY_DURINGPRELOAD: + Msg( "---- FINISH DURING PRELOAD ( HIGH PRIORITY )----\n" ); + break; + case LOADERPRIORITY_BEFOREPLAY: + Msg( "---- FINISH BEFORE GAMEPLAY ( NORMAL PRIORITY )----\n" ); + break; + case LOADERPRIORITY_ANYTIME: + Msg( "---- FINISH ANYTIME ( NORMAL PRIORITY )----\n" ); + break; + } + lastPriority = pFileJob->m_Priority; + } + + char szAnonymousString[MAX_PATH]; + const char *pAnonymousStatus = ""; + if ( !pFileJob->m_pCallback ) + { + V_snprintf( szAnonymousString, sizeof( szAnonymousString ), "(%s) ", pFileJob->m_bClaimed ? "Claimed" : "Unclaimed" ); + pAnonymousStatus = szAnonymousString; + + if ( pFileJob->m_bClaimed ) + { + totalClaimed += pFileJob->m_nActualBytesRead; + } + else + { + totalUnclaimed += pFileJob->m_nActualBytesRead; + } + } + + char szFilename[MAX_PATH]; + Msg( "Submit:%5dms AsyncDuration:%5dms Tag:%d Thread:%8.8x Size:%7d %s%s\n", + pFileJob->m_SubmitTime - m_StartTime, + asyncDuration, + pFileJob->m_SubmitTag, + pFileJob->m_ThreadId, + pFileJob->m_nActualBytesRead, + pAnonymousStatus, + GetFilename( pFileJob->m_hFilename, szFilename, sizeof( szFilename ) ) ); + + iIndex = m_SubmittedJobs.Next( iIndex ); + } + + Msg( "%d Total Jobs\n", m_SubmittedJobs.Count() ); + } + + Msg( "%d Queued Jobs\n", (int)g_nQueuedJobs ); + Msg( "%d Active Jobs\n", (int)g_nActiveJobs ); + Msg( "Peak IO Memory: %.2f MB\n", (float)g_nIOMemoryPeak / ( 1024.0f * 1024.0f ) ); + Msg( "Peak Anonymous IO Memory: %.2f MB\n", (float)g_nAnonymousIOMemoryPeak / ( 1024.0f * 1024.0f ) ); + Msg( " Total Anonymous Claimed: %d\n", totalClaimed ); + Msg( " Total Anonymous Unclaimed: %d\n", totalUnclaimed ); + if ( m_EndTime ) + { + Msg( "Queuing Duration: %dms\n", m_EndTime - m_StartTime ); + } +} + +//----------------------------------------------------------------------------- +// Initialization +//----------------------------------------------------------------------------- +InitReturnVal_t CQueuedLoader::Init() +{ + InitReturnVal_t nRetVal = BaseClass::Init(); + if ( nRetVal != INIT_OK ) + { + return nRetVal; + } + + return INIT_OK; +} + +//----------------------------------------------------------------------------- +// Shutdown +//----------------------------------------------------------------------------- +void CQueuedLoader::Shutdown() +{ + BaseClass::Shutdown(); +} + +//----------------------------------------------------------------------------- +// Install a type specific interface from managing system. +//----------------------------------------------------------------------------- +void CQueuedLoader::InstallLoader( ResourcePreload_t type, IResourcePreload *pLoader ) +{ + m_pLoaders[type] = pLoader; +} + +void CQueuedLoader::InstallProgress( ILoaderProgress *pProgress ) +{ + m_pProgress = pProgress; +} + +//----------------------------------------------------------------------------- +// Invoke the loader systems to purge dead resources +//----------------------------------------------------------------------------- +void CQueuedLoader::PurgeUnreferencedResources() +{ + ResourcePreload_t purgeOrder[RESOURCEPRELOAD_COUNT]; + + // the purge operations require a specific order (models and cubemaps before materials) + int numPurges = 0; + purgeOrder[numPurges++] = RESOURCEPRELOAD_SOUND; + purgeOrder[numPurges++] = RESOURCEPRELOAD_STATICPROPLIGHTING; + purgeOrder[numPurges++] = RESOURCEPRELOAD_MODEL; + purgeOrder[numPurges++] = RESOURCEPRELOAD_CUBEMAP; + purgeOrder[numPurges++] = RESOURCEPRELOAD_MATERIAL; + + // iterate according to order + for ( int i = 0; i < numPurges; i++ ) + { + ResourcePreload_t loader = purgeOrder[i]; + if ( m_pLoaders[loader] ) + { + m_pLoaders[loader]->PurgeUnreferencedResources(); + } + } + + m_pProgress->UpdateProgress( PROGRESS_PREPURGE ); +} + + +//----------------------------------------------------------------------------- +// Invoke the loader systems to purge all resources, if possible +//----------------------------------------------------------------------------- +void CQueuedLoader::PurgeAll() +{ + ResourcePreload_t purgeOrder[RESOURCEPRELOAD_COUNT]; + + // the purge operations require a specific order (models and cubemaps before materials) + int numPurges = 0; + purgeOrder[numPurges++] = RESOURCEPRELOAD_SOUND; + purgeOrder[numPurges++] = RESOURCEPRELOAD_STATICPROPLIGHTING; + purgeOrder[numPurges++] = RESOURCEPRELOAD_MODEL; + purgeOrder[numPurges++] = RESOURCEPRELOAD_CUBEMAP; + purgeOrder[numPurges++] = RESOURCEPRELOAD_MATERIAL; + + // iterate according to order + for ( int i = 0; i < numPurges; i++ ) + { + ResourcePreload_t loader = purgeOrder[i]; + if ( m_pLoaders[loader] ) + { + m_pLoaders[loader]->PurgeAll(); + } + } + *m_szMapNameToCompareSame = 0; +} + + +//----------------------------------------------------------------------------- +// Invoke the loader systems to request i/o jobs, which are batched. +//----------------------------------------------------------------------------- +void CQueuedLoader::GetJobRequests() +{ + COM_TimestampedLog( "CQueuedLoader::GetJobRequests - Start" ); + + // causes the batch queue to fill with i/o requests + m_bCanBatch = true; + m_bBatching = true; + + float t0 = Plat_FloatTime(); + + if ( !IsPC() && !m_bDynamic ) + { + // cubemap textures must be first to install correctly before their cubemap materials are built (and precache the cubmeap textures) + // cannot be overlapped, must run serially + BuildResources( m_pLoaders[RESOURCEPRELOAD_CUBEMAP], &m_ResourceNames[RESOURCEPRELOAD_CUBEMAP], &m_LoaderTimes[RESOURCEPRELOAD_CUBEMAP] ); + + // Overlapping these is not critical in any way, total time is currently < 2 seconds. + // These operations flood calls (AddJob) back into the queued loader (which has to mutex its lists), + // so in fact it's slightly slower to queue these at this stage. As these routines age they may become more heavyweight. + CJob *jobs[5]; + jobs[0] = g_pThreadPool->QueueCall( BuildResources, m_pLoaders[RESOURCEPRELOAD_SOUND], &m_ResourceNames[RESOURCEPRELOAD_SOUND], &m_LoaderTimes[RESOURCEPRELOAD_SOUND] ); + jobs[1] = g_pThreadPool->QueueCall( BuildMaterialResources, m_pLoaders[RESOURCEPRELOAD_MATERIAL], &m_ResourceNames[RESOURCEPRELOAD_MATERIAL], &m_LoaderTimes[RESOURCEPRELOAD_MATERIAL] ); + jobs[2] = g_pThreadPool->QueueCall( BuildResources, m_pLoaders[RESOURCEPRELOAD_STATICPROPLIGHTING], &m_ResourceNames[RESOURCEPRELOAD_STATICPROPLIGHTING], &m_LoaderTimes[RESOURCEPRELOAD_STATICPROPLIGHTING] ); + jobs[3] = g_pThreadPool->QueueCall( BuildResources, m_pLoaders[RESOURCEPRELOAD_MODEL], &m_ResourceNames[RESOURCEPRELOAD_MODEL], &m_LoaderTimes[RESOURCEPRELOAD_MODEL] ); + jobs[4] = g_pThreadPool->QueueCall( BuildResources, m_pLoaders[RESOURCEPRELOAD_ANONYMOUS], &m_ResourceNames[RESOURCEPRELOAD_ANONYMOUS], &m_LoaderTimes[RESOURCEPRELOAD_ANONYMOUS] ); + + // all jobs must finish + float flLastUpdateT = -1000.0f; + // Update as if this takes 2 seconds + float flDelta = ( PROGRESS_CREATEDRESOURCES - PROGRESS_PARSEDRESLIST ) * 0.03 / 2.0f; + float flProgress = PROGRESS_PARSEDRESLIST; + while( true ) + { + bool bIsDone = true; + for ( int i=0; i<ARRAYSIZE( jobs ); i++ ) + { + if ( !jobs[i]->IsFinished() ) + { + bIsDone = false; + break; + } + } + if ( bIsDone ) + break; + + // Can't sleep; that will allow this thread to be used by the thread pool + float newt = Plat_FloatTime(); + if ( newt - flLastUpdateT > .03 ) + { + m_pProgress->UpdateProgress( flProgress ); + flProgress = clamp( flProgress + flDelta, PROGRESS_PARSEDRESLIST, PROGRESS_CREATEDRESOURCES ); + + // Necessary to take into account any waits for vsync + flLastUpdateT = Plat_FloatTime(); + } + } + + for ( int i=0; i<ARRAYSIZE( jobs ); i++ ) + { + jobs[i]->Release(); + } + } + else + { + BuildResources( m_pLoaders[RESOURCEPRELOAD_CUBEMAP], &m_ResourceNames[RESOURCEPRELOAD_CUBEMAP], &m_LoaderTimes[RESOURCEPRELOAD_CUBEMAP] ); + BuildResources( m_pLoaders[RESOURCEPRELOAD_SOUND], &m_ResourceNames[RESOURCEPRELOAD_SOUND], &m_LoaderTimes[RESOURCEPRELOAD_SOUND] ); + BuildMaterialResources( m_pLoaders[RESOURCEPRELOAD_MATERIAL], &m_ResourceNames[RESOURCEPRELOAD_MATERIAL], &m_LoaderTimes[RESOURCEPRELOAD_MATERIAL] ); + BuildResources( m_pLoaders[RESOURCEPRELOAD_STATICPROPLIGHTING], &m_ResourceNames[RESOURCEPRELOAD_STATICPROPLIGHTING], &m_LoaderTimes[RESOURCEPRELOAD_STATICPROPLIGHTING] ); + BuildResources( m_pLoaders[RESOURCEPRELOAD_MODEL], &m_ResourceNames[RESOURCEPRELOAD_MODEL], &m_LoaderTimes[RESOURCEPRELOAD_MODEL] ); + BuildResources( m_pLoaders[RESOURCEPRELOAD_ANONYMOUS], &m_ResourceNames[RESOURCEPRELOAD_ANONYMOUS], &m_LoaderTimes[RESOURCEPRELOAD_ANONYMOUS] ); + } + + if ( g_QueuedLoader.GetSpewDetail() & LOADER_DETAIL_TIMING ) + { + for ( int i = RESOURCEPRELOAD_UNKNOWN+1; i<RESOURCEPRELOAD_COUNT; i++ ) + { + Msg( "QueuedLoader: %s Creating: %.2f seconds\n", g_ResourceLoaderNames[i], m_LoaderTimes[i] ); + } + Msg( "QueuedLoader: Total Creating: %.2f seconds\n", Plat_FloatTime() - t0 ); + } + + m_pProgress->UpdateProgress( PROGRESS_CREATEDRESOURCES ); + + COM_TimestampedLog( "CQueuedLoader::GetJobRequests - End" ); +} + +void CQueuedLoader::AddResourceToTable( const char *pFilename ) +{ + const char *pExt = V_GetFileExtension( pFilename ); + if ( !pExt ) + { + // unknown + // all resources are identified by their extension + return; + } + + const char *pTypeDir = NULL; + const char *pName = pFilename; + ResourcePreload_t type = RESOURCEPRELOAD_UNKNOWN; + + if ( !V_stricmp( pExt, "wav" ) ) + { + type = RESOURCEPRELOAD_SOUND; + pTypeDir = "sound\\"; + } + else if ( !V_stricmp( pExt, "vmt" ) ) + { + type = RESOURCEPRELOAD_MATERIAL; + pTypeDir = "materials\\"; + } + else if ( !V_stricmp( pExt, "vtf" ) ) + { + if ( V_stristr( pFilename, "maps\\" ) ) + { + // only want cubemap textures + if ( !m_bLoadForHDR && V_stristr( pFilename, ".hdr." ) ) + { + return; + } + else if ( m_bLoadForHDR && !V_stristr( pFilename, ".hdr." ) ) + { + return; + } + type = RESOURCEPRELOAD_CUBEMAP; + pTypeDir = "materials\\"; + } + else + { + return; + } + } + else if ( !V_stricmp( pExt, "mdl" ) ) + { + type = RESOURCEPRELOAD_MODEL; + pTypeDir = "models\\"; + } + else if ( !V_stricmp( pExt, "vhv" ) ) + { + // want static props only + pName = V_stristr( pFilename, "sp_" ); + if ( !pName ) + { + return; + } + + if ( !m_bLoadForHDR && V_stristr( pFilename, "_hdr_" ) ) + { + return; + } + else if ( m_bLoadForHDR && !V_stristr( pFilename, "_hdr_" ) ) + { + return; + } + type = RESOURCEPRELOAD_STATICPROPLIGHTING; + } + else + { + // unknown, ignored + return; + } + + if ( pTypeDir ) + { + // want object name only + // skip past game/type directory prefixing + const char *pDir = V_stristr( pName, pTypeDir ); + if ( pDir ) + { + pName = pDir + strlen( pTypeDir ); + } + } + + FileNameHandle_t hFilename = m_Filenames.FindOrAddFileName( pName ); + m_ResourceNames[type].InsertNoSort( hFilename ); +} + + +//----------------------------------------------------------------------------- +// Parse the raw resource list into resource dictionaries +//----------------------------------------------------------------------------- +void CQueuedLoader::ParseResourceList( CUtlBuffer &resourceList ) +{ + // parse resource list into known types + characterset_t breakSet; + CharacterSetBuild( &breakSet, "" ); + char szToken[MAX_PATH]; + for ( ;; ) + { + int nTokenSize = resourceList.ParseToken( &breakSet, szToken, sizeof( szToken ) ); + if ( nTokenSize <= 0 ) + { + break; + } + + AddResourceToTable( szToken ); + } + + // add any additional resources + // duplicates don't need to be culled, loaders are supposed to handle resources that already exist + for ( int i = 0; i < m_AdditionalResources.GetNumStrings(); i++ ) + { + if ( g_QueuedLoader.GetSpewDetail() ) + { + Msg( "QueuedLoader: Appending: %s\n", m_AdditionalResources.String( i ) ); + } + AddResourceToTable( m_AdditionalResources.String( i ) ); + } + + if ( g_QueuedLoader.GetSpewDetail() ) + { + for ( int i = RESOURCEPRELOAD_UNKNOWN+1; i < RESOURCEPRELOAD_COUNT; i++ ) + { + Msg( "QueuedLoader: %s: %d Entries\n", g_ResourceLoaderNames[i], m_ResourceNames[i].Count() ); + } + } + + m_pProgress->UpdateProgress( PROGRESS_PARSEDRESLIST ); +} + +//----------------------------------------------------------------------------- +// Mark the start of the queued loading process. +//----------------------------------------------------------------------------- +bool CQueuedLoader::BeginMapLoading( const char *pMapName, bool bLoadForHDR, bool bOptimizeMapReload ) +{ + if ( IsPC() ) + { + return false; + } + + if ( CommandLine()->FindParm( "-noqueuedload" ) || ( g_pFullFileSystem->GetDVDMode() != DVDMODE_STRICT ) ) + { + return false; + } + + if ( m_bStarted ) + { + // already started, shouldn't be started more than once + Assert( 0 ); + return true; + } + + COM_TimestampedLog( "CQueuedLoader::BeginMapLoading" ); + + // set the IO throttle markers based on available memory + // these safety watermarks throttle the i/o from flooding memory, when the cores cannot keep up + // the delta must be larger than any single operation, otherwise deadlock + // markers that are too close will cause excessive suspension + size_t usedMemory, freeMemory; + MemAlloc_GlobalMemoryStatus( &usedMemory, &freeMemory ); + if ( freeMemory >= 64*1024*1024 ) + { + // lots of available memory, can afford to have let the i/o get ahead + g_nHighIOSuspensionMark = 10*1024*1024; + g_nLowIOSuspensionMark = 2*1024*1024; + } + else + { + // low memory, suspend the i/o more frequently + g_nHighIOSuspensionMark = 5*1024*1024; + g_nLowIOSuspensionMark = 1*1024*1024; + } + + if ( GetSpewDetail() ) + { + Msg( "QueuedLoader: Suspend I/O at [%.2f,%.2f] MB\n", (float)g_nLowIOSuspensionMark/(1024.0f*1024.0f), (float)g_nHighIOSuspensionMark/(1024.0f*1024.0f) ); + } + + m_bStarted = true; + m_bDynamic = false; + m_bLoadForHDR = bLoadForHDR; + + // map pak will be accessed asynchronously throughout loading and into game frame + g_pFullFileSystem->BeginMapAccess(); + + // remove any prior stale entries + CleanQueue(); + Assert( m_SubmittedJobs.Count() == 0 && g_nActiveJobs == 0 && g_nQueuedJobs == 0 ); + + m_bActive = true; + m_nSubmitCount = 0; + m_StartTime = Plat_MSTime(); + m_EndTime = 0; + m_bCanBatch = false; + m_bBatching = false; + m_bDoProgress = false; + + g_nIOMemory = 0; + g_nAnonymousIOMemory = 0; + g_nIOMemoryPeak = 0; + g_nAnonymousIOMemoryPeak = 0; + + m_bSameMap = bOptimizeMapReload && ( V_stricmp( pMapName, m_szMapNameToCompareSame ) == 0 ); + if ( m_bSameMap ) + { + // Data will persist (so reloading a map is v. fast) + } + else + { + // Full load of the new map's data + V_strncpy( m_szMapNameToCompareSame, pMapName, sizeof( m_szMapNameToCompareSame ) ); + } + + m_pProgress->BeginProgress(); + m_pProgress->UpdateProgress( PROGRESS_START ); + + // load this map's resource list before any other i/o + char szBaseName[MAX_PATH]; + char szFilename[MAX_PATH]; + V_FileBase( pMapName, szBaseName, sizeof( szBaseName ) ); + V_snprintf( szFilename, sizeof( szFilename ), "reslists_xbox/%s%s.lst", szBaseName, GetPlatformExt() ); + + MEM_ALLOC_CREDIT(); + + CUtlBuffer resListBuffer( 0, 0, CUtlBuffer::TEXT_BUFFER ); + if ( !g_pFullFileSystem->ReadFile( szFilename, "GAME", resListBuffer, 0, 0 ) ) + { + // very bad, a valid reslist is critical + DevWarning( "QueuedLoader: Failed to get reslist '%s', Non-Optimal Loading.\n", szFilename ); + m_bActive = false; + return false; + } + + if ( XBX_IsLocalized() ) + { + // find optional localized reslist fixup + V_snprintf( szFilename, sizeof( szFilename ), "reslists_xbox/%s%s.lst", XBX_GetLanguageString(), GetPlatformExt() ); + CUtlBuffer localizedBuffer( 0, 0, CUtlBuffer::TEXT_BUFFER ); + if ( g_pFullFileSystem->ReadFile( szFilename, "GAME", localizedBuffer, 0, 0 ) ) + { + // append it + resListBuffer.EnsureCapacity( resListBuffer.TellPut() + localizedBuffer.TellPut() ); + resListBuffer.Put( localizedBuffer.PeekGet(), localizedBuffer.TellPut() ); + } + } + + m_pProgress->UpdateProgress( PROGRESS_GOTRESLIST ); + + // due to its size, the bsp load is a lengthy i/o operation + // this causes a non-batched async i/o operation to commence immediately + if ( !m_pLoaders[RESOURCEPRELOAD_MODEL]->CreateResource( pMapName ) ) + { + // very bad, a valid bsp is critical + DevWarning( "QueuedLoader: Failed to mount BSP '%s', Non-Optimal Loading.\n", pMapName ); + m_bActive = false; + return false; + } + + // parse the raw resource list into loader specific dictionaries + ParseResourceList( resListBuffer ); + + // run the distributed precache loaders, generating a batch of i/o requests + GetJobRequests(); + + // event each loader to discard dead resources + PurgeUnreferencedResources(); + + // sort and start async fulfilling the i/o requests + // waits for all "must complete" jobs to finish + SubmitBatchedJobsAndWait(); + + // progress is only relevant during preload + // normal load process takes over any progress bar + // disable progress tracking to prevent any late queued operation from updating + m_pProgress->EndProgress(); + + return m_bActive; +} + +//----------------------------------------------------------------------------- +// Signal the end of the queued loading process, i/o will still be in progress. +//----------------------------------------------------------------------------- +void CQueuedLoader::EndMapLoading( bool bAbort ) +{ + if ( !m_bStarted ) + { + // already stopped or never started + return; + } + + ///////////////////////////////////////////////////// + // TBD: Cannot abort!!!! feature has not been done // + ///////////////////////////////////////////////////// + bAbort = false; + + if ( m_bActive ) + { + if ( bAbort ) + { + PurgeQueue(); + } + else + { + // finish all outstanding priority jobs + SubmitPendingJobs(); + while ( g_nHighPriorityJobs != 0 || g_nJobsToFinishBeforePlay != 0 ) + { + // yield some time + g_pThreadPool->Yield( MAIN_THREAD_YIELD_TIME ); + } + } + + m_EndTime = Plat_MSTime(); + m_bActive = false; + + // transmit the end map event + for ( int i = RESOURCEPRELOAD_UNKNOWN+1; i < RESOURCEPRELOAD_COUNT; i++ ) + { + if ( m_pLoaders[i] ) + { + m_pLoaders[i]->OnEndMapLoading( bAbort ); + } + } + + // free any unclaimed anonymous buffers + int iIndex = m_AnonymousJobs.First(); + while ( iIndex != m_AnonymousJobs.InvalidIndex() ) + { + FileJob_t *pFileJob = m_AnonymousJobs[iIndex]; + if ( pFileJob->m_bFreeTargetAfterIO && pFileJob->m_pTargetData ) + { + g_pFullFileSystem->FreeOptimalReadBuffer( pFileJob->m_pTargetData ); + pFileJob->m_pTargetData = NULL; + } + g_nAnonymousIOMemory -= pFileJob->m_nActualBytesRead; + iIndex = m_AnonymousJobs.Next( iIndex ); + } + m_AnonymousJobs.Purge(); + + if ( g_nIOMemory || g_nAnonymousIOMemory ) + { + // expected to be zero, otherwise logic flaw + DevWarning( "CQueuedLoader: Unclaimed I/O memory: total:%d anonymous:%d\n", (int)g_nIOMemory, (int)g_nAnonymousIOMemory ); + g_nIOMemory = 0; + g_nAnonymousIOMemory = 0; + } + + // no longer needed + m_AdditionalResources.RemoveAll(); + } + + g_pFullFileSystem->EndMapAccess(); + m_bStarted = false; +} + +//----------------------------------------------------------------------------- +// Returns true if loader is accepting queue requests. +//----------------------------------------------------------------------------- +bool CQueuedLoader::IsMapLoading() const +{ + return m_bActive; +} + +//----------------------------------------------------------------------------- +// Returns true if loader is working on same map as last load +//----------------------------------------------------------------------------- +bool CQueuedLoader::IsSameMapLoading() const +{ + return m_bActive && m_bSameMap; +} + +//----------------------------------------------------------------------------- +// Returns true if the loader is idle, indicates all i/o and work has completed. +//----------------------------------------------------------------------------- +bool CQueuedLoader::IsFinished() const +{ + return ( m_bActive == false && g_nActiveJobs == 0 && g_nQueuedJobs == 0 ); +} + +//----------------------------------------------------------------------------- +// Returns true if loader is batching +//----------------------------------------------------------------------------- +bool CQueuedLoader::IsBatching() const +{ + return m_bBatching; +} + + +//----------------------------------------------------------------------------- +// Returns true if loader is batching +//----------------------------------------------------------------------------- +bool CQueuedLoader::IsDynamic() const +{ + return m_bDynamic; +} + + +int CQueuedLoader::GetSpewDetail() const +{ + int spewDetail = loader_spew_info.GetInt(); + if ( spewDetail <= 0 ) + { + return spewDetail; + } + + return 1 << ( spewDetail - 1 ); +} + + +void CQueuedLoader::DynamicLoadMapResource( const char *pFilename, DynamicResourceCallback_t pCallback, void *pContext, void *pContext2 ) +{ + Assert( m_bActive == false ); + + m_bActive = true; + m_bDynamic = true; + m_DynamicFileName = pFilename; + m_pfnDynamicCallback = pCallback; + m_pDynamicContext = pContext; + m_pDynamicContext2 = pContext2; + + CleanQueue(); + AddResourceToTable( m_DynamicFileName ); + + // run the distributed precache loaders, generating a batch of i/o requests + GetJobRequests(); + + // sort and start async fulfilling the i/o requests + Assert( m_bBatching && g_nActiveJobs == 0 ); + SubmitBatchedJobs(); + Assert( !m_bBatching ); +} + +void CQueuedLoader::QueueDynamicLoadFunctor( CFunctor* pFunctor ) +{ + AUTO_LOCK( m_FunctorQueueMutex ); + m_FunctorQueue.AddToTail( pFunctor ); +} + +bool CQueuedLoader::CompleteDynamicLoad() +{ + Assert( m_bActive && m_bDynamic && !m_bBatching ); + bool bDone = true; + if ( m_bDynamic ) + { + CUtlVector< CFunctor* > functors; + { + AUTO_LOCK( m_FunctorQueueMutex ); + functors.Swap( m_FunctorQueue ); + } + FOR_EACH_VEC( functors, i ) + { + ( *functors[i] )(); + functors[i]->Release(); + } + + { + AUTO_LOCK( m_FunctorQueueMutex ); + bDone = m_FunctorQueue.Count() == 0 && g_nQueuedJobs == 0 && g_nActiveJobs == 0; + } + + if ( bDone ) + { + if ( m_pfnDynamicCallback ) + { + ( *m_pfnDynamicCallback )( m_DynamicFileName, m_pDynamicContext, m_pDynamicContext2 ); + } + m_DynamicFileName.Clear(); + m_bActive = false; + m_bDynamic = false; + } + } + return bDone; +} + +void CQueuedLoader::QueueCleanupDynamicLoadFunctor( CFunctor* pFunctor ) +{ + Assert( ThreadInMainThread() ); + + m_CleanupFunctorQueue.AddToTail( pFunctor ); +} + +bool CQueuedLoader::CleanupDynamicLoad() +{ + Assert( ThreadInMainThread() ); + + FOR_EACH_VEC( m_CleanupFunctorQueue, i ) + { + ( *m_CleanupFunctorQueue[i] )(); + m_CleanupFunctorQueue[i]->Release(); + } + m_CleanupFunctorQueue.Purge(); + + return true; +} + |