Skip to content

Architecture

High-level overview

graph TD
    FB["skr::ApplicationBuilder"] -->|AddExtension| FE["FreyrExtension"]
    FE -->|registers| CM["ComponentManager"]
    FE -->|registers| SM["SystemManager"]
    FE -->|configures| OPT["FreyrOptions"]
    FB -->|Build| APP["IApplication"]
    APP -->|GetService| SC["Scene"]

    SC --> CM
    SC --> EM["EntityManager"]
    SC --> EVM["EventManager"]
    SC --> SM
    SC --> TM["TaskManager"]

    CM --> ARCH["Archetype[]"]
    ARCH --> CHUNK["ArchetypeChunk[]"]
    TM --> WORKERS["Worker Threads"]
    WORKERS -->|process| CHUNK

Core components

Scene

The central orchestrator. All entity and component operations flow through Scene, which delegates to the appropriate manager.

Scene::Update(dt)
├─ SystemManager::PreUpdate(dt)   → all systems
├─ SystemManager::Update(dt)      → all systems  (ForEach / ForEachParallel live here)
├─ SystemManager::PostUpdate(dt)  → all systems
├─ fixed timestep accumulator
│   ├─ SystemManager::PreFixedUpdate(dt)
│   ├─ SystemManager::FixedUpdate(dt)
│   └─ SystemManager::PostFixedUpdate(dt)
├─ ExecuteTasks()                 → flush async tasks
└─ DestroyEntities()              → process deferred destruction queue

ComponentManager

Manages archetype discovery and entity placement. When an entity's component set changes, ComponentManager moves it to the correct archetype, migrating its data in place.

EntityManager

Allocates and recycles entity IDs. Uses a free-list internally so destroyed entity slots are reused immediately.

SystemManager

Holds all registered system instances. Calls lifecycle hooks in registration order.

EventManager

Thread-safe publish/subscribe bus. Each event type gets its own Publisher<T>, backed by a shared-read/exclusive-write mutex and a pending-listener queue for safe concurrent subscription.

TaskManager

A fixed-size thread pool with one MPMC (multi-producer, multi-consumer) lock-free queue per worker. Tasks (chunk iterations) are distributed either in dispatch order or by chunk affinity, depending on the configured FreyrExecutionStategy.


Memory layout

Archetype [Position, Velocity]
├── ArchetypeChunk (capacity = 512 entities)
│   ├── ComponentArray<Position>   [p0, p1, p2, ..., p511]  ← contiguous
│   └── ComponentArray<Velocity>   [v0, v1, v2, ..., v511]  ← contiguous
└── ArchetypeChunk (capacity = 512 entities)
    ├── ComponentArray<Position>   [p512, ..., p1023]
    └── ComponentArray<Velocity>   [v512, ..., v1023]

Each ComponentArray<T> is a raw contiguous buffer of T values. A chunk owns one array per registered component type. When a system iterates ForEach<Position, Velocity>, it strides through both arrays simultaneously — all data remains in cache.


Execution flow for ForEachParallel

sequenceDiagram
    participant S as System
    participant SC as Scene
    participant CM as ComponentManager
    participant TM as TaskManager
    participant W1 as Worker 1
    participant W2 as Worker 2

    S->>SC: ForEachParallel<Position, Velocity>(fn)
    SC->>CM: find matching archetypes
    CM-->>SC: [Archetype A, Archetype B]
    SC->>TM: enqueue chunk tasks
    TM->>W1: Chunk 0 task
    TM->>W2: Chunk 1 task
    W1-->>W1: process entities 0–511
    W2-->>W2: process entities 512–1023
    TM-->>SC: all tasks done

Execution strategies

Strategy Behaviour Best for
ChunkAffinity Each chunk is preferentially assigned to the same worker thread across frames Systems that access the same chunks repeatedly — maximises L1/L2 reuse
DispatchOrder Chunks are enqueued to workers in creation order Predictable ordering, simpler debugging

Configure via FreyrOptionsBuilder::WithExecutionStrategy.