Skip to content

Latest commit

 

History

99 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ecss – Lightweight high‑speed C++ Entity Component System (sector / chunk based)

CI (build & tests) License: MIT C++20 docs

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).

What is ECS?

Projects using ECSS

Stellar Forge Engine
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.

🔑 Core Characteristics

  • 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)

🧠 Design Principles

  • 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 memmove for insertion / defrag) — registering a non‑trivially‑copyable one still works, but names it at compile time and once at runtime; declare ecss::AllowNonTrivial<T> when it is deliberate

🧱 Memory Model (Conceptual)

[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

🧵 Threading (when Registry<true>)

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.

⚠️ An array must not be restructured while this thread is iterating it: the writer would wait for the view you are holding. Debug builds assert; release builds block.

⚠️ The array's shape is guaranteed, a component's value is not: two threads on one component type, one writing, is yours to arrange. 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.

🧹 Defragmentation

  1. Erase marks liveness bits dead and increments per‑array fragmentation counter
  2. Heuristic: (dead / size) > threshold → compaction candidate
  3. Compaction moves alive runs left; updates id→ptr map only for moved sectors (O(moved))
  4. Trivial sectors copy via raw bytes; non‑trivial invoke move/dtor from layout function table
  5. Optional manual trigger per array or global registry.defragment()

🔍 Iteration Forms

  • 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)

✅ Why Not Full Archetypes?

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).

⚡ Representative Costs (intent)

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)

🚀 Quick Start

#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
}

Deferred Erase & Maintenance

reg.destroyEntity(e);  // marks dead; memory reclaimed later
reg.update();          // drains + optional compaction

Ranged View

ecss::Ranges<ecss::EntityId> slice({ {100,200}, {400,450} });
for (auto [id, p] : reg.view<Position>(slice)) { /* ... */ }

Per-Type Defrag Threshold

reg.setDefragmentThreshold<Position>(0.15f); // 15% dead triggers

🛠 Installation

CMake (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/ecss
add_subdirectory(external/ecss)
target_link_libraries(my_app PRIVATE ecss)

Manual: copy headers & target_include_directories().

🧪 Benchmarks

Live dashboard: https://wagnerks.github.io/ecss_benchmarks (compares iteration & structural ops against other ECS libraries under large entity counts).

🔧 Tips

  • 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 calling update()
  • Use ranged views for sparse workloads to reduce cache waste
  • Tune per‑array defrag thresholds rather than forcing global compaction

🆚 Compared To entt / flecs (conceptual)

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)

📁 Key Files

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

📚 Documentation

⚖️ License

MIT – see LICENSE.

📌 Status

Active. Core layout & iteration model stable; expect additive utilities (instrumentation, bulk helpers) without heavy churn. Feedback & performance traces welcome.

About

High-performance C++ ECS with cache-friendly sector-based architecture and multithreading support.

Topics

Resources

Stars

24 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages