# 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/<area>/test_<name>.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 <cond> 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).
