Texture Caching, Concept art and more!

Published April 09, 2015
Advertisement
It's been a month or so since i last wrote a journal entry here, but this has not been due to lack of work on the game.

Rather the opposite in fact, i find myself spending so much time creating the game that i have very little time left in my life to write journal entries! Oh, what a first world problem it is.

Of many things i have busied myself with over the past month, one of them has been rewriting and optimising how resources are managed within the game.

I have settled on doing this in a particular way which i thought it worth sharing with the rest of the community.

Firstly, each resource has an ID key which is called a ResId, and is 32 bits long. This is a strongly typed value:BOOST_STRONG_TYPEDEF(UINT32, ResId);
This actually creates a class containing just the native type (UINT32) with various operators for assignment, copying and comparison. It ensures that I am forced to use "ResId" as my variable type all through my code and not get lazy and start using UINT32, which means that if i choose to change the size of ResId (for example to 64 bits) at a later date, this can be done relatively painlessly in one place.

ResId types are calculated at compile time by a pre-build step. The pre-build step is a perl script (attached at the bottom of this journal entry, if you are curious!), which enumerates a set of directories and builds a header file, which is full of definitions like this:/* Compile time hashes for files within the Textures directory. * Automatically generated by Util/makehashes.pl. DO NOT MODIFY * by hand! */#pragma once#define TEX_CONVEYOR (static_cast(0x4eec1561)) /* Textures/conveyor.png */#define TEX_CRATE_BREAKABLE (static_cast(0x64d3a7f5)) /* Textures/crate-breakable.png */#define TEX_CRATE_DYNAMITE (static_cast(0x57f6ebd3)) /* Textures/crate-dynamite.png */#define TEX_CRATE_MORTAR (static_cast(0xb1ddcc8a)) /* Textures/crate-mortar.png */
As you may have noticed, each of these is a simple CRC32 value of the constant name, e.g. 0x4eeC1561 is a CRC32 of "TEX_CONVEYOR". The script which generates these header files only updates them on disk in the event they need to change, to avoid needless rebuilds of the project dependencies.

