-
Notifications
You must be signed in to change notification settings - Fork 2
Threading Model
This page documents every threading primitive, thread-safety contract, and concurrency pattern in SparkEngine. Understanding these rules is critical for avoiding deadlocks, data races, and main-thread stalls.
Source: Threading infrastructure spans Utils/, Engine/Persistence/, Engine/Networking/, Engine/AI/, Engine/ECS/Systems/, and Graphics/.
SparkEngine uses a main-thread-primary architecture. The main thread owns:
- The ECS registry (EnTT)
- Physics simulation (Jolt Physics)
- Graphics rendering (D3D11/D3D12/Vulkan)
- ImGui editor UI
- Coroutine scheduling
Background threads handle:
- Job system work stealing (CPU-bound tasks)
- Texture streaming (async I/O)
- Database queries (connection pool)
- Console subprocess I/O (pipe reader)
- Async logging (writer thread)
| Subsystem | Thread-Safe? | Pattern |
|---|---|---|
JobSystem |
Yes | Thread pool + mutex + CV |
ThreadSafeQueue |
Yes | Bounded FIFO + mutex |
SimpleConsole |
Yes | Mutex-guarded log sink |
Logger |
Yes | Async writer thread + atomic levels |
ConsoleProcessManager |
Partial | Pipe threads + command queue |
NetworkManager |
Partial | 4-mutex hierarchy |
AsyncDatabasePool |
Yes | Worker pool + callbacks on main |
TextureStreaming |
Yes | Streaming threads + mutex |
PhysicsSystem |
No | Main thread only |
GraphicsEngine |
No* | Main thread render, atomic frame state |
ParallelSystemExecutor |
No* | Dispatches to JobSystem from main |
ParallelPerceptionSystem |
Partial | Snapshot → parallel → writeback |
CoroutineScheduler |
No | Main thread only |
ECS (EnTT registry) |
No | Main thread only (use snapshots) |
* Call from main thread only; internal state may use atomics.
Source: SparkEngine/Source/Utils/JobSystem.h
The JobSystem singleton provides a fixed-size worker pool for CPU-bound tasks.
Main Thread Worker Threads (N = hardware_concurrency)
| |
+-- Submit(task) ------------> m_jobQueue (mutex + CV)
| |
+-- ParallelFor(range) ------> splits into batches → Submit each
| |
+-- future.get() <------------ worker completes, promise fulfilled
| Primitive | Purpose |
|---|---|
std::atomic<bool> m_initialized |
Guard against double-init |
std::atomic<bool> m_stop |
Shutdown signal |
std::mutex m_queueMutex |
Guards m_jobQueue
|
std::condition_variable m_condition |
Wakes workers on new jobs |
std::vector<std::thread> m_workers |
Worker thread pool |
// Submit a single task
auto future = JobSystem::Get().Submit([]{ return ExpensiveComputation(); });
auto result = future.get(); // Blocks until complete
// Parallel loop
JobSystem::Get().ParallelFor(0, 1000, [](int i){ ProcessItem(i); });Shutdown() sets m_stop = true, notifies all workers via m_condition.notify_all(), and joins all threads. Pending jobs in the queue are abandoned.
Source: SparkEngine/Source/Utils/ThreadSafeQueue.h
A bounded FIFO queue used by NetworkManager and AudioEngine for cross-thread message passing.
| Method | Behavior |
|---|---|
TryPush(item) |
Returns false if at capacity |
ForcePush(item) |
Always enqueues (unbounded variant) |
TryPop() |
Returns std::optional<T>, non-blocking |
PopAll() |
Drains entire queue into vector (batch efficiency) |
Single std::mutex guards all operations. No condition variable — callers poll or use PopAll() each frame.
Source: SparkEngine/Source/Engine/Networking/NetworkManager.h
NetworkManager uses four mutexes with a strict acquisition order:
m_stateMutex → m_queueMutex → m_handlerMutex
↑
m_clientsMutex (separate hierarchy)
m_inputMutex (separate hierarchy)
Rule: Never acquire a higher-order mutex while holding a lower-order one. Violating this order causes deadlock.
| Mutex | Protects |
|---|---|
m_stateMutex |
m_role, m_connectionState, m_localClientID
|
m_queueMutex |
m_outgoingQueue, m_incomingQueue
|
m_handlerMutex |
m_handlers (message handler map) |
m_clientsMutex |
m_clients (connected client map), m_nextClientID
|
m_inputMutex |
m_pendingInputs (client input states) |
-
std::atomic<uint32_t> m_nextNetworkID{1}— Lock-free network ID generation
Socket operations run on the main thread during Update(). The mutexes protect against concurrent Send() calls from game systems that may run on JobSystem workers.
Source: SparkEngine/Source/Engine/Persistence/AsyncDatabase.h, SparkEngine/Source/Engine/Persistence/AsyncDatabase.cpp
TrinityCore-inspired architecture: one database connection per worker thread, plus a dedicated sync connection for the main thread.
Main Thread Worker Threads (1 per connection)
| |
+-- AsyncQuery(stmt, params) -----> m_workQueue (mutex + CV)
| |
+-- ProcessCallbacks() <----------- m_completedCallbacks (mutex)
| |
+-- SyncQuery(stmt, params) ------> m_syncConnection (m_syncMutex)
| Primitive | Purpose |
|---|---|
std::vector<std::thread> m_workers |
Worker pool |
std::mutex m_queueMutex + m_queueCV
|
Work queue synchronization |
std::mutex m_syncMutex |
Guards main-thread sync connection |
std::mutex m_callbackMutex |
Guards completed callback list |
std::atomic<bool> m_open |
Pool state |
std::atomic<bool> m_stopping |
Graceful shutdown |
std::atomic<int> m_pendingCount |
Outstanding query count |
ProcessCallbacks() must be called on the main thread every frame. Async query results accumulate in m_completedCallbacks; without draining, callbacks never fire and memory grows.
m_stopping.store(true)-
m_queueCV.notify_all()— wakes all workers - Workers exit their wait loops and terminate
- Join all worker threads
- Close all connections
Source: SparkEngine/Source/Graphics/TextureStreaming.cpp
Dedicated streaming threads load textures from disk without blocking the main thread.
| Primitive | Purpose |
|---|---|
std::vector<std::thread> m_streamingThreads |
Loader threads |
std::mutex m_streamingMutex + m_streamingCondition
|
Request queue sync |
std::mutex m_texturesMutex |
Guards texture cache |
std::atomic<bool> m_shouldStop |
Shutdown flag |
StreamingThreadFunction() waits on m_streamingCondition for new requests, pops from m_streamingQueue, loads the texture, and stores in the cache under m_texturesMutex.
Source: SparkEngine/Source/Utils/Logger.h, SparkEngine/Source/Utils/Logger.cpp
Log messages are queued and written by a background thread. This prevents logging from stalling the main thread during file I/O.
| Primitive | Purpose |
|---|---|
std::thread m_writerThread |
Background log writer |
std::mutex m_logMutex + m_queueCV
|
Message queue sync |
std::atomic<bool> m_initialized |
Init guard |
std::atomic<bool> m_asyncEnabled |
Async mode toggle |
std::atomic<bool> m_stopThread |
Shutdown signal |
std::atomic<LogLevel> m_globalLevel |
Level filter (lock-free reads) |
std::atomic<LogLevel> m_categoryLevels[] |
Per-category levels |
SPARK_LOG_*() macros are safe to call from any thread. Messages are queued under m_logMutex and dispatched by the writer thread to registered sinks (stderr, file, editor).
Source: SparkEngine/Source/Utils/Process.h
Cross-platform RAII wrapper for child processes with optional stdin/stdout/stderr piping. Uses CreateProcessW on Windows and fork/execvp on Linux/macOS. Provides a fluent Process::Builder API for launching subprocesses, capturing output, and managing lifecycle (wait, poll, kill).
Used by ConsoleProcessManager for SparkConsole IPC and CrashHandler for the detached crash reporter.
Source: SparkEngine/Source/Utils/ConsoleProcessManager.h
Manages the SparkConsole.exe subprocess via Spark::Process with stdin/stdout pipes.
| Primitive | Purpose |
|---|---|
std::optional<Process> m_process |
Subprocess handle (piped stdin/stdout) |
std::thread m_consoleThread |
Pipe reader thread |
std::atomic<bool> m_shouldStopThread |
Shutdown flag |
std::atomic<bool> m_initialized |
Init guard |
std::atomic<bool> m_consoleRunning |
Subprocess alive state |
std::mutex m_messageMutex |
Guards m_messageQueue (outgoing) |
std::mutex m_commandMutex |
Guards m_commandQueue (incoming) |
The console thread reads from the subprocess pipe and pushes commands into m_commandQueue. The main thread calls ProcessCommands() each frame to drain the queue and execute commands.
Source: SparkEngine/Source/Engine/ECS/Systems/ParallelSystemExecutor.h
Systems declare their read/write component access:
executor.DeclareReads<Transform>(renderSystem);
executor.DeclareWrites<Transform>(physicsSystem);
executor.BuildSchedule();BuildSchedule() groups systems into parallel batches where no write-write or read-write conflicts exist.
Batch 0: [PhysicsSystem, AudioSystem] ← parallel (no conflicts)
Batch 1: [AnimationSystem] ← sequential (writes Transform)
Batch 2: [RenderSystem, AISystem] ← parallel (no conflicts)
- Single-system batches execute directly on the main thread
- Multi-system batches submit to
JobSystemand wait for all futures - The executor itself is NOT thread-safe — call only from the main thread
Source: SparkEngine/Source/Engine/AI/ParallelPerception.h, SparkEngine/Source/Engine/AI/ParallelPerception.cpp
Main Thread:
1. RebuildSpatialIndex(world) — build Octree from entity positions
2. GatherAgentData(world) — snapshot agent state into m_agentJobs
3. UpdateAllAgents() — dispatch to JobSystem
Worker Threads:
4. ProcessAgentPerception(job) — query Octree (read-only), check sight/hearing
Main Thread:
5. WriteBackResults(world) — copy results back to ECS
Key insight: Workers never touch the ECS registry. They read from the Octree (immutable during processing) and write to per-agent job data (no sharing between agents). This eliminates all data races without locks.
Source: SparkEngine/Source/Physics/PhysicsSystem.h
@note PhysicsSystem is NOT thread-safe. Call all public methods from the
main game thread. The physics simulation runs on the calling thread
inside Update().
No threading primitives. Jolt Physics supports multithreaded job dispatch. All raycasts, collision queries, and body manipulation must happen on the main thread.
Source: SparkEngine/Source/Engine/Coroutine/CoroutineTypes.h
C++20 coroutines (WaitForSeconds, WaitForFrames, WaitUntil) yield and resume on the main thread only. The CoroutineScheduler ticks each frame and resumes ready coroutines. No threading involved.
These subsystems must only be called from the main thread:
- PhysicsSystem — all public methods
- ECS registry (EnTT) — entity creation, component access, iteration
- GraphicsEngine — render passes, resource creation
- CoroutineScheduler — tick and resume
-
ParallelSystemExecutor — call
Execute()from main; workers run on JobSystem - ParallelPerceptionSystem — Gather/WriteBack from main; processing on workers
- Editor UI — all ImGui rendering
| Thread | Owner | Purpose |
|---|---|---|
| N worker threads | JobSystem |
CPU-bound tasks, ECS parallel batches |
| M streaming threads | TextureStreaming |
Async texture loading |
| K database workers | AsyncDatabasePool |
Async SQL queries |
| 1 writer thread | Logger |
Async log sink dispatch |
| 1 console thread | ConsoleProcessManager |
Subprocess pipe I/O |
auto future = JobSystem::Get().Submit([]{ return ComputeNavMesh(); });
// ... do other work ...
auto mesh = future.get(); // Block when result neededexecutor.DeclareReads<Transform>(renderSystem);
executor.DeclareWrites<Velocity>(physicsSystem);
executor.BuildSchedule();
executor.Execute(world, dt); // Main thread — workers handle batchesauto future = db.AsyncQuery(STMT_LOAD_PLAYER, {playerId});
// Later, on main thread:
db.ProcessCallbacks(); // Fires completion handlersSPARK_LOG_INFO(LogCategory::Core, "Message from any thread");| Pattern | Why It's Wrong | Fix |
|---|---|---|
| Access ECS from worker thread | EnTT is not thread-safe | Use snapshots or the Gather-Process-Writeback pattern |
| Call Physics from non-main thread | Jolt Physics dispatch is thread-safe; query from any thread | Queue physics commands for main thread |
| Reverse NetworkManager lock order | Deadlock | Always: state → queue → handler |
| Hold mutex during long I/O | Main thread stalls | Use async patterns (JobSystem, AsyncDB) |
| Busy-wait on atomics | CPU waste | Use std::condition_variable
|
Create std::thread directly |
Unmanaged lifetime | Use JobSystem::Submit() instead |
-
NetworkManager: Acquire locks in order:
m_stateMutex→m_queueMutex→m_handlerMutex - General: Never hold two mutexes from different subsystems simultaneously
- JobSystem: Never submit a job that blocks waiting for another job (potential pool exhaustion)
-
AsyncDatabase: Never call
SyncQuery()from a worker thread (deadlocks onm_syncMutex) -
Main thread: Never block indefinitely on a
std::future— use timeouts or poll
- Job System — Detailed job system API and usage
- Persistence System — AsyncDatabasePool architecture
- Networking — Network message threading
- Entity Component System — ECS execution order
- AI and Navigation — Parallel perception system
Published from 173799a3d275. Edit the canonical source in wiki/.
- Documentation
- Docs route
- Wiki index
- Guides
- Tutorials
- Samples
- Examples
- API Reference
- API route
- Reference
- Build Guide
- Dependencies
- FAQ
- Changelog
- Roadmap
- Contributing
- Code of Conduct
- Home
- FAQ
- Getting Started
- Quick-Start Tutorial
- Making Your First Game
- Making Your First Multiplayer Game
- Artist Workflow Guide
- Editor Walkthrough
- Migration Guide
- How SparkEngine Works
- Architecture Overview
- Engine Architecture Flowchart
- Creating a Game Module
- Game Modules (catalog)
- Entity Component System
- Rendering and Graphics
- Physics
- Cloth Simulation
- Audio
- Input System
- Camera System
- Scripting with AngelScript
- Visual Scripting
- AI and Navigation
- Animation
- 2D Systems
- Networking
- Dedicated Server
- Multiplayer Quick Start
- Area Server Architecture
- Scene Management
- Large World Support
- Collaborative Editing
- Coroutine System
- Event System
- Event Response System
- Job System
- UI System
- UI Layout Extensions
- Localization
- Dialogue System
- Destruction System
- Replay System
- Achievement System
- Loading System
- Mod System
- Content Delivery
- Tween System
- Memory Integrity
- Gameplay Systems
- Terrain and Procedural Generation
- Save System
- Persistence System
- Day Night Cycle and Weather
- Cinematic Sequencer
- Runtime Prefabs
- SparkEditor
- Editor Tutorials
- SparkConsole
- SparkDaemon
- Shader Pipeline
- Asset Pipeline
- Asset Validation
- Asset Migration
- Game Packaging
- Online Services
- DataTable System
- Loot and Crafting System
- CSG System
- Font System
- Timer Manager
- Movie Render Pipeline
- HLOD and World Partition
- Remote Debug System
- Selection Manager
- Asset Dependency Graph
- Editor Automation
- File Watcher
- Project Templates
- System Requirements
- VR Support
- Mobile Platform
- Accessibility
- Platform Input
- Cross-Compilation: Wine Testing
- RHI Abstraction Layer
- D3D11 Backend
- D3D12 Backend
- Vulkan Backend
- OpenGL Backend
- Metal Backend
- DXR Raytracing
- Hybrid Ray Tracing
- Upscaling (DLSS/FSR)
- Render Graph
- Shader Graph
- GPU Particles
- GPU-Driven Rendering
- Volumetric Fog
- Volumetric Clouds
- Global Illumination
- Virtual Texturing
- Water Rendering
- Clustered Lighting
- Material System
- Post-Processing
- Shadow System
- Particle System
- Decal System
- Sky and Atmosphere
- Foliage System
- Mesh Shaders
- Neural Rendering
- Configuration Reference
- Performance Tips
- Benchmark Framework
- Threading Model
- Memory Safety
- Memory Management Patterns
- Build System and CMake Modules
- Profiler and Debugging
- Performance Profiling Guide
- Telemetry System
- Golden Image Testing
- Utilities
- Testing
- Codebase Statistics
- Codebase Health
- Error Handling Patterns
- Hot Reload Overview
- Troubleshooting
- Contributing
- Workflow Patterns
- Build Optimizations
- CI Reproducible Builds
- GitHub API and PR Checks
- Git Rebase Conflicts
- Clang-Format
- Code Quality Violations
- AI Bloat Pattern
- MinGW + Wine Cross-Compilation
- Live Editor Testing
- Engine & Renderer Landscape
- DuetOS Portability Catalog
- Five-Engine Analysis
- Eleven-Engine Analysis
- ThorVG / Unity Graphics Analysis
- Advanced Techniques Catalog
- Third-Party Library Evaluation
- Engine Viability Evaluation
- Engine Feature Recommendations
- Project Recommendations
- Mac Compatibility Analysis
- Codebase Observations
- Codebase Bloat Audit
- Test Suite Audit
- Documentation Coverage Audit
- ThirdParty Dependencies Audit
- Load Test Baseline
- Gameplay Systems Status
- SparkGame Module Status
- Stub and Abandoned Features
- Memory Integrity System
- Memory Safety Evaluation
- Hardware Acceleration Systems
- Jolt Physics Integration
- GPU/CPU Separation Plan
- Daemon Services Architecture
- Reflection & Polymorphism Refactoring Plan
- SparkBuild In-Tree
- Wine No-JobSystem Breakthrough
- Wine Role and Fallback Tiers