> Cinder 0.1.0 documentation: cinder.ecs API
> Canonical page: https://cinder.ash10.dev/docs/api/ecs/ · Source: `docs/api/ecs.md` in the engine repository (engine commit 47c9352, imported 2026-11-09)
> Index of every page for agents: https://cinder.ash10.dev/llms.txt

# `cinder.ecs`


## `cinder.ecs`

Source: `cinder/ecs/__init__.mojo`

Entity component system: archetype world, typed queries, change detection, command buffers, parent-child transforms and a scheduler that runs non-conflicting systems in parallel.


## `cinder.ecs.commands`

Source: `cinder/ecs/commands.mojo`

Deferred structural changes: record while iterating, apply when it is safe. A `CommandBuffer` stores spawns (returning placeholder entities resolved on apply), despawns, and typed adds/removes as type-erased byte payloads with the component's move/destroy functions, so it can be filled from jobs that must not mutate the world's archetypes.

### struct `CommandBuffer`

```mojo
struct CommandBuffer(Movable, Sized)
```

Records spawns, despawns, adds and removes for a later `apply` on the main thread. Recording never touches the world, so a job can fill a buffer while systems iterate; one buffer is not safe to share between threads (no locks). Added values are moved into a payload the buffer owns; every add command still in the buffer owns its value, so dropping the buffer (or `clear`) destroys the values of commands that were never applied.

Fields:

- `commands`: List[_Command]
- `payload`: List[Block]
- `payload_used`: Int
- `placeholders`: Int
- `resolved`: List[Entity] — After `apply`: the real entity for each placeholder spawn (null while not applied).
- `resuming`: Bool — True after an `apply` that raised: the next `apply` continues with the commands left and keeps the entities already in `resolved`.

- **`__init__`** `__init__(out self)`
- **`clear`** `clear(mut self)`  
  Drops every recorded command, destroying the values of unapplied adds. Use it to discard what is left after an `apply` that raised.
- **`spawn`** `spawn(mut self) -> Int`  
  Returns a placeholder index usable with `add_to_spawned`; see `resolved` after apply.
- **`despawn`** `despawn(mut self, e: Entity)`  
  Records a despawn of `e`; `apply` skips it when `e` is no longer alive by then.
- **`add`** `add[T: Component](mut self, e: Entity, var value: T)`  
  Records adding (or replacing) `T` on `e`, moving `value` into the buffer. `apply` raises when `T` was never registered with the world or when `e` is not alive.
- **`add_to_spawned`** `add_to_spawned[T: Component](mut self, placeholder: Int, var value: T)`  
  Like `add`, for the entity created by the `spawn()` call that returned `placeholder`.
- **`remove`** `remove[T: Component](mut self, e: Entity)`  
  Records removing `T` from `e`; `apply` skips a missing `T`, raises if `e` is dead.
- **`__len__`** `__len__(self) -> Int`
- **`apply`** `apply(mut self, mut world: World) raises -> Int`  
  Applies every command in order and clears the buffer. Returns how many were applied. Payload values are moved into the world through the world's registered move function, so the world must already know the component type (register it or add it once directly). When a command raises (unregistered component, dead target), the commands before it have taken effect and are removed from the buffer; the failing command and the ones after it stay, still owning their values, and the error propagates. A later `apply` continues with them (placeholder spawns already made keep their entity in `resolved`); `clear` or dropping the buffer discards them instead.


## `cinder.ecs.hierarchy`

Source: `cinder/ecs/hierarchy.mojo`

Parent-child transforms: `Parent`, `LocalTransform`, `WorldTransform` and their propagation.

### struct `Parent`

```mojo
struct Parent(Component)
```

Parent link for `propagate_transforms` (id 1); a dead parent or a self link is ignored.

Fields:

- `entity`: Entity

- **`component_id`** `component_id() -> Int`

### struct `LocalTransform`

```mojo
struct LocalTransform(Component)
```

Transform relative to the `Parent` (to world space without one); component id 2.

Fields:

- `matrix`: Mat4

- **`component_id`** `component_id() -> Int`

### struct `WorldTransform`

```mojo
struct WorldTransform(Component)
```

World-space transform, parent world * local, written by `propagate_transforms`; id 3.

Fields:

- `matrix`: Mat4

- **`component_id`** `component_id() -> Int`

