Repository navigation
Astra ECS v3.1 - Hardening: Scheduler Seam, TypeContext, Container Fixes & CI
Release Highlights
Astra 3.1 is a hardening release focused on correctness, portability, and safe integration patterns. It ships a clean threading seam (no surprise thread spawning), process-shared type identity for multi-module and DLL scenarios, two container correctness fixes found by new fuzz suites, and a GitHub Actions CI matrix covering MSVC, gcc, and clang (distro-default on ubuntu-latest; verified locally with gcc 15.1 / clang 18). The public API is backward-compatible; two deprecated footguns (TypeID_v and MakeComponentMask constexpr) are removed.
Parallel Execution & The Scheduler Seam
The old View::ParallelForEach, Relations::ParallelForEachDescendant, and ParallelExecutor implementations spawned std::async tasks ad-hoc per call. Benchmarking at 1 M entities with a trivial store-only kernel showed this was actively harmful: the ad-hoc async path took 2.43 ms against 1.53 ms for the sequential path -- a 1.59x loss due to thread-pool contention and synchronisation overhead on a memory-bound kernel.
IWorkScheduler seam
A thin scheduler interface lives at Core/WorkScheduler.hpp:
class IWorkScheduler {
public:
virtual ~IWorkScheduler() = default;
virtual void ParallelFor(size_t count, size_t minBatch,
const std::function<void(size_t, size_t)>& fn) = 0;
virtual size_t WorkerCount() const noexcept = 0;
};Inject via Registry::Config::workScheduler (std::shared_ptr<IWorkScheduler>). When nullptr (the new default), all parallel APIs run sequentially inline -- safe in single-threaded contexts and faster on memory-bound workloads.
// Sequential (default -- fastest for memory-bound kernels)
Astra::Registry registry;
// Parallel with your own pool
auto scheduler = std::make_shared<MyEnkiTSAdapter>();
Astra::Registry::Config config;
config.workScheduler = scheduler;
Astra::Registry registry(config);The reference fork-join pool is available in tests/Support/TestWorkerPool.hpp for test and benchmark use. With the injected test pool on a compute-heavy kernel at 1 M entities: 2.17 ms (injected pool) vs 2.43 ms (old ad-hoc async) -- an 11% improvement once thread overhead is no longer paid per-call. Sequential remains the right default; parallelism pays when the kernel is compute-bound rather than memory-bound. For the store-only (memory-bound) kernel, sequential at 1.53 ms still wins; the default is now sequential rather than the silently-losing parallel path.
Multi-Module & DLL Support
Static typeindex-based IDs break across DLL boundaries because each module gets its own static. TypeContext (Core/TypeContext.hpp) replaces the old scheme with process-shared type identity.
How it works
- Stable XXHash64 type-name hashes are the cross-module identity key.
- Dense sequential ComponentIDs are assigned from the shared context and are mask-compatible with the existing archetype bitmask system.
- A pending static-registration queue is drained when
SetTypeContextis called from each module, so components registered before the context is installed are not lost. MetaRegistrynow lives inside the context; the singleton API (MetaRegistry::Instance()) is preserved.
DLL hot-reload sequence
// 1. Serialize living entities (snapshot or raw binary)
// 2. Unload old DLL; load new DLL
// 3. Install shared context in the new module
SetTypeContext(sharedCtx);
// 4. Refresh descriptors for recompiled components
componentRegistry->ReRegisterComponent<MyComponent>();
// 5. Deserialize -- IDs are stable across the reloadTypeID_v (a static-init template variable that was a DLL footgun) is removed; use Astra::TypeID<T>::Value() instead (IDs resolve through the active TypeContext).
Container Fixes & Fuzzing
New differential fuzz suites (21 fuzz tests covering FlatMap/FlatSet churn with adversarial hashing, SmallVector spill-boundary, and Bitmap bulk ops) found two correctness bugs in the internal containers.
FlatMap / FlatSet duplicate-insertion bug
Emplace inserted a key at the first empty-or-deleted (tombstone) slot encountered during the probe chain before completing the duplicate scan. Because tombstones do not terminate the probe sequence, a key already present later in the chain could be inserted a second time, causing a silent size overcount and returning iterators to two distinct slots for the same logical key.
Fix: defer slot selection until an EMPTY slot terminates the scan. If a matching key is found during the scan, return the existing slot (no insert). Only after confirming no duplicate exists is the deferred slot used for insertion.
SwissTable H2 metadata-range hazard
SwissTable::H2 extracted the top 7 bits of the hash via & 0x7F, which already excluded the 0x80 sentinel. However, a degenerate hasher whose top 7 bits are all zero produces H2 == 0, which falls outside the range [1, 127] that Group::Match assumes for probe bytes, breaking the SIMD match logic.
Fix: H2 now folds 0 to 1 centrally (return h2 == 0 ? 1 : h2) so any hasher is guaranteed to produce a value in [1, 127].
Cross-Compiler & CI
Approximately 25 portability fixes were required to build cleanly under current gcc and clang (verified locally with gcc 15.1 / clang 18):
- Dependent-name disambiguators (
typename/template) in templated base-class member access. - Removed in-class partial specialisations (not permitted by the standard; MSVC accepted them as an extension).
- Fixed member-initialiser order to match declaration order (gcc
-Wreorder). - Corrected attribute placement (
[[nodiscard]]beforeinline, not after). - Warning hygiene: signed/unsigned comparisons, unused parameters, shadow declarations.
A GitHub Actions CI matrix (.github/workflows/ci.yml) runs on every push to every branch and on every pull request:
| Job | Compiler | Standard |
|---|---|---|
| windows-msvc | MSVC on windows-latest | C++20 |
| linux-gcc | current distro gcc on ubuntu-latest (unpinned) | C++20 |
| linux-clang | current distro clang on ubuntu-latest (unpinned) | C++20 |
Compilers are unpinned (distro-default on ubuntu-latest and MSVC on windows-latest). Verified locally with gcc 15.1 / clang 18. All three jobs build in Release, run the full 496-test suite, and must be green before merge.
Documentation
- Removed the nonexistent
threadSafeconfig key from the README (it was never implemented). - Corrected component requirements section (copyable/movable constraints clarified).
- Fixed
CreateEntityWithexamples to match the actual API signature. - Added Threading Model section documenting the scheduler seam and default-sequential behaviour.
- Added Multi-Module (DLL) section documenting
TypeContext,SetTypeContext, and the hot-reload sequence.
Removals
| Removed | Replacement | Reason |
|---|---|---|
TypeID_v<T> (static template variable) |
Astra::TypeID<T>::Value() |
Static-init order fiasco across DLL boundaries |
MakeComponentMask constexpr overload |
Runtime ComponentMask construction |
Bitmap is not a literal type under clang; constexpr was misleading |
Requirements
- C++20 compatible compiler (MSVC 2022+, GCC 11+, Clang 13+)
- Premake5 for project generation
- Optional: GoogleTest, Google Benchmark
License
MIT License - See LICENSE file for details
Full Changelog: v3.0.0...v3.1.0