Skip to content

Astra 3.1.0

Latest

Choose a tag to compare

@T3mps T3mps released this 11 Jun 09:18
· 11 commits to main since this release

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 SetTypeContext is called from each module, so components registered before the context is installed are not lost.
  • MetaRegistry now 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 reload

TypeID_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]] before inline, 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 threadSafe config key from the README (it was never implemented).
  • Corrected component requirements section (copyable/movable constraints clarified).
  • Fixed CreateEntityWith examples 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