### def `propagate_transforms`

```mojo
def propagate_transforms(mut world: World) raises -> Int
```

Writes `WorldTransform` = parent world * `LocalTransform` for every entity with a local transform (adding the component when missing). Parents are resolved recursively with memoisation, so the order of entities does not matter and each is computed once. Only transforms whose value changed are marked changed. Scratch lists live in the world, so a steady-state call allocates nothing. Returns the number of updated entities.



## `cinder.ecs.schedule`

Source: `cinder/ecs/schedule.mojo`

System scheduling by declared component access. Each system names the components it reads and writes. `Schedule.build` groups systems into stages in declaration order: a system joins the current stage unless it conflicts with one already there (write/write or read/write on the same component, or a declared exclusive world access), in which case a new stage starts. Systems within a stage run in parallel on the job system; stages run one after another, so a system always sees the effects of every earlier conflicting system.

- `comptime SystemFn = def(Pointer[World, MutUntrackedOrigin]) thin -> None` — A system body, called with an untracked world pointer, possibly on a job worker thread.
### struct `SystemDesc`

```mojo
struct SystemDesc(Copyable, Movable)
```

A named system plus the component ids it reads and writes, used by `Schedule` for staging. The declarations are trusted: nothing checks what `run` actually accesses.

Fields:

- `name`: String
- `run`: SystemFn
- `reads`: List[Int]
- `writes`: List[Int]
- `exclusive`: Bool — Structural changes (spawn/add/remove): conflicts with every other system.

- **`__init__`** `__init__( out self, name: String, run: SystemFn, exclusive: Bool = False )`
- **`reads_`** `reads_[T: Component](var self) -> SystemDesc`  
  Declares a read of `T`; returns the description for chaining.
- **`writes_`** `writes_[T: Component](var self) -> SystemDesc`  
  Declares a write of `T`; returns the description for chaining.
- **`conflicts_with`** `conflicts_with(self, other: SystemDesc) -> Bool`  
  Whether the two systems may not share a stage. True when either is exclusive or one writes a component id the other reads or writes; two readers of the same component never conflict.

### struct `Schedule`

```mojo
struct Schedule(Movable)
```

Systems grouped into stages by declared access (see the module docstring).

Fields:

- `systems`: List[SystemDesc]
- `stages`: List[List[Int]] — System indices per stage, filled by `build`.
- `built`: Bool
- `contexts`: List[_JobContext]

- **`__init__`** `__init__(out self)`
- **`add`** `add(mut self, var system: SystemDesc)`  
  Appends a system after those already added and marks the stages for rebuilding.
- **`build`** `build(mut self)`  
  Assigns systems to stages in declaration order. A system joins the last stage unless it conflicts with any system there, otherwise it opens a new stage; earlier stages are never revisited, so the order affects packing.
- **`stage_count`** `stage_count(mut self) -> Int`  
  Number of stages, building them first when systems changed since the last build.
- **`run`** `run( mut self, mut world: World, jobs: Optional[Pointer[JobSystem, MutUntrackedOrigin]], )`  
  Runs every stage. With a job system, systems in one stage run as parallel jobs; without one (or for single-system stages) they run inline on the calling thread.


## `cinder.ecs.world`

Source: `cinder/ecs/world.mojo`

Archetype ECS: entities, type-erased component columns, typed queries, change detection. Components are structs conforming to `Component` (a unique `component_id()`). The world registers a component type on first use, recording its size, alignment, and type-erased move/destroy functions, so columns can hold any Movable type. Entities with the same component set live in one archetype, one contiguous column per component (16-byte aligned blocks), so a query iterates plain arrays. Change detection: every column keeps a per-row tick written by `set`/`get_mut`/`add`; the world's tick advances with `advance_tick()` (once per frame or per system) and queries filter rows with `changed_since(tick)`. Structural changes (spawn, despawn, add, remove) are immediate here; `CommandBuffer` defers them for use inside queries and jobs.

