Skip to content

Custom Assets and Asset Management

George Kudrayvtsev edited this page Nov 19, 2013 · 4 revisions

Note: All namespaces in this article assume using namespace zen; for brevity's sake.

Zenderer was designed to optimize asset loading, with basic reference counting in place to ensure that no file is ever loaded more than once unnecessarily. This page outlines the architecture and use cases of the asset system, as well as the asset management API.

Qualifications

Assets are typically objects (in the OO sense) that are meant to be dynamically loaded from the disk, with some concept of binary data stored internally. The asset::zAsset abstract base class requires the following methods to be implemented:

  • LoadFromFile(string_t&) — All concepts of an asset in Zenderer must be able to load from the disk.
  • void* GetData() — All assets must be able to provide access to raw internal binary data. This should be primarily used by the asset implementation itself, if LoadFromExisting() (explained below) is defined.
  • Destroy()

The following methods are not required, but should be defined in order to ensure maximum versatility for the object:

  • LoadFromExisting(zAsset*) — Assets should be copyable. There is intentionally no copy constructor provided, because this may cause implicit copies that the user is unaware of. There is a very barebones default implementation, essentially performing a soft copy, that just copies asset metadata.
  • Reload() — Most assets in Zenderer are tied in some way to the current OpenGL context, and it's important for them to be able to be reloaded in some fashion, in case the user wants to recreate the context but not lose all of their work, toggle fullscreen mode, or something similar. Reload() gives the ability to use existing metadata (or raw data) to recreate the asset from scratch. In most cases, this would rely exclusively on being loaded from disk. It's understandable that it's sometimes impossible to load an asset again, such as in the case of textures loaded from raw data, as that data is lost when the relevant OpenGL context is destroyed.

Implementation Notes

Assets cannot be constructed without using the provided asset::zAssetManager API (described below). Constructors should be private, copy constructors should be = delete-ed The destructor's only functionality should be to call Destroy(), and perhaps do some logging.

All assets have an internal util::zLog reference to the static engine log. This should be used extensively in any loading functions in order to provide maximum feedback to the user.

Assets provide a concept of an "owner," which is just stored as a void* internally. This is a remnant of previous iterations of the engine, and is in place for when there is a need to create assets that already exist, but under a different context. For example, this was used in previous iterations to have multiple instances of a vertex mesh offloaded to separate vertex buffers. Without the concept of an "owner," a secondary instance could potentially refer to the wrong buffer, causing a crash when rendering.

Zenderer uses this feature to differentiate between identical assets that apply to different window contexts. Despite the fact that there can only be one context at a time (currently), the system is being used to limit, for example, textures to their respective windows.

Additionally, it's possible to pass additional settings directly to assets via the third parameter to the constructor. It's a void* argument, and should be interpreted according to what's expected by the asset itself. This is used in Zenderer to pass size data immediately to gui::zFont assets, instead of calling SetSize() later after loading.

Zenderer Assets

The assets used throughout the engine are the following:

Management

Zenderer comes with an API exclusively for the purpose of managing assets, in order to prevent memory leaks and unnecessary loading of assets. Users often times will not interact with the API directly, but will just pass on the instance to the various high-level graphics component.

The internal details of the API are fairly straightforward: there's a list of existing assets, and anytime the user wants to create an asset, the API checks this list for an asset that matches the one that should be loaded. If it exists, a reference counter is incremented and the original is returned. If it doesn't exist, a new one is loaded and added to the list.
Asset creation is a template, meaning the user must provide the type of asset they wish to create, since this then creates a cleaner interface for the inheritance-based architecture around asset::zAsset, and ensures that all child-class constructors are called as needed.

Create(string_t&) is the bread and butter of the API. It does all of the things described above and is used like so:

asset::zAssetManager Assets;
Assets.Init();

// We want to load a texture from the disk using the filename "SomeFile.png"
gfxcore::zTexture* TestTexture = Assets.Create<gfxcore::zTexture*>("SomeFile.png");

Recreate(string_t&) functions similarly to Create(), except it ignores any existing assets, creating a copy even if an instance already exists.

asset::zAssetManager Assets;
Assets.Init();

gfxcore::zTexture* T1 = Assets.Create<gfxcore::zTexture*>("SomeFile.png");
gfxcore::zTexture* T2 = Assets.Create<gfxcore::zTexture*>("SomeFile.png");
bool same = (T1 == T2);     // true

// A copy, not considering existing instances.
gfxcore::zTexture* T3 = Assets.Recreate<gfxcore::zTexture*>("SomeFile.png");
bool same = (T1 == T3);     // false

Cleanup should be done through Delete(zAsset*). This will search the internal instance list and delete the matching instance, if it exists. It will only delete if there are no more references to it, though. Any created assets will automatically be cleaned up when the zAssetManager instance goes out of scope, too, so beware. I typically just define a single manager at the beginning of my program and use it throughout. There is no need for templates in this method call, thanks to virtual destructors.

All of these methods do adequate logging to the static engine log, more-so with ZEN_DEBUG_BUILD set.
Here's a full example of typically use of the asset management API.

using namespace asset;

using util::LogMode;
using util::zLog;

zLog& Log = zLog::GetEngineLog();

zAssetManager Assets;
if(!Assets.Init())
{
    Log << Log.SetMode(LogMode::ZEN_FATAL) << Log.SetSystem("AssetMgr")
        << "Asset manager failed to initialize." << zLog::endl;
    return;
}

// Create a texture from a file.
gfxcore::zTexture* GameBackground = Assets.Create<gfxcore::zTexture>("BG.png");
if(GameBackground == nullptr)
{
    Log << Log.SetMode(LogMode::ZEN_FATAL) << Log.SetSystem("Game")
        << "Background texture failed to load." << zLog::endl;
    return;
}

// This is the same texture object as above.
gfxcore::zTexture* MenuBackground = Assets.Create<gfxcore::zTexture>("BG.png");

// (MenuBackground == GameBackground) == true

// This creates a copy of the texture object above.
gfxcore::zTexture* ScreenBackground = Assets.Recreate<gfxcore::zTexture>("BG.png");

// (MenuBackground != ScreenBackground) == true

// This decreases the number of references to the texture object.
// So MenuBackground is still valid.
Assets.Delete(GameBackground);

// This actually deletes the texture object.
Assets.Delete(MenuBackground);

// This creates a font using the settings parameter for custom sizing.
gui::fontcfg_t cfg;
cfg.size = 24;
gui::zFont* Font = Assets.Create<gui::zFont>("Font.ttf", nullptr,
                                             static_cast<void*>(&cfg));

// This implicitly destroys the ScreenBackground texture object.
Assets.Destroy();