May 18 2009
In most modern games, asynchronous file loading is a standard requirement. This is especially true for open world games or games which never want to show a loading screen. On the surface, it seems like an easy thing to do: spawn a new thread and load files on request. However, there are a few complications which get in the way.
One of the most obvious issues is knowing when the file has actually been loaded. In an asynch environment, you issue a request, and some time later, the request is fulfilled. Most systems return a handle which the game can check periodically to see if the data has been read. This is fairly easy to implement, works well in a multithreaded environment, and is pretty easy to debug. Other alternatives are to use a callback mechanism or put the calling thread to sleep until the data is loaded. The sleep option is usually done in custom scripting languages, instead of trying to do it in C++.
A standard approach is to compress the data at build time and decompress at run time. If you are reading from DVD or Blu-ray, then it is practically a requirement due to the slow read speeds of those media. On the PS3 or XBox360, it is common to have your file system decompress on the fly, either on the SPUs or on another core. The complication is that you are no longer loading the data directly where it will be used. Instead, you need to load it a temporary buffer, decompress the data, then write it to the final memory location.
On some consoles, there is not unified memory. This means that you need to decide either ahead of time or at load time where the data will be loaded. On the PS3, half of the memory is “main RAM” while the other half in “graphics memory” (ignoring that data can be moved and read between the two systems). A simplistic approach is to put the data in the RAM segment based on the type of data…geometry to RSX memory, AI parameters to system RAM. Since some types of data can be in either segment, this approach limits how flexibility the memory arrangement is.
Most file systems use a Table of Contents (ToC) to manage where and how the data is loaded. Having a ToC for the data which is loaded gives:
- Position on disc
- Memory segment to load into
- Compression parameters
- Data type
- Resource Id
- Human readable name
A ToC also gives you flexibility during development. The ToC can either contain all of the data in a single packed file, or it can simply list the files which need to be loaded. Assembling a packed file can take a lot of time during development, especially when you have just changed one file. This arrangement also makes it easy to do single file overrides. Your nightly build system creates packed files for the game from checked in assets, but individuals can modify single files which are loaded seperately from the main packed file.
If you can override individual files, then you should look into hot-reloading of data: reloading data in the middle of the game so that artists can see texture changes without restarting, etc. On the prototype I’m working on, I have a PC app which monitors my processed file directory and sending a message over the network to the game to reload it. I have the file change logic on the PC in a single app, while the hot reloading is based in the game itself. Since I’m working on a multiplatform game, this makes it easy for any new platform to load in the file. Even the PC version registers itself with the file monitor app instead of scanning the directories explicitly.
One minor point to mention is that DX9 is not fully multithreaded. This means that if you are allocating vertex, index, or texture buffers, they all have to be done from the render thread. Microsoft explains it fairly well in this presentation. This restriction makes it a bit more complicated, but it mostly involves extra signaling between the loading and render thread.
Leave a Reply
You must be logged in to post a comment.