- `comptime Block = SIMD[DType.float32, 4]` — Column storage unit: 16 bytes, 16-byte aligned, so every component layout fits.
- `comptime BLOCK_BYTES = 16` — Bytes per `Block`; also the largest component alignment `register` accepts.
- `comptime ErasedPtr = Pointer[NoneType, MutUntrackedOrigin]` — Untyped mutable pointer to one component value (a column element or a command payload).
- `comptime DestroyFn = def(ErasedPtr) thin -> None` — Type-erased destructor: takes the value out of the pointee and drops it.
- `comptime MoveFn = def(ErasedPtr, ErasedPtr) thin -> None` — Type-erased move `(dst, src)`: takes the value out of `src` into uninitialised `dst`.
- `comptime MAX_COMPONENTS = 256` — Number of component ids: `component_id()` must lie in 0 .. MAX_COMPONENTS - 1.
### trait `Component`

```mojo
trait Component(Copyable, Deinitable, Movable)
```

A component type. `component_id()` must be unique per type (0 .. MAX_COMPONENTS - 1).

- **`component_id`** `component_id() -> Int`  
  The type's id, unique among component types and in 0 .. MAX_COMPONENTS - 1.

### struct `Entity`

```mojo
struct Entity( Equatable, Hashable, ImplicitlyCopyable, RegisterPassable, Writable )
```

Generational entity id: index + generation. `Entity.null()` is never live.

Fields:

- `index`: UInt32
- `generation`: UInt32

- **`null`** `null() -> Entity`  
  The null entity (index 0xFFFFFFFF, generation 0): never alive, False as a Bool.
- **`is_null`** `is_null(self) -> Bool`  
  Whether the index is the null index 0xFFFFFFFF (the generation is not compared).
- **`packed`** `packed(self) -> UInt64`  
  Index in the low 32 bits, generation in the high 32; `from_packed` inverts it.
- **`from_packed`** `from_packed(v: UInt64) -> Entity`  
  Inverse of `packed()`: index from the low 32 bits, generation from the high 32.
- **`write_to`** `write_to(self, mut w: Some[Writer])`

### def `component_stride`

```mojo
def component_stride[T: Component]() -> Int
```

Element stride: the size rounded up to the type's alignment, so arrays stay tight (an 8-byte component takes 8 bytes per row) while every element is correctly aligned.


### struct `ComponentInfo`

```mojo
struct ComponentInfo(ImplicitlyCopyable, Movable)
```

Registration record per component id: size, alignment, row stride, type name and type-erased destroy/move. Every id has one; ids that were never registered hold no-op functions, size 0, an empty type name and `registered` False.

Fields:

- `id`: Int
- `size`: Int
- `align`: Int
- `stride`: Int — `component_stride[T]()`: the size rounded up to the type's own alignment (not to 16). Column storage starts on a 16-byte boundary and `register` rejects alignments above 16, so every element is correctly aligned.
- `type_name`: StaticString — `reflect[T].name()` of the registered type; tells apart same-size types sharing an id.
- `destroy`: DestroyFn
- `move`: MoveFn
- `registered`: Bool


### struct `Column`

```mojo
struct Column(Movable)
```

One component's storage inside an archetype: `count` elements of `stride` bytes.

Fields:

- `component`: Int
- `stride`: Int
- `blocks`: List[Block]
- `ticks`: List[UInt32]

- **`__init__`** `__init__(out self, component: Int, stride: Int, capacity: Int)`
- **`capacity`** `capacity(self) -> Int`  
  Rows that fit in the allocated blocks before the column must grow.
- **`base`** `base(self) -> ErasedPtr`  
  Untyped pointer to row 0; dangles once the column grows.
- **`element`** `element(self, row: Int) -> ErasedPtr`  
  Untyped pointer to `row` (`row * stride` bytes past `base`); no bounds check.
- **`grow`** `grow(mut self, needed: Int)`  
  Ensures room for `needed` rows, at least doubling the capacity. Existing rows are relocated bytewise (no move constructor runs), so stored values must not point into themselves, and pointers from `base`/`element` dangle afterwards. `ticks` is left to the caller.

### struct `Archetype`

```mojo
struct Archetype(Movable)
```

Entities sharing one component set. Columns are parallel to `entities`.

Fields:

- `signature`: List[Int] — Sorted component ids.
- `entities`: List[Entity]
- `columns`: List[Column]
- `column_of`: List[Int] — Component id -> column index, or -1.

- **`__init__`** `__init__( out self, var signature: List[Int], infos: List[ComponentInfo] )`
- **`count`** `count(self) -> Int`  
  Number of entities (rows) stored in this archetype.
- **`has`** `has(self, component: Int) -> Bool`  
  Whether component id `component` is in this archetype's signature.
