//========= 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;
}