Skip to main content

Utilities

This module provides async task execution, model and texture loading and management, data serialization and deserialization using yaml and json and more

Asset manager

caution

Not yet exposed to lua

The asset manager currently is non-copyable and non-movable

  AssetManager();
~AssetManager();

AssetManager( const AssetManager& ) = delete;
AssetManager& operator=( const AssetManager& ) = delete;
AssetManager( AssetManager&& ) = delete;
AssetManager& operator=( AssetManager&& ) = delete;

The purpose of this class is to provide means to load, store and manage different kinds of assets, it is tied deeply with asset lifetime also.

Since geometry needs to be uploaded to the GPU and model files take time to load and those could also have textures, I had to come up with a way of loading models in an asynchronous way and not crash due to the OpenGL thread safety.

All I had to do was add a bool flag and right before rendering the frame, doo all the necessary OpenGL API calls on the main thread.

Asset types

Textures

There are two functions used for the texture loading

  auto addTexture( const std::string& textureName,
const std::string& texturePath ) -> Texture*;

// this one is just prepending the path as pathToProject/resources/textures/...
auto addTexture( const std::string& textureName ) -> Texture*;
tip

The default folder for textures is resources/textures/... so you should save the model as gltf and separate textures into its own folder

We also have a load from memory function but that is not yet implemented.

Meshes

  auto addMesh( const std::string& meshName, const std::string& meshPath ) -> Mesh*;
auto addMesh( const std::string& meshName ) -> Mesh*;

Each Mesh instance has its own VAO, EBO and VBO.

Because we have a per mesh buffer, we have a single vector of submeshes for an easier GPU "upload", a submesh looks like this

struct Submesh
{
uint32_t vertexOffest{ 0 };
uint32_t indexOffset{ 0 };
uint32_t indexCount{ 0 };
};

When we load a mesh, we parse each submesh and we insert vertices in the main Mesh.vertices vector, then we take the vertices.size value and assign it to the vertex offset variable in the submesh.

So if we had 0->10 vertices for the first mesh, and the second mesh is the same size in vertices, the offset will be as follows

submeshes.at(0).vertexOffset = 0
submeshes.at(1).vertexOffset = 10

I think you understand where this is going

Task manager

caution

Not yet exposed to lua

This is the class responsible for executing tasks on a separate thread.
It has a predefined number of threads initialized, I used std::thread::hardware_concurrency() as a member variable m_threadCount initializer.

The concept is as follows, we enqueue a lambda or a function and the task manager pops from the front of the queue and executes it, and so on.

The function for enqueueing is the following

 template <typename Func, typename... Args>
auto enqueue( Func&& func, Args&&... args )
-> std::future<std::invoke_result_t<Func, Args...>>
{
using return_type = std::invoke_result_t<Func, Args...>;
auto p_task = std::make_shared<std::packaged_task<return_type()>>(
std::bind( std::forward<Func>( func ), std::forward<Args>( args )... ) );

{
std::lock_guard lock( m_queue_mutex );
if ( m_stop )
{
throw std::runtime_error( "Thread pool is stopped" );
}
m_tasks.emplace( [p_task]() { ( *p_task )(); } );
}
m_cvar.notify_one();
return p_task->get_future();
}

Common use case is when we try to load a heavy model file and if we did that on the main thread, the UI would freeze for the remainder of the loading phase.