# Cinder 0.1.0: full documentation > Every documentation page of https://cinder.ash10.dev, in navigation order, as Markdown. Engine commit 47c9352, imported 2026-11-09. Index: https://cinder.ash10.dev/llms.txt --- # AGENTS.md: working on or with the Cinder engine Context file for AI coding agents (Claude Code reads it as `CLAUDE.md`; Codex, Cursor and others read `AGENTS.md`). Drop it at the root of a Cinder checkout or of a game built on Cinder. Generated by https://cinder.ash10.dev from the engine documentation at commit 47c9352 (Cinder 0.1.0, imported 2026-11-09). When this file and the engine repository disagree, the repository wins; re-download this file. ## What Cinder is A 3D game engine written in **Mojo 1.2.0** (pinned; nightlies are never used) over one C11 shim, `libcinder_native`, wrapping wgpu-native (WGSL), SDL3, Jolt (through joltc), cgltf, KTX-Software, meshoptimizer, miniaudio, Dear ImGui and Tracy. Honest status: Linux x86-64 is built and tested. macOS arm64 is a declared target but has **not been built or run**. Benchmarks ran only on Mesa lavapipe (a CPU rasteriser), so the performance budgets are recorded there, not met on reference hardware. Never claim macOS support or performance numbers that `REPORT.md` does not state. ## Documentation for agents - Index of every page: https://cinder.ash10.dev/llms.txt; everything in one file: https://cinder.ash10.dev/llms-full.txt - Any docs page as Markdown: its URL without the trailing slash plus `.md`, e.g. https://cinder.ash10.dev/docs/guides/ecs.md - Pages and every API symbol (name, signature, summary, URL) as JSON: https://cinder.ash10.dev/docs/index.json - MCP server (read-only search and page tools): `https://cinder.ash10.dev/mcp` (Streamable HTTP) - Read first: the guide for the module you touch (https://cinder.ash10.dev/docs/guides/), then its API page (https://cinder.ash10.dev/docs/api/), then `docs/MOJO_NOTES.md` before writing any Mojo. ## Build, run and test Mojo has no Windows build. On Windows (and anywhere without a native toolchain) every command runs in the Linux toolchain container through `tools/in_container.sh`; on a native Linux host drop the prefix. If the repository is reached through a substituted drive on Windows, set `CINDER_HOST_ROOT` to its real path. ```bash docker build -t cinder-toolchain:1 -f docker/Dockerfile.toolchain docker # once tools/in_container.sh pixi install # once tools/in_container.sh pixi run native # build libcinder_native (Release) + CTest tools/in_container.sh pixi run test # lint, API doc check, every Mojo test, X11 + Wayland spikes tools/in_container.sh pixi run format-check # mojo format, then git diff --exit-code tools/in_container.sh pixi run lint # banned legacy Mojo forms tools/in_container.sh pixi run api-docs-check # every public symbol has a docstring tools/in_container.sh pixi run api-docs # regenerate docs/api/ from docstrings tools/in_container.sh pixi run native-sanitize # Debug shim with ASan + UBSan tools/in_container.sh pixi run test-asan # tests against the sanitized shim tools/in_container.sh pixi run test-zero-alloc # zero Mojo allocations per steady-state frame tools/in_container.sh pixi run test-golden # 16 golden images (SSIM + pixel thresholds) tools/in_container.sh pixi run example # the tutorial game (docs/TUTORIAL.md) tools/in_container.sh pixi run test-example # the tutorial game, compiled and run headless tools/in_container.sh pixi run sample # the sample game, windowed tools/in_container.sh pixi run sample-headless # the sample game, offscreen autopilot tools/in_container.sh pixi run bench-all # stress + sample benchmarks, 10% regression gate tools/in_container.sh pixi run -e bindgen bindgen # regenerate cinder/ffi from the C headers ``` A single test program runs as `mojo run -I . tests//test_.mojo` with `CINDER_NATIVE_LIB=native/build/release/lib/libcinder_native.so` set (see the matching `pixi.toml` task). There is no `mojo test` subcommand in 1.2.0. Every Mojo build uses `--Werror`. Headless runs (`--headless`) need no display and are what tests, CI and benchmarks use. ## Layering rules (never break these) Dependencies point downward only: a module imports from layers below it, never from above. ``` sample/ (and your game) -> cinder/app -> cinder/tools cinder/{render,physics,anim,audio,input,assets,scene} -> cinder/ecs, cinder/gfx, cinder/platform -> cinder/core -> cinder/ffi -> native/ (libcinder_native) ``` - `cinder/ffi` is generated by `tools/bindgen` and **never edited by hand**; a layout test checks every struct offset and enum constant against the C compiler. Change the C header, then regenerate. - One shim, one shared library, flat C11 ABI. Every C callback lives in C and writes fixed-size events into lock-free rings; Mojo drains them once per frame on the main thread (the pull model, ADR-0003). Mojo code never runs on a C thread. - `cinder/platform` is the only module with per-OS branches. `cinder/core` does no GPU or window I/O. - Every `unsafe_` call carries a `# SAFETY:` comment. Each shared library is loaded exactly once. - Games use public engine APIs only (the sample game is the reference: `sample/`). ## The zero-allocation rule A steady-state frame performs **zero heap allocations from Mojo code**; `tests/perf/test_zero_alloc.mojo` asserts it through an LD_PRELOADed counter (`pixi run test-zero-alloc`). In anything reachable from `App.run_frame` (systems, render, animation, physics sync, your `fixed_update`/`update`/`draw_ui`): - Keep per-frame data in persistent scratch fields that are cleared, never freed. `List.clear()` keeps capacity; `resize(n, fill)` allocates only when growing. - No `List[...] = [x]` literals, no growing lists, no new `String` building of long text per frame. - Use `Array[T, N]` locals (stack storage) for small fixed-size per-call data. - Moving a `List` out of a struct and back does not allocate (how the frame graph recycles records). - Third-party libraries may still allocate; the rule covers Mojo code. ## Mojo 1.2.0: what bites The compiler and the current Mojo docs win over memory: Mojo changed a lot in 1.0 to 1.2. Before writing Mojo, read https://cinder.ash10.dev/docs/mojo-notes.md. Key forms: - `def` is the only function keyword (`fn` removed in 1.1.0). Functions do not raise unless marked `raises`. `var` declares every variable; there is no `let`; `alias` is gone, use `comptime`. - Conventions: default (immutable, `imm` when explicit), `mut`, `var` (owned, caller passes `x^`), `ref`, `out`, `deinit` (`def __deinit__(deinit self)`). `read`, `inout`, `borrowed`, `owned` are errors or banned. - `comptime if` / `comptime for` replace `@parameter if/for`. Constraints: `where else "msg"`. - Structs are `Movable`, not copyable, by default; declare `Copyable` and prefer `x.copy()`. - `Pointer[T, origin]` replaces `UnsafePointer`; unsafe operations are prefixed (`unsafe_load`, `unsafe_store`, `ptr[unsafe_offset=i]`, `unsafe_free`). `memcpy`/`memset` are gone. - `Array[T, N]` replaces `InlineArray`; a list literal defaults to `Array`, so annotate `List` explicitly. - Format with t-strings: `t"value={x}"`. - **ASAP destruction.** Values die after their last *syntactic* use. A stack value handed to C by pointer must be followed by `std.benchmark.keep(value)` after the last C call that reads it; an `OwnedDLHandle` closes at its last use (function pointers live only in the struct owning the handle); early returns skip a trailing `keep`, so structure such code as if/else with one `keep` at the end. - `Optional[def(...) thin abi("C")]` is 16 bytes: nullable C function pointers are bound as opaque pointers. - A `mut self` method cannot also take a field of `self` (mutably or not) as an argument: copy to a local or let the method touch `self.field` directly. - Inside a parametric struct write `Self.T` in field and method signatures. - Struct fields are destroyed in declaration order: a struct holding a native library and objects whose destructors call into it declares the library **last**. - A struct storing pointers to its own fields breaks when moved: box it (`cinder/core/boxed.mojo`). - wgpu-native aborts the process on a validation error inside `wgpuQueueSubmit`; keep copy strides 256-aligned and pass them explicitly. - Tests: module-level `def test_*() raises:` functions, and the file ends with `def main() raises: TestSuite.discover_tests[__functions_in_module()]().run()`. Banned in `*.mojo` (CI lint `tools/lint_legacy_mojo.sh` fails on any of them): `tools/lint_legacy_mojo.sh`, `fn `, `alias `, `inout`, `borrowed`, `owned `, `let `, `@parameter`, `__del__`, `InlineArray`, `StringSlice`, `UnsafePointer`, `MutUnsafePointer`, `ImmUnsafePointer`, `std.gpu`, `.load(`, `.store(`, `.free(`, `memcpy(`, `memset(`, `read `, `@parameter if`, `@parameter for`, `@register_passable`, `__comptime_assert`, `steal_data`, `.size`. ## Conventions Right-handed, Y up, -Z forward, metres, column-major matrices, reverse-Z with an infinite far plane, linear lighting, sRGB render targets, RGBA16F intermediates. Recoverable failures raise typed errors; broken invariants abort with a message. The simulation is a fixed 60 Hz step (`Game.fixed_update`, where physics happens); rendering runs once per frame (`Game.update`, `Game.draw_ui`). A program is an `App` plus your `Game` struct (`start`, `fixed_update`, `update`, `draw_ui`). End the game before the app, explicitly, because the game's bodies live in the app's physics world: ```mojo game.shutdown(app) _ = game^ _ = app^ ``` ## Threading The main thread owns SDL, the window, presentation and ImGui. Everything else runs on the work-stealing job system (one worker per physical core minus one). Shared state crosses threads only through jobs, atomics or the locks named in `docs/THREADING.md`. Jolt steps on its own two-thread pool (ADR-0012). miniaudio's device thread runs C only. ## How to work in this repository - Read the relevant guide and ADR before changing a subsystem; a change to a pinned version or a layering rule needs a new ADR. - Add or update tests with every change; never skip a test to make a run pass. - Public symbols need docstrings (`pixi run api-docs-check`); regenerate `docs/api/` after API changes. - Run `pixi run format-check`, `pixi run lint` and `pixi run test` before declaring work done, and report results faithfully, including anything not run (for example macOS). --- > Cinder 0.1.0 documentation: Cinder documentation > Canonical page: https://cinder.ash10.dev/docs/ · Source: generated by this site (engine commit 47c9352, imported 2026-11-09) > Index of every page for agents: https://cinder.ash10.dev/llms.txt # Cinder documentation Cinder 0.1.0 is a 3D game engine written in Mojo 1.2.0 over one C11 native shim. These pages are imported verbatim from the engine repository; the [status report](https://cinder.ash10.dev/docs/report.md) is the source of truth for what has been verified. Every page is also available as raw Markdown at its path plus `.md`, and the whole set is indexed for AI agents in [/llms.txt](https://cinder.ash10.dev/llms.txt). ## Start - [Build your first game](https://cinder.ash10.dev/docs/tutorial.md): This tutorial builds a small game on Cinder from nothing: a red ball you push around a lit ledge to knock six crates off the edge. It takes about an hour. The finished code is in examples/first_game/ and is compiled and run headless by tests/examples/test_first_game.mojo, so… - [First game: complete source](https://cinder.ash10.dev/docs/examples/first-game.md): The finished code of the tutorial, copied verbatim from examples/first_game/ in the engine repository. The engine's test tests/examples/test_first_game.mojo compiles it and runs it headless, so it builds under the pinned toolchain (Mojo 1.2.0). Each # Step N comment marks… - [Sample game entry point](https://cinder.ash10.dev/docs/examples/sample-game.md): sample/main.mojo from the engine repository, verbatim: the entry point of the third-person sample game (ADR-0014). The game itself lives in sample/game/ (components, levels, settings, gameplay) and is built only on public engine APIs. - [Using Cinder with AI coding agents](https://cinder.ash10.dev/agents.md): A ready-to-use AGENTS.md / CLAUDE.md brief, the llms.txt and JSON indexes, raw Markdown URLs, the MCP endpoint and prompts that work. ## Guides - [Subsystem guides](https://cinder.ash10.dev/docs/guides.md): One guide per engine module: key types and ownership, setup, per-frame use, threading rules and pitfalls. Each links to its generated API reference. - [Animation guide (cinder/anim)](https://cinder.ash10.dev/docs/guides/anim.md): cinder/anim is skeletal animation written in Mojo: skeletons built from glTF skins or read from cooked .cskel files, clip sampling into local poses, blend trees evaluated on a shared phase, animators with states, crossfades, events and root motion, and an AnimationSystem that… - [Application runtime (cinder/app)](https://cinder.ash10.dev/docs/guides/app.md): cinder/app is the top of the engine: App owns every subsystem, runs the fixed-step game loop and renders, either into an SDL window or, with --headless, into an offscreen texture that can be read back. Gameplay implements the Game trait and is called from App.run_frame. The… - [Assets guide (cinder/assets)](https://cinder.ash10.dev/docs/guides/assets.md): cinder/assets gets content from disk into the renderer. It has two paths: - [Audio (cinder/audio)](https://cinder.ash10.dev/docs/guides/audio.md): cinder/audio is the engine's sound layer over miniaudio. It provides mixing buses, decoded and streamed sounds, one-shots that clean up after themselves, 3D spatialisation with distance attenuation, a single listener, and an ECS bridge that moves sounds with entities and the… - [cinder/core guide](https://cinder.ash10.dev/docs/guides/core.md): cinder/core is the lowest Mojo layer of the engine: logging, assertions, a bump arena, generational handles and pools, a bounded ring, byte packing and hashing, file helpers, a monotonic clock with a fixed-step accumulator, CPU images, the job system, Tracy zones, Boxed heap… - [ECS guide (cinder/ecs)](https://cinder.ash10.dev/docs/guides/ecs.md): cinder/ecs is the archetype entity component system every gameplay-facing module builds on. It sits directly above cinder/core (it uses core/math for Mat4 and core/jobs for the scheduler) and below render, physics, anim, audio, assets and scene, which define their own… - [cinder/gfx guide](https://cinder.ash10.dev/docs/guides/gfx.md): cinder/gfx is the safe, typed layer over wgpu-native. It owns the wgpu instance, adapter, device and queue (Gfx), keeps every GPU object the engine creates in generational pools behind typed handles (Resources), compiles WGSL with editor-friendly diagnostics and hot reload… - [Input (cinder/input)](https://cinder.ash10.dev/docs/guides/input.md): cinder/input maps raw platform events to named gameplay controls. A game asks pressed("jump") or axis("move_x") instead of testing scancodes, players can rebind controls at runtime, and the whole map saves to a small text file. The package sits beside audio, physics, anim and… - [Physics guide (cinder/physics)](https://cinder.ash10.dev/docs/guides/physics.md): cinder/physics is the safe Mojo layer over Jolt. Jolt is reached through joltc inside libcinder_native; the parts of the shim that need C callbacks live in native/src/physics_shim.c (query filters, closest-hit collectors, CharacterVirtual extended update defaults) and… - [cinder/platform guide](https://cinder.ash10.dev/docs/guides/platform.md): cinder/platform is the thin Mojo layer over SDL3 and the native shim's surface helpers. It loads libcinder_native exactly once, initialises SDL, creates windows, drains SDL's event queue into engine-level Events, opens and closes gamepads as they appear, creates the wgpu… - [cinder/render: the renderer](https://cinder.ash10.dev/docs/guides/render.md): cinder/render turns a RenderScene (camera, lights, instances, joint palettes, environment) into one submitted command buffer per frame. It is a clustered forward+ renderer with cascaded and atlas shadow maps, GPU-baked image-based lighting, GTAO, physically based bloom, AgX… - [Scene guide (cinder/scene)](https://cinder.ash10.dev/docs/guides/scene.md): cinder/scene is the layer between the ECS and the rest of the engine's content pipeline. It has three independent parts: - [Tools: debug overlay, Tracy profiling, headless mode](https://cinder.ash10.dev/docs/guides/tools.md): Cinder's developer tooling is three things: the Dear ImGui debug overlay in cinder/tools, the Tracy client wrapper in cinder/core/profile.mojo, and the --headless mode of cinder/app that lets the same executables run without a display for tests, benchmarks and captures. ## Engine design - [Cinder architecture](https://cinder.ash10.dev/docs/architecture.md): Dependencies point downward only. A module may import from the layers below it, never from the same layer except where noted, and never from above. - [Threading model](https://cinder.ash10.dev/docs/threading.md): Fixed by section 4 of the brief; this page records how it is realised and which shared state exists. - [Mojo 1.2.0 notes for Cinder](https://cinder.ash10.dev/docs/mojo-notes.md): Source of truth: https://mojolang.org/docs (manual and std reference for 1.2.0) and the release notes for 1.0.0, 1.1.0 and 1.2.0. Everything below was read from those pages on 2026-11-08 and confirmed by compiling spikes/hello/ under the pinned toolchain. When the compiler… - [Toolchain](https://cinder.ash10.dev/docs/toolchain.md): Every version below is pinned. Changing one requires an ADR and a green CI run. - [Render validation](https://cinder.ash10.dev/docs/render-validation.md): Golden-image tests (tests/golden/test_golden_gltf.mojo) render Khronos glTF-Sample-Assets models headless and compare them with committed references per platform. This file records how the references were produced and reviewed, and the tolerances. - [Benchmarks](https://cinder.ash10.dev/docs/benchmarks.md): How Cinder measures performance, what the numbers mean, and how regressions are caught. The measured results for each run live in REPORT.md (section 4); this page is the method. ## Decisions - [Architecture decision records](https://cinder.ash10.dev/docs/adr.md): Every significant build-versus-reuse, layering and deviation decision, with context and consequences. Deviations from the original brief each point to one of these. - [ADR-0001: Build-versus-reuse decisions for the dependency stack](https://cinder.ash10.dev/docs/adr/0001-dependency-stack.md): Status: Accepted, 2026-11-08 - [ADR-0002: No MAX / max.gpu for rendering; GPU compute is WGSL](https://cinder.ash10.dev/docs/adr/0002-no-max-gpu.md): Status: Accepted, 2026-11-08 - [ADR-0003: One C11 shim owning every callback; Mojo pulls events](https://cinder.ash10.dev/docs/adr/0003-native-shim-pull-model.md): Status: Accepted, 2026-11-08 (callback-in-Mojo question deferred to spike 4) - [ADR-0004: Building wgpu-native from source through CMake](https://cinder.ash10.dev/docs/adr/0004-wgpu-native-build.md): Status: Accepted, 2026-11-08 - [ADR-0005: Generating the Mojo bindings](https://cinder.ash10.dev/docs/adr/0005-bindings-generation.md): Status: Accepted, 2026-11-08 (decided by spike 9; supersedes the "Proposed" version of the same day) - [ADR-0006: Start from scratch rather than fork stargine](https://cinder.ash10.dev/docs/adr/0006-no-fork-of-stargine.md): Status: Accepted, 2026-11-08 - [ADR-0007: Development host is a Linux container; Windows host is a file server](https://cinder.ash10.dev/docs/adr/0007-dev-environment-container.md): Status: Accepted, 2026-11-08 - [ADR-0008: Job system scheduling in Mojo, threads and parking from the shim](https://cinder.ash10.dev/docs/adr/0008-job-system-threads.md): Status: Accepted, 2026-11-08 - [ADR-0009: Graphics layer shape (typed handles, error scopes for shader diagnostics)](https://cinder.ash10.dev/docs/adr/0009-gfx-layer.md): Status: accepted, 2026-11-08. - [ADR-0010: Renderer architecture (forward+, frame graph, shader assembly)](https://cinder.ash10.dev/docs/adr/0010-renderer.md): Status: accepted, 2026-11-08. - [ADR-0011: Keep the Mojo archetype ECS (benchmarked against flecs)](https://cinder.ash10.dev/docs/adr/0011-ecs-mojo-vs-flecs.md): Status: accepted, 2026-11-08. - [ADR-0012: Phase 6 subsystems: physics, animation, audio and input](https://cinder.ash10.dev/docs/adr/0012-physics-animation-audio-input.md): Status: accepted, 2026-11-09. - [ADR-0013: Tools and the application runtime (Phase 7)](https://cinder.ash10.dev/docs/adr/0013-tools-and-app-runtime.md): Status: accepted, 2026-11-09. - [ADR-0014: Sample game, benchmarks and the zero-allocation frame (Phase 8)](https://cinder.ash10.dev/docs/adr/0014-sample-game-and-performance.md): Status: accepted, 2026-11-09. ## API reference - [API reference](https://cinder.ash10.dev/docs/api.md): Generated by tools/gen_api_docs.py from the docstrings in cinder/; do not edit by hand. - [cinder.anim API](https://cinder.ash10.dev/docs/api/anim.md): Source: cinder/anim/__init__.mojo - [cinder.app API](https://cinder.ash10.dev/docs/api/app.md): Source: cinder/app/__init__.mojo - [cinder.assets API](https://cinder.ash10.dev/docs/api/assets.md): Source: cinder/assets/__init__.mojo - [cinder.audio API](https://cinder.ash10.dev/docs/api/audio.md): Source: cinder/audio/__init__.mojo - [cinder.core API](https://cinder.ash10.dev/docs/api/core.md): Source: cinder/core/__init__.mojo - [cinder.ecs API](https://cinder.ash10.dev/docs/api/ecs.md): Source: cinder/ecs/__init__.mojo - [cinder.gfx API](https://cinder.ash10.dev/docs/api/gfx.md): Source: cinder/gfx/__init__.mojo - [cinder.input API](https://cinder.ash10.dev/docs/api/input.md): Source: cinder/input/__init__.mojo - [cinder.physics API](https://cinder.ash10.dev/docs/api/physics.md): Source: cinder/physics/__init__.mojo - [cinder.platform API](https://cinder.ash10.dev/docs/api/platform.md): Source: cinder/platform/__init__.mojo - [cinder.render API](https://cinder.ash10.dev/docs/api/render.md): Source: cinder/render/__init__.mojo - [cinder.scene API](https://cinder.ash10.dev/docs/api/scene.md): Source: cinder/scene/__init__.mojo - [cinder.tools API](https://cinder.ash10.dev/docs/api/tools.md): Source: cinder/tools/__init__.mojo ## Project - [Deliverable report](https://cinder.ash10.dev/docs/report.md): 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. - [Status](https://cinder.ash10.dev/docs/status.md): 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. - [Changelog](https://cinder.ash10.dev/docs/changelog.md): 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). - [Engine README](https://cinder.ash10.dev/docs/readme.md): 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). - [Third-party notices](https://cinder.ash10.dev/docs/third-party-notices.md): 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. ## Using Cinder with AI agents - [Using Cinder with AI coding agents](https://cinder.ash10.dev/agents.md): a ready-to-use agent context file, prompts, the llms.txt index and the MCP endpoint. --- > Cinder 0.1.0 documentation: Build your first game > Canonical page: https://cinder.ash10.dev/docs/tutorial/ · Source: `docs/TUTORIAL.md` in the engine repository (engine commit 47c9352, imported 2026-11-09) > Index of every page for agents: https://cinder.ash10.dev/llms.txt # Build your first game This tutorial builds a small game on Cinder from nothing: a red ball you push around a lit ledge to knock six crates off the edge. It takes about an hour. The finished code is in `examples/first_game/` and is compiled and run headless by `tests/examples/test_first_game.mojo`, so every snippet below is code that builds under the pinned toolchain. Each `# Step N` comment in `examples/first_game/game.mojo` marks where that step's code lives. What you will use: the `App` runtime and `Game` trait (`cinder/app`), the ECS world and its built-in components (`cinder/ecs`, `cinder/scene`), the physics world (`cinder/physics`), the renderer's meshes, materials and lights (`cinder/render`), input actions (`cinder/input`) and an ImGui HUD through the raw bindings. Deeper material on each lives in `docs/guides/`. ## 0. Prerequisites Follow the README quick start once: the toolchain container, `pixi run native`, and `pixi run test` green. Every command below runs inside the container (`tools/in_container.sh ...`), on Linux x86-64. ```bash tools/in_container.sh pixi run example # the finished game, windowed tools/in_container.sh pixi run example -- --headless --frames 120 --screenshot .cache/first.png ``` ## 1. An App and a Game A Cinder program is an `App` that owns every subsystem (window or offscreen target, GPU device, renderer, ECS world, physics, animation, audio, input, debug UI) and a `Game`, a struct you write that implements four methods. `App.run_frame` calls them in a fixed order every frame: | Method | When | Use it for | |---|---|---| | `start(mut self, mut app)` | once, on the first frame | loading content, spawning entities, binding input | | `fixed_update(mut self, mut app, dt)` | zero or more times per frame, at `AppConfig.fixed_hz` (60 Hz) | anything that reads or drives physics | | `update(mut self, mut app, dt)` | once per frame, variable `dt` | timers, camera, UI state | | `draw_ui(mut self, mut app)` | once per frame, inside an ImGui frame | HUD, menus | `examples/first_game/main.mojo` is the whole entry point. It parses the engine options (`--headless`, `--width`, `--height`, ...) with `parse_app_args`, which hands back the arguments it did not recognise, builds the `App`, and loops: ```mojo var config = AppConfig(String("First game"), 1280, 720, False) config.audio = False var rest = parse_app_args(args, config) var app = App(config^) var game = FirstGame() while app.run_frame(game): frames += 1 if max_frames >= 0 and frames >= max_frames: break game.shutdown(app) _ = game^ _ = app^ ``` The last three lines matter. Mojo destroys a value at its last use, so without the explicit order the `App` could be destroyed while the game still holds physics bodies that live in it. The game is ended first, then the app, always. The game struct carries only what it needs to remember between frames: ```mojo struct FirstGame(Game): var shapes: List[Shape] var bodies: List[UInt32] var ball: UInt32 var crates: List[UInt32] var fallen: Int var won: Bool var elapsed: Float32 var ui: CStrings ``` ## 2. A ledge, crates and a ball Content is entities in `app.world`. Visible things carry a `LocalTransform` (or a `RigidBody`, which writes the transform for you) and a `MeshRenderer` that names a mesh and a material registered with the renderer. Physics objects live in `app.physics[]` and are linked to entities by the `RigidBody` component. Register the meshes and materials once. `cube` and `plane` are built-in primitives; the second `add_mesh` argument says whether to build LODs with meshoptimizer: ```mojo var floor_mesh = app.renderer[].add_mesh(String("ledge"), plane(LEDGE_HALF, 1), False) var box_mesh = app.renderer[].add_mesh(String("box"), cube(0.5), True) var grey = app.renderer[].add_material(Material(String("grey"))) var red = Material(String("red")) red.base_color = Vec4(0.9, 0.15, 0.1, 1.0) red.metallic = 0.2 red.roughness = 0.35 var red_id = app.renderer[].add_material(red) ``` The ledge is a static box in physics and a plane in the renderer. Static bodies are created inactive (the second `create_body` argument), on layer 0; the default `LayerConfig` makes layer 0 the static layer that collides with everything but itself: ```mojo var ledge_shape = app.physics[].box(Vec3(LEDGE_HALF, 0.5, LEDGE_HALF)) self.shapes.append(ledge_shape) self.bodies.append(app.physics[].create_body( BodyDesc(ledge_shape, Vec3(0.0, -0.5, 0.0), MotionType.static(), 0), False)) var ledge = app.world.spawn() app.world.add[LocalTransform](ledge, LocalTransform(Mat4.identity())) app.world.add[MeshRenderer](ledge, MeshRenderer(floor_mesh, grey, False)) ``` Dynamic crates share one shape (shapes are reference counted by Jolt; the game keeps the handle so it can release it at the end). A `RigidBody` component makes the engine copy the body's position and rotation into the entity's transform after every fixed step, interpolated to the render time: ```mojo var crate_shape = app.physics[].box(Vec3(0.5, 0.5, 0.5)) for i in range(CRATE_COUNT): var p = Vec3(Float32(i) * 1.2 - Float32(CRATE_COUNT - 1) * 0.6, 0.5, -2.5) var crate_desc = BodyDesc(crate_shape, p, MotionType.dynamic(), 1) crate_desc.mass = 10.0 var body = app.physics[].create_body(crate_desc) var e = app.world.spawn() app.world.add[RigidBody](e, RigidBody(body, MotionType.dynamic(), p, Quat.identity())) app.world.add[MeshRenderer](e, MeshRenderer(box_mesh, wood_id, True)) ``` The ball is the same recipe with a sphere shape. Masses are set explicitly: left at zero, Jolt derives mass from the shape's volume at the density of water, which makes a one-metre crate weigh a tonne. The crates are 10 kg (`crate_desc.mass = 10.0`) and the ball 20 kg. Two more `BodyDesc` fields tune the ball: damping so it stops rolling when you let go, and `allow_sleeping = False` so Jolt never puts the player's body to sleep while a force is about to be applied. ## 3. A sun and a camera Lights and cameras are entities too. A `LightSource` shines down its entity's -Z axis, so a directional light is placed with a look-at matrix; the last argument asks for cascaded shadow maps. The camera is the entity with an active `CameraComponent`; its transform is the camera's world matrix (the inverse of a view matrix): ```mojo var sun = app.world.spawn() app.world.add[LocalTransform](sun, LocalTransform( Mat4.look_at(Vec3(6.0, 10.0, 4.0), Vec3.zero(), Vec3.unit_y()).inverse())) app.world.add[LightSource](sun, LightSource( LightKind.directional(), Vec3(1.0, 0.95, 0.9), 3.0, 0.0, 0.0, 0.0, True)) var camera = app.world.spawn() app.world.add[LocalTransform](camera, LocalTransform( Mat4.look_at(Vec3(0.0, 7.0, 9.0), Vec3(0.0, 0.0, -0.5), Vec3.unit_y()).inverse())) app.world.add[CameraComponent](camera, CameraComponent(0.9, 0.05, True)) ``` `CameraComponent` takes the vertical field of view in radians and the near plane; the renderer uses reverse-Z with an infinite far plane, so there is no far distance to pick. Image-based lighting comes from the renderer's default environment, so the scene is lit from all directions even with a single sun. Run it now (`pixi run example`): the crates settle on the ledge, the ball sits in front of them, the sun casts shadows. Nothing moves yet. ## 4. Input as actions and axes Gameplay never reads keys directly. It binds named axes and actions in `start`, and reads them by name; the same names can be rebound at runtime (`InputMap.listen_action`) and saved as text (`InputMap.save`). Keys are SDL scancodes: ```mojo app.input.bind_axis(String("move_x"), Binding.key(KEY_D, 1.0)) app.input.bind_axis(String("move_x"), Binding.key(KEY_A, -1.0)) app.input.bind_axis(String("move_z"), Binding.key(KEY_S, 1.0)) app.input.bind_axis(String("move_z"), Binding.key(KEY_W, -1.0)) app.input.bind_axis(String("move_x"), Binding.gamepad_axis(0, 1.0)) app.input.bind_action(String("jump"), Binding.key(KEY_SPACE)) app.input.bind_action(String("restart"), Binding.key(KEY_R)) ``` Several bindings on one axis add up, clamped to [-1, 1] for digital bindings (mouse deltas are left in pixels); a gamepad stick on the same axis just works. ## 5. Pushing the ball: the fixed step Everything that drives physics goes in `fixed_update`, which runs at exactly 60 Hz however fast the screen refreshes, so the game behaves the same on every machine (and the headless test can replay it). Forces are per step; impulses are instantaneous: ```mojo def fixed_update(mut self, mut app: App, dt: Float32) raises: if self.won: return var push = Vec3(app.input.axis(String("move_x")), 0.0, app.input.axis(String("move_z"))) if push.length() > 1.0: push = push.normalized() app.physics[].add_force(self.ball, push * PUSH_FORCE) if app.input.just_pressed(String("jump")) and self.ball_grounded(app): app.physics[].add_impulse(self.ball, Vec3(0.0, 80.0, 0.0)) self.fallen = 0 for body in self.crates: if app.physics[].position(body).y() < FALL_Y: self.fallen += 1 if self.fallen == CRATE_COUNT: self.won = True if app.physics[].position(self.ball).y() < FALL_Y: self.reset_ball(app) ``` `just_pressed` is true for exactly one frame's worth of fixed steps, so a held key fires once. Teleporting a body is `set_position_rotation` with `activate = True`, followed by zeroing its velocities, which is what `reset_ball` does. ## 6. Per-frame logic `update` runs once per rendered frame with the real elapsed time. It is the place for timers, camera smoothing and anything cosmetic: ```mojo def update(mut self, mut app: App, dt: Float32) raises: if not self.won: self.elapsed += dt if app.input.just_pressed(String("restart")): self.restart(app) ``` ## 7. A HUD `draw_ui` is called inside an ImGui frame. The bindings are the raw dcimgui C API on `app.lib.ptr()`, and ImGui wants NUL-terminated C strings that stay alive for the frame, which is what `CStrings` does (clear it at the start of each `draw_ui`): ```mojo def draw_ui(mut self, mut app: App) raises: self.ui.clear() var lib = app.lib.ptr() lib[].ImGui_SetNextWindowPos(ImVec2(12.0, 12.0), Int32(ImGuiCond_Always)) if lib[].ImGui_Begin(self.ui.c(String("hud")), Optional[Pointer[Bool, MutUntrackedOrigin]](), Int32(ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoInputs)): lib[].ImGui_TextUnformatted(self.ui.c( String(t"crates knocked off {self.fallen}/{CRATE_COUNT} time {Int(self.elapsed)} s"))) lib[].ImGui_End() ``` The engine's own overlay is on by default in windowed builds (`--no-debug-ui` hides it) and appears next to yours, with the frame-time graph, the entity inspector (your entities are there), the render graph and the log. ## 8. Cleaning up Shapes and bodies are owned by whoever created them. `shutdown` destroys the bodies and then the shapes, and `main` calls it before the game and the app go out of scope. The headless test checks `app.physics[].body_count() == 0` afterwards, and `App` itself asserts that no GPU resource leaked when it is destroyed. ```mojo def shutdown(mut self, mut app: App): for b in self.bodies: app.physics[].destroy_body(b) for s in self.shapes: app.physics[].destroy_shape(s) ``` ## 9. Running it headless, and testing it `--headless` renders to an offscreen texture and reads it back; `--frames N` stops after N frames; `--screenshot PATH` writes the last frame. That is how the test drives the game without a keyboard: it lines the ball up behind each crate and applies the same force the move axis would, checks that every crate drops off and the game reports `won`, restarts, reads a frame and checks that the GPU reported no validation errors. ```bash tools/in_container.sh pixi run mojo run -I . -I examples tests/examples/test_first_game.mojo ``` ## Where to go next - Load real content: `cinder/assets` imports glTF (`load_gltf`, `upload_gltf`) and `cinder/scene.spawn_gltf` turns a file into entities; `docs/guides/assets.md`. - A walking character instead of a ball: `CharacterController` in `cinder/physics` and an `Animator` with a blend tree in `cinder/anim`; the sample game (`sample/`) does both. - Sound: `AudioEngine` buses, one-shots and ECS `AudioSource`s; `docs/guides/audio.md`. - Saving: make your components `SceneComponent`s and write the world with `SceneRegistry` (`docs/guides/scene.md`); the sample's F5/F9 save and load are forty lines. - Performance: `docs/BENCHMARKS.md`, and `tests/perf/test_zero_alloc.mojo` for the rule that a steady-state frame allocates nothing. --- > Cinder 0.1.0 documentation: First game: complete source > Canonical page: https://cinder.ash10.dev/docs/examples/first-game/ · Source: `examples/first_game/` in the engine repository (engine commit 47c9352, imported 2026-11-09) > Index of every page for agents: https://cinder.ash10.dev/llms.txt # First game: complete source The finished code of the tutorial, copied verbatim from `examples/first_game/` in the engine repository. The engine's test `tests/examples/test_first_game.mojo` compiles it and runs it headless, so it builds under the pinned toolchain (Mojo 1.2.0). Each `# Step N` comment marks where that tutorial step's code lives. ```bash tools/in_container.sh pixi run example # windowed tools/in_container.sh pixi run example -- --headless --frames 120 --screenshot .cache/first.png ``` ## `examples/first_game/main.mojo` ```mojo """Entry point of the tutorial game (docs/TUTORIAL.md, step 1). pixi run example [--headless] [--frames N] [--screenshot PATH] """ from std.sys import argv from cinder.app import App, AppConfig, parse_app_args from cinder.core.image import save_png from first_game import FirstGame def main() raises: var args = List[String]() var raw = argv() for i in range(1, len(raw)): args.append(String(raw[i])) var config = AppConfig(String("First game"), 1280, 720, False) config.audio = False var rest = parse_app_args(args, config) var max_frames = -1 var screenshot = String() var i = 0 while i < len(rest): if rest[i] == "--frames" and i + 1 < len(rest): max_frames = Int(rest[i + 1]) i += 1 elif rest[i] == "--screenshot" and i + 1 < len(rest): screenshot = rest[i + 1] i += 1 else: raise Error(t"unknown argument {rest[i]}") i += 1 var headless = config.headless var app = App(config^) var game = FirstGame() var frames = 0 while app.run_frame(game): frames += 1 if max_frames >= 0 and frames >= max_frames: break if screenshot.byte_length() > 0 and headless: save_png(app.lib.ptr(), screenshot, app.read_frame()) print( t"frames {frames}, won {game.won}, gpu errors {app.gpu_error_count()}" ) # Step 8: the game's bodies live in the app's physics world, so end the game first. game.shutdown(app) _ = game^ _ = app^ ``` ## `examples/first_game/game.mojo` ```mojo """The game built in docs/TUTORIAL.md: a ball you push around a lit floor to knock crates off a ledge. Every numbered step of the tutorial is a `# Step N` comment in this file.""" from std.memory import Pointer from cinder.app import App, Game from cinder.core.math.mat4 import Mat4 from cinder.core.math.quat import Quat from cinder.core.math.vec import Vec3, Vec4 from cinder.ecs import Entity, LocalTransform from cinder.ffi.cinder_native import ( ImGuiCond_Always, ImGuiWindowFlags_AlwaysAutoResize, ImGuiWindowFlags_NoDecoration, ImGuiWindowFlags_NoInputs, ImVec2, ) from cinder.input import Binding from cinder.physics import BodyDesc, MotionType, RigidBody, Shape from cinder.render import Material, cube, plane from cinder.render.lights import LightKind from cinder.scene.render_sync import CameraComponent, LightSource, MeshRenderer from cinder.tools import CStrings # SDL scancodes used by the default bindings. comptime KEY_A = 4 comptime KEY_D = 7 comptime KEY_R = 21 comptime KEY_S = 22 comptime KEY_W = 26 comptime KEY_SPACE = 44 comptime CRATE_COUNT = 6 comptime LEDGE_HALF = 4.0 comptime FALL_Y: Float32 = -3.0 comptime PUSH_FORCE: Float32 = 300.0 """Newtons applied to the 20 kg ball at full stick deflection.""" struct FirstGame(Game): """A ball pushed by the player knocks crates off a ledge; knock them all off to win. """ var shapes: List[Shape] var bodies: List[UInt32] var ball: UInt32 var crates: List[UInt32] var fallen: Int var won: Bool var elapsed: Float32 var ui: CStrings def __init__(out self): self.shapes = List[Shape]() self.bodies = List[UInt32]() self.ball = 0 self.crates = List[UInt32]() self.fallen = 0 self.won = False self.elapsed = 0.0 self.ui = CStrings() # Step 2: content lives in the ECS world; physics bodies are created in the physics world and # linked to entities through the RigidBody component. def start(mut self, mut app: App) raises: var floor_mesh = app.renderer[].add_mesh( String("ledge"), plane(LEDGE_HALF, 1), False ) var box_mesh = app.renderer[].add_mesh(String("box"), cube(0.5), True) var ball_mesh = app.renderer[].add_mesh( String("ball"), cube(0.35), True ) var grey = app.renderer[].add_material(Material(String("grey"))) var wood = Material(String("wood")) wood.base_color = Vec4(0.7, 0.45, 0.2, 1.0) var wood_id = app.renderer[].add_material(wood) var red = Material(String("red")) red.base_color = Vec4(0.9, 0.15, 0.1, 1.0) red.metallic = 0.2 red.roughness = 0.35 var red_id = app.renderer[].add_material(red) # The ledge: a static box the size of the visible plane. var ledge_shape = app.physics[].box(Vec3(LEDGE_HALF, 0.5, LEDGE_HALF)) self.shapes.append(ledge_shape) self.bodies.append( app.physics[].create_body( BodyDesc( ledge_shape, Vec3(0.0, -0.5, 0.0), MotionType.static(), 0 ), False, ) ) var ledge = app.world.spawn() app.world.add[LocalTransform](ledge, LocalTransform(Mat4.identity())) app.world.add[MeshRenderer]( ledge, MeshRenderer(floor_mesh, grey, False) ) # Crates along the far edge. var crate_shape = app.physics[].box(Vec3(0.5, 0.5, 0.5)) self.shapes.append(crate_shape) for i in range(CRATE_COUNT): var p = Vec3( Float32(i) * 1.2 - Float32(CRATE_COUNT - 1) * 0.6, 0.5, -2.5 ) var crate_desc = BodyDesc(crate_shape, p, MotionType.dynamic(), 1) crate_desc.mass = 10.0 var body = app.physics[].create_body(crate_desc) self.crates.append(body) self.bodies.append(body) var e = app.world.spawn() app.world.add[RigidBody]( e, RigidBody(body, MotionType.dynamic(), p, Quat.identity()) ) app.world.add[MeshRenderer]( e, MeshRenderer(box_mesh, wood_id, True) ) # The ball the player pushes. var ball_shape = app.physics[].sphere(0.35) self.shapes.append(ball_shape) var start = Vec3(0.0, 0.35, 2.5) var ball_desc = BodyDesc(ball_shape, start, MotionType.dynamic(), 1) ball_desc.mass = 20.0 ball_desc.linear_damping = 0.6 ball_desc.allow_sleeping = False self.ball = app.physics[].create_body(ball_desc) self.bodies.append(self.ball) var ball = app.world.spawn() app.world.add[RigidBody]( ball, RigidBody(self.ball, MotionType.dynamic(), start, Quat.identity()), ) app.world.add[MeshRenderer](ball, MeshRenderer(ball_mesh, red_id, True)) # Step 3: a shadowed sun and a camera. Lights and cameras are entities too. var sun = app.world.spawn() app.world.add[LocalTransform]( sun, LocalTransform( Mat4.look_at( Vec3(6.0, 10.0, 4.0), Vec3.zero(), Vec3.unit_y() ).inverse() ), ) app.world.add[LightSource]( sun, LightSource( LightKind.directional(), Vec3(1.0, 0.95, 0.9), 3.0, 0.0, 0.0, 0.0, True, ), ) var camera = app.world.spawn() app.world.add[LocalTransform]( camera, LocalTransform( Mat4.look_at( Vec3(0.0, 7.0, 9.0), Vec3(0.0, 0.0, -0.5), Vec3.unit_y() ).inverse() ), ) app.world.add[CameraComponent](camera, CameraComponent(0.9, 0.05, True)) # Step 4: input is named actions and axes, bound to keys (rebindable at runtime). app.input.bind_axis(String("move_x"), Binding.key(KEY_D, 1.0)) app.input.bind_axis(String("move_x"), Binding.key(KEY_A, -1.0)) app.input.bind_axis(String("move_z"), Binding.key(KEY_S, 1.0)) app.input.bind_axis(String("move_z"), Binding.key(KEY_W, -1.0)) app.input.bind_axis(String("move_x"), Binding.gamepad_axis(0, 1.0)) app.input.bind_axis(String("move_z"), Binding.gamepad_axis(1, 1.0)) app.input.bind_action(String("jump"), Binding.key(KEY_SPACE)) app.input.bind_action(String("restart"), Binding.key(KEY_R)) app.physics[].optimize_broad_phase() # Step 5: gameplay that touches physics runs in the fixed step, at a constant rate. def fixed_update(mut self, mut app: App, dt: Float32) raises: if self.won: return var push = Vec3( app.input.axis(String("move_x")), 0.0, app.input.axis(String("move_z")), ) if push.length() > 1.0: push = push.normalized() app.physics[].add_force(self.ball, push * PUSH_FORCE) if app.input.just_pressed(String("jump")) and self.ball_grounded(app): app.physics[].add_impulse(self.ball, Vec3(0.0, 80.0, 0.0)) self.fallen = 0 for body in self.crates: if app.physics[].position(body).y() < FALL_Y: self.fallen += 1 if self.fallen == CRATE_COUNT: self.won = True if app.physics[].position(self.ball).y() < FALL_Y: self.reset_ball(app) def ball_grounded(self, app: App) -> Bool: return app.physics[].position(self.ball).y() < 0.4 def reset_ball(mut self, mut app: App): app.physics[].set_position_rotation( self.ball, Vec3(0.0, 0.35, 2.5), Quat.identity(), True ) app.physics[].set_linear_velocity(self.ball, Vec3.zero()) app.physics[].set_angular_velocity(self.ball, Vec3.zero()) # Step 6: per-frame logic (timers, camera, UI state) runs once per rendered frame. def update(mut self, mut app: App, dt: Float32) raises: if not self.won: self.elapsed += dt if app.input.just_pressed(String("restart")): self.restart(app) def restart(mut self, mut app: App): for i in range(len(self.crates)): var p = Vec3( Float32(i) * 1.2 - Float32(CRATE_COUNT - 1) * 0.6, 0.5, -2.5 ) app.physics[].set_position_rotation( self.crates[i], p, Quat.identity(), True ) app.physics[].set_linear_velocity(self.crates[i], Vec3.zero()) app.physics[].set_angular_velocity(self.crates[i], Vec3.zero()) self.reset_ball(app) self.fallen = 0 self.won = False self.elapsed = 0.0 # Step 7: the HUD is immediate-mode ImGui through the bindings; CStrings keeps the C strings # alive for the frame. def draw_ui(mut self, mut app: App) raises: self.ui.clear() var lib = app.lib.ptr() lib[].ImGui_SetNextWindowPos( ImVec2(12.0, 12.0), Int32(ImGuiCond_Always) ) if lib[].ImGui_Begin( self.ui.c(String("hud")), Optional[Pointer[Bool, MutUntrackedOrigin]](), Int32( ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoInputs ), ): var line = String( t"crates knocked off {self.fallen}/{CRATE_COUNT} time" t" {Int(self.elapsed)} s" ) if self.won: line = String(t"you win in {Int(self.elapsed)} s R restarts") lib[].ImGui_TextUnformatted(self.ui.c(line)) lib[].ImGui_TextUnformatted( self.ui.c(String("WASD push, Space hop, R restart")) ) lib[].ImGui_End() # Step 8: physics objects belong to the game; release them before the App goes away. def shutdown(mut self, mut app: App): for b in self.bodies: app.physics[].destroy_body(b) for s in self.shapes: app.physics[].destroy_shape(s) self.bodies.clear() self.shapes.clear() ``` ## `examples/first_game/__init__.mojo` ```mojo """The tutorial game from docs/TUTORIAL.md.""" from .game import CRATE_COUNT, PUSH_FORCE, FirstGame ``` --- > Cinder 0.1.0 documentation: Sample game entry point > Canonical page: https://cinder.ash10.dev/docs/examples/sample-game/ · Source: `sample/main.mojo` in the engine repository (engine commit 47c9352, imported 2026-11-09) > Index of every page for agents: https://cinder.ash10.dev/llms.txt # Sample game entry point `sample/main.mojo` from the engine repository, verbatim: the entry point of the third-person sample game (ADR-0014). The game itself lives in `sample/game/` (components, levels, settings, gameplay) and is built only on public engine APIs. ```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 ``` ## `sample/main.mojo` ```mojo """Cinder sample game entry point. pixi run sample [--headless] [--level arena|sponza] [--autopilot] [--frames N] [--profile] [--screenshot PATH] `--headless` renders offscreen (tests, benchmarks); `--frames N` stops after N frames; `--screenshot PATH` writes the last headless frame as a PNG. """ from std.sys import argv from cinder.app import App, AppConfig, parse_app_args from cinder.core.image import save_png from game import SampleGame def main() raises: var args = List[String]() var raw = argv() for i in range(1, len(raw)): args.append(String(raw[i])) var config = AppConfig(String("Cinder sample"), 1280, 720, False) var rest = parse_app_args(args, config) var level = String("arena") var autopilot = False var max_frames = -1 var screenshot = String() var verbose = False var i = 0 while i < len(rest): if rest[i] == "--level" and i + 1 < len(rest): level = rest[i + 1] i += 1 elif rest[i] == "--autopilot": autopilot = True elif rest[i] == "--verbose": verbose = True elif rest[i] == "--frames" and i + 1 < len(rest): max_frames = Int(rest[i + 1]) i += 1 elif rest[i] == "--screenshot" and i + 1 < len(rest): screenshot = rest[i + 1] i += 1 else: raise Error(t"unknown argument {rest[i]}") i += 1 var game = SampleGame(level, autopilot) game.verbose = verbose config.render = game.settings.render_settings() var config_headless = config.headless var app = App(config^) var frames = 0 while app.run_frame(game): frames += 1 if max_frames >= 0 and frames >= max_frames: break if screenshot.byte_length() > 0 and config_headless: save_png(app.lib.ptr(), screenshot, app.read_frame()) print( t"frames {frames}, collected" t" {game.collected}/{game.total_collectables}, won {game.won}, gpu" t" errors {app.gpu_error_count()}" ) # The game holds physics and audio objects that live in the app: end it first. game.shutdown(app) _ = game^ _ = app^ ``` --- > Cinder 0.1.0 documentation: Using Cinder with AI coding agents > Canonical page: https://cinder.ash10.dev/agents/ · Source: generated by this site (engine commit 47c9352, imported 2026-11-09) > Index of every page for agents: https://cinder.ash10.dev/llms.txt # Using Cinder with AI coding agents Cinder's documentation is built to be read by agents as well as people. Everything on this site is available as plain Markdown or JSON, there is a ready-made context file for coding agents, and a read-only MCP server answers documentation queries. Everything here is generated from the engine repository at commit `47c9352` (Cinder 0.1.0). ## 1. Drop the agent brief into your repository The brief covers what Cinder is and its honest status, the container build workflow and every `pixi` task you need, the layering rules, the zero-allocation rule and the Mojo 1.2.0 pitfalls that the engine's own notes record. Claude Code reads `CLAUDE.md`; Codex, Cursor, Aider and most other agents read `AGENTS.md`. The content is identical. ```bash curl -fsSL https://cinder.ash10.dev/agents/AGENTS.md -o AGENTS.md curl -fsSL https://cinder.ash10.dev/agents/CLAUDE.md -o CLAUDE.md ``` Download: [AGENTS.md](https://cinder.ash10.dev/agents/AGENTS.md) · [CLAUDE.md](https://cinder.ash10.dev/agents/CLAUDE.md). The full text is at the [end of this page](#the-brief). ## 2. Point the agent at the docs | What | URL | Use it for | |---|---|---| | llms.txt index | [https://cinder.ash10.dev/llms.txt](https://cinder.ash10.dev/llms.txt) | A map of every page with one-line summaries, per the llms.txt convention. | | Full text | [https://cinder.ash10.dev/llms-full.txt](https://cinder.ash10.dev/llms-full.txt) | Every page plus the brief in one file, for long-context models. | | Any page as Markdown | page URL without the trailing slash plus `.md`, e.g. [/docs/guides/ecs.md](https://cinder.ash10.dev/docs/guides/ecs.md) | Fetching exactly the guide or API page the task needs. | | JSON index | [https://cinder.ash10.dev/docs/index.json](https://cinder.ash10.dev/docs/index.json) | Pages, headings and 1306 API symbols with signatures, summaries and anchors. | | Search index | [https://cinder.ash10.dev/search-index.json](https://cinder.ash10.dev/search-index.json) | Section-level plain text, for local retrieval. | | MCP | `https://cinder.ash10.dev/mcp` | Search and page tools over Streamable HTTP (below). | Every HTML page also advertises its Markdown twin with ``, and the server returns the Markdown when a request sends `Accept: text/markdown`. Each docs page has **View as Markdown**, **Copy page as Markdown** and **Copy agent prompt** buttons at its foot. ## 3. Connect the MCP server The site runs a small, stateless, read-only MCP server next to the static pages. It needs no key and stores nothing. Tools: `search_docs` (ranked hits across pages, headings and API symbols), `get_page` (a page or one section as Markdown), `get_symbol` (signature and summary of an API symbol, e.g. `CommandBuffer` or `cinder.physics.character.CharacterController`) and `list_pages`. Every page is also exposed as an MCP resource. Claude Code: ```bash claude mcp add --transport http cinder-docs https://cinder.ash10.dev/mcp ``` Clients configured with JSON (Cursor, Claude Desktop through a remote connector, and others): ```json { "mcpServers": { "cinder-docs": { "type": "http", "url": "https://cinder.ash10.dev/mcp" } } } ``` Why an MCP endpoint and not only static files: the static files already cover agents that can fetch URLs, but an MCP server lets an agent search the API by symbol name without loading the whole corpus. It is served by the same Node process as the pages (the ash10 panel runs Node), it is read-only and stateless, and it can be switched off with `MCP_ENABLED=0` without affecting anything else. ## 4. Prompts that work Each prompt assumes the brief is in the repository root. Copy, then fill in the brackets. Start a new game on Cinder: ```text Read AGENTS.md, then https://cinder.ash10.dev/docs/tutorial.md and https://cinder.ash10.dev/docs/examples/first-game.md. Create a new game in games/[name]/ modelled on examples/first_game: a struct implementing the Game trait (start, fixed_update, update, draw_ui) run by cinder.app.App. The game is: [describe it]. Use only public engine APIs, keep physics in fixed_update, keep every per-frame path allocation-free, add a headless test like tests/examples/test_first_game.mojo, and run it with tools/in_container.sh. Report exactly which commands you ran and their results. ``` Add a feature to an engine subsystem: ```text Read AGENTS.md. I want to change cinder/[module]: [the change]. Before editing, read https://cinder.ash10.dev/docs/guides/[module].md, https://cinder.ash10.dev/docs/api/[module].md and the ADRs it links. Respect the layering rules (no imports from higher layers, never hand-edit cinder/ffi), add docstrings to any new public symbol, add or extend a test under tests/[module]/, then run pixi run lint, format-check, api-docs-check and the module's test task in the container. Tell me if the change needs an ADR. ``` Review a change for the zero-allocation rule: ```text Read AGENTS.md, section "The zero-allocation rule", and https://cinder.ash10.dev/docs/mojo-notes.md, section "Allocation facts measured in Phase 8". Review the diff below for anything reachable from App.run_frame that allocates per frame (List growth, list literals, long String building, new Lists in loops). For each finding give the file, line, why it allocates and an allocation-free rewrite. Then tell me to run tools/in_container.sh pixi run test-zero-alloc to confirm. [paste the diff] ``` Fix a Mojo compile error: ```text Read AGENTS.md, section "Mojo 1.2.0: what bites", and https://cinder.ash10.dev/docs/mojo-notes.md. This code fails to compile under Mojo 1.2.0 (pinned; do not suggest nightly features or pre-1.0 syntax such as fn, let, alias, inout or UnsafePointer). Explain the error and give a fix that also passes tools/lint_legacy_mojo.sh. [paste the code and the compiler output] ``` Answer a question from the docs only: ```text Use the cinder-docs MCP server (or https://cinder.ash10.dev/llms.txt if it is not connected). Answer from the documentation only and cite the page URL and section anchor for every claim. If the docs do not say, say so. Question: [question] ``` ## 5. Ground rules to give any agent - Cinder is pinned to Mojo 1.2.0. Much of what models remember about Mojo (`fn`, `let`, `alias`, `inout`, `UnsafePointer`, `@parameter if`) is removed or banned here. - Linux x86-64 is the only verified platform. Do not let an agent claim macOS support or performance budgets: the [status report](https://cinder.ash10.dev/docs/report.md) is the source of truth. - Mojo has no Windows build; on Windows everything runs through `tools/in_container.sh`. - The generated bindings in `cinder/ffi` and the generated API reference in `docs/api` are never edited by hand. ## The brief This is the exact content of [/agents/AGENTS.md](https://cinder.ash10.dev/agents/AGENTS.md). ~~~~markdown # AGENTS.md: working on or with the Cinder engine Context file for AI coding agents (Claude Code reads it as `CLAUDE.md`; Codex, Cursor and others read `AGENTS.md`). Drop it at the root of a Cinder checkout or of a game built on Cinder. Generated by https://cinder.ash10.dev from the engine documentation at commit 47c9352 (Cinder 0.1.0, imported 2026-11-09). When this file and the engine repository disagree, the repository wins; re-download this file. ## What Cinder is A 3D game engine written in **Mojo 1.2.0** (pinned; nightlies are never used) over one C11 shim, `libcinder_native`, wrapping wgpu-native (WGSL), SDL3, Jolt (through joltc), cgltf, KTX-Software, meshoptimizer, miniaudio, Dear ImGui and Tracy. Honest status: Linux x86-64 is built and tested. macOS arm64 is a declared target but has **not been built or run**. Benchmarks ran only on Mesa lavapipe (a CPU rasteriser), so the performance budgets are recorded there, not met on reference hardware. Never claim macOS support or performance numbers that `REPORT.md` does not state. ## Documentation for agents - Index of every page: https://cinder.ash10.dev/llms.txt; everything in one file: https://cinder.ash10.dev/llms-full.txt - Any docs page as Markdown: its URL without the trailing slash plus `.md`, e.g. https://cinder.ash10.dev/docs/guides/ecs.md - Pages and every API symbol (name, signature, summary, URL) as JSON: https://cinder.ash10.dev/docs/index.json - MCP server (read-only search and page tools): `https://cinder.ash10.dev/mcp` (Streamable HTTP) - Read first: the guide for the module you touch (https://cinder.ash10.dev/docs/guides/), then its API page (https://cinder.ash10.dev/docs/api/), then `docs/MOJO_NOTES.md` before writing any Mojo. ## Build, run and test Mojo has no Windows build. On Windows (and anywhere without a native toolchain) every command runs in the Linux toolchain container through `tools/in_container.sh`; on a native Linux host drop the prefix. If the repository is reached through a substituted drive on Windows, set `CINDER_HOST_ROOT` to its real path. ```bash docker build -t cinder-toolchain:1 -f docker/Dockerfile.toolchain docker # once tools/in_container.sh pixi install # once tools/in_container.sh pixi run native # build libcinder_native (Release) + CTest tools/in_container.sh pixi run test # lint, API doc check, every Mojo test, X11 + Wayland spikes tools/in_container.sh pixi run format-check # mojo format, then git diff --exit-code tools/in_container.sh pixi run lint # banned legacy Mojo forms tools/in_container.sh pixi run api-docs-check # every public symbol has a docstring tools/in_container.sh pixi run api-docs # regenerate docs/api/ from docstrings tools/in_container.sh pixi run native-sanitize # Debug shim with ASan + UBSan tools/in_container.sh pixi run test-asan # tests against the sanitized shim tools/in_container.sh pixi run test-zero-alloc # zero Mojo allocations per steady-state frame tools/in_container.sh pixi run test-golden # 16 golden images (SSIM + pixel thresholds) tools/in_container.sh pixi run example # the tutorial game (docs/TUTORIAL.md) tools/in_container.sh pixi run test-example # the tutorial game, compiled and run headless tools/in_container.sh pixi run sample # the sample game, windowed tools/in_container.sh pixi run sample-headless # the sample game, offscreen autopilot tools/in_container.sh pixi run bench-all # stress + sample benchmarks, 10% regression gate tools/in_container.sh pixi run -e bindgen bindgen # regenerate cinder/ffi from the C headers ``` A single test program runs as `mojo run -I . tests//test_.mojo` with `CINDER_NATIVE_LIB=native/build/release/lib/libcinder_native.so` set (see the matching `pixi.toml` task). There is no `mojo test` subcommand in 1.2.0. Every Mojo build uses `--Werror`. Headless runs (`--headless`) need no display and are what tests, CI and benchmarks use. ## Layering rules (never break these) Dependencies point downward only: a module imports from layers below it, never from above. ``` sample/ (and your game) -> cinder/app -> cinder/tools cinder/{render,physics,anim,audio,input,assets,scene} -> cinder/ecs, cinder/gfx, cinder/platform -> cinder/core -> cinder/ffi -> native/ (libcinder_native) ``` - `cinder/ffi` is generated by `tools/bindgen` and **never edited by hand**; a layout test checks every struct offset and enum constant against the C compiler. Change the C header, then regenerate. - One shim, one shared library, flat C11 ABI. Every C callback lives in C and writes fixed-size events into lock-free rings; Mojo drains them once per frame on the main thread (the pull model, ADR-0003). Mojo code never runs on a C thread. - `cinder/platform` is the only module with per-OS branches. `cinder/core` does no GPU or window I/O. - Every `unsafe_` call carries a `# SAFETY:` comment. Each shared library is loaded exactly once. - Games use public engine APIs only (the sample game is the reference: `sample/`). ## The zero-allocation rule A steady-state frame performs **zero heap allocations from Mojo code**; `tests/perf/test_zero_alloc.mojo` asserts it through an LD_PRELOADed counter (`pixi run test-zero-alloc`). In anything reachable from `App.run_frame` (systems, render, animation, physics sync, your `fixed_update`/`update`/`draw_ui`): - Keep per-frame data in persistent scratch fields that are cleared, never freed. `List.clear()` keeps capacity; `resize(n, fill)` allocates only when growing. - No `List[...] = [x]` literals, no growing lists, no new `String` building of long text per frame. - Use `Array[T, N]` locals (stack storage) for small fixed-size per-call data. - Moving a `List` out of a struct and back does not allocate (how the frame graph recycles records). - Third-party libraries may still allocate; the rule covers Mojo code. ## Mojo 1.2.0: what bites The compiler and the current Mojo docs win over memory: Mojo changed a lot in 1.0 to 1.2. Before writing Mojo, read https://cinder.ash10.dev/docs/mojo-notes.md. Key forms: - `def` is the only function keyword (`fn` removed in 1.1.0). Functions do not raise unless marked `raises`. `var` declares every variable; there is no `let`; `alias` is gone, use `comptime`. - Conventions: default (immutable, `imm` when explicit), `mut`, `var` (owned, caller passes `x^`), `ref`, `out`, `deinit` (`def __deinit__(deinit self)`). `read`, `inout`, `borrowed`, `owned` are errors or banned. - `comptime if` / `comptime for` replace `@parameter if/for`. Constraints: `where else "msg"`. - Structs are `Movable`, not copyable, by default; declare `Copyable` and prefer `x.copy()`. - `Pointer[T, origin]` replaces `UnsafePointer`; unsafe operations are prefixed (`unsafe_load`, `unsafe_store`, `ptr[unsafe_offset=i]`, `unsafe_free`). `memcpy`/`memset` are gone. - `Array[T, N]` replaces `InlineArray`; a list literal defaults to `Array`, so annotate `List` explicitly. - Format with t-strings: `t"value={x}"`. - **ASAP destruction.** Values die after their last *syntactic* use. A stack value handed to C by pointer must be followed by `std.benchmark.keep(value)` after the last C call that reads it; an `OwnedDLHandle` closes at its last use (function pointers live only in the struct owning the handle); early returns skip a trailing `keep`, so structure such code as if/else with one `keep` at the end. - `Optional[def(...) thin abi("C")]` is 16 bytes: nullable C function pointers are bound as opaque pointers. - A `mut self` method cannot also take a field of `self` (mutably or not) as an argument: copy to a local or let the method touch `self.field` directly. - Inside a parametric struct write `Self.T` in field and method signatures. - Struct fields are destroyed in declaration order: a struct holding a native library and objects whose destructors call into it declares the library **last**. - A struct storing pointers to its own fields breaks when moved: box it (`cinder/core/boxed.mojo`). - wgpu-native aborts the process on a validation error inside `wgpuQueueSubmit`; keep copy strides 256-aligned and pass them explicitly. - Tests: module-level `def test_*() raises:` functions, and the file ends with `def main() raises: TestSuite.discover_tests[__functions_in_module()]().run()`. Banned in `*.mojo` (CI lint `tools/lint_legacy_mojo.sh` fails on any of them): `tools/lint_legacy_mojo.sh`, `fn `, `alias `, `inout`, `borrowed`, `owned `, `let `, `@parameter`, `__del__`, `InlineArray`, `StringSlice`, `UnsafePointer`, `MutUnsafePointer`, `ImmUnsafePointer`, `std.gpu`, `.load(`, `.store(`, `.free(`, `memcpy(`, `memset(`, `read `, `@parameter if`, `@parameter for`, `@register_passable`, `__comptime_assert`, `steal_data`, `.size`. ## Conventions Right-handed, Y up, -Z forward, metres, column-major matrices, reverse-Z with an infinite far plane, linear lighting, sRGB render targets, RGBA16F intermediates. Recoverable failures raise typed errors; broken invariants abort with a message. The simulation is a fixed 60 Hz step (`Game.fixed_update`, where physics happens); rendering runs once per frame (`Game.update`, `Game.draw_ui`). A program is an `App` plus your `Game` struct (`start`, `fixed_update`, `update`, `draw_ui`). End the game before the app, explicitly, because the game's bodies live in the app's physics world: ```mojo game.shutdown(app) _ = game^ _ = app^ ``` ## Threading The main thread owns SDL, the window, presentation and ImGui. Everything else runs on the work-stealing job system (one worker per physical core minus one). Shared state crosses threads only through jobs, atomics or the locks named in `docs/THREADING.md`. Jolt steps on its own two-thread pool (ADR-0012). miniaudio's device thread runs C only. ## How to work in this repository - Read the relevant guide and ADR before changing a subsystem; a change to a pinned version or a layering rule needs a new ADR. - Add or update tests with every change; never skip a test to make a run pass. - Public symbols need docstrings (`pixi run api-docs-check`); regenerate `docs/api/` after API changes. - Run `pixi run format-check`, `pixi run lint` and `pixi run test` before declaring work done, and report results faithfully, including anything not run (for example macOS). ~~~~ --- > Cinder 0.1.0 documentation: Subsystem guides > Canonical page: https://cinder.ash10.dev/docs/guides/ · Source: generated by this site (engine commit 47c9352, imported 2026-11-09) > Index of every page for agents: https://cinder.ash10.dev/llms.txt # Subsystem guides One guide per engine module: key types and ownership, setup, per-frame use, threading rules and pitfalls. Each links to its generated API reference. - [Animation guide (cinder/anim)](https://cinder.ash10.dev/docs/guides/anim.md): cinder/anim is skeletal animation written in Mojo: skeletons built from glTF skins or read from cooked .cskel files, clip sampling into local poses, blend trees evaluated on a shared phase, animators with states, crossfades, events and root motion, and an AnimationSystem that… - [Application runtime (cinder/app)](https://cinder.ash10.dev/docs/guides/app.md): cinder/app is the top of the engine: App owns every subsystem, runs the fixed-step game loop and renders, either into an SDL window or, with --headless, into an offscreen texture that can be read back. Gameplay implements the Game trait and is called from App.run_frame. The… - [Assets guide (cinder/assets)](https://cinder.ash10.dev/docs/guides/assets.md): cinder/assets gets content from disk into the renderer. It has two paths: - [Audio (cinder/audio)](https://cinder.ash10.dev/docs/guides/audio.md): cinder/audio is the engine's sound layer over miniaudio. It provides mixing buses, decoded and streamed sounds, one-shots that clean up after themselves, 3D spatialisation with distance attenuation, a single listener, and an ECS bridge that moves sounds with entities and the… - [cinder/core guide](https://cinder.ash10.dev/docs/guides/core.md): cinder/core is the lowest Mojo layer of the engine: logging, assertions, a bump arena, generational handles and pools, a bounded ring, byte packing and hashing, file helpers, a monotonic clock with a fixed-step accumulator, CPU images, the job system, Tracy zones, Boxed heap… - [ECS guide (cinder/ecs)](https://cinder.ash10.dev/docs/guides/ecs.md): cinder/ecs is the archetype entity component system every gameplay-facing module builds on. It sits directly above cinder/core (it uses core/math for Mat4 and core/jobs for the scheduler) and below render, physics, anim, audio, assets and scene, which define their own… - [cinder/gfx guide](https://cinder.ash10.dev/docs/guides/gfx.md): cinder/gfx is the safe, typed layer over wgpu-native. It owns the wgpu instance, adapter, device and queue (Gfx), keeps every GPU object the engine creates in generational pools behind typed handles (Resources), compiles WGSL with editor-friendly diagnostics and hot reload… - [Input (cinder/input)](https://cinder.ash10.dev/docs/guides/input.md): cinder/input maps raw platform events to named gameplay controls. A game asks pressed("jump") or axis("move_x") instead of testing scancodes, players can rebind controls at runtime, and the whole map saves to a small text file. The package sits beside audio, physics, anim and… - [Physics guide (cinder/physics)](https://cinder.ash10.dev/docs/guides/physics.md): cinder/physics is the safe Mojo layer over Jolt. Jolt is reached through joltc inside libcinder_native; the parts of the shim that need C callbacks live in native/src/physics_shim.c (query filters, closest-hit collectors, CharacterVirtual extended update defaults) and… - [cinder/platform guide](https://cinder.ash10.dev/docs/guides/platform.md): cinder/platform is the thin Mojo layer over SDL3 and the native shim's surface helpers. It loads libcinder_native exactly once, initialises SDL, creates windows, drains SDL's event queue into engine-level Events, opens and closes gamepads as they appear, creates the wgpu… - [cinder/render: the renderer](https://cinder.ash10.dev/docs/guides/render.md): cinder/render turns a RenderScene (camera, lights, instances, joint palettes, environment) into one submitted command buffer per frame. It is a clustered forward+ renderer with cascaded and atlas shadow maps, GPU-baked image-based lighting, GTAO, physically based bloom, AgX… - [Scene guide (cinder/scene)](https://cinder.ash10.dev/docs/guides/scene.md): cinder/scene is the layer between the ECS and the rest of the engine's content pipeline. It has three independent parts: - [Tools: debug overlay, Tracy profiling, headless mode](https://cinder.ash10.dev/docs/guides/tools.md): Cinder's developer tooling is three things: the Dear ImGui debug overlay in cinder/tools, the Tracy client wrapper in cinder/core/profile.mojo, and the --headless mode of cinder/app that lets the same executables run without a display for tests, benchmarks and captures. --- > Cinder 0.1.0 documentation: Animation guide (cinder/anim) > Canonical page: https://cinder.ash10.dev/docs/guides/anim/ · Source: `docs/guides/anim.md` in the engine repository (engine commit 47c9352, imported 2026-11-09) > Index of every page for agents: https://cinder.ash10.dev/llms.txt # Animation guide (`cinder/anim`) `cinder/anim` is skeletal animation written in Mojo: skeletons built from glTF skins or read from cooked `.cskel` files, clip sampling into local poses, blend trees evaluated on a shared phase, animators with states, crossfades, events and root motion, and an `AnimationSystem` that advances every animator as its own job and publishes `JointPalette` components for the render bridge. There is no native code in the path; the only C involved is the glTF importer that produces the clips. In the module map (`docs/ARCHITECTURE.md`) the package sits beside `physics` and `render`. It imports `cinder/core` (math, jobs), `cinder/assets` (`Clip`, `ClipChannel`, `GltfAsset`, the cooked-file `Cursor`), `cinder/ecs` and `cinder/scene/render_sync` (`JointPalette`), and is owned by `cinder/app`, which holds one `AnimationSystem`. | Module | Contents | |---|---| | `anim/skeleton.mojo` | `Skeleton`, `skeleton_from_gltf`, `write_skeleton`, `read_skeleton`, `MAX_BONES` | | `anim/sampling.mojo` | `Pose`, `sample_clip`, `wrap_time` (plus `sample_component`, `sample_rotation`) | | `anim/blend.mojo` | `BlendNode`, `BlendTree` | | `anim/animator.mojo` | `AnimationEvent`, `AnimationState`, `Animator` | | `anim/system.mojo` | `AnimatorRef`, `AnimationSystem` | Signatures are in `docs/api/anim.md`; `tests/anim/test_animation.mojo` covers every feature below and is where the snippets come from. ## Units and conventions Clip times are seconds. A blend tree and an animator run on a normalised **phase** in `[0, 1)`; leaf clips sample at `phase * duration`. Clips keep glTF **node** indices in their channels and `Skeleton.node_to_bone` maps them to bones, so clips from the same glTF file work with the skeleton of any of its skins. Bones are stored parents-first; `joints` lists the skin's joints in palette order. Matrices are column-major `Mat4`; rest transforms are `Transform` (translation, rotation, scale) in parent space. The palette is expressed in the mesh node's rest space: `inverse(mesh node world) * bone world * inverse bind`, the same convention the importer uses at the bind pose, so the render instance's transform is the mesh node's world transform and the palette moves bones relative to it. ## Skeletons ```mojo from cinder.anim import Skeleton, read_skeleton, skeleton_from_gltf, write_skeleton from cinder.assets.gltf import load_gltf var asset = load_gltf(lib.ptr(), String("assets/samples/Fox/Fox.glb")) var skeleton = skeleton_from_gltf(asset, 0) # skin 0 print(skeleton.bone_count(), skeleton.joint_count()) # >= 24 bones, 24 joints for the Fox var spine = skeleton.find_bone(String("b_Spine01_02")) var bytes = write_skeleton(skeleton) # CSKL v1 var back = read_skeleton(bytes.copy()) # raises on bad magic, version or trailing bytes ``` `skeleton_from_gltf(asset, skin)` includes every joint of the skin plus the ancestors needed to reach the outermost ancestor shared by all joints, so an animated parent that is not itself a joint still moves its children. Bones are emitted depth-first from the roots so parents precede children (`validate()` enforces this, together with `1 <= bone_count <= MAX_BONES`, which is 256, and consistent `joints` / `inverse_bind` lengths). The mesh node is the first node that uses the skin and carries a mesh; `mesh_node`, `mesh_node_inverse` and `morph_count` (morph target weights animated on that node, 0 when none) come from it. `Skeleton` fields are public and a skeleton can be built by hand (the tests' `_two_bone_skeleton` does so). `world_transforms(local)` / `world_transforms_into(local, out)` give bone matrices in model space; `palette(local)` / `palette_into(local, worlds, out)` give skinning matrices in palette order. The `_into` forms resize their output lists and allocate nothing once sized; the plain forms allocate and are for tests and tools (ADR-0014). The cooker (`cinder/assets/cook.mojo`) writes one `.cskel` per skin under the logical name `#skeleton` and one `.cclip` per animation. `write_skeleton` stores every bone/node index plus one so that -1 round-trips as 0; `read_skeleton` consumes its byte list and revalidates the result, so a cooked skeleton from an older version is rejected rather than misread. ## Poses and clip sampling A `Pose` is the skeleton's local transforms plus morph weights. `Pose(skeleton)` starts at the rest pose with zero weights; `reset(skeleton)` returns to it, `copy_from(other)` copies without allocating and `blend_with(b, t)` lerps in place (translation and scale linearly, rotation with `Quat.nlerp` along the shorter arc, weights linearly). ```mojo from cinder.anim import Pose, sample_clip, wrap_time from cinder.assets.cooked import Clip, ClipChannel var clip = Clip(String("synthetic")) clip.duration = 2.0 # node 0, translation (path 0), linear (interpolation 0), 3 components: (0,0,0) -> (2,0,0) over 2 s clip.channels.append(ClipChannel(0, 0, 0, 3, _floats(0.0, 2.0), _floats(0.0, 0.0, 0.0, 2.0, 0.0, 0.0))) var pose = Pose(skeleton) sample_clip(clip, skeleton, 0.5, pose) # root translation is (0.5, 0, 0) sample_clip(clip, skeleton, 5.0, pose) # past the end: the last key holds var looped = wrap_time(2.5, clip.duration) # 0.5 ``` `sample_clip(clip, skeleton, time, pose)` writes every channel of the clip at `time` into `pose`. Sampling follows glTF: `interpolation` 0 is linear (slerp for rotations), 1 is step, 2 is cubic spline (`values` holds in-tangent, value, out-tangent per key; tangents are per second and scaled by the key span; spline rotations are normalised). Before the first key or after the last the end value holds, so looping is the caller's job through `wrap_time(time, duration)`, which maps any time into `[0, duration)`. Channels whose node is not in the skeleton are skipped, and because the pose starts from the rest pose, channels a clip does not animate keep the skeleton's rest values. Weight channels (`path` 3) are applied only when they target the mesh node. Clips come from the cooked `.cclip` format or straight from a `GltfAsset`: ```mojo var clips = List[Clip]() for a in range(len(asset.animations)): var clip = Clip(asset.animations[a].name) clip.duration = asset.animations[a].duration for ch in asset.animations[a].channels: clip.channels.append( ClipChannel(ch.node, ch.path, ch.interpolation, ch.components, ch.times.copy(), ch.values.copy()) ) clips.append(clip^) ``` ## Blend trees A `BlendTree` is a flat list of `BlendNode`s with a `root`, plus named parameters. Leaves are clips (`add_clip(clip_index, speed=1.0)`); inner nodes are `add_blend_1d(parameter)` or `add_blend_2d(parameter_x, parameter_y)`. `add_child(node, child, at: Vec2)` attaches a child with full weight at parameter value `at` (1D nodes use `at.x()`; their children must be added in increasing order). A node takes at most `MAX_CHILDREN = 8` children. A tree must be acyclic: `add_child` raises when the edge would close a cycle (`add_child(n, n)`, or attaching an ancestor of `n` under `n`); a node may still be shared by several parents. Finish with `set_root`; `validate(clip_count)` runs when the tree becomes an animator state and also rejects child indices out of range and cycles made by editing `nodes` directly. ```mojo from cinder.anim import AnimationState, Animator, BlendTree from cinder.core.math.vec import Vec2 var tree = BlendTree() var speed = tree.add_parameter(String("speed"), 0.0) var blend = tree.add_blend_1d(speed) var walk_leaf = tree.add_clip(walk_index) var run_leaf = tree.add_clip(run_index) tree.add_child(blend, walk_leaf, Vec2(1.0, 0.0)) # pure Walk at speed 1 tree.add_child(blend, run_leaf, Vec2(4.0, 0.0)) # pure Run at speed 4 tree.set_root(blend) var tree2 = BlendTree() var px = tree2.add_parameter(String("x")) var py = tree2.add_parameter(String("y")) var node2 = tree2.add_blend_2d(px, py) tree2.add_child(node2, tree2.add_clip(survey_index), Vec2(0.0, 0.0)) tree2.add_child(node2, tree2.add_clip(walk_index), Vec2(0.0, 1.0)) tree2.add_child(node2, tree2.add_clip(run_index), Vec2(1.0, 0.0)) tree2.set_root(node2) ``` Weights: a 1D node interpolates linearly between the two thresholds around the parameter and clamps outside them. A 2D node uses gradient-band interpolation (Johansen 2009): smooth, sums to one, and reproduces each child exactly at its own point. `leaf_weights()` / `leaf_weights_into(weights)` return one weight per node (leaves carry their blended weight, inner nodes their subtree's). Phase synchronisation: every leaf samples at `phase * clip.duration`, so a walk and a run blended 50/50 stay in step (same foot down at the same phase). The phase advances at `cycle_rate(clips)` per second, the weighted average of each leaf's `speed / duration`; `cycle_rate_with(clips, weights)` is the allocation-free form the animator uses. With the Fox at `speed = 2.5` the rate is `0.5 / walk.duration + 0.5 / run.duration`, which the test asserts. `evaluate(clips, skeleton, phase, pose, scratch)` and `evaluate_with(..., pose, scratch, weights)` blend every weighted leaf into `pose`; `scratch` is a second pose of the same skeleton. Blending is sequential normalised lerp: leaf *i* is mixed in with weight `w_i / (accumulated + w_i)`, so the result is the weighted average regardless of node order. ## The animator `Animator(skeleton^, clips^)` owns a skeleton, its clips, the current `pose` and the scratch it needs. States are `AnimationState(name, tree^, looping=True)`; `add_state` validates the tree against the clip count and returns the state index, and `add_clip_state(name, clip, looping, speed)` is the one-leaf shortcut. `find_state(name)` looks an index up; `set_parameter(name, value)` sets the parameter on every state that declares it and raises if none does. ```mojo var animator = Animator(skeleton.copy(), clips.copy()) var survey = animator.add_clip_state(String("survey"), survey_index) var locomotion = animator.add_state(AnimationState(String("locomotion"), tree^)) animator.add_event(locomotion, 0.5, String("footstep")) animator.play(locomotion) animator.set_parameter(String("speed"), 2.5) animator.update(dt) # phase advances at the blended cycle rate for ev in animator.take_events(): print(ev.state, ev.name) animator.play(survey, 0.5) # crossfade over 0.5 s animator.update(0.25) print(animator.fade_weight()) # 0.5: halfway between the two states var palette = animator.palette() # List[Mat4] in palette order ``` - **Playback.** `play(state, fade_seconds=0, start_phase=0, restart=False)` switches states and clears `finished`; playing the current state again is a no-op (so calling it every frame is safe), even when that state is non-looping and has finished. Pass `restart=True` to start the current state again from `start_phase`, for example to replay a finished jump; with a fade the old playthrough fades out as the new one starts. `update(dt)` raises if nothing has been played. `speed` is a global rate multiplier (the test sets it to 0 to freeze phases and watch the fade alone). `current_state()`, `phase()` and `finished` (a non-looping state reached phase 1 and holds its last pose) report where playback stands. - **Crossfades.** With `fade_seconds > 0` the outgoing state keeps advancing at its own rate; both trees are evaluated and the poses are blended with a linear weight, `fade_weight() = 1 - remaining / duration`, so the fade starts exactly at the outgoing pose and ends exactly in the new one. - **Events.** `add_event(state, phase, name)` with `phase` in `[0, 1)`. During `update` every event whose phase lies in `(phase_before, phase_after]` on the unwrapped timeline fires once, including across loop wraps and several whole cycles in one long `dt` (a 2 s update over a 1 s looping clip with events at 0.0 and 0.5 fires four). `take_events()` returns and clears the list; events are reported for the current state only. - **Root motion.** `enable_root_motion(bone=0)` removes the horizontal (X, Z) translation of `bone` from the pose, pinning it to the rest translation while keeping its Y so authored crouches and jumps still show, and accumulates the displacement in model space; `take_root_motion()` returns and clears it. Loop wraps are unwrapped exactly: the remainder of the old cycle, any full cycles in between, and the start of the new one. The flag can be turned off again by setting `root_motion = False`. Root motion evaluates the tree at extra phases each update with freshly allocated scratch poses, so it is not allocation-free; the sample game keeps it off and drives the character from the controller instead (ADR-0012). - **Outputs.** `pose` (local transforms plus `morph_weights`), `palette()` / `palette_into(out)` (skinning matrices for the render), `bone_matrices()` / `bone_matrices_into(out)` (every bone in model space, for attachments, IK or debug draw). ## The animation system and the render bridge Animators live in `AnimationSystem.animators`; `add(animator^)` returns the index and an entity refers to it with `AnimatorRef(index)` (component id 8). `publish(world)` writes a `JointPalette` (component id 9, `cinder/scene/render_sync.mojo`) for every entity with an `AnimatorRef`: joints through `palette_into` and the pose's morph weights, in place when the component already exists, otherwise added. `build_render_scene` then copies `JointPalette.joints` into `RenderScene.joints` and gives the instance its `joint_offset`, so the skinned draw sees the matrices. `__len__` is the animator count and `clear()` drops them all (a level restart), after which every `AnimatorRef` is stale and `publish` raises on it. ```mojo from cinder.anim import AnimationSystem, AnimatorRef from cinder.core.jobs import JobSystem var system = AnimationSystem() var index = system.add(animator^) var man = world.spawn() world.add[LocalTransform](man, LocalTransform(asset.nodes[skeleton.mesh_node].world)) world.add[MeshRenderer](man, MeshRenderer(mesh, material, True)) # skinned world.add[AnimatorRef](man, AnimatorRef(index)) var jobs = JobSystem(lib.ptr(), 3) system.update(dt, Optional[Pointer[JobSystem, MutUntrackedOrigin]](_ptr(jobs))) _ = system.publish(world) _ = build_render_scene(world, scene) # scene.joints now holds the palette ``` Inside `App.run_frame` the order is: fixed steps (physics, `Game.fixed_update`), `write_transforms`, `Game.update`, `anim.update(dt, jobs)`, `anim.publish(world)`, `propagate_transforms`, audio, render bridge, render. Animation therefore runs once per rendered frame on the frame's variable `dt`, not inside the fixed step; set blend parameters and call `play` from `Game.update` (or `fixed_update`) and they take effect in the same frame's `update`. The sample sets its locomotion `speed` parameter from the controller's horizontal speed every frame. ## Threading: one job per animator `AnimationSystem.update(dt, jobs)` builds one `_AnimatorJob` (animator pointer, `dt`, failure flag) per animator, keeps them in a persistent list that outlives the wait, and submits each to the engine job system with a shared `Counter`; the caller then `wait`s on the counter, executing jobs itself while it waits. Each job touches only its own animator, so no synchronisation is needed beyond the counter, and the results are bit-identical to serial evaluation (the test runs 16 animators both ways and compares every palette matrix). With no job system, or with a single animator, the same jobs run inline one after the other. Both paths behave the same: an animator whose `update` raised (one that was never played) sets its flag and the others still run; after every animator has been updated the call raises `animator failed to update` for the first flagged one (with `: it was never played` when that is the cause), and `updates` is not incremented. `publish` runs on the calling thread. This is the one place Mojo code runs on a non-main thread: job workers are threads Cinder itself created, which `docs/THREADING.md` rule 1 allows. Do not touch `system.animators` from another job while `update` is in flight, and keep the `AnimationSystem` where it is (it hands raw pointers to its own list elements to the jobs and waits for them before returning, so the list must not grow during the call; `add` between frames is fine). ## Pitfalls - A `Pose` is tied to the skeleton it was built for; `sample_clip`, `evaluate` and `blend_with` index by bone and do no resizing. - Children of a 1D node must be added with increasing thresholds or `add_child` raises; a clip node cannot take children. - `Animator.set_parameter` raises when no state declares the parameter, so declare it on every tree that should react, or set `states[i].tree.parameters[p]` directly. - `update` before `play` raises; `AnimationSystem.update` turns that into a failure after all other animators have run, with or without jobs, so `play` an animator before (or right after) adding it to the system. - `play` of the current state does not restart it, even once a non-looping state has finished; use `play(state, restart=True)`. - Phases are truncated with `Int(phase)` when counting loop wraps; the unwrapped phase, not a comparison of wrapped values, is what the code uses (`docs/MOJO_NOTES.md`, Phase 6). - `palette()`, `bone_matrices()`, `leaf_weights()`, `evaluate()` and `cycle_rate()` allocate; the `_into` / `_with` forms do not. The engine's frame path uses only the latter. - Morph weights are published from `pose.morph_weights`; a skeleton whose `mesh_node` is unknown (-1) accepts weight channels from any node. - The render instance's transform must be the mesh node's world transform (as in the snippet above), or the palette's `mesh_node_inverse` term puts the skin in the wrong place. ## See also - `docs/adr/0012-physics-animation-audio-input.md`, animation decisions 1 to 6. - `docs/adr/0014-sample-game-and-performance.md`, the zero-allocation frame and the `_into` convention. - `docs/THREADING.md`, job data ownership rules; `docs/ARCHITECTURE.md`, the frame loop. - `docs/api/anim.md`, the generated reference; `docs/api/assets.md` for `Clip` and the cooker. - `tests/anim/test_animation.mojo`: sampling modes and loops; the Fox skeleton, `.cskel` round trip, 1D and 2D blend trees, phase sync and crossfades; events and root motion; the system on jobs versus serial, and a skinned render through the ECS bridge. - `docs/guides/physics.md` for the character controller the sample drives its animator from. --- > Cinder 0.1.0 documentation: Application runtime (cinder/app) > Canonical page: https://cinder.ash10.dev/docs/guides/app/ · Source: `docs/guides/app.md` in the engine repository (engine commit 47c9352, imported 2026-11-09) > Index of every page for agents: https://cinder.ash10.dev/llms.txt # Application runtime (`cinder/app`) `cinder/app` is the top of the engine: `App` owns every subsystem, runs the fixed-step game loop and renders, either into an SDL window or, with `--headless`, into an offscreen texture that can be read back. Gameplay implements the `Game` trait and is called from `App.run_frame`. The sample game (`sample/`), the tests under `tests/app` and `tests/sample`, the zero-allocation test and the benchmarks in `bench/` all run this one loop. Source: `cinder/app/app.mojo` (`AppConfig`, `Game`, `App`), `cinder/app/cli.mojo` (`parse_app_args`). API reference: `docs/api/app.md`. Design: `docs/adr/0013-tools-and-app-runtime.md` (decisions 1 to 3), `docs/adr/0014-sample-game-and-performance.md` (zero allocations, benchmarks). ## `AppConfig` `AppConfig(title="Cinder", width=1280, height=720, headless=False)` plus fields set afterwards: | Field | Default | Meaning | |---|---|---| | `title`, `width`, `height` | `"Cinder"`, 1280, 720 | Window title and size. In windowed mode `App.width`/`height` are replaced by the window's pixel size. | | `headless` | `False` | No window, no swapchain, no overlay, device-less audio; frames go to an offscreen RGBA8 target. | | `fixed_hz` | 60.0 | Simulation rate of the fixed step (`FixedStep(fixed_hz)`, at most 8 steps per frame). | | `render` | `RenderSettings()` | Renderer settings (MSAA, shadows, AO, bloom, FXAA, ...). Tests set `config.render.msaa = 1`. | | `layers` | `LayerConfig.default()` | Physics object layers and collision pairs. | | `audio` | `True` | Create the `AudioEngine`. | | `debug_ui` | `True` | Create the ImGui overlay (windowed only; ignored headless). | | `profile` | `False` | Call `Profiler.start()` at construction (the Tracy client). | | `worker_threads` | 3 | Worker threads of the `JobSystem`. | `AppConfig` is `Copyable`; `App.__init__` takes it by `var`, so pass `config^`. ## The `Game` trait ```mojo trait Game: def start(mut self, mut app: App) raises: ... def fixed_update(mut self, mut app: App, dt: Float32) raises: ... def update(mut self, mut app: App, dt: Float32) raises: ... def draw_ui(mut self, mut app: App) raises: ... ``` - `start` runs once, on the first `run_frame`. Create meshes, materials, bodies, buses, register scene components (`app.registry.register[T]()`) and load the input map here. - `fixed_update(dt)` runs once per simulation step with `dt = 1 / fixed_hz`, after the physics step. - `update(dt)` runs once per frame with the real (or overridden) frame time. - `draw_ui` runs only in windowed mode with the overlay enabled, between `DebugUi.begin_frame` and `DebugUi.draw`; ImGui calls made here (through `app.lib.ptr()[]`) land in the same frame. There is no shutdown hook. The game releases what it created (bodies, shapes, sounds) itself before it is destroyed; `tests/app/test_app_headless.mojo` adds a plain `shutdown(app)` method to its `DropGame` and calls it explicitly. ## `App` ownership and destruction order `App` is a `Movable` value whose fields are declared in destruction order, because Mojo destroys fields in declaration order (`docs/MOJO_NOTES.md`). Subsystems that hand out pointers to themselves are held in `Boxed[T]` cells (`cinder/core/boxed.mojo`): a heap allocation with a stable address, so the `App` value can move while `Resources` keeps pointing at the same `Gfx`. Access a boxed field with `app.physics[]`, `app.renderer[]`, `app.gfx[]`, `app.res[]`, `app.jobs[]`; `.ptr()` gives the raw `Pointer` the subsystems themselves use. Declaration (and destruction) order, top to bottom: 1. `debug_ui: Optional[DebugUi]`, `assets: Optional[Boxed[AssetServer]]`, `anim`, `audio: Optional[AudioEngine]`, `physics_sync`, `bridge`, `physics: Boxed[PhysicsWorld]`, `input: InputMap`, `world: World`, `scene: RenderScene`, `registry: SceneRegistry`, `logs: MemorySink`, `events` 2. `swapchain: Optional[Swapchain]`, `target_view`, `target_tex`, `readback` (handles), `renderer: Boxed[Renderer]`, `res: Boxed[Resources]`, `gfx: Boxed[Gfx]` 3. `window: Optional[Boxed[Window]]`, `platform: Optional[Boxed[Platform]]`, `jobs: Boxed[JobSystem]` 4. `profiler`, `clock`, `fixed`, `config`, counters and timing fields 5. `lib: NativeLibrary`, last: every destructor above calls through it. `App.__deinit__` only destroys the offscreen target (texture, view, readback buffer) through `Resources` before the fields go. Two consequences for callers: - **Destroy the game before the app.** A `Game` holds handles into `app.physics[]`, `app.audio` and `app.renderer[]`; releasing them needs the app alive. `sample/main.mojo` ends with `_ = game^` then `_ = app^`, in that order, and the tests `keep(game)` and `keep(app)` after their last assertions so ASAP destruction does not reorder them. - **Leaks are reported, not fatal.** `live_objects()` returns physics shapes + bodies + characters plus live sounds and user buses; tests assert it is 0 after the game released its objects. `app.res[].live_counts()` covers GPU resources (shown in the overlay's Frame tab); they are not part of `live_objects()` because the engine's own targets, pipelines and meshes are alive for the App's whole life. Other fields: `width`/`height` (current size), `frame_index`, `fixed_steps` (total steps run), `quit_requested`, `started`, `events` (this frame's platform events, reused), `logs` (a `MemorySink`: `app.logs.records` holds every `LogRecord`; `app.log(level, category, message)` appends one; platform, GPU and audio diagnostics are forwarded here each frame). ## Construction ```mojo from cinder.app import App, AppConfig, Game var config = AppConfig(String("cinder headless test"), 160, 120, True) config.render.msaa = 1 var app = App(config^) ``` `App.__init__` loads the native library (`NativeLibrary()`, path from `CINDER_NATIVE_LIB` or the default per OS), creates the profiler (started if `config.profile`), clock, `FixedStep`, job system, log sink, registry, ECS world, input map, animation system, physics world (with `config.layers`), physics sync, render bridge and the audio engine (headless when `config.headless`). Then, windowed: `Platform`, `Window`, `Gfx` on the window's surface, `Swapchain` (whose format the renderer uses); headless: `Gfx` without a surface and RGBA8 as the target format. `Resources` (capacity 4096), `Renderer` and a default `RenderScene` with a camera looking at the origin follow. Finally headless apps create the offscreen target and windowed apps with `config.debug_ui` create the `DebugUi` and set `Platform.forward_to_imgui`. `mount_assets(root)` is optional and attaches an `AssetServer` over a cooked-asset directory (its `manifest.txt`); `App.run_frame` then calls `AssetServer.update` every frame and the overlay's Assets tab lists its records. ## `run_frame` and the loop order `run_frame[G: Game](mut game: G, frame_seconds: Float64 = -1.0) -> Bool` runs one frame and returns `False` once a quit was requested (window close, `EventKind.quit`, or `request_quit()`). With `frame_seconds >= 0` that value replaces the clock's measured delta, which is how tests and benchmarks get deterministic step counts (`1.0 / 60.0` gives exactly one fixed step per frame). In order: 1. First call only: `game.start(app)`, then the clock is reset so loading time does not become dt. 2. `dt` = `frame_seconds` or `clock.tick()`. 3. `_poll`: windowed apps pump SDL events; quit/close set `quit_requested`; resize updates `width`/`height`, the swapchain and the camera aspect; events go to `input.process` unless `ui_consumes_event` says the overlay takes them (mouse presses, motion and wheel while it wants the mouse, key presses while it wants the keyboard; key and button releases always reach the input map, so nothing stays held when the overlay takes focus); SDL diagnostics go to `logs`. Headless apps skip this. 4. `fixed.advance(dt)` steps, each inside a `"fixed step"` profiler zone: `world.advance_tick()`, `physics_sync.push_kinematics`, `physics[].step(step_dt)` (timed into `physics_ms`), `physics_sync.pull_bodies`, `game.fixed_update(app, step_dt)`, `fixed_steps += 1`. 5. `physics_sync.write_transforms(world, fixed.alpha())` interpolates rendered transforms. 6. `game.update(app, dt)`. 7. `"animation"` zone: `anim.update(dt, jobs)` and `anim.publish(world)`. 8. `propagate_transforms(world)`. 9. `"audio"` zone, if audio exists: `sync_audio(world, audio)`; headless: `read_frames` for `dt` worth of samples (capped at one second) so sounds progress without a device; `audio.update()`. 10. `assets[].update()` if mounted. 11. `_render` (`"render"` zone, timed into `render_ms`): `bridge.build(world, scene)`; windowed: acquire the swapchain frame, import its view as a handle, `renderer[].render`, overlay (`_draw_overlay`: `debug_ui.begin_frame(dt)`, `game.draw_ui(app)`, `debug_ui.draw(...)`, then `debug_ui.render` into its own render pass), forget the view, present; headless: `renderer[].render` into `target_view`. GPU diagnostics go to `logs`. 12. Allocation deltas are stored if `count_allocations`; `physics_ms` is set; `input.end_frame()`; `profiler.frame_mark()`; `frame_index += 1`; `frame_ms` is set. The typical main loop: ```mojo var game = SampleGame(level, autopilot) var app = App(config^) var frames = 0 while app.run_frame(game): frames += 1 if max_frames >= 0 and frames >= max_frames: break _ = game^ _ = app^ ``` ## Windowed vs headless; `read_frame` Both modes run the same code; headless differs only in what step 3 and step 11 touch. The offscreen target is an RGBA8 texture with `RenderAttachment | CopySrc | TextureBinding` usage and a map-readable buffer whose row stride is rounded up to `READBACK_ALIGN = 256` bytes (a wgpu requirement; an unaligned copy aborts the process, `docs/MOJO_NOTES.md`). `read_frame()` (headless only, raises otherwise) copies the target into the readback buffer, waits for the queue, maps it and returns an `Image` (`width`, `height`, `pixel(x, y)`): ```mojo var frame = app.read_frame() var lit = 0 for y in range(frame.height): for x in range(frame.width): var p = frame.pixel(x, y) if Int(p[0]) + Int(p[1]) + Int(p[2]) > 30: lit += 1 save_png(app.lib.ptr(), String(".cache/tests/sample/frame.png"), frame) ``` Headless apps have no overlay (`app.debug_ui` is empty, `draw_ui` is never called) and never see platform events, so the loop ends only through `request_quit()` or the caller's own frame limit. ## Command line: `parse_app_args` `parse_app_args(args, config) -> List[String]` applies the engine options to `config` and returns what it did not consume, so every executable shares the same flags and keeps its own: | Option | Effect | |---|---| | `--headless` | `config.headless = True` | | `--profile` | `config.profile = True` (start Tracy at construction) | | `--no-audio` | `config.audio = False` | | `--no-debug-ui` | `config.debug_ui = False` | | `--width N`, `--height N` | positive integers, else raises | | `--title TEXT` | window title | A flag missing its value raises (`"--width needs a value"`). `sample/main.mojo` parses the rest itself (`--level`, `--autopilot`, `--frames N`, `--screenshot PATH`, `--verbose`) and raises on anything unknown: ```mojo var config = AppConfig(String("Cinder sample"), 1280, 720, False) var rest = parse_app_args(args, config) var i = 0 while i < len(rest): if rest[i] == "--level" and i + 1 < len(rest): level = rest[i + 1] i += 1 elif rest[i] == "--autopilot": autopilot = True else: raise Error(t"unknown argument {rest[i]}") i += 1 ``` `pixi run sample-headless` runs `sample --headless --autopilot --frames 600`. ## Timing and allocation accounting - `frame_ms`: CPU time of the last `run_frame`, from after `start` to the end of the frame. - `physics_ms`: time inside `physics[].step` summed over the frame's fixed steps. - `render_ms`: time inside `_render` (scene build, submission, overlay, present call), measured on the CPU; it does not wait for the GPU. `bench/stress.mojo` and `bench/sample_bench.mojo` sample these three per frame into their JSON results (`docs/BENCHMARKS.md`). `count_allocations` turns on per-phase Mojo heap allocation counts, read through the shim's `cn_alloc_counter_from_executable` at phase boundaries. It needs `libcinder_alloccount.so` preloaded (`tools/run_tests.sh` does this for `tests/perf/test_zero_alloc.mojo`; without it every counter reads 0). After a frame, `alloc_other` (poll and step calculation), `alloc_fixed`, `alloc_update`, `alloc_anim`, `alloc_propagate`, `alloc_audio` (audio sync and asset update) and `alloc_render` hold the deltas of the last frame: ```mojo var lib = app.lib.ptr() lib[].cn_alloc_counter_reset() lib[].cn_alloc_counter_set_enabled(True) app.count_allocations = True for f in range(frames): app.renderer[].alloc_marks = f == frames - 1 assert_true(app.run_frame(game, 1.0 / 60.0)) lib[].cn_alloc_counter_set_enabled(False) print(t"fixed {app.alloc_fixed} update {app.alloc_update} render {app.alloc_render}") ``` `gpu_error_count()` returns `Gfx.error_count`, the number of uncaptured wgpu errors delivered through the shim queue; tests assert it is 0 after rendering. ## Logging `App.logs` is a `MemorySink`; everything the app learns about is appended there as `LogRecord` (level, category, message): SDL log lines (`Platform.take_diagnostics`), GPU diagnostics (`Gfx.take_diagnostics`), and your own `app.log(LogLevel.error(), String("app"), message)` calls. The overlay's Log tab shows the last 200 records. Tests scan `app.logs.records` for `LogLevel.error()`. Audio diagnostics stay in `AudioEngine.diagnostics` and are not forwarded. ## Threading `run_frame` runs on the main thread: the SDL pump, presentation, ImGui and the subsystem calls all happen there (`docs/THREADING.md`). Work fans out from inside the subsystems: animation submits one job per animator to `App.jobs`, Jolt uses its own thread pool inside `physics[].step`, the asset server decodes on the job system, and the miniaudio device thread (windowed) mixes on its own and only posts events. Nothing in `cinder/app` runs Mojo on a foreign thread. ## Pitfalls - Field order in `App` is load-bearing. Adding a field that needs `lib` or `gfx` after them breaks shutdown; add new subsystems in the first block. - `App` is `Movable` but not `Copyable`; functions return it by value (`def _app() raises -> App`). Pointers into it (`app.renderer.ptr()`) are stable because of `Boxed`, pointers to the `App` value itself are not. - `run_frame` calls `start` lazily; `app.world` is empty before the first frame. - With `frame_seconds` left at the default, the first frame's dt is the time since `start` finished, not since construction. - `FixedStep` caps steps at 8 per frame and drops the excess accumulator, so a frame longer than 133 ms at 60 Hz loses simulation time instead of spiralling. - `read_frame` after `request_quit` still works: the quit only stops `run_frame` from running. - Headless audio is pumped by `run_frame`; if you drive `AudioEngine` yourself, call `read_frames`. ## Tests and references - `tests/app/test_app_headless.mojo`: a cube falls onto a lit floor for 45 frames; checks step counts, physics-driven transforms, a visible readback, no GPU errors, no error logs, quit, and `live_objects() == 0`; plus `parse_app_args` coverage and `ui_consumes_event` (releases reach the input map while the overlay has focus). - `tests/tools/test_debug_ui.mojo`: the windowed path with the overlay. - `tests/sample/test_sample_game.mojo`, `tests/perf/test_zero_alloc.mojo`, `bench/`. - `docs/adr/0013-tools-and-app-runtime.md`, `docs/adr/0014-sample-game-and-performance.md`, `docs/THREADING.md`, `docs/MOJO_NOTES.md` (Phase 7 and Phase 8 sections). --- > Cinder 0.1.0 documentation: Assets guide (cinder/assets) > Canonical page: https://cinder.ash10.dev/docs/guides/assets/ · Source: `docs/guides/assets.md` in the engine repository (engine commit 47c9352, imported 2026-11-09) > Index of every page for agents: https://cinder.ash10.dev/llms.txt # Assets guide (`cinder/assets`) `cinder/assets` gets content from disk into the renderer. It has two paths: - **Direct import**: `load_gltf` parses a `.gltf`/`.glb` through cgltf into a CPU-side `GltfAsset`; `upload_gltf`/`instantiate` push it to the GPU. Simple, synchronous, and what the sample's Sponza level and the golden tests use. - **Cooked assets**: `cook_gltf` (and the `cinder-cook` CLI) turn a glTF source into versioned binary files (`.cmesh`, `.cmat`, `.cclip`, `.cskel`, `.ktx2`) named by stable ids and listed in a manifest; the `AssetServer` loads those asynchronously on the job system with reference counting and hot reload. The package sits beside `render`, `physics`, `anim`, `audio` and `scene` in the module map; it imports `cinder/core` (bytes, fs, hash, image, jobs), `cinder/gfx` (resources), `cinder/render` (mesh, material, renderer) and the cgltf, libktx and meshoptimizer bindings in `cinder/ffi`. `cinder/scene` uses it to spawn glTF into the ECS; `cinder/app` mounts an `AssetServer`. Files: `gltf.mojo`, `cooked.mojo`, `cook.mojo`, `textures.mojo`, `runtime.mojo`, and the CLI `tools/cinder_cook.mojo`. API reference: [`docs/api/assets.md`](https://cinder.ash10.dev/docs/api/assets.md). Tests: `test_gltf_loader.mojo`, `test_cook.mojo`, `test_textures.mojo`, `test_asset_server.mojo`, `test_fuzz.mojo`. Related decisions: [ADR-0003](https://cinder.ash10.dev/docs/adr/0003-native-shim-pull-model.md) (C shim, no Mojo in C callbacks), [ADR-0014](https://cinder.ash10.dev/docs/adr/0014-sample-game-and-performance.md) (the sample imports glTF directly and does not yet use the cooked path). ## glTF import ```mojo var lib = NativeLibrary() var asset = load_gltf(lib.ptr(), String("tests/fixtures/skinned_quad.gltf")) ref prim = asset.meshes[0].primitives[0] assert_true(prim.mesh.has_skin()) assert_equal(len(prim.mesh.tangents), 4, "tangents generated when absent") var b = asset.world_bounds() # SceneBounds: min, max, centre(), radius() ``` `GltfAsset` is a plain tree of owned lists: `images` (`GltfImage`: name + decoded RGBA8 `Image`), `textures` (`GltfTexture`: image index, `linear_filter`, wgpu `address_mode`), `materials` (`GltfMaterial`: a render `Material` plus five texture indices, -1 when absent, in slot order base colour, metallic-roughness, normal, occlusion, emissive), `meshes` (`GltfMesh` -> `GltfPrimitive(mesh: MeshData, material)`), `nodes` (`GltfNode` with `parent`, `children`, `mesh`, `skin`, `local` and `world` matrices, morph `weights`), `skins` (`GltfSkin`: joint node indices and inverse bind matrices), `animations` (`GltfAnimation` of `GltfChannel`s: node, `path` 0..3 = translation/rotation/scale/weights, `interpolation` 0..2 = linear/step/cubic, `components`, `times`, `values`) and `roots` (the default scene's nodes, else every parentless node). All indices are into these lists. What the importer does for you: only triangle primitives are kept; normals are computed when missing, tangents generated with MikkTSpace when missing, `uv0` zero-filled, joint weights renormalised, indices bounds-checked and a missing index buffer replaced by a sequential one; images come from files (percent-decoded relative URIs), data URIs or GLB buffer views; `KHR_texture_transform` and `KHR_materials_emissive_strength` are read into the `Material`. Malformed input raises (`parse failed`, `validation failed`, `out of range`) and never crashes; the fuzz test mutates glTF bytes to hold that line. Upload and instantiate: ```mojo var up = upload_gltf(renderer, res, asset, lods=True) # up.mesh_ids[mesh][primitive], up.material_ids[material], up.default_material var scene = RenderScene(Camera.look_at(Vec3(0.0, 1.0, 4.0), Vec3(0.0, 1.0, 0.0))) var added = instantiate(renderer, res, asset, scene) # one RenderInstance per (node, primitive) ``` `upload_gltf` creates one sampler per glTF texture, uploads each image once per colour space (base colour and emissive as sRGB, the rest linear), adds every material and one default material, and `renderer.add_mesh`es every primitive (building LODs with meshoptimizer when `lods` is true). `instantiate` additionally appends instances with the node's world transform, joints at bind pose (`inverse(node.world) * joint.world * inverse_bind`) and default morph weights. For an entity hierarchy instead of a flat instance list, use `spawn_gltf` from `cinder/scene` (see the [scene guide](https://cinder.ash10.dev/docs/guides/scene.md)). ## Cooked formats (`cooked.mojo`) All cooked files are little-endian, start with a four-byte magic and a `u32` version, and are fully bounds-checked on read: bad magic, a different version, truncation, trailing bytes, out of range indices and inconsistent counts all raise `cooked ...` errors. Strings are `u32 length` + bytes. Readers take `var data: List[UInt8]` and consume it. | extension | magic | version | written by | read by | |---|---|---|---|---| | `.cmesh` | `CMSH` | 1 | `write_mesh(cook_mesh(data, lods, material))` | `read_mesh` | | `.cclip` | `CCLP` | 1 | `write_clip(Clip)` | `read_clip` | | `.cmat` | `CMAT` | 1 | `write_material(CookedMaterial)` | `read_material` | | `.cskel` | `CSKL` | 1 | `cinder.anim.write_skeleton` | `cinder.anim.read_skeleton` | | `.ktx2` | KTX 2.0 | - | `cook_texture_ktx2` (libktx) | `load_ktx2` | **`.cmesh`** header after the version: `u32 vertex_count`, `u32 vertex_bytes`, `u32 skin_bytes`, `u32 index_bytes`, `u32 lod_count`, `vec3 bounds_min`, `vec3 bounds_max`, `u32 morph_target_count`, `u32 morph_bytes`, `string material` (logical path or empty); then `lod_count` entries of `u32 first_index, u32 index_count, f32 error` (`MeshLod`), then the vertex, skin, index and morph byte blocks. The vertex stream is the renderer's 56-byte `Vertex` (position, normal, tangent+handedness, uv0, uv1), the skin stream 24 bytes per vertex (joints u16x4, weights f32x4), indices `u32`, morph deltas 32 bytes per target per vertex. `read_mesh` checks each block size against the counts, `1 <= lod_count <= 16`, LOD ranges within the index buffer and a multiple of 3, and that every index addresses a vertex. A `CookedMesh` goes to the GPU through `Renderer.add_cooked_mesh` without further processing. **`.cclip`**: `string name`, `f32 duration`, `u32 channel_count`, then per `ClipChannel` `u32 node, path, interpolation, components, key_count, value_count`, `f32 times[]`, `f32 values[]`. `values` holds `components` floats per key (three times that for cubic spline: in-tangent, value, out-tangent); times must be sorted. **`.cmat`**: `string name`, `vec4 base_color`, `f32 metallic, roughness`, `vec3 emissive`, `f32 emissive_strength, normal_scale, occlusion_strength`, `u32 alpha_mode` (0..2), `f32 alpha_cutoff`, `u32 double_sided`, `vec2 uv_scale, uv_offset`, `f32 uv_rotation`, five `(u8 texcoord, string image)` pairs, `u32 linear_filter`, `u32 address_mode`. `CookedMaterial` is a render `Material` whose texture slots are *logical image paths* instead of GPU views; the `AssetServer` resolves them. Ids: `asset_id(logical) = fnv1a64(logical)` and `asset_file_name(logical, ext)` = sixteen hex digits + `.` + ext. Files are addressed by logical path, not by content, so the name of `Fox#mesh0/prim0` never changes while its bytes may. ```mojo var sphere = uv_sphere(0.5, 12, 24) var lods = sphere.build_lods(lib.ptr()) var cooked = cook_mesh(sphere, lods, String("mat")) var back = read_mesh(write_mesh(cooked)) assert_equal(back.vertex_bytes, cooked.vertex_bytes) ``` ## The cooker (`cook.mojo`, `tools/cinder_cook.mojo`) ```mojo var manifest = CookManifest.load(String(OUT) + "/manifest.txt") var report = cook_gltf(lib.ptr(), String("assets/samples/Fox/Fox.glb"), String("Fox"), String(OUT), manifest) manifest.save(String(OUT) + "/manifest.txt") print(report.cooked, "cooked,", report.skipped, "up to date") # report.outputs: logical paths ``` `cook_gltf(lib, source_path, prefix, out_dir, manifest)` produces, for a source with logical `prefix`: | logical path | file | content | |---|---|---| | `#mesh/prim