Armed with this metadata which we built at compile time, we are now able to define a texture cache class:/** The TextureCache preloads all textures into SRVs and allows access to them via operator[]. * It may throw a std::runtime_exception when constructed with a directory name and DX11 object, * if it cannot load a texture. All textures are loaded from a virtual directory within the zip * file on PhysicsFS. */class TextureCache{private: /** All resources are stored in an unordered_map which uses a nerfed hash function. * Performing std::hash on ResId always just returns the same ResId, as ResId's are hashed * already at compile time. */ std::unordered_map, std::hash> textures; /** Used internally by operator[] */ ID3D11ShaderResourceView* Get(ResId id); /** Builds a descriptive string with an error text and error number from a windows stringtable * resource, and throws it as a string within a std::runtime_error exception object. */ void ReportError(HRESULT hr, UINT caption_id);public: /** Create empty but initialised object, used when assigning inplace */ TextureCache(); /** Copy constructor, usage of CComPtr above ensures that we don't lose our SRVs * when we copy, so this is mainly just to copy the unordered_map itself. */ TextureCache(const TextureCache ©); /** Initialise texture cache by reading all textures from a given directory using WIC. * The directory is virtualised via PhysicsFS. The DX11 object contains the context and * device upon which the SRVs are to be associated. */ TextureCache(DX11* dx11, const std::wstring &directory); /** Release all CComPtrs by nulling them */ void ReleaseAll(); ~TextureCache(); /** Look up any given texture resource by ID, e.g: * ID3D11ShaderResourceView* srv = cache[TEX_CRATE_DYNAMITE]; */ ID3D11ShaderResourceView* operator[] (ResId id);};
You might have noticed something strange about the hash function i am using. I have written my own hash function for "hashing" ResIds, which effectively does nothing. This is because ResIds are hashed at compile time, so any further implicit hashing would be a time wasting exercise:namespace std{ /** This is a nerfed hashing function for resource caches where unordered_map is used. * To prevent the unordered_map doing anything with the Resource Id, we just return it * verbatim here. Otherwise, we can't gaurantee that the hasher for unsigned int won't do * anything to the resource id, wasting time. There's no use hashing a value at runtime * that's already been created as a hashed value at compile time. */ template<> struct hash { inline size_t operator()(const ResId &v) const { return v; } };};
We can then construct a texture cache simply and effectively on startup, without any needless heap allocation as follows:textures = TextureCache(dx11, L"Textures/*.png");
When called, this constructor enumerates the content of the Textures directory, finding all PNG files, and loading them with WIC. Each one it loads, it builds a CRC32 value of the constant name (which it determines from the filename) and stores it in the unordered_map keyed by its ResId:TextureCache::TextureCache(DX11* dx11, const std::wstring &directory){ HRESULT hr; const FileList tex = Util::ReadDirectory(directory, true); for (FileListIter f = tex.begin(); f != tex.end(); f++) { CComPtr LoadedTexture; size_t texturelen = 0; const char* texturedata = Util::ReadWholeFile(*f, texturelen, true); if (texturedata) { hr = DirectX::CreateWICTextureFromMemory(dx11->Device, dx11->Context, (const uint8_t*)texturedata, texturelen, nullptr, &LoadedTexture); if(FAILED(hr)) { ReportError(hr, IDS_ERROR_LOADING_TEXTURE); } else { std::string processed = Util::ProcessFileName(Util::GetFileName(*f), "TEX_"); ResId crc32 = static_cast(Util::CRC32(processed)); DebugOut() << "Loaded texture " << processed << " (" << Util::WStringToAscii(*f) << "), ID: 0x" << std::hex << crc32; Util::SetDebugName(LoadedTexture, processed); textures[crc32] = LoadedTexture; } } else { ReportError(0, IDS_CANT_READ_TEXTURE); } }}
You might also have noticed that there is no operator[] method used for assignment. This is on purpose, so that if i accidentally ever type "cache[VALUE] = someSRV;" this will be caught as a compile-time error. Such an action is not expected and should be forbidden at this point.

There are a few gotcha's with this implementation, the most important being hash collisions. It is possible, but unlikely, that I can get hash collisions by using CRC32 to hash the resource IDs. If this does become a problem, I will look to moving to CRC64 or FNV-1a instead. Time will tell if this will prove to be a problem at all.

A future enhancement would be runtime loading of new textures, and releasing of unused textures to stay within a memory budget. Right now, i have enough memory and small enough number of textures that this isn't required.

So, enough code. I am sure your first question when looking at this journal entry was "well, who is that lady in the picture at the top of the page?"

The lady in question is the secret agent character for the game. Half way through the game she replaces the boss at intervals, giving you advice and subtle warnings about your manager's antics. Having enough free time to do so, and wanting to try my hand at drawing once more, i decided to create the concept for this character and start shading it myself.[table][td='33'][/td][td='33'][/td][td='33']secretagent.png[/td][td='33']secretagent-2.png[/td][td='33']secretagent-3.png[/td][/table]

As you can see from the image, i am now about half way through this and all i can say is that creating art takes a lot longer than creating code especially if it doesn't come naturally like it doesn't to me. For me to create reasonable art, i have to spend many tedious hours within GIMP, gently massaging pixels to be where i want them. Time will tell if i am able to create some art passable enough for the game, and of course feedback from anyone more artistically inclinded about my creation is always more than welcome!

On with the show, and back to the grindstone. Please comment and leave feedback if you have any questions!

Keep an eye out for my new article, coming to this site soon: How to automate deployment of your game!

3 likes 1 comments

Comments

Navyman

As a programmer I can understand that art does not come easy to all.

April 11, 2015 06:32 PM
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Advertisement