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 #
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] — Afterapply: the real entity for each placeholder spawn (null while not applied). -
resuming: Bool — True after anapplythat raised: the nextapplycontinues with the commands left and keeps the entities already inresolved. -
__init____init__(out self) -
clearclear(mut self)
Drops every recorded command, destroying the values of unapplied adds. Use it to discard what is left after anapplythat raised. -
spawnspawn(mut self) -> Int
Returns a placeholder index usable withadd_to_spawned; seeresolvedafter apply. -
despawndespawn(mut self, e: Entity)
Records a despawn ofe;applyskips it wheneis no longer alive by then. -
addadd[T: Component](mut self, e: Entity, var value: T)
Records adding (or replacing)Tone, movingvalueinto the buffer.applyraises whenTwas never registered with the world or wheneis not alive. -
add_to_spawnedadd_to_spawned[T: Component](mut self, placeholder: Int, var value: T)
Likeadd, for the entity created by thespawn()call that returnedplaceholder. -
removeremove[T: Component](mut self, e: Entity)
Records removingTfrome;applyskips a missingT, raises ifeis dead. -
__len____len__(self) -> Int -
applyapply(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 laterapplycontinues with them (placeholder spawns already made keep their entity inresolved);clearor 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 #
struct Parent(Component)Parent link for propagate_transforms (id 1); a dead parent or a self link is ignored.
Fields:
-
entity: Entity -
component_idcomponent_id() -> Int
struct LocalTransform #
struct LocalTransform(Component)Transform relative to the Parent (to world space without one); component id 2.
Fields:
-
matrix: Mat4 -
component_idcomponent_id() -> Int
struct WorldTransform #
struct WorldTransform(Component)World-space transform, parent world * local, written by propagate_transforms; id 3.
Fields:
-
matrix: Mat4 -
component_idcomponent_id() -> Int
def propagate_transforms #
def propagate_transforms(mut world: World) raises -> IntWrites 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 #
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 ofT; returns the description for chaining. -
writes_writes_[T: Component](var self) -> SystemDesc
Declares a write ofT; returns the description for chaining. -
conflicts_withconflicts_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 #
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 bybuild. -
built: Bool -
contexts: List[_JobContext] -
__init____init__(out self) -
addadd(mut self, var system: SystemDesc)
Appends a system after those already added and marks the stages for rebuilding. -
buildbuild(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_countstage_count(mut self) -> Int
Number of stages, building them first when systems changed since the last build. -
runrun( 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 perBlock; also the largest component alignmentregisteraccepts.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 ofsrcinto uninitialiseddst.comptime MAX_COMPONENTS = 256— Number of component ids:component_id()must lie in 0 .. MAX_COMPONENTS - 1.
trait Component #
trait Component(Copyable, Deinitable, Movable)A component type. component_id() must be unique per type (0 .. MAX_COMPONENTS - 1).
component_idcomponent_id() -> Int
The type's id, unique among component types and in 0 .. MAX_COMPONENTS - 1.
struct Entity #
struct Entity( Equatable, Hashable, ImplicitlyCopyable, RegisterPassable, Writable )Generational entity id: index + generation. Entity.null() is never live.
Fields:
-
index: UInt32 -
generation: UInt32 -
nullnull() -> Entity
The null entity (index 0xFFFFFFFF, generation 0): never alive, False as a Bool. -
is_nullis_null(self) -> Bool
Whether the index is the null index 0xFFFFFFFF (the generation is not compared). -
packedpacked(self) -> UInt64
Index in the low 32 bits, generation in the high 32;from_packedinverts it. -
from_packedfrom_packed(v: UInt64) -> Entity
Inverse ofpacked(): index from the low 32 bits, generation from the high 32. -
write_towrite_to(self, mut w: Some[Writer])
def component_stride #
def component_stride[T: Component]() -> IntElement 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 #
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: Intsize: Intalign: Intstride: 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 andregisterrejects 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: DestroyFnmove: MoveFnregistered: Bool
struct Column #
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) -
capacitycapacity(self) -> Int
Rows that fit in the allocated blocks before the column must grow. -
basebase(self) -> ErasedPtr
Untyped pointer to row 0; dangles once the column grows. -
elementelement(self, row: Int) -> ErasedPtr
Untyped pointer torow(row * stridebytes pastbase); no bounds check. -
growgrow(mut self, needed: Int)
Ensures room forneededrows, at least doubling the capacity. Existing rows are relocated bytewise (no move constructor runs), so stored values must not point into themselves, and pointers frombase/elementdangle afterwards.ticksis left to the caller.
struct Archetype #
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] ) -
countcount(self) -> Int
Number of entities (rows) stored in this archetype. -
hashas(self, component: Int) -> Bool
Whether component idcomponentis in this archetype's signature. -
matchesmatches(self, with_: List[Int], without: List[Int]) -> Bool
Whether the archetype has every id inwith_and none of the ids inwithout. -
reserve_rowreserve_row(mut self, entity: Entity, tick: UInt32) -> Int
Appendsentitywith a new row stampedtickin every column; returns the row index. Component values in the new row are uninitialised: the caller must write every column. -
swap_removeswap_remove( mut self, row: Int, infos: List[ComponentInfo], destroy: Bool ) -> Entity
Removesrow, moving the last row into it. Returns the entity that moved (or null).
struct World #
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 forpropagate_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) -
registerregister[T: Component](mut self) raises -> Int
RegistersTunder itscomponent_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 whenTneeds alignment above 16 bytes.addregisters implicitly;CommandBuffer.applyneeds the type registered beforehand. -
advance_tickadvance_tick(mut self) -> UInt32
Increments the change tick and returns it; later writes are stamped with it. -
spawnspawn(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_aliveis_alive(self, e: Entity) -> Bool
Whethereis not null, in range, alive, and of the index's current generation. -
despawndespawn(mut self, e: Entity) raises
Destroys the entity's components and frees its index for reuse. Raises wheneis not alive. The row is swap-removed, so the archetype's last entity moves into it (pointers into that archetype dangle). -
countcount(self) -> Int
Number of live entities. -
addadd[T: Component](mut self, e: Entity, var value: T) raises
Adds (or replaces) a component. Replacing marks it changed. -
removeremove[T: Component](mut self, e: Entity) raises -> Bool
Removes a component; False when the entity did not have it. -
hashas[T: Component](self, e: Entity) -> Bool
Whethereis alive and has aT; False (no raise) for dead or null entities. -
getget[T: Component](self, e: Entity) raises -> T
A copy of the component. -
get_ptrget_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_mutget_mut[ T: Component ](mut self, e: Entity) raises -> Pointer[T, MutUntrackedOrigin]
Likeget_ptrand marks the component changed at the current tick. -
setset[T: Component](mut self, e: Entity, var value: T) raises
Overwrites an existingTand marks it changed (viaget_mut). Raises wheneis not alive or has noT; useaddto attach a new component. -
changed_tickchanged_tick[T: Component](self, e: Entity) raises -> UInt32
The tick at whiche'sTwas last written. Raises wheneis dead or has noT. -
entitiesentities(self) -> List[Entity]
A new list of every live entity, grouped by archetype (not in spawn order). -
archetype_countarchetype_count(self) -> Int
Archetypes created so far, including the empty one (they are never removed). -
queryquery(self) -> Query
Starts aQuerybuilder over this world. The query keeps an untracked pointer to the world and must not outlive it. Systems that run every frame should keep aQuerySpecand usecollect_chunks, which reuses its list. -
collect_prop_chunkscollect_prop_chunks(mut self)
collect_chunksofprop_specinto the world's ownprop_chunksscratch (transform propagation). -
collect_chunkscollect_chunks(self, spec: QuerySpec, mut out: List[Chunk])
Fillsout(cleared first, capacity kept) with the chunks matchingspec. Systems keep theirQuerySpecand chunk list across frames so steady-state frames allocate nothing.
def pass_through #
def pass_through(p: ErasedPtr)Hands p to keep; the body of the no-op destroy/move used for unregistered ids.
struct Chunk #
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 ) -
hashas[T: Component](self) -> Bool
Whether every entity in this chunk hasT. -
entityentity(self, row: Int) -> Entity
The entity atrowof this chunk's archetype. -
columncolumn[T: Component](self) -> Pointer[T, MutUntrackedOrigin]
Base pointer of the component column (elementrowiscolumn[T]()[unsafe_offset=row]). Aborts when the chunk has noT(usehasfor optional components). -
stridestride[T: Component]() -> Int
Bytes between consecutiveTelements of a column (component_stride[T]()). -
getget[T: Component](self, row: Int) -> Pointer[T, MutUntrackedOrigin]
Pointer to theTatrow; does not mark it changed (seemark_changed). Aborts when the chunk has noT(usehas);rowis not range-checked. The pointer dangles after the next structural change. -
mark_changedmark_changed[T: Component](self, row: Int)
Stamps theTatrowwith the world's current tick; aborts when the chunk has noT. -
changed_sincechanged_since[T: Component](self, row: Int, tick: UInt32) -> Bool
Whether theTatrowwas written aftertick(a write at exactlytickis not). Aborts when the chunk has noT.
struct QuerySpec #
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 componentT; returns the spec for chaining. -
withoutwithout[T: Component](var self) -> QuerySpec
Excludes archetypes that haveT; returns the spec for chaining.
struct Query #
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 componentT; returns the query for chaining. -
withoutwithout[T: Component](var self) -> Query
Excludes archetypes that haveT; returns the query for chaining. -
chunkschunks(self) -> List[Chunk]
A new list with oneChunkper non-empty matching archetype, in archetype order. Each chunk'scountis captured here and goes stale after a structural change. -
countcount(self) -> Int
Number of matching entities (builds the chunk list to sum it).