` line per entry, replacing `path`; raises on error.
- **`find`** `find(self, logical: String) -> Int`
Index of the entry for `logical`, or -1.
- **`record`** `record(mut self, logical: String, source_hash: String, file: String)`
Replaces the entry for `logical`, or appends one when there is none.
- **`prune`** `prune( mut self, root: String, produced: List[String] ) -> List[ManifestEntry]`
Removes the entries under `root` (logical paths starting with it, `root` itself excepted) that are not in `produced`, and returns them in manifest order.
- **`up_to_date`** `up_to_date( self, logical: String, source_hash: String, out_dir: String ) -> Bool`
Whether `logical` is recorded with `source_hash` and its file exists in `out_dir`.
### struct `CookReport`
```mojo
struct CookReport(Movable)
```
What one `cook_gltf` call did. `cooked` counts outputs written; `skipped` counts outputs found up to date when the whole source was skipped (the two never mix in one report).
Fields:
- `cooked`: Int
- `skipped`: Int
- `outputs`: List[String] — Logical paths produced (or found up to date), in cook order.
- **`__init__`** `__init__(out self)`
### def `cook_gltf`
```mojo
def cook_gltf( lib: NativePtr, source_path: String, prefix: String, out_dir: String, mut manifest: CookManifest, ) raises -> CookReport
```
Cooks every mesh primitive, material and animation of a glTF file. `prefix` is the logical name of the source (e.g. `models/Fox`); outputs are `#mesh/prim`, `#material` and `#anim`.
## `cinder.assets.cooked`
Source: `cinder/assets/cooked.mojo`
Cooked asset formats (little-endian binary, versioned, bounds-checked on read). - `.cmesh`: engine vertex stream (56-byte vertices), optional skin stream, u32 indices with LOD ranges, bounds, morph deltas. Written by the cooker from glTF primitives (tangents generated, LODs built with meshoptimizer), loaded by `AssetServer` without any further processing. - `.cclip`: an animation clip (channels of keyframes targeting node indices). Asset ids are the FNV-1a hash of the logical asset path (`models/Fox.glb#mesh0/prim1`), so they are stable across cooks and machines.
- `comptime MESH_MAGIC = "CMSH"` — First four bytes of a `.cmesh` file.
- `comptime MESH_VERSION = 1` — `.cmesh` layout version; `read_mesh` rejects any other (re-cook, there are no migrations).
- `comptime CLIP_MAGIC = "CCLP"` — First four bytes of a `.cclip` file.
- `comptime CLIP_VERSION = 1` — `.cclip` layout version; `read_clip` rejects any other.
### def `asset_id`
```mojo
def asset_id(logical_path: String) -> UInt64
```
Stable 64-bit id of a logical asset path: its FNV-1a hash.
### def `asset_file_name`
```mojo
def asset_file_name(logical_path: String, extension: String) -> String
```
`<16 hex digits>.`: content-addressed by logical path, not by contents.
### struct `CookedMesh`
```mojo
struct CookedMesh(Movable)
```
What `.cmesh` holds. Vertex and index bytes are already in GPU layout.
Fields:
- `vertex_count`: UInt32
- `vertex_bytes`: List[UInt8]
- `skin_bytes`: List[UInt8]
- `index_bytes`: List[UInt8]
- `lods`: List[MeshLod]
- `bounds_min`: Vec3
- `bounds_max`: Vec3
- `morph_target_count`: UInt32
- `morph_bytes`: List[UInt8]
- `material`: String — Logical path of the cooked material, or empty.
- **`__init__`** `__init__(out self)`
### def `cook_mesh`
```mojo
def cook_mesh( data: MeshData, lod_indices: List[List[UInt32]], material: String ) -> CookedMesh
```
Packs a validated `MeshData` and its LOD index lists.
### def `write_mesh`
```mojo
def write_mesh(mesh: CookedMesh) -> List[UInt8]
```
Serialises a `CookedMesh` to `.cmesh` bytes (version MESH_VERSION, little-endian). Layout: magic, version, vertex count, byte lengths of the vertex, skin and index streams, LOD count, bounds min and max, morph target count and byte length, material path (u32 length + bytes), the LOD table (first index, index count, error), then the vertex, skin, index and morph streams.
### struct `Cursor`
```mojo
struct Cursor(Movable)
```
Bounds-checked little-endian reader over an owned byte buffer (the cooked-format readers). Every read raises when it would run past the end of the data.
Fields:
- `data`: List[UInt8]
- `pos`: Int
- **`__init__`** `__init__(out self, var data: List[UInt8])`
- **`remaining`** `remaining(self) -> Int`
Bytes left after the read position.
- **`u8`** `u8(mut self) raises -> UInt8`
Reads one byte; raises at the end of the data.
- **`u32`** `u32(mut self) raises -> UInt32`
Reads a little-endian u32; raises when fewer than 4 bytes remain.
- **`f32`** `f32(mut self) raises -> Float32`
Reads a float32 from the bit pattern of a little-endian u32.
- **`bytes`** `bytes(mut self, count: Int) raises -> List[UInt8]`
Copies the next `count` bytes; raises if `count` is negative or above `remaining()`.
- **`string`** `string(mut self) raises -> String`
Reads a u32 byte length and that many bytes of UTF-8 text; raises when the length exceeds the data or the bytes are not valid UTF-8.
- **`magic`** `magic(mut self, expected: String) raises`
Reads the bytes of `expected`; raises `bad magic` at the first mismatch.
### def `read_mesh`
```mojo
def read_mesh(var data: List[UInt8]) raises -> CookedMesh
```
Parses `.cmesh` bytes into a `CookedMesh`, taking ownership of `data`. Raises on a wrong magic or version; stream lengths that disagree with the vertex count (56 bytes per vertex, 24 per skin vertex, 32 per vertex and morph target); index bytes not a multiple of 4; a LOD count outside 1..16; a LOD range outside the index buffer or not a multiple of 3; any index >= vertex count; truncated data or trailing bytes.
### struct `ClipChannel`
```mojo
struct ClipChannel(Copyable, Movable)
```
Keyframes for one node property. `path`: 0 translation, 1 rotation, 2 scale, 3 weights. `interpolation`: 0 linear, 1 step, 2 cubic spline. `values` holds `components` floats per key (3 per cubic-spline key: in-tangent, value, out-tangent).
Fields:
- `node`: Int
- `path`: Int
- `interpolation`: Int
- `components`: Int
- `times`: List[Float32]
- `values`: List[Float32]
### struct `Clip`
```mojo
struct Clip(Copyable, Movable)
```
A cooked animation clip: name, duration (the latest keyframe time) and its channels.
Fields:
- `name`: String
- `duration`: Float32
- `channels`: List[ClipChannel]
- **`__init__`** `__init__(out self, name: String)`
### def `write_clip`
```mojo
def write_clip(clip: Clip) -> List[UInt8]
```
Serialises a clip to `.cclip` bytes (version CLIP_VERSION, little-endian). Layout: magic, version, name, duration, channel count, then per channel its node, path, interpolation, components, key count and value count followed by the times and values.
### def `read_clip`
```mojo
def read_clip(var data: List[UInt8]) raises -> Clip
```
Parses `.cclip` bytes into a `Clip`, taking ownership of `data`. Raises on a wrong magic or version, more than 100000 channels, a path above 3, an interpolation above 2, components outside 1..64, a value count other than keys * components (* 3 for cubic spline), decreasing keyframe times, truncated data or trailing bytes. Node indices are not checked.
- `comptime MATERIAL_MAGIC = "CMAT"` — First four bytes of a `.cmat` file.
- `comptime MATERIAL_VERSION = 1` — `.cmat` layout version; `read_material` rejects any other.
### struct `CookedMaterial`
```mojo
struct CookedMaterial(Copyable, Movable)
```
Material parameters plus the logical image path per map slot (empty when absent).
Fields:
- `material`: Material
- `images`: Array[String, 5]
- `linear_filter`: Bool
- `address_mode`: UInt32
- **`__init__`** `__init__(out self, var material: Material)`
### def `write_material`
```mojo
def write_material(cm: CookedMaterial) -> List[UInt8]
```
Serialises a `CookedMaterial` to `.cmat` bytes (version MATERIAL_VERSION, little-endian). Layout: magic, version, name, factors, alpha mode and cutoff, double-sided flag, UV transform, then per map slot (5) the texcoord set and image path, then the sampler's linear-filter flag and address mode.
### def `read_material`
```mojo
def read_material(var data: List[UInt8]) raises -> CookedMaterial
```
Parses `.cmat` bytes into a `CookedMaterial`, taking ownership of `data`. Raises on a wrong magic or version, an alpha mode above 2, truncated data or trailing bytes. Only the parameters and image paths are read; `AssetServer` resolves the textures and creates the sampler.
## `cinder.assets.gltf`
Source: `cinder/assets/gltf.mojo`
glTF 2.0 import through cgltf: meshes (with tangents, second UV set, skinning, morph targets), metallic-roughness materials (KHR_texture_transform, KHR_materials_emissive_strength), images (file, data URI or GLB buffer view), nodes, skins and scenes. `load_gltf` produces a CPU-side `GltfAsset`; `instantiate` uploads it through a `Renderer` and fills a `RenderScene` with one instance per (node, primitive), joints at the bind pose and the default morph weights. Malformed files raise; they never crash (cgltf validates the structure and every accessor read is bounds-checked by cgltf).
### struct `GltfImage`
```mojo
struct GltfImage(Copyable, Movable)
```
An image decoded on the CPU by `load_gltf`, with its glTF name (empty when unnamed).
Fields:
- `name`: String
- `image`: Image
### struct `GltfTexture`
```mojo
struct GltfTexture(ImplicitlyCopyable, Movable)
```
A glTF texture: image index (-1 without one) plus sampler settings. `linear_filter` is True for a linear or unspecified magnification filter; `address_mode` is the WGPU address mode from the sampler's `wrap_s` (repeat when there is no sampler).
Fields:
- `image`: Int
- `linear_filter`: Bool
- `address_mode`: UInt32
### struct `GltfMaterial`
```mojo
struct GltfMaterial(Copyable, Movable)
```
Engine material parameters plus the asset texture index per map (-1 when absent).
Fields:
- `material`: Material
- `textures`: Array[Int, 5]
- **`__init__`** `__init__(out self, var material: Material)`
### struct `GltfPrimitive`
```mojo
struct GltfPrimitive(Copyable, Movable)
```
A triangle primitive as validated `MeshData` and its material index (-1 when none). Point, line and strip primitives are dropped on import.
Fields:
- `mesh`: MeshData
- `material`: Int
### struct `GltfMesh`
```mojo
struct GltfMesh(Copyable, Movable)
```
A glTF mesh: name, its triangle primitives and the mesh's default morph weights.
Fields:
- `name`: String
- `primitives`: List[GltfPrimitive]
- `weights`: List[Float32]
### struct `GltfNode`
```mojo
struct GltfNode(Copyable, Movable)
```
A scene node; every reference is an index into `GltfAsset`, -1 when absent. `local` and `world` are computed by cgltf at import (the world matrix walks the parents); `weights` are the node's morph weight overrides (empty when it has none).
Fields:
- `name`: String
- `parent`: Int
- `children`: List[Int]
- `mesh`: Int
- `skin`: Int
- `local`: Mat4
- `world`: Mat4
- `weights`: List[Float32]
### struct `GltfSkin`
```mojo
struct GltfSkin(Copyable, Movable)
```
Joint node indices and an inverse bind matrix each (identity where the file has none).
Fields:
- `joints`: List[Int]
- `inverse_bind`: List[Mat4]
### struct `GltfChannel`
```mojo
struct GltfChannel(Copyable, Movable)
```
One animated node property. `path`: 0 translation, 1 rotation, 2 scale, 3 weights; `interpolation`: 0 linear, 1 step, 2 cubic spline (values hold in-tangent, value, out-tangent).
Fields:
- `node`: Int
- `path`: Int
- `interpolation`: Int
- `components`: Int
- `times`: List[Float32]
- `values`: List[Float32]
### struct `GltfAnimation`
```mojo
struct GltfAnimation(Copyable, Movable)
```
An animation: name, duration (latest keyframe time over its channels) and channels. Channels without a sampler, target node or supported target path are skipped on import.
Fields:
- `name`: String
- `duration`: Float32
- `channels`: List[GltfChannel]
- **`__init__`** `__init__(out self, name: String)`
### struct `SceneBounds`
```mojo
struct SceneBounds(ImplicitlyCopyable, Movable)
```
Axis-aligned box from `GltfAsset.world_bounds`; all zero when the asset has no meshes.
Fields:
- `min`: Vec3
- `max`: Vec3
- **`centre`** `centre(self) -> Vec3`
Midpoint of the box.
- **`radius`** `radius(self) -> Float32`
Half the diagonal: the radius of the sphere around `centre` that encloses the box.
### struct `GltfAsset`
```mojo
struct GltfAsset(Movable)
```
Everything `load_gltf` imported from one file, CPU side; cross references are list indices. `roots` holds the default scene's root nodes, or every parentless node when the file has no default scene.
Fields:
- `path`: String
- `images`: List[GltfImage]
- `textures`: List[GltfTexture]
- `materials`: List[GltfMaterial]
- `meshes`: List[GltfMesh]
- `nodes`: List[GltfNode]
- `skins`: List[GltfSkin]
- `animations`: List[GltfAnimation]
- `roots`: List[Int]
- **`__init__`** `__init__(out self, path: String)`
- **`world_bounds`** `world_bounds(self) -> SceneBounds`
Axis-aligned bounds of every mesh instance in world space. Skinned meshes are posed on the CPU with their bind-pose joint matrices (what the GPU draws at rest).
### def `load_gltf`
```mojo
def load_gltf(lib: NativePtr, path: String) raises -> GltfAsset
```
Parses a .gltf or .glb file (and its buffers and images) into a `GltfAsset`.
### struct `UploadedGltf`
```mojo
struct UploadedGltf(Movable)
```
Renderer ids for an uploaded asset: `mesh_ids[mesh][primitive]`, `material_ids[material]` and a default material for primitives without one.
Fields:
- `mesh_ids`: List[List[Int]]
- `material_ids`: List[Int]
- `default_material`: Int
- **`__init__`** `__init__(out self)`
### def `upload_gltf`
```mojo
def upload_gltf( mut renderer: Renderer, mut res: Resources, asset: GltfAsset, lods: Bool = True, ) raises -> UploadedGltf
```
Uploads every image, material and primitive of `asset` to the renderer. Base colour and emissive images are sRGB; the rest are linear. Materials without textures share the renderer's default textures.
### def `instantiate`
```mojo
def instantiate( mut renderer: Renderer, mut res: Resources, asset: GltfAsset, mut scene: RenderScene, lods: Bool = True, ) raises -> Int
```
Uploads the asset and appends its instances, joints and morph weights to `scene`. Returns the number of instances added. Base colour and emissive images are sRGB; the rest are linear. Materials without textures share the renderer's default textures.
## `cinder.assets.runtime`
Source: `cinder/assets/runtime.mojo`
Runtime asset server: asynchronous loading of cooked assets on the job system, reference counting, and hot reload. Loading a cooked mesh has two halves: a job reads and parses the file (CPU only, any thread) into a heap slot; `update()` on the main thread uploads finished slots to the GPU through the renderer and flips the handle to ready. Nothing GPU-related happens off the main thread. Each logical asset is loaded once and shared; `release` drops a reference and frees the GPU mesh when the last one goes. `check_for_changes` reloads assets whose cooked file changed on disk and swaps the GPU mesh (or material) under the same renderer index, so instances keep working.
### struct `AssetState`
```mojo
struct AssetState(Equatable, ImplicitlyCopyable, RegisterPassable)
```
Load state of an `AssetServer` record: loading, ready or failed.
Fields:
- `value`: Int
- **`loading`** `loading() -> AssetState`
Load started and not finished; a material also stays here while its textures load.
- **`ready`** `ready() -> AssetState`
Loaded; the accessors return data.
- **`failed`** `failed() -> AssetState`
The load failed (see `AssetServer.error`) or the record was released.
### struct `AssetRecord`
```mojo
struct AssetRecord(Movable)
```
One logical asset known to an `AssetServer` and its loaded result. `kind` is 0 mesh, 1 clip, 2 texture, 3 material. Records are never removed: releasing the last reference leaves the record with state failed and error `released` (and an empty `logical`, so loading the path again makes a new record).
Fields:
- `logical`: String
- `file`: String
- `kind`: Int
- `state`: AssetState
- `refcount`: Int
- `mesh_index`: Int — Renderer mesh index once ready (meshes).
- `clip`: Optional[Clip]
- `texture_view`: TextureViewHandle
- `texture`: TextureHandle
- `material_index`: Int — Renderer material index once built (materials); kept across hot reloads.
- `sampler`: SamplerHandle — Material: the sampler the server created for it (null until built).
- `cooked_material`: Optional[CookedMaterial]
- `pending_textures`: List[Int] — Material: asset indices of textures that must be ready before the material is built (one reference each, held until the material is released or reloaded).
- `retired_textures`: List[Int] — Material being hot reloaded: the previous version's texture references, released once the new version is built (the renderer still draws the old one until then).
- `error`: String
- `mtime`: Int
- `slot_index`: Int
- `reloads`: Int
- **`__init__`** `__init__(out self, logical: String, file: String, kind: Int)`
### struct `AssetServer`
```mojo
struct AssetServer(Movable)
```
Loads cooked assets listed in `/manifest.txt`, shared by logical path and refcounted. Jobs only read, parse and transcode files; `update` does every GPU upload. All methods are main-thread calls (there are no locks). Indices from `load_*` stay valid for the server's lifetime. Dropping the server waits for all jobs but does not free the GPU resources of assets that were never released.
Fields:
- `lib`: NativePtr
- `jobs`: Pointer[JobSystem, MutUntrackedOrigin]
- `renderer`: Pointer[Renderer, MutUntrackedOrigin]
- `root`: String
- `manifest`: CookManifest
- `records`: List[AssetRecord]
- `counter`: Counter
- `in_flight`: Int
- `slots`: List[List[_LoadSlot]] — Load slots as single-element lists: their heap storage never moves, so jobs can hold pointers into them while this list grows.
- `slot_busy`: List[Bool]
- **`__init__`** `__init__( out self, lib: NativePtr, jobs: Pointer[JobSystem, MutUntrackedOrigin], renderer: Pointer[Renderer, MutUntrackedOrigin], root: String, ) raises`
- **`load_mesh`** `load_mesh(mut self, logical: String) raises -> Int`
Returns the asset index (also a reference). Loading starts immediately on a job. Raises when `logical` is not in the manifest or is known as another kind of asset. A known path returns its existing record (refcount + 1); one whose load failed is loaded again.
- **`load_clip`** `load_clip(mut self, logical: String) raises -> Int`
Returns the asset index of a cooked clip (also a reference); a job parses the file. Same sharing, kind check and retry of failed loads as `load_mesh`.
- **`load_texture`** `load_texture(mut self, logical: String) raises -> Int`
Returns the asset index of a cooked KTX2 texture (also a reference). The job reads and transcodes it (BC7 when the device supports block compression, else RGBA8); `update` uploads it. Same sharing, kind check and retry of failed loads as `load_mesh`.
- **`load_material`** `load_material(mut self, logical: String) raises -> Int`
Loads a cooked material and every texture it references; ready when all of them are. Same sharing, kind check and retry of failed loads as `load_mesh`.
- **`material_index`** `material_index(self, index: Int) raises -> Int`
Renderer material index of a ready material; raises otherwise.
- **`texture_view`** `texture_view(self, index: Int) raises -> TextureViewHandle`
View of a ready texture; raises otherwise. A hot reload destroys it for a new one.
- **`update`** `update(mut self) raises -> Int`
Main thread: finishes loads whose job completed (GPU upload for meshes). Returns how many assets became ready or failed this call.
- **`wait_all`** `wait_all(mut self) raises`
Blocks until every pending load is applied (tests, loading screens).
- **`state`** `state(self, index: Int) -> AssetState`
State of the record at `index` (failed once released).
- **`mesh_index`** `mesh_index(self, index: Int) raises -> Int`
Renderer mesh index of a ready asset, stable across hot reloads; raises when not ready. The kind is not checked: a ready asset that is not a mesh returns -1.
- **`clip`** `clip(self, index: Int) raises -> Clip`
A copy of a ready clip with all its keyframes; raises when not a ready clip.
- **`error`** `error(self, index: Int) -> String`
Message of a failed load (`released` after the last release), empty otherwise.
- **`refcount`** `refcount(self, index: Int) -> Int`
Outstanding references to the record at `index`; 0 once released.
- **`release`** `release(mut self, index: Int) raises`
Drops one reference; the last one frees what the asset holds on the GPU (mesh, texture, renderer material and its sampler), releases a material's textures and forgets the asset.
- **`check_for_changes`** `check_for_changes(mut self) -> Int`
Starts a reload for every ready asset whose cooked file changed. Returns how many.
- **`live_count`** `live_count(self) -> Int`
Number of records with a positive refcount.
## `cinder.assets.textures`
Source: `cinder/assets/textures.mojo`
KTX2 textures: cooking (BasisU ETC1S or UASTC with full mip chains) and runtime loading (transcode to BC7 when the device has block compression, otherwise to RGBA8), through libktx.
- `comptime VK_FORMAT_R8G8B8A8_UNORM: UInt32 = 37` — Vulkan format id for linear RGBA8: written for non-sRGB images, accepted when uncompressed.
- `comptime VK_FORMAT_R8G8B8A8_SRGB: UInt32 = 43` — Vulkan format id for sRGB RGBA8: written for sRGB images and read back as "is sRGB".
- `comptime VK_FORMAT_BC7_SRGB_BLOCK: UInt32 = 146` — Vulkan format id libktx reports after transcoding an sRGB texture to BC7.
### def `cook_texture_ktx2`
```mojo
def cook_texture_ktx2( lib: NativePtr, img: Image, path: String, srgb: Bool, normal_map: Bool, uastc: Bool, quality: Int = 128, ) raises
```
Writes `img` with a full mip chain as a BasisU-compressed KTX2 file. `uastc` picks the high-quality UASTC codec (normal maps, detail textures); otherwise ETC1S at `quality` (1..255). sRGB images carry the sRGB transfer function in the data format descriptor so loaders choose an sRGB GPU format.
### struct `LoadedTexture`
```mojo
struct LoadedTexture(Movable)
```
A KTX2 file transcoded on the CPU: one byte list per mip level, ready for `upload_loaded`.
Fields:
- `width`: Int
- `height`: Int
- `levels`: List[List[UInt8]]
- `bc7`: Bool
- `srgb`: Bool
- **`__init__`** `__init__(out self, width: Int, height: Int, bc7: Bool, srgb: Bool)`
### def `load_ktx2`
```mojo
def load_ktx2( lib: NativePtr, path: String, prefer_bc7: Bool ) raises -> LoadedTexture
```
Reads and transcodes a KTX2 file (CPU only; safe on a job thread).
### def `upload_loaded`
```mojo
def upload_loaded( mut res: Resources, label: String, loaded: LoadedTexture ) raises -> Tuple[TextureHandle, TextureViewHandle]
```
Creates the GPU texture (BC7 or RGBA8, sRGB variants when flagged) and uploads every level.
---
> Cinder 0.1.0 documentation: cinder.audio API
> Canonical page: https://cinder.ash10.dev/docs/api/audio/ · Source: `docs/api/audio.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.audio`
## `cinder.audio`
Source: `cinder/audio/__init__.mojo`
Audio over miniaudio: buses, decoded and streamed sounds, one-shots, 3D spatialisation, a listener, and the ECS bridge that follows entities and the camera.
## `cinder.audio.engine`
Source: `cinder/audio/engine.mojo`
Audio over miniaudio: buses, decoded and streamed sounds, one-shots, 3D spatialisation and a listener. The engine can run headless (no device) for tests and servers; frames are then pulled with `read_frames`. Sounds live in the shim (miniaudio structs are opaque to Mojo) and are tracked here with generational handles. Release sounds with `release` and destroy the buses you created with `destroy_bus`; `live_sounds()` and `live_buses()` must be zero before the engine is destroyed (the destructor still frees whatever is left). Sound-end and log callbacks post to the shim queue and are drained by `update`.
### struct `SoundHandle`
```mojo
struct SoundHandle(Equatable, ImplicitlyCopyable, Movable)
```
Generational handle to a sound owned by `AudioEngine`: a slot `index` and that slot's `generation`. It goes stale once the sound is released (one-shots are released by `AudioEngine.update` without any call of yours); `AudioEngine.is_valid` tells, and every method that takes a stale handle raises.
Fields:
- `index`: Int
- `generation`: Int
- **`null`** `null() -> SoundHandle`
The invalid handle (index -1); it converts to False.
### struct `Attenuation`
```mojo
struct Attenuation(Equatable, ImplicitlyCopyable, Movable)
```
Distance attenuation model of a spatial sound, mapped to miniaudio's models by `AudioEngine.set_attenuation`.
Fields:
- `value`: UInt8
- **`none`** `none() -> Attenuation`
No distance attenuation.
- **`inverse`** `inverse() -> Attenuation`
miniaudio's inverse-distance model; the `SoundDesc` default.
- **`linear`** `linear() -> Attenuation`
miniaudio's linear model.
- **`exponential`** `exponential() -> Attenuation`
miniaudio's exponential model.
### struct `SoundDesc`
```mojo
struct SoundDesc(Copyable, Movable)
```
How `AudioEngine.load` creates a sound. The constructor takes the file path, bus index, `streamed`, `looping` and `spatial`; the other fields keep their defaults until set on the value: volume 1, pitch 1, inverse attenuation with `min_distance` 1 m, `max_distance` 100 m and rolloff 1, not a one-shot. The attenuation settings apply only when `spatial` is set.
Fields:
- `path`: String
- `bus`: Int
- `streamed`: Bool — Decode while playing (music); otherwise the whole file is decoded up front.
- `looping`: Bool
- `spatial`: Bool
- `volume`: Float32
- `pitch`: Float32
- `attenuation`: Attenuation
- `min_distance`: Float32
- `max_distance`: Float32
- `rolloff`: Float32
- `one_shot`: Bool — Released automatically when it ends.
- **`__init__`** `__init__( out self, path: String, bus: Int = 0, streamed: Bool = False, looping: Bool = False, spatial: Bool = True, )`
### struct `AudioEngine`
```mojo
struct AudioEngine(Movable)
```
Owns the miniaudio engine, its mixing buses and every loaded sound. Bus 0 is the master bus and lives as long as the engine; buses made with `create_bus` are destroyed with `destroy_bus`. Sounds are addressed by `SoundHandle` and freed with `release` (one-shots by `update`); the destructor frees whatever is left, but `live_sounds()` and `live_buses()` should be zero first. The `NativePtr` library must outlive the engine. Call every method from the main thread: miniaudio's threads only post events, which `update` drains. The constructor raises when the event queue or the miniaudio engine cannot be created (for example no audio device without `headless`).
Fields:
- `lib`: NativePtr
- `queue`: Optional[Pointer[CnEventQueue, MutUntrackedOrigin]]
- `engine`: Optional[Pointer[ma_engine, MutUntrackedOrigin]]
- `buses`: List[Optional[Pointer[ma_sound_group, MutUntrackedOrigin]]] — One entry per bus index ever created; a destroyed bus leaves an empty entry so the other indices stay stable (indices are not reused).
- `bus_names`: List[String]
- `bus_parents`: List[Int] — Parent bus index of each bus (-1 for the master bus on the engine endpoint).
- `slots`: List[_Slot]
- `free_slots`: List[Int]
- `diagnostics`: List[LogRecord]
- `ended`: List[SoundHandle] — Sounds that finished since the last `take_ended`.
- `headless`: Bool
- `sample_rate`: Int
- `channels`: Int
- **`__init__`** `__init__( out self, lib: NativePtr, headless: Bool = False, sample_rate: Int = 48000, channels: Int = 2, ) raises`
- **`create_bus`** `create_bus(mut self, name: String, parent: Int = 0) raises -> Int`
A mixing bus under `parent` (0 = master, -1 = the engine endpoint). Returns its index; destroy it with `destroy_bus`. Raises when `parent` does not exist (or was destroyed) or miniaudio fails to create the group.
- **`destroy_bus`** `destroy_bus(mut self, bus: Int) raises`
Destroys a bus made with `create_bus`; its index stays invalid (indices are not reused) and `live_buses()` drops by one. Raises for the master bus (0, owned by the engine), a bus that does not exist or was already destroyed, and a bus that still has sounds or live child buses routed into it: release or destroy those first.
- **`find_bus`** `find_bus(self, name: String) -> Int`
Index of the first live bus called `name` (the master bus is 0), or -1 when there is none.
- **`set_bus_volume`** `set_bus_volume(self, bus: Int, volume: Float32) raises`
Sets the linear volume of bus `bus` (1 is unity), which scales every sound and bus routed through it. Raises when the bus does not exist.
- **`bus_volume`** `bus_volume(self, bus: Int) raises -> Float32`
Linear volume of bus `bus`. Raises when the bus does not exist.
- **`set_master_volume`** `set_master_volume(self, volume: Float32)`
Sets the engine's own output volume (linear), separate from the master bus volume; miniaudio's result code is ignored.
- **`live_buses`** `live_buses(self) -> Int`
Buses created with `create_bus` and not yet destroyed with `destroy_bus` (the master bus is the engine's own); should be zero before the engine is destroyed.
- **`load`** `load(mut self, desc: SoundDesc) raises -> SoundHandle`
Creates a sound from a file (WAV, FLAC, MP3, Vorbis). It starts stopped.
- **`play_one_shot`** `play_one_shot( mut self, var desc: SoundDesc, position: Vec3 ) raises -> SoundHandle`
Loads, positions and starts a sound that is released when it ends.
- **`release`** `release(mut self, handle: SoundHandle) raises`
Destroys the sound and invalidates `handle` and its copies; the slot is reused with a new generation. Raises on a stale handle, so do not release a one-shot that `update` already released.
- **`is_valid`** `is_valid(self, handle: SoundHandle) -> Bool`
Whether `handle` refers to a live sound (in range, same generation, not released). Never raises.
- **`live_sounds`** `live_sounds(self) -> Int`
Number of sounds not yet released, one-shots included; should be zero before the engine is destroyed.
- **`play`** `play(self, handle: SoundHandle) raises`
Starts the sound. Raises on a stale handle or when miniaudio fails to start it.
- **`stop`** `stop(self, handle: SoundHandle) raises`
Stops the sound without releasing it. Raises on a stale handle; miniaudio's result code is ignored.
- **`is_playing`** `is_playing(self, handle: SoundHandle) raises -> Bool`
Whether the sound is playing. Raises on a stale handle.
- **`at_end`** `at_end(self, handle: SoundHandle) raises -> Bool`
Whether the sound has reached the end of its data. Raises on a stale handle.
- **`seek`** `seek(self, handle: SoundHandle, frame: Int) raises`
Moves the playback cursor to PCM frame `frame`. Raises on a stale handle; miniaudio's result code is ignored.
- **`set_volume`** `set_volume(self, handle: SoundHandle, volume: Float32) raises`
Sets the sound's linear volume (1 is unity). Raises on a stale handle.
- **`set_pitch`** `set_pitch(self, handle: SoundHandle, pitch: Float32) raises`
Sets the sound's pitch multiplier (1 is the original pitch). Raises on a stale handle.
- **`set_pan`** `set_pan(self, handle: SoundHandle, pan: Float32) raises`
Sets the sound's stereo pan (-1 left, 0 centre, 1 right); the only positioning control of a non-spatial sound. Raises on a stale handle.
- **`set_looping`** `set_looping(self, handle: SoundHandle, looping: Bool) raises`
Turns looping on or off. Raises on a stale handle.
- **`fade`** `fade( self, handle: SoundHandle, from_volume: Float32, to_volume: Float32, milliseconds: Int, ) raises`
Fades the sound's volume from `from_volume` to `to_volume` over `milliseconds`. Raises on a stale handle.
- **`set_position`** `set_position(self, handle: SoundHandle, p: Vec3) raises`
Sets the sound's world position in metres (used while spatialisation is on). Raises on a stale handle.
- **`set_velocity`** `set_velocity(self, handle: SoundHandle, v: Vec3) raises`
Sets the sound's velocity in metres per second, used for the Doppler effect. Raises on a stale handle.
- **`set_spatial`** `set_spatial(self, handle: SoundHandle, enabled: Bool) raises`
Turns 3D spatialisation of the sound on or off. Raises on a stale handle.
- **`set_attenuation`** `set_attenuation( self, handle: SoundHandle, model: Attenuation, min_distance: Float32, max_distance: Float32, rolloff: Float32, ) raises`
Sets the distance attenuation: the `model`, `min_distance` and `max_distance` in metres and the `rolloff` factor. An unrecognised model value falls back to inverse. Raises on a stale handle.
- **`set_listener`** `set_listener( self, position: Vec3, forward: Vec3, up: Vec3 = Vec3.unit_y(), velocity: Vec3 = Vec3.zero(), )`
Places miniaudio's listener 0: `position` in metres, the `forward` direction it faces, the world `up` and its `velocity` in metres per second (Doppler). The engine convention is -Z forward and +Y up; `sync_audio` calls this from the `AudioListener` entity.
- **`update`** `update(mut self) raises -> Int`
Drains the shim queue: log messages become diagnostics, finished sounds are recorded (and one-shots released). Returns the number of sounds that ended.
- **`take_ended`** `take_ended(mut self) -> List[SoundHandle]`
Returns the handles of sounds that ended since the last call and clears the list. Handles of one-shots are already stale when they are returned.
- **`error_count`** `error_count(self) -> Int`
Number of error-level records in `diagnostics` (miniaudio log messages collected by `update`).
- **`read_frames`** `read_frames(self, frames: Int) raises -> List[Float32]`
Headless engines: mixes `frames` interleaved float frames (tests, offline rendering).
## `cinder.audio.sync`
Source: `cinder/audio/sync.mojo`
ECS <-> audio: `AudioSource` entities position their sounds, `AudioListener` places the listener (attach it to the camera entity so the listener follows the camera).
- `comptime AUDIO_SOURCE_COMPONENT_ID = 10` — ECS component id of `AudioSource`.
- `comptime AUDIO_LISTENER_COMPONENT_ID = 11` — ECS component id of `AudioListener`.
### struct `AudioSource`
```mojo
struct AudioSource(Component)
```
A spatial sound that follows its entity. `velocity` feeds the Doppler effect.
Fields:
- `sound`: SoundHandle
- `velocity`: Vec3
- **`component_id`** `component_id() -> Int`
### struct `AudioListener`
```mojo
struct AudioListener(Component)
```
Marks the entity whose world transform drives the listener (position, -Z forward, +Y up). With several, the first one found wins.
Fields:
- `velocity`: Vec3
- **`component_id`** `component_id() -> Int`
### def `sync_audio`
```mojo
def sync_audio(world: World, audio: AudioEngine) raises -> Int
```
Pushes entity transforms to the sounds and the listener. Returns the number of sources positioned; stale handles are skipped.
---
> Cinder 0.1.0 documentation: cinder.core API
> Canonical page: https://cinder.ash10.dev/docs/api/core/ · Source: `docs/api/core.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.core`
## `cinder.core`
Source: `cinder/core/__init__.mojo`
Core: math, logging, assertions, allocators, containers, handles, time, file system. No I/O to GPU, audio or window systems lives here.
## `cinder.core.arena`
Source: `cinder/core/arena.mojo`
LinearArena: a bump allocator for per-frame scratch memory. One block is allocated up front; `alloc_bytes` hands out aligned sub-ranges in O(1) and `reset` returns everything at once. There is no per-allocation free. After warm-up the engine's frame loop allocates only from arenas and pools, which is what makes the zero-allocation steady-state test possible.
### struct `LinearArena`
```mojo
struct LinearArena(Movable)
```
Bump allocator over one fixed block. Every address returned is aligned as requested.
Fields:
- **`__init__`** `__init__(out self, capacity: Int) raises`
Allocates `capacity` bytes once. Raises on a non-positive capacity.
- **`capacity`** `capacity(self) -> Int`
Usable bytes requested at construction; the 64-byte alignment slack is not counted.
- **`used`** `used(self) -> Int`
Bytes handed out since the last reset, including alignment padding.
- **`high_water`** `high_water(self) -> Int`
Largest `used()` ever observed; the number to size the arena by.
- **`alloc_bytes`** `alloc_bytes( mut self, size: Int, alignment: Int = 16 ) raises -> Pointer[UInt8, MutUntrackedOrigin]`
Returns `size` bytes aligned to `alignment` (a power of two <= 64). Raises when exhausted.
- **`alloc`** `alloc[ T: AnyType ](mut self, count: Int = 1) raises -> Pointer[T, MutUntrackedOrigin]`
Uninitialised storage for `count` values of `T`, aligned for `T`. The caller initialises it.
- **`reset`** `reset(mut self)`
Makes the whole block available again. Previously returned pointers become dangling.
## `cinder.core.assertions`
Source: `cinder/core/assertions.mojo`
Engine assertions: broken invariants abort with an explicit message. `cinder_assert` is compiled in unless the build defines `CINDER_ASSERTS=0` (`mojo build -D CINDER_ASSERTS=0`), which release builds of the shipped game set and nothing else does. `cinder_check` is never compiled out and is used where the cost is negligible and the invariant guards memory safety (handle validation, FFI preconditions). Both abort rather than raise: an invariant violation is a bug, not a recoverable condition (section 4 of the brief).
- `comptime ASSERTS_ENABLED: Bool = get_defined_bool["CINDER_ASSERTS", True]()` — Whether `cinder_assert` is compiled in. Read from -D CINDER_ASSERTS at build time.
### def `cinder_assert`
```mojo
def cinder_assert(condition: Bool, message: StaticString)
```
Aborts with `message` when `condition` is false, unless asserts are compiled out.
### def `cinder_check`
```mojo
def cinder_check(condition: Bool, message: StaticString)
```
Aborts with `message` when `condition` is false, in every build.
## `cinder.core.base64`
Source: `cinder/core/base64.mojo`
Base64 decoding (RFC 4648, with or without padding) for data URIs in glTF files.
### def `decode_base64`
```mojo
def decode_base64(text: StringSpan) raises -> List[UInt8]
```
Decodes base64 text; whitespace is skipped and '=' padding is optional.
## `cinder.core.boxed`
Source: `cinder/core/boxed.mojo`
Boxed: a value at a stable heap address. Subsystems that hand out `Pointer`s to themselves (Gfx, Resources, Renderer, JobSystem, ...) must never move once those pointers exist. `Boxed[T]` owns one `T` on the heap so the owner can move freely while the pointee stays put.
### struct `Boxed`
```mojo
struct Boxed[T: Deinitable & Movable](Movable)
```
Owns one `T` on the heap so pointers to it stay valid while the box itself moves. The value is moved in at construction and destroyed, then freed, when the box is destroyed. `ptr()` and `[]` give untracked mutable access; neither may outlive the box.
Fields:
- **`__init__`** `__init__(out self, var value: Self.T)`
- **`ptr`** `ptr(self) -> Pointer[Self.T, MutUntrackedOrigin]`
A raw pointer valid for the box's lifetime.
- **`__getitem__`** `__getitem__(ref self) -> ref[MutUntrackedOrigin] Self.T`
## `cinder.core.bytes`
Source: `cinder/core/bytes.mojo`
Little-endian byte packing for GPU uploads (std140/std430-style layouts are the caller's job).
### struct `ByteWriter`
```mojo
struct ByteWriter(Movable, Sized)
```
Appends plain values to a byte buffer. `pad_to` aligns the length for struct rules.
Fields:
- `bytes`: List[UInt8]
- **`__init__`** `__init__(out self, capacity: Int = 256)`
- **`__len__`** `__len__(self) -> Int`
- **`clear`** `clear(mut self)`
Empties the buffer, keeping its capacity (same as `reset`).
- **`u8`** `u8(mut self, v: UInt8)`
Appends one byte.
- **`u32`** `u32(mut self, v: UInt32)`
Appends `v` as four bytes, little-endian.
- **`i32`** `i32(mut self, v: Int32)`
Appends `v` as four bytes, little-endian two's complement.
- **`f32`** `f32(mut self, v: Float32)`
Appends the IEEE-754 bits of `v` as four bytes, little-endian.
- **`vec2`** `vec2(mut self, v: Vec2)`
Appends x then y as two `f32` (8 bytes, no padding).
- **`vec3`** `vec3(mut self, v: Vec3)`
Three floats, no padding (callers add `f32` or `pad_to(16)` for vec4 alignment).
- **`vec3_padded`** `vec3_padded(mut self, v: Vec3, w: Float32 = 0.0)`
Appends x, y, z, then `w` (default 0) as four `f32`: a vec3 in a 16-byte vec4 slot.
- **`vec4`** `vec4(mut self, v: Vec4)`
Appends x, y, z, w as four `f32` (16 bytes).
- **`mat4`** `mat4(mut self, m: Mat4)`
Column-major, matching WGSL `mat4x4`.
- **`pad_to`** `pad_to(mut self, alignment: Int)`
Zero-fills to the next multiple of `alignment`; no-op when already aligned. `alignment` must be positive: 0 or a negative value is a bug and `cinder_assert` aborts. With assertions compiled out it leaves the bytes unchanged.
- **`span`** `span(self) -> Span[UInt8, origin_of(self.bytes)]`
A view of the bytes written so far, borrowed from the writer; invalid after it changes.
- **`reset`** `reset(mut self)`
Empties the writer, keeping its capacity (frame scratch buffers).
- **`take`** `take(deinit self) -> List[UInt8]`
Consumes the writer and returns its bytes.
## `cinder.core.cstring`
Source: `cinder/core/cstring.mojo`
C strings to Mojo `String`s, decoding the bytes as UTF-8. C APIs (SDL, the shim, stb) hand back NUL-terminated UTF-8. Converting byte by byte with `chr` treats every byte as a code point and double-encodes anything outside ASCII, so every conversion goes through `string_from_c` instead.
### def `string_from_c`
```mojo
def string_from_c( p: Optional[Pointer[c_char, MutUntrackedOrigin]], max_len: Int = -1 ) -> String
```
The UTF-8 text at `p` up to its NUL terminator, or up to `max_len` bytes when `max_len` is not negative (fixed-size C buffers). A null pointer gives the empty string. Invalid UTF-8 sequences become U+FFFD instead of failing: C text is diagnostic data here and must never make the caller raise. The caller guarantees `p` points at a terminated string or at least `max_len` readable bytes.
## `cinder.core.fs`
Source: `cinder/core/fs.mojo`
Filesystem helpers with typed errors. Thin on purpose: the asset database layers on top.
### struct `FileError`
```mojo
struct FileError(Copyable, Movable, Writable)
```
A failed file operation, with the path and the underlying message.
Fields:
- `path`: String
- `message`: String
- **`write_to`** `write_to(self, mut writer: Some[Writer])`
### def `file_exists`
```mojo
def file_exists(path: String) -> Bool
```
True when something exists at `path` (a file or a directory). Never raises.
### def `read_bytes`
```mojo
def read_bytes(path: String) raises FileError -> List[UInt8]
```
Reads a whole file into memory.
### def `read_text`
```mojo
def read_text(path: String) raises FileError -> String
```
Reads a whole UTF-8 text file.
### def `write_bytes`
```mojo
def write_bytes(path: String, data: Span[UInt8, _]) raises FileError
```
Writes (truncating) `data` to `path`.
### def `write_text`
```mojo
def write_text(path: String, text: String) raises FileError
```
Writes (truncating) `text` to `path`. Raises `FileError` carrying the path on failure.
## `cinder.core.handles`
Source: `cinder/core/handles.mojo`
Generational handles and the slot pool that issues them. A `Handle[Tag]` names a slot in a `Pool[T, Tag]`. The generation counter makes a stale handle (one whose slot was freed and reused) fail validation instead of aliasing a new object. `Tag` is a phantom type so that, for example, a texture handle cannot be passed where a buffer handle is expected.
### struct `Handle`
```mojo
struct Handle[Tag: AnyType]( Boolable, Equatable, Hashable, ImplicitlyCopyable, RegisterPassable, Writable, )
```
A 64-bit generational handle: 32-bit slot index, 32-bit generation. Zero is the null handle.
Fields:
- `index`: UInt32
- `generation`: UInt32
- **`null`** `null() -> Handle[Self.Tag]`
The null handle (index 0, generation 0); never valid in any `Pool`.
- **`is_null`** `is_null(self) -> Bool`
True when the generation is 0, whatever the index; pools never issue generation 0.
- **`packed`** `packed(self) -> UInt64`
The handle as one integer, for serialization and GPU-side ids.
- **`from_packed`** `from_packed(v: UInt64) -> Handle[Self.Tag]`
Inverse of `packed()`: low 32 bits are the index, high 32 bits the generation.
- **`write_to`** `write_to(self, mut writer: Some[Writer])`
### struct `Pool`
```mojo
struct Pool[T: Copyable & Deinitable, Tag: AnyType](Movable)
```
A slot map: O(1) insert, remove and lookup by `Handle[Self.Tag]`, with generation checking. Slots are never reallocated once created, so a `Pool` holds at most `capacity` live values; `insert` raises when full, which keeps steady-state frames free of allocation (section 4 of the brief). Freed slots are recycled through an intrusive free list. Generations start at 1 and skip 0 so the zero handle is never valid.
Fields:
- **`__init__`** `__init__(out self, capacity: Int) raises`
Creates a pool with room for `capacity` values (1 <= capacity < 2^32).
- **`capacity`** `capacity(self) -> Int`
Number of slots, fixed at construction; the pool never grows past it.
- **`live`** `live(self) -> Int`
Number of values currently stored.
- **`insert`** `insert(mut self, var value: Self.T) raises -> Handle[Self.Tag]`
Stores `value` and returns its handle. Raises when the pool is full.
- **`is_valid`** `is_valid(self, h: Handle[Self.Tag]) -> Bool`
True when `h` names a live value (right slot, current generation).
- **`get`** `get(self, h: Handle[Self.Tag]) raises -> Self.T`
A copy of the value behind `h`; raises on a null, stale or foreign handle. Pooled values are small records (handles, descriptors), so copying is the intended access pattern; `update` writes a value back.
- **`update`** `update(mut self, h: Handle[Self.Tag], var value: Self.T) raises`
Replaces the value behind `h`; raises on an invalid handle.
- **`remove`** `remove(mut self, h: Handle[Self.Tag]) raises -> Self.T`
Removes and returns the value behind `h`, retiring the handle's generation.
- **`handles`** `handles(self) -> List[Handle[Self.Tag]]`
Handles of every live value, in slot order (allocates; for tools and tests).
## `cinder.core.hash`
Source: `cinder/core/hash.mojo`
Content hashing: 64-bit FNV-1a over bytes and strings (stable across runs and platforms).
- `comptime FNV_OFFSET: UInt64 = 14695981039346656037` — FNV-1a 64-bit offset basis; also the hash of empty input.
- `comptime FNV_PRIME: UInt64 = 1099511628211` — FNV-1a 64-bit prime.
### def `fnv1a64`
```mojo
def fnv1a64(data: Span[UInt8, _]) -> UInt64
```
64-bit FNV-1a of `data`. Stable across runs and platforms, which is why the asset cooker uses it as a content hash. Not cryptographic: do not use it where collisions could be engineered.
### def `fnv1a64_string`
```mojo
def fnv1a64_string(text: String) -> UInt64
```
`fnv1a64` of the string's UTF-8 bytes, without a terminator.
### def `hex64`
```mojo
def hex64(v: UInt64) -> String
```
Sixteen lowercase hex digits.
## `cinder.core.image`
Source: `cinder/core/image.mojo`
CPU images: RGBA8 load/decode/save through the shim (stb) and perceptual comparison (SSIM). `Image` owns its pixels in a `List[UInt8]` (tightly packed RGBA8). `ssim` is the structural similarity index on luma with an 8x8 box window; golden-image tests compare against it.
### struct `Image`
```mojo
struct Image(Copyable, Movable)
```
A tightly packed RGBA8 image.
Fields:
- `width`: Int
- `height`: Int
- `pixels`: List[UInt8]
- **`__init__`** `__init__(out self, width: Int, height: Int)`
- **`__init__`** `__init__( out self, width: Int, height: Int, var pixels: List[UInt8] ) raises`
- **`from_rows`** `from_rows( width: Int, height: Int, rows: List[UInt8], stride: Int ) raises -> Image`
Repacks rows of `stride` bytes (GPU readback with 256-byte alignment) into a tight image.
- **`pixel`** `pixel(self, x: Int, y: Int) -> SIMD[DType.uint8, 4]`
The RGBA8 value at column `x`, row `y`; (0, 0) is the first pixel in memory. `x` must be in [0, width) and `y` in [0, height). Anything else is a bug and aborts through `cinder_check` in every build (an unchecked index would read outside the pixel list, or silently wrap into the next row).
- **`set_pixel`** `set_pixel(mut self, x: Int, y: Int, value: SIMD[DType.uint8, 4])`
Writes RGBA8 `value` at column `x`, row `y`; addressed and range-checked like `pixel`.
- **`swap_rb`** `swap_rb(mut self)`
BGRA <-> RGBA in place (swapchain readbacks are BGRA).
- **`luma`** `luma(self, x: Int, y: Int) -> Float64`
Rec. 709 luma of the pixel at (`x`, `y`) in [0, 255], from the stored (gamma-encoded) values; alpha is ignored. `compare` computes SSIM on this value.
### def `load_image`
```mojo
def load_image(lib: NativePtr, path: String) raises -> Image
```
Loads the image file at `path` through the shim's stb_image and returns it as RGBA8. Any channel count in the file is expanded to four. `lib` must point at the loaded native shim. Raises with stb's failure reason when the file cannot be read or decoded. The stb buffer is copied into the `Image` and freed before returning.
### def `decode_image`
```mojo
def decode_image(lib: NativePtr, bytes: Span[UInt8, _]) raises -> Image
```
Decodes an encoded image held in memory (stb_image) to RGBA8. Raises with stb's failure reason on undecodable data; the shim also rejects inputs larger than 2^31 - 1 bytes. The stb buffer is copied into the `Image` and freed before returning.
### def `save_png`
```mojo
def save_png(lib: NativePtr, path: String, img: Image) raises
```
Writes `img` to `path` as an RGBA8 PNG through the shim's stb_image_write. Raises when the file cannot be written; the shim also refuses an image with zero width or height.
### struct `ImageDiff`
```mojo
struct ImageDiff(ImplicitlyCopyable, Movable, Writable)
```
Result of `compare`: SSIM on luma, plus the raw pixel statistics tests may also bound.
Fields:
- `ssim`: Float64
- `max_channel_delta`: Int
- `differing_pixels`: Int — Pixels where any channel differs by more than `tolerance` passed to `compare`.
- **`write_to`** `write_to(self, mut w: Some[Writer])`
### def `compare`
```mojo
def compare( a: Image, b: Image, tolerance: Int = 2, window: Int = 8 ) raises -> ImageDiff
```
SSIM (luma, non-overlapping `window` boxes, K1 = 0.01, K2 = 0.03, L = 255) and pixel deltas.
## `cinder.core.jobs`
Source: `cinder/core/jobs.mojo`
The job system: a work-stealing thread pool with dependency counters. Design (ADR-0008, docs/THREADING.md): - One worker per logical core minus one (the main thread is the remaining core), each with a fixed-capacity Chase-Lev deque of job indices. A worker pushes and pops at the bottom of its own deque; idle workers steal from the top of others'. - Jobs are `JobFn` thin function pointers plus an untyped data pointer; the caller owns the data for the job's lifetime. Each job decrements a `Counter` when it finishes; `wait` on a counter helps run jobs until it reaches zero, so waiting never idles a core. - OS threads and semaphores come from the native shim (`cn_thread_*`, `cn_semaphore_*`); worker entry points are Mojo `abi("C")` functions (verified by spike 4). Everything else is Mojo with `Atomic` operations. - No allocation after construction: the job table, deques and counters are fixed-capacity. `submit` returns False when the system is saturated; callers then run the job inline.
- `comptime JobFn = def(Pointer[NoneType, MutUntrackedOrigin]) thin -> None` — Signature of a job: receives the untyped data pointer it was submitted with.
- `comptime INVALID_JOB: Int32 = -1` — Job index meaning "no job": `Deque.pop` and `Deque.steal` return it when nothing was taken.
- `comptime MAX_WORKERS = 64` — Largest `worker_count` that `JobSystem` accepts.
### struct `Counter`
```mojo
struct Counter(Movable)
```
Completion counter: incremented per submitted job, decremented when each finishes.
Fields:
- `remaining`: Atomic[Int32]
- **`__init__`** `__init__(out self)`
- **`load`** `load(self) -> Int32`
Jobs still outstanding on this counter (acquire load); 0 when all have finished.
### struct `Job`
```mojo
struct Job(ImplicitlyCopyable, Movable)
```
One unit of work in the job table.
Fields:
- `run`: JobFn
- `data`: Pointer[NoneType, MutUntrackedOrigin]
- `counter`: Optional[Pointer[Counter, MutUntrackedOrigin]]
- **`__init__`** `__init__( out self, run: JobFn, data: Pointer[NoneType, MutUntrackedOrigin], counter: Optional[Pointer[Counter, MutUntrackedOrigin]], )`
### struct `Deque`
```mojo
struct Deque(Movable)
```
Chase-Lev work-stealing deque of job indices with a fixed power-of-two capacity. The owner thread calls `push` and `pop`; any thread calls `steal`. Indices wrap through the mask; `push` fails when the deque is full instead of growing.
Fields:
- **`__init__`** `__init__(out self, capacity: Int)`
- **`push`** `push(mut self, job: Int32) -> Bool`
Owner only. Returns False when full.
- **`pop`** `pop(mut self) -> Int32`
Owner only. Returns INVALID_JOB when empty.
- **`steal`** `steal(mut self) -> Int32`
Any thread. Returns INVALID_JOB when empty or when it lost a race (caller retries elsewhere).
### struct `Shared`
```mojo
struct Shared(Movable)
```
State shared by all workers; lives at a stable heap address for the system's lifetime.
Fields:
- `jobs`: Allocation[Job]
- `job_capacity`: Int
- `next_job`: Atomic[Int64]
- `deques`: List[Deque]
- `worker_count`: Int
- `stop`: Atomic[Int32]
- `idle`: Atomic[Int32]
- `semaphore`: Optional[Pointer[CnSemaphore, MutUntrackedOrigin]]
- `executed`: Atomic[Int64]
- `lib`: Pointer[CinderNative, MutUntrackedOrigin]
- **`__init__`** `__init__( out self, worker_count: Int, job_capacity: Int, semaphore: Optional[Pointer[CnSemaphore, MutUntrackedOrigin]], lib: Pointer[CinderNative, MutUntrackedOrigin], )`
### struct `WorkerContext`
```mojo
struct WorkerContext(Movable)
```
Passed to each worker thread entry: which deque is mine and where the shared state lives.
Fields:
- `shared`: Pointer[Shared, MutUntrackedOrigin]
- `index`: Int
- **`__init__`** `__init__( out self, shared: Pointer[Shared, MutUntrackedOrigin], index: Int )`
### struct `JobSystem`
```mojo
struct JobSystem(Movable)
```
Owns the worker threads, the shared state and the job table. Deque 0 belongs to the main thread. Hold one instance for the life of the program (section 4: the job system runs everything that is not SDL or presentation). `__deinit__` stops and joins every worker.
Fields:
- **`__init__`** `__init__( out self, lib: Pointer[CinderNative, MutUntrackedOrigin], worker_count: Int, job_capacity: Int = 1 << 14, ) raises`
Starts `worker_count` workers (0 runs everything on the calling thread). `job_capacity` must be a power of two; it bounds the number of jobs in flight between `reset` calls.
- **`worker_count`** `worker_count(self) -> Int`
Number of worker threads started, not counting the main thread (which owns deque 0).
- **`executed`** `executed(self) -> Int`
Jobs completed since construction (diagnostics).
- **`submit`** `submit( mut self, run: JobFn, data: Pointer[NoneType, MutUntrackedOrigin], counter: Optional[Pointer[Counter, MutUntrackedOrigin]], ) -> Bool`
Queues a job from the main thread. Increments `counter` before the job can run. Returns False when the job table or the main deque is full; the caller must then run it inline.
- **`wait`** `wait(mut self, counter: Pointer[Counter, MutUntrackedOrigin])`
Runs jobs on the calling thread until `counter` reaches zero.
- **`reset`** `reset(mut self)`
Recycles the job table. Only valid when no job is queued or running (e.g. at frame start).
## `cinder.core.log`
Source: `cinder/core/log.mojo`
Structured, levelled logging. A `Logger` owns a minimum level and a sink. Records carry a level, a category and a message plus optional key=value fields, and are formatted as one line: `[LEVEL] category: message key=value ...`. Sinks are a trait so tests capture records in memory and the engine routes them to stderr, a file or the debug console. Logging never raises; a failing sink drops the record.
### struct `LogLevel`
```mojo
struct LogLevel( Comparable, Equatable, ImplicitlyCopyable, RegisterPassable, Writable )
```
Severity, ordered. Compare with the usual operators.
Fields:
- `value`: UInt8
- **`trace`** `trace() -> LogLevel`
Severity 0, the lowest: a logger at this level passes everything.
- **`debug`** `debug() -> LogLevel`
Severity 1, between `trace` and `info`.
- **`info`** `info() -> LogLevel`
Severity 2; the default `Logger` minimum level.
- **`warn`** `warn() -> LogLevel`
Severity 3, between `info` and `error`.
- **`error`** `error() -> LogLevel`
Severity 4, the highest named level.
- **`name`** `name(self) -> StaticString`
Label printed in records: TRACE, DEBUG, INFO, WARN, or ERROR for 4 and above.
- **`write_to`** `write_to(self, mut writer: Some[Writer])`
### struct `LogField`
```mojo
struct LogField(Copyable, Movable, Writable)
```
One key=value pair attached to a record.
Fields:
- `key`: String
- `value`: String
- **`write_to`** `write_to(self, mut writer: Some[Writer])`
### struct `LogRecord`
```mojo
struct LogRecord(Copyable, Movable, Writable)
```
A formatted log entry.
Fields:
- `level`: LogLevel
- `category`: String
- `message`: String
- `fields`: List[LogField]
- **`__init__`** `__init__( out self, level: LogLevel, category: String, message: String, var fields: List[LogField], )`
- **`write_to`** `write_to(self, mut writer: Some[Writer])`
### trait `LogSink`
```mojo
trait LogSink
```
Receives formatted records. Implementations must not raise.
- **`emit`** `emit(mut self, record: LogRecord)`
Handles one record that passed the filter. Must not raise: drop the record instead.
### struct `PrintSink`
```mojo
struct PrintSink(LogSink, Movable)
```
Writes each record as one line to standard output.
Fields:
- `emitted`: Int — Number of records written so far.
- **`__init__`** `__init__(out self)`
- **`emit`** `emit(mut self, record: LogRecord)`
Prints the record as one line to standard output and increments `emitted`.
### struct `MemorySink`
```mojo
struct MemorySink(LogSink, Movable)
```
Keeps every record; used by tests and the in-engine console.
Fields:
- `records`: List[LogRecord]
- **`__init__`** `__init__(out self)`
- **`emit`** `emit(mut self, record: LogRecord)`
Appends a copy of the record to `records`, which grows without bound.
### struct `Logger`
```mojo
struct Logger[S: LogSink & Movable & Deinitable](Movable)
```
Filters by minimum level and forwards to its sink.
Fields:
- `min_level`: LogLevel
- `sink`: Self.S
- **`__init__`** `__init__( out self, var sink: Self.S, min_level: LogLevel = LogLevel.info() )`
- **`enabled`** `enabled(self, level: LogLevel) -> Bool`
True when `level` >= `min_level`; check it before building costly messages.
- **`log`** `log( mut self, level: LogLevel, category: String, message: String, var fields: List[LogField], )`
Emits a record when `level` passes the filter. Field lists are consumed.
- **`log`** `log(mut self, level: LogLevel, category: String, message: String)`
Emits a record without fields when `level` passes the filter.
- **`trace`** `trace(mut self, category: String, message: String)`
Logs `message` under `category` at TRACE, subject to `min_level`.
- **`debug`** `debug(mut self, category: String, message: String)`
Logs `message` under `category` at DEBUG, subject to `min_level`.
- **`info`** `info(mut self, category: String, message: String)`
Logs `message` under `category` at INFO, subject to `min_level`.
- **`warn`** `warn(mut self, category: String, message: String)`
Logs `message` under `category` at WARN, subject to `min_level`.
- **`error`** `error(mut self, category: String, message: String)`
Logs `message` under `category` at ERROR, subject to `min_level`.
## `cinder.core.math`
Source: `cinder/core/math/__init__.mojo`
SIMD math for the engine. Conventions (docs/ARCHITECTURE.md, fixed): right-handed, Y up, -Z forward, metres, column-major matrices, reverse-Z depth with an infinite far plane. All types are plain 32-bit float values backed by `SIMD[DType.float32, 4]`; a `Vec3` keeps its fourth lane at zero.
## `cinder.core.math.mat4`
Source: `cinder/core/math/mat4.mojo`
Mat4: a column-major 4x4 float matrix, as WGSL and glTF store it.
### struct `Mat4`
```mojo
struct Mat4(Equatable, ImplicitlyCopyable, RegisterPassable, Writable)
```
Column-major 4x4 matrix. `c0..c3` are the columns; element (row r, column c) is `cC[r]`.
Fields:
- `c0`: F4
- `c1`: F4
- `c2`: F4
- `c3`: F4
- **`identity`** `identity() -> Mat4`
The identity matrix.
- **`zero`** `zero() -> Mat4`
All sixteen elements zero; also what `inverse` returns for a singular matrix.
- **`from_rows`** `from_rows(r0: Vec4, r1: Vec4, r2: Vec4, r3: Vec4) -> Mat4`
Builds a matrix from its rows (handy for literals written the way maths books print them).
- **`translation`** `translation(t: Vec3) -> Mat4`
Translation by `t` (metres): the identity with `t` in the last column.
- **`scaling`** `scaling(s: Vec3) -> Mat4`
Scale by `s.x()`, `s.y()`, `s.z()` along the x, y and z axes.
- **`rotation`** `rotation(q: Quat) -> Mat4`
Rotation matrix of a unit quaternion.
- **`trs`** `trs(t: Vec3, r: Quat, s: Vec3) -> Mat4`
Translation * Rotation * Scale, the glTF node transform order.
- **`look_at`** `look_at(eye: Vec3, target: Vec3, up: Vec3) -> Mat4`
Right-handed view matrix: the camera looks down -Z, with +Y up and +X right.
- **`perspective_reverse_z_infinite`** `perspective_reverse_z_infinite( fov_y: Float32, aspect: Float32, near: Float32 ) -> Mat4`
Right-handed perspective projection, reverse-Z (near maps to 1) with an infinite far plane, clip depth in [0, 1] as wgpu expects.
- **`orthographic_reverse_z`** `orthographic_reverse_z( left: Float32, right: Float32, bottom: Float32, top: Float32, near: Float32, far: Float32, ) -> Mat4`
Right-handed orthographic projection with reverse-Z (near maps to 1, far to 0).
- **`column`** `column(self, c: Int) -> Vec4`
Column `c` (0 to 3) as a Vec4. Any other index is a bug: `cinder_assert` aborts. With assertions compiled out (`-D CINDER_ASSERTS=0`) the index wraps to `c & 3`.
- **`__getitem__`** `__getitem__(self, row: Int, col: Int) -> Float32`
Element (`row`, `col`); both indices are checked like `row` and `column`.
- **`row`** `row(self, r: Int) -> Vec4`
Row `r` (0 to 3), gathered from the four columns. Any other index is a bug: `cinder_assert` aborts. With assertions compiled out the index wraps to `r & 3`.
- **`to_quat`** `to_quat(self) -> Quat`
Rotation of an orthonormal (possibly scaled) matrix's upper 3x3 (Shepperd's method, choosing the largest diagonal term for stability). Scale is removed first.
- **`transform_point`** `transform_point(self, p: Vec3) -> Vec3`
Applies the affine part to a point (w = 1), ignoring any projective row.
- **`transform_vector`** `transform_vector(self, v: Vec3) -> Vec3`
Applies the linear part to a direction (w = 0).
- **`project_point`** `project_point(self, p: Vec3) -> Vec3`
Full projective transform with perspective divide.
- **`transpose`** `transpose(self) -> Mat4`
Rows and columns swapped; for a pure rotation this is also its inverse.
- **`determinant`** `determinant(self) -> Float32`
Determinant of the full 4x4 matrix, by expansion in 2x2 minors. Exactly 0 is what makes `inverse` return the zero matrix; a negative value means the matrix mirrors space (`Transform.from_mat4` relies on this).
- **`inverse`** `inverse(self) -> Mat4`
General inverse by cofactors. A singular matrix returns the zero matrix; callers that need to detect that check `determinant()` first.
- **`approx_equal`** `approx_equal(self, o: Mat4, eps: Float32 = EPSILON) -> Bool`
True when every element matches within `eps` under `approx_eq`.
- **`write_to`** `write_to(self, mut writer: Some[Writer])`
## `cinder.core.math.quat`
Source: `cinder/core/math/quat.mojo`
Quat: a unit quaternion (x, y, z, w) representing a rotation.
### struct `Quat`
```mojo
struct Quat(Equatable, ImplicitlyCopyable, RegisterPassable, Writable)
```
Rotation quaternion. Lanes are (x, y, z, w); `w` is the scalar part.
Fields:
- `data`: F4
- **`__init__`** `__init__(out self, x: Float32, y: Float32, z: Float32, w: Float32)`
- **`identity`** `identity() -> Quat`
The no-rotation quaternion (0, 0, 0, 1).
- **`from_axis_angle`** `from_axis_angle(axis: Vec3, angle: Float32) -> Quat`
Rotation of `angle` radians about the unit vector `axis` (right-handed).
- **`from_euler`** `from_euler(yaw: Float32, pitch: Float32, roll: Float32) -> Quat`
Yaw about +Y, then pitch about +X, then roll about +Z (applied in that order to a body).
- **`x`** `x(self) -> Float32`
Lane 0, the x term of the vector part.
- **`y`** `y(self) -> Float32`
Lane 1, the y term of the vector part.
- **`z`** `z(self) -> Float32`
Lane 2, the z term of the vector part.
- **`w`** `w(self) -> Float32`
Lane 3, the scalar part: cos(angle / 2) for a unit quaternion.
- **`vector_part`** `vector_part(self) -> Vec3`
(x, y, z) as a Vec3: the rotation axis times sin(angle / 2) for a unit quaternion.
- **`dot`** `dot(self, o: Quat) -> Float32`
4D dot product; negative when the two are on opposite hemispheres (see `slerp`).
- **`length`** `length(self) -> Float32`
Euclidean norm over all four lanes; 1 for a unit quaternion.
- **`normalized`** `normalized(self) -> Quat`
Unit quaternion; the zero quaternion becomes the identity.
- **`conjugate`** `conjugate(self) -> Quat`
The vector part negated; for a unit quaternion this is the inverse rotation.
- **`inverse`** `inverse(self) -> Quat`
Inverse for any non-zero quaternion (equals the conjugate for unit quaternions). The zero quaternion has no inverse and returns the zero quaternion, as `Mat4.inverse` returns the zero matrix for a singular matrix.
- **`rotate`** `rotate(self, v: Vec3) -> Vec3`
Rotates `v` by this unit quaternion: v' = v + 2w(u x v) + 2u x (u x v).
- **`nlerp`** `nlerp(self, o: Quat, t: Float32) -> Quat`
Normalised linear interpolation along the shorter arc.
- **`slerp`** `slerp(self, o: Quat, t: Float32) -> Quat`
Spherical interpolation along the shorter arc; falls back to nlerp when nearly parallel.
- **`approx_equal`** `approx_equal(self, o: Quat, eps: Float32 = EPSILON) -> Bool`
True when both represent the same rotation (q and -q are equal rotations).
- **`write_to`** `write_to(self, mut writer: Some[Writer])`
## `cinder.core.math.scalar`
Source: `cinder/core/math/scalar.mojo`
Scalar helpers shared by the vector, quaternion and matrix types.
- `comptime EPSILON: Float32 = 1e-6` — Default tolerance for approximate comparisons of unit-scale quantities.
### def `radians`
```mojo
def radians(deg: Float32) -> Float32
```
Converts degrees to radians.
### def `degrees`
```mojo
def degrees(rad: Float32) -> Float32
```
Converts radians to degrees.
### def `clamp`
```mojo
def clamp(x: Float32, lo: Float32, hi: Float32) -> Float32
```
Clamps `x` to [lo, hi].
### def `saturate`
```mojo
def saturate(x: Float32) -> Float32
```
Clamps `x` to [0, 1].
### def `lerp`
```mojo
def lerp(a: Float32, b: Float32, t: Float32) -> Float32
```
Linear interpolation; exact at t = 0 and t = 1.
### def `approx_eq`
```mojo
def approx_eq(a: Float32, b: Float32, eps: Float32 = EPSILON) -> Bool
```
True when |a - b| <= eps * max(1, |a|, |b|): absolute near zero, relative elsewhere.
## `cinder.core.math.transform`
Source: `cinder/core/math/transform.mojo`
Transform: translation, rotation and scale as separate terms (poses, animation keys).
### struct `Transform`
```mojo
struct Transform(Equatable, ImplicitlyCopyable, Movable, Writable)
```
A TRS transform. Composition order is scale, then rotation, then translation.
Fields:
- `translation`: Vec3
- `rotation`: Quat
- `scale`: Vec3
- **`identity`** `identity() -> Transform`
Zero translation, identity rotation and unit scale.
- **`from_mat4`** `from_mat4(m: Mat4) -> Transform`
Decomposes a matrix without shear: translation from the last column, scale from the column lengths, rotation from the normalised columns. A mirrored matrix (negative determinant) has no proper rotation; the reflection is folded into a negative x scale and the rotation is taken from the matrix with its first column negated, so `to_mat4()` rebuilds the input.
- **`to_mat4`** `to_mat4(self) -> Mat4`
The equivalent matrix, `Mat4.trs`: scale, then rotation, then translation.
- **`lerp`** `lerp(self, o: Transform, t: Float32) -> Transform`
Linear translation and scale, normalised-lerp rotation along the shorter arc.
- **`approx_equal`** `approx_equal(self, o: Transform, eps: Float32 = EPSILON) -> Bool`
True when translation, rotation and scale match within `eps` (q and -q are equal).
- **`write_to`** `write_to(self, mut writer: Some[Writer])`
## `cinder.core.math.vec`
Source: `cinder/core/math/vec.mojo`
Vec2, Vec3 and Vec4: 32-bit float vectors backed by SIMD registers.
- `comptime F2 = SIMD[DType.float32, 2]` — Storage of `Vec2`: two float32 lanes.
- `comptime F4 = SIMD[DType.float32, 4]` — Storage of `Vec3`, `Vec4`, `Quat` and each `Mat4` column: four float32 lanes.
### struct `Vec2`
```mojo
struct Vec2(Equatable, ImplicitlyCopyable, RegisterPassable, Writable)
```
A 2D vector.
Fields:
- `data`: F2
- **`__init__`** `__init__(out self, x: Float32, y: Float32)`
- **`zero`** `zero() -> Vec2`
(0, 0).
- **`x`** `x(self) -> Float32`
Lane 0.
- **`y`** `y(self) -> Float32`
Lane 1.
- **`dot`** `dot(self, o: Vec2) -> Float32`
Sum of the component-wise products.
- **`length_squared`** `length_squared(self) -> Float32`
Squared Euclidean length; avoids the square root when only comparing lengths.
- **`length`** `length(self) -> Float32`
Euclidean length.
- **`approx_equal`** `approx_equal(self, o: Vec2, eps: Float32 = EPSILON) -> Bool`
True when x and y each match within `eps` under `approx_eq`.
- **`write_to`** `write_to(self, mut writer: Some[Writer])`
### struct `Vec3`
```mojo
struct Vec3(Equatable, ImplicitlyCopyable, RegisterPassable, Writable)
```
A 3D vector. The fourth SIMD lane is always zero so 4-wide operations stay exact.
Fields:
- `data`: F4
- **`__init__`** `__init__(out self, x: Float32, y: Float32, z: Float32)`
- **`zero`** `zero() -> Vec3`
(0, 0, 0).
- **`one`** `one() -> Vec3`
(1, 1, 1), the unit scale.
- **`unit_x`** `unit_x() -> Vec3`
+X, the engine's right axis.
- **`unit_y`** `unit_y() -> Vec3`
+Y, the engine's up axis (right-handed, Y-up).
- **`unit_z`** `unit_z() -> Vec3`
+Z, pointing backwards: the engine's forward is -Z.
- **`forward`** `forward() -> Vec3`
The engine's forward direction, -Z.
- **`splat`** `splat(v: Float32) -> Vec3`
All three components set to `v` (the fourth lane stays zero).
- **`x`** `x(self) -> Float32`
Lane 0.
- **`y`** `y(self) -> Float32`
Lane 1.
- **`z`** `z(self) -> Float32`
Lane 2.
- **`__getitem__`** `__getitem__(self, i: Int) -> Float32`
- **`with_x`** `with_x(self, v: Float32) -> Vec3`
A copy with x replaced by `v`.
- **`with_y`** `with_y(self, v: Float32) -> Vec3`
A copy with y replaced by `v`.
- **`with_z`** `with_z(self, v: Float32) -> Vec3`
A copy with z replaced by `v`.
- **`dot`** `dot(self, o: Vec3) -> Float32`
Sum of the component-wise products of x, y and z.
- **`cross`** `cross(self, o: Vec3) -> Vec3`
Right-handed cross product `self x o` (unit_x cross unit_y is unit_z).
- **`length_squared`** `length_squared(self) -> Float32`
Squared Euclidean length; avoids the square root when only comparing lengths.
- **`length`** `length(self) -> Float32`
Euclidean length.
- **`distance`** `distance(self, o: Vec3) -> Float32`
Euclidean distance to `o`.
- **`normalized`** `normalized(self) -> Vec3`
Unit vector in the same direction; zero stays zero.
- **`lerp`** `lerp(self, o: Vec3, t: Float32) -> Vec3`
`self + (o - self) * t` per component; `t` is unclamped, so it extrapolates.
- **`min`** `min(self, o: Vec3) -> Vec3`
Component-wise minimum.
- **`max`** `max(self, o: Vec3) -> Vec3`
Component-wise maximum.
- **`abs`** `abs(self) -> Vec3`
Component-wise absolute value.
- **`max_component`** `max_component(self) -> Float32`
Largest of x, y and z by signed value (not by magnitude; use `abs()` first for that).
- **`approx_equal`** `approx_equal(self, o: Vec3, eps: Float32 = EPSILON) -> Bool`
True when x, y and z each match within `eps` under `approx_eq`.
- **`to_vec4`** `to_vec4(self, w: Float32) -> Vec4`
(x, y, z, `w`): pass 1 for a point and 0 for a direction.
- **`write_to`** `write_to(self, mut writer: Some[Writer])`
### struct `Vec4`
```mojo
struct Vec4(Equatable, ImplicitlyCopyable, RegisterPassable, Writable)
```
A 4D vector, also used for homogeneous points and colours.
Fields:
- `data`: F4
- **`__init__`** `__init__(out self, x: Float32, y: Float32, z: Float32, w: Float32)`
- **`zero`** `zero() -> Vec4`
(0, 0, 0, 0).
- **`x`** `x(self) -> Float32`
Lane 0.
- **`y`** `y(self) -> Float32`
Lane 1.
- **`z`** `z(self) -> Float32`
Lane 2.
- **`w`** `w(self) -> Float32`
Lane 3: 1 for a point, 0 for a direction, alpha for a colour.
- **`__getitem__`** `__getitem__(self, i: Int) -> Float32`
- **`xyz`** `xyz(self) -> Vec3`
(x, y, z) as a Vec3, dropping w without a perspective divide.
- **`dot`** `dot(self, o: Vec4) -> Float32`
Sum of the component-wise products over all four lanes.
- **`length`** `length(self) -> Float32`
Euclidean length over all four lanes.
- **`approx_equal`** `approx_equal(self, o: Vec4, eps: Float32 = EPSILON) -> Bool`
True when all four components match within `eps` under `approx_eq`.
- **`write_to`** `write_to(self, mut writer: Some[Writer])`
## `cinder.core.profile`
Source: `cinder/core/profile.mojo`
Tracy profiling for the engine: zones, frame marks, plots and messages. Profiling is off until `Profiler.start()` (nothing listens on the network before that) and every call is a cheap no-op while it is off. `Zone` opens on construction and closes when it is destroyed. Mojo destroys a value right after its last use, so end the zone explicitly where it should close: var z = profiler.zone("render") ... # the instrumented work _ = z^ String lifetimes: Tracy copies zone names, zone text and messages during the call, but keys plots by the name *pointer* and reads it for the rest of the program. `Profiler.plot` therefore interns each plot name once in a table that is never freed (one small allocation per distinct name, the first time it is plotted while profiling runs).
- `comptime COLOR_RENDER: UInt32 = 0x3F7FBF` — Zone colour (0xRRGGBB) for rendering.
- `comptime COLOR_PHYSICS: UInt32 = 0xBF7F3F` — Zone colour (0xRRGGBB) for physics and fixed steps.
- `comptime COLOR_ANIM: UInt32 = 0x7FBF3F` — Zone colour (0xRRGGBB) for animation.
- `comptime COLOR_AUDIO: UInt32 = 0xBF3F7F` — Zone colour (0xRRGGBB) for audio.
- `comptime COLOR_ASSETS: UInt32 = 0x7F7F7F` — Zone colour (0xRRGGBB) for asset loading.
### struct `Profiler`
```mojo
struct Profiler(ImplicitlyCopyable, Movable)
```
A handle to the shim's Tracy client; copies share the process-wide profiler and its plot name table.
Fields:
- `lib`: NativePtr
- **`__init__`** `__init__(out self, lib: NativePtr)`
- **`available`** `available(self) -> Bool`
Whether the shim was built with Tracy.
- **`start`** `start(self) -> Bool`
Starts the Tracy client (idempotent). Returns False when Tracy is not built in.
- **`stop`** `stop(self)`
Shuts the Tracy client down if it is running; a no-op otherwise or without Tracy.
- **`is_started`** `is_started(self) -> Bool`
Whether the Tracy client is running (`start` without `stop`).
- **`is_connected`** `is_connected(self) -> Bool`
Whether a Tracy viewer is connected; always False while stopped or without Tracy.
- **`zone`** `zone(self, name: String, color: UInt32 = 0) -> Zone`
Opens a zone named `name` that ends when the returned `Zone` is destroyed. `color` is 0xRRGGBB (0 keeps Tracy's default; see the `COLOR_*` constants). Mojo destroys a value after its last use, so end the zone explicitly with `_ = z^` where it should close. While profiling is off the zone is inert and its `id` is 0.
- **`frame_mark`** `frame_mark(self)`
Marks the end of a frame.
- **`plot`** `plot(self, name: String, value: Float64)`
Adds `value` to the Tracy plot called `name`. Does nothing while profiling is off. Tracy keeps the name pointer for the program's life, so the name is interned (copied once into a never-freed table); later calls with the same name reuse that copy and do not allocate.
- **`plot_name_count`** `plot_name_count(self) -> Int`
How many distinct plot names have been interned (shared by every copy).
- **`message`** `message(self, text: String)`
Sends `text` to Tracy's message log (info severity); a no-op while profiling is off.
### struct `Zone`
```mojo
struct Zone(Movable)
```
An open profiling zone; ends when destroyed. `id` is 0 while profiling is off.
Fields:
- `lib`: NativePtr
- `id`: UInt64
- **`__init__`** `__init__(out self, lib: NativePtr, name: String, color: UInt32)`
- **`text`** `text(self, note: String)`
Attaches text to the open zone.
## `cinder.core.ring`
Source: `cinder/core/ring.mojo`
RingBuffer: a fixed-capacity FIFO queue with no allocation after construction.
### struct `RingBuffer`
```mojo
struct RingBuffer[T: Copyable & Deinitable](Movable, Sized)
```
Single-threaded bounded FIFO. `push` returns False when full; `pop` returns None when empty. Capacity must be a power of two so index wrapping is a mask. Cross-thread queues live in the native shim (CnEventQueue); this type is for in-engine command and event buffering.
Fields:
- **`__init__`** `__init__(out self, capacity: Int) raises`
- **`capacity`** `capacity(self) -> Int`
Number of slots, a power of two fixed at construction.
- **`__len__`** `__len__(self) -> Int`
- **`is_empty`** `is_empty(self) -> Bool`
True when nothing is queued (`pop` would return None).
- **`is_full`** `is_full(self) -> Bool`
True when every slot is taken (`push` would return False).
- **`push`** `push(mut self, var value: Self.T) -> Bool`
Appends `value`; returns False (and drops nothing already queued) when full.
- **`pop`** `pop(mut self) -> Optional[Self.T]`
Removes and returns the oldest value, or None when empty.
- **`peek`** `peek(self) raises -> Self.T`
A copy of the oldest value; raises when empty.
- **`clear`** `clear(mut self)`
Pops and destroys every queued value, oldest first.
## `cinder.core.time`
Source: `cinder/core/time.mojo`
Time: a monotonic clock and the fixed-timestep accumulator that drives the simulation. The frame loop (section 4 of the brief) simulates at a fixed 60 Hz and renders at whatever rate the display allows, interpolating between the last two simulation states with `alpha`.
- `comptime NS_PER_SECOND: Int = 1_000_000_000` — Nanoseconds per second.
### struct `Clock`
```mojo
struct Clock(Movable)
```
Monotonic wall clock in nanoseconds, with per-frame deltas.
Fields:
- **`__init__`** `__init__(out self)`
- **`now_ns`** `now_ns(self) -> Int`
Current `perf_counter_ns()` reading. Monotonic, but its origin is arbitrary (not the clock's creation), so only differences between readings are meaningful.
- **`elapsed_seconds`** `elapsed_seconds(self) -> Float64`
Seconds since the clock was created.
- **`tick`** `tick(mut self) -> Float64`
Seconds since the previous `tick` (or creation). Never negative.
### struct `FixedStep`
```mojo
struct FixedStep(ImplicitlyCopyable, Movable)
```
Fixed-timestep accumulator. Feed real frame times with `advance`; it returns how many simulation steps of `step_seconds` to run (capped at `max_steps_per_frame` to avoid the spiral of death) and leaves `alpha()` as the interpolation factor for rendering between the previous and current simulation state.
Fields:
- `step_seconds`: Float64
- `max_steps_per_frame`: Int
- **`__init__`** `__init__(out self, hz: Float64 = 60.0, max_steps_per_frame: Int = 8)`
- **`advance`** `advance(mut self, frame_seconds: Float64) -> Int`
Accounts for `frame_seconds` of real time and returns the number of steps to simulate.
- **`alpha`** `alpha(self) -> Float64`
Fraction of a step accumulated but not yet simulated, in [0, 1).
- **`total_steps`** `total_steps(self) -> Int`
Sum of every step count `advance` has returned.
---
> 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).
---
> Cinder 0.1.0 documentation: cinder.gfx API
> Canonical page: https://cinder.ash10.dev/docs/api/gfx/ · Source: `docs/api/gfx.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.gfx`
## `cinder.gfx`
Source: `cinder/gfx/__init__.mojo`
Safe, typed layer over wgpu-native. `Gfx` owns the instance, adapter, device and queue and drains the shim's event queue into diagnostics (validation errors, device loss, wgpu log). Resource modules build on it with one distinct handle type per resource kind and ownership through `__deinit__`.
## `cinder.gfx.commands`
Source: `cinder/gfx/commands.mojo`
Command recording: encoders, render and compute passes, copies, submission and readback.
- `comptime NoChain = Optional[Pointer[WGPUChainedStruct, MutUntrackedOrigin]]` — Null `nextInChain` for wgpu descriptors that carry no chained extension struct.
### struct `ColorAttachment`
```mojo
struct ColorAttachment(Copyable, Movable)
```
A colour target of a render pass. `view` may be a swapchain view obtained outside Resources; `resolve` is the single-sample target when `view` is multisampled (null otherwise).
Fields:
- `view`: WGPUTextureView
- `clear`: Bool
- `clear_color`: WGPUColor
- `store`: Bool
- `resolve`: WGPUTextureView
- **`__init__`** `__init__( out self, view: WGPUTextureView, clear: Bool, var clear_color: WGPUColor, store: Bool, resolve: WGPUTextureView = WGPUTextureView(), )`
- **`cleared`** `cleared( view: WGPUTextureView, r: Float64, g: Float64, b: Float64, a: Float64 ) -> ColorAttachment`
Clears `view` to (r, g, b, a) at load and stores the result; no resolve target.
- **`cleared_resolving`** `cleared_resolving( view: WGPUTextureView, resolve: WGPUTextureView, r: Float64, g: Float64, b: Float64, a: Float64, ) -> ColorAttachment`
Clears the multisampled `view` to (r, g, b, a), resolves into `resolve` and discards the multisampled contents (store op Discard).
- **`loaded`** `loaded(view: WGPUTextureView) -> ColorAttachment`
Keeps the existing contents of `view` (load op Load) and stores the result.
- `comptime MAX_COLOR_ATTACHMENTS = 4` — Colour attachments per render pass (wgpu allows 8; the engine uses at most 1 + resolve).
### struct `DepthAttachment`
```mojo
struct DepthAttachment(ImplicitlyCopyable, Movable)
```
Depth target. Reverse-Z clears to 0.0 (far).
Fields:
- `view`: WGPUTextureView
- `clear`: Bool
- `clear_depth`: Float32
- `store`: Bool
- **`none`** `none() -> DepthAttachment`
No depth target: `begin_render_pass` then passes no depth-stencil attachment.
- **`cleared_reverse_z`** `cleared_reverse_z(view: WGPUTextureView) -> DepthAttachment`
Clears `view` to 0.0 (the far plane under reverse-Z) and stores the result.
### struct `RenderPass`
```mojo
struct RenderPass(Movable)
```
An open render pass. `end` must be called exactly once.
Fields:
- `gfx`: Pointer[Gfx, MutUntrackedOrigin]
- `res`: Pointer[Resources, MutUntrackedOrigin]
- `raw`: WGPURenderPassEncoder
- `ended`: Bool
- **`__init__`** `__init__( out self, gfx: Pointer[Gfx, MutUntrackedOrigin], res: Pointer[Resources, MutUntrackedOrigin], raw: WGPURenderPassEncoder, )`
- **`set_pipeline`** `set_pipeline(self, h: RenderPipelineHandle) raises`
Binds the render pipeline; raises if the handle is not live in `Resources`.
- **`set_bind_group`** `set_bind_group(self, index: UInt32, h: BindGroupHandle) raises`
Binds a bind group at group `index` with no dynamic offsets; raises on an invalid handle.
- **`set_bind_group_dynamic`** `set_bind_group_dynamic( self, index: UInt32, h: BindGroupHandle, mut offsets: List[UInt32] ) raises`
Binds a bind group at group `index` with one dynamic offset per dynamic binding. `offsets` is read during the call only. Raises on an invalid handle; a wrong offset count or alignment surfaces as a validation diagnostic through `Gfx.pump`.
- **`set_vertex_buffer`** `set_vertex_buffer( self, slot: UInt32, h: BufferHandle, offset: UInt64 = 0 ) raises`
Binds `h` to vertex slot `slot` from `offset` to the end of the buffer. Raises on an invalid handle or when `offset` is not below the buffer size (nothing is bound then).
- **`set_index_buffer`** `set_index_buffer( self, h: BufferHandle, format: WGPUIndexFormat, offset: UInt64 = 0 ) raises`
Binds `h` as the index buffer with `format` from `offset` to the end of the buffer. Raises on an invalid handle or when `offset` is not below the buffer size (nothing is bound then).
- **`set_viewport`** `set_viewport( self, x: Float32, y: Float32, width: Float32, height: Float32 )`
Sets the viewport rectangle in pixels with depth range 0..1.
- **`set_scissor`** `set_scissor(self, x: UInt32, y: UInt32, width: UInt32, height: UInt32)`
Sets the scissor rectangle in pixels.
- **`draw`** `draw( self, vertex_count: UInt32, instance_count: UInt32 = 1, first_vertex: UInt32 = 0, first_instance: UInt32 = 0, )`
Draws non-indexed primitives with the bound pipeline and vertex buffers.
- **`draw_indexed`** `draw_indexed( self, index_count: UInt32, instance_count: UInt32 = 1, first_index: UInt32 = 0, base_vertex: Int32 = 0, first_instance: UInt32 = 0, )`
Draws indexed primitives from the bound index buffer; `base_vertex` is added to each index.
- **`end`** `end(mut self)`
Ends and releases the pass encoder. Later calls do nothing. A pass dropped without `end` is ended and released by its destructor, which runs right after the pass's last use, so the encoder is never left with an open pass.
### struct `ComputePass`
```mojo
struct ComputePass(Movable)
```
An open compute pass from `CommandEncoder.begin_compute_pass`. `end` must be called exactly once.
Fields:
- `gfx`: Pointer[Gfx, MutUntrackedOrigin]
- `res`: Pointer[Resources, MutUntrackedOrigin]
- `raw`: WGPUComputePassEncoder
- `ended`: Bool
- **`__init__`** `__init__( out self, gfx: Pointer[Gfx, MutUntrackedOrigin], res: Pointer[Resources, MutUntrackedOrigin], raw: WGPUComputePassEncoder, )`
- **`set_pipeline`** `set_pipeline(self, h: ComputePipelineHandle) raises`
Binds the compute pipeline; raises if the handle is not live in `Resources`.
- **`set_bind_group`** `set_bind_group(self, index: UInt32, h: BindGroupHandle) raises`
Binds a bind group at group `index` with no dynamic offsets; raises on an invalid handle.
- **`set_bind_group_dynamic`** `set_bind_group_dynamic( self, index: UInt32, h: BindGroupHandle, mut offsets: List[UInt32] ) raises`
Binds a bind group at group `index` with one dynamic offset per dynamic binding. `offsets` is read during the call only. Raises on an invalid handle; a wrong offset count or alignment surfaces as a validation diagnostic through `Gfx.pump`.
- **`dispatch`** `dispatch(self, x: UInt32, y: UInt32 = 1, z: UInt32 = 1)`
Dispatches an x by y by z grid of workgroups with the bound pipeline.
- **`end`** `end(mut self)`
Ends and releases the pass encoder. Later calls do nothing. A pass dropped without `end` is ended and released by its destructor, which runs right after the pass's last use, so the encoder is never left with an open pass.
### def `command_encoder_descriptor`
```mojo
def command_encoder_descriptor(ref label: String) -> WGPUCommandEncoderDescriptor
```
The descriptor `CommandEncoder` creates its encoder with: no chain, `label` as the label. The view borrows `label`, which must outlive the wgpu call (`keep` it).
### struct `CommandEncoder`
```mojo
struct CommandEncoder(Movable)
```
Records one command buffer. `finish_and_submit` consumes it; an encoder dropped without it is released by its destructor and its commands are discarded.
Fields:
- `gfx`: Pointer[Gfx, MutUntrackedOrigin]
- `res`: Pointer[Resources, MutUntrackedOrigin]
- `raw`: WGPUCommandEncoder
- **`__init__`** `__init__( out self, gfx: Pointer[Gfx, MutUntrackedOrigin], res: Pointer[Resources, MutUntrackedOrigin], label: String, ) raises`
- **`begin_render_pass`** `begin_render_pass( self, colors: List[ColorAttachment], depth: DepthAttachment ) raises -> RenderPass`
Begins a render pass on the given colour targets and optional depth target. Raises when there are more than `MAX_COLOR_ATTACHMENTS` colours or wgpu returns NULL. The depth attachment is used only when `depth.view` is non-null; stencil is not used. Call `RenderPass.end` before recording anything else on this encoder.
- **`begin_compute_pass`** `begin_compute_pass(self) raises -> ComputePass`
Begins a compute pass without timestamp writes; raises if wgpu returns NULL. Call `ComputePass.end` before recording anything else on this encoder.
- **`copy_buffer_to_buffer`** `copy_buffer_to_buffer( self, src: BufferHandle, src_offset: UInt64, dst: BufferHandle, dst_offset: UInt64, size: UInt64, ) raises`
Records a copy of `size` bytes between two buffers. Raises on an invalid handle; range and usage errors surface as validation diagnostics through `Gfx.pump`.
- **`copy_texture_to_buffer`** `copy_texture_to_buffer( self, texture: TextureHandle, dst: BufferHandle, bytes_per_row: UInt32, width: UInt32, height: UInt32, ) raises`
Copies mip 0, layer 0 of a `Resources` texture into `dst` at offset 0, with `bytes_per_row` stride and `height` rows per image. Raises on an invalid handle.
- **`copy_raw_texture_to_buffer`** `copy_raw_texture_to_buffer( self, texture: WGPUTexture, dst: BufferHandle, bytes_per_row: UInt32, width: UInt32, height: UInt32, ) raises`
Copies mip 0 of a texture outside `Resources` (a swapchain texture) into `dst`.
- **`finish_and_submit`** `finish_and_submit(deinit self) raises`
Finishes the encoder, submits the command buffer to the queue and releases both. Raises if `wgpuCommandEncoderFinish` returns NULL (the encoder is still released). Does not wait for the GPU; use `wait_for_queue` for that.
### def `wait_for_queue`
```mojo
def wait_for_queue(mut gfx: Gfx) raises GfxError
```
Blocks until all submitted work has completed (tests and shutdown; never in the frame loop).
### def `read_buffer`
```mojo
def read_buffer( mut gfx: Gfx, res: Resources, buffer: BufferHandle, size: Int ) raises -> List[UInt8]
```
Maps a MapRead buffer, copies `size` bytes out and unmaps it (tests and tools only).
## `cinder.gfx.debug_draw`
Source: `cinder/gfx/debug_draw.mojo`
Immediate-mode debug lines: segments, boxes, spheres, axes and grids drawn in one line-list pass. Vertices accumulate on the CPU each frame (`line`, `aabb`, ...) and are uploaded once in `flush`, which records the draw into an open `RenderPass`. The vertex buffer grows by doubling and is never shrunk, so the steady state allocates nothing. Depth testing is reverse-Z when a depth format is given; `depth_format == Undefined` draws on top of everything.
- `comptime VERTEX_BYTES = 16` — Float32x3 position + Unorm8x4 colour.
- `comptime SHADER = """` — WGSL for debug lines: transforms positions by the camera uniform and passes the vertex colour through unlit.
### struct `Color8`
```mojo
struct Color8(Equatable, ImplicitlyCopyable, RegisterPassable)
```
8-bit RGBA colour.
Fields:
- `r`: UInt8
- `g`: UInt8
- `b`: UInt8
- `a`: UInt8
- **`red`** `red() -> Color8`
Opaque red (255, 0, 0, 255).
- **`green`** `green() -> Color8`
Opaque green (0, 255, 0, 255).
- **`blue`** `blue() -> Color8`
Opaque blue (0, 0, 255, 255).
- **`white`** `white() -> Color8`
Opaque white (255, 255, 255, 255).
- **`yellow`** `yellow() -> Color8`
Opaque yellow (255, 255, 0, 255).
### struct `DebugDraw`
```mojo
struct DebugDraw(Movable)
```
Per-frame line accumulator plus the GPU objects that draw it.
Fields:
- `gfx`: Pointer[Gfx, MutUntrackedOrigin]
- `res`: Pointer[Resources, MutUntrackedOrigin]
- `shader`: ShaderHandle
- `pipeline`: RenderPipelineHandle
- `camera_buffer`: BufferHandle
- `camera_group`: BindGroupHandle
- `vertex_buffer`: BufferHandle
- `vertex_capacity`: Int — Vertices the GPU buffer can hold.
- `vertices`: List[UInt8]
- `vertex_count`: Int
- `grows`: Int — Times the vertex buffer was reallocated (tests check this stays flat in steady state).
- **`__init__`** `__init__( out self, gfx: Pointer[Gfx, MutUntrackedOrigin], res: Pointer[Resources, MutUntrackedOrigin], mut shaders: ShaderManager, color_format: WGPUTextureFormat, depth_format: WGPUTextureFormat = WGPUTextureFormat_Undefined, initial_capacity: Int = 4096, ) raises`
- **`clear`** `clear(mut self)`
Drops this frame's lines (called by `flush`).
- **`line`** `line(mut self, a: Vec3, b: Vec3, color: Color8)`
Queues one world-space segment from `a` to `b` for this frame (two vertices). Only CPU memory grows here; `upload` enlarges the GPU buffer if needed, `flush` draws and clears.
- **`aabb`** `aabb(mut self, min: Vec3, max: Vec3, color: Color8)`
The 12 edges of an axis-aligned box.
- **`sphere`** `sphere( mut self, centre: Vec3, radius: Float32, color: Color8, segments: Int = 24, )`
Three great circles (XY, XZ, YZ).
- **`axes`** `axes(mut self, origin: Vec3, length: Float32)`
X red, Y green, Z blue.
- **`grid`** `grid(mut self, half_extent: Int, spacing: Float32, color: Color8)`
An XZ ground grid centred on the origin with `2 * half_extent + 1` lines per axis.
- **`upload`** `upload(mut self, view_proj: Mat4) raises`
Writes the camera and this frame's vertices to the GPU (before the render pass opens).
- **`flush`** `flush(mut self, pass_: RenderPass) raises`
Records the draw for the vertices uploaded by `upload`, then clears them.
## `cinder.gfx.device`
Source: `cinder/gfx/device.mojo`
Gfx: instance, adapter, device and queue, with every wgpu callback pulled from the shim queue.
- `comptime EventQueuePtr = Optional[Pointer[CnEventQueue, MutUntrackedOrigin]]` — Nullable pointer to the shim's CnEventQueue that carries every wgpu callback as an event.
- `comptime NoChain = Optional[Pointer[WGPUChainedStruct, MutUntrackedOrigin]]` — Null `nextInChain` for wgpu descriptors that carry no chained extension struct.
- `comptime DEFAULT_TIMEOUT_NS = 10_000_000_000` — How long `Gfx.await_event` pumps before raising GfxError: 10 seconds.
### struct `GfxError`
```mojo
struct GfxError(Copyable, Movable, Writable)
```
A recoverable graphics failure (adapter/device request, timeout, lost device).
Fields:
- `message`: String
- **`write_to`** `write_to(self, mut writer: Some[Writer])`
### struct `Gfx`
```mojo
struct Gfx(Movable)
```
Owns one wgpu instance, adapter, device and queue. Callbacks never run in Mojo: `pump` drives wgpu and drains the shim's event queue, turning log lines, uncaptured validation errors and device loss into `diagnostics` (LogRecords for the engine logger) while returning every other event to the caller that awaits it.
Fields:
- `lib`: NativePtr
- `events`: EventQueuePtr
- `instance`: WGPUInstance
- `adapter`: WGPUAdapter
- `device`: WGPUDevice
- `queue`: WGPUQueue
- `adapter_name`: String
- `backend`: UInt32
- `wireframe_supported`: Bool — True when the device was created with wgpu-native's PolygonModeLine feature.
- `bc_supported`: Bool — True when the device was created with BC (DXT/BC7) texture compression.
- `diagnostics`: List[LogRecord]
- `error_count`: Int — Uncaptured validation errors seen since creation. Tests assert this stays zero.
- `lost`: Bool
- `surface`: WGPUSurface — The window surface when constructed from a `Window`; null for headless devices.
- `surface_window`: Optional[Pointer[Window, MutUntrackedOrigin]]
- **`__init__`** `__init__( out self, lib: NativePtr, surface: WGPUSurface ) raises GfxError`
Creates instance, adapter (compatible with `surface` when given) and device.
- **`__init__`** `__init__(out self, lib: NativePtr, ref window: Window) raises GfxError`
Creates instance, a surface for `window`, a compatible adapter and device. `Gfx` keeps the surface (`surface` field) and destroys it at teardown.
- **`__init__`** `__init__( out self, lib: NativePtr, surface: WGPUSurface, window: Optional[Pointer[Window, MutUntrackedOrigin]], ) raises GfxError`
- **`next_request_id`** `next_request_id(mut self) -> UInt64`
Returns a fresh id (1, 2, ...) that tags an async shim request so its reply event can be matched by `await_event` / `take_pending`.
- **`pump`** `pump(mut self, wait: Bool)`
Drives wgpu callbacks and drains the shim queue. Log, error and device-lost events become diagnostics; everything else is held for `await_event` / `take_pending`.
- **`await_event`** `await_event( mut self, kind: UInt32, request_id: UInt64, timeout_ns: Int = DEFAULT_TIMEOUT_NS, ) raises GfxError -> CnEvent`
Pumps until the event (kind, request id) arrives; raises GfxError on timeout.
- **`take_pending`** `take_pending( mut self, kind: UInt32, request_id: UInt64, mut out: List[CnEvent] )`
Moves every pending event matching (kind, request id) into `out`, in arrival order.
- **`take_diagnostics`** `take_diagnostics(mut self) -> List[LogRecord]`
Returns and clears the LogRecords collected by `pump`: wgpu log lines (`wgpu`), uncaptured validation errors (`wgpu.validation`) and device loss (`wgpu.device`). `error_count` and `lost` are not reset.
## `cinder.gfx.pipeline`
Source: `cinder/gfx/pipeline.mojo`
Typed descriptors for bind group layouts, bind groups, pipeline layouts and pipelines. Each `create_*` builds the wgpu descriptor from engine-side structs, calls wgpu with every pointee kept alive for the call, and adopts the result into `Resources` under a typed handle.
- `comptime NoChain = Optional[Pointer[WGPUChainedStruct, MutUntrackedOrigin]]` — Null `nextInChain` for wgpu descriptors that carry no chained extension struct.
### struct `BindingKind`
```mojo
struct BindingKind(Equatable, ImplicitlyCopyable, RegisterPassable)
```
What a bind group layout slot holds.
Fields:
- `value`: UInt8
- **`uniform_buffer`** `uniform_buffer() -> BindingKind`
Uniform buffer; `LayoutEntry.dynamic_offset` and `min_binding_size` apply.
- **`storage_buffer`** `storage_buffer() -> BindingKind`
Read-write storage buffer; `dynamic_offset` and `min_binding_size` apply.
- **`readonly_storage_buffer`** `readonly_storage_buffer() -> BindingKind`
Read-only storage buffer; `dynamic_offset` and `min_binding_size` apply.
- **`texture_2d`** `texture_2d() -> BindingKind`
Filterable float texture with a 2D view dimension.
- **`sampler`** `sampler() -> BindingKind`
Filtering sampler.
- **`comparison_sampler`** `comparison_sampler() -> BindingKind`
Comparison sampler, as made by `Resources.create_comparison_sampler`.
- **`depth_texture_2d`** `depth_texture_2d() -> BindingKind`
Depth texture with a 2D view dimension.
- **`depth_texture_2d_array`** `depth_texture_2d_array() -> BindingKind`
Depth texture with a 2D-array view dimension.
- **`texture_cube`** `texture_cube() -> BindingKind`
Filterable float texture with a cube view dimension.
- **`texture_2d_array`** `texture_2d_array() -> BindingKind`
Filterable float texture with a 2D-array view dimension.
- **`storage_texture_2d_write`** `storage_texture_2d_write() -> BindingKind`
Write-only storage texture; `LayoutEntry.format` names the texel format.
- **`storage_texture_2d_array_write`** `storage_texture_2d_array_write() -> BindingKind`
Write-only storage texture with a 2D-array view; `LayoutEntry.format` names the texel format.
- **`depth_texture_multisampled_2d`** `depth_texture_multisampled_2d() -> BindingKind`
Multisampled depth texture with a 2D view dimension.
- **`non_filtering_sampler`** `non_filtering_sampler() -> BindingKind`
Non-filtering sampler, for use with `texture_2d_unfilterable` textures.
- **`texture_2d_unfilterable`** `texture_2d_unfilterable() -> BindingKind`
Float texture read with textureLoad or a non-filtering sampler (RGBA32Float).
### struct `LayoutEntry`
```mojo
struct LayoutEntry(ImplicitlyCopyable, Movable)
```
One slot of a bind group layout. `format` matters for storage textures only.
Fields:
- `binding`: UInt32
- `visibility`: UInt64
- `kind`: BindingKind
- `dynamic_offset`: Bool
- `min_binding_size`: UInt64
- `format`: WGPUTextureFormat
- **`__init__`** `__init__( out self, binding: UInt32, visibility: UInt64, kind: BindingKind, dynamic_offset: Bool = False, min_binding_size: UInt64 = 0, format: WGPUTextureFormat = WGPUTextureFormat_Undefined, )`
### struct `BindingResource`
```mojo
struct BindingResource(ImplicitlyCopyable, Movable)
```
What to bind in a slot: exactly one of buffer, texture view or sampler is non-null.
Fields:
- `binding`: UInt32
- `buffer`: BufferHandle
- `buffer_offset`: UInt64
- `buffer_size`: UInt64
- `texture_view`: TextureViewHandle
- `sampler`: SamplerHandle
- **`for_buffer`** `for_buffer( binding: UInt32, buffer: BufferHandle, offset: UInt64, size: UInt64 ) -> BindingResource`
Binds `size` bytes of `buffer` from `offset`; a `size` of 0 binds from `offset` to the end of the buffer (resolved in `create_bind_group`).
- **`for_texture`** `for_texture( binding: UInt32, view: TextureViewHandle ) -> BindingResource`
Binds a texture view from `Resources` (owned or imported).
- **`for_sampler`** `for_sampler(binding: UInt32, sampler: SamplerHandle) -> BindingResource`
Binds a sampler from `Resources`.
### struct `VertexAttribute`
```mojo
struct VertexAttribute(ImplicitlyCopyable, Movable)
```
One vertex attribute: its format, byte offset within the vertex and shader location.
Fields:
- `format`: WGPUVertexFormat
- `offset`: UInt64
- `location`: UInt32
### struct `VertexLayout`
```mojo
struct VertexLayout(Copyable, Movable)
```
One vertex buffer slot: stride, step mode and attributes.
Fields:
- `stride`: UInt64
- `step_mode`: WGPUVertexStepMode
- `attributes`: List[VertexAttribute]
- **`__init__`** `__init__( out self, stride: UInt64, var attributes: List[VertexAttribute], step_mode: WGPUVertexStepMode = WGPUVertexStepMode_Vertex, )`
### struct `DepthState`
```mojo
struct DepthState(ImplicitlyCopyable, Movable)
```
Depth testing. Reverse-Z means GreaterEqual is the engine's default compare.
Fields:
- `format`: WGPUTextureFormat
- `write`: Bool
- `compare`: WGPUCompareFunction
- `bias`: Int32
- `bias_slope`: Float32
- `bias_clamp`: Float32
- **`__init__`** `__init__( out self, format: WGPUTextureFormat, write: Bool, compare: WGPUCompareFunction, bias: Int32 = 0, bias_slope: Float32 = 0.0, bias_clamp: Float32 = 0.0, )`
- **`none`** `none() -> DepthState`
No depth-stencil state: `build_render_pipeline` omits it because the format is Undefined.
- **`reverse_z`** `reverse_z(format: WGPUTextureFormat, write: Bool = True) -> DepthState`
Reverse-Z depth test (GreaterEqual) on `format`, writing depth unless `write` is False, with no bias.
- **`reverse_z_biased`** `reverse_z_biased( format: WGPUTextureFormat, bias: Int32, bias_slope: Float32 ) -> DepthState`
Shadow-map depth: reverse-Z, written, with constant and slope bias (negative values push depth away from the light in reverse-Z).
### struct `BlendMode`
```mojo
struct BlendMode(Equatable, ImplicitlyCopyable, RegisterPassable)
```
Colour blending preset of a `ColorTarget`, expanded to a WGPUBlendState when a pipeline is built. Any value other than alpha or additive builds as opaque.
Fields:
- `value`: UInt8
- **`opaque`** `opaque() -> BlendMode`
No blending: the source replaces the destination (One, Zero).
- **`alpha`** `alpha() -> BlendMode`
Premultiplied-free source-over: src * a + dst * (1 - a).
- **`additive`** `additive() -> BlendMode`
Adds source to destination for colour and alpha (One, One).
### struct `ColorTarget`
```mojo
struct ColorTarget(ImplicitlyCopyable, Movable)
```
One colour output of a render pipeline: its texture format and blend mode, written with all channels enabled.
Fields:
- `format`: WGPUTextureFormat
- `blend`: BlendMode
### struct `RenderPipelineDesc`
```mojo
struct RenderPipelineDesc(Copyable, Movable)
```
Everything needed for a render pipeline. Entry points name functions in `shader`.
Fields:
- `label`: String
- `layout`: PipelineLayoutHandle
- `shader`: ShaderHandle
- `vertex_entry`: String
- `fragment_entry`: String
- `vertex_layouts`: List[VertexLayout]
- `color_targets`: List[ColorTarget]
- `depth`: DepthState
- `topology`: WGPUPrimitiveTopology
- `cull_mode`: WGPUCullMode
- `sample_count`: UInt32
- `wireframe`: Bool — Line polygon mode (needs the device feature; see `Gfx.wireframe_supported`).
- `front_face_cw`: Bool
- **`__init__`** `__init__( out self, label: String, layout: PipelineLayoutHandle, shader: ShaderHandle, vertex_entry: String, fragment_entry: String, var vertex_layouts: List[VertexLayout], var color_targets: List[ColorTarget], depth: DepthState = DepthState.none(), topology: WGPUPrimitiveTopology = WGPUPrimitiveTopology_TriangleList, cull_mode: WGPUCullMode = WGPUCullMode_None, sample_count: UInt32 = 1, wireframe: Bool = False, front_face_cw: Bool = False, )`
### def `create_bind_group_layout`
```mojo
def create_bind_group_layout( mut res: Resources, label: String, entries: List[LayoutEntry] ) raises -> BindGroupLayoutHandle
```
Creates a WGPUBindGroupLayout from `entries` and adopts it into `res`. Each entry becomes a buffer, sampler, texture or storage-texture layout according to its `BindingKind`; a kind not handled here leaves every binding type unused. Raises if wgpu returns NULL; other validation errors surface through `Gfx.pump` diagnostics. The caller destroys the handle with `Resources.destroy_bind_group_layout`.
### def `create_pipeline_layout`
```mojo
def create_pipeline_layout( mut res: Resources, label: String, layouts: List[BindGroupLayoutHandle] ) raises -> PipelineLayoutHandle
```
Creates a WGPUPipelineLayout whose bind group `layouts` are in group order and adopts it. Raises on an invalid layout handle or a NULL result. The caller destroys the handle with `Resources.destroy_pipeline_layout`.
### def `create_bind_group`
```mojo
def create_bind_group( mut res: Resources, label: String, layout: BindGroupLayoutHandle, bindings: List[BindingResource], ) raises -> BindGroupHandle
```
Creates a WGPUBindGroup for `layout` from `bindings` and adopts it into `res`. The raw wgpu objects behind the handles are captured at creation time; replacing them behind the same handle later does not update this bind group. Raises on any invalid handle or a NULL result; mismatches with the layout surface through `Gfx.pump` diagnostics. The caller destroys the handle with `Resources.destroy_bind_group`.
### def `build_render_pipeline`
```mojo
def build_render_pipeline( res: Resources, desc: RenderPipelineDesc ) raises -> WGPURenderPipeline
```
Creates the raw pipeline object (used by `create_render_pipeline` and by hot reload). Raises on an invalid handle, a NULL result, or a validation error drained right after creation; the invalid object is then released and the error carries wgpu's validation message. The caller owns the returned object.
### def `create_render_pipeline`
```mojo
def create_render_pipeline( mut res: Resources, desc: RenderPipelineDesc ) raises -> RenderPipelineHandle
```
Builds a render pipeline with `build_render_pipeline` and adopts it into `res`. Raises if creation fails, including validation errors drained right after creation. The caller destroys the handle with `Resources.destroy_render_pipeline`.
### def `build_compute_pipeline`
```mojo
def build_compute_pipeline( res: Resources, label: String, layout: PipelineLayoutHandle, shader: ShaderHandle, entry: String, ) raises -> WGPUComputePipeline
```
Creates the raw compute pipeline object, adopted by `create_compute_pipeline` or passed to `Resources.replace_compute_pipeline` to swap it behind an existing handle. A null `layout` handle passes a null pipeline layout to wgpu. Raises on an invalid handle, a NULL result, or a validation error drained right after creation; the invalid object is then released and the error carries wgpu's validation message. The caller owns the returned object.
### def `create_compute_pipeline`
```mojo
def create_compute_pipeline( mut res: Resources, label: String, layout: PipelineLayoutHandle, shader: ShaderHandle, entry: String, ) raises -> ComputePipelineHandle
```
Builds a compute pipeline with `build_compute_pipeline` and adopts it into `res`. The caller destroys the handle with `Resources.destroy_compute_pipeline`.
## `cinder.gfx.resources`
Source: `cinder/gfx/resources.mojo`
Typed GPU resources: one handle type per kind, pooled records, ownership and leak tracking. Every resource created through `Resources` is tracked in a generational pool for its kind, so a `BufferHandle` cannot be passed where a `TextureHandle` is expected (compile error), stale handles fail validation (raise), and `live_counts()` reports exactly what is still alive. Check `live_counts().total() == 0` before teardown to assert a clean shutdown. `__deinit__` releases everything left and, when anything was left, appends one `gfx.resources` warning LogRecord with those counts to `Gfx.diagnostics` (read it with `Gfx.take_diagnostics`). Ownership rules: a pool owns every object it holds except views wrapped by `import_texture_view`, which are only dropped (`forget_texture_view`), never released. A texture cannot be destroyed while views created from it are alive (`destroy_texture` raises). `adopt_*` and `create_*` take ownership even when they raise: the new object is released if it cannot be stored.
- `comptime NoChain = Optional[Pointer[WGPUChainedStruct, MutUntrackedOrigin]]` — Null `nextInChain` for wgpu descriptors that carry no chained extension struct.
### struct `BufferTag`
```mojo
struct BufferTag
```
Phantom tag that makes `BufferHandle` a distinct type.
Fields:
### struct `TextureTag`
```mojo
struct TextureTag
```
Phantom tag that makes `TextureHandle` a distinct type.
Fields:
### struct `TextureViewTag`
```mojo
struct TextureViewTag
```
Phantom tag that makes `TextureViewHandle` a distinct type.
Fields:
### struct `SamplerTag`
```mojo
struct SamplerTag
```
Phantom tag that makes `SamplerHandle` a distinct type.
Fields:
### struct `ShaderTag`
```mojo
struct ShaderTag
```
Phantom tag that makes `ShaderHandle` a distinct type.
Fields:
### struct `BindGroupLayoutTag`
```mojo
struct BindGroupLayoutTag
```
Phantom tag that makes `BindGroupLayoutHandle` a distinct type.
Fields:
### struct `PipelineLayoutTag`
```mojo
struct PipelineLayoutTag
```
Phantom tag that makes `PipelineLayoutHandle` a distinct type.
Fields:
### struct `BindGroupTag`
```mojo
struct BindGroupTag
```
Phantom tag that makes `BindGroupHandle` a distinct type.
Fields:
### struct `RenderPipelineTag`
```mojo
struct RenderPipelineTag
```
Phantom tag that makes `RenderPipelineHandle` a distinct type.
Fields:
### struct `ComputePipelineTag`
```mojo
struct ComputePipelineTag
```
Phantom tag that makes `ComputePipelineHandle` a distinct type.
Fields:
- `comptime BufferHandle = Handle[BufferTag]` — Generational handle to a WGPUBuffer owned by `Resources.buffers`.
- `comptime TextureHandle = Handle[TextureTag]` — Generational handle to a WGPUTexture owned by `Resources.textures`.
- `comptime TextureViewHandle = Handle[TextureViewTag]` — Generational handle to a WGPUTextureView in `Resources.texture_views` (owned or imported).
- `comptime SamplerHandle = Handle[SamplerTag]` — Generational handle to a WGPUSampler owned by `Resources.samplers`.
- `comptime ShaderHandle = Handle[ShaderTag]` — Generational handle to a WGPUShaderModule owned by `Resources.shaders`.
- `comptime BindGroupLayoutHandle = Handle[BindGroupLayoutTag]` — Generational handle to a WGPUBindGroupLayout owned by `Resources.bind_group_layouts`.
- `comptime PipelineLayoutHandle = Handle[PipelineLayoutTag]` — Generational handle to a WGPUPipelineLayout owned by `Resources.pipeline_layouts`.
- `comptime BindGroupHandle = Handle[BindGroupTag]` — Generational handle to a WGPUBindGroup owned by `Resources.bind_groups`.
- `comptime RenderPipelineHandle = Handle[RenderPipelineTag]` — Generational handle to a WGPURenderPipeline owned by `Resources.render_pipelines`.
- `comptime ComputePipelineHandle = Handle[ComputePipelineTag]` — Generational handle to a WGPUComputePipeline owned by `Resources.compute_pipelines`.
### struct `BufferDesc`
```mojo
struct BufferDesc(Copyable, Movable)
```
What to create a buffer with. `usage` is a WGPUBufferUsage bit set.
Fields:
- `label`: String
- `size`: UInt64
- `usage`: UInt64
- `mapped_at_creation`: Bool
### struct `TextureDesc`
```mojo
struct TextureDesc(Copyable, Movable)
```
A 2D texture (array layers and 3D arrive with the renderer). `usage` is WGPUTextureUsage bits.
Fields:
- `label`: String
- `width`: UInt32
- `height`: UInt32
- `depth_or_layers`: UInt32
- `mip_levels`: UInt32
- `sample_count`: UInt32
- `format`: WGPUTextureFormat
- `usage`: UInt64
### struct `SamplerDesc`
```mojo
struct SamplerDesc(Copyable, Movable)
```
Filtering and addressing. `linear` selects trilinear filtering; otherwise nearest.
Fields:
- `label`: String
- `linear`: Bool
- `address_mode`: UInt32
- `max_anisotropy`: UInt16
### struct `BufferRecord`
```mojo
struct BufferRecord(Copyable, Movable)
```
Pool entry for a buffer: the WGPUBuffer plus the size and usage it was created with. Copies handed out by `Resources.buffer` share the raw object; only the pool releases it.
Fields:
- `raw`: WGPUBuffer
- `size`: UInt64
- `usage`: UInt64
### struct `TextureRecord`
```mojo
struct TextureRecord(Copyable, Movable)
```
Pool entry for a texture: the WGPUTexture plus the extent, mip count, format and usage it was created with. Copies share the raw object; only the pool releases it.
Fields:
- `raw`: WGPUTexture
- `width`: UInt32
- `height`: UInt32
- `depth_or_layers`: UInt32
- `mip_levels`: UInt32
- `format`: WGPUTextureFormat
- `usage`: UInt64
### struct `TextureViewRecord`
```mojo
struct TextureViewRecord(Copyable, Movable)
```
Pool entry for a view: the WGPUTextureView and the texture it views. `texture` is null for views wrapped by `import_texture_view`, whose raw object the caller owns (`is_imported`).
Fields:
- `raw`: WGPUTextureView
- `texture`: TextureHandle
- **`is_imported`** `is_imported(self) -> Bool`
True for a view wrapped by `import_texture_view`: `Resources` never releases it.
### struct `SamplerRecord`
```mojo
struct SamplerRecord(Copyable, Movable)
```
Pool entry for a sampler: the WGPUSampler, released by `Resources.destroy_sampler`.
Fields:
- `raw`: WGPUSampler
### struct `ShaderRecord`
```mojo
struct ShaderRecord(Copyable, Movable)
```
Pool entry for a shader: the WGPUShaderModule and the WGSL path it was compiled from. `ShaderManager` swaps `raw` in place on hot reload, keeping the handle stable.
Fields:
- `raw`: WGPUShaderModule
- `source_path`: String
### struct `BindGroupLayoutRecord`
```mojo
struct BindGroupLayoutRecord(Copyable, Movable)
```
Pool entry for a bind group layout: the WGPUBindGroupLayout the pool owns.
Fields:
- `raw`: WGPUBindGroupLayout
### struct `PipelineLayoutRecord`
```mojo
struct PipelineLayoutRecord(Copyable, Movable)
```
Pool entry for a pipeline layout: the WGPUPipelineLayout the pool owns.
Fields:
- `raw`: WGPUPipelineLayout
### struct `BindGroupRecord`
```mojo
struct BindGroupRecord(Copyable, Movable)
```
Pool entry for a bind group: the WGPUBindGroup the pool owns.
Fields:
- `raw`: WGPUBindGroup
### struct `RenderPipelineRecord`
```mojo
struct RenderPipelineRecord(Copyable, Movable)
```
Pool entry for a render pipeline: the WGPURenderPipeline the pool owns.
Fields:
- `raw`: WGPURenderPipeline
### struct `ComputePipelineRecord`
```mojo
struct ComputePipelineRecord(Copyable, Movable)
```
Pool entry for a compute pipeline: the WGPUComputePipeline the pool owns.
Fields:
- `raw`: WGPUComputePipeline
### struct `LiveCounts`
```mojo
struct LiveCounts(ImplicitlyCopyable, Movable, Writable)
```
Resources alive per kind.
Fields:
- `buffers`: Int
- `textures`: Int
- `texture_views`: Int
- `samplers`: Int
- `shaders`: Int
- `bind_group_layouts`: Int
- `pipeline_layouts`: Int
- `bind_groups`: Int
- `render_pipelines`: Int
- `compute_pipelines`: Int
- **`total`** `total(self) -> Int`
Sum over every kind; zero means nothing is left to release.
- **`write_to`** `write_to(self, mut writer: Some[Writer])`
### struct `Resources`
```mojo
struct Resources(Movable)
```
Pools of every GPU resource kind, owning the wgpu objects behind typed handles.
Fields:
- `gfx`: Pointer[Gfx, MutUntrackedOrigin]
- `buffers`: Pool[BufferRecord, BufferTag]
- `textures`: Pool[TextureRecord, TextureTag]
- `texture_views`: Pool[TextureViewRecord, TextureViewTag]
- `samplers`: Pool[SamplerRecord, SamplerTag]
- `shaders`: Pool[ShaderRecord, ShaderTag]
- `bind_group_layouts`: Pool[BindGroupLayoutRecord, BindGroupLayoutTag]
- `pipeline_layouts`: Pool[PipelineLayoutRecord, PipelineLayoutTag]
- `bind_groups`: Pool[BindGroupRecord, BindGroupTag]
- `render_pipelines`: Pool[RenderPipelineRecord, RenderPipelineTag]
- `compute_pipelines`: Pool[ComputePipelineRecord, ComputePipelineTag]
- **`__init__`** `__init__( out self, gfx: Pointer[Gfx, MutUntrackedOrigin], capacity: Int = 4096 ) raises`
- **`live_counts`** `live_counts(self) -> LiveCounts`
Live handles per pool right now. Imported texture views count until they are forgotten. A non-zero `total()` just before teardown is what `__deinit__` reports as left alive.
- **`create_buffer`** `create_buffer(mut self, desc: BufferDesc) raises -> BufferHandle`
Creates a GPU buffer. Validation failures surface through `Gfx.pump` diagnostics; a NULL object (out of memory) raises, as does a full buffer pool (the new buffer is then released).
- **`buffer`** `buffer(self, h: BufferHandle) raises -> BufferRecord`
A copy of the buffer's record; raises on a null, stale or foreign handle.
- **`destroy_buffer`** `destroy_buffer(mut self, h: BufferHandle) raises`
Removes the handle, then destroys and releases the WGPUBuffer. Raises on an invalid handle, in which case nothing is released.
- **`write_buffer`** `write_buffer( self, h: BufferHandle, offset: UInt64, data: Span[UInt8, _] ) raises`
Uploads `data` through the queue (copied immediately; no mapping).
- **`write_texture`** `write_texture( self, h: TextureHandle, mip_level: UInt32, width: UInt32, height: UInt32, layer: UInt32, data: Span[UInt8, _], bytes_per_row: UInt32, rows_per_image: UInt32 = 0, ) raises`
Uploads one mip level of one array layer through the queue (copied immediately). `rows_per_image` is the number of rows in `data`; it defaults to `height` and must be the block-row count for block-compressed formats.
- **`create_texture`** `create_texture(mut self, desc: TextureDesc) raises -> TextureHandle`
Creates a 2D WGPUTexture (`depth_or_layers` array layers) and returns its handle. Validation failures surface through `Gfx.pump` diagnostics; a NULL object raises, as does a full texture pool (the new texture is then released).
- **`texture`** `texture(self, h: TextureHandle) raises -> TextureRecord`
A copy of the texture's record; raises on a null, stale or foreign handle.
- **`texture_view_count`** `texture_view_count(self, h: TextureHandle) raises -> Int`
How many live views created through `Resources` view this texture; raises on an invalid handle.
- **`destroy_texture`** `destroy_texture(mut self, h: TextureHandle) raises`
Removes the handle, then destroys and releases the WGPUTexture. Raises, releasing nothing, on an invalid handle or while views created from the texture are still alive: destroy those views first (`destroy_texture_view`), so no live view handle is left pointing at a destroyed texture.
- **`create_texture_view_ex`** `create_texture_view_ex( mut self, texture: TextureHandle, dimension: WGPUTextureViewDimension, base_mip: UInt32, mip_count: UInt32, base_layer: UInt32, layer_count: UInt32, ) raises -> TextureViewHandle`
A view of a mip range and layer range with an explicit dimension (2D array, cube, one layer of an array as 2D, one mip as a storage target).
- **`create_texture_view`** `create_texture_view( mut self, texture: TextureHandle ) raises -> TextureViewHandle`
A view of the whole texture with its own format and dimension.
- **`import_texture_view`** `import_texture_view( mut self, raw: WGPUTextureView ) raises -> TextureViewHandle`
Wraps a view the caller owns (a swapchain frame) so renderers can target it. Drop it with `forget_texture_view`; the raw view is never released here, not even at teardown, and `destroy_texture_view` refuses it.
- **`forget_texture_view`** `forget_texture_view(mut self, h: TextureViewHandle) raises`
Drops an imported view without releasing the wgpu object. Raises, dropping nothing, on an invalid handle or on a view created by `Resources` (that one must be released with `destroy_texture_view`, or it would leak).
- **`texture_view`** `texture_view(self, h: TextureViewHandle) raises -> TextureViewRecord`
A copy of the view's record; raises on a null, stale or foreign handle.
- **`destroy_texture_view`** `destroy_texture_view(mut self, h: TextureViewHandle) raises`
Removes the handle and releases the WGPUTextureView. Raises, releasing nothing, on an invalid handle or on an imported view (the caller owns that one; drop it with `forget_texture_view`).
- **`create_sampler`** `create_sampler(mut self, desc: SamplerDesc) raises -> SamplerHandle`
Creates a sampler with the same address mode on u, v and w and LOD clamp 0..32. `linear` picks linear min/mag/mip filtering, otherwise nearest. A NULL object raises; invalid settings surface as validation diagnostics through `Gfx.pump`.
- **`create_comparison_sampler`** `create_comparison_sampler( mut self, label: String ) raises -> SamplerHandle`
Linear PCF sampler: clamp to edge, compare GreaterEqual (reverse-Z shadow maps).
- **`sampler`** `sampler(self, h: SamplerHandle) raises -> SamplerRecord`
A copy of the sampler's record; raises on a null, stale or foreign handle.
- **`destroy_sampler`** `destroy_sampler(mut self, h: SamplerHandle) raises`
Removes the handle and releases the WGPUSampler; raises on an invalid handle.
- **`adopt_shader`** `adopt_shader( mut self, raw: WGPUShaderModule, source_path: String ) raises -> ShaderHandle`
Takes ownership of a compiled module (from `ShaderManager`) and returns its handle. Raises on a null module. Ownership passes even when this raises: if the pool is full the module is released before the error propagates.
- **`shader`** `shader(self, h: ShaderHandle) raises -> ShaderRecord`
A copy of the shader's record; raises on a null, stale or foreign handle.
- **`destroy_shader`** `destroy_shader(mut self, h: ShaderHandle) raises`
Removes the handle and releases the WGPUShaderModule. A `ShaderManager` that loaded it keeps watching the file; a later change then fails to reload and logs an error.
- **`adopt_bind_group_layout`** `adopt_bind_group_layout( mut self, raw: WGPUBindGroupLayout ) raises -> BindGroupLayoutHandle`
Takes ownership of a WGPUBindGroupLayout built in `pipeline.mojo`. Raises on a null `raw`; a full pool releases `raw` and raises.
- **`bind_group_layout`** `bind_group_layout( self, h: BindGroupLayoutHandle ) raises -> BindGroupLayoutRecord`
A copy of the layout's record; raises on a null, stale or foreign handle.
- **`destroy_bind_group_layout`** `destroy_bind_group_layout(mut self, h: BindGroupLayoutHandle) raises`
Removes the handle and releases the WGPUBindGroupLayout; raises on an invalid handle.
- **`adopt_pipeline_layout`** `adopt_pipeline_layout( mut self, raw: WGPUPipelineLayout ) raises -> PipelineLayoutHandle`
Takes ownership of a WGPUPipelineLayout built in `pipeline.mojo`. Raises on a null `raw`; a full pool releases `raw` and raises.
- **`pipeline_layout`** `pipeline_layout( self, h: PipelineLayoutHandle ) raises -> PipelineLayoutRecord`
A copy of the layout's record; raises on a null, stale or foreign handle.
- **`destroy_pipeline_layout`** `destroy_pipeline_layout(mut self, h: PipelineLayoutHandle) raises`
Removes the handle and releases the WGPUPipelineLayout; raises on an invalid handle.
- **`adopt_bind_group`** `adopt_bind_group( mut self, raw: WGPUBindGroup ) raises -> BindGroupHandle`
Takes ownership of a WGPUBindGroup built in `pipeline.mojo`. Raises on a null `raw`; a full pool releases `raw` and raises.
- **`bind_group`** `bind_group(self, h: BindGroupHandle) raises -> BindGroupRecord`
A copy of the bind group's record; raises on a null, stale or foreign handle.
- **`destroy_bind_group`** `destroy_bind_group(mut self, h: BindGroupHandle) raises`
Removes the handle and releases the WGPUBindGroup; raises on an invalid handle.
- **`adopt_render_pipeline`** `adopt_render_pipeline( mut self, raw: WGPURenderPipeline ) raises -> RenderPipelineHandle`
Takes ownership of a WGPURenderPipeline built in `pipeline.mojo`. Raises on a null `raw`; a full pool releases `raw` and raises.
- **`render_pipeline`** `render_pipeline( self, h: RenderPipelineHandle ) raises -> RenderPipelineRecord`
A copy of the pipeline's record; raises on a null, stale or foreign handle.
- **`replace_render_pipeline`** `replace_render_pipeline( mut self, h: RenderPipelineHandle, raw: WGPURenderPipeline ) raises`
Swaps the object behind an existing handle (hot reload), releasing the old one. Takes ownership of `raw` in every case: on a null `raw` or an invalid handle it raises, and a non-null `raw` is released first, so a failed hot reload never leaks.
- **`destroy_render_pipeline`** `destroy_render_pipeline(mut self, h: RenderPipelineHandle) raises`
Removes the handle and releases the WGPURenderPipeline; raises on an invalid handle.
- **`adopt_compute_pipeline`** `adopt_compute_pipeline( mut self, raw: WGPUComputePipeline ) raises -> ComputePipelineHandle`
Takes ownership of a WGPUComputePipeline built in `pipeline.mojo`. Raises on a null `raw`; a full pool releases `raw` and raises.
- **`compute_pipeline`** `compute_pipeline( self, h: ComputePipelineHandle ) raises -> ComputePipelineRecord`
A copy of the pipeline's record; raises on a null, stale or foreign handle.
- **`replace_compute_pipeline`** `replace_compute_pipeline( mut self, h: ComputePipelineHandle, raw: WGPUComputePipeline ) raises`
Swaps the object behind an existing handle (hot reload), releasing the old one. Takes ownership of `raw` in every case: on a null `raw` or an invalid handle it raises, and a non-null `raw` is released first, so a failed hot reload never leaks.
- **`destroy_compute_pipeline`** `destroy_compute_pipeline(mut self, h: ComputePipelineHandle) raises`
Removes the handle and releases the WGPUComputePipeline; raises on an invalid handle.
## `cinder.gfx.shaders`
Source: `cinder/gfx/shaders.mojo`
ShaderManager: WGSL modules from files, compile diagnostics with file:line:column, hot reload. Every loaded shader remembers its path and modification time. `check_for_changes` recompiles files that changed on disk; a successful recompile swaps the module behind the same `ShaderHandle` and reports the handle so pipeline owners rebuild, a failed one keeps the last good module and reports the error. Compile messages come from wgpu's async compilation-info callback through the shim queue.
- `comptime NoChain = Optional[Pointer[WGPUChainedStruct, MutUntrackedOrigin]]` — Null `nextInChain` for wgpu descriptors that carry no chained extension struct.
### struct `ShaderMessage`
```mojo
struct ShaderMessage(Copyable, Movable, Writable)
```
One compiler diagnostic, already formatted with its source location.
Fields:
- `path`: String
- `line`: Int
- `column`: Int
- `is_error`: Bool
- `text`: String
- **`write_to`** `write_to(self, mut writer: Some[Writer])`
### struct `ShaderError`
```mojo
struct ShaderError(Copyable, Movable, Writable)
```
Raised when a WGSL file fails to compile. `messages` holds every diagnostic.
Fields:
- `path`: String
- `messages`: List[ShaderMessage]
- **`write_to`** `write_to(self, mut writer: Some[Writer])`
### struct `ShaderSource`
```mojo
struct ShaderSource(Copyable, Movable)
```
A loaded shader file and its on-disk state.
Fields:
- `handle`: ShaderHandle
- `path`: String
- `mtime_ns`: Int
- `reload_failures`: Int
### struct `ReloadResult`
```mojo
struct ReloadResult(Copyable, Movable)
```
Outcome of `check_for_changes` for one changed file.
Fields:
- `handle`: ShaderHandle
- `path`: String
- `success`: Bool
### def `parse_wgsl_report`
```mojo
def parse_wgsl_report(path: String, report: String) -> List[ShaderMessage]
```
Turns a naga validation report into messages with locations. naga marks each location as `wgsl:LINE:COLUMN`; every such mark becomes one error message carrying the whole report text. A report with no mark yields one message at line 0.
### struct `ShaderManager`
```mojo
struct ShaderManager(Movable)
```
Owns the list of shader files; the modules themselves live in `Resources`.
Fields:
- `gfx`: Pointer[Gfx, MutUntrackedOrigin]
- `resources`: Pointer[Resources, MutUntrackedOrigin]
- `sources`: List[ShaderSource]
- `diagnostics`: List[LogRecord]
- **`__init__`** `__init__( out self, gfx: Pointer[Gfx, MutUntrackedOrigin], resources: Pointer[Resources, MutUntrackedOrigin], )`
- **`compile_source`** `compile_source( mut self, path: String, source: String ) raises -> WGPUShaderModule`
Compiles WGSL text and returns the module, or raises ShaderError with every diagnostic. wgpu-native 29 does not implement the compilation-info callback, so the module is created inside a validation error scope; naga's report (which names `wgsl:line:column` locations) is popped through the shim queue and parsed into `ShaderMessage`s.
- **`load`** `load(mut self, path: String) raises -> ShaderHandle`
Loads and compiles a WGSL file, registering it for hot reload.
- **`check_for_changes`** `check_for_changes(mut self) -> List[ReloadResult]`
Recompiles every registered file whose modification time changed. On success the module behind the handle is replaced (old one released); on failure the last good module stays and an error diagnostic with file:line:column is recorded.
- **`take_diagnostics`** `take_diagnostics(mut self) -> List[LogRecord]`
Returns and clears the reload LogRecords from `check_for_changes` (info on a successful reload, error with the reason when the last good module was kept).
## `cinder.gfx.swapchain`
Source: `cinder/gfx/swapchain.mojo`
Swapchain: surface configuration, per-frame texture acquisition and presentation. Handles the three real-world cases a frame loop meets: resize (reconfigure on the next acquire), minimise (zero-sized window; frames are skipped until it is restored) and surface loss/outdated (reconfigure and retry once, then report). Device loss is `Gfx.lost` and is the caller's decision.
- `comptime NoChain = Optional[Pointer[WGPUChainedStruct, MutUntrackedOrigin]]` — Null `nextInChain` for wgpu descriptors that carry no chained extension struct.
### struct `SurfaceFrame`
```mojo
struct SurfaceFrame(Movable)
```
The acquired swapchain texture for one frame. `present` consumes it.
Fields:
- `texture`: WGPUTexture
- `view`: WGPUTextureView
- `suboptimal`: Bool
- `width`: UInt32
- `height`: UInt32
### struct `Swapchain`
```mojo
struct Swapchain(Movable)
```
Owns the configuration of a wgpu surface created by `Window.create_surface`.
Fields:
- `gfx`: Pointer[Gfx, MutUntrackedOrigin]
- `surface`: WGPUSurface
- `format`: WGPUTextureFormat
- `usage`: WGPUTextureUsage
- `width`: UInt32
- `height`: UInt32
- `present_mode`: WGPUPresentMode
- `configured`: Bool
- `needs_reconfigure`: Bool
- `frames_skipped`: Int — Frames skipped because the window had no area (minimised).
- `reconfigures`: Int — Reconfigurations performed (resize, outdated or lost surfaces).
- **`__init__`** `__init__( out self, gfx: Pointer[Gfx, MutUntrackedOrigin], surface: WGPUSurface, width: Int, height: Int, prefer_srgb: Bool = True, readback: Bool = False, ) raises GfxError`
Picks a surface format (8-bit BGRA/RGBA, sRGB when `prefer_srgb`) and configures.
- **`resize`** `resize(mut self, width: Int, height: Int)`
Records a new size; the surface is reconfigured on the next `acquire`.
- **`acquire`** `acquire(mut self) raises GfxError -> Optional[SurfaceFrame]`
Returns the frame's texture, or None when the window is minimised. Outdated and lost surfaces are reconfigured and retried once; a second failure raises.
- **`present`** `present(self, var frame: SurfaceFrame)`
Presents the acquired frame and releases its view and texture.
- **`discard`** `discard(self, var frame: SurfaceFrame)`
Releases an acquired frame without presenting (used when a frame is abandoned).
## `cinder.gfx.util`
Source: `cinder/gfx/util.mojo`
Small conversions shared by the gfx modules.
### def `string_view`
```mojo
def string_view(ref text: String) -> WGPUStringView
```
A non-owning WGPUStringView over `text`. `text` must outlive every use of the view.
### def `empty_string_view`
```mojo
def empty_string_view() -> WGPUStringView
```
A WGPUStringView with a null pointer and zero length, for absent labels and strings.
### def `c_message`
```mojo
def c_message(ev: CnEvent) -> String
```
The NUL-terminated message of a shim event as a String, decoded as UTF-8 (invalid sequences become U+FFFD). Reads at most the 1000-byte message buffer.
### def `zero_event`
```mojo
def zero_event() -> CnEvent
```
An all-zero CnEvent, used as the out-parameter that `cn_event_queue_pop` fills.
### def `ptr_to`
```mojo
def ptr_to[ T: AnyType ](ref value: T) -> Optional[Pointer[T, MutUntrackedOrigin]]
```
Raw pointer for a C call. The pointee must stay alive past the call (`keep` it).
### def `opaque_to`
```mojo
def opaque_to[ T: AnyType ](ref value: T) -> Optional[OpaquePointer[MutUntrackedOrigin]]
```
`ptr_to` for `void*` parameters.
### def `handle_from`
```mojo
def handle_from[ T: AnyType ](address: UInt64) -> Optional[Pointer[T, MutUntrackedOrigin]]
```
Rebuilds a wgpu object handle delivered through a CnEvent.
---
> Cinder 0.1.0 documentation: cinder.input API
> Canonical page: https://cinder.ash10.dev/docs/api/input/ · Source: `docs/api/input.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.input`
## `cinder.input`
Source: `cinder/input/__init__.mojo`
Input: action and axis mapping over keyboard, mouse and gamepad, rebindable and saved to disk.
## `cinder.input.map`
Source: `cinder/input/map.mojo`
Input mapping: named actions (buttons) and axes over keyboard, mouse and gamepad, rebindable at runtime and saved as a diffable text file. Feed every platform `Event` to `process`, call `end_frame` once per frame (it clears the just-pressed and just-released edges and the per-frame mouse deltas), and query by name. An axis sums its bindings: a key contributes `scale` while held, an analog source its value times `scale` after the dead zone. `listen` arms a capture: the next key, button or axis event becomes the binding, which is how a settings menu rebinds a control.
- `comptime DEVICE_KEY = 0` — Keyboard source; `code` is an SDL scancode (0-511 are tracked).
- `comptime DEVICE_MOUSE_BUTTON = 1` — Mouse button source; `code` is the SDL button (1 = left, 2 = middle, 3 = right; 0-7 are tracked).
- `comptime DEVICE_MOUSE_AXIS = 2` — Codes: 0 = x delta, 1 = y delta, 2 = wheel.
- `comptime DEVICE_GAMEPAD_BUTTON = 3` — Gamepad button source; `code` is an SDL_GamepadButton (0-31 are tracked). All connected gamepads share one state.
- `comptime DEVICE_GAMEPAD_AXIS = 4` — Gamepad axis source; `code` is an SDL_GamepadAxis (0-7 are tracked), value in [-1, 1]. All connected gamepads share one state.
- `comptime INPUT_MAP_VERSION = 1` — Version in the `cinder-input` header; `InputMap.from_text` rejects any other.
### struct `Binding`
```mojo
struct Binding(Equatable, ImplicitlyCopyable, Movable, Writable)
```
One source: `device` and its `code` (scancode, button or axis index). `scale` multiplies the contribution to an axis (digital sources contribute `scale` while held), `dead_zone` applies to analog sources.
Fields:
- `device`: Int
- `code`: Int
- `scale`: Float32
- `dead_zone`: Float32
- **`key`** `key(scancode: Int, scale: Float32 = 1.0) -> Binding`
A keyboard binding for SDL scancode `scancode`.
- **`mouse_button`** `mouse_button(button: Int, scale: Float32 = 1.0) -> Binding`
A mouse button binding (1 = left, 2 = middle, 3 = right).
- **`mouse_axis`** `mouse_axis(axis: Int, scale: Float32 = 1.0) -> Binding`
A mouse axis binding: 0 = x delta, 1 = y delta, any other code = wheel.
- **`gamepad_button`** `gamepad_button(button: Int, scale: Float32 = 1.0) -> Binding`
A gamepad button binding (SDL_GamepadButton index).
- **`gamepad_axis`** `gamepad_axis( axis: Int, scale: Float32 = 1.0, dead_zone: Float32 = 0.15 ) -> Binding`
An analog gamepad axis binding (SDL_GamepadAxis index) with a dead zone.
- **`same_source`** `same_source(self, o: Binding) -> Bool`
True when both bindings read the same device and code, whatever their scale and dead zone.
- **`write_to`** `write_to(self, mut w: Some[Writer])`
### struct `InputMap`
```mojo
struct InputMap(Movable)
```
Named actions and axes with their bindings, plus the device state they are evaluated against. Text format (`to_text`, `from_text`): a `cinder-input 1` header, then one `action NAME DEVICE CODE SCALE DEAD_ZONE` or `axis NAME ...` line per binding, where DEVICE is `key`, `mouse_button`, `mouse_axis`, `gamepad_button` or `gamepad_axis`; an entry without bindings is a bare `action NAME` or `axis NAME` line. Fields are separated by single spaces, so names must not contain spaces.
Fields:
- `actions`: List[_Action]
- `axes`: List[_Axis]
- `keys_down`: List[Bool]
- `mouse_buttons_down`: List[Bool]
- `gamepad_buttons_down`: List[Bool]
- `gamepad_axes`: List[Float32]
- `mouse_delta_x`: Float32
- `mouse_delta_y`: Float32
- `wheel`: Float32
- `listening_action`: Int
- `listening_axis`: Int
- `listening_scale`: Float32
- **`__init__`** `__init__(out self)`
- **`add_action`** `add_action(mut self, name: String) raises -> Int`
Adds an action without bindings and returns its index; raises when the name is taken.
- **`add_axis`** `add_axis(mut self, name: String) raises -> Int`
Adds an axis without bindings and returns its index; raises when the name is taken.
- **`find_action`** `find_action(self, name: String) -> Int`
Index of the action called `name`, or -1.
- **`find_axis`** `find_axis(self, name: String) -> Int`
Index of the axis called `name`, or -1.
- **`bind_action`** `bind_action(mut self, name: String, binding: Binding) raises`
Appends `binding` to the action, creating the action when it does not exist. When the new source is already down the action becomes held (with a just-pressed edge).
- **`bind_axis`** `bind_axis(mut self, name: String, binding: Binding) raises`
Appends `binding` to the axis, creating the axis when it does not exist.
- **`clear_action_bindings`** `clear_action_bindings(mut self, name: String) raises`
Removes every binding of the action; raises for an unknown action. An action held at that moment is released (`pressed` turns False and `just_released` fires), so it cannot stay stuck with no binding to release it.
- **`clear_axis_bindings`** `clear_axis_bindings(mut self, name: String) raises`
Removes every binding of the axis; raises for an unknown axis.
- **`rebind_action`** `rebind_action(mut self, name: String, binding: Binding) raises`
Replaces every binding of the action with `binding`.
- **`action_bindings`** `action_bindings(self, name: String) raises -> List[Binding]`
A copy of the action's bindings; raises for an unknown action.
- **`axis_bindings`** `axis_bindings(self, name: String) raises -> List[Binding]`
A copy of the axis' bindings; raises for an unknown axis.
- **`listen_action`** `listen_action(mut self, name: String) raises`
The next key, mouse button or gamepad button event replaces the action's bindings. An action held through its old bindings is released at that point (see `clear_action_bindings`).
- **`listen_axis`** `listen_axis(mut self, name: String, scale: Float32 = 1.0) raises`
The next key, button or analog event is added to the axis with `scale`.
- **`is_listening`** `is_listening(self) -> Bool`
True while `listen_action` or `listen_axis` waits for an event.
- **`cancel_listening`** `cancel_listening(mut self)`
Disarms a pending `listen_action` or `listen_axis` without changing any binding.
- **`process`** `process(mut self, ev: Event)`
Updates device state from one platform event (and captures it while listening).
- **`end_frame`** `end_frame(mut self)`
Clears edges and per-frame deltas; call once per frame after reading input.
- **`pressed`** `pressed(self, name: String) raises -> Bool`
True while any key or button bound to the action is held; mouse and gamepad axis bindings never press an action. Raises for an unknown action.
- **`just_pressed`** `just_pressed(self, name: String) raises -> Bool`
True when the action went from released to held since the last `end_frame`; raises for an unknown action.
- **`just_released`** `just_released(self, name: String) raises -> Bool`
True when the action went from held to released since the last `end_frame`; raises for an unknown action.
- **`axis`** `axis(self, name: String) raises -> Float32`
Sum of the axis' bindings, clamped to [-1, 1] for digital-only axes; analog and mouse contributions are not clamped (mouse deltas are in pixels).
- **`to_text`** `to_text(self) -> String`
Diffable text: one `action`/`axis` line per binding, sorted as defined.
- **`from_text`** `from_text(text: String) raises -> InputMap`
Parses the text format written by `to_text`; blank lines are skipped. Raises on a missing header, another version, a line that is not 2 or 6 fields, an unknown entry or device, or a malformed number.
- **`save`** `save(self, path: String) raises`
Writes `to_text()` to `path`; raises when the file cannot be written.
- **`load`** `load(path: String) raises -> InputMap`
Reads `path` and parses it with `from_text`; raises when the file cannot be read or does not parse.
---
> Cinder 0.1.0 documentation: cinder.physics API
> Canonical page: https://cinder.ash10.dev/docs/api/physics/ · Source: `docs/api/physics.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.physics`
## `cinder.physics`
Source: `cinder/physics/__init__.mojo`
Physics over Jolt: rigid-body world, queries, character controller and ECS synchronisation.
## `cinder.physics.character`
Source: `cinder/physics/character.mojo`
Character controller over Jolt's CharacterVirtual: slopes, steps and moving platforms. The character is a capsule whose position is at its feet. `update` applies the engine's standard velocity rules (stick to the ground and inherit its velocity, otherwise fall under gravity), then runs Jolt's extended update which walks stairs up to `step_height` and sticks to the floor when walking down. Contacts with dynamic bodies push them with the character's mass.
- `comptime PI_F32: Float32 = 3.14159265358979` — Pi as `Float32`, for converting `max_slope_degrees` to radians.
### struct `GroundState`
```mojo
struct GroundState(Equatable, ImplicitlyCopyable, Movable, Writable)
```
Ground contact state of a `CharacterController`, mirrored from Jolt's ground state.
Fields:
- `value`: UInt8
- **`on_ground`** `on_ground() -> GroundState`
Standing on a surface no steeper than `max_slope_degrees`.
- **`on_steep_ground`** `on_steep_ground() -> GroundState`
Touching a surface steeper than `max_slope_degrees`; `update` treats it like the air and applies gravity.
- **`not_supported`** `not_supported() -> GroundState`
Touching something that cannot hold the character (its edge, a falling body).
- **`in_air`** `in_air() -> GroundState`
Touching nothing; `update` applies gravity.
- **`write_to`** `write_to(self, mut w: Some[Writer])`
### struct `CharacterDesc`
```mojo
struct CharacterDesc(Copyable, Movable)
```
Parameters of a `CharacterController` in metres, kilograms and degrees. `radius` and `height` size the capsule (`height` must exceed `2 * radius + 0.01`); `max_slope_degrees` is the steepest walkable slope and `step_height` the tallest step walked up; `mass` and `max_strength` set how hard dynamic bodies are pushed; `layer` is the object layer the character collides as; `gravity` (m/s^2, default -9.81 on Y) applies while it is not standing on ground.
Fields:
- `radius`: Float32
- `height`: Float32 — Total standing height including both hemispheres.
- `max_slope_degrees`: Float32
- `step_height`: Float32
- `mass`: Float32
- `layer`: Int
- `gravity`: Vec3
- `max_strength`: Float32 — Maximum force (N) the character can push dynamic bodies with.
- **`__init__`** `__init__( out self, radius: Float32 = 0.3, height: Float32 = 1.8, layer: Int = 1, max_slope_degrees: Float32 = 50.0, step_height: Float32 = 0.4, mass: Float32 = 70.0, )`
### struct `CharacterController`
```mojo
struct CharacterController(Movable)
```
A capsule character over Jolt's CharacterVirtual, positioned at its feet. It is not a rigid body: it has no body id. It creates and owns its capsule shape and counts itself in `world.live_characters`; its destructor destroys the character and the shape and decrements the count. It keeps a raw pointer to the `PhysicsWorld`, so the world must not move and must outlive the controller; use it from the thread that steps the world. The constructor raises when `height` does not exceed `2 * radius + 0.01` or Jolt cannot create the character.
Fields:
- `world`: Pointer[PhysicsWorld, MutUntrackedOrigin]
- `desc`: CharacterDesc
- `shape`: Shape
- `character`: Optional[Pointer[JPH_CharacterVirtual, MutUntrackedOrigin]]
- `extended`: JPH_ExtendedUpdateSettings
- **`__init__`** `__init__( out self, world: Pointer[PhysicsWorld, MutUntrackedOrigin], desc: CharacterDesc, position: Vec3, ) raises`
- **`position`** `position(self) -> Vec3`
World position of the character's feet, in metres.
- **`set_position`** `set_position(self, position: Vec3)`
Teleports the character so its feet are at `position` (metres).
- **`rotation`** `rotation(self) -> Quat`
World rotation of the character.
- **`set_rotation`** `set_rotation(self, rotation: Quat)`
Sets the world rotation of the character.
- **`velocity`** `velocity(self) -> Vec3`
Linear velocity of the character in metres per second.
- **`ground_state`** `ground_state(self) -> GroundState`
Ground contact state from Jolt's last character update (see `GroundState`).
- **`is_supported`** `is_supported(self) -> Bool`
Whether Jolt considers the character supported by the ground it touches.
- **`ground_normal`** `ground_normal(self) -> Vec3`
World-space surface normal of the ground contact.
- **`ground_velocity`** `ground_velocity(self) -> Vec3`
Velocity of the ground under the character in metres per second (a moving platform's), refreshed at the start of `update`.
- **`ground_body`** `ground_body(self) -> UInt32`
Id of the body under the character's feet; `INVALID_BODY` when Jolt reports none.
- **`update`** `update( mut self, dt: Float32, desired_velocity: Vec3, jump_speed: Float32 = 0.0 )`
Moves the character for one step. `desired_velocity` is the horizontal velocity the player asks for (its Y is ignored); a positive `jump_speed` launches the character when it stands on the ground. Call before `PhysicsWorld.step` with the same `dt`.
## `cinder.physics.sync`
Source: `cinder/physics/sync.mojo`
ECS <-> physics synchronisation with interpolation. `RigidBody` links an entity to a Jolt body and stores the two most recent simulated poses. The fixed-step loop is: push_kinematics(world, physics, dt) # moved LocalTransforms drive kinematic bodies physics.step(dt) pull_bodies(world, physics) # dynamic bodies: previous <- current <- Jolt ... write_transforms(world, alpha) # every frame: LocalTransform at alpha between the poses Kinematic bodies follow the entity; dynamic bodies drive it; static bodies are left alone.
- `comptime RIGID_BODY_COMPONENT_ID = 7` — ECS component id of `RigidBody`.
### struct `RigidBody`
```mojo
struct RigidBody(Component)
```
The physics side of an entity. `scale` is preserved when the body writes the transform (Jolt bodies carry no scale).
Fields:
- `body`: UInt32
- `motion`: MotionType
- `interpolate`: Bool
- `previous_position`: Vec3
- `previous_rotation`: Quat
- `position`: Vec3
- `rotation`: Quat
- `scale`: Vec3
- `last_pushed_tick`: UInt32 — Change tick of the LocalTransform last pushed to a kinematic body.
- **`__init__`** `__init__( out self, body: UInt32, motion: MotionType, position: Vec3, rotation: Quat, scale: Vec3 = Vec3.one(), interpolate: Bool = True, )`
- **`component_id`** `component_id() -> Int`
### struct `PhysicsSync`
```mojo
struct PhysicsSync(Movable)
```
Per-frame physics <-> ECS synchronisation with persistent query specs and scratch buffers, so steady-state frames allocate nothing. The free functions below are convenience wrappers that build a temporary instance.
Fields:
- **`__init__`** `__init__(out self)`
- **`pull_bodies`** `pull_bodies( mut self, mut world: World, physics: PhysicsWorld ) raises -> Int`
After a physics step: copies each dynamic body's pose into its `RigidBody`, keeping the previous pose for interpolation. Returns the number of bodies read.
- **`push_kinematics`** `push_kinematics( mut self, mut world: World, physics: PhysicsWorld, dt: Float32 ) raises -> Int`
Before a physics step: kinematic bodies whose `LocalTransform` changed since the last push are moved so they reach the new pose during this step. Returns the count.
- **`write_transforms`** `write_transforms( mut self, mut world: World, alpha: Float32 ) raises -> Int`
Writes `LocalTransform` for dynamic bodies at `alpha` in [0, 1] between the previous and current poses. Bodies whose id is `INVALID_BODY` are skipped, as in `pull_bodies`. Only changed transforms are marked. Returns the count updated.
### def `pull_bodies`
```mojo
def pull_bodies(mut world: World, physics: PhysicsWorld) raises -> Int
```
`PhysicsSync.pull_bodies` through a temporary `PhysicsSync`, so it allocates query scratch on every call. Returns the number of dynamic bodies read.
### def `push_kinematics`
```mojo
def push_kinematics( mut world: World, physics: PhysicsWorld, dt: Float32 ) raises -> Int
```
`PhysicsSync.push_kinematics` through a temporary `PhysicsSync`, so it allocates query scratch on every call. Returns the number of kinematic bodies moved.
### def `write_transforms`
```mojo
def write_transforms(mut world: World, alpha: Float32) raises -> Int
```
`PhysicsSync.write_transforms` through a temporary `PhysicsSync`, so it allocates scratch on every call. Returns the number of transforms written.
## `cinder.physics.world`
Source: `cinder/physics/world.mojo`
Rigid-body world over Jolt (joltc): layers, shapes, bodies, queries and queued events. Every Jolt object created here is counted; `live_objects()` must be zero before the world is destroyed. The destructor does not abort on leaks (an abort would hide the real failure of a test that raised): it prints an ERROR record naming the leaked counts (`leak_report()`), and tests and `App.live_objects()` assert zero explicitly. Destroying a body or shape the world does not know (twice, or an invalid id) leaves the counts alone and records an ERROR in `diagnostics`. Jolt runs its own thread pool for the simulation step; its listeners only post to the shim queue (ADR-0003) and `poll_events()` drains that queue on the calling thread, so Mojo never runs inside a Jolt callback. Units: metres, seconds, kilograms; the coordinate system is the engine's (right-handed, Y up).
- `comptime INVALID_BODY: UInt32 = 0xFFFFFFFF` — Jolt's invalid body id.
- `comptime MAX_LAYERS = 32` — Object layers are bits of a 32-bit mask in filters.
### def `to_jolt`
```mojo
def to_jolt(v: Vec3) -> JPH_Vec3
```
Converts an engine `Vec3` to a joltc `JPH_Vec3`.
### def `from_jolt`
```mojo
def from_jolt(v: JPH_Vec3) -> Vec3
```
Converts a joltc `JPH_Vec3` to an engine `Vec3`.
### def `quat_to_jolt`
```mojo
def quat_to_jolt(q: Quat) -> JPH_Quat
```
Converts an engine `Quat` to a joltc `JPH_Quat` (x, y, z, w).
### def `quat_from_jolt`
```mojo
def quat_from_jolt(q: JPH_Quat) -> Quat
```
Converts a joltc `JPH_Quat` to an engine `Quat`.
### def `mat_to_jolt`
```mojo
def mat_to_jolt(m: Mat4) -> JPH_Mat4
```
Column-major on both sides.
### struct `LayerConfig`
```mojo
struct LayerConfig(Copyable, Movable)
```
Object layers and which pairs collide. Layer `i` is bit `i` in query masks. Layers flagged static go to a separate broad-phase layer so static geometry is never tested against itself.
Fields:
- `count`: Int
- `static_mask`: UInt32
- `pairs`: List[Tuple[Int, Int]]
- **`__init__`** `__init__(out self, count: Int, static_mask: UInt32 = 1)`
- **`collide`** `collide(mut self, a: Int, b: Int)`
Enables collision between layers `a` and `b` (symmetric).
- **`default`** `default() -> LayerConfig`
Two layers: 0 static, 1 moving; moving collides with both.
### struct `MotionType`
```mojo
struct MotionType(Equatable, ImplicitlyCopyable, Movable)
```
How a body moves: static, kinematic (driven by the caller) or dynamic (simulated).
Fields:
- `value`: UInt8
- **`static`** `static() -> MotionType`
A body that never moves.
- **`kinematic`** `kinematic() -> MotionType`
A body moved by the caller (`move_kinematic`, `set_position_rotation`), not by forces or contacts.
- **`dynamic`** `dynamic() -> MotionType`
A body simulated by Jolt: gravity, forces, impulses and contacts move it.
- **`to_jolt`** `to_jolt(self) -> JPH_MotionType`
The matching joltc `JPH_MotionType`; any value other than static or kinematic maps to dynamic.
### struct `Shape`
```mojo
struct Shape(ImplicitlyCopyable, Movable)
```
A reference-counted Jolt shape that the world tracks; copy freely, destroy once.
Fields:
- `ptr`: Optional[Pointer[JPH_Shape, MutUntrackedOrigin]]
### struct `BodyDesc`
```mojo
struct BodyDesc(Copyable, Movable)
```
Creation parameters for `PhysicsWorld.create_body`. The constructor takes the shape, position (metres), motion type, object layer and rotation; the other fields keep their defaults until set: friction 0.5, restitution 0, linear and angular damping 0.05, gravity factor 1, not a sensor, discrete motion quality, sleeping allowed, user data 0, zero initial linear velocity (ignored for static bodies) and mass 0. The shape is borrowed: the caller still destroys it with `destroy_shape`.
Fields:
- `shape`: Shape
- `position`: Vec3
- `rotation`: Quat
- `motion`: MotionType
- `layer`: Int
- `friction`: Float32
- `restitution`: Float32
- `linear_damping`: Float32
- `angular_damping`: Float32
- `gravity_factor`: Float32
- `is_sensor`: Bool — Sensors report contacts but exert no forces (triggers).
- `continuous`: Bool — Linear-cast motion quality for fast small bodies.
- `allow_sleeping`: Bool
- `user_data`: UInt64
- `linear_velocity`: Vec3
- `mass`: Float32 — Dynamic bodies: 0 derives the mass from the shape volume and Jolt's default density (1000 kg/m^3); otherwise this mass with the shape's inertia tensor scaled to match.
- **`__init__`** `__init__( out self, shape: Shape, position: Vec3, motion: MotionType, layer: Int, rotation: Quat = Quat.identity(), )`
### struct `PhysicsEventKind`
```mojo
struct PhysicsEventKind(Equatable, ImplicitlyCopyable, Movable, Writable)
```
Kind of a `PhysicsEvent`; writes as its snake_case name.
Fields:
- `value`: UInt8
- **`contact_added`** `contact_added() -> PhysicsEventKind`
Two bodies started touching.
- **`contact_persisted`** `contact_persisted() -> PhysicsEventKind`
Two bodies that were touching are still touching.
- **`contact_removed`** `contact_removed() -> PhysicsEventKind`
Two bodies stopped touching; the event carries only the body ids.
- **`body_activated`** `body_activated() -> PhysicsEventKind`
A body woke up; the event carries the body and its `user_data`.
- **`body_deactivated`** `body_deactivated() -> PhysicsEventKind`
A body went to sleep; the event carries the body and its `user_data`.
- **`write_to`** `write_to(self, mut w: Some[Writer])`
### struct `PhysicsEvent`
```mojo
struct PhysicsEvent(ImplicitlyCopyable, Movable)
```
One queued Jolt event. Contacts: `body1`/`body2`, world `normal` (from body 1 to body 2), first contact `point` on body 1 and the manifold `contact_count`. Activation: `body1` and the body's `user_data`.
Fields:
- `kind`: PhysicsEventKind
- `body1`: UInt32
- `body2`: UInt32
- `normal`: Vec3
- `point`: Vec3
- `contact_count`: Int
- `user_data`: UInt64
- **`involves`** `involves(self, body: UInt32) -> Bool`
Whether `body` is `body1` or `body2`.
- **`pair`** `pair(self, a: UInt32, b: UInt32) -> Bool`
Whether the event is between bodies `a` and `b`, in either order.
### struct `RayHit`
```mojo
struct RayHit(ImplicitlyCopyable, Movable)
```
Closest hit of `PhysicsWorld.raycast`: the `body` id, `fraction` of the ray (the hit is at `origin + fraction * direction`), the world `point` and the hit surface `normal`.
Fields:
- `body`: UInt32
- `fraction`: Float32
- `point`: Vec3
- `normal`: Vec3
### struct `ShapeHit`
```mojo
struct ShapeHit(ImplicitlyCopyable, Movable)
```
Closest hit of `PhysicsWorld.shape_cast`: the `body` id, `fraction` of the sweep, the contact `point` on the hit body, that body's surface `normal`, and `penetration_depth`, which is greater than zero when the shape already overlaps at the start.
Fields:
- `body`: UInt32
- `fraction`: Float32
- `point`: Vec3
- `normal`: Vec3
- `penetration_depth`: Float32
### struct `PhysicsWorld`
```mojo
struct PhysicsWorld(Movable)
```
One Jolt physics system with its thread pool, layer filters and event queue. Shapes are `Shape` handles that the caller destroys once with `destroy_shape`; bodies are `UInt32` ids that the caller destroys with `destroy_body`. Shapes, bodies and characters are counted in `live_objects()`, which should be zero before the world is destroyed; the destructor prints `leak_report()` when it is not. Destroying an unknown shape or body is ignored and recorded as an ERROR in `diagnostics`. The world must not move while a `CharacterController` points at it, and the `NativePtr` library must outlive it. Call every method from one thread (the fixed-step thread); Jolt's workers run only inside `step`. The constructor raises on a layer count outside [1, `MAX_LAYERS`], a collision pair out of range, or a failed Jolt allocation.
Fields:
- `lib`: NativePtr
- `layers`: LayerConfig
- `queue`: Optional[Pointer[CnEventQueue, MutUntrackedOrigin]]
- `jobs`: Optional[Pointer[JPH_JobSystem, MutUntrackedOrigin]]
- `bp_layers`: Optional[
- `pair_filter`: Optional[
- `bp_filter`: Optional[
- `system`: Optional[Pointer[JPH_PhysicsSystem, MutUntrackedOrigin]]
- `bodies`: Optional[Pointer[JPH_BodyInterface, MutUntrackedOrigin]]
- `contact_listener`: Optional[
- `activation_listener`: Optional[
- `body_filter`: Optional[Pointer[CnPhysicsBodyFilter, MutUntrackedOrigin]] — One reusable body filter for queries that ignore a body.
- `filters`: List[_MaskFilter] — Layer-mask filters cached per distinct mask.
- `diagnostics`: List[LogRecord] — ERROR records for misuse the world survived (destroying an unknown shape or body).
- `live_shapes`: Int
- `live_bodies`: Int
- `live_characters`: Int — Maintained by `CharacterController`.
- `steps`: Int
- `dropped_events`: UInt64
- **`__init__`** `__init__( out self, lib: NativePtr, layers: LayerConfig = LayerConfig.default(), max_bodies: Int = 4096, worker_threads: Int = 2, event_capacity: Int = 1 << 14, ) raises`
Creates the Jolt system. `JPH_Init` is idempotent in joltc, so every world calls it; the process-wide Jolt state is never shut down while a world exists.
- **`set_gravity`** `set_gravity(self, g: Vec3)`
Sets the gravity acceleration in m/s^2 (scaled per body by `BodyDesc.gravity_factor`).
- **`optimize_broad_phase`** `optimize_broad_phase(self)`
Call after creating many static bodies and before the first step.
- **`live_objects`** `live_objects(self) -> Int`
Shapes, bodies and characters still alive. Should be zero before the world is destroyed; the destructor prints `leak_report()` when it is not, but does not abort.
- **`leak_report`** `leak_report(self) -> Optional[LogRecord]`
The ERROR record the destructor prints when `live_objects()` is not zero (category `physics`, with `shapes`, `bodies` and `characters` fields), or None without leaks.
- **`error_count`** `error_count(self) -> Int`
Number of ERROR records in `diagnostics`.
- **`box`** `box( mut self, half_extents: Vec3, convex_radius: Float32 = 0.05 ) raises -> Shape`
Creates a box shape with `half_extents` in metres; `convex_radius` (metres) rounds its edges. The caller destroys it with `destroy_shape`. Raises when Jolt fails to create it.
- **`sphere`** `sphere(mut self, radius: Float32) raises -> Shape`
Creates a sphere shape of `radius` metres. The caller destroys it with `destroy_shape`. Raises when Jolt fails to create it.
- **`capsule`** `capsule( mut self, half_height: Float32, radius: Float32 ) raises -> Shape`
Capsule along Y: a cylinder of `2 * half_height` capped by hemispheres of `radius`.
- **`cylinder`** `cylinder( mut self, half_height: Float32, radius: Float32 ) raises -> Shape`
Creates a cylinder along Y, `2 * half_height` tall with `radius` (metres). The caller destroys it with `destroy_shape`. Raises when Jolt fails to create it.
- **`convex_hull`** `convex_hull( mut self, points: List[Vec3], convex_radius: Float32 = 0.05 ) raises -> Shape`
Creates the convex hull of `points` (metres, shape space); `convex_radius` rounds it. The caller destroys it with `destroy_shape`. Raises with fewer than 4 points or when Jolt cannot build the hull.
- **`mesh`** `mesh( mut self, vertices: List[Vec3], indices: List[UInt32] ) raises -> Shape`
Static triangle mesh collider (Jolt mesh shapes only work on static or kinematic bodies).
- **`scaled`** `scaled(mut self, shape: Shape, scale: Vec3) raises -> Shape`
Creates a shape that is `shape` scaled per axis by `scale`. It is a new tracked shape: destroy it with `destroy_shape` in addition to the original. Raises when Jolt fails to create it.
- **`destroy_shape`** `destroy_shape(mut self, shape: Shape)`
Releases the caller's reference to `shape` and decrements the live count; bodies created from it keep their own reference. Call once per created shape and do not use its copies afterwards; a null shape is ignored. A shape this world did not create, or one already destroyed, is not released again: the count is left alone and an ERROR record is added to `diagnostics`.
- **`create_body`** `create_body( mut self, desc: BodyDesc, activate: Bool = True ) raises -> UInt32`
Creates a body from `desc`, adds it to the world (awake when `activate`) and returns its id; destroy it with `destroy_body`. The body keeps its own reference to the shape, so the shape handle may be destroyed right after. A positive `desc.mass` is applied to dynamic bodies only. Raises when the layer is out of range, the shape is null, or Jolt's body limit (`max_bodies`) is reached.
- **`destroy_body`** `destroy_body(mut self, id: UInt32)`
Removes body `id` from the world and destroys it. Jolt may reuse the index, but with a new sequence number, so a stale id never names the new body. An id that is not a live body of this world (`INVALID_BODY`, never created, already destroyed) is ignored: the live count is left alone and an ERROR record is added to `diagnostics`.
- **`position`** `position(self, id: UInt32) -> Vec3`
World position of body `id`, in metres.
- **`rotation`** `rotation(self, id: UInt32) -> Quat`
World rotation of body `id`.
- **`transform`** `transform(self, id: UInt32) -> Mat4`
World matrix of body `id`, with unit scale.
- **`set_position_rotation`** `set_position_rotation( self, id: UInt32, position: Vec3, rotation: Quat, activate: Bool = True )`
Teleports body `id` to `position` (metres) and `rotation`, waking it when `activate`. Kinematic bodies that should carry riders are moved with `move_kinematic` instead.
- **`move_kinematic`** `move_kinematic( self, id: UInt32, target_position: Vec3, target_rotation: Quat, dt: Float32, )`
Sets a kinematic body's velocity so it reaches the target over `dt` (next step).
- **`linear_velocity`** `linear_velocity(self, id: UInt32) -> Vec3`
Linear velocity of body `id`, in metres per second.
- **`set_linear_velocity`** `set_linear_velocity(self, id: UInt32, velocity: Vec3)`
Sets the linear velocity of body `id`, in metres per second.
- **`angular_velocity`** `angular_velocity(self, id: UInt32) -> Vec3`
Angular velocity of body `id`, in radians per second.
- **`set_angular_velocity`** `set_angular_velocity(self, id: UInt32, velocity: Vec3)`
Sets the angular velocity of body `id`, in radians per second.
- **`add_force`** `add_force(self, id: UInt32, force: Vec3)`
Adds `force` (newtons) to body `id` at its centre of mass.
- **`add_impulse`** `add_impulse(self, id: UInt32, impulse: Vec3)`
Applies `impulse` (newton seconds) to body `id` at its centre of mass; wakes a sleeping body.
- **`add_impulse_at`** `add_impulse_at(self, id: UInt32, impulse: Vec3, point: Vec3)`
Applies `impulse` (newton seconds) to body `id` at the world-space `point` (metres), which can also spin it.
- **`is_active`** `is_active(self, id: UInt32) -> Bool`
Whether body `id` is awake.
- **`activate`** `activate(self, id: UInt32)`
Wakes body `id`.
- **`set_motion_type`** `set_motion_type(self, id: UInt32, motion: MotionType)`
Changes the motion type of body `id` and wakes it. Mesh shapes work only on static or kinematic bodies.
- **`motion_type`** `motion_type(self, id: UInt32) -> MotionType`
Current motion type of body `id`.
- **`user_data`** `user_data(self, id: UInt32) -> UInt64`
The 64-bit user value stored on body `id` (`BodyDesc.user_data` or `set_user_data`).
- **`set_user_data`** `set_user_data(self, id: UInt32, value: UInt64)`
Stores a 64-bit user value on body `id`; activation events report it.
- **`body_count`** `body_count(self) -> Int`
Number of bodies in the Jolt system.
- **`step`** `step(mut self, dt: Float32, collision_steps: Int = 1) raises`
Advances the simulation by `dt`. Deterministic for a given sequence of calls and inputs (Jolt's deterministic mode is on by default).
- **`poll_events`** `poll_events(mut self) -> List[PhysicsEvent]`
Drains the listener queue (call once per step after `step`).
- **`raycast`** `raycast( mut self, origin: Vec3, direction: Vec3, layer_mask: UInt32 = 0xFFFFFFFF, ignore_body: UInt32 = INVALID_BODY, ) -> Optional[RayHit]`
Closest hit along `origin + t * direction`, t in [0, 1] (`direction` carries the range). `layer_mask` selects object layers; `ignore_body` is skipped.
- **`shape_cast`** `shape_cast( mut self, shape: Shape, transform: Mat4, direction: Vec3, layer_mask: UInt32 = 0xFFFFFFFF, ignore_body: UInt32 = INVALID_BODY, ) -> Optional[ShapeHit]`
Closest hit of `shape` at `transform` swept by `direction` (t in [0, 1]).
---
> Cinder 0.1.0 documentation: cinder.platform API
> Canonical page: https://cinder.ash10.dev/docs/api/platform/ · Source: `docs/api/platform.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.platform`
## `cinder.platform`
Source: `cinder/platform/__init__.mojo`
Platform layer: the native library, SDL3 windows, input events and wgpu surfaces. This is the only engine module with per-operating-system branches (library file names, surface creation inside the shim). Everything above it is platform-neutral.
## `cinder.platform.events`
Source: `cinder/platform/events.mojo`
Platform events in engine terms, translated from SDL3's event union.
### struct `EventKind`
```mojo
struct EventKind(Equatable, ImplicitlyCopyable, RegisterPassable, Writable)
```
What an `Event` describes.
Fields:
- `value`: UInt8
- **`none`** `none() -> EventKind`
No event: an SDL event this layer does not translate (`Platform.poll` drops it).
- **`quit`** `quit() -> EventKind`
The application was asked to quit (SDL_EVENT_QUIT).
- **`window_close`** `window_close() -> EventKind`
The user asked to close the window `window_id`.
- **`window_resized`** `window_resized() -> EventKind`
Pixel size changed; `a`/`b` carry the new width and height.
- **`window_minimized`** `window_minimized() -> EventKind`
Window `window_id` was minimised.
- **`window_restored`** `window_restored() -> EventKind`
Window `window_id` was restored.
- **`window_focus_gained`** `window_focus_gained() -> EventKind`
Window `window_id` gained keyboard focus.
- **`window_focus_lost`** `window_focus_lost() -> EventKind`
Window `window_id` lost keyboard focus.
- **`key`** `key() -> EventKind`
`scancode`, `keycode`, `mods`, `down`, `repeat`.
- **`mouse_motion`** `mouse_motion() -> EventKind`
`x`, `y` absolute; `dx`, `dy` relative.
- **`mouse_button`** `mouse_button() -> EventKind`
`button` (1 = left, 2 = middle, 3 = right), `down`, `x`, `y`.
- **`mouse_wheel`** `mouse_wheel() -> EventKind`
`dx`, `dy` scroll amounts.
- **`gamepad_added`** `gamepad_added() -> EventKind`
A gamepad was connected; `device` is its SDL joystick id (`Platform.poll` opens it).
- **`gamepad_removed`** `gamepad_removed() -> EventKind`
A gamepad was disconnected; `device` is its SDL joystick id (`Platform.poll` closes it).
- **`gamepad_button`** `gamepad_button() -> EventKind`
`device`, `button` (SDL_GamepadButton), `down`.
- **`gamepad_axis`** `gamepad_axis() -> EventKind`
`device`, `axis` (SDL_GamepadAxis), `value` normalised to [-1, 1].
- **`write_to`** `write_to(self, mut writer: Some[Writer])`
### struct `Event`
```mojo
struct Event(ImplicitlyCopyable, RegisterPassable, Writable)
```
One translated platform event. Fields are interpreted per `kind` (see `EventKind` docstrings).
Fields:
- `kind`: EventKind
- `window_id`: UInt32
- `device`: UInt32
- `a`: Int32
- `b`: Int32
- `scancode`: UInt32
- `keycode`: UInt32
- `mods`: UInt16
- `button`: UInt8
- `axis`: UInt8
- `down`: Bool
- `repeat`: Bool
- `x`: Float32
- `y`: Float32
- `dx`: Float32
- `dy`: Float32
- `value`: Float32
- **`empty`** `empty(kind: EventKind) -> Event`
An event of `kind` with every other field zero or False.
- **`write_to`** `write_to(self, mut writer: Some[Writer])`
### def `translate_event`
```mojo
def translate_event(ev: SDL_Event) -> Event
```
Translates an SDL event into an `Event`; unknown SDL events become `EventKind.none()`.
## `cinder.platform.native`
Source: `cinder/platform/native.mojo`
Loading libcinder_native exactly once and sharing it by pointer. `NativeLibrary` keeps the generated `CinderNative` struct (library handle plus every resolved symbol) at a stable heap address for the life of the program. Subsystems hold a `NativePtr` to it and never copy function pointers out (docs/MOJO_NOTES.md: the handle must outlive every call).
- `comptime NativePtr = Pointer[CinderNative, MutUntrackedOrigin]` — How subsystems refer to the loaded native library.
- `comptime LIBRARY_PATH_ENV = "CINDER_NATIVE_LIB"` — Environment variable that, when set and non-empty, replaces the library path `default_library_path` returns.
### def `default_library_path`
```mojo
def default_library_path() -> String
```
The library path for this OS relative to the repository root, unless CINDER_NATIVE_LIB is set.
### struct `NativeLibrary`
```mojo
struct NativeLibrary(Movable)
```
Owns the one loaded copy of libcinder_native.
Fields:
- **`__init__`** `__init__(out self, path: String) raises`
Loads the library at `path`, resolving every bound symbol. Raises when the library is missing, a symbol is missing, or the shim's ABI version differs from the bindings'.
- **`__init__`** `__init__(out self) raises`
Loads the library from `default_library_path()`.
- **`ptr`** `ptr(self) -> NativePtr`
Pointer to the loaded bindings, valid only while this `NativeLibrary` lives: it must be destroyed after every subsystem holding the pointer.
## `cinder.platform.window`
Source: `cinder/platform/window.mojo`
SDL3 platform: initialisation, windows, event polling, gamepads and wgpu surface creation.
- `comptime SdlWindowPtr = Optional[Pointer[SDL_Window, MutUntrackedOrigin]]` — Nullable pointer to an SDL window.
- `comptime EventQueuePtr = Optional[Pointer[CnEventQueue, MutUntrackedOrigin]]` — Nullable pointer to a shim event queue (the platform's SDL log queue).
### struct `GamepadSlot`
```mojo
struct GamepadSlot(ImplicitlyCopyable, Movable)
```
An open gamepad: SDL joystick id and the SDL_Gamepad handle.
Fields:
- `id`: UInt32
- `handle`: Optional[Pointer[SDL_Gamepad, MutUntrackedOrigin]]
### struct `Platform`
```mojo
struct Platform(Movable)
```
SDL initialised for video, events and gamepads. Create one, before any Window, on the main thread. SDL's log goes to a shim queue and is drained into `diagnostics` by `poll`.
Fields:
- `lib`: NativePtr
- `gamepads`: List[GamepadSlot]
- `diagnostics`: List[LogRecord] — SDL log lines collected since the last `take_diagnostics`.
- `forward_to_imgui`: Bool — When set (by the debug UI), every raw SDL event is also handed to the ImGui backend.
- **`__init__`** `__init__(out self, lib: NativePtr) raises`
- **`video_driver`** `video_driver(self) -> String`
Name of the current SDL video driver; empty when SDL reports none.
- **`last_error`** `last_error(self) -> String`
SDL's most recent error message (`SDL_GetError`).
- **`poll`** `poll(mut self, mut events: List[Event])`
Drains SDL's event queue into `events` (cleared first; capacity is retained so a warmed-up list does not allocate), opening and closing gamepads as they come and go, and moves SDL log lines into `diagnostics`.
- **`take_diagnostics`** `take_diagnostics(mut self) -> List[LogRecord]`
Moves out the SDL log records collected by `poll`, leaving `diagnostics` empty.
- **`push_event`** `push_event(self, ev: SDL_Event) -> Bool`
Injects an SDL event (tests and replay). Returns False when SDL refused it.
### struct `Window`
```mojo
struct Window(Movable)
```
An SDL window. Destroyed with the struct; create the wgpu surface through `create_surface`.
Fields:
- `lib`: NativePtr
- `handle`: SdlWindowPtr
- `id`: UInt32
- **`__init__`** `__init__( out self, platform: Platform, title: String, width: Int, height: Int, resizable: Bool = True, hidden: Bool = False, ) raises`
- **`pixel_size`** `pixel_size(self) raises -> Tuple[Int, Int]`
Drawable size in pixels as (width, height); with high pixel density it can differ from the size the window was created with. Raises when SDL fails.
- **`set_size`** `set_size(self, width: Int, height: Int) raises`
Requests a new window size and waits for it to apply (`SDL_SyncWindow`, whose result is ignored); raises when SDL rejects the request.
- **`set_title`** `set_title(mut self, title: String)`
Sets the window title, keeping the text alive in the window; SDL failures are ignored.
- **`show`** `show(self)`
Shows the window (one created with `hidden`); SDL failures are ignored.
- **`set_relative_mouse`** `set_relative_mouse(self, enabled: Bool) -> Bool`
Turns SDL relative mouse mode on or off for this window; returns False when SDL refused.
- **`create_surface`** `create_surface(self, instance: WGPUInstance) raises -> WGPUSurface`
Creates the wgpu surface for this window through the shim (X11, Wayland or Metal).
- **`destroy_surface`** `destroy_surface(self, surface: WGPUSurface)`
Destroys a surface returned by `create_surface`, through the shim with this window's handle.
---
> Cinder 0.1.0 documentation: cinder.render API
> Canonical page: https://cinder.ash10.dev/docs/api/render/ · Source: `docs/api/render.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.render`
## `cinder.render`
Source: `cinder/render/__init__.mojo`
Renderer: cameras, meshes, materials, textures, the frame graph and the forward+ pipeline. Everything here talks to the GPU only through `cinder.gfx`.
## `cinder.render.camera`
Source: `cinder/render/camera.mojo`
Cameras and frustum culling. Right-handed, Y up, camera looks down -Z. Projection is reverse-Z with an infinite far plane, so the frustum has five planes. `cull_spheres` tests four spheres at a time with SIMD.
- `comptime F4 = SIMD[DType.float32, 4]` — Four float32 lanes: one coordinate of four spheres in `Frustum.cull_spheres_into`.
### struct `Plane`
```mojo
struct Plane(ImplicitlyCopyable, RegisterPassable)
```
`normal . p + d >= 0` is the inside half-space.
Fields:
- `normal`: Vec3
- `d`: Float32
- **`from_row`** `from_row(v: Vec4) -> Plane`
Plane from a clip-space row combination, scaled so `normal` has unit length.
- **`distance`** `distance(self, p: Vec3) -> Float32`
Signed distance from `p` to the plane (positive inside), in the units of `p`.
### struct `Frustum`
```mojo
struct Frustum(Copyable, Movable)
```
Left, right, bottom, top, near planes extracted from a view-projection matrix (Gribb/Hartmann). Reverse-Z with infinite far: the near plane is `row3 - row2` (clip z <= w); no far plane.
Fields:
- `planes`: Array[Plane, 5]
- **`__init__`** `__init__(out self, view_proj: Mat4)`
- **`contains_sphere`** `contains_sphere(self, centre: Vec3, radius: Float32) -> Bool`
True unless the sphere lies entirely behind one of the five planes. Conservative: a sphere near a frustum edge or corner may pass without intersecting it.
- **`contains_point`** `contains_point(self, p: Vec3) -> Bool`
True when `p` is on the inside of all five planes (near, left, right, bottom, top).
- **`cull_spheres_into`** `cull_spheres_into(self, spheres: List[Vec4], mut out: List[Bool])`
Visibility per sphere (xyz centre, w radius), four spheres per SIMD step, written into a list supplied by the caller (cleared first, capacity kept).
- **`cull_spheres`** `cull_spheres(self, spheres: List[Vec4]) -> List[Bool]`
Visibility per sphere (xyz centre, w radius); allocates the result list. Use `cull_spheres_into` in per-frame code to reuse a scratch list.
### struct `Camera`
```mojo
struct Camera(ImplicitlyCopyable, Movable)
```
A perspective camera. `near` is the only clip distance (infinite far).
Fields:
- `position`: Vec3
- `rotation`: Quat
- `fov_y`: Float32
- `aspect`: Float32
- `near`: Float32
- **`__init__`** `__init__( out self, position: Vec3, rotation: Quat, fov_y: Float32 = 1.0471976, aspect: Float32 = 16.0 / 9.0, near: Float32 = 0.05, )`
- **`look_at`** `look_at( eye: Vec3, target: Vec3, up: Vec3 = Vec3.unit_y(), fov_y: Float32 = 1.0471976, aspect: Float32 = 16.0 / 9.0, near: Float32 = 0.05, ) -> Camera`
A camera at `eye` looking at `target`, with its rotation taken from `Mat4.look_at`. `up` only orients the camera (the stored `up()` is re-orthogonalised); `fov_y` is the vertical field of view in radians, `near` the near clip distance in metres.
- **`forward`** `forward(self) -> Vec3`
World-space view direction: `Vec3.forward()` (-Z) rotated by `rotation`, unit length.
- **`right`** `right(self) -> Vec3`
World-space right axis: +X rotated by `rotation`.
- **`up`** `up(self) -> Vec3`
World-space up axis: +Y rotated by `rotation`.
- **`view`** `view(self) -> Mat4`
World-to-view matrix (right-handed, looking down -Z), column-major.
- **`projection`** `projection(self) -> Mat4`
Reverse-Z perspective with an infinite far plane: depth 1 at `near`, 0 at infinity.
- **`view_projection`** `view_projection(self) -> Mat4`
`projection() * view()`: world space to clip space.
- **`frustum`** `frustum(self) -> Frustum`
The five-plane culling frustum of `view_projection()` (no heap allocation).
- **`set_viewport`** `set_viewport(mut self, width: Int, height: Int)`
Sets `aspect` to `width / height` in pixels; ignored when `height` is not positive.
## `cinder.render.frame_graph`
Source: `cinder/render/frame_graph.mojo`
Frame graph: declared passes, transient textures with lifetimes and aliasing, automatic ordering. The graph is rebuilt every frame (cheap: a few passes). `compile` orders passes by their read/write dependencies, culls passes that contribute nothing to an exported resource, computes each transient's lifetime as a pass-index interval and assigns physical textures so that transients with compatible descriptions and disjoint lifetimes share one allocation. Physical textures persist in a pool across frames and are dropped when their description stops being requested (resize). The renderer executes the compiled order itself (`order`) and looks up each transient's view with `view_of`; the graph never calls back into user code (no closures cross the GPU layer). `to_graphviz` renders the compiled graph for the debug UI.
### struct `TransientDesc`
```mojo
struct TransientDesc(Equatable, ImplicitlyCopyable, Movable)
```
Size in pixels, format, `WGPUTextureUsage` bits, sample and mip count of a transient. Transients share one pooled physical texture only when their descriptions are equal.
Fields:
- `width`: UInt32
- `height`: UInt32
- `format`: WGPUTextureFormat
- `usage`: UInt64
- `sample_count`: UInt32
- `mip_levels`: UInt32
### struct `FrameGraph`
```mojo
struct FrameGraph(Movable)
```
One frame's passes and resources, compiled into an execution order and pooled textures. Per frame: `reset`, declare resources (`create`, `import_view`, `create_external`) and passes (`add_pass`, `read`, `write`), `export` what must survive, `compile`, then walk `order` and look up views with `view_of`. Physical textures are owned by the graph and freed by `release` (or when a compile no longer requests them); declaration and compile do not allocate in steady state.
Fields:
- `resources`: List[_Resource]
- `passes`: List[_Pass]
- `order`: List[Int] — Pass indices in execution order (set by `compile`).
- `physicals`: List[_Physical]
- `compiled`: Bool
- `culled_count`: Int
- **`__init__`** `__init__(out self)`
- **`reset`** `reset(mut self)`
Starts a new frame's declaration. Physical textures stay pooled; pass records keep their read/write list capacity for reuse.
- **`create`** `create(mut self, name: String, desc: TransientDesc) -> Int`
Declares a transient texture and returns its resource index (valid until `reset`). The physical texture is assigned by `compile`, possibly shared with other transients.
- **`import_view`** `import_view( mut self, name: String, view: TextureViewHandle, desc: TransientDesc ) -> Int`
A view the caller keeps alive (swapchain, persistent target). Never aliased.
- **`create_external`** `create_external(mut self, name: String) -> Int`
A buffer-backed resource (no texture): passes writing it are ordered before readers and kept alive by them, but no physical texture is allocated.
- **`export`** `export(mut self, resource: Int)`
Marks a resource as needed outside the graph; passes feeding it are kept.
- **`add_pass`** `add_pass(mut self, name: String) -> Int`
Declares a pass and returns its index; reuses a spare pass record when there is one.
- **`read`** `read(mut self, pass_: Int, resource: Int)`
Declares that `pass_` reads `resource` (ordered after the resource's producer).
- **`write`** `write(mut self, pass_: Int, resource: Int)`
Declares that `pass_` writes `resource`; writers of a resource keep declaration order.
- **`compile`** `compile(mut self, mut res: Resources) raises`
Orders, culls, computes lifetimes and assigns physical textures.
- **`release`** `release(mut self, mut res: Resources) raises`
Destroys every pooled physical texture and its view; raises on a `Resources` error.
- **`view_of`** `view_of(self, resource: Int) raises -> TextureViewHandle`
View of `resource` after `compile`: the imported view or its physical texture's view. Raises for an external (buffer-backed) resource and for one with no physical texture (unused, culled or not compiled).
- **`texture_of`** `texture_of(self, resource: Int) raises -> TextureHandle`
Physical texture behind a transient after `compile`. Raises for imported and external resources and for transients without a physical texture.
- **`find`** `find(self, name: String) -> Int`
Index of the resource with this name, or -1.
- **`physical_count`** `physical_count(self) -> Int`
Pooled physical textures after the last `compile` (`FrameStats.transient_textures`).
- **`is_culled`** `is_culled(self, pass_: Int) -> Bool`
True when the last `compile` culled the pass (it feeds no exported resource).
- **`lifetime`** `lifetime(self, resource: Int) -> Tuple[Int, Int]`
(first, last) execution-order indices using the resource; (-1, -1) if no kept pass does.
- **`to_graphviz`** `to_graphviz(self) -> String`
DOT text: passes as boxes in execution order, resources as ellipses, edges for reads/writes.
## `cinder.render.lights`
Source: `cinder/render/lights.mojo`
Lights and the clustered-culling configuration shared with `shaders/common.wgsl`.
- `comptime CLUSTER_X = 16` — Cluster grid columns across the screen; mirrors `CLUSTER_X` in common.wgsl.
- `comptime CLUSTER_Y = 9` — Cluster grid rows across the screen; mirrors `CLUSTER_Y` in common.wgsl.
- `comptime CLUSTER_Z = 24` — Logarithmic depth slices of the cluster grid; mirrors `CLUSTER_Z` in common.wgsl.
- `comptime CLUSTER_COUNT = CLUSTER_X * CLUSTER_Y * CLUSTER_Z` — Total clusters (16 x 9 x 24); sizes the `cluster_grid` count buffer (one u32 each).
- `comptime CLUSTER_MAX_LIGHTS = 32` — Light indices stored per cluster; lights beyond this are dropped by `clusters.wgsl`.
- `comptime LIGHT_BYTES = 64` — Bytes per packed light: the size of `Light` in common.wgsl.
- `comptime MAX_LIGHTS = 1024` — Most local lights the renderer uploads per frame; extra lights are dropped.
### struct `LightKind`
```mojo
struct LightKind(Equatable, ImplicitlyCopyable, RegisterPassable)
```
Light type tag; values mirror `LIGHT_*` in common.wgsl (0 directional, 1 point, 2 spot).
Fields:
- `value`: UInt32
- **`directional`** `directional() -> LightKind`
Sun-like light at infinity; only its direction matters.
- **`point`** `point() -> LightKind`
Omnidirectional light at a position, attenuated to zero at `range`.
- **`spot`** `spot() -> LightKind`
Cone light at a position along `direction`, attenuated to zero at `range`.
### struct `Light`
```mojo
struct Light(ImplicitlyCopyable, Movable)
```
A light. Directional: `direction` (travel direction), `color * intensity` in lux. Point and spot: position, range (metres), `intensity` in watts, spot cone angles in radians.
Fields:
- `kind`: LightKind
- `position`: Vec3
- `direction`: Vec3
- `color`: Vec3
- `intensity`: Float32
- `range`: Float32
- `inner_angle`: Float32
- `outer_angle`: Float32
- `cast_shadows`: Bool
- **`__init__`** `__init__( out self, kind: LightKind, position: Vec3 = Vec3.zero(), direction: Vec3 = Vec3(0.0, -1.0, 0.0), color: Vec3 = Vec3.one(), intensity: Float32 = 1.0, range: Float32 = 10.0, inner_angle: Float32 = 0.4, outer_angle: Float32 = 0.6, cast_shadows: Bool = False, )`
- **`sun`** `sun( direction: Vec3, color: Vec3 = Vec3.one(), lux: Float32 = 3.0, cast_shadows: Bool = True, ) -> Light`
Directional light travelling along `direction` (normalised), with `lux` illuminance. Position, range and cone angles are zero; shadows default to on (cascaded shadow maps).
- **`point_light`** `point_light( position: Vec3, color: Vec3, watts: Float32, range: Float32, cast_shadows: Bool = False, ) -> Light`
Point light at `position` (metres) emitting `watts`, reaching zero at `range` metres.
- **`spot_light`** `spot_light( position: Vec3, direction: Vec3, color: Vec3, watts: Float32, range: Float32, inner: Float32, outer: Float32, cast_shadows: Bool = False, ) -> Light`
Spot light along `direction` (normalised) emitting `watts` with a `range` in metres. `inner` and `outer` are cone half-angles in radians from the axis: full intensity inside `inner`, fading to zero at `outer`.
- **`bounding_sphere`** `bounding_sphere(self) -> Vec4`
Influence sphere: xyz position, w range in metres (meaningless for directional lights).
- **`pack`** `pack(self, mut w: ByteWriter, shadow_view: Int)`
64 bytes matching `Light` in common.wgsl.
### def `pack_lights_into`
```mojo
def pack_lights_into( mut w: ByteWriter, lights: List[Light], shadow_views: List[Int] )
```
Appends the GPU light array (at least one zeroed entry) to `w`.
### def `pack_lights`
```mojo
def pack_lights(lights: List[Light], shadow_views: List[Int]) -> List[UInt8]
```
`pack_lights_into` into a new byte list (allocates; at least one zeroed 64-byte entry). `shadow_views[i]` is light i's first shadow-view index (-1 or missing: no shadow).
### struct `ClusterParams`
```mojo
struct ClusterParams(ImplicitlyCopyable, Movable)
```
Logarithmic z slicing: slice = log2(z) * scale + bias between `near` and `far`.
Fields:
- `scale`: Float32
- `bias`: Float32
- `far`: Float32
- **`compute`** `compute(near: Float32, far: Float32) -> ClusterParams`
Slice parameters mapping `near` to slice 0 and `far` to `CLUSTER_Z` (metres).
- **`slice_of`** `slice_of(self, view_z: Float32) -> Int`
Z slice for a positive view-space distance in metres, clamped to [0, CLUSTER_Z - 1]. Mirrors `cluster_z_slice` in common.wgsl.
## `cinder.render.material`
Source: `cinder/render/material.mojo`
glTF 2.0 metallic-roughness materials and their GPU bind groups. Uniform layout (96 bytes, 16-byte aligned fields, mirrored by `shaders/material.wgsl`): 0 base_color: vec4 16 emissive.rgb, emissive_strength 32 metallic, roughness, normal_scale, occlusion_strength 48 alpha_cutoff, alpha_mode (0 opaque, 1 mask, 2 blend), double_sided, texture_flags 64 uv_scale.xy, uv_offset.xy (KHR_texture_transform, applied to every map) 80 uv_rotation, texcoord_bits (3 bits per map: base, mr, normal, occlusion, emissive), pad, pad
- `comptime MATERIAL_UNIFORM_SIZE = 96` — Bytes in the material uniform (group 2, binding 0); see the module docstring for the layout.
### struct `AlphaMode`
```mojo
struct AlphaMode(Equatable, ImplicitlyCopyable, RegisterPassable)
```
How alpha is used; packed into the material uniform and mirrored in WGSL.
Fields:
- `value`: UInt32
- **`opaque`** `opaque() -> AlphaMode`
Alpha is ignored; the surface is fully opaque.
- **`mask`** `mask() -> AlphaMode`
Alpha-tested: fragments with alpha below `alpha_cutoff` are discarded.
- **`blend`** `blend() -> AlphaMode`
Alpha-blended; drawn after opaque batches, sorted back to front.
- `comptime TEX_BASE_COLOR = 0` — Slot of the base colour map in `Material.textures`, `texcoords` and `texture_flags`.
- `comptime TEX_METALLIC_ROUGHNESS = 1` — Slot of the metallic-roughness map in `Material.textures`, `texcoords` and `texture_flags`.
- `comptime TEX_NORMAL = 2` — Slot of the normal map in `Material.textures`, `texcoords` and `texture_flags`.
- `comptime TEX_OCCLUSION = 3` — Slot of the occlusion map in `Material.textures`, `texcoords` and `texture_flags`.
- `comptime TEX_EMISSIVE = 4` — Slot of the emissive map in `Material.textures`, `texcoords` and `texture_flags`.
### struct `Material`
```mojo
struct Material(Copyable, Movable)
```
CPU description. Texture views are null when a map is absent (defaults are bound instead).
Fields:
- `name`: String
- `base_color`: Vec4
- `metallic`: Float32
- `roughness`: Float32
- `emissive`: Vec3
- `emissive_strength`: Float32
- `normal_scale`: Float32
- `occlusion_strength`: Float32
- `alpha_mode`: AlphaMode
- `alpha_cutoff`: Float32
- `double_sided`: Bool
- `uv_scale`: Vec2
- `uv_offset`: Vec2
- `uv_rotation`: Float32
- `textures`: Array[TextureViewHandle, 5]
- `texcoords`: Array[UInt8, 5]
- `sampler`: SamplerHandle
- **`__init__`** `__init__(out self, name: String = String("material"))`
- **`texture_flags`** `texture_flags(self) -> UInt32`
Bit `i` set when map slot `i` (`TEX_*`) has a texture view.
- **`texcoord_bits`** `texcoord_bits(self) -> UInt32`
Texture coordinate set per map, 3 bits per `TEX_*` slot (the shader takes uv1 for 1).
- **`pack_uniform`** `pack_uniform(self) -> List[UInt8]`
The 96-byte uniform laid out as in the module docstring, in a new list (allocates).
### def `material_bind_group_layout`
```mojo
def material_bind_group_layout( mut res: Resources, ) raises -> BindGroupLayoutHandle
```
Creates the group-2 material layout matching `shaders/material.wgsl`; raises on a GPU error. Binding 0 is the uniform (vertex and fragment), 1 the sampler, 2..6 the maps in `TEX_*` order.
### struct `GpuMaterial`
```mojo
struct GpuMaterial(ImplicitlyCopyable, Movable)
```
Uniform buffer and bind group of an uploaded material. `alpha_mode` and `double_sided` are copied from the `Material` to select its pipeline.
Fields:
- `uniform`: BufferHandle
- `bind_group`: BindGroupHandle
- `alpha_mode`: AlphaMode
- `double_sided`: Bool
### def `upload_material`
```mojo
def upload_material( mut res: Resources, layout: BindGroupLayoutHandle, defaults: DefaultTextures, material: Material, ) raises -> GpuMaterial
```
Creates the material's uniform buffer and group-2 bind group; raises on a GPU error. Absent maps bind the defaults (white base colour, metallic-roughness and occlusion, flat normal, black emissive) and a null sampler binds `sampler_linear_repeat`. The texture views stay owned by the caller; the returned buffer and bind group are new allocations.
### def `update_material`
```mojo
def update_material( mut res: Resources, gpu: GpuMaterial, material: Material ) raises
```
Re-uploads the parameters (textures are fixed per bind group).
## `cinder.render.mesh`
Source: `cinder/render/mesh.mojo`
Mesh data on the CPU, GPU meshes, the engine vertex format and procedural primitives. Vertex stream (56 bytes): position f32x3 @0, normal f32x3 @12, tangent f32x4 @24 (w = handedness), uv0 f32x2 @40, uv1 f32x2 @48. Skinning attributes live in a second stream (`SkinVertex`, 24 bytes): joints u16x4 @0, weights f32x4 @8. Indices are u32.
- `comptime VERTEX_STRIDE = 56` — Bytes per vertex in the static stream (slot 0); layout in the module docstring.
- `comptime SKIN_VERTEX_STRIDE = 24` — Bytes per vertex in the skinning stream (slot 1): u16x4 joints, f32x4 weights.
- `comptime PI = Float32(3.14159265358979)` — Pi as Float32, for the procedural primitives.
### def `vertex_layout`
```mojo
def vertex_layout() -> VertexLayout
```
Slot 0: the static vertex stream. Locations 0..4.
### def `skin_vertex_layout`
```mojo
def skin_vertex_layout() -> VertexLayout
```
Slot 1: joints and weights. Locations 5 and 6.
### struct `Bounds`
```mojo
struct Bounds(ImplicitlyCopyable, Movable)
```
Axis-aligned bounding box in mesh space (metres).
Fields:
- `min`: Vec3
- `max`: Vec3
- **`centre`** `centre(self) -> Vec3`
Midpoint of the box.
- **`radius`** `radius(self) -> Float32`
Half the box diagonal: the radius of the bounding sphere around `centre()`.
- **`sphere`** `sphere(self) -> Vec4`
Bounding sphere: xyz centre, w radius (the input of `Frustum.cull_spheres_into`).
- **`empty`** `empty() -> Bounds`
Inverted box (min +3.4e38, max -3.4e38); the first `include` collapses it to a point.
- **`include`** `include(mut self, p: Vec3)`
Grows the box to contain `p`.
### struct `MorphTarget`
```mojo
struct MorphTarget(Copyable, Movable)
```
Per-vertex position and normal deltas of one morph target (normals may be empty).
Fields:
- `positions`: List[Vec3]
- `normals`: List[Vec3]
### struct `MeshLod`
```mojo
struct MeshLod(ImplicitlyCopyable, Movable)
```
An index range of the combined index buffer and the simplification error it carries.
Fields:
- `first_index`: UInt32
- `index_count`: UInt32
- `error`: Float32
### struct `MeshData`
```mojo
struct MeshData(Copyable, Movable)
```
CPU-side triangle mesh. Parallel per-vertex arrays; `tangents`, `uv1`, `joints` and `weights` may be empty and are filled with defaults at upload.
Fields:
- `positions`: List[Vec3]
- `normals`: List[Vec3]
- `tangents`: List[Vec4]
- `uv0`: List[Vec2]
- `uv1`: List[Vec2]
- `joints`: List[SIMD[DType.uint16, 4]]
- `weights`: List[Vec4]
- `indices`: List[UInt32]
- `morph_targets`: List[MorphTarget]
- **`__init__`** `__init__(out self)`
- **`vertex_count`** `vertex_count(self) -> Int`
Number of vertices (`len(positions)`).
- **`triangle_count`** `triangle_count(self) -> Int`
Number of triangles (`len(indices) // 3`).
- **`has_skin`** `has_skin(self) -> Bool`
True when every vertex has joint indices and weights (`joints` and `weights` both match `positions`, non-empty), which is what `pack_skin_vertices` reads.
- **`bounds`** `bounds(self) -> Bounds`
Bounds of `positions`; a zero box at the origin when the mesh has no vertices.
- **`validate`** `validate(self) raises`
Raises when the arrays are inconsistent. Checks that normals and uv0 match the vertex count, that tangents and uv1 are empty or match it, that `joints` and `weights` are either both empty or both match it (a skin needs one of each per vertex), that the index count is a multiple of 3 and that every index is in range. Morph targets are not checked (missing deltas pack as zero).
- **`compute_normals`** `compute_normals(mut self)`
Area-weighted face normals accumulated per vertex.
- **`compute_tangents`** `compute_tangents(mut self, lib: NativePtr) raises`
MikkTSpace tangents through the shim (requires normals and uv0).
- **`pack_vertices`** `pack_vertices(self) -> List[UInt8]`
The 56-byte-per-vertex static stream as a new byte list (allocates). Missing tangents become (1, 0, 0, 1) and missing uv1 repeats uv0. Expects normals and uv0 for every vertex (see `validate`).
- **`pack_skin_vertices`** `pack_skin_vertices(self) -> List[UInt8]`
The 24-byte-per-vertex skin stream: little-endian u16x4 joints, then f32x4 weights. Allocates; expects `joints` and `weights` for every vertex.
- **`pack_indices`** `pack_indices(self) -> List[UInt8]`
`indices` as little-endian u32 bytes in a new list (allocates).
- **`pack_morph_deltas`** `pack_morph_deltas(self) -> List[UInt8]`
Per target, per vertex: position delta (vec4) then normal delta (vec4), as common.wgsl reads.
- **`build_lods`** `build_lods( self, lib: NativePtr, levels: Int = 4 ) raises -> List[List[UInt32]]`
LOD index lists (level 0 is the original, vertex-cache optimised) via meshoptimizer. Each level targets half the triangles of the previous one with a relative error bound of 2%; a level that cannot be simplified further ends the chain.
### struct `GpuMesh`
```mojo
struct GpuMesh(Copyable, Movable)
```
Buffers for one mesh. `skin_vertices` is null when the mesh is not skinned, `morph_buffer` when it has no morph targets. `lods[0]` is the full-detail index range. `index_count` is the full-detail index count (`lods[0].index_count`, 0 without LODs), the same for `upload_mesh_lods` and `upload_mesh_bytes`; the index buffer holds every LOD range, so its total length is the sum over `lods`.
Fields:
- `vertices`: BufferHandle
- `skin_vertices`: BufferHandle
- `indices`: BufferHandle
- `index_count`: UInt32
- `vertex_count`: UInt32
- `bounds`: Bounds
- `lods`: List[MeshLod]
- `morph_buffer`: BufferHandle
- `morph_target_count`: UInt32
- **`__init__`** `__init__( out self, vertices: BufferHandle, skin_vertices: BufferHandle, indices: BufferHandle, index_count: UInt32, vertex_count: UInt32, bounds: Bounds, var lods: List[MeshLod], morph_buffer: BufferHandle, morph_target_count: UInt32, )`
- **`select_lod`** `select_lod( self, projected_diameter_px: Float32, screen_height_px: Float32 ) -> Int`
Smaller on screen -> coarser LOD. Level 1 below 1/4 of the screen height, level 2 below 1/8, level 3 below 1/16 (resolution independent).
### def `upload_mesh`
```mojo
def upload_mesh( mut res: Resources, label: String, data: MeshData ) raises -> GpuMesh
```
Uploads with a single LOD.
### def `upload_mesh_lods`
```mojo
def upload_mesh_lods( mut res: Resources, label: String, data: MeshData, lod_indices: List[List[UInt32]], ) raises -> GpuMesh
```
Uploads vertex streams plus one combined index buffer holding every LOD range.
### def `upload_mesh_bytes`
```mojo
def upload_mesh_bytes( mut res: Resources, label: String, vertex_count: UInt32, vertex_bytes: List[UInt8], skin_bytes: List[UInt8], index_bytes: List[UInt8], var lods: List[MeshLod], bounds: Bounds, morph_target_count: UInt32, morph_bytes: List[UInt8], ) raises -> GpuMesh
```
Uploads streams that are already in GPU layout (cooked meshes).
### def `destroy_mesh`
```mojo
def destroy_mesh(mut res: Resources, mesh: GpuMesh) raises
```
Destroys the mesh's vertex and index buffers, and its skin and morph buffers when present. The handles in `mesh` are left as they are; raises on a `Resources` error.
### def `cube`
```mojo
def cube(half: Float32 = 0.5) -> MeshData
```
24-vertex cube with per-face normals, CCW winding seen from outside.
### def `uv_sphere`
```mojo
def uv_sphere( radius: Float32 = 0.5, rings: Int = 16, segments: Int = 32 ) -> MeshData
```
UV sphere centred at the origin, poles on +Y and -Y; `radius` in metres. `(rings + 1) * (segments + 1)` vertices (the seam is duplicated), counter-clockwise seen from outside. u runs around the sphere from +X towards +Z, v from the top pole (0) to the bottom (1); tangents point along increasing u.
### def `plane`
```mojo
def plane(half: Float32 = 0.5, subdivisions: Int = 1) -> MeshData
```
An XZ plane facing +Y.
## `cinder.render.renderer`
Source: `cinder/render/renderer.mojo`
The forward+ renderer: shadows, depth prepass, GTAO, clustered lights, PBR, sky, bloom, tonemap. `Renderer.render(scene, target)` builds the frame graph for the current settings, compiles it (ordering, transient aliasing), fills the GPU buffers from the scene and records every pass into one command buffer. CPU work per frame: frustum culling (SIMD), LOD selection, batching by (mesh, material, skinning), cascade fitting, light packing. Nothing here waits on the GPU.
- `comptime FRAME_UNIFORM_SIZE = 720` — Bytes in the frame uniform: the size of `Frame` in common.wgsl (checked each frame).
- `comptime PASS_UNIFORM_STRIDE = 256` — Bytes per view-projection slot in `pass_uniforms`, the dynamic-offset alignment.
- `comptime MAX_PASS_VIEWS = 128` — Slots in `pass_uniforms`: four cascades, the atlas views, then the camera.
- `comptime INSTANCE_BYTES = 144` — Bytes per instance in the `instances` buffer: model, normal matrix, u32x4 params.
- `comptime MORPH_WEIGHTS_BYTES = 32` — Bytes of morph weights bound per morphed instance: eight f32 weights.
- `comptime POST_UNIFORM_STRIDE = 256` — Bytes per slot in `post_uniforms` (and per morph-weight slot), the dynamic-offset alignment.
- `comptime MAX_CACHED_POST_GROUPS = 64` — Post bind groups kept across frames before the cache is rebuilt.
- `comptime MAX_POST_PASSES = 32` — Slots in `post_uniforms` per frame.
- `comptime INITIAL_MORPH_SLOTS = 32` — Initial slots in the morph-weight buffer; it doubles when a frame has more morphed instances.
- `comptime BLOOM_MIPS = 6` — Levels in the bloom chain; level 0 is half the output resolution.
- `comptime ENV_SIZE = 256` — Face size in pixels of the baked environment cube (RGBA16F, full mip chain).
- `comptime PREFILTER_SIZE = 128` — Face size in pixels of the prefiltered specular cube.
- `comptime PREFILTER_MIPS = 6` — Mips of the prefiltered cube; mip m holds roughness `m / (PREFILTER_MIPS - 1)`.
- `comptime IRRADIANCE_SIZE = 32` — Face size in pixels of the diffuse irradiance cube.
- `comptime BRDF_LUT_SIZE = 256` — Size in pixels of the split-sum BRDF lookup texture.
- `comptime SHADER_DIR = "cinder/render/shaders"` — Directory of the renderer's WGSL files, relative to the working directory.
### struct `Tonemapper`
```mojo
struct Tonemapper(Equatable, ImplicitlyCopyable, RegisterPassable)
```
Tone curve of the tonemap pass; values mirror `post.settings.z` in post.wgsl.
Fields:
- `value`: UInt32
- **`agx`** `agx() -> Tonemapper`
AgX (the default).
- **`aces`** `aces() -> Tonemapper`
ACES filmic curve.
- **`none`** `none() -> Tonemapper`
No curve: the exposed colour is clamped to [0, 1]; also used for debug views.
### struct `DebugMode`
```mojo
struct DebugMode(Equatable, ImplicitlyCopyable, RegisterPassable)
```
Mirrors DEBUG_* in common.wgsl.
Fields:
- `value`: UInt32
- **`none`** `none() -> DebugMode`
Normal lit output.
- **`normals`** `normals() -> DebugMode`
World-space normals mapped to colour (`n * 0.5 + 0.5`).
- **`cascades`** `cascades() -> DebugMode`
Colour per sun shadow cascade, shaded by the sun angle.
- **`clusters`** `clusters() -> DebugMode`
Heat map of the light count in each fragment's cluster (full scale at 16 lights).
- **`overdraw`** `overdraw() -> DebugMode`
Overdraw count: forward pipelines blend additively with the depth test off, so every rasterised fragment (hidden or not, opaque or blended) adds 0.1 grey; brightness is the fragment count / 10 (white at 10 or more) and empty sky is black.
- **`lod`** `lod() -> DebugMode`
Colour per instance from its LOD index (bits 8..15 of the instance flags), skinned and morphed instances included.
- **`albedo`** `albedo() -> DebugMode`
Unlit base colour (material factor times the base colour map).
- **`ao`** `ao() -> DebugMode`
The ambient occlusion term as greyscale.
### struct `RenderSettings`
```mojo
struct RenderSettings(ImplicitlyCopyable, Movable)
```
Quality knobs, read at the start of every `render` (change them between frames). `cascade_resolution`, `atlas_size` and `atlas_tile` are read at construction only.
Fields:
- `msaa`: UInt32 — 1 or 4.
- `bloom`: Bool
- `bloom_strength`: Float32
- `bloom_threshold`: Float32
- `tonemapper`: Tonemapper
- `exposure`: Float32
- `fxaa`: Bool
- `ao`: Bool
- `shadows`: Bool
- `shadow_distance`: Float32
- `cascade_resolution`: Int
- `atlas_size`: Int
- `atlas_tile`: Int
- `ibl_intensity`: Float32
- `debug`: DebugMode
- `wireframe`: Bool
- `cull`: Bool
- **`__init__`** `__init__(out self)`
### struct `Environment`
```mojo
struct Environment(ImplicitlyCopyable, Movable)
```
Procedural sky by default; `equirect` (an RGBA16F 2D texture view) replaces it when set.
Fields:
- `zenith`: Vec3
- `ground`: Vec3
- `sun_intensity`: Float32
- `equirect`: TextureViewHandle
- **`__init__`** `__init__(out self)`
### struct `RenderInstance`
```mojo
struct RenderInstance(Copyable, Movable)
```
One drawn object: mesh and material indices into the renderer's registries.
Fields:
- `mesh`: Int
- `material`: Int
- `transform`: Mat4
- `joint_offset`: Int — Index of the first joint matrix in `RenderScene.joints` (skinned meshes), else -1.
- `morph_weights`: List[Float32]
- `cast_shadows`: Bool
- **`__init__`** `__init__( out self, mesh: Int, material: Int, transform: Mat4, joint_offset: Int = -1, cast_shadows: Bool = True, )`
### struct `RenderScene`
```mojo
struct RenderScene(Movable)
```
Everything one `render` call draws; owned by the caller and only read by the renderer.
Fields:
- `camera`: Camera
- `lights`: List[Light]
- `instances`: List[RenderInstance]
- `joints`: List[Mat4] — Skinning matrices (joint world * inverse bind), referenced by instances.
- `environment`: Environment
- **`__init__`** `__init__(out self, camera: Camera)`
### struct `FrameStats`
```mojo
struct FrameStats(ImplicitlyCopyable, Movable)
```
Counters of the last `render`, in `Renderer.stats`.
Fields:
- `instances_submitted`: Int
- `instances_drawn`: Int
- `draw_calls`: Int — Forward (lit) draws, opaque and blended.
- `prepass_draw_calls`: Int
- `shadow_draw_calls`: Int
- `lights`: Int
- `shadow_views`: Int
- `passes`: Int
- `transient_textures`: Int
### struct `Renderer`
```mojo
struct Renderer(Movable)
```
The forward+ renderer; `render` draws a `RenderScene` into a target view. Owns the pipelines, persistent GPU buffers and textures, the mesh and material registries, the frame graph, the shader library and per-frame scratch (steady-state frames do not allocate). Holds raw pointers to `Gfx` and `Resources`, which must outlive it.
Fields:
- `gfx`: Pointer[Gfx, MutUntrackedOrigin]
- `res`: Pointer[Resources, MutUntrackedOrigin]
- `lib`: NativePtr
- `shaders`: ShaderManager
- `library`: ShaderLibrary
- `settings`: RenderSettings
- `defaults`: DefaultTextures
- `graph`: FrameGraph
- `stats`: FrameStats
- `frame_index`: Int
- `graphviz`: String — DOT text of the last compiled frame graph; refreshed by `graph_dot()` on demand.
- `morph_groups`: List[BindGroupHandle] — Object groups binding a morphed mesh's delta buffer, one per morph buffer in use; batches of meshes without morph targets use `object_group`.
- `morph_group_keys`: List[SIMD[DType.int64, 2]] — (index, generation) of the morph buffer each entry of `morph_groups` binds.
- `post_group_keys`: List[SIMD[DType.int64, 4]] — (input index, input generation, bloom index, bloom generation) per cached post group.
- `alloc_marks`: Bool — Print the Mojo allocation counter after each render phase (needs the counter library).
- `gtao_groups`: List[BindGroupHandle]
- `gtao_group_keys`: List[SIMD[DType.int64, 4]]
- `meshes`: List[GpuMesh]
- `materials`: List[GpuMaterial]
- `material_params`: List[Material]
- `forward_shader`: ShaderHandle
- `depth_shader`: ShaderHandle
- `clusters_shader`: ShaderHandle
- `sky_shader`: ShaderHandle
- `ibl_shader`: ShaderHandle
- `post_shader`: ShaderHandle
- `gtao_shader`: ShaderHandle
- `gtao_msaa_shader`: ShaderHandle
- `frame_layout`: BindGroupLayoutHandle
- `object_layout`: BindGroupLayoutHandle
- `material_layout`: BindGroupLayoutHandle
- `pass_layout`: BindGroupLayoutHandle
- `cluster_out_layout`: BindGroupLayoutHandle
- `gtao_layout`: BindGroupLayoutHandle
- `gtao_msaa_layout`: BindGroupLayoutHandle
- `ibl_layout`: BindGroupLayoutHandle
- `post_layout`: BindGroupLayoutHandle
- `scene_pipeline_layout`: PipelineLayoutHandle
- `cluster_pipeline_layout`: PipelineLayoutHandle
- `gtao_pipeline_layout`: PipelineLayoutHandle
- `gtao_msaa_pipeline_layout`: PipelineLayoutHandle
- `ibl_pipeline_layout`: PipelineLayoutHandle
- `post_pipeline_layout`: PipelineLayoutHandle
- `frame_uniform`: BufferHandle
- `pass_uniforms`: BufferHandle
- `post_uniforms`: BufferHandle
- `lights_buffer`: BufferHandle
- `shadow_views_buffer`: BufferHandle
- `cluster_grid`: BufferHandle
- `cluster_indices`: BufferHandle
- `instances_buffer`: BufferHandle
- `instances_capacity`: Int
- `joints_buffer`: BufferHandle
- `joints_capacity`: Int
- `morph_weights_buffer`: BufferHandle
- `morph_slot_capacity`: Int — Morph-weight slots (POST_UNIFORM_STRIDE bytes each) in `morph_weights_buffer`.
- `empty_morph_buffer`: BufferHandle
- `cascade_texture`: TextureHandle
- `cascade_array_view`: TextureViewHandle
- `cascade_layer_views`: List[TextureViewHandle]
- `atlas_texture`: TextureHandle
- `atlas_view`: TextureViewHandle
- `env_texture`: TextureHandle
- `env_cube_view`: TextureViewHandle
- `prefiltered_texture`: TextureHandle
- `prefiltered_view`: TextureViewHandle
- `irradiance_texture`: TextureHandle
- `irradiance_view`: TextureViewHandle
- `brdf_texture`: TextureHandle
- `brdf_view`: TextureViewHandle
- `shadow_sampler`: SamplerHandle
- `env_sampler`: SamplerHandle
- `post_sampler`: SamplerHandle
- `nearest_sampler`: SamplerHandle
- `env_baked`: Bool
- `env_key`: String
- `frame_group`: BindGroupHandle
- `frame_group_ao_view`: TextureViewHandle
- `shadow_frame_group`: BindGroupHandle
- `cluster_frame_group`: BindGroupHandle — Frame group variant for the cluster pass: the cluster buffers it writes are replaced by a dummy.
- `dummy_cluster_buffer`: BufferHandle — Frame group variant for shadow passes: the shadow maps being written are replaced by dummies.
- `dummy_depth_array`: TextureHandle
- `dummy_depth_2d`: TextureHandle
- `object_group`: BindGroupHandle
- `object_group_key`: String
- `pass_group`: BindGroupHandle
- `cluster_out_group`: BindGroupHandle
- `pipelines`: List[_CachedPipeline]
- `sky_pipeline`: RenderPipelineHandle
- `sky_pipeline_msaa`: UInt32
- `cluster_pipeline`: ComputePipelineHandle
- `gtao_pipeline`: ComputePipelineHandle
- `gtao_blur_pipeline`: ComputePipelineHandle
- `gtao_msaa_pipeline`: ComputePipelineHandle
- `gtao_msaa_blur_pipeline`: ComputePipelineHandle
- `ibl_env_pipeline`: ComputePipelineHandle
- `ibl_prefilter_pipeline`: ComputePipelineHandle
- `ibl_irradiance_pipeline`: ComputePipelineHandle
- `ibl_brdf_pipeline`: ComputePipelineHandle
- `bloom_down_pipeline`: RenderPipelineHandle
- `bloom_up_pipeline`: RenderPipelineHandle
- `tonemap_pipeline`: RenderPipelineHandle
- `fxaa_pipeline`: RenderPipelineHandle
- `blit_pipeline`: RenderPipelineHandle
- `post_target_format`: WGPUTextureFormat
- `atlas`: ShadowAtlas
- `batches`: List[_Batch]
- `blend_batches`: List[_Batch]
- `instance_bytes`: ByteWriter
- `post_groups`: List[BindGroupHandle]
- `cluster_params`: ClusterParams
- **`__init__`** `__init__( out self, gfx: Pointer[Gfx, MutUntrackedOrigin], res: Pointer[Resources, MutUntrackedOrigin], lib: NativePtr, settings: RenderSettings, target_format: WGPUTextureFormat, ) raises`
- **`add_mesh`** `add_mesh( mut self, label: String, data: MeshData, lods: Bool = True ) raises -> Int`
Uploads `data` and returns its index in `meshes`; raises if validation or upload fails. With `lods` and more than 64 triangles, up to four LODs are built with meshoptimizer; otherwise the mesh has one. Always appends (removed slots are only reused by `add_cooked_mesh`).
- **`add_cooked_mesh`** `add_cooked_mesh( mut self, label: String, cooked: CookedMesh ) raises -> Int`
Registers a cooked mesh (streams already in GPU layout). Reuses a removed slot if any.
- **`replace_cooked_mesh`** `replace_cooked_mesh( mut self, index: Int, label: String, cooked: CookedMesh ) raises`
Hot reload: swaps the GPU buffers behind `index`; instances keep their mesh index.
- **`remove_mesh`** `remove_mesh(mut self, index: Int) raises`
Frees the GPU buffers; the slot is reused by the next `add_cooked_mesh`. Instances that still reference it are the caller's error (rendering them raises on the invalid handle).
- **`add_material`** `add_material(mut self, material: Material) raises -> Int`
Uploads `material` and returns its index in `materials`; raises on a GPU error. A copy of the parameters is kept in `material_params`; texture views stay the caller's.
- **`replace_material`** `replace_material(mut self, index: Int, material: Material) raises`
Hot reload: uploads `material` into slot `index` (instances keep their material index) and frees the uniform buffer and bind group of the version it replaces. The new version is uploaded before anything is freed, so a failed upload raises and leaves the slot as it was. Replacing a removed slot makes it live again. `material_params[index]` becomes a copy of `material`. Texture views and samplers are the caller's: the old ones are not destroyed (destroy them after this call if nothing else uses them). Raises when `index` is out of range.
- **`remove_material`** `remove_material(mut self, index: Int) raises`
Frees the uniform buffer and bind group of slot `index` and leaves the slot empty (null handles); removing an empty slot does nothing. Indices stay stable: `add_material` always appends, so a removed slot is never handed out again (only `replace_material` on the same index revives it). `render` raises if a visible instance still uses a removed material. Texture views and samplers are the caller's, and `material_params[index]` keeps the last parameters. Raises when `index` is out of range.
- **`material_is_live`** `material_is_live(self, index: Int) -> Bool`
True when slot `index` exists and has not been removed.
- **`mesh_lod_count`** `mesh_lod_count(self, mesh: Int) -> Int`
Number of LOD index ranges of mesh `mesh`; level 0 is full detail.
- **`reload_shaders`** `reload_shaders(mut self) raises -> Int`
Hot reload: recompiles changed programs and rebuilds every pipeline on success. Pipelines keep their handles; a failed compile keeps the last good module and pipelines.
- **`render`** `render( mut self, scene: RenderScene, target: TextureViewHandle, width: Int, height: Int, ) raises`
Renders `scene` into `target` (format given at construction). Raises GfxError on device loss.
- **`graph_dot`** `graph_dot(mut self) -> String`
DOT text of the last frame's graph (built on demand: debug UI, docs).
- **`release`** `release(mut self) raises`
Frees the frame graph's pooled textures (everything else dies with Resources).
## `cinder.render.shader_library`
Source: `cinder/render/shader_library.mojo`
Shader programs assembled from WGSL files with `#include "file.wgsl"` lines, with hot reload. WGSL has no preprocessor, so includes are resolved here (relative to the shader directory, once per program, in order of first appearance). Error locations reported by naga refer to the assembled text; `ShaderLibrary` maps them back to the file and line they came from before raising.
### struct `AssembledShader`
```mojo
struct AssembledShader(Copyable, Movable)
```
WGSL text with every include spliced in, mapped back to its source files. `chunks` locate each assembled line in its file; `files` lists every file read (`directory/name`), in order of first inclusion.
Fields:
- `text`: String
- `chunks`: List[_Chunk]
- `files`: List[String]
- **`__init__`** `__init__(out self)`
- **`locate`** `locate(self, assembled_line: Int) -> Tuple[String, Int]`
File and file line for a line of the assembled text.
### def `assemble`
```mojo
def assemble(directory: String, entry: String) raises -> AssembledShader
```
Reads `entry` and splices every `#include "x"` (each file at most once) into one text.
### struct `ShaderLibrary`
```mojo
struct ShaderLibrary(Movable)
```
Loads `directory/.wgsl` programs with includes; `check_for_changes` hot reloads.
Fields:
- `gfx`: Pointer[Gfx, MutUntrackedOrigin]
- `res`: Pointer[Resources, MutUntrackedOrigin]
- `shaders`: Pointer[ShaderManager, MutUntrackedOrigin]
- `directory`: String
- `programs`: List[_Program]
- `diagnostics`: List[LogRecord]
- `reloaded`: List[ShaderHandle] — Handles whose module changed in the last `check_for_changes` (pipelines must be rebuilt).
- **`__init__`** `__init__( out self, gfx: Pointer[Gfx, MutUntrackedOrigin], res: Pointer[Resources, MutUntrackedOrigin], shaders: Pointer[ShaderManager, MutUntrackedOrigin], directory: String, )`
- **`compile`** `compile(mut self, entry: String) raises -> WGPUShaderModule`
Assembles and compiles without registering (used for one-off modules and by `load`).
- **`load`** `load(mut self, entry: String) raises -> ShaderHandle`
Assembles, compiles and registers `entry` (a file in `directory`) for hot reload. Returns the handle `Resources` holds the module under. Raises on a missing include or a compile error, with locations mapped to the source files.
- **`check_for_changes`** `check_for_changes(mut self) -> Int`
Recompiles programs whose files changed. Returns how many modules were replaced; failures keep the last good module and add an error diagnostic naming file:line:column.
- **`take_diagnostics`** `take_diagnostics(mut self) -> List[LogRecord]`
Returns and clears the reload records from `check_for_changes` (info or error).
## `cinder.render.shadows`
Source: `cinder/render/shadows.mojo`
Cascaded shadow maps for the sun and an atlas for spot and point lights (CPU-side maths). Cascade fitting: each cascade covers a slice of the camera frustum (practical split scheme, lambda 0.75). The orthographic projection is fitted to the slice's bounding sphere so it has a constant size while the camera rotates, and its origin is snapped to whole shadow texels in light space so edges do not shimmer when the camera moves.
- `comptime CASCADE_COUNT = 4` — Sun shadow cascades; matches the four-entry cascade arrays of `Frame` in common.wgsl.
- `comptime SHADOW_VIEW_BYTES = 96` — Bytes per packed `ShadowView`: the size of `ShadowView` in common.wgsl.
### struct `Cascade`
```mojo
struct Cascade(ImplicitlyCopyable, Movable)
```
One fitted sun cascade: an orthographic reverse-Z light view-projection (world to clip).
Fields:
- `view_proj`: Mat4
- `split_end`: Float32 — View-space distance where this cascade ends.
- `texel_world`: Float32 — World-space size of one shadow texel in this cascade.
### def `cascade_splits_into`
```mojo
def cascade_splits_into( mut out: Array[Float32, CASCADE_COUNT], near: Float32, shadow_distance: Float32, lambda_: Float32 = 0.75, )
```
End distances of the four cascades (practical split: blend of log and uniform) into a stack array (no allocation).
### def `cascade_splits`
```mojo
def cascade_splits( near: Float32, shadow_distance: Float32, lambda_: Float32 = 0.75 ) -> List[Float32]
```
End distances of the four cascades (practical split: blend of log and uniform).
### def `fit_cascade`
```mojo
def fit_cascade( camera: Camera, light_dir: Vec3, z0: Float32, z1: Float32, resolution: Int ) -> Cascade
```
Orthographic light view-projection for one slice with texel snapping. `light_dir` is the direction light travels. Depth range is the sphere diameter plus a generous margin behind the slice so casters outside the camera frustum still cast.
### def `fit_cascades_into`
```mojo
def fit_cascades_into( mut out: List[Cascade], camera: Camera, light_dir: Vec3, shadow_distance: Float32, resolution: Int, )
```
`fit_cascades` into a list supplied by the caller (cleared first, capacity kept).
### def `fit_cascades`
```mojo
def fit_cascades( camera: Camera, light_dir: Vec3, shadow_distance: Float32, resolution: Int ) -> List[Cascade]
```
`fit_cascades_into` into a new list (allocates); per-frame code uses the `_into` variant.
### struct `ShadowView`
```mojo
struct ShadowView(ImplicitlyCopyable, Movable)
```
One atlas tile: a light-space view-projection and its uv rectangle (matches common.wgsl).
Fields:
- `view_proj`: Mat4
- `uv_origin_x`: Float32
- `uv_origin_y`: Float32
- `uv_size`: Float32
- `texel_uv`: Float32
- `near`: Float32
- `far`: Float32
- **`pack`** `pack(self, mut w: ByteWriter)`
Appends the 96-byte GPU form (`ShadowView` in common.wgsl). `view_proj`, then the uv rectangle (origin x, origin y, size, size), then the texel size in uv, near, far and a zero pad.
### struct `ShadowAtlas`
```mojo
struct ShadowAtlas(Movable)
```
Fixed grid of square tiles in one depth texture. Spot lights take one tile, point lights six.
Fields:
- `size`: Int
- `tile_size`: Int
- `tiles_per_row`: Int
- `next_tile`: Int
- `views`: List[ShadowView]
- **`__init__`** `__init__(out self, size: Int = 4096, tile_size: Int = 512)`
- **`capacity`** `capacity(self) -> Int`
Number of tiles in the atlas (`tiles_per_row` squared).
- **`reset`** `reset(mut self)`
Frees every tile and clears `views` (capacity kept); called at the start of each frame.
- **`tile_pixel_origin`** `tile_pixel_origin(self, view_index: Int) -> Tuple[Int, Int]`
Top-left pixel of the tile used by shadow view `view_index` (views fill tiles in order).
- **`allocate`** `allocate(mut self, light: Light) -> Int`
Adds the light's shadow views; returns the first view index or -1 when the atlas is full.
- **`pack_views_into`** `pack_views_into(self, mut w: ByteWriter)`
Appends the GPU shadow-view array (at least one zeroed entry) to `w`.
- **`pack_views`** `pack_views(self) -> List[UInt8]`
`pack_views_into` into a new byte list (allocates; at least one zeroed entry).
## `cinder.render.textures`
Source: `cinder/render/textures.mojo`
GPU textures from CPU images: CPU box-filter mip chains, sRGB or linear formats, defaults.
### struct `GpuTexture`
```mojo
struct GpuTexture(ImplicitlyCopyable, Movable)
```
An uploaded 2D texture and a view of all its mips; `width` and `height` are mip 0 in pixels. The texture and view are owned by whoever uploaded them and freed through `Resources`.
Fields:
- `texture`: TextureHandle
- `view`: TextureViewHandle
- `width`: UInt32
- `height`: UInt32
- `mip_levels`: UInt32
### def `mip_count`
```mojo
def mip_count(width: Int, height: Int) -> Int
```
Levels in a full mip chain down to 1x1 (`floor(log2(max(width, height))) + 1`).
### def `downsample`
```mojo
def downsample(img: Image) -> Image
```
2x2 box filter (edge pixels of odd sizes are clamped), in the image's own encoding.
### def `upload_image`
```mojo
def upload_image( mut res: Resources, label: String, img: Image, srgb: Bool, mips: Bool = True ) raises -> GpuTexture
```
Uploads an RGBA8 image as a 2D texture with a CPU box-filtered mip chain. `srgb` selects `RGBA8UnormSrgb` (colour data) over `RGBA8Unorm` (linear data); `mips = False` uploads level 0 only. The caller owns the result; raises on a GPU error.
### def `solid_image`
```mojo
def solid_image(r: UInt8, g: UInt8, b: UInt8, a: UInt8) -> Image
```
1x1 RGBA8 image of one colour (used for the default textures).
### struct `DefaultTextures`
```mojo
struct DefaultTextures(ImplicitlyCopyable, Movable)
```
1x1 fallbacks: white (base colour, occlusion, metallic-roughness), flat normal, black emissive.
Fields:
- `white`: GpuTexture
- `normal`: GpuTexture
- `black`: GpuTexture
- `sampler_linear_repeat`: SamplerHandle
- `sampler_nearest_clamp`: SamplerHandle
- **`__init__`** `__init__(out self, mut res: Resources) raises`
---
> Cinder 0.1.0 documentation: cinder.scene API
> Canonical page: https://cinder.ash10.dev/docs/api/scene/ · Source: `docs/api/scene.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.scene`
## `cinder.scene`
Source: `cinder/scene/__init__.mojo`
Scene files: versioned text and binary formats with migrations, over the ECS world.
## `cinder.scene.format`
Source: `cinder/scene/format.mojo`
Scene serialisation: a human-diffable text format and a binary format, versioned, with migrations. Both formats carry the same intermediate representation (`SceneDoc`: entities with component records of typed fields), so a document can be read in either format, migrated version by version, and written in either. Component types take part by implementing `SceneComponent` and being registered in a `SceneRegistry`, which stores type-erased read/write functions. Text format (version 2): cinder-scene 2 entity 0 Position x=1.5 y=-2 Label text="hello \\"world\\"" entity 1 Parent entity=#0 Floats are written as the shortest decimal that round-trips, or as `0x...` IEEE bits when the decimal form does not reproduce the value exactly. Entity references (`#n`) are file-local indices remapped on load. Binary: magic `CSCN`, version, then the same records with type tags.
- `comptime SCENE_VERSION = 2` — Version this engine writes and instantiates; older documents are upgraded by `SceneRegistry.migrate`, newer ones are rejected.
- `comptime MAGIC = "CSCN"` — The four bytes that open a binary scene file.
- `comptime KIND_FLOAT = 1` — A single Float32 in `Field.floats[0]` (also mirrored in `Field.number`).
- `comptime KIND_INT = 2` — A signed 64-bit integer in `Field.integer`.
- `comptime KIND_BOOL = 3` — A boolean in `Field.integer` (0 or 1); `true`/`false` in the text format.
- `comptime KIND_STRING = 4` — Text in `Field.text`; written quoted and escaped in the text format.
- `comptime KIND_ENTITY = 5` — A file-local entity index in `Field.integer` (-1 = no entity); `#n` in the text format.
- `comptime KIND_FLOATS = 6` — A fixed list of floats (vec3, vec4, mat4 ...).
### struct `Field`
```mojo
struct Field(Copyable, Movable)
```
One named, typed value of a component record. `kind` is a `KIND_*` constant and selects which of `number`, `integer`, `text` or `floats` holds the value; the others keep their zero defaults.
Fields:
- `name`: String
- `kind`: Int
- `number`: Float64
- `integer`: Int64
- `text`: String
- `floats`: List[Float32]
- **`__init__`** `__init__(out self, name: String, kind: Int)`
### struct `ComponentRecord`
```mojo
struct ComponentRecord(Copyable, Movable)
```
A component as stored in a scene: its scene name and its fields in write order.
Fields:
- `name`: String
- `fields`: List[Field]
- **`__init__`** `__init__(out self, name: String)`
- **`find`** `find(self, name: String) -> Int`
Index of the field called `name`, or -1.
- **`rename_field`** `rename_field(mut self, old: String, new: String) -> Bool`
Renames field `old` to `new` (for migrations); returns False when there is no field `old`. An existing field called `new` is not checked for.
### struct `EntityRecord`
```mojo
struct EntityRecord(Copyable, Movable)
```
One entity of a scene: the records of the components it carries.
Fields:
- `components`: List[ComponentRecord]
- **`__init__`** `__init__(out self)`
- **`find`** `find(self, name: String) -> Int`
Index of the component record called `name`, or -1.
### struct `SceneDoc`
```mojo
struct SceneDoc(Movable)
```
The format-independent scene: a version and the entities in file order. Entity fields hold indices into `entities`. Produced by `SceneRegistry.to_doc`, `read_text` and `read_binary`; consumed by `SceneRegistry.migrate`, `SceneRegistry.instantiate`, `write_text` and `write_binary`.
Fields:
- `version`: Int
- `entities`: List[EntityRecord]
- **`__init__`** `__init__(out self, version: Int = SCENE_VERSION)`
### struct `FieldWriter`
```mojo
struct FieldWriter(Movable)
```
Collects a component's fields. Entities are written as file-local indices.
Fields:
- `record`: ComponentRecord
- `index_of`: Pointer[List[Int], MutUntrackedOrigin] — Entity slot index -> file-local index (-1 when not in the file).
- **`__init__`** `__init__( out self, name: String, index_of: Pointer[List[Int], MutUntrackedOrigin] )`
- **`float`** `float(mut self, name: String, v: Float32)`
Appends a `KIND_FLOAT` field.
- **`int`** `int(mut self, name: String, v: Int)`
Appends a `KIND_INT` field.
- **`bool`** `bool(mut self, name: String, v: Bool)`
Appends a `KIND_BOOL` field (stored as 0 or 1).
- **`string`** `string(mut self, name: String, v: String)`
Appends a `KIND_STRING` field.
- **`entity`** `entity(mut self, name: String, e: Entity)`
Appends a `KIND_ENTITY` field with `e`'s file-local index: -1 when `e` is null, beyond the index table or not part of the snapshot. Only the slot index is looked up; the generation is not checked.
- **`floats`** `floats(mut self, name: String, values: List[Float32])`
Appends a `KIND_FLOATS` field holding a copy of `values`.
- **`vec3`** `vec3(mut self, name: String, v: Vec3)`
Appends `v` as a three-float `KIND_FLOATS` field.
- **`mat4`** `mat4(mut self, name: String, m: Mat4)`
Appends `m` as a 16-float `KIND_FLOATS` field in column-major order.
### struct `FieldReader`
```mojo
struct FieldReader(Movable)
```
Reads a component's fields by name; missing fields take the given default.
Fields:
- `record`: ComponentRecord
- `entities`: Pointer[List[Entity], MutUntrackedOrigin] — File-local index -> spawned entity.
- `error`: String
- **`__init__`** `__init__( out self, var record: ComponentRecord, entities: Pointer[List[Entity], MutUntrackedOrigin], )`
- **`float`** `float(mut self, name: String, default: Float32) -> Float32`
Field `name` as a float, or `default` when absent. An integer field is converted; any other kind sets `error` and returns `default`.
- **`int`** `int(mut self, name: String, default: Int) -> Int`
Field `name` as an integer, or `default` when absent; another kind sets `error` and returns `default`.
- **`bool`** `bool(mut self, name: String, default: Bool) -> Bool`
Field `name` as a boolean, or `default` when absent; another kind sets `error` and returns `default`.
- **`string`** `string(mut self, name: String, default: String) -> String`
Field `name` as a string, or `default` when absent; another kind sets `error` and returns `default`.
- **`entity`** `entity(mut self, name: String) -> Entity`
Field `name` remapped to the spawned entity. Returns the null entity when the field is absent, not an entity field, -1 or out of range; never sets `error`.
- **`floats`** `floats(mut self, name: String, count: Int) -> List[Float32]`
The `count` floats of field `name`. Returns `count` zeros when the field is absent, and also when it is not a `KIND_FLOATS` field of exactly `count` values, in which case `error` is set.
- **`vec3`** `vec3(mut self, name: String, default: Vec3) -> Vec3`
Field `name` as a Vec3, or `default` when absent; a field of another shape sets `error` and yields zero.
- **`mat4`** `mat4(mut self, name: String) -> Mat4`
Field `name` as a column-major Mat4, or identity when absent; a field of another shape sets `error` and yields an all-zero matrix.
### trait `SceneComponent`
```mojo
trait SceneComponent(Component)
```
A component that can be saved: name, field writer and reader.
- **`scene_name`** `scene_name() -> String`
The component's name in scene files, matched against component records on load.
- **`write_scene`** `write_scene(self, mut w: FieldWriter)`
Writes the component's fields into `w`.
- **`read_scene`** `read_scene(mut r: FieldReader) -> Self`
Builds the component from `r`, with defaults for missing fields. When `r.error` is set afterwards (the typed getters set it on a kind mismatch) the component is not added and `SceneRegistry.instantiate` raises.
- `comptime WriteFn = def( Pointer[World, MutUntrackedOrigin], Entity, Pointer[FieldWriter, MutUntrackedOrigin], ) thin -> Bool` — Type-erased writer of one component type: fills the `FieldWriter` and returns False when the entity lacks the component or writing it raised.
- `comptime ReadFn = def( Pointer[World, MutUntrackedOrigin], Entity, Pointer[FieldReader, MutUntrackedOrigin], ) thin -> Bool` — Type-erased reader of one component type: builds the component from the `FieldReader`, adds it to the entity and returns False when the reader recorded an error or the add failed.
- `comptime MigrationFn = def(Pointer[SceneDoc, MutUntrackedOrigin]) thin -> None` — Upgrades a `SceneDoc` in place from one version to the next; `SceneRegistry.migrate` then increments `version` itself.
### struct `SceneRegistry`
```mojo
struct SceneRegistry(Movable)
```
Known component types and version migrations.
Fields:
- `types`: List[_Registered]
- `migrations`: List[Tuple[Int, MigrationFn]] — (from_version, migration) pairs; each converts a document from `from_version` to +1.
- **`__init__`** `__init__(out self)`
- **`register`** `register[T: SceneComponent](mut self)`
Makes `T` saveable under `T.scene_name()`. Duplicate names are not rejected; lookups on load use the first registration.
- **`add_migration`** `add_migration(mut self, from_version: Int, migration: MigrationFn)`
Registers `migration` to upgrade documents at `from_version` to `from_version + 1`.
- **`to_doc`** `to_doc(self, mut world: World) raises -> SceneDoc`
Snapshots every live entity (in archetype order) with its registered components.
- **`migrate`** `migrate(self, mut doc: SceneDoc) raises`
Applies registered migrations until the document is at SCENE_VERSION.
- **`instantiate`** `instantiate( self, mut world: World, doc: SceneDoc ) raises -> List[Entity]`
Spawns the document's entities (after migration) and returns them in file order.
### def `format_float`
```mojo
def format_float(v: Float32) -> String
```
Shortest decimal that round-trips, else the IEEE bits as 0x....
### def `parse_float`
```mojo
def parse_float(text: String) raises -> Float32
```
Parses a float written by `format_float`: a decimal, or `0x` followed by up to 8 hex digits of IEEE bits. Raises on malformed text.
### def `bitcast_u32_to_f32`
```mojo
def bitcast_u32_to_f32(bits: UInt32) -> Float32
```
Reinterprets IEEE-754 single-precision bits as a Float32.
### def `write_text`
```mojo
def write_text(doc: SceneDoc) -> String
```
Serialises `doc` in the text format described in the module docstring: a `cinder-scene N` header, one `entity i` line per entity and one indented `Name key=value ...` line per component.
### def `read_text`
```mojo
def read_text(text: String) raises -> SceneDoc
```
Parses the text format into a `SceneDoc` at the file's version (not migrated). Blank lines and lines starting with `#` are skipped. Raises on a missing header, entities out of order, a component line before any entity, an unterminated quote or bracket, a token without `=`, or a value that does not parse.
### def `write_binary`
```mojo
def write_binary(doc: SceneDoc) -> List[UInt8]
```
Serialises `doc` in the binary format: `MAGIC`, then little-endian u32 version and entity count; per entity a component count, per component its name and field count, per field its name, a u8 kind and the value. Strings are a u32 byte length plus bytes; integers, bools and entities are 64-bit (low word first); float lists carry a u32 count.
### def `read_binary`
```mojo
def read_binary(var data: List[UInt8]) raises -> SceneDoc
```
Parses the binary format written by `write_binary` into a `SceneDoc` at the file's version (not migrated). Raises on a bad magic, truncated data, an unknown field kind, or implausible counts (over 10 million entities, 10 000 components per entity or fields per component, or 1 million floats per field). Trailing bytes are ignored.
## `cinder.scene.gltf_spawn`
Source: `cinder/scene/gltf_spawn.mojo`
Spawning a glTF asset into the ECS: one entity per node (transform hierarchy) and one child entity per mesh primitive (`MeshRenderer`), plus static collision triangles for level geometry.
### struct `SpawnedGltf`
```mojo
struct SpawnedGltf(Movable)
```
Result of `spawn_gltf`: the root entity, the entity of each glTF node and of each mesh primitive, and the renderer ids of the upload. The entities stay in the world after this value is dropped.
Fields:
- `root`: Entity — Carries `root_transform`; every glTF root node is its child.
- `node_entities`: List[Entity] — Entity per glTF node (nodes outside the scene graph get the null entity).
- `primitive_entities`: List[Entity] — Every primitive entity, in node then primitive order.
- `uploaded`: UploadedGltf
- **`__init__`** `__init__(out self, root: Entity, var uploaded: UploadedGltf)`
- **`entity_of_node`** `entity_of_node(self, node: Int) -> Entity`
Entity spawned for glTF node `node`; the null entity for an out-of-range index or a node outside the scene graph.
### def `spawn_gltf`
```mojo
def spawn_gltf( mut renderer: Renderer, mut res: Resources, asset: GltfAsset, mut world: World, root_transform: Mat4 = Mat4.identity(), lods: Bool = True, cast_shadows: Bool = True, skinned_meshes: Bool = True, ) raises -> SpawnedGltf
```
Uploads the asset and spawns its node hierarchy. Skinned mesh nodes get a `MeshRenderer` too (the caller attaches an `AnimatorRef` so the animation system publishes their palette) unless `skinned_meshes` is False. Every node is spawned at most once, even when the file reaches it twice (a root that is also a child, a node shared by two parents, or a cycle); a node whose glTF parent has not been spawned yet hangs under `root`, so the `Parent` links never form a cycle.
### def `collision_triangles`
```mojo
def collision_triangles( asset: GltfAsset, root_transform: Mat4 = Mat4.identity() ) -> Tuple[List[Vec3], List[UInt32]]
```
World-space triangles of every non-skinned mesh node, for a static mesh collider.
## `cinder.scene.render_sync`
Source: `cinder/scene/render_sync.mojo`
Bridge from the ECS world to a `RenderScene`: render components and their collection. Components: `MeshRenderer` (renderer mesh and material indices, shadow flag), `LightSource`, `CameraComponent`. `build_render_scene` reads `WorldTransform` (propagated beforehand) and fills the renderer's scene lists in one pass; it allocates nothing in steady state because the scene's lists are cleared and refilled in place.
### struct `MeshRenderer`
```mojo
struct MeshRenderer(Component)
```
Draws renderer mesh `mesh` with material `material` at the entity's `WorldTransform`; `cast_shadows` is passed to the render instance.
Fields:
- `mesh`: Int
- `material`: Int
- `cast_shadows`: Bool
- **`component_id`** `component_id() -> Int`
### struct `LightSource`
```mojo
struct LightSource(Component)
```
A light positioned by the entity's world transform (direction = transform's -Z).
Fields:
- `kind`: LightKind
- `color`: Vec3
- `intensity`: Float32
- `range`: Float32
- `inner_angle`: Float32
- `outer_angle`: Float32
- `cast_shadows`: Bool
- **`component_id`** `component_id() -> Int`
### struct `CameraComponent`
```mojo
struct CameraComponent(Component)
```
A camera at the entity's world transform, looking down its -Z with its +Y up. `RenderBridge.build` uses the first `active` one it finds, keeping the scene camera's current aspect ratio.
Fields:
- `fov_y`: Float32
- `near`: Float32
- `active`: Bool
- **`component_id`** `component_id() -> Int`
### struct `JointPalette`
```mojo
struct JointPalette(Component)
```
Skinning matrices and morph weights for an entity's mesh, written by the animation system each frame and consumed by `build_render_scene`.
Fields:
- `joints`: List[Mat4]
- `morph_weights`: List[Float32]
- **`component_id`** `component_id() -> Int`
### struct `RenderBridge`
```mojo
struct RenderBridge(Movable)
```
ECS -> RenderScene with persistent query specs and chunk scratch (no steady-state allocations). `build_render_scene` is a convenience wrapper around a temporary instance.
Fields:
- **`__init__`** `__init__(out self)`
- **`build`** `build(mut self, world: World, mut scene: RenderScene) raises -> Int`
Fills `scene.instances`, `scene.joints` and `scene.lights` from the world and sets the camera from the first active `CameraComponent`. Returns the number of instances.
### def `build_render_scene`
```mojo
def build_render_scene(world: World, mut scene: RenderScene) raises -> Int
```
One-off bridge build (tests); the App keeps a `RenderBridge`.
---
> Cinder 0.1.0 documentation: cinder.tools API
> Canonical page: https://cinder.ash10.dev/docs/api/tools/ · Source: `docs/api/tools.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.tools`
## `cinder.tools`
Source: `cinder/tools/__init__.mojo`
Developer tools: the ImGui debug overlay (frame graph, inspector, assets, render graph, log).
## `cinder.tools.cstrings`
Source: `cinder/tools/cstrings.mojo`
Frame-scoped C strings for immediate-mode UI calls. ImGui keeps label pointers only until the frame is rendered, and Mojo `String`s move when a list grows (inline small-string storage), so labels are copied into `List[UInt8]` buffers whose heap data stays put. Call `clear` once per frame after rendering.
### struct `CStrings`
```mojo
struct CStrings(Movable)
```
Owns NUL-terminated byte copies of strings handed to C; pointers from `c` stay valid until `clear`.
Fields:
- **`__init__`** `__init__(out self)`
- **`clear`** `clear(mut self)`
Frees every buffer; pointers returned by `c` become dangling.
- **`__len__`** `__len__(self) -> Int`
- **`c`** `c( mut self, text: String ) -> Optional[Pointer[c_char, MutUntrackedOrigin]]`
A NUL-terminated copy of `text`, valid until `clear`.
## `cinder.tools.debug_ui`
Source: `cinder/tools/debug_ui.mojo`
The ImGui debug overlay: frame-time graph and render stats, entity inspector (view and edit components through the scene registry), asset browser, render graph view and log console. ImGui runs through the shim's SDL3 + WebGPU backends (`cn_imgui_*`); widgets are the generated dcimgui bindings. The overlay draws into the frame's final render pass after the scene.
- `comptime HISTORY = 240` — Frames kept in the frame-time graph.
### struct `DebugUi`
```mojo
struct DebugUi(Movable)
```
The ImGui debug overlay. Construction initialises the shim's ImGui backend for the window and device (raising when that fails); destruction shuts it down. `App` creates it for windowed apps with `debug_ui` set and declares it first, so it is destroyed before the window and GPU device. Per frame: `begin_frame`, `draw`, then `render` into the final pass.
Fields:
- `lib`: NativePtr
- `frame_ms`: List[Float32] — Ring of the last `HISTORY` frame times in milliseconds.
- `frame_cursor`: Int
- `frames`: Int
- `selected`: Int — Index into the last entity snapshot (-1 = none).
- `entities`: List[Entity]
- `edits_applied`: Int
- `visible`: Bool
- `requested_tab`: Int — Tab to force-select on the next frame (-1 = none): 0 Frame, 1 Entities, 2 Assets, 3 Render graph, 4 Log.
- **`__init__`** `__init__( out self, lib: NativePtr, window: Optional[Pointer[SDL_Window, MutUntrackedOrigin]], device: WGPUDevice, format: WGPUTextureFormat, ) raises`
- **`begin_frame`** `begin_frame(mut self, dt_seconds: Float64)`
Records `dt_seconds` in the frame-time ring, frees the previous frame's labels and starts a new ImGui frame.
- **`render`** `render(self, pass_: RenderPass)`
Draws the accumulated ImGui data into an open render pass.
- **`wants_mouse`** `wants_mouse(self) -> Bool`
True when ImGui wants the mouse (`App` then keeps mouse events from the input map); False when ImGui has no IO.
- **`wants_keyboard`** `wants_keyboard(self) -> Bool`
True when ImGui wants the keyboard (`App` then keeps key events from the input map); False when ImGui has no IO.
- **`average_frame_ms`** `average_frame_ms(self) -> Float32`
Mean frame time in ms over the recorded frames (at most `HISTORY`); 0 before the first frame.
- **`draw`** `draw( mut self, mut world: World, registry: SceneRegistry, renderer: Renderer, assets: Optional[Pointer[AssetServer, MutUntrackedOrigin]], logs: List[LogRecord], physics_live: Int, audio_live: Int, ) raises`
Builds the overlay for this frame (between `begin_frame` and `render`).
- **`select_tab`** `select_tab(mut self, index: Int)`
Makes tab `index` the active one on the next `draw`.
---
> Cinder 0.1.0 documentation: Deliverable report
> Canonical page: https://cinder.ash10.dev/docs/report/ · Source: `REPORT.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: deliverable report
Living document. Every claim below was verified in this repository under the pinned toolchain on the
date given; nothing is reported from memory or intention. Phases are those of the mission brief.
## 1. What was built (and what was not)
| Area | State | Where |
|---|---|---|
| Toolchain pins, container, CI skeleton | Done | `pixi.toml`, `docker/`, `.github/workflows/ci.yml`, `docs/TOOLCHAIN.md` |
| Mojo 1.2.0 notes and banned legacy forms, with CI lint | Done | `docs/MOJO_NOTES.md`, `tools/lint_legacy_mojo.sh` |
| Architecture and ADRs 0001 to 0007 | Done | `docs/ARCHITECTURE.md`, `docs/adr/` |
| Vendored dependencies (14 submodules, exact pins, licences) | Done | `third_party/`, `THIRD_PARTY_NOTICES.md` |
| Native shim `libcinder_native` (C11): lock-free MPMC event queue, wgpu async/log callbacks, SDL surface + log capture, Jolt contact and activation listeners, miniaudio engine/sound ownership and callbacks, ImGui backends behind a C ABI | Done for Phase 1 scope | `native/` |
| Native build (CMake + Ninja): SDL3, wgpu-native via cargo, joltc + Jolt 5.6, miniaudio, cgltf, Dear ImGui + dcimgui, layout printer, CTest, ASan/UBSan configuration | Done | `native/CMakeLists.txt` |
| Bindings generator (libclang) producing Mojo 1.2 bindings, C layout printer and Mojo layout test from one parse | Done | `tools/bindgen/cinder_bindgen.py`, `cinder/ffi/`, `native/layout/`, `tests/ffi/` |
| Phase 1 spikes 1 to 9 | Done on Linux | `spikes/` |
| Phase 2 math library (Vec2/3/4, Quat, Mat4; RH, Y up, -Z forward, column-major, reverse-Z infinite) with reference-checked property tests | Done | `cinder/core/math/`, `tests/core/test_math.mojo` |
| Phase 2: logging, assertions, linear arena, generational pool and handles, ring buffer, clock and fixed step, fs, work-stealing job system (ADR-0008) | Done, tested (29 tests) | `cinder/core/`, `tests/core/` |
| Phase 3 platform layer: SDL3 windows, pixel-size/resize, event translation (keyboard, mouse, wheel, window, gamepad), surface creation through the shim, SDL log capture | Done, tested on X11 and Wayland | `cinder/platform/`, `tests/platform/` |
| Phase 3 gfx layer: device + diagnostics, pooled typed resources, shader manager with located errors and hot reload, typed pipelines/bind groups/vertex layouts, command encoding and passes, swapchain with resize/minimise/loss handling, debug draw (ADR-0009) | Done, tested (11 tests incl. pixel readback) | `cinder/gfx/`, `tests/gfx/`, `docs/adr/0009-gfx-layer.md` |
| Phase 4 renderer: frame graph, WGSL assembly with hot reload, forward+ clustered lighting, cascaded + atlas shadows, GPU-baked IBL, GTAO, bloom, AgX/ACES, MSAA, FXAA, instancing, meshoptimizer LODs, GPU skinning and morph targets, debug views (ADR-0010) | Done, tested on lavapipe (pixel tests + 16 golden images) | `cinder/render/`, `tests/render/`, `tests/golden/`, `docs/RENDER_VALIDATION.md` |
| glTF 2.0 import (meshes, tangents via MikkTSpace, materials with KHR_texture_transform and KHR_materials_emissive_strength, images from files/data URIs/GLB, nodes, skins, morph targets) | Done, tested | `cinder/assets/gltf.mojo`, `tests/assets/` |
| Phase 5 ECS: archetype world with tight type-erased columns, compile-time typed queries (with/without filters), change ticks, command buffers, Parent/Local/World transform hierarchy, access-declared system schedule on the job system; benchmarked against flecs 4.2.0 through its iterator API and kept (ADR-0011) | Done, tested (5 tests + bench) | `cinder/ecs/`, `bench/ecs_bench.mojo`, `docs/adr/0011-ecs-mojo-vs-flecs.md` |
| Phase 5 asset pipeline: `cinder-cook` CLI (meshes optimised + LODs, materials, animation clips, KTX2 textures via libktx: ETC1S/UASTC BasisU, sRGB and normal-map aware), incremental by FNV-1a content hash with stable logical IDs and a manifest; runtime `AssetServer` (async loads on the job system, refcounting, shared instances, hot reload on file change, materials resolve their textures, BC7 transcode when the adapter has BC) | Done, tested (cook 4, textures 2, asset server 1, fuzz 3) | `cinder/assets/{cook,cooked,textures,runtime}.mojo`, `tools/cinder_cook.mojo`, `tests/assets/` |
| Phase 5 scene: diffable text format (exact float round trip) and binary format, component registry with versioned migrations, ECS to RenderScene bridge (MeshRenderer, LightSource, CameraComponent) | Done, tested (5 + 1) | `cinder/scene/`, `tests/scene/` |
| Phase 6 physics: Jolt world with bit layers, every main shape plus mesh/convex/scaled colliders, static/kinematic/dynamic bodies, sensors, continuous collision, queued contact and activation events, ray and shape casts with layer masks and body ignore, CharacterVirtual controller (steps, slopes, jumps, pushing, moving platforms), ECS sync with interpolation, deterministic replay (ADR-0012) | Done, tested (5 tests, also under ASan) | `cinder/physics/`, `native/src/physics_shim.c`, `tests/physics/` |
| Phase 6 animation: skeletons from glTF skins and cooked `.cskel`, step/linear/cubic-spline sampling, phase-synced 1D and 2D blend trees, crossfades, animation events, root motion, one-job-per-animator system feeding `JointPalette` to the render bridge | Done, tested (4 tests incl. a skinned animated render) | `cinder/anim/`, `tests/anim/` |
| Phase 6 audio: miniaudio buses, decoded and streamed sounds, one-shots, 3D attenuation and panning, ECS sources, listener following the camera entity, headless engine for tests | Done, tested (2 tests) | `cinder/audio/`, `native/src/audio.c`, `tests/audio/` |
| Phase 6 input: action/axis mapping over keyboard, mouse and gamepad, dead zones, runtime rebinding by listening, exact text save/load | Done, tested (3 tests) | `cinder/input/`, `tests/input/` |
| Phase 7 runtime: `App` owning every subsystem (heap-boxed for stable pointers), fixed-step `Game` loop, windowed and `--headless` modes (offscreen render + readback), engine CLI options (ADR-0013) | Done, tested (2 tests, also under ASan) | `cinder/app/`, `cinder/core/boxed.mojo`, `tests/app/` |
| Phase 7 tools: ImGui debug overlay with frame-time graph and render stats, entity inspector (edits through the scene registry), asset browser, render-graph view, log console; Tracy client with manual lifetime and RAII zones | Done, tested (windowed overlay test on X11 and Wayland; profiler test) | `cinder/tools/`, `cinder/core/profile.mojo`, `native/src/profile.c`, `tests/tools/`, `tests/core/test_profile.mojo` |
| Phase 8 sample game: third-person game built only on public engine APIs (skeletal KayKit character on `CharacterController`, idle/walk/run blend tree and jump state, procedural arena and Khronos Sponza with a Jolt mesh collider, 32 orbiting shadowed point lights plus a sun and IBL, 500+ dynamic crates, collectables, win and restart, 3D effects and music on buses, pause menu with settings saved to disk, scene-text save/load, autopilot), CC0 assets fetched with pinned hashes (ADR-0014) | Done, tested (headless playthrough with save/load and restart; glTF spawn) | `sample/`, `cinder/scene/gltf_spawn.mojo`, `cinder/assets/gltf.mojo` (`upload_gltf`), `tools/fetch_game_assets.py`, `tests/sample/`, `tests/scene/test_gltf_spawn.mojo` |
| Phase 8 performance: `bench/stress` (50k instances, 2,000 bodies, 256 lights) and `bench/sample_bench` writing JSON, `tools/bench_check.py` with adapter-scoped baselines and a 10% regression gate, allocation counter library and the zero-allocations-per-frame test, engine refactored to persistent scratch (ByteWriter reset, cached query specs and chunk lists, recycled frame-graph records, stack arrays for cascades and frustum corners, cached post-process and GTAO bind groups, lazy frame-graph DOT) | Done; budgets measured only on lavapipe (section 4) | `bench/`, `tools/bench_check.py`, `native/src/alloc_counter.c`, `tests/perf/test_zero_alloc.mojo`, `docs/BENCHMARKS.md` |
| Phase 9 documentation: README quick start, sample and benchmark sections; tutorial built on a compiled example game; one guide per subsystem; API reference generated from docstrings with a coverage gate (every public symbol documented); benchmark methodology; changelog; doc-name checker | Done; release tag pending the defect sweep and final gate | `README.md`, `docs/TUTORIAL.md`, `examples/first_game/`, `tests/examples/`, `docs/guides/`, `docs/api/`, `tools/gen_api_docs.py`, `tools/check_doc_names.py`, `docs/BENCHMARKS.md`, `CHANGELOG.md` |
## 2. Pinned version matrix
| Component | Version |
|---|---|
| Mojo | 1.2.0 (build 2016d0cd) |
| pixi | v0.63.2 |
| Rust (wgpu-native build only) | 1.93.0 |
| CMake / Ninja | >=3.28,<4 / >=1.11 (conda-forge) |
| wgpu-native | v29.0.1.1 (webgpu-headers 673658b) |
| SDL3 | release-3.4.20 |
| joltc / Jolt Physics | 2bbcf73 / v5.6.0 |
| cgltf | v1.15 |
| miniaudio | 0.11.26 |
| Dear ImGui / Dear Bindings | v1.93.0-docking / v0.27 |
| stb, KTX-Software, meshoptimizer, MikkTSpace, Tracy, flecs | 4e9b368, v4.4.2, v1.3, 3e895b4, v0.15.0, v4.2.0 (all built into the shim) |
| Linux toolchain image | Ubuntu 24.04, clang 18, Mesa lavapipe, Xvfb, Weston |
## 3. Test results (Linux x86-64, lavapipe, 2026-11-09)
Three consecutive full runs of `tools/run_tests.sh` and `spikes/window/run_window_spike.sh`, identical
results each time (no flakes):
| Program | Tests | Result |
|---|---|---|
| `tests/ffi/test_layout_cinder_native.mojo` | 892 (size, alignment, every field offset of every record incl. flecs and libktx; every enum and flag constant) | 892 pass |
| `spikes/ffi/test_ffi_spikes.mojo` (spikes 3, 4, queue contract) | 5 | 5 pass |
| `spikes/wgpu_compute/test_compute.mojo` (spike 2) | 1 | pass |
| `spikes/physics/test_physics.mojo` (spike 5) | 1 | pass |
| `spikes/audio/test_audio.mojo` (spike 6) | 1 | pass |
| `spikes/gltf/test_gltf.mojo` (spike 7) | 2 | 2 pass |
| `spikes/window/test_window.mojo` (spike 1) | 1 | pass on X11 (Xvfb) and on Wayland (Weston headless) |
| `spikes/imgui/test_imgui.mojo` (spike 8) | 1 | pass on X11 and on Wayland |
| `tests/core/test_math.mojo` | 12 (500 random cases per property) | 12 pass |
| `tests/core/test_core.mojo` | 12 (handles, pool, arena, ring, time, fs, log, abort assertions) | 12 pass |
| `tests/core/test_jobs.mojo` | 5 (parallel sum, 100k stolen jobs exactly once, saturation, zero workers) | 5 pass, also against the ASan+UBSan shim |
| `tests/gfx/test_device.mojo` | 1 (device creation, validation error captured and counted) | pass |
| `tests/gfx/test_resources.mojo` | 2 (typed handles, live counts, upload, stale handle rejection, leak accounting at shutdown) | 2 pass |
| `tests/gfx/test_shaders.mojo` | 2 (compile error with `path:line:col`, hot reload keeps last good module) | 2 pass |
| `tests/gfx/test_render.mojo` | 1 (compute pass doubles a uniform, triangle drawn offscreen, pixel readback exact) | pass |
| `tests/gfx/test_debug_draw.mojo` | 2 (lines on expected pixels under identity and translated cameras; buffer growth bounded) | 2 pass |
| `tests/platform/test_platform.mojo` | 2 (window create/resize/surface; injected SDL events translated) | 2 pass on X11 and on Wayland |
| `tests/platform/test_swapchain.mojo` | 1 (configure, 3 presented frames, resize, minimise skip, restore, cleared frame read back) | pass on X11 and on Wayland |
| `tests/core/test_image.mojo` | 3 (PNG round trip, decode from memory, SSIM behaviour) | 3 pass |
| `tests/render/test_render_core.mojo` | 5 (camera/frustum incl. SIMD culling, primitives, MikkTSpace tangents, mips, material upload) | 5 pass |
| `tests/render/test_frame_graph.mojo` | 2 (ordering independent of declaration order, culling, lifetimes, aliasing, pooling, resize, cycle detection) | 2 pass |
| `tests/render/test_shader_programs.mojo` | 5 (all 8 WGSL programs compile; include errors map to file:line; cluster slicing; cascade splits and texel snapping; atlas allocation) | 5 pass |
| `tests/render/test_renderer.mojo` | 3 (lit scene: sky, shadows proven by differencing, bloom peak, determinism; MSAA+FXAA+ACES, debug modes, LODs, wireframe, feature toggles; resize) | 3 pass |
| `tests/assets/test_gltf_loader.mojo` | 3 (base64; skinned fixture incl. computed normals/tangents, joints, render; malformed files raise) | 3 pass |
| `tests/golden/test_golden_gltf.mojo` | 16 Khronos models at 512x512 (SSIM >= 0.985, <= 0.5% pixels differ by > 8/255) | 16 pass against the reviewed Linux references |
| `tests/ecs/test_ecs.mojo` | 5 (spawn/despawn/add/remove across archetypes, typed queries and filters, change ticks, command buffers with placeholders, hierarchy propagation, scheduled systems on jobs) | 5 pass |
| `tests/scene/test_scene_format.mojo` | 5 (text and binary exact round trip, float formatting, migrations, malformed input rejected) | 5 pass |
| `tests/scene/test_render_sync.mojo` | 1 (ECS components to RenderScene with world transforms) | pass |
| `tests/assets/test_cook.mojo` | 4 (cooked mesh/material/clip formats round trip, glTF cook incl. KTX2 textures, incremental skip by content hash, manifest) | 4 pass |
| `tests/assets/test_textures.mojo` | 2 (ETC1S and UASTC cook, load, full mip chain, colour within tolerance, BC7 transcode and upload validation clean; corrupt KTX2 rejected) | 2 pass |
| `tests/assets/test_fuzz.mojo` | 3 (mutated glTF, cooked mesh and scene files never crash; every failure is a raised error) | 3 pass |
| `tests/assets/test_asset_server.mojo` | 1 (async loads on jobs, shared refcounts, material pulls its textures and renders, release order, hot reload on file change) | pass |
| `tests/physics/test_physics_world.mojo` | 5 (layers, sensors, kinematic platform, events; ray/shape casts, mesh and hull colliders; character controller over step, ramp, jump, wall, crate, moving platform; ECS sync with interpolation; bit-identical replay of 72 bodies) | 5 pass, also under ASan+UBSan |
| `tests/anim/test_animation.mojo` | 4 (step/linear/cubic sampling and loops; Fox skeleton cooked round trip, 1D/2D blend weights, phase sync, crossfade; events and root motion incl. loop wraps; 16 animators on jobs bit-identical to serial, CesiumMan rendered through the ECS bridge) | 4 pass |
| `tests/audio/test_audio_engine.mojo` | 2 (buses, left/right panning, distance attenuation, muted bus, streamed loop, one-shot end event and release, stale handles; ECS sources and camera listener) | 2 pass |
| `tests/input/test_input_map.mojo` | 3 (edges per frame, digital and analog axes with dead zones, listening rebinds, exact save/load) | 3 pass |
| `tests/core/test_profile.mojo` | 1 (Tracy no-ops while stopped, start/stop lifecycle, zones, plots, messages, frame marks) | pass, also under ASan |
| `tests/app/test_app_headless.mojo` | 2 (headless game: 45 fixed steps, physics drives an entity, offscreen frame visible, no validation errors, zero live objects at shutdown; CLI option parsing) | 2 pass, also under ASan |
| `tests/tools/test_debug_ui.mojo` | 1 (windowed app with the overlay: six frames, inspector snapshot through the registry, no validation errors) | pass on X11 and on Wayland |
| `tests/scene/test_gltf_spawn.mojo` | 1 (KayKit character, BoxTextured and Avocado spawned into one world: an entity per glTF node, a primitive entity per mesh part, skeleton and 30 clips read, collision triangles extracted, six instances render validation clean) | pass |
| `tests/sample/test_sample_game.mojo` | 2 (arena autopilot: crates, lights, collectables and controller spawned, wins after real travel time with zero validation errors and a visible frame, mid-game save then reload restores collected state, restart clears it, music and effects buses alive, no leaks; settings text round trip) | 2 pass |
| `tests/examples/test_first_game.mojo` | 1 (the tutorial game headless: the ball pushes every crate off the ledge with the move-axis force, win detected, restart resets, frame read back, no validation errors, every body released) | see the final gate below |
| `tests/perf/test_zero_alloc.mojo` | 1 (300 static instances, 100 bodies, 32 lights + sun, animated skinned character: 90 warm-up frames, then 60 frames with zero allocations from Mojo code, counted through the LD_PRELOADed interposer) | pass (0 Mojo allocations; third-party libraries still allocate) |
| Native CTest `event_queue` (Release) | 1 | pass |
| Native CTest `event_queue` (Debug + ASan + UBSan) | 1 | pass, no sanitizer reports |
Gate items also verified: `tools/lint_legacy_mojo.sh` clean; `--Werror` on every Mojo test build; zero
wgpu validation errors, device-lost or warning-level log events in spikes 1, 2 and 8 (the tests fail on
any such event).
**macOS:** none of the above has been run on macOS. No Apple Silicon machine or runner was available
to this run (see Blockers). Every phase gate therefore remains half open.
## 4. Benchmark results
The reference hardware (Linux RTX 3060-class, Apple M2) is not available; the only GPU here is Mesa
lavapipe (llvmpipe, LLVM 20.1.2) inside Docker on the Windows host's CPU. Every number below is from
that configuration and is a regression baseline for it (`bench/baselines/*.json`, checked by
`tools/bench_check.py` with a 10% tolerance), not a statement about the section 8 budgets. Method:
`docs/BENCHMARKS.md`.
### Section 8 budgets
| Budget | Measured here (lavapipe) | Status |
|---|---|---|
| Sample p99 <= 16.6 ms at 1080p high | see `sample_arena` below | **Unmeasured on reference hardware**; far above on a CPU rasteriser |
| `bench/stress` >= 60 fps (50k instances, 2,000 bodies, 256 lights) | see `stress` below | **Unmeasured on reference hardware** |
| Physics step <= 4 ms, 2,000 bodies | 0.73 ms median, 2.34 ms p99 over 120 frames (Jolt, 2 worker threads, bodies falling and settling) | Met on this CPU |
| Cold start <= 2 s | stress 0.97 s; sample arena 1.55 s (includes the KayKit character and the environment bake); sample Sponza above 2 s (source glTF textures decoded in process) | Met for arena and stress on this host; Sponza not met |
| Zero allocations per steady-state frame | `tests/perf/test_zero_alloc`: 0 Mojo allocations over 60 frames | Met (asserted by the test) |
### Results (`pixi run bench-all`, 1920x1080, high preset, headless)
| Benchmark | Frames | Frame ms (median / p99) | Physics ms (median / p99) | Render submit ms (median / p99) | Start-up ms | Notes |
|---|---|---|---|---|---|---|
| `stress` (50,000 instances, 2,000 bodies, 256 lights) | 120 | 721.3 / 785.0 | 0.728 / 2.343 | 715.0 / 778.5 | 972 | 24257 instances drawn after culling in 17 draw calls; 0 GPU errors |
| `sample_arena` (high preset) | 300 | 294.6 / 356.9 | 0.007 / 0.011 | 294.2 / 356.4 | 1548 | autopilot collected 4; 0 GPU errors |
Adapter for both: `llvmpipe (LLVM 20.1.2, 256 bits)`. Render time is almost all of frame time in both runs, and
on this adapter the GPU work is software rasterisation of 1080p on the same CPU, so these numbers
say nothing about the GPU budgets. The engine's CPU-only share of a frame was not isolated here
(a Tracy capture on reference hardware would). The stress scene's physics p99 includes the frames in which the
falling boxes first touch the floor and each other. The sample's bodies are mostly asleep, so its
physics time is microseconds.
ECS vs flecs (`pixi run bench-ecs`: 100,000 entities, two archetypes, 100 frames of
`position += velocity * dt` through a two-component query; flecs 4.2.0 through
`ecs_query_iter`/`ecs_query_next`/`ecs_field_w_size`), container on the Windows host:
| Run | Mojo ns / entity-iteration | flecs ns / entity-iteration | ratio |
|---|---|---|---|
| 1 | 0.758 | 0.829 | 0.91 |
| 2 | 0.746 | 0.750 | 1.00 |
| 3 | 0.728 | 0.744 | 0.98 |
Within the 10% band required to keep the Mojo ECS (ADR-0011).
## 5. Known limitations and Mojo workarounds
- **ASan `alloc_dealloc_mismatch` disabled in `tools/run_tests_asan.sh`.** KTX-Software at the
pinned commit allocates Basis supercompression global data with `new[]` (`lib/basis_encode.cpp`,
`ktxTexture2_CompressBasisEx`) and frees it with `free` (`lib/texture2.c`, `ktxTexture2_destruct`),
an alloc-dealloc mismatch inside the vendored library that only the in-process encoder path (the
cooker) hits. ASan cannot scope that check with a suppression file, so that single check is off
for the sanitizer runs; heap overflow, use-after-free, LeakSanitizer and UBSan stay on for every
library and all Mojo code. The submodule is not patched; revisit when the pin moves.
| Item | Reproducer | Workaround |
|---|---|---|
| A `comptime Array[StaticString, N]` literal with N in the low thousands exhausts the Mojo 1.2.0 compiler (OOM at 13 GB) | `bugs/comptime_array_literal_oom/` | Symbol names are one newline-joined string literal split at load time. |
| Untracked-origin pointers do not keep their pointee alive; ASAP destruction frees trivially destructible stack values before C reads them | documented in `docs/MOJO_NOTES.md` (language semantics, not a defect) | `std.benchmark.keep(value)` after the last C call; the safe layer owns descriptors in engine storage. |
| `OwnedDLHandle` is closed at its last syntactic use | `docs/MOJO_NOTES.md` | Function pointers live only inside the struct that owns the handle. |
| `Optional[def(...) thin abi("C")]` is 16 bytes | `docs/MOJO_NOTES.md` | Nullable C callback fields are bound as nullable opaque pointers. |
| wgpu-native 29.0.1.1 does not implement `wgpuInstanceWaitAny` | `third_party/wgpu-native/src/unimplemented.rs` | `cn_wgpu_pump` + event queue polling with a deadline (ADR-0003 amendment). |
| wgpu-native built from the git tag reports version 0 from `wgpuGetVersion` | `third_party/wgpu-native/Cargo.toml` (`version = "0.0.0"`) | Not relied upon. |
| Jolt 5.6 builds hair compute shaders with glslangValidator when Vulkan is on | `third_party/JoltPhysics/Build/CMakeLists.txt` | `JPH_USE_VK=OFF` (Jolt is CPU-only in Cinder, ADR-0002). |
| wgpu-native 29 aborts (Rust panic) on a validation error inside `wgpuQueueSubmit` instead of reporting it through the error callback | documented in `docs/MOJO_NOTES.md` | Copy strides are explicit and 256-aligned; resource-creation errors still arrive through the callback and are tested. |
| `wgpuShaderModuleGetCompilationInfo` is unimplemented in wgpu-native 29 (aborts) | `third_party/wgpu-native/src/unimplemented.rs` | Shader errors captured with a validation error scope and naga's `wgsl:line:col` report parsed (ADR-0009). |
| `mojo format` has no check mode | `pixi run mojo format --help` | Format in place, then `git diff --exit-code`. |
### Defect sweep before v0.1.0 (Phase 9)
Writing a docstring for every public symbol meant reading every public function, and that review
found real defects. Each was fixed at its root with a test that exercises it. Where the agent or
operator ran the new test against the old code, it failed there. The fixes, by area:
| Area | Fixed |
|---|---|
| gfx | Encoder labels reached no descriptor; passes and encoders dropped without `end`/submit were never released; rejected pipelines leaked and reported the wrong diagnostic; `adopt_*`/`create_*` leaked on a full pool and accepted null; imported views could be released by `Resources` (also at shutdown); unchecked buffer offsets; `read_buffer` left a buffer mapped on error; the shutdown leak count was unreadable (now a diagnostic record); `destroy_texture` left its views alive (now refused); `replace_*_pipeline` leaked the new object on a stale handle |
| render | Frame graph did not order early readers before a resource's second writer; `GpuMesh.index_count` disagreed between upload paths; more than 32 morphed instances reused slot 0's weights; only the first morphed mesh's delta buffer was bound per frame (one cached object group per morph buffer now; the `MorphPrimitivesTest` golden reference had captured this bug and was regenerated and re-reviewed, `docs/RENDER_VALIDATION.md`); overdraw and LOD debug views showed the wrong data; dropped lights kept shadow tiles; batch keys collided at 4,096 materials or 8 LODs; skin weights were not validated; `Renderer.replace_material`/`remove_material` added |
| core, platform, input, app | Mirrored `Transform.from_mat4` produced a wrong rotation; Tracy plot names dangled; out-of-range `Mat4`/`Image` indices, `pad_to(0)` and the zero quaternion's inverse are now checked or defined; `NativeLibrary` leaked on an ABI mismatch; UTF-8 text from C was double-encoded (shared `string_from_c`); negative binding codes indexed out of range; clearing a held action's bindings left it held; key and button releases were swallowed while the debug overlay had focus |
| ecs, scene, assets | Chunk access for a missing component read the wrong column (now a hard check); two types could share a component id; a failing `CommandBuffer.apply` double-destroyed payloads; `FieldReader.bool` skipped kind checks; non-ASCII names were mangled in scene and cooked formats; `spawn_gltf` duplicated nodes reachable twice and could recurse forever; `AssetServer` miscounted in-flight loads on early release, leaked renderer materials and texture references across reloads, returned records of the wrong kind and never retried failed loads; texture hot reload left materials binding a destroyed view; cooked sampler selection was order-dependent; the cook manifest kept stale entries |
| physics, anim, audio | Double or invalid `destroy_body`/`destroy_shape` corrupted leak counts; leaks at teardown were silent (now an error record and `leak_report()`); `AnimationSystem.update` behaved differently with and without jobs; a finished state could not be replayed (`play(..., restart=True)`); audio buses could not be destroyed (`destroy_bus`); blend trees accepted cycles; `write_transforms` did not skip invalid bodies |
| sample | No shutdown path: the game now releases its level, music, effects and both buses, and its test checks that nothing it made outlives shutdown; the jump animation could not replay |
Remaining limitations found in the same review, documented in the guides:
- Only the first directional light is used (`docs/guides/render.md`).
- Physics `destroy_shape` cannot tell a stale copy of a shape handle from a new shape that Jolt
allocated at the same address (`docs/guides/physics.md`).
- `NativeLibrary`'s leak fix has no automated test: the allocation counter does not export a
free count through the shim yet.
## 6. Deviations from the brief, each with its ADR
| Deviation | ADR |
|---|---|
| Own bindings generator instead of mojo-bindgen (its 0.3.4 output does not parse under 1.2.0; it needs Python 3.14 but no nightly Mojo, so that part of the concern was unfounded) | ADR-0005 |
| wgpu-native is built by cargo driven from CMake, not by CMake itself (it is Rust) | ADR-0004 |
| Development host is a Linux container on Windows; macOS verification deferred | ADR-0007 |
| stargine not forked | ADR-0006 |
| Mojo ECS kept over flecs after the mandated benchmark (parity, within the band) | ADR-0011 |
| Jolt steps on its own 2-thread pool rather than the engine job system (joltc cannot be driven without callbacks into Mojo) | ADR-0012 |
| Tracy is started on demand (`--profile`) rather than at process start, so tests and headless runs open no sockets | ADR-0013 |
## 7. Blockers escalated
1. **No macOS runner or machine.** Exact problem: every gate requires both platforms; no Apple Silicon
host was reachable from this session. Tried: none possible (Docker runs Linux only). Options:
(a) enable the `macos-14` job in `.github/workflows/ci.yml` by pushing to GitHub, (b) a local Apple
Silicon machine running the same pixi tasks, (c) accept Linux-only gates for now and batch macOS
verification later. Recommendation: (a), it is already written.
2. **No reference GPU hardware for section 8.** Same options; benchmarks will run on CI hardware for
regression tracking, but budget compliance cannot be claimed from lavapipe.
## 8. Exact commands
```bash
docker build -t cinder-toolchain:1 -f docker/Dockerfile.toolchain docker
tools/in_container.sh pixi install
tools/in_container.sh pixi run gen-dcimgui # Dear Bindings C API (committed)
tools/in_container.sh pixi run -e bindgen bindgen # cinder/ffi, layout printer, layout test (committed)
tools/in_container.sh pixi run native # Release shim + CTest + layout facts
tools/in_container.sh pixi run native-sanitize # Debug shim with ASan+UBSan + CTest
tools/in_container.sh pixi run test # lint, every Mojo test program, X11 + Wayland spikes
tools/in_container.sh pixi run format-check
```
Cooking assets and running the sample game: not available yet (Phases 5 and 8).
---
> Cinder 0.1.0 documentation: Status
> Canonical page: https://cinder.ash10.dev/docs/status/ · Source: `docs/STATUS.md` in the engine repository (engine commit 47c9352, imported 2026-11-09)
> Index of every page for agents: https://cinder.ash10.dev/llms.txt
# Status
Last updated: 2026-11-09. One row per phase; "verified" means executed in this repository under the pinned
toolchain, with the command named. Nothing here is claimed from memory.
| Phase | State | Verified | Open risks |
|---|---|---|---|
| 0 Reconnaissance | **Done on Linux**; macOS gate not runnable on this host | `pixi run mojo --version` = 1.2.0; `spikes/hello` runs and its TestSuite passes (2/2); all 14 third-party submodules checked out at the pinned refs via `tools/vendor_submodules.sh` | No macOS runner available (see Blockers). mojo-bindgen's generator pins a nightly (ADR-0005). |
| 1 Risk spikes | 9 of 9 pass on Linux (last full run recorded in REPORT.md); macOS not run | `tools/run_tests.sh` and `spikes/window/run_window_spike.sh` in the toolchain container. 1: SDL3 window + wgpu surface + triangle + resize, pixel-verified on X11 (Xvfb) and Wayland (Weston headless). 2: compute round trip with every callback through the shim queue. 3: 96-byte struct by value both ways. 4: Mojo `abi("C")` callback from the calling thread and a native thread. 5: Jolt world, queued contact and activation events, sleep, bit-identical rerun. 6: headless miniaudio engine, sine spatialised left/right/centre. 7: cgltf skinned fixture parsed, malformed input rejected. 8: dcimgui window drawn through the WebGPU backend, pixel-verified on both display servers. 9: layout test, 528 records and every enum/flag constant identical between the C compiler and Mojo. Native shim CTest green under ASan+UBSan. | wgpu-native 29 lacks `wgpuInstanceWaitAny` (ADR-0003 amendment). Mojo compiler OOM on large comptime array literals (`bugs/comptime_array_literal_oom`, worked around). macOS gate for every spike still open. |
| 2 Foundations | Core done on Linux: math, log, assertions, arena, pool/handles, ring, time, fs, job system; CI workflow written but not yet run on GitHub | `tests/core/test_math.mojo` (12), `test_core.mojo` (12), `test_jobs.mojo` (5, also against the ASan shim) all pass; `pixi run format-check`, `lint` clean | Containers beyond these (sparse sets, bitsets) arrive with the ECS. Benchmarks (`bench/`) not started. |
| 3 Platform + gfx | Done on Linux (X11 and Wayland); macOS not run | `cinder/platform`: SDL3 init, windows, event translation (keyboard, mouse, wheel, window, gamepad), surfaces; `cinder/gfx`: `Gfx` (instance/adapter/device/queue, diagnostics, error counting, device-lost flag), `Resources` (pooled generational handles for every wgpu object, leak accounting), `ShaderManager` (WGSL compile with `path:line:col` errors via validation scopes, hot reload with last-good fallback), typed pipeline layer (bind group layouts, bind groups, pipeline layouts, vertex layouts, render and compute pipelines), command layer (encoders, render/compute passes, copies, submit, readback), `Swapchain` (configure, acquire with outdated/lost recovery, minimise skipping, resize, present), `DebugDraw` (lines, boxes, spheres, axes, grids; bounded buffer growth). Tests: `tests/gfx/test_device` (1), `test_resources` (2), `test_shaders` (2), `test_render` (1, offscreen triangle + compute, pixel readback), `test_debug_draw` (2, pixel positions), `tests/platform/test_platform` (2) and `test_swapchain` (1) on X11 and Wayland. ADR-0009. | wgpu-native aborts the process on `wgpuQueueSubmit` validation errors (documented in MOJO_NOTES); text debug draw arrives with the ImGui tools layer (Phase 7). |
| 4 Renderer | Core done on Linux; macOS not run | `cinder/render`: camera + SIMD frustum culling, meshes with meshoptimizer LODs and morph buffers, glTF materials (KHR_texture_transform, emissive_strength), textures with mips, frame graph (ordering, culling, aliasing, external resources, DOT dump), WGSL `#include` assembly with file:line error mapping and hot reload, forward+ clustered lights (16x9x24 compute), 4 cascaded shadow maps with texel snapping and PCF, spot/point shadow atlas, GPU-baked IBL (prefiltered specular, irradiance, BRDF LUT), GTAO (single and MSAA depth), physically based bloom, AgX/ACES, exposure, 4x MSAA, FXAA, GPU skinning and morph targets, instancing, debug views (normals, cascades, clusters, overdraw, LOD, albedo, AO, wireframe). `cinder/assets/gltf.mojo` imports .gltf/.glb (files, data URIs, GLB buffers). Tests: `tests/render/*` (15), `tests/assets/test_gltf_loader` (3), `tests/golden/test_golden_gltf` (16 Khronos models, SSIM + pixel thresholds, references reviewed in `docs/RENDER_VALIDATION.md`). ADR-0010. | Golden references exist for Linux/lavapipe only (no Metal machine). GPU culling/indirect draws deferred to Phase 8; debug text waits for ImGui (Phase 7); device-loss recovery test still to add. |
| 5 Assets, ECS, scene | Done on Linux; macOS not run | `cinder/ecs`: archetype World (type-erased tight columns, 16-byte aligned), typed queries with with/without filters, change ticks, CommandBuffer, Parent/Local/World transforms with propagation, access-declared Schedule running stages in parallel on the job system; benchmarked at parity with flecs (ADR-0011). `cinder/scene`: text (diffable, exact floats) and binary formats with migrations; ECS -> RenderScene bridge. `cinder/assets`: glTF animations parsed, cooked mesh/material/clip formats, incremental content-hash cooker + `cinder-cook` CLI, AssetServer (async loads on jobs, refcounts, hot reload, materials resolving their textures), KTX2 cook/load through libktx (ETC1S/UASTC, BC7 transcode when supported). Tests: ecs (5), scene format (5), render sync (1), cook (4), textures (2), fuzz (3), asset server (1); `pixi run bench-ecs`. | GPU-side scene streaming and a scene editor wait for Phase 7 tools; cooked formats are v1 (migrations exist for scenes, cooked assets re-cook on format change). |
| 6 Physics, anim, audio, input | Done on Linux; macOS not run | `cinder/physics`: Jolt world (layers as bits, shapes incl. mesh/convex/scaled, static/kinematic/dynamic bodies, sensors, continuous motion, mass override), queued contact/activation events, closest-hit ray and shape casts with layer masks and body ignore (C collectors in the shim), `CharacterController` over CharacterVirtual (steps, slopes, jumps, pushing, moving platforms), ECS sync with interpolation, bit-identical replay of a recorded input stream (72 bodies). `cinder/anim`: skeletons from glTF skins + cooked `.cskel`, step/linear/cubic sampling, phase-synced 1D/2D blend trees, crossfades, events, root motion, one-job-per-animator system publishing `JointPalette` to the render bridge. `cinder/audio`: miniaudio buses, decoded/streamed sounds, one-shots released on their end event, 3D attenuation, ECS sources and a camera-following listener, headless engine for tests. `cinder/input`: action/axis map over keyboard, mouse, gamepad; listening rebinds; exact text save/load. Tests: physics (5), anim (4), audio (2), input (3), all also under ASan+UBSan. ADR-0012. | Jolt runs on its own 2-thread pool (ADR-0012); no device-backed audio test (headless only) until a machine with an audio device runs the suite; SIMD animation blending is per-transform SIMD (Vec3/Quat lanes), not batched across bones. |
| 7 Tools, profiling | Done on Linux; macOS not run | `cinder/app`: `App` runtime (subsystem ownership in `Boxed` cells, fixed-step `Game` loop, windowed with swapchain or `--headless` with offscreen readback, `parse_app_args`). `cinder/tools`: ImGui debug overlay (frame-time graph and render stats, entity inspector editing numeric fields through the scene registry, asset browser, render graph DOT view, log console). Tracy client with manual lifetime (`cinder/core/profile.mojo`, `native/src/profile.c`), zones in the app loop, renderer and asset server. Tests: `tests/core/test_profile` (1), `tests/app/test_app_headless` (2), `tests/tools/test_debug_ui` (1, X11 and Wayland). ADR-0013. | Built-in components are not yet `SceneComponent`s, so the inspector shows game-registered types only; Tracy capture verified only as a running client (no server attached in CI). |
| 8 Sample game, perf | Done on Linux; budgets unmeasured on reference hardware | `sample/`: third-person game over `App` (KayKit character on the character controller with idle/walk/run blend and jump, procedural arena and Sponza levels with mesh colliders, 32+ orbiting lights with shadows and IBL, 500+ crates, collectables, win and restart, 3D effects and music on buses, pause menu with settings saved to disk, text save/load, autopilot). `bench/stress` and `bench/sample_bench` write JSON; `tools/bench_check.py` fails on >10% regression against `bench/baselines/`. `tests/perf/test_zero_alloc` asserts zero Mojo allocations over 60 steady frames through an LD_PRELOADed allocation counter. Tests: gltf spawn (1), sample playthrough + save/load (2), zero alloc (1). ADR-0014. | Reference hardware (RTX 3060 Linux, Apple M2) is not available on this host; container GPU is lavapipe, so the section 8 budgets are recorded, not met (REPORT.md section 4). Sponza cold start is above 2 s here because the sample loads source glTF textures, not cooked KTX2. |
| 9 Docs, release | In progress on Linux | README quick start, sample and benchmark sections; `docs/TUTORIAL.md` built on `examples/first_game` (compiled and run headless by `tests/examples/test_first_game.mojo`); a guide per subsystem in `docs/guides/` (13); API reference generated from docstrings into `docs/api/` by `tools/gen_api_docs.py`, with `pixi run api-docs-check` failing the test task when a public symbol lacks a docstring (0 missing); `docs/BENCHMARKS.md`; `CHANGELOG.md`; `tools/check_doc_names.py` checks that names quoted in docs exist. | Defects found while documenting are being fixed before the v0.1.0 tag (REPORT.md section 5). README quick start not yet re-verified in a fresh container. |
## Blockers escalated to the operator
See `REPORT.md` → Blockers. Summary:
1. **No macOS/Metal machine or CI runner.** Every phase gate requires both platforms. Linux work proceeds;
macOS verification is pending a runner (GitHub Actions `macos-14` or a local Apple Silicon machine).
2. **No reference GPU hardware.** Section 8 budgets cannot be measured on lavapipe. Benchmarks are built
and run for correctness and regression tracking; budget compliance is unreported until hardware exists.
## Decisions log
- 2026-11-08 ADR-0001..0007 accepted (0005 proposed, decided by spike 9).
- 2026-11-08 ADR-0011 accepted: Mojo ECS kept after the flecs benchmark (ratios 0.91 / 1.00 / 0.98).
- 2026-11-09 ADR-0014 accepted: sample game as a `Game` over scene components, JSON benchmarks with adapter-scoped baselines, zero-allocation frame enforced by an interposed counter.
- 2026-11-09 ADR-0013 accepted: App runtime, headless mode, Tracy manual lifetime, data-driven ImGui overlay.
- 2026-11-09 ADR-0012 accepted: Phase 6 subsystem designs (Jolt thread pool, phase-synced blend trees, miniaudio buses, text input maps).
- 2026-11-08 Operator asked about forking stargine; declined, ADR-0006.
- 2026-11-08 ADR-0005 decided: own generator (mojo-bindgen output does not parse under 1.2.0).
- 2026-11-08 ADR-0011 accepted: keep the Mojo ECS (within the 10% band of flecs: ratios 0.91, 1.00, 0.98).
- 2026-11-08 ADR-0010 accepted: renderer architecture (forward+, frame graph, shader assembly, IBL bake, post chain).
- 2026-11-08 ADR-0009 accepted: gfx layer shape (pooled handles, error-scope shader diagnostics, typed pipeline layer).
- 2026-11-08 ADR-0003 amended: awaiting is a pump loop because wgpu-native 29.0.1.1 does not implement
`wgpuInstanceWaitAny`; spike 4 shows Mojo C callbacks work from native threads, pull model kept.
---
> Cinder 0.1.0 documentation: Changelog
> Canonical page: https://cinder.ash10.dev/docs/changelog/ · Source: `CHANGELOG.md` in the engine repository (engine commit 47c9352, imported 2026-11-09)
> Index of every page for agents: https://cinder.ash10.dev/llms.txt
# Changelog
All notable changes to Cinder. The format follows Keep a Changelog; versions follow semantic
versioning. Dates are the day the phase gate passed on Linux x86-64 (`REPORT.md` has the runs).
## [0.1.0] - 2026-11-09
First release: a complete Linux x86-64 engine with a playable sample. macOS arm64 is a declared
target but has not been built or run (no machine or runner was available; see `REPORT.md`,
Blockers).
### Added
- **Build and toolchain.** pixi environment pinning Mojo 1.2.0, CMake/Ninja and Python; one C11
shim `libcinder_native` over pinned submodules (wgpu-native v29.0.1.1, SDL release-3.4.20,
Jolt Physics v5.6.0 via joltc, cgltf v1.15, stb, KTX-Software v4.4.2, meshoptimizer v1.3,
MikkTSpace, miniaudio 0.11.26, Dear ImGui v1.93.0-docking via Dear Bindings v0.27, Tracy
v0.15.0, flecs v4.2.0 for the ECS benchmark; exact commits in `REPORT.md`); generated Mojo bindings with a layout test covering every struct offset and enum
constant; ASan/UBSan shim build and test runner; Docker toolchain container.
- **Core** (`cinder/core`): logging with sinks, assertions, arena and generational handle pools,
rings, SIMD vector/matrix/quaternion math, work-stealing job system with per-frame graphs,
time, filesystem, PNG images, Tracy profiler zones, heap cells with stable addresses.
- **Platform** (`cinder/platform`): SDL3 windows, event translation for keyboard, mouse, wheel,
window and gamepad, surface creation for X11, Wayland and Metal, ImGui event forwarding.
- **Gfx** (`cinder/gfx`): typed wgpu layer with pooled generational handles and leak accounting,
WGSL shader manager with `path:line:col` diagnostics and hot reload with last-good fallback,
pipelines, command recording, swapchain with resize and loss recovery, debug draw.
- **Renderer** (`cinder/render`): frame graph (ordering, culling, transient aliasing, external
resources, DOT dump), clustered forward+ lighting, four cascaded shadow maps and a spot/point
shadow atlas, GPU-baked image-based lighting, GTAO, physically based bloom, AgX and ACES tone
mapping, exposure, 4x MSAA, FXAA, GPU skinning and morph targets, instancing, meshoptimizer
LODs, debug views; zero heap allocations per steady-state frame.
- **Assets** (`cinder/assets`): glTF 2.0 import (files, GLB, data URIs, KHR_texture_transform,
KHR_materials_emissive_strength, skins, animations), cooked mesh/material/clip/skeleton
formats, incremental content-hash cooker and `cinder-cook` CLI, KTX2 textures through libktx
(ETC1S/UASTC, BC7 transcode), `AssetServer` with async loads, refcounts and hot reload.
- **ECS** (`cinder/ecs`): archetype world with tightly packed columns, typed queries with
with/without filters and cached query specs, change ticks, command buffers, parent/local/world
transforms with propagation, access-declared schedules running stages in parallel on the job
system; measured within 10% of flecs (ADR-0011).
- **Scene** (`cinder/scene`): diffable text format with exact floats and a binary format, a
component registry with versioned migrations, the ECS to render-scene bridge, glTF spawning
into the world with collision triangle extraction.
- **Physics** (`cinder/physics`): Jolt world with bit layers, box/sphere/capsule/cylinder/convex
hull/mesh/scaled shapes, static/kinematic/dynamic bodies, sensors, continuous collision, queued
contact and activation events, ray and shape casts, `CharacterController` over
CharacterVirtual (slopes, steps, moving platforms, pushing), ECS sync with interpolation,
deterministic replay.
- **Animation** (`cinder/anim`): skeletons from glTF and `.cskel`, step/linear/cubic sampling,
phase-synchronised 1D and 2D blend trees, crossfades, events, root motion, one job per animator.
- **Audio** (`cinder/audio`): miniaudio buses, decoded and streamed sounds, one-shots, 3D
attenuation, ECS sources and a camera-following listener, headless engine for tests.
- **Input** (`cinder/input`): action/axis maps over keyboard, mouse and gamepad, dead zones,
runtime rebinding, exact text save/load.
- **App** (`cinder/app`): the `App` runtime and `Game` trait, fixed-step loop with interpolated
rendering, windowed and `--headless` modes with frame readback, shared CLI options, per-phase
timing and allocation accounting.
- **Tools** (`cinder/tools`): ImGui debug overlay with frame-time graph, entity inspector editing
through the scene registry, asset browser, render-graph view and log console; Tracy client with
manual lifetime.
- **Sample game** (`sample/`): third-person collect-them-all with a skeletal character on the
character controller, arena and Sponza levels, 32 orbiting shadowed lights, 500+ crates, 3D
audio and music, pause menu with saved settings and rebindable keys, save/load, autopilot.
CC0 assets fetched with pinned hashes.
- **Tutorial game** (`examples/first_game`): the code built in `docs/TUTORIAL.md`, run headless
by its own test.
- **Benchmarks** (`bench/`): stress scene (50k instances, 2,000 bodies, 256 lights), sample game,
ECS vs flecs; JSON results, adapter-scoped baselines and a 10% regression gate
(`tools/bench_check.py`); allocation counter library and the zero-allocation test.
- **Documentation**: README quick start, `docs/ARCHITECTURE.md`, `docs/THREADING.md`,
`docs/TOOLCHAIN.md`, `docs/MOJO_NOTES.md`, `docs/RENDER_VALIDATION.md`, `docs/BENCHMARKS.md`,
`docs/TUTORIAL.md`, a guide per subsystem in `docs/guides/`, the generated API reference in
`docs/api/`, fourteen ADRs, `docs/STATUS.md` and `REPORT.md`.
### Quality gates
- Every public symbol has a docstring (`pixi run api-docs-check`, part of `pixi run test`) and
every code name quoted in the guides exists (`pixi run docs-check`).
- A pre-release review of every public function fixed about 60 defects across all subsystems,
each with a test (`REPORT.md`, section 5).
### Known limitations
- Only the first directional light is used.
- Not built or run on macOS; golden images exist for lavapipe only; the section 8 budgets are
recorded on lavapipe, not met on reference hardware (none available).
- The sample's Sponza level loads source glTF textures and starts in more than two seconds.
- KTX-Software's vendored allocator mismatch is tolerated under ASan
(`alloc_dealloc_mismatch=0`); see `REPORT.md`.
- Built-in components are not `SceneComponent`s, so the inspector edits game-registered types only.
---
> Cinder 0.1.0 documentation: Engine README
> Canonical page: https://cinder.ash10.dev/docs/readme/ · Source: `README.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
A 3D game engine written in Mojo 1.2 over a single C11 native shim (wgpu-native, SDL3, Jolt, miniaudio,
cgltf, Dear ImGui). Targets Linux x86-64 (Vulkan) and macOS Apple Silicon (Metal).
**Project state:** Phase 0 (reconnaissance, pins, architecture) and Phase 1 (nine risk spikes) are
complete and verified on Linux. The macOS half of every gate has not been run: no Apple hardware or
runner was available. Phases 2 to 9 (engine proper) have not started. See
[docs/STATUS.md](https://cinder.ash10.dev/docs/status.md) for exactly what has been verified and [REPORT.md](https://cinder.ash10.dev/docs/report.md) for the
deliverable report.
## Quick start (Linux, or any host with Docker)
```bash
git clone --recursive cinder && cd cinder
docker build -t cinder-toolchain:1 -f docker/Dockerfile.toolchain docker
tools/in_container.sh pixi install
tools/in_container.sh pixi run -e bindgen bindgen # regenerate cinder/ffi from the C headers
tools/in_container.sh pixi run native # build the native shim, run its CTest suite
tools/in_container.sh pixi run test # lint, layout test, every spike (incl. X11 + Wayland)
```
On a native Linux or macOS host with [pixi](https://pixi.sh) installed, drop the `tools/in_container.sh`
prefix (the windowed spikes then need a display, or Xvfb/Weston as the script starts them). The Windows
host used for development runs everything through the container (ADR-0007); on Windows set
`CINDER_HOST_ROOT` to the repository's real path if it is reached through a substituted drive.
## The sample game
```bash
tools/in_container.sh pixi run sample # windowed: arena level, WASD/Space, Esc for the menu
tools/in_container.sh pixi run sample -- --level sponza # the Khronos atrium with a mesh collider
tools/in_container.sh pixi run sample-headless # offscreen autopilot run (tests, CI, benchmarks)
```
`sample/main.mojo` accepts `--headless`, `--level arena|sponza`, `--autopilot`, `--frames N`,
`--screenshot PATH`, `--verbose` and the engine options from `cinder.app.parse_app_args`. Assets
are CC0 and fetched by `tools/fetch_game_assets.py` (attribution in `assets/game/ATTRIBUTION.md`).
Settings, key bindings and saves land in `.cache/sample/`.
## Benchmarks
```bash
tools/in_container.sh pixi run bench-all # stress scene + sample game headless, JSON in .cache/bench, baseline check
tools/in_container.sh pixi run test-zero-alloc # asserts zero Mojo allocations per steady-state frame
```
`tools/bench_check.py --update` records new baselines in `bench/baselines/`. Numbers are only
comparable on the same adapter; see REPORT.md for the measured results and the budgets.
## Layout
| Path | Contents |
|---|---|
| `cinder/ffi/` | Generated raw bindings (one module, one library struct). Never edited by hand. |
| `native/` | `libcinder_native` C11 shim, the ImGui C++ shim, CMake build of all vendored libraries, layout printer. |
| `third_party/` | Vendored dependencies as pinned git submodules. Licences in [THIRD_PARTY_NOTICES.md](https://cinder.ash10.dev/docs/third-party-notices.md). |
| `spikes/` | Phase 1 risk spikes, each a TestSuite program; `spikes/common` holds their shared helpers. |
| `tests/` | Every test suite (core, gfx, render, golden images, ECS, scene, assets, physics, anim, audio, input, app, tools, sample, perf). |
| `tools/` | Container runner, vendoring, bindings generator, lint, test drivers, asset fetchers, benchmark checker. |
| `sample/` | The sample game (a `Game` over `cinder.app.App`). |
| `bench/` | Benchmarks (stress scene, sample game, ECS vs flecs) and recorded baselines. |
| `bugs/` | Minimal reproducers for toolchain defects met, with the workaround used. |
| `docs/` | Toolchain, Mojo notes, architecture, ADRs, status. |
| `docker/` | Toolchain image. |
## Documentation
- [docs/TOOLCHAIN.md](https://cinder.ash10.dev/docs/toolchain.md): exact pinned versions.
- [docs/MOJO_NOTES.md](https://cinder.ash10.dev/docs/mojo-notes.md): Mojo 1.2.0 idioms used, lifetime rules learned the hard way, banned legacy forms.
- [docs/ARCHITECTURE.md](https://cinder.ash10.dev/docs/architecture.md): module map, C boundary, threading model.
- [docs/adr/](https://cinder.ash10.dev/docs/adr.md): architecture decision records.
## Licence
Apache-2.0. See LICENSE.
---
> Cinder 0.1.0 documentation: Third-party notices
> Canonical page: https://cinder.ash10.dev/docs/third-party-notices/ · Source: `THIRD_PARTY_NOTICES.md` in the engine repository (engine commit 47c9352, imported 2026-11-09)
> Index of every page for agents: https://cinder.ash10.dev/llms.txt
# Third-party notices
Every dependency is vendored as a git submodule under `third_party/` at the exact ref below and built
from source by `native/`. Licence texts are in each submodule at the path given. All licences are
permissive; no copyleft code is linked into Cinder.
| Component | Path | Pinned ref | Commit | Licence | Licence file |
|---|---|---|---|---|---|
| wgpu-native | `third_party/wgpu-native` | `v29.0.1.1` | `589e6ca` | MIT OR Apache-2.0 | `LICENSE.MIT`, `LICENSE.APACHE` |
| webgpu-headers (nested in wgpu-native) | `third_party/wgpu-native/ffi/webgpu-headers` | as pinned by wgpu-native | `673658b` | BSD-3-Clause | `LICENSE` |
| SDL3 | `third_party/SDL` | `release-3.4.20` | `f00dc91` | zlib | `LICENSE.txt` |
| joltc (Jolt Physics C API) | `third_party/joltc` | commit `2bbcf73b1bec` | `2bbcf73` | MIT | `LICENSE` |
| Jolt Physics | `third_party/JoltPhysics` | `v5.6.0` (the tag joltc `2bbcf73` fetches by default) | `e77f175` | MIT | `LICENSE` |
| cgltf | `third_party/cgltf` | `v1.15` | `360db1a` | MIT | `LICENSE` |
| stb (stb_image, stb_image_write) | `third_party/stb` | commit `4e9b368bfc07` | `4e9b368` | MIT OR Public Domain (Unlicense) | `LICENSE` |
| KTX-Software (libktx, Basis Universal) | `third_party/KTX-Software` | `v4.4.2` | `4d6fc70` | Apache-2.0 (libktx); bundled parts under the licences in `LICENSES/` | `LICENSE.md`, `LICENSES/` |
| meshoptimizer | `third_party/meshoptimizer` | `v1.3` | `9e1f07b` | MIT | `LICENSE.md` |
| MikkTSpace | `third_party/MikkTSpace` | commit `3e895b49d05e` | `3e895b4` | zlib (header of `mikktspace.c`) | `mikktspace.c` |
| miniaudio | `third_party/miniaudio` | `0.11.26` | `877ea4a` | MIT-0 OR Public Domain (Unlicense) | `LICENSE` |
| Dear ImGui (docking) | `third_party/imgui` | `v1.93.0-docking` | `b916186` | MIT | `LICENSE.txt` |
| Dear Bindings (dcimgui generator) | `third_party/dear_bindings` | `v0.27` | `67ede41` | MIT | `LICENSE.txt` |
| Tracy | `third_party/tracy` | `v0.15.0` | `f2a4f25` | BSD-3-Clause | `LICENSE` |
| flecs (benchmark comparison only, not shipped) | `third_party/flecs` | `v4.2.0` | `6566d4b` | MIT | `LICENSE` |
## Tools (not linked into the engine)
| Tool | Version | Licence |
|---|---|---|
| Mojo compiler and stdlib | 1.2.0 | Modular's Mojo licence (compiler), Apache-2.0 with LLVM exceptions (stdlib) |
| pixi | v0.63.2 | BSD-3-Clause |
| Rust toolchain (builds wgpu-native) | 1.93.0 | MIT OR Apache-2.0 |
| mojo-bindgen | see ADR-0005 | MIT |
| ply (dear_bindings dependency) | 3.11 | BSD-3-Clause |
## Assets
Sample and test assets are CC0 or CC-BY and are listed with attribution in `assets/ATTRIBUTION.md` once
added (Phase 4 onwards). Khronos glTF-Sample-Assets are used under their per-model licences.