- **`matches`** `matches(self, with_: List[Int], without: List[Int]) -> Bool`  
  Whether the archetype has every id in `with_` and none of the ids in `without`.
- **`reserve_row`** `reserve_row(mut self, entity: Entity, tick: UInt32) -> Int`  
  Appends `entity` with a new row stamped `tick` in every column; returns the row index. Component values in the new row are uninitialised: the caller must write every column.
- **`swap_remove`** `swap_remove( mut self, row: Int, infos: List[ComponentInfo], destroy: Bool ) -> Entity`  
  Removes `row`, moving the last row into it. Returns the entity that moved (or null).

### struct `World`

```mojo
struct World(Movable)
```

The entity store: component registry, archetypes, entity locations and the change tick. New entities start in archetype 0 (no components); archetypes are created on demand and never removed. The world has no locks and belongs to the main thread: structural changes, `register` and `advance_tick` must not overlap any other access. Dropping the world destroys every stored component.

Fields:

- `infos`: List[ComponentInfo]
- `archetypes`: List[Archetype]
- `locations`: List[_Location]
- `free_indices`: List[UInt32]
- `tick`: UInt32
- `live_count`: Int
- `prop_computed`: List[Bool] — Scratch for `propagate_transforms` (kept between frames).
- `prop_results`: List[Mat4]
- `prop_todo`: List[Entity]
- `prop_chunks`: List[Chunk]
- `prop_spec`: QuerySpec — Filled on the first propagation (the hierarchy module defines the component).

- **`__init__`** `__init__(out self)`
- **`register`** `register[T: Component](mut self) raises -> Int`  
  Registers `T` under its `component_id()` and returns the id; repeat calls are no-ops. Raises when the id is outside 0 .. MAX_COMPONENTS - 1, when the id is already registered by another type (different type name, size or alignment), or when `T` needs alignment above 16 bytes. `add` registers implicitly; `CommandBuffer.apply` needs the type registered beforehand.
- **`advance_tick`** `advance_tick(mut self) -> UInt32`  
  Increments the change tick and returns it; later writes are stamped with it.
- **`spawn`** `spawn(mut self) -> Entity`  
  Creates an entity without components and returns it. A freed index is reused with its generation incremented, so handles to the despawned entity stay dead; otherwise a new index starts at generation 1.
- **`is_alive`** `is_alive(self, e: Entity) -> Bool`  
  Whether `e` is not null, in range, alive, and of the index's current generation.
- **`despawn`** `despawn(mut self, e: Entity) raises`  
  Destroys the entity's components and frees its index for reuse. Raises when `e` is not alive. The row is swap-removed, so the archetype's last entity moves into it (pointers into that archetype dangle).
- **`count`** `count(self) -> Int`  
  Number of live entities.
- **`add`** `add[T: Component](mut self, e: Entity, var value: T) raises`  
  Adds (or replaces) a component. Replacing marks it changed.
- **`remove`** `remove[T: Component](mut self, e: Entity) raises -> Bool`  
  Removes a component; False when the entity did not have it.
- **`has`** `has[T: Component](self, e: Entity) -> Bool`  
  Whether `e` is alive and has a `T`; False (no raise) for dead or null entities.
- **`get`** `get[T: Component](self, e: Entity) raises -> T`  
  A copy of the component.
- **`get_ptr`** `get_ptr[ T: Component ](self, e: Entity) raises -> Pointer[T, MutUntrackedOrigin]`  
  Direct pointer, valid until the next structural change. Does not mark the row changed.
- **`get_mut`** `get_mut[ T: Component ](mut self, e: Entity) raises -> Pointer[T, MutUntrackedOrigin]`  
  Like `get_ptr` and marks the component changed at the current tick.
- **`set`** `set[T: Component](mut self, e: Entity, var value: T) raises`  
  Overwrites an existing `T` and marks it changed (via `get_mut`). Raises when `e` is not alive or has no `T`; use `add` to attach a new component.
- **`changed_tick`** `changed_tick[T: Component](self, e: Entity) raises -> UInt32`  
  The tick at which `e`'s `T` was last written. Raises when `e` is dead or has no `T`.
- **`entities`** `entities(self) -> List[Entity]`  
  A new list of every live entity, grouped by archetype (not in spawn order).
