ecss is a minimal, performance‑oriented ECS. One (or several explicitly grouped) component types for an entity are stored contiguously inside a fixed layout memory block called a sector:
[SectorHeader(id|mask) | CompA | CompB | ...]
Sectors live inside chunked storage that grows by powers of two. Offsets are computed at compile time (no RTTI / string hashing in hot loops) and the core stays small (a few headers, no code generation, no macro DSL).
ECSS was originally developed inside the Stellar Forge Engine as its core ECS solution.
After proving its performance and flexibility in a real engine, ECSS was extracted into a standalone library.
- Sector (chunk) storage: tightly packed, deterministic offsets, low fragmentation
- Explicit grouping: opt‑in only (avoid archetype explosion); unrelated types live in their own arrays
- O(1) id → sector* lookup (direct pointer map, lock‑free snapshot view in TS build)
- Optional thread safety:
Registry<true>adds shared/unique locks + per‑sector pin counters - Deferred erase + opportunistic / explicit defragmentation (alive runs compacted left)
- Dense reflection: a small integer per component type, assigned on first use from one process-wide counter and shared by every registry
- Header‑only style integration, no external deps (C++20/23 stdlib only)
- Branch‑lean inner loops (mask test + pointer arithmetic only)
- No virtual dispatch in hot iteration
- Deterministic layout => stable perf tuning / profiling
- Costs are per
SectorsArray, never across whole registry - Favor trivial POD components (enables raw
memmovefor insertion / defrag) — registering a non‑trivially‑copyable one still works, but names it at compile time and once at runtime; declareecss::AllowNonTrivial<T>when it is deliberate
[Chunk]
├─ sector N : [Hdr | CompA | CompB]
├─ sector N+1 : [Hdr | CompA | CompB]
└─ ... (power‑of‑two growth)
- Header: id + 32‑bit liveness bit mask (≤ 32 grouped types)
- Group only hot, co‑accessed sets (e.g. Position + Velocity)
- Random middle insert shifts are local to one array; trivial components => fast
memmove
| Operation | Mechanism |
|---|---|
| Read / iterate / lookup | No lock — seqlock‑published snapshots |
| Iteration | Structural hold on the array (counted per thread, not per sector) |
| Append | Unique lock only; relocates nothing, so no pin wait |
| Relocating change (middle insert / defrag / clear) | Unique lock + wait for pins and holds, outside the lock |
| In‑place change (destroy / overwrite a member) | Unique lock + that one sector unpinned |
| Reclamation | Retire old buffers, freed after a grace period (deferred) |
Reading costs about the same as the non‑thread‑safe build (1.0–1.3x) and scales: 6.8x on eight
threads. Only structural changes made one call at a time cost noticeably more — batch them, or
record them into a CommandBuffer.
access<Read<T>, Write<U>>() claims types a
system at a time (29.5 ns for one), and setAccessTracking(true) finds the ones you missed and
costs nothing in release.
- Erase marks liveness bits dead and increments per‑array fragmentation counter
- Heuristic:
(dead / size) > threshold→ compaction candidate - Compaction moves alive runs left; updates id→ptr map only for moved sectors (O(moved))
- Trivial sectors copy via raw bytes; non‑trivial invoke move/dtor from layout function table
- Optional manual trigger per array or global
registry.defragment()
- Linear:
for (auto s : *array)(may include dead bits) - Alive filtered:
beginAlive<T>() - Ranged:
beginRanged(ranges)on sparse id windows - Views:
reg.view<Main, Others...>()(alive Main + projected grouped / foreign components) - Ranged View:
reg.view<Main, ...>(Ranges)
Traditional archetypes relocate entities whenever composition changes & explode with many combos. ecss lets you group only where locality wins; adding an unrelated component touches only its dedicated array (no reshuffle of existing grouped data).
| Operation | Cost |
|---|---|
| Id → sector* | O(1) |
| Append insert | Amortized O(1) |
| Random insert (trivial types) | O(shift) raw memmove (local array only) |
| Defrag scan | O(sectors) early‑out per dead run |
| View iteration | O(alive visited) (mask + projections) |
#include <ecss/Registry.h>
struct Position { float x,y; }; struct Velocity { float dx,dy; };
using Reg = ecss::Registry<true>; // thread-safe
int main(){
Reg reg; auto e = reg.takeEntity();
reg.registerArray<Position, Velocity>(); // optional grouping -- before the first add
reg.addComponent<Position>(e, {0,0});
reg.addComponent<Velocity>(e, {1,0});
for (auto [id, p, v] : reg.view<Position, Velocity>()) {
if (p && v) p->x += v->dx;
}
reg.update(); // process deferred erases & maybe defrag
}reg.destroyEntity(e); // marks dead; memory reclaimed later
reg.update(); // drains + optional compactionecss::Ranges<ecss::EntityId> slice({ {100,200}, {400,450} });
for (auto [id, p] : reg.view<Position>(slice)) { /* ... */ }reg.setDefragmentThreshold<Position>(0.15f); // 15% dead triggersCMake (FetchContent):
include(FetchContent)
FetchContent_Declare(
ecss
GIT_REPOSITORY https://github.com/wagnerks/ecss.git
GIT_TAG v0.1.0 # or latest release
)
FetchContent_MakeAvailable(ecss)
target_link_libraries(my_app PRIVATE ecss)Submodule:
git submodule add https://github.com/wagnerks/ecss external/ecssadd_subdirectory(external/ecss)
target_link_libraries(my_app PRIVATE ecss)Manual: copy headers & target_include_directories().
Live dashboard: https://wagnerks.github.io/ecss_benchmarks (compares iteration & structural ops against other ECS libraries under large entity counts).
- Keep grouped sets small & hot (avoid cold heavy data in same sector)
- Make components trivially movable when possible
- Batch structural changes:
takeEntities,insertBulk,destroyEntities,CommandBuffer - Call
update()once per frame — anywhere in it, nothing waits.setAutoMaintenance(true)moves the erase and compaction pass onto view creation, but not the freeing of retired memory, so keep callingupdate() - Use ranged views for sparse workloads to reduce cache waste
- Tune per‑array defrag thresholds rather than forcing global compaction
| Aspect | ecss |
|---|---|
| Grouping | Explicit, opt‑in micro groups (sectors) |
| Moves on add/remove | Only arrays of affected types |
| Hot iteration | Straight line, no variant dispatch |
| Random insert | Local to one array; trivial => memmove |
| Thread safety | Shared/unique + per‑sector pins |
| Reflection | Dense ids per registry (no RTTI) |
| File | Purpose |
|---|---|
Sector.h |
Trivial sector header + type‑erased member ops |
SectorLayoutMeta.h |
Compile‑time layout (offsets, masks, function table) |
SectorsArray.h |
Sparse+linear container (insert/erase/defrag/iterators) |
PinCounters.h |
Per‑sector pin tracking & wait primitives |
Registry.h |
Entities, views, maintenance, grouping API |
Ranges.h |
Half‑open range set for id windows |
- Overview / Architecture:
docs/architecture.md - Examples:
docs/examples.md(more patterns; also see tests + https://github.com/wagnerks/StelForge ) - Batching & Deferral:
docs/batching.md(structural changes, bulk APIs, command buffer) - FAQ:
docs/faq.md - API Reference: generated from the headers, https://wagnerks.github.io/ecss/ecss/annotated/
MIT – see LICENSE.
Active. Core layout & iteration model stable; expect additive utilities (instrumentation, bulk helpers) without heavy churn. Feedback & performance traces welcome.