Threading & Async Loading¶
Doriax includes thread utilities and async loading support for resource-heavy work such as loading large models, generating terrain, or pre-warming audio pools in the background. Threading support depends on the target platform and build options.
Why async loading?¶
Synchronous loading freezes the game loop for the duration of the load. For large assets (multi-megabyte models, terrain heightmaps, audio files), this produces noticeable hitches. Async loading moves the heavy work to a worker thread while the main thread continues rendering a loading screen or animating a progress indicator.
Automatic async resource loading¶
The simplest way to use threading is to enable async loading and let the engine handle the rest. When enabled, the resource pools (textures, models, sounds, shaders) load data on a worker thread pool automatically:
While resources load, the entity is created immediately and the resource appears when
its data is ready. Query Engine.getQueuedResourceCount() to see how many resources
are still pending.
Async thread scope (manual background work, C++)¶
For your own background work, bracket it with Engine::AsyncThreadScope (an RAII
helper around Engine::startAsyncThread() / Engine::endAsyncThread()). While inside
an async scope, GPU resource creation is deferred to a commit queue instead of
touching the graphics API from the wrong thread. When the outermost scope ends, the
queue is flushed automatically (you can also flush manually with
Engine::commitThreadQueue() from the main thread):
Use Engine::isAsyncThread() to check whether the current code runs inside an async
scope.
Thread pool¶
ThreadPoolManager is the worker pool behind async resource loading. In C++ you can
also dispatch your own tasks to it. enqueue is an instance method — get the singleton
first:
The pool is initialized on demand with one thread per hardware core by default;
Engine::setMaxResourceLoadingThreads(n) re-initializes it with a specific worker
count. The pool is shared across the engine; avoid blocking workers with long
synchronous operations. (In Lua only initialize, shutdown, and getQueueSize are
exposed — task dispatch is C++-only.)
Tracking loading progress¶
ResourceProgress is a static tracker that reports per-resource and overall build
progress while async loads are running. Read the overall progress to drive a loading
bar:
OverallBuildProgress carries totalProgress (0–1), totalBuilds,
completedBuilds, currentBuildName, currentBuildType, and hasActiveBuilds. Your
own loaders can participate by calling ResourceProgress.startBuild(id, type, name),
updateProgress(id, value), and completeBuild(id) / failBuild(id).
Platform caveats¶
| Platform | Threading behavior |
|---|---|
| Desktop (Windows, Linux, macOS) | Full std::thread support; thread pool fully functional |
| Android | Full thread support via POSIX threads |
| iOS | Full thread support via POSIX threads |
| HTML5 (Emscripten) | Threads require SharedArrayBuffer and COOP/COEP headers on the server. Without pthreads, the engine defines NO_THREAD_SUPPORT and runs single-threaded |
For web, always test async loading with pthreads enabled in your deployment environment.
Practical rules¶
- Prefer the built-in async loading (
Engine.asyncLoading = true) over manual threads — the resource pools already handle the thread-safety details. - For manual background work, wrap it in
Engine::AsyncThreadScopeso GPU resource creation is deferred to the commit queue instead of running on the wrong thread. - Spawn physics bodies and mutate scene hierarchies only on the main thread.
- Do not hold scene references or entity handles on worker threads without synchronization.
- Test async loading on the slowest target device — the loading time and race conditions may not reproduce on a fast desktop machine.
Loading screens¶
SceneManager shows a loading screen for you. Pick a UI scene as the Loading Scene
in Project Settings → General → Loading, turn on
Async Loading there, and every loadScene goes through it:
- The loading scene appears on top of the running scene, which keeps running below it but no longer gets UI input.
- After the loading delay, the old scene is replaced behind it. The new scene's entities and scripts are built on the main thread, so the game freezes for that moment with the loading scene on screen.
- With async loading on, the new scene's textures, sounds and models stream in on worker
threads while the loading scene keeps animating. Once they are loaded,
Engine.onSceneLoadedfires and the loading scene is removed, or fades out first if a script holds the load.
The same settings can be made from code, at startup:
Engine::setAsyncLoading(true);
SceneManager::setLoadingScene("Loading");
SceneManager::setLoadingDelay(0.3f);
// LoadingScreen.cpp, on the Loading scene
void LoadingScreen::onUpdate() {
if (!SceneManager::isLoading()) return;
bar->setWidth((unsigned int)(360 * SceneManager::getLoadingProgress()));
}
A level script starts gameplay in
Engine.onSceneLoaded: the level is loaded,
and the loading scene still covers it while it places the player and camera. See
SceneManager.setLoadingScene.
Preloading the next scene¶
A loading screen hides the wait; preloading removes most of it.
SceneManager.preloadScene
loads the next scene's textures, sounds and models on worker threads while the player is
still in the current one. When loadScene runs later, only the scene's entities are
built, and the loading screen closes almost at once.