8: Assets and Factories
Last Updated: 10/14/2020Note: This is NOT engine documentation. I write these articles as I learn. As such, information present here may be incorrect or out-of-date. I'm leaving these pages online for historical purposes only.
Loading meshes, textures, and shaders can be computationally expensive, especially on slower hardware. The engine should avoid doing expensive work and cache wherever possible. However, the typical way of doing this involves rigid factory functions, but I want to avoid that as much as possible because it breaks OOP. Requiring the client programmers to manage all assets manually in a thread-safe way would lead to a lot of duplicated code or additional boilerplate, which is undesirable.
Variadic templates to the rescue
My compromise is a variadic-template manager factory cache that calls constructors. Here is the material variant, shown below:/**
Helper to get a material by type. If one is not allocated, it will be created. Supports constructors via parameter pack
@param args arguments to pass to material constructor if needed
*/
template<typename T, typename ... A>
static Ref<T> AccessMaterialOfType(A ... args){
Ref<T> mat;
mtx.lock();
std::type_index t(typeid(T));
if (materials.find(t) != materials.end()){
mat = materials.at(t);
}
else{
mat = new T(args...);
materials.insert(std::make_pair(t,mat));
}
mtx.unlock();
return mat;
}
And sample usage:
int a =3, b = 5, c = 4;
auto cachedMat = Material::Manager::AccessMaterialOfType<CustomMaterial>(a, b, c);
The Material::Manager class maintains a thread-safe cache. The AccessMaterialOfType method checks the cache,
and if the material of that type is already loaded, it simply returns the cached material. Otherwise, the appropriate constructor
is selected via the variadic arguments, and a reference to that is stored in the cache and returned.
For the material system, checking by type alone is enough most of the time because generally only one instance of a particular type of material needs to be loaded, because draw settings are stored in MaterialInstances, which reference a single material. However, there are undoubtedly situations where multiple materials of the same type need to be loaded. In that case, the client programmer does not need to use the factory function at all and instead construct their Material normally and manage it themselves.
Ref<CustomMaterial> mat(new CustomMaterial(a,b,c));
In the cases where the default behavior is appropriate, Material::Manager provides this thread-safe cached factory that does not break OOP.
Leveraging reference counting
Because all materials and mesh assets are reference counted, once a material is in the cache, it will not be deallocated unless it is first removed from the cache. The Material Manager provides a thread-safe method to achieve this. But since assets are reference counted, it is safe to remove a material or mesh asset from the cache while it is still in use. It will remain allocated until its last user is deallocated, then its reference count will reach 0 and it will be deallocated. This is useful in dynamic-loading maps where you know that a material is not needed anymore but do not know what order the assets will be deallocated in. The system takes care of it for you.Next up: Coming soon!