- **`archetype_count`** `archetype_count(self) -> Int`  
  Archetypes created so far, including the empty one (they are never removed).
- **`query`** `query(self) -> Query`  
  Starts a `Query` builder over this world. The query keeps an untracked pointer to the world and must not outlive it. Systems that run every frame should keep a `QuerySpec` and use `collect_chunks`, which reuses its list.
- **`collect_prop_chunks`** `collect_prop_chunks(mut self)`  
  `collect_chunks` of `prop_spec` into the world's own `prop_chunks` scratch (transform propagation).
- **`collect_chunks`** `collect_chunks(self, spec: QuerySpec, mut out: List[Chunk])`  
  Fills `out` (cleared first, capacity kept) with the chunks matching `spec`. Systems keep their `QuerySpec` and chunk list across frames so steady-state frames allocate nothing.

### def `pass_through`

```mojo
def pass_through(p: ErasedPtr)
```

Hands `p` to `keep`; the body of the no-op destroy/move used for unregistered ids.


### struct `Chunk`

```mojo
struct Chunk(ImplicitlyCopyable, Movable)
```

One archetype's rows matched by a query: typed column access by row index.

Fields:

- `world`: Pointer[World, MutUntrackedOrigin]
- `archetype`: Int
- `count`: Int

- **`__init__`** `__init__( out self, world: Pointer[World, MutUntrackedOrigin], archetype: Int )`
- **`has`** `has[T: Component](self) -> Bool`  
  Whether every entity in this chunk has `T`.
- **`entity`** `entity(self, row: Int) -> Entity`  
  The entity at `row` of this chunk's archetype.
- **`column`** `column[T: Component](self) -> Pointer[T, MutUntrackedOrigin]`  
  Base pointer of the component column (element `row` is `column[T]()[unsafe_offset=row]`). Aborts when the chunk has no `T` (use `has` for optional components).
- **`stride`** `stride[T: Component]() -> Int`  
  Bytes between consecutive `T` elements of a column (`component_stride[T]()`).
- **`get`** `get[T: Component](self, row: Int) -> Pointer[T, MutUntrackedOrigin]`  
  Pointer to the `T` at `row`; does not mark it changed (see `mark_changed`). Aborts when the chunk has no `T` (use `has`); `row` is not range-checked. The pointer dangles after the next structural change.
- **`mark_changed`** `mark_changed[T: Component](self, row: Int)`  
  Stamps the `T` at `row` with the world's current tick; aborts when the chunk has no `T`.
- **`changed_since`** `changed_since[T: Component](self, row: Int, tick: UInt32) -> Bool`  
  Whether the `T` at `row` was written after `tick` (a write at exactly `tick` is not). Aborts when the chunk has no `T`.

### struct `QuerySpec`

```mojo
struct QuerySpec(Copyable, Movable)
```

The filters of a query without a world pointer: safe to keep in a system struct.

Fields:

- `with_ids`: List[Int]
- `without_ids`: List[Int]

- **`__init__`** `__init__(out self)`
- **`with_`** `with_[T: Component](var self) -> QuerySpec`  
  Requires component `T`; returns the spec for chaining.
- **`without`** `without[T: Component](var self) -> QuerySpec`  
  Excludes archetypes that have `T`; returns the spec for chaining.

### struct `Query`

```mojo
struct Query(Movable)
```

Builder: `world.query().with_[A]().with_[B]().without[C]().chunks()` lists matching chunks. Component access inside a chunk is typed (`chunk.column[A]()`); the filters decide which archetypes take part.

Fields:

- `world`: Pointer[World, MutUntrackedOrigin]
- `with_ids`: List[Int]
- `without_ids`: List[Int]

- **`__init__`** `__init__(out self, world: Pointer[World, MutUntrackedOrigin])`
- **`with_`** `with_[T: Component](var self) -> Query`  
  Requires component `T`; returns the query for chaining.
- **`without`** `without[T: Component](var self) -> Query`  
  Excludes archetypes that have `T`; returns the query for chaining.
- **`chunks`** `chunks(self) -> List[Chunk]`  
  A new list with one `Chunk` per non-empty matching archetype, in archetype order. Each chunk's `count` is captured here and goes stale after a structural change.
- **`count`** `count(self) -> Int`  
  Number of matching entities (builds the chunk list to sum it).
