DocsAPI reference

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

View as MarkdownSource: docs/api/ecs.md