` | `.cmesh` | primitive with LODs (`build_lods` when more than 64 triangles, else one LOD) and its material's logical path | | `#material` | `.cmat` | material with `#image` per used slot, and one sampler (filter and address mode): the base colour texture's, else the first present map's in slot order (metallic-roughness, normal, occlusion, emissive) | | `#image` | `.ktx2` | every image referenced by a material: sRGB when used as base colour or emissive, UASTC (and normal-map mode) when used as a normal map, ETC1S otherwise | | `#skeleton` | `.cskel` | `skeleton_from_gltf(asset, s)` | | `#anim` | `.cclip` | the animation's channels copied verbatim | | `#` | `.root` | the source path, as text; this entry carries the source hash | Incrementality is keyed on content: the source file's FNV-1a hash (`hex64`) is recorded on every entry. If the root entry's hash is unchanged and every output file still exists, the source is not even parsed (`report.skipped` = number of outputs); otherwise everything is re-cooked. The test appends one whitespace byte to a source and asserts a full re-cook. A re-cook prunes the outputs the source no longer produces (it lost a mesh, material, image, skin or animation): their entries leave the manifest and their files are deleted, so they are neither served nor counted in `report.skipped` later. **Manifest** (`/manifest.txt`): one line per entry, three space-separated fields ` `; lines with any other field count are ignored on load. Logical paths therefore must not contain spaces. `CookManifest.find`, `record` (replace or append), `prune(root, produced)` (drop the entries under `root` not in `produced`) and `up_to_date(logical, hash, out_dir)` are the whole API; the `AssetServer` loads the same file. Strings inside cooked files are UTF-8; one that is not valid UTF-8 is rejected on load. **CLI**: `pixi run cook [more sources...]` runs `tools/cinder_cook.mojo`. Each source's prefix is its file name without directory and extension (`models/Fox.glb` becomes `Fox`), so two sources with the same base name collide. The manifest is loaded before and saved after all sources; a failing source is printed and the others still cook; the exit status is 1 if any failed. ## KTX2 textures (`textures.mojo`) ```mojo cook_texture_ktx2(lib.ptr(), img, path, srgb=True, normal_map=False, uastc=False) # quality=128 var loaded = load_ktx2(lib.ptr(), path, prefer_bc7=gfx.bc_supported) assert_equal(len(loaded.levels), 7) # full chain for 64x32 var handles = upload_loaded(res, String("label"), loaded) # (TextureHandle, TextureViewHandle) ``` Cooking builds the full mip chain on the CPU (`downsample`), writes it into a `ktxTexture2` and compresses with BasisU: UASTC (higher quality, normal-map mode available) or ETC1S at `quality` 1..255. sRGB images record the transfer function in the data format descriptor. Loading transcodes to BC7 when `prefer_bc7` (the device advertises BC compression) or RGBA8 otherwise, into `LoadedTexture(width, height, levels, bc7, srgb)`; uncompressed KTX2 input is accepted only as RGBA8. `upload_loaded` creates the texture in the matching format (`BC7RGBAUnorm[Srgb]` or `RGBA8Unorm[Srgb]`) and writes every level, rounding BC7 copies up to whole 4x4 blocks. libktx errors surface as `ktxTexture2_CreateFromNamedFile ...` and friends; a truncated file raises. ## The asset server (`runtime.mojo`) `AssetServer(lib, jobs, renderer, root)` reads `root/manifest.txt` and resolves logical paths through it. The `App` creates one with `App.mount_assets(root)` and calls `assets.update()` once per frame. ```mojo var jobs = JobSystem(lib.ptr(), 2, job_capacity=256) var server = AssetServer(lib.ptr(), _ptr(jobs), _ptr(renderer), String(OUT)) var fox = server.load_mesh(String("Fox#mesh0/prim0")) # asset index; a job starts now var fox_again = server.load_mesh(String("Fox#mesh0/prim0")) assert_equal(fox, fox_again) # one record, refcount 2 var clip = server.load_clip(String("Fox#anim0")) var mat = server.load_material(String("Box#material0")) # pulls in its KTX2 textures server.wait_all() # or: server.update() each frame assert_true(server.state(fox) == AssetState.ready(), server.error(fox)) scene.instances.append(RenderInstance(server.mesh_index(fox), server.material_index(mat), Mat4.identity())) var duration = server.clip(clip).duration # a copy of the Clip server.release(fox) server.release(fox) # last reference: GPU mesh removed ``` Every `load_*` returns an `Int` index into `server.records` and counts as one reference; loading the same logical path again returns the same index and bumps the refcount. Loading it as another kind (`load_clip` on a mesh path) raises (`is a mesh, not a clip`), and loading a path whose earlier load failed starts that load again on the same record. A logical path that is not in the manifest raises immediately (`not in .../manifest.txt (run cinder-cook)`). States are `AssetState.loading()`, `ready()`, `failed()`; `error(index)` holds the message of a failed load (the test corrupts a cooked file and checks `cooked` appears in it). Accessors `mesh_index`, `clip`, `texture_view`, `material_index` raise until the asset is ready. Loading is split in two: 1. A job (`_load_job`) reads and parses the file into a heap `_LoadSlot`: `read_mesh`, `read_clip`, `read_material`, or `load_ktx2` (libktx reads and transcodes on the job). The slot's `done` atomic is stored with release ordering last. 2. `update()` on the main thread polls slots with acquire loads and finishes them: a mesh goes through `Renderer.add_cooked_mesh` (or `replace_cooked_mesh` on reload), a texture through `upload_loaded`, a clip is simply stored. A material's slot yields a `CookedMaterial`; the server then calls `load_texture` for each referenced image and the material becomes ready in `_finish_materials` once every pending texture is ready (the first failed texture fails the material). The sampler is created from the cooked filter and address mode. `wait_all()` loops `jobs.wait(counter)` + `update()` until nothing is in flight; it is for tests and loading screens. `release(index)` decrements; at zero it removes the renderer mesh, destroys the texture and view, frees the material's renderer slot (uniform buffer and bind group; the slot index is not reused) and its sampler, releases the textures a material pulled in, and marks the record `failed` with error `released` (indices are never reused). Releasing while a load job is outstanding waits for the job first; a material that only waits for its textures has no job and is released at once. `live_count()` counts records with a positive refcount. `__deinit__` waits for all jobs before freeing slots. **Hot reload**: `check_for_changes()` compares each ready, referenced asset's file mtime with the one recorded when its load started and restarts the load for changed files; the count of started reloads is returned and `records[i].reloads` incremented. For meshes the renderer index is stable (`replace_cooked_mesh`), so existing `MeshRenderer`s and instances keep working; the test rewrites a cooked file and asserts the same `mesh_index`. A material reload is in place too: the new version is uploaded into the same `material_index` once its textures are ready (the renderer draws the old one until then), the old uniform buffer, bind group and sampler are freed, and the texture references of the old version are released only then, so an unchanged texture keeps a single reference and is never reloaded. Replacing and removing go through `Renderer.replace_material` and `Renderer.remove_material`. Nothing calls `check_for_changes` automatically; poll it from your frame loop or a debug key. ## Threading rules - `load_*`, `update`, `wait_all`, `release`, `check_for_changes` and the accessors are main-thread calls: `AssetServer` has no locks and its record list is owned by the thread that owns the renderer. - Job threads only run `_load_job`: file I/O, cooked-format parsing and KTX2 transcoding. They never touch `Renderer`, `Resources` or wgpu; GPU uploads happen in `update()`. - Slots are stored as single-element lists (`slots: List[List[_LoadSlot]]`) so their heap addresses stay put while the outer list grows; a job may hold a pointer to its slot until its `done` flag is set. Do not move or shrink that list. - `JobSystem.submit` returns `False` when the job table is full; the server then runs the load inline on the main thread, so `load_*` can block briefly under a saturated job system. - `load_gltf`, `upload_gltf`, `instantiate`, `cook_gltf` and `cook_texture_ktx2` are synchronous and, where they create GPU resources, main-thread only. `cook_texture_ktx2` asks libktx for four compression threads internally (C threads; no Mojo runs on them). ## Pitfalls - Cooked formats have no migrations: a version mismatch raises `cooked mesh version N (this engine reads M)` and the fix is to re-cook (`docs/STATUS.md`). Bump the `*_VERSION` constants in `cooked.mojo` when a layout changes. - The cooker decides incrementality from the source hash alone. Changing cooker code or the cooked format does not re-cook an unchanged source; delete the output or the manifest. - `cinder-cook` derives prefixes from base names, so `a/Fox.glb` and `b/Fox.glb` overwrite each other's entries. Call `cook_gltf` with explicit prefixes when that matters. - A texture reload gets a new `TextureView`. Before the old texture is freed, every ready material that binds the old view is rebuilt in place (`Renderer.replace_material`, same index), so instances keep drawing. Code that copied a view out with `texture_view()` and bound it elsewhere must fetch it again after a reload. - `AssetServer.clip()` returns a copy of the whole `Clip` (all keyframes); fetch it once when the asset becomes ready, not every frame. - `release` past zero raises `released too often`; indices stay valid but dead, and `state` of a released record is `failed`. - The importer decodes PNG/JPEG on the CPU during `load_gltf`; ADR-0014 notes the sample's Sponza start time comes from this. The cooked KTX2 path exists for that reason but the sample does not use it yet. - Pointer-lifetime rules from [`docs/MOJO_NOTES.md`](https://cinder.ash10.dev/docs/mojo-notes.md) apply to every C call here (`keep(text)` after passing a `String`'s C span to cgltf or libktx; descriptors owned until the call returns). Follow the existing `_cstr` + `keep` pattern when extending the loaders. --- > Cinder 0.1.0 documentation: Audio (cinder/audio) > Canonical page: https://cinder.ash10.dev/docs/guides/audio/ · Source: `docs/guides/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 # Audio (`cinder/audio`) `cinder/audio` is the engine's sound layer over miniaudio. It provides mixing buses, decoded and streamed sounds, one-shots that clean up after themselves, 3D spatialisation with distance attenuation, a single listener, and an ECS bridge that moves sounds with entities and the listener with the camera. It sits beside `render`, `physics`, `anim` and `input` in the module map (`docs/ARCHITECTURE.md`): it depends on `core`, `ecs`, `platform` and the generated `ffi` bindings, and is owned and driven by `cinder/app`. Source: `cinder/audio/engine.mojo` (the engine), `cinder/audio/sync.mojo` (ECS components and `sync_audio`), `native/src/audio.c` (the shim side). API reference: `docs/api/audio.md`. Design decisions: `docs/adr/0012-physics-animation-audio-input.md`, "Audio" section. ## Key types and ownership | Type | Role | |---|---| | `AudioEngine` | Owns the miniaudio engine, the buses and every sound. One per `App` (`App.audio`, an `Optional[AudioEngine]`). | | `SoundDesc` | How to load a sound: path, bus, `streamed`, `looping`, `spatial`, volume, pitch, attenuation model and distances, `one_shot`. | | `SoundHandle` | Generational handle `(index, generation)` to a loaded sound. `SoundHandle.null()` is the invalid handle; `Bool(handle)` is false for it. | | `Attenuation` | Distance model: `none()`, `inverse()` (default), `linear()`, `exponential()`. | | `AudioSource` | ECS component: a `SoundHandle` plus a `velocity` (Doppler). | | `AudioListener` | ECS component marking the entity whose `WorldTransform` drives the listener. | Everything miniaudio-shaped lives in the shim. `ma_engine`, `ma_sound` and `ma_sound_group` are large structs that are opaque to Mojo, so `cn_audio_engine_create`, `cn_audio_sound_create_in_group` and `cn_audio_group_create` allocate them in C and hand back pointers; Mojo keeps those pointers in `AudioEngine.engine`, `AudioEngine.buses` and a slot table (`AudioEngine.slots`). Sound handles are generational: `release` bumps the slot's generation and puts the index on the free list, so a stale handle fails `is_valid` and every method that takes a handle raises `"audio: stale sound handle"`. Bus 0 is always the master bus, created by the constructor on the engine endpoint; `live_buses()` does not count it. `live_sounds()` counts slots that still hold a sound. Lifetimes and leaks: `AudioEngine.__deinit__` destroys any remaining sounds, then the buses in reverse order, then the engine and its event queue, so leaking is not fatal. The convention, enforced by the tests and by `App.live_objects()`, is that `live_sounds()` and `live_buses()` are zero before the engine goes away: `release` every sound and `destroy_bus` every bus you created (`live_buses()` counts created buses not yet destroyed) (`App.live_objects()` sums physics objects, live sounds and live buses for the shutdown leak report). Because `App.audio` is a field of `App`, a `Game` that holds `SoundHandle`s must be destroyed before the `App`; see `docs/guides/app.md`. The engine needs the `NativePtr` of a `NativeLibrary` that outlives it. Inside `App` that is `App.lib`, declared last so it is destroyed last. ## Creating an engine Through `App`, `AppConfig.audio` (default `True`) creates the engine; headless apps get a device-less engine automatically. Standalone, as the tests do: ```mojo from cinder.audio import AudioEngine, SoundDesc from cinder.platform import NativeLibrary var lib = NativeLibrary() var audio = AudioEngine(lib.ptr(), True) # headless=True: no device, output is pulled var sfx = audio.create_bus(String("sfx")) # parent defaults to 0 (master) var music = audio.create_bus(String("music")) ``` The constructor arguments are `(lib, headless=False, sample_rate=48000, channels=2)`. It raises `"audio: miniaudio engine creation failed (no device?)"` when a device is requested on a machine without one; pass `headless=True` (or run the app with `--headless`) in that case. `create_bus(name, parent=0)` returns the bus index; `parent=-1` attaches a bus directly to the engine endpoint, which is how the master bus itself is created. `find_bus(name)` returns `-1` when no live bus has that name. `destroy_bus(index)` destroys a bus made with `create_bus` (through the shim's `cn_audio_group_destroy`) and lowers `live_buses()` by one. Its index then stays invalid: indices are never reused, so every bus method, `load` and `create_bus(parent=index)` raise `"audio: bus does not exist"` for it, as does a second `destroy_bus`. It also raises for the master bus (0 belongs to the engine) and for a bus that still has sounds (release them first) or live child buses (destroy those first, children before parents). ## Loading and playing `load` creates the sound stopped; `play` starts it. `SoundDesc` takes the common options positionally: `SoundDesc(path, bus=0, streamed=False, looping=False, spatial=True)`. The remaining fields (`volume`, `pitch`, `attenuation`, `min_distance`, `max_distance`, `rolloff`, `one_shot`) are set on the value after construction. Supported file types are WAV, FLAC, MP3 and Vorbis. ```mojo # Streamed, looping, non-spatial music on its own bus (from sample/game/game.mojo). var song = audio.load( SoundDesc(String("assets/game/audio/music_level.wav"), music, True, True, False) ) audio.play(song) audio.set_bus_volume(music, 0.6) # Decoded, looping, spatial effect at a position. var tone = audio.load(SoundDesc(String("tone.wav"), sfx, False, True, True)) audio.set_position(tone, Vec3(-10.0, 0.0, 0.0)) audio.play(tone) audio.stop(tone) audio.release(tone) ``` - `streamed=True` maps to `MA_SOUND_FLAG_STREAM`: the file is decoded while it plays (music). Otherwise `MA_SOUND_FLAG_DECODE` decodes the whole file at load time (effects). - `spatial=False` sets `MA_SOUND_FLAG_NO_SPATIALIZATION`: the sound is mixed centred, with `set_pan` as the only positioning control. - Per-sound controls after loading: `set_volume`, `set_pitch`, `set_pan`, `set_looping`, `fade(from, to, milliseconds)`, `seek(frame)`, `is_playing`, `at_end`. - A failed `load` raises `"audio: could not load ''"` and leaves nothing behind (the slot is returned to the free list; the test asserts `live_sounds()` is unchanged). ### One-shots `play_one_shot(desc, position)` sets `one_shot=True` and `looping=False` on the description, loads, positions the sound if it is spatial, starts it and returns the handle. When the sound ends, the next `update()` releases it. The sample fires impact sounds this way: ```mojo var desc = SoundDesc(String("assets/game/audio/impact.wav"), self.sfx_bus, False, False, True) desc.min_distance = 2.0 desc.max_distance = 60.0 _ = app.audio.value().play_one_shot(desc^, position) ``` Do not keep one-shot handles around for long: once released, `is_valid` is false and `play` raises. ## 3D position, attenuation and the listener Spatial sounds take `set_position(handle, Vec3)` and `set_velocity(handle, Vec3)`; `set_spatial` toggles spatialisation on an existing sound. The attenuation model and its distances are applied at load time from the `SoundDesc` (defaults: `Attenuation.inverse()`, `min_distance=1.0`, `max_distance=100.0`, `rolloff=1.0`) and can be changed with `set_attenuation(handle, model, min_distance, max_distance, rolloff)`. The listener is miniaudio's listener 0. `set_listener(position, forward, up=Vec3.unit_y(), velocity=Vec3.zero())` sets its position, direction, world up and velocity. The engine convention is right-handed, Y up, -Z forward, so a listener looking down -Z hears a source at negative X on the left; `tests/audio/test_audio_engine.mojo` checks exactly that with RMS measurements per channel. ## The ECS bridge `cinder/audio/sync.mojo` connects sounds to entities. `AudioSource(sound, velocity)` on an entity with a `WorldTransform` makes `sync_audio` set that sound's position to the transform's translation and its velocity to the component's `velocity`. `AudioListener(velocity)` marks the listener entity: its `WorldTransform` gives the position, `-Z` transformed by the matrix gives `forward`, `+Y` gives `up`. When several entities carry `AudioListener`, the first chunk found wins. ```mojo from cinder.audio import AudioListener, AudioSource from cinder.ecs import LocalTransform, World, propagate_transforms var camera = world.spawn() world.add[LocalTransform](camera, LocalTransform(Mat4.identity())) world.add[AudioListener](camera, AudioListener(Vec3.zero())) var emitter = world.spawn() world.add[LocalTransform](emitter, LocalTransform(Mat4.translation(Vec3(-10.0, 0.0, 0.0)))) world.add[AudioSource](emitter, AudioSource(tone, Vec3.zero())) _ = propagate_transforms(world) # WorldTransform must exist before sync_audio var positioned = sync_audio(world, audio) # returns the number of sources positioned ``` `sync_audio` skips sources whose handle is no longer valid instead of raising, so releasing a sound does not require removing its component first. Component ids are `AUDIO_SOURCE_COMPONENT_ID = 10` and `AUDIO_LISTENER_COMPONENT_ID = 11`. `App.run_frame` calls `propagate_transforms` and then `sync_audio` every frame, so with `App` you only add the components. ## Per-frame work: `update`, `take_ended`, `read_frames` `update()` drains the shim's event queue. Log callbacks from miniaudio become `LogRecord`s in `AudioEngine.diagnostics` (category `"audio"`; `error_count()` counts those at error level). Sound-end events are matched to a live handle, recorded in `AudioEngine.ended`, and one-shots are released. `update()` returns how many sounds ended. `take_ended()` moves the ended list out so gameplay can react once: ```mojo _ = audio.update() for h in audio.take_ended(): if h == self.footstep: self.footstep = SoundHandle.null() ``` In headless mode there is no device pulling the mixer, so nothing progresses unless you pull it. `read_frames(frames)` mixes `frames` frames and returns `frames * channels` interleaved `Float32` samples; it raises on a device-backed engine. `App.run_frame` does this for you: each frame it reads `dt * sample_rate` frames (capped at one second) and discards them, so one-shots end and streams advance in tests and benchmarks exactly as they would with a device. ## Threading - The miniaudio device thread never runs Mojo. The shim registers only two callbacks (`on_log` and `on_sound_end` in `native/src/audio.c`), and both post fixed-size events to a `CnEventQueue` that `update()` drains on the main thread (ADR-0003, `docs/THREADING.md`). - Setters such as `set_position` and `set_bus_volume` are direct `ma_*` calls from the calling thread; miniaudio makes them safe against its own mixing thread. Call them from the main thread. - With a headless engine there is no audio thread at all: mixing happens inside `read_frames`, so the end callbacks fire on the caller's thread during that call and still only post events. ## Pitfalls - The sound-end callback fires on the processing step after the one that reached the end, and the sound is already stopped inside it (`docs/MOJO_NOTES.md`, Phase 6). A headless test therefore reads one more buffer with `read_frames` before calling `update()` and expecting the end event. - `update()` releases one-shots, so a handle returned by `play_one_shot` becomes stale without any call of yours. Check `is_valid` before reusing it. - Sound paths are passed to C as a borrowed C string; `load` keeps the `String` alive across the call with `keep`. If you add a shim call that takes a path, do the same (`MOJO_NOTES.md`, untracked pointers do not keep their pointee alive). - `set_master_volume` is the engine's own volume, separate from bus 0's volume; the sample's settings apply `master_volume` to the engine and `music_volume`/`sfx_volume` to buses found by name. - Buses cannot be destroyed individually; they live until the engine does. Create them once in `Game.start`. - `AppConfig.audio = False` (`--no-audio`) leaves `App.audio` empty; always test `if app.audio:` before `app.audio.value()`. ## Tests and references - `tests/audio/test_audio_engine.mojo`: writes WAV files, then checks buses, left/right panning, distance attenuation, bus muting, streamed looping music, one-shot release through `update`, failed loads, and the ECS bridge (sources follow entities, listener follows the camera, stale handles are skipped). Run with `pixi run test-audio-engine`. - `tests/sample/test_sample_game.mojo`: after the level is cleared, 240 headless frames let the one-shots end so only the music loop remains (`live_sounds() == 1`, `live_buses() == 2`). - `docs/adr/0012-physics-animation-audio-input.md`: miniaudio used as is, headless engines for tests, callbacks post events, listener follows an entity. - `docs/adr/0013-tools-and-app-runtime.md`: how `App` owns the engine and pumps it headless. --- > Cinder 0.1.0 documentation: cinder/core guide > Canonical page: https://cinder.ash10.dev/docs/guides/core/ · Source: `docs/guides/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` guide `cinder/core` is the lowest Mojo layer of the engine: logging, assertions, a bump arena, generational handles and pools, a bounded ring, byte packing and hashing, file helpers, a monotonic clock with a fixed-step accumulator, CPU images, the job system, Tracy zones, `Boxed` heap cells and the SIMD math package. It performs no GPU, window or audio I/O. Everything above it (`platform`, `gfx`, `ecs`, `render`, ...) depends on it; it depends only on the generated bindings in `cinder/ffi` and, for two modules that call into the shim (`jobs`, `image`, `profile`), on `libcinder_native` being loaded. Note on layering: `core/profile.mojo` and `core/image.mojo` import `NativePtr` from `cinder.platform.native` so they can call the shim through the single loaded library. That is the one upward reference in the module map of `docs/ARCHITECTURE.md`; `core/jobs.mojo` takes a raw `Pointer[CinderNative, MutUntrackedOrigin]` instead and does not import `platform`. Source: `cinder/core/*.mojo`, `cinder/core/math/*.mojo`. Reference: `docs/api/core.md`. Tests: `tests/core/test_core.mojo`, `test_math.mojo`, `test_jobs.mojo`, `test_image.mojo`, `test_profile.mojo`. ## Key types | Module | Types | Ownership and lifetime | |---|---|---| | `log` | `LogLevel`, `LogField`, `LogRecord`, trait `LogSink`, `PrintSink`, `MemorySink`, `Logger[S]` | A `Logger` owns its sink by value (`Logger[MemorySink](MemorySink(), LogLevel.info())`). Records are copied into `MemorySink.records`. | | `assertions` | `cinder_assert`, `cinder_check`, `ASSERTS_ENABLED` | Free functions; both abort, never raise. | | `arena` | `LinearArena` | Owns one heap block for its lifetime; pointers it hands out are untracked and die at `reset()`. | | `handles` | `Handle[Tag]`, `Pool[T, Tag]` | A `Pool` owns its values; a `Handle` is an 8-byte copyable name (index + generation). Zero generation is the null handle. | | `ring` | `RingBuffer[T]` | Single-threaded bounded FIFO, owns its slots. | | `bytes`, `hash`, `base64` | `ByteWriter`, `fnv1a64`, `fnv1a64_string`, `hex64`, `decode_base64` | `ByteWriter` owns a `List[UInt8]`; `take()` consumes it. | | `fs` | `FileError`, `file_exists`, `read_bytes`, `read_text`, `write_bytes`, `write_text` | Free functions with a typed error. | | `time` | `Clock`, `FixedStep` | Plain values. | | `image` | `Image`, `ImageDiff`, `load_image`, `decode_image`, `save_png`, `compare` | `Image` owns its RGBA8 pixels in a `List[UInt8]`; pixels decoded by stb are copied in and the stb buffer freed before the function returns. | | `jobs` | `JobFn`, `Counter`, `Job`, `Deque`, `Shared`, `WorkerContext`, `JobSystem` | `JobSystem` owns the worker threads, the job table and the per-thread deques; `__deinit__` stops and joins every worker. Job data is owned by the submitter. | | `profile` | `Profiler`, `Zone`, `COLOR_*` | `Profiler` is a copyable handle to the shim's Tracy client; copies share one plot-name table that is never freed (Tracy reads plot names for the program's life). `Zone` ends when destroyed. | | `cstring` | `string_from_c` | Copies a NUL-terminated (or length-capped) C string into a new `String`, decoding UTF-8. | | `boxed` | `Boxed[T]` | Owns one `T` on the heap; `ptr()` is valid for the box's lifetime. | | `math` | `Vec2`, `Vec3`, `Vec4`, `Quat`, `Mat4`, `Transform`, `EPSILON`, `radians`, `degrees`, `clamp`, `saturate`, `lerp`, `approx_eq` | Register-passable values backed by `SIMD[DType.float32, 4]`. | ## Logging `Logger[S]` filters on `min_level` and forwards a `LogRecord` to its sink. Sinks implement the `LogSink` trait (`emit(mut self, record: LogRecord)`) and must not raise. Records print as one line, `[LEVEL] category: message key=value ...`. The convenience methods `trace`, `debug`, `info`, `warn`, `error` take a category and a message; the full `log` overload also takes a `List[LogField]`, which it consumes. ```mojo from cinder.core.log import LogField, LogLevel, Logger, MemorySink var logger = Logger[MemorySink](MemorySink(), LogLevel.info()) logger.debug("gfx", "dropped") # below min_level, filtered logger.info("gfx", "device created") var fields = List[LogField]() fields.append(LogField("adapter", "lavapipe")) logger.log(LogLevel.warn(), "gfx", "slow", fields^) # logger.sink.records[1] prints as "[WARN] gfx: slow adapter=lavapipe" logger.min_level = LogLevel.trace() # the filter can be changed at any time ``` Other subsystems do not hold a logger. `Platform`, `Gfx` and `ShaderManager` collect `LogRecord`s in a `diagnostics` list and hand them over through `take_diagnostics()`; the owner forwards them to its logger once per frame. ## Assertions `cinder_assert(condition, message)` is compiled out when the build defines `CINDER_ASSERTS=0` (`mojo build -D CINDER_ASSERTS=0`); `cinder_check` is never compiled out and guards memory safety (handle validation, FFI preconditions). Both call `std.os.abort` with `cinder assertion failed: ...` or `cinder check failed: ...`. Recoverable conditions raise instead; a failed assertion is a bug. Tests verify the abort in a child process with `std.testing.assert_aborts._assert_aborts` (the module name shadows the function, so the underscore form is called directly). ## Arena `LinearArena(capacity)` allocates one block (plus one cache line so any alignment up to 64 can be served) and bumps an offset. `alloc_bytes(size, alignment=16)` returns a `Pointer[UInt8, MutUntrackedOrigin]`; `alloc[T](count)` returns uninitialised, `T`-aligned storage. `reset()` makes everything available again and `high_water()` is the number to size the arena by. ```mojo from cinder.core.arena import LinearArena var arena = LinearArena(1024) var p16 = arena.alloc_bytes(10, 16) # 16-byte aligned var f = arena.alloc[Float64](4) # aligned for Float64 f[] = 1.5 f[unsafe_offset=3] = 2.5 arena.reset() # every earlier pointer now dangles var again = arena.alloc_bytes(3, 1) # same address as the first allocation ``` Raises on a non-positive capacity, on an alignment that is not a power of two in [1, 64], on a negative size and when the block is exhausted (`LinearArena exhausted: ...`). Filling it exactly is allowed. ## Handles and pools `Handle[Tag]` is a 64-bit generational handle (32-bit slot, 32-bit generation) whose `Tag` is a phantom type: a `Handle[TextureTag]` cannot be passed where a `Handle[BufferTag]` is expected. `packed()` and `from_packed()` round-trip through one `UInt64` for serialisation. `Pool[T, Tag](capacity)` is a slot map with O(1) `insert`, `get`, `update`, `remove`; generations start at 1 and skip 0 on wrap, so the zero handle is never valid. Slots are never reallocated: the pool holds at most `capacity` values and `insert` raises `Pool is full` instead of growing, which keeps steady-state frames allocation-free. ```mojo from cinder.core.handles import Handle, Pool struct TextureTag: var _unused: UInt8 var pool = Pool[String, TextureTag](4) var a = pool.insert(String("a")) var removed = pool.remove(a) # "a"; a is now stale var c = pool.insert(String("c")) # reuses a's slot with a new generation # c.index == a.index, c.generation != a.generation # pool.get(a) raises "invalid handle Handle(0@1)" pool.update(c, String("c2")) ``` `get` returns a copy (pooled values are small records); `handles()` allocates and is for tools and tests. `is_valid` is the non-raising check. `cinder/gfx/resources.mojo` builds every GPU resource pool on this type. ## Ring buffer, bytes, hashing `RingBuffer[T](capacity)` requires a power-of-two capacity (>= 2). `push` returns `False` when full without dropping anything queued, `pop` returns `None` when empty, `peek` raises when empty. It is single-threaded by design: cross-thread queues live in the shim (`CnEventQueue`), see ADR-0003. `ByteWriter` appends little-endian `u8`, `u32`, `i32`, `f32`, `vec2`, `vec3`, `vec3_padded`, `vec4` and column-major `mat4` values for GPU uploads; `pad_to(16)` aligns for std140 rules (the layout itself is the caller's job). `pad_to` needs a positive alignment: 0 or a negative value is a bug that `cinder_assert` aborts on (with assertions compiled out it pads nothing). `reset()` keeps capacity for frame scratch; `take()` consumes the writer. `fnv1a64` / `fnv1a64_string` are the stable content hashes used by the asset cooker; `hex64` formats them. `decode_base64` accepts standard and URL-safe alphabets, skips whitespace and treats padding as optional; it raises on any other character. ## Filesystem and time `read_bytes`, `read_text`, `write_bytes`, `write_text` wrap `open` and raise `FileError(path, message)`, declared as `raises FileError`, so a caller sees the path in the error text (`FileError(path: message)`). `file_exists` never raises. `Clock` is a monotonic nanosecond clock; `tick()` returns seconds since the previous tick and is never negative. `FixedStep(hz=60.0, max_steps_per_frame=8)` is the accumulator that drives the simulation: ```mojo from cinder.core.time import FixedStep var step = FixedStep(hz=60.0, max_steps_per_frame=4) var n = step.advance(frame_seconds) # simulation steps to run this frame, capped at 4 var alpha = step.alpha() # [0, 1): interpolation between the last two states ``` When the cap is reached with more than a step still owed, the backlog is dropped rather than stalling rendering. Negative frame times count as zero. ## Images `Image(width, height)` is a tightly packed RGBA8 buffer. `load_image(lib, path)` and `decode_image(lib, bytes)` go through the shim's stb wrappers and raise with `cn_image_failure_reason()` in the message; `save_png` raises when the file cannot be written. `Image.from_rows(width, height, rows, stride)` repacks a GPU readback whose rows are padded to the 256-byte copy alignment; `swap_rb()` converts BGRA swapchain readbacks. `pixel(x, y)` and `set_pixel(x, y, v)` abort through `cinder_check` (in every build) when `x` is outside `[0, width)` or `y` outside `[0, height)`. `compare(a, b, tolerance=2, window=8)` returns an `ImageDiff` with SSIM on luma over non-overlapping windows plus `max_channel_delta` and `differing_pixels`; it raises when the sizes differ. Golden-image tests bound all three. ## Job system `JobSystem(lib, worker_count, job_capacity=1 << 14)` starts `worker_count` threads (`cinder-worker`, created through `cn_thread_create`) each owning a Chase-Lev deque of job indices; deque 0 belongs to the main thread. A job is a thin function pointer `JobFn = def(Pointer[NoneType, MutUntrackedOrigin]) thin -> None` plus an untyped data pointer and an optional `Counter`. `submit` queues from the main thread and returns `False` when the job table or the main deque is full: the caller must then run the job inline. `wait(counter)` runs jobs on the calling thread until the counter reaches zero, so waiting never idles a core. `reset()` recycles the job table and is legal only when nothing is queued or running (frame start). With `worker_count == 0` everything runs on the thread that waits. ```mojo from cinder.core.jobs import Counter, JobFn, JobSystem def _sum_chunk(data: Pointer[NoneType, MutUntrackedOrigin]) -> None: # SAFETY: data was submitted as a ChunkJob kept alive until the counter reaches zero. var job = data.unsafe_bitcast[ChunkJob]()[] ... var lib = NativeLibrary() var jobs = JobSystem(lib.ptr(), 3) # three worker threads var counter = Counter() var counter_ptr = Pointer(to=counter).unsafe_mut_cast[True]().unsafe_origin_cast[MutUntrackedOrigin]() for c in range(CHUNKS): if not jobs.submit(_sum_chunk, desc_ptr.unsafe_offset(c).unsafe_bitcast[NoneType](), Optional(counter_ptr)): _sum_chunk(desc_ptr.unsafe_offset(c).unsafe_bitcast[NoneType]()) # saturated: run inline jobs.wait(counter_ptr) keep(descriptors) keep(counter) ``` Construction raises on a worker count outside [0, 64], a non-power-of-two capacity, or a failed semaphore or thread creation (workers already started are stopped and joined first). The orderings are the conservative ones from ADR-0008; `executed()` is a diagnostic count. ## Profiling `Profiler(lib)` wraps the shim's Tracy client. Nothing listens on the network until `start()`, which is idempotent and returns `False` when the shim was built without Tracy (`available()`). While stopped, `zone`, `plot`, `message` and `frame_mark` are no-ops and `Zone.id` is 0. A zone is one line: ```mojo from cinder.core.profile import COLOR_RENDER, Profiler var profiler = Profiler(lib.ptr()) _ = profiler.start() var z = profiler.zone(String("frame"), COLOR_RENDER) z.text(String("frame body")) _ = z^ # ends the zone; otherwise it ends at the last use of `z` profiler.frame_mark() ``` Tracy copies zone names, zone text and messages during the call, but keys plots by the name pointer and reads it until the process exits. `plot(name, value)` therefore interns each distinct name once (a never-freed copy shared by every `Profiler` copy; `plot_name_count()` reports how many) and hands Tracy that copy, so a temporary `String` is safe and repeated plots of a name do not allocate. Plots sent while profiling is stopped are dropped without interning. ## C strings `string_from_c(ptr, max_len=-1)` turns text returned by C (SDL errors, driver names, SDL log lines, stb failure reasons) into a `String`. It decodes the bytes as UTF-8, replacing invalid sequences with U+FFFD; building a `String` one `chr(byte)` at a time would double-encode every non-ASCII character. `max_len` caps the scan for fixed-size C buffers without a guaranteed terminator. ## Boxed `Boxed[T](value^)` owns `T` at a stable heap address. Subsystems that hand out pointers to themselves (`Gfx`, `Resources`, `JobSystem`, the renderer) must not move after those pointers exist, so the `App` runtime keeps them in `Boxed` cells and passes `box.ptr()`; `box[]` is a mutable reference. ## Math Fixed conventions (`docs/ARCHITECTURE.md`): right-handed, Y up, -Z forward, metres, column-major matrices, reverse-Z depth with an infinite far plane. `Vec3` keeps its fourth SIMD lane at zero. `Mat4` stores columns `c0..c3`; `m[row, col]` indexes an element, `column(c)` and `row(r)` return `Vec4`s. Indices outside 0..3 are bugs: `cinder_assert` aborts (with assertions compiled out the index wraps to `i & 3`). Constructors: `identity`, `zero`, `from_rows`, `translation`, `scaling`, `rotation(q)`, `trs`, `look_at`, `perspective_reverse_z_infinite(fov_y, aspect, near)`, `orthographic_reverse_z(l, r, b, t, near, far)`. Point and vector transforms are `transform_point`, `transform_vector`, `project_point` (perspective divide). `Quat` offers `from_axis_angle`, `from_euler(yaw, pitch, roll)`, `rotate`, `nlerp`, `slerp`, `inverse` (the zero quaternion returns the zero quaternion, like `Mat4.inverse` of a singular matrix); `Transform` is a TRS triple with `to_mat4`, `from_mat4` (no shear; a negative determinant folds into the x scale and the rotation is taken from the un-mirrored basis, so `to_mat4()` rebuilds the input), `__mul__` (parent * child) and `lerp`. ```mojo from cinder.core.math import Mat4, Vec3, approx_eq, radians var proj = Mat4.perspective_reverse_z_infinite(radians(60.0), 16.0 / 9.0, 0.1) var on_near = proj.project_point(Vec3(0.0, 0.0, -0.1)) # depth 1.0 var far = proj.project_point(Vec3(0.0, 0.0, -100000.0)) # depth approaches 0.0 var view = Mat4.look_at(Vec3(1.0, 2.0, 3.0), Vec3(1.0, 2.0, -7.0), Vec3.unit_y()) ``` Comparisons use `approx_equal(other, eps=EPSILON)`; `approx_eq` is absolute near zero and relative elsewhere. `tests/core/test_math.mojo` checks every operation against Float64 reference loops over Philox-seeded inputs. ## Threading and error handling - Everything in `core` except `JobSystem` is single-threaded and owned by one thread. `RingBuffer`, `LinearArena`, `Pool` and the loggers have no internal synchronisation. - `JobSystem` is the only place Mojo code runs on a thread Cinder did not inherit from the OS loader: `_worker_entry` is an `abi("C")` function invoked by the shim's thread (ADR-0008, spike 4). Job data must stay valid until the counter it was submitted with reaches zero; `std.benchmark.keep` the descriptors and the counter after `wait`. `submit` and `reset` are main-thread calls. - Raises (recoverable, `Error` unless noted): `LinearArena` construction and allocation, `Pool` construction, `insert`, `get`, `update`, `remove`, `RingBuffer` construction and `peek`, the `fs` functions (`FileError`), `Image` construction from a wrong-sized buffer, `from_rows`, `load_image`, `decode_image`, `save_png`, `compare`, `decode_base64`, `JobSystem` construction. - Aborts: `cinder_assert`, `cinder_check`. Out-of-range `List` indexing in Mojo also aborts; the handle and ring types check their indices before touching storage. - Returns a status instead of raising: `RingBuffer.push` (`False` when full), `JobSystem.submit` (`False` when saturated), `Profiler.start` (`False` without Tracy). - Logging never raises; a sink that fails drops the record. ## Pitfalls - Untracked pointers do not keep their pointee alive (`docs/MOJO_NOTES.md`). Everything handed to C or to a job by pointer (`Image.pixels`, a `String` turned into a C string, job descriptors) must be followed by `keep(value)` after the last call that can read it. `profile.mojo` and `image.mojo` do this internally for their own temporaries; callers do it for job data. - `Pointer(to=x)` cast to `MutUntrackedOrigin` ends the compiler's view of `x`; `_ = x` is not a read. - A struct that stores pointers to its own fields breaks when moved. Box it (`Boxed`) and hand out `box.ptr()`. - `String` has inline small-string storage; a C pointer from `as_c_string_span()` dies when the owning `List[String]` reallocates. Hand long-lived C strings as `List[UInt8]`. - `Pool.get` copies; write back with `update`. A `Handle` whose slot was removed and reused fails validation rather than aliasing the new value. - `LinearArena.reset()` invalidates every pointer it ever returned; nothing checks this at run time. - `JobSystem.reset()` while jobs are queued corrupts the job table. The frame loop calls it once at frame start. - `FixedStep` drops backlog beyond `max_steps_per_frame` steps; if simulation must never skip, raise the cap, do not loop on `advance`. - `List[...] = [x]` literals allocate every time; `List.clear()` keeps capacity. Per-frame scratch is a persistent field cleared each frame, never a fresh list (`docs/MOJO_NOTES.md`, Phase 8). - A destructor that aborts on leaks hides the real failure when a test raises; count and assert in tests instead. ## Related documents - `docs/ARCHITECTURE.md` (module map, conventions), `docs/THREADING.md` (threads and shared state). - `docs/MOJO_NOTES.md` (lifetime rules that every `unsafe_` call here relies on). - ADR-0003 `docs/adr/0003-native-shim-pull-model.md`: why cross-thread queues are in C and `RingBuffer` is single-threaded. - ADR-0008 `docs/adr/0008-job-system-threads.md`: job system design. - Tests: `tests/core/test_core.mojo` (handles, arena, ring, time, fs, logging, assertions, `ByteWriter.pad_to` bounds, UTF-8 C strings), `tests/core/test_math.mojo`, `tests/core/test_jobs.mojo` (contention, saturation, zero workers), `tests/core/test_image.mojo` (PNG round trip, SSIM), `tests/core/test_profile.mojo`. --- > Cinder 0.1.0 documentation: ECS guide (cinder/ecs) > Canonical page: https://cinder.ash10.dev/docs/guides/ecs/ · Source: `docs/guides/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 # ECS guide (`cinder/ecs`) `cinder/ecs` is the archetype entity component system every gameplay-facing module builds on. It sits directly above `cinder/core` (it uses `core/math` for `Mat4` and `core/jobs` for the scheduler) and below `render`, `physics`, `anim`, `audio`, `assets` and `scene`, which define their own components on top of it. The `App` runtime owns one `World`; the sample game and every bridge (`PhysicsSync`, the animation system, `RenderBridge`) read and write that world. Source: `cinder/ecs/world.mojo`, `commands.mojo`, `hierarchy.mojo`, `schedule.mojo`. API reference: [`docs/api/ecs.md`](https://cinder.ash10.dev/docs/api/ecs.md). Tests: `tests/ecs/test_ecs.mojo`. Decision record: [ADR-0011](https://cinder.ash10.dev/docs/adr/0011-ecs-mojo-vs-flecs.md) (why this ECS and not flecs, and the benchmark that keeps it honest). ## Model - An `Entity` is a generational id (`index`, `generation`). `Entity.null()` is never live; `World.is_alive(e)` is false for despawned entities even after their slot is reused. - A component is any struct conforming to `Component`. The trait is `Copyable, Deinitable, Movable` plus a static `component_id() -> Int` that must be unique per type and lie in `0 .. MAX_COMPONENTS - 1` (256). `Copyable` is required (so `World.get` can hand out a copy); `ImplicitlyCopyable` is not. The sample marks its components `ImplicitlyCopyable` for convenience, the engine's own (`Parent`, `MeshRenderer`, ...) do not. - Entities with the same component set share an `Archetype`: one `Column` per component, each a contiguous, 16-byte-aligned byte array with one element per row, parallel to the archetype's entity list. Element stride is `component_stride[T]()`: the size rounded up to the type's alignment, so an 8-byte component costs 8 bytes per row. A type whose alignment exceeds 16 is rejected at registration. - Columns are type-erased. On first use `World.register[T]()` records size, alignment, stride, the type name (`reflect[T].name()`) and two thin function pointers (`_destroy[T]`, `_move[T]`), so heap-owning components (a `String`, a `List`) are destroyed and moved correctly. Registering a second type under an id already taken raises (`two types`), even when both types have the same size: the type name, size and alignment must all match. Rows are relocated bytewise on archetype moves and column growth, so a component must not hold pointers into itself. - Adding or removing a component moves the entity to another archetype (creating it on demand); despawning swap-removes the row. Replacing an existing component with `add` keeps the archetype and marks the row changed. ### Component ids in use | id | component | module | |---|---|---| | 1, 2, 3 | `Parent`, `LocalTransform`, `WorldTransform` | `cinder/ecs/hierarchy.mojo` | | 4, 5, 6 | `MeshRenderer`, `LightSource`, `CameraComponent` | `cinder/scene/render_sync.mojo` | | 7 | `RigidBody` | `cinder/physics/sync.mojo` | | 8 | `AnimatorRef` | `cinder/anim/system.mojo` | | 9 | `JointPalette` | `cinder/scene/render_sync.mojo` | | 10, 11 | `AudioSource`, `AudioListener` | `cinder/audio/sync.mojo` | | 100 .. 105 | sample game components | `sample/game/components.mojo` | Two types sharing an id is caught only when their sizes differ (`register` raises); pick an unused id and keep game components well above the engine's range. ## Entities and components From `tests/ecs/test_ecs.mojo`: ```mojo from cinder.ecs import Component, Entity, World @fieldwise_init struct Position(Component): var x: Float32 var y: Float32 @staticmethod def component_id() -> Int: return 10 var world = World() var a = world.spawn() world.add[Position](a, Position(1.0, 2.0)) world.add[Velocity](a, Velocity(0.5, 0.0)) assert_true(world.has[Position](a) and not world.has[Label](a)) assert_equal(world.get[Position](a).x, 1.0) # a copy world.add[Position](a, Position(3.0, 3.0)) # replace: same archetype, marked changed assert_true(world.remove[Velocity](a)) # False when absent world.despawn(a) assert_false(world.is_alive(a)) ``` Access paths and what they do to change ticks: | call | returns | marks changed | |---|---|---| | `get[T](e)` | copy | no | | `get_ptr[T](e)` | `Pointer[T, MutUntrackedOrigin]` | no | | `get_mut[T](e)` | same pointer | yes | | `set[T](e, v)` / `add[T](e, v)` | - | yes | Pointers from `get_ptr`/`get_mut`/`Chunk` are valid until the next structural change (spawn, despawn, add, remove) on the world. `get`, `get_ptr`, `get_mut`, `set`, `remove` and `despawn` raise on a dead entity ("not alive") or a missing component ("no component"). ## Queries and chunks `world.query()` builds a filter; `.chunks()` returns one `Chunk` per non-empty archetype whose signature has every `with_` component and none of the `without` ones. A chunk gives typed access to that archetype's rows: ```mojo _ = world.advance_tick() for chunk in world.query().with_[Position]().with_[Velocity]().chunks(): var pos = chunk.column[Position]() # tight typed column base var vel = chunk.column[Velocity]() for row in range(chunk.count): pos[unsafe_offset=row].x += vel[unsafe_offset=row].dx pos[unsafe_offset=row].y += vel[unsafe_offset=row].dy chunk.mark_changed[Position](row) ``` (The loop body is the one `bench/ecs_bench.mojo` times; the test does the same through `chunk.get[T](row)[]`.) `chunk.get[T](row)` returns a pointer to one element, `chunk.entity(row)` the owning entity, `chunk.has[T]()` whether the archetype carries a component you did not filter on (the render bridge uses this for the optional `JointPalette`). `Chunk.stride[T]()` is the element stride if you step through the column by bytes. Writing through `column`/`get` does not touch ticks; call `mark_changed[T](row)` yourself. `column`, `get`, `mark_changed` and `changed_since` abort (`cinder_check`, kept in release builds: one compare per call) when the chunk's archetype has no `T`; test `chunk.has[T]()` first for a component you did not filter on. ADR-0011 records that the typed `column[T]()` loop is what the compiler vectorises at flecs parity; byte stepping with a runtime stride was slower. A `Query` holds a pointer to the world, so it is built and consumed in one place. Systems that run every frame keep a `QuerySpec` (the same filters without the pointer) and a chunk list and call `world.collect_chunks(spec, chunks)`, which clears and refills the list without allocating once it has grown to size: ```mojo struct MySystem(Movable): var spec: QuerySpec var chunks: List[Chunk] def __init__(out self): self.spec = QuerySpec().with_[Position]().with_[Velocity]() self.chunks = List[Chunk]() def run(mut self, world: World): world.collect_chunks(self.spec, self.chunks) for ci in range(len(self.chunks)): var chunk = self.chunks[ci] ... ``` This is the pattern behind the zero-allocation frame ([ADR-0014](https://cinder.ash10.dev/docs/adr/0014-sample-game-and-performance.md)). ## Change detection Every column keeps a `UInt32` tick per row. `World.tick` starts at 1 and `advance_tick()` increments it; the `App` calls it once per fixed step. A row's tick is set to the current tick by `add`, `set`, `get_mut`, `Chunk.mark_changed` and by `CommandBuffer.apply`. Readers compare: ```mojo var start_tick = world.tick _ = world.advance_tick() # ... systems run ... for chunk in world.query().with_[Position]().chunks(): for row in range(chunk.count): if chunk.changed_since[Position](row, start_tick): ... ``` `changed_since` is a strict `>` against the stored tick, so record the tick *before* advancing and writing. `world.changed_tick[T](e)` reads one entity's tick. A newly spawned row carries the tick of its spawn; a component carried across an archetype move keeps its tick. ## Command buffers Structural changes are immediate on the `World`, which makes them illegal while iterating chunks (the row you are on may be swap-removed) and from jobs. `CommandBuffer` records them instead: ```mojo _ = world.register[Position]() # the world must know every type a buffer will add _ = world.register[Label]() var cmds = CommandBuffer() var p0 = cmds.spawn() # placeholder index, not an Entity cmds.add_to_spawned[Position](p0, Position(1.0, 1.0)) cmds.add_to_spawned[Label](p0, Label(String("deferred"))) cmds.add[Label](existing, Label(String("late label"))) cmds.remove[Position](existing) cmds.despawn(existing) var applied = cmds.apply(world) # in order; clears the buffer var e0 = cmds.resolved[0] # real entity for placeholder 0 ``` Payloads are stored as raw 16-byte blocks together with the type's destroy function, so a buffer dropped without `apply` destroys what it owns (checked under ASan in the test). `apply` moves each payload into the world through the registered move function; adding a type the world has never seen raises `never registered` rather than corrupting a column, so register (or `add` once directly) before the first deferred add. A despawn of an entity that died in the meantime is skipped silently; a `remove` of an absent component is a no-op. When a command raises (unregistered type, an `add` or `remove` on a dead entity), `apply` propagates the error with the buffer in a consistent state: the commands before the failing one have taken effect and left the buffer (their payloads now belong to the world), the failing one and those after it remain and still own their values. Fix the cause and call `apply` again to continue (spawns already made keep their entity in `resolved`), or call `cmds.clear()` (or drop the buffer) to discard the rest, which destroys their values. ## Hierarchy and transforms `hierarchy.mojo` defines `Parent(entity)`, `LocalTransform(matrix)` and `WorldTransform(matrix)` (all `Mat4`, column-major, right-handed Y-up as everywhere in Cinder) and `propagate_transforms(world) -> Int`: ```mojo world.add[LocalTransform](root, LocalTransform(Mat4.translation(Vec3(1.0, 0.0, 0.0)))) world.add[LocalTransform](child, LocalTransform(Mat4.translation(Vec3(0.0, 2.0, 0.0)))) world.add[Parent](child, Parent(root)) assert_equal(propagate_transforms(world), 2) # both WorldTransforms written assert_equal(propagate_transforms(world), 0) # unchanged values are not rewritten ``` Rules, all from the implementation: - Every entity with a `LocalTransform` gets `WorldTransform = parent.world * local`; the component is added when missing. An entity with a `Parent` but no `LocalTransform` is treated as identity for its children but gets no `WorldTransform` itself. - Parents are resolved recursively with memoisation, so archetype order does not matter and each entity is computed once per call. A dead or self-referencing parent is ignored (the entity becomes a root); a chain deeper than 256 raises `cycle?`. - Only values that actually changed are written through `set`, so change ticks on `WorldTransform` are meaningful. The return value is the number of entities updated. - Scratch lists (`prop_computed`, `prop_results`, `prop_todo`, `prop_chunks`, `prop_spec`) live in the `World` and are reused, so a steady-state call allocates nothing. The `App` calls `propagate_transforms` once per frame after animation and physics have written local transforms and before `RenderBridge.build` reads the world transforms. ## Schedule `Schedule` runs systems whose component access is declared up front. A system is a thin function `def(Pointer[World, MutUntrackedOrigin]) -> None` (`SystemFn`); `SystemDesc` carries its name, the components it reads and writes, and an `exclusive` flag for systems that do structural changes: ```mojo def _integrate(world: Pointer[World, MutUntrackedOrigin]): for chunk in world[].query().with_[Position]().with_[Velocity]().chunks(): for row in range(chunk.count): var p = chunk.get[Position](row) var v = chunk.get[Velocity](row) p[].x += v[].dx p[].y += v[].dy var integrate: def(Pointer[World, MutUntrackedOrigin]) thin -> None = _integrate var schedule = Schedule() schedule.add(SystemDesc(String("integrate"), integrate).reads_[Velocity]().writes_[Position]()) schedule.add(SystemDesc(String("decay"), decay).writes_[Health]()) # same stage schedule.add(SystemDesc(String("read positions"), read).reads_[Position]()) # new stage assert_equal(schedule.stage_count(), 2) schedule.run(world, Optional[Pointer[JobSystem, MutUntrackedOrigin]](jobs_ptr)) schedule.run(world, Optional[Pointer[JobSystem, MutUntrackedOrigin]]()) # inline, same result ``` `build()` (called lazily by `run` and `stage_count`) walks systems in declaration order: a system joins the current stage unless it conflicts with any system already there (write/write or read/write on the same component id, or either side `exclusive`), in which case a new stage starts. Later systems can still join an earlier-opened stage only if it is the *last* one, so declaration order affects packing. Stages run one after another; within a stage every system is submitted as a job on the given `JobSystem` and `run` waits on a `Counter` before the next stage. Without a job system, or for a single-system stage, systems run inline on the calling thread. ## Threading rules - The `World` has no locks and is owned by the main thread. Spawn, despawn, add, remove, `register`, `advance_tick`, `propagate_transforms`, `CommandBuffer.apply` and anything that can create an archetype are main-thread operations. - Systems inside a `Schedule.run` stage run concurrently on job workers. They may read and write component data through chunks, restricted to what they declared; they must not perform structural changes unless marked `exclusive` (which serialises them into their own stage). Record structural changes into a `CommandBuffer` and apply it on the main thread after `run`. - Anything else that touches the world from a job (the animation system's pose jobs, for instance) follows the same rule: read declared data, publish back on the main thread. - `Schedule.run` blocks the caller until every stage has finished; it is called from the main thread, which also executes jobs while waiting (see [`docs/THREADING.md`](https://cinder.ash10.dev/docs/threading.md)). ## Pitfalls - A pointer from `get_ptr`, `get_mut` or a `Chunk` dangles after the next structural change. Re-query after `add`/`remove`/`spawn`/`despawn`/`apply`. - `remove[T]` on the last component of an entity moves it back to the empty archetype; the entity stays alive. - Components are relocated bytewise (`unsafe_memcpy`): never store a pointer to a component's own field inside it, and never keep raw pointers to components across frames. - `Entity` indices are reused after despawn with a bumped generation; compare whole entities (or `packed()`), never bare indices, and check `is_alive` before trusting a stored handle. - `World.entities()` allocates a fresh list; it is for tests and tooling, not per-frame code. `Query.chunks()` also allocates; use `QuerySpec` + `collect_chunks` in systems. - Two component types with the same `component_id()` and the same size are not detected. - `propagate_transforms` ignores a `Parent` pointing at a dead entity instead of raising; a child whose parent was despawned silently becomes a root. - See [`docs/MOJO_NOTES.md`](https://cinder.ash10.dev/docs/mojo-notes.md) for the lifetime rules behind the `MutUntrackedOrigin` pointers used throughout (`keep` after the last C call, no copies of function pointers out of their owning struct). --- > Cinder 0.1.0 documentation: cinder/gfx guide > Canonical page: https://cinder.ash10.dev/docs/guides/gfx/ · Source: `docs/guides/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` guide `cinder/gfx` is the safe, typed layer over wgpu-native. It owns the wgpu instance, adapter, device and queue (`Gfx`), keeps every GPU object the engine creates in generational pools behind typed handles (`Resources`), compiles WGSL with editor-friendly diagnostics and hot reload (`ShaderManager`), translates engine-side descriptors into wgpu descriptor graphs (`pipeline`), records and submits command buffers (`commands`), configures and presents a window surface (`Swapchain`) and draws debug lines (`DebugDraw`). It sits above `cinder/core` and `cinder/platform`; the renderer, tools and app layers build on it and never call wgpu directly. Source: `cinder/gfx/*.mojo`. Reference: `docs/api/gfx.md`. Design: ADR-0009. Tests: `tests/gfx/test_device.mojo`, `test_resources.mojo`, `test_shaders.mojo`, `test_render.mojo`, `test_debug_draw.mojo` (headless, lavapipe in CI) and `tests/platform/test_swapchain.mojo` (windowed). ## Key types and ownership | Type | Owns | Lifetime rules | |---|---|---| | `Gfx` | `instance`, `adapter`, `device`, `queue`; the shim event queue (`events`, capacity 4096); the window `surface` when constructed from a `Window`. | One per device. Other gfx objects hold `Pointer[Gfx, MutUntrackedOrigin]`, so it must outlive them and must not move (the app keeps it in a `Boxed`). `__deinit__` releases everything in reverse order. | | `GfxError` | A recoverable failure: adapter or device request, `await_event` timeout, surface problems. | Raised by `Gfx`, `Swapchain`, `wait_for_queue`. | | `Resources` | One `Pool` per kind: buffers, textures, texture views, samplers, shaders, bind group layouts, pipeline layouts, bind groups, render pipelines, compute pipelines. | Holds a pointer to `Gfx`; destroy it before the `Gfx`. `__deinit__` releases whatever is still alive (imported views are only dropped) and, if anything was, appends a `gfx.resources` warning with the counts to `Gfx.diagnostics`. | | `BufferHandle`, `TextureHandle`, `TextureViewHandle`, `SamplerHandle`, `ShaderHandle`, `BindGroupLayoutHandle`, `PipelineLayoutHandle`, `BindGroupHandle`, `RenderPipelineHandle`, `ComputePipelineHandle` | Nothing: `Handle[Tag]` values (`cinder.core.handles`) with one phantom tag per kind. | Copyable; stale handles raise `invalid handle`. A `TextureHandle` cannot be passed where a `BufferHandle` is expected. | | `BufferDesc`, `TextureDesc`, `SamplerDesc` | Creation parameters (label, size/extent, usage bits, format). | Plain values. | | `ShaderManager` | The list of loaded shader files (path, mtime); the modules themselves live in `Resources.shaders`. | Holds pointers to `Gfx` and `Resources`. | | `ShaderError`, `ShaderMessage` | Compile failure with `path:line:column: error: text` messages. | Raised by `compile_source` and `load`. | | `LayoutEntry`, `BindingKind`, `BindingResource`, `VertexLayout`, `VertexAttribute`, `DepthState`, `BlendMode`, `ColorTarget`, `RenderPipelineDesc` | Engine-side descriptors. | Plain values; `create_*` functions translate them. | | `CommandEncoder`, `RenderPass`, `ComputePass`, `ColorAttachment`, `DepthAttachment` | One command buffer and its passes. | `finish_and_submit(deinit self)` consumes the encoder; an encoder dropped without it is released and its commands discarded. `RenderPass.end` / `ComputePass.end` are idempotent; a pass dropped without `end` is ended and released by its destructor right after its last use. | | `Swapchain`, `SurfaceFrame` | The surface configuration; one acquired texture and its view per frame. | `present(frame^)` or `discard(frame^)` consumes the frame. `__deinit__` unconfigures the surface. | | `DebugDraw`, `Color8` | Its shader, pipeline, camera buffer and bind group, and a doubling vertex buffer, all in `Resources`. | Holds pointers to `Gfx` and `Resources`. | | `util`: `string_view`, `empty_string_view`, `ptr_to`, `opaque_to`, `c_message`, `zero_event`, `handle_from` | Conversions for raw calls. | `string_view(text)` and `ptr_to(value)` borrow; the pointee must be `keep`-fenced past the C call. | Swapchain views are the only raw `WGPUTextureView`s outside the pools: their lifetime is one frame and they belong to the surface. `ColorAttachment` therefore takes raw views (`frame.view` or `res.texture_view(h).raw`), and `Resources.import_texture_view` / `forget_texture_view` wrap a swapchain view under a `TextureViewHandle` without releasing it. `destroy_texture_view` raises on an imported view and `forget_texture_view` raises on an owned one, so neither can release a view the caller owns or leak one `Resources` owns. ## Creating a device ```mojo from cinder.ffi.cinder_native import WGPUSurface from cinder.gfx import Gfx from cinder.platform import NativeLibrary var lib = NativeLibrary() var gfx = Gfx(lib.ptr(), WGPUSurface()) # headless; or Gfx(lib.ptr(), window) assert_true(Bool(gfx.device) and Bool(gfx.queue)) gfx.pump(False) assert_equal(gfx.error_count, 0) keep(gfx) keep(lib) ``` Construction creates the instance, requests an adapter (compatible with the surface when one is given), requests a device with the optional features the adapter has (`wireframe_supported` for wgpu-native's `PolygonModeLine`, `bc_supported` for BC texture compression) and reads `adapter_name` and `backend`. Each async request is awaited through the shim queue (see below); a failure tears everything down and raises `GfxError`. ## Pulling callbacks No wgpu callback ever runs Mojo code (ADR-0003). The shim registers every callback and writes a `CnEvent` into `Gfx.events`. `Gfx.pump(wait)` calls `cn_wgpu_pump` (`wgpuInstanceProcessEvents` plus `wgpuDevicePoll`) and drains the queue: - `CN_EVENT_WGPU_LOG` becomes a `LogRecord` in `diagnostics` with category `wgpu` (error, warn or info by wgpu level; the shim only forwards `Warn` and above). - `CN_EVENT_WGPU_UNCAPTURED_ERROR` increments `error_count` and adds an error record with category `wgpu.validation` and a `type` field. - `CN_EVENT_WGPU_DEVICE_LOST` sets `lost = True` and adds an error record with category `wgpu.device`. - Everything else (adapter, device, error scope, buffer mapped, work done) is held in `_pending` for `await_event(kind, request_id, timeout_ns=10 s)`, which pumps with `wait=True` until the matching event arrives and raises `GfxError("timed out waiting for wgpu event ...")` otherwise. `take_pending` moves matching events out without blocking. The frame loop calls `gfx.pump(False)` once per frame and forwards `take_diagnostics()` to the logger. Device loss is reported, not handled: the application decides what to do with `gfx.lost`. ## Resources ```mojo from cinder.gfx.resources import BufferDesc, Resources, SamplerDesc, TextureDesc var res = Resources(_gfx_ptr(gfx), 64) # pool capacity for buffers/textures/views/bind groups var buf = res.create_buffer(BufferDesc(String("vertices"), 1024, WGPUBufferUsage_Storage | WGPUBufferUsage_CopyDst | WGPUBufferUsage_CopySrc, False)) var tex = res.create_texture(TextureDesc(String("albedo"), 64, 32, 1, 1, 1, WGPUTextureFormat_RGBA8Unorm, WGPUTextureUsage_TextureBinding | WGPUTextureUsage_RenderAttachment)) var view = res.create_texture_view(tex) var sampler = res.create_sampler(SamplerDesc(String("linear"), True, WGPUAddressMode_Repeat, 1)) res.write_buffer(buf, 0, Span(data)) # raises when offset + len exceeds the buffer res.destroy_texture_view(view) res.destroy_texture(tex) res.destroy_sampler(sampler) res.destroy_buffer(buf) assert_equal(res.live_counts().total(), 0) ``` `Resources(gfx, capacity=4096)` sizes the buffer, texture, view and bind group pools by `capacity`; samplers get 256 and shaders, layouts and pipelines 1024 each. `create_*` raise when wgpu returns NULL (out of memory); wgpu validation failures do not return NULL and surface through `Gfx.pump` instead. `write_buffer` and `write_texture` upload through the queue and copy immediately (no mapping). `create_texture_view_ex` selects dimension, mip and layer ranges; `create_comparison_sampler` is the reverse-Z PCF sampler (`GreaterEqual`). Layouts, bind groups and pipelines are created by `cinder.gfx.pipeline` and adopted (`adopt_*`); `replace_render_pipeline` and `replace_compute_pipeline` swap the object behind an existing handle for hot reload. `adopt_*` raise on a null object; `adopt_*` and `create_*` release the new object when its pool is full, so ownership passes even when they raise. A texture cannot be destroyed while views of it are alive: `destroy_texture` raises (releasing nothing) until those views are destroyed; `texture_view_count(h)` reports how many there are. `live_counts()` reports what is alive per kind; the engine asserts `total() == 0` before shutdown. Leaked objects are released by `__deinit__` without error, and it appends one `gfx.resources` warning (`released N objects left alive at shutdown: LiveCounts(...)`) to `gfx.diagnostics`, readable afterwards with `take_diagnostics` (`test_shutdown_releases_leaks_and_counts_them`). ## Shaders ```mojo from cinder.gfx.shaders import ShaderManager var shaders = ShaderManager(_ptr(gfx), _ptr(res)) var handle = shaders.load(String("shaders/lit.wgsl")) # compiles, adopts, registers for reload ... for result in shaders.check_for_changes(): # once per frame or on a timer if result.success: print("reloaded", result.path) # same handle, new module behind it: # rebuild pipelines that use it for record in shaders.take_diagnostics(): logger.log(record.level, record.category, record.message) ``` `compile_source(path, source) -> WGPUShaderModule` is the primitive: `wgpuShaderModuleGetCompilationInfo` is unimplemented in wgpu-native 29 (it aborts the process), so the module is created inside a `Validation` error scope that is popped through the shim queue (`cn_wgpu_device_pop_error_scope`, `CN_EVENT_WGPU_ERROR_SCOPE`). naga marks locations as `wgsl:LINE:COLUMN`; `parse_wgsl_report` turns each mark into a `ShaderMessage(path, line, column, is_error, text)` carrying the whole report, so the raised `ShaderError` prints as `bad.wgsl:4:18: error: ...`. A report with no mark yields one message at line 0. The module returned by `compile_source` is not pooled; `Resources.adopt_shader` does that (`load` does both). `check_for_changes` stats every registered file and recompiles those whose mtime changed. Success updates `Resources.shaders` under the same `ShaderHandle`, releases the old module and logs `reloaded ` at info; failure keeps the last good module, increments `reload_failures` and logs `reload failed, keeping last good module: ` at error. The result list says which handles changed so pipeline owners can `build_render_pipeline` and `replace_render_pipeline` under the handles they already hold. wgpu also reports a failed compile as an uncaptured validation error, so `gfx.error_count` rises; tests that expect a failure drain `gfx.take_diagnostics()` afterwards. ## Pipelines, commands, readback The render test draws a triangle offscreen; abridged: ```mojo var module = shaders.compile_source(String("test_render.wgsl"), String(SHADER)) var shader = res.adopt_shader(module, String("test_render.wgsl")) var uniform_layout = create_bind_group_layout(res, String("tint uniform"), [LayoutEntry(0, UInt64(WGPUShaderStage_Vertex | WGPUShaderStage_Fragment), BindingKind.uniform_buffer(), False, 16)]) var render_layout = create_pipeline_layout(res, String("render"), [uniform_layout]) var uniform_group = create_bind_group(res, String("tint uniform"), uniform_layout, [BindingResource.for_buffer(0, tint, 0, 16)]) var pipeline = create_render_pipeline(res, RenderPipelineDesc( String("triangle"), render_layout, shader, String("vs_main"), String("fs_main"), [VertexLayout(8, [VertexAttribute(WGPUVertexFormat_Float32x2, 0, 0)])], [ColorTarget(WGPUTextureFormat_RGBA8Unorm, BlendMode.opaque())])) var encoder = CommandEncoder(_ptr(gfx), _ptr(res), String("frame")) var compute = encoder.begin_compute_pass() compute.set_pipeline(doubler) compute.set_bind_group(0, storage_group) compute.dispatch(1) compute.end() var pass_ = encoder.begin_render_pass( [ColorAttachment.cleared(res.texture_view(target_view).raw, 0.0, 0.0, 1.0, 1.0)], DepthAttachment.none()) pass_.set_pipeline(pipeline) pass_.set_bind_group(0, uniform_group) pass_.set_vertex_buffer(0, vertices) pass_.draw(3) pass_.end() encoder.copy_texture_to_buffer(target, readback, BYTES_PER_ROW, SIZE, SIZE) encoder^.finish_and_submit() wait_for_queue(gfx) var pixels = read_buffer(gfx, res, readback, BYTES_PER_ROW * SIZE) ``` `create_bind_group_layout` maps each `BindingKind` (uniform, storage, read-only storage, 2D / cube / array / depth / multisampled-depth / unfilterable textures, filtering / non-filtering / comparison samplers, write-only storage textures with `LayoutEntry.format`) onto the four wgpu binding layouts. `BindingResource.for_buffer / for_texture / for_sampler` build bind group entries; a buffer size of 0 means "to the end of the buffer". `RenderPipelineDesc` defaults to triangle lists, no culling, one sample, CCW front faces and `DepthState.none()`; `DepthState.reverse_z(format)` uses `GreaterEqual` and `reverse_z_biased` adds shadow-map bias. `wireframe=True` chains `WGPUPrimitiveStateExtras` and needs `gfx.wireframe_supported`. `build_render_pipeline` / `build_compute_pipeline` return the raw object for hot reload; `create_*` adopt it. Both builders call `_creation_error`, which pumps once; when the creation produced an uncaptured error they release the invalid object and raise `pipeline '

`, `#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.