A C++ resource manager that loads assets (.obj models and textures) asynchronously on a thread pool, caches them so they are never loaded twice, and works with any resource type through a small template-based API. A GLFW/OpenGL demo lets you spawn models at runtime and switch between single-threaded and multithreaded loading to measure the difference.
- Async loading on a custom
ThreadPool(std::thread, mutex + condition variable), so the render loop never blocks on disk or parsing work. - Multithreaded
.objparsing: in multithread mode the model receives the thread pool and can split its parsing across workers. - Mono / multi-thread switch at runtime (press
R), with the load time of each resource printed to the console. - Thread-safe resource cache keyed by file name:
Get<T>()returns an already-loaded asset instead of reloading it. - Generic, extensible API: any class deriving from
IResourcethat is constructible from(std::string)or(std::string, ThreadPool&)can be managed with no change to the manager (Open/Closed Principle). - Completion callbacks with arbitrary arguments (
createAsync<T>(id, callback, args...)). - OpenGL stays on the main thread: workers only parse/decode on the CPU; GPU uploads are posted back to the main thread (
Application::postToMain) and executed inrunMainThreadTasks(). - Demo scene with a scene graph, directional Blinn-Phong lighting, textures and a mouse-look camera.
Main thread (owns OpenGL context) Worker threads
ββββββββββββββββββββββββββββββββββ createAsync ββββββββββββββββββββββββββ
β Application / Graph / Camera β βββββββββββββββΊ β ThreadPool β
β β β job queue (mutex+CV) β
β runMainThreadTasks() β βββββββββββββ¬βββββββββββββ
β ββ model->uploadToGPU() β β runs
β ββ scene graph updates β βββ postToMain ββββββββββββββ€
ββββββββββββββββ¬ββββββββββββββββββ (GL work only here) βΌ
β Get<T>(id) ββββββββββββββββββββββββββ
βΌ β ResourceManager:: β
ββββββββββββββββββββββββββββββββββ β Create<T>(id) β
β ResourceManager β ββββββββββββββ β (parse file, store in β
β unordered_map<string, β stores β cache under a mutex) β
β unique_ptr<IResource>> β ββββββββββββββββββββββββββ
ββββββββββββββββββββββββββββββββββ
| Component | Role |
|---|---|
IResource |
Empty polymorphic base class for every resource type (Model, Texture, β¦). |
ResourceManager |
Owns the cache (mutex-protected unordered_map), creates resources, exposes Create, createAsync, Get, Delete, clearAll. Constructed with a bool multithread flag. |
ThreadPool |
Fixed pool (hardware_concurrency() - 1 workers by default) with a shared job queue. Accepts free functions, lambdas and member functions. |
| Construction dispatch | if constexpr + std::is_constructible_v picks T(id, pool) in multithread mode (so T can parallelize internally) or T(id) otherwise. |
| Main-thread task queue | Application::postToMain / runMainThreadTasks hand finished work back to the GL thread. |
Press 1β4 once: the model is loaded asynchronously, uploaded to the GPU on the main thread when ready, then added to the scene. Press the same key again: the model is fetched from the cache (Get<Model>) and a new instance is added to the scene without reloading.
- Windows
- CMake β₯ [3.24]
- A compiler supporting C++[20] (MSVC recommended)
- Dependencies ([vendored in
dependencies/]):
git clone https://github.com/Potatoes251/ResourceManager.git
cd ResourceManager
cmake -S . -B build
cmake --build build --config ReleaseRun the executable from the build folder. Make sure the assets/ folder (models, textures, shaders) is reachable from the working directory.
| Input | Action |
|---|---|
1 |
Add the Diablo model (diablo3_pose.obj); pressing again adds another instance |
2 |
Add Tyra (tyra.obj) |
3 |
Add the Dragon (dragon.obj) |
4 |
Add the Armadillo (armadillo.obj) |
R |
Wipe the scene and restart, toggling between multithread and single-thread loading |
| Mouse | Look around |
[WASDQE] |
Move the camera |
| Close window | Quit |
The app starts in multithread mode; each press of R flips the mode. The console prints the mode and load time for every resource:
Parsed dragon.obj in 0.412300s (multithread)
- Press
1β4to load the models and note the times printed in the console. - Press
Rto wipe the scene; the mode flips (check the console output formultithread/singlethread). - Load the same models again and compare.
Caveats
- Hot cache: the OS caches recently read files, so the second run can be faster regardless of threading. Alternate the order or restart the app between tests.
- Model size: with small models, thread overhead can make multithreading slower. Use large
.objfiles for meaningful results.
ResourceManager rm(true /* multithread */);
// Synchronous load on the calling thread
rm.Create<Texture>("container.jpg");
// Asynchronous load, callback runs on the worker once the model is parsed
rm.createAsync<Model>("dragon.obj",
[](Model* model, Application* app) {
app->postToMain([=] { model->uploadToGPU(); }); // GL calls on main thread only
},
&app);
// Later: fetch from the cache, no reload
Model* dragon = rm.Get<Model>("dragon.obj");- Create a class deriving from
IResource. - Give it a constructor
T(std::string path)orT(std::string path, ThreadPool& pool)if it can parallelize its own loading. - Use
rm.Create<T>(path),rm.createAsync<T>(path, ...)andrm.Get<T>(path).
The manager itself needs no modification.
- Worker threads never call OpenGL; GPU uploads go through the main-thread task queue.
- The resource map and the pool's job queue are each protected by a
std::mutex. - Workers are signalled with a
std::condition_variable; shutdown uses an atomic stop flag.
- Lock-free job queue with atomics
- Per-thread work queues and work stealing
- Compile-time string-ID hashing for cache keys instead of
std::string - Resource dependency tracking (model β textures)
- Prioritization of threads by task type
- CPU affinity experiments (
SetThreadIdealProcessor) - More resource types (shaders, sounds, scripts)