# SimEngine > SimEngine is a pre-release, general-purpose C++20 / Vulkan 1.3 game engine built for coding agents. Agents drive it from the command line: typed commands, seeded headless scenarios, offscreen captures and one JSON document per command. No LLM in the runtime. MIT OR Apache-2.0; the repository opens with the first distributable milestone. There is no hosted API, no API key and no MCP server: SimEngine is a set of local executables. Any agent that can run a shell command and read JSON can use it. You cannot ship a game with it yet (no supported physics, game loop, input or audio). ## Start here - [Agent guide](https://indiega.me/simengine/agent-guide.md): setup, the agent loop, implementation contract, scenario format, run artifacts, session protocol, skills and exit codes. - [Copy-ready agent prompt](https://indiega.me/simengine/agent-prompt.txt): set up the engine, create a first scene, change it and prove the change. - [CLI reference](https://indiega.me/simengine/cli.md): every command and option of simengine_host, simengine_capture, simengine_desktop and the example driver. - [Full context](https://indiega.me/simengine/llms-full.txt): all of the above in one document. ## Skills - [simengine-create-project](https://indiega.me/simengine/skills/simengine-create-project/SKILL.md) - [simengine-modify-scene](https://indiega.me/simengine/skills/simengine-modify-scene/SKILL.md) - [simengine-run-capture-diagnose](https://indiega.me/simengine/skills/simengine-run-capture-diagnose/SKILL.md) ## Examples - [first_scene scenario](https://indiega.me/simengine/examples/first_scene/scenario.json): the reference scenario (schema simengine.scenario v1). ## Pages - [SimEngine](https://indiega.me/simengine): spec sheet, architecture, scale, roadmap. - [AI game engine](https://indiega.me/ai-game-engine): what an AI game engine is, comparison with Unity, Unreal and Godot, FAQ. - [Docs](https://indiega.me/simengine/docs): the human-readable version of these files, one page per document (agent guide, CLI reference, skills). - [Why we built SimEngine](https://indiega.me/blog/why-we-built-simengine) --- # SimEngine agent guide > Use this guide when a coding agent (Claude Code, Codex, Cursor, Copilot, Gemini CLI or any agent with a shell) builds, runs or changes a SimEngine project. It is written for agents first and humans second. Last updated: 2026-10-29. Engine state: pre-release, M1 foundations done, M2 in progress. ## Read this first - **SimEngine is a local C++20 / Vulkan 1.3 game engine driven from the command line.** There is no hosted API, no API key, no account and no MCP server. Every tool is a local executable that prints one JSON document per command. - **The repository is not public yet.** It opens with the first distributable milestone (MIT OR Apache-2.0). Until then, this guide documents the interface so you can evaluate it; you can only run it with access to a checkout. - **You cannot ship a game with SimEngine today.** What works: seeded headless scenarios of point entities, offscreen PBR capture to PNG, a Windows scene playground and offline cooking of a strict static glTF/GLB subset. What does not exist yet: supported physics or collisions, a game loop, input for gameplay, audio, textured materials, shadows, terrain or orbital propagation. Do not promise or attempt these. - **The runtime never calls an LLM.** You drive development; the engine never waits for you. ## How you work with SimEngine The loop the engine is optimized for. Every step is a command you run and a document you parse. 1. **Discover**: `simengine_host --capabilities` prints the scenario schema, units, frames, the components and commands of exactly the compiled-in modules, limits, the determinism scope, every exit and error code, and an example scenario valid for this build. Read it instead of guessing from documentation. 2. **Author**: edit versioned text. A scenario (`simengine.scenario`, version 1) declares a seed, a tick schedule, entities, scheduled commands and expectations. Every position states its value, unit and frame. 3. **Run headless**: `simengine_host --run scenario.json --out RUN_DIR` loads, steps and checks expectations, then writes the result and artifacts. 4. **See**: `simengine_capture --scene scene.sescene --out-dir CAPTURE_DIR` renders offscreen through Vulkan and writes a PNG plus a manifest. A black, empty or background-only image is a failure (exit 12), never a pass. 5. **Prove**: a change is done only when its expectations pass, the `final_state_sha256` changed as intended and the capture shows what you meant. Compiling, surviving one tick or printing a success boolean is not acceptance. ## Setup From a SimEngine checkout, read `AGENTS.md` first, then use the maintained, rerunnable setup scripts that humans and CI also use: - **Linux**: `./tools/setup.sh --install-missing` detects the environment, installs only missing recorded prerequisites (Ubuntu/Debian), configures and builds the `linux-release` preset, runs the test suite and the host smoke check. - **Windows 11**: install Visual Studio 2022 (C++ x64), CMake 3.28+, Ninja and Python 3.12+, then run `tools/setup.ps1`. No Vulkan SDK is needed: the pinned Khronos headers are fetched and the loader is opened at run time. Expect zero failures among the selected CTest tests; report skips separately. The final smoke check prints one JSON summary naming the Vulkan device, or a structured `BLOCKED` outcome on a machine without a Vulkan 1.3 driver. Report failures verbatim with the exit code. Do not work around a failure silently. ## Implementation contract 1. **Start from capabilities.** Run `--capabilities` before writing a scenario. Use only the commands, components and modules it lists; a disabled module's commands do not exist and are rejected. 2. **Keep your project outside the engine checkout.** Never edit engine sources, tests or `examples/` to get a pass. 3. **Be explicit.** Positions are `{ "value": [x, y, z], "unit": "m", "frame": "world" }`. Frames are right-handed, `+y` up. Durations are nanoseconds (`"unit": "ns"`). 4. **Validate before you run.** `--load FILE` validates all-or-nothing and prints the initial state; `simengine_capture --check-scene FILE` validates a scene without a GPU. 5. **Use new or empty output directories.** Existing content is never overwritten. 6. **Classify outcomes by exit code, then by `error.code`.** Do not parse stderr prose. Diagnostics on stderr are JSON Lines (`simengine.diagnostic`, version 1). 7. **Do not retry blindly.** A failed expectation (exit 12) is a result, not a flake: read which expectation failed and why. Timeouts (exit 11) mean your `--max-wall-ms` bound was reached. 8. **Missing GPU is BLOCKED, not PASS.** Exit codes 3 to 5 mean no usable Vulkan device. Report `BLOCKED_ENVIRONMENT`; never fake a capture. 9. **Report evidence.** Finish with the commands run, exit codes, `final_state_sha256` values, PNG paths and what SimEngine could not do. ## Scenario format (simengine.scenario v1) This is the scenario of the `first_scene` example, abridged: ```json { "schema": "simengine.scenario", "schema_version": 1, "name": "first_scene", "seed": 20261027, "time": { "tick_duration": { "value": 16666667, "unit": "ns" }, "ticks": 120 }, "frame": { "id": "world", "kind": "cartesian", "handedness": "right", "up_axis": "+y" }, "entities": [ { "id": "table", "position": { "value": [0, 0, -1], "unit": "m", "frame": "world" } }, { "id": "ball", "position": { "value": [2, 0.25, 1.5], "unit": "m", "frame": "world" } } ], "generators": [ { "kind": "scatter_box", "id_prefix": "barrel_", "count": 3, "min": { "value": [1.5, 0, -3.5], "unit": "m", "frame": "world" }, "max": { "value": [3.5, 0, -2], "unit": "m", "frame": "world" } } ], "commands": [ { "at_tick": 60, "command": "simengine.entity.set_position", "version": 1, "args": { "entity": "ball", "position": { "value": [0.25, 1, -1], "unit": "m", "frame": "world" } } } ], "expect": [ { "name": "ball_on_table", "entity": "ball", "quantity": "position", "value": { "value": [0.25, 1, -1], "unit": "m", "frame": "world" }, "tolerance": { "value": 0, "unit": "m" } } ] } ``` The full file is at `/simengine/examples/first_scene/scenario.json`. Without the optional `point_mass` module, motion is a sequence of scheduled teleports; with it (`-DSIMENGINE_MODULES=point_mass`), entities gain `point_mass.body` (velocity) and the scene `point_mass.world` (gravity), with the commands `point_mass.apply_impulse` and `point_mass.set_velocity`. Always confirm with `--capabilities`. ## Run artifacts `simengine_host --run FILE --out DIR` writes into DIR: - `scenario.json`: a byte copy of the input - `initial_state.json` and `final_state.json` (`simengine.scene_state`); the result carries the SHA-256 of the final state - `trace.jsonl` when `--trace-every N` is set - `result.json`: the same `simengine.host.result` document printed to stdout Two runs of the same scenario on the same build and configuration give the same `final_state_sha256`. Cross-platform bitwise determinism is not promised. ## Interactive control: --session `simengine_host --session [--max-wall-ms MS]` reads JSON Lines requests on stdin and writes one `simengine.session.response` per request on stdout. Operations: `load`, `step`, `inspect`, `command` (with dry run and an expected state version), `expectations`, `capabilities` and `quit`. Use it when you need to step and inspect without re-running a whole scenario. ## The first_scene example and its skills `examples/first_scene` is the reference project. Its driver `first_scene.py` calls only the public tools, in separate processes, and verifies what they write: - `python first_scene.py new DEST` creates a project (and copies the three skills into `DEST/.claude/skills`) - `python first_scene.py check --project P --build B` validates engine compatibility, scene and scenario without a GPU - `python first_scene.py run --project P --build B` runs the full journey: load, run twice, capture, verify, manifest - `python first_scene.py verify [MANIFEST]` re-verifies a finished journey from its files Expected outcome for an unchanged project: `PASS`, 3 of 3 expectations, three PNG captures and a `manifest.json`. The driver stops with `incompatible_engine` (exit 21) if the engine reports interface versions the skills were not written for. Agent skills (load them; they are the procedures this guide summarizes): - [simengine-create-project](/simengine/skills/simengine-create-project/SKILL.md): create, check, run and verify a project - [simengine-modify-scene](/simengine/skills/simengine-modify-scene/SKILL.md): add an object, move it, add an expectation, prove the change - [simengine-run-capture-diagnose](/simengine/skills/simengine-run-capture-diagnose/SKILL.md): run, capture, and diagnose failures by exit code ## Exit codes `simengine_host` (also in the JSON `exit_code`; `error.code` names the failure): | Code | Name | Meaning | | --- | --- | --- | | 0 | ok | Success | | 1 | internal_error | Unexpected exception inside the host | | 2 | usage_error | Invalid command line | | 3 | vulkan_loader_unavailable | No Vulkan loader library on this machine | | 4 | vulkan_driver_unavailable | Loader present, no driver supporting Vulkan 1.3 | | 5 | no_vulkan_device | Instance created, zero physical devices | | 6 | no_suitable_device | Devices exist, none meets the requirements | | 7 | backend_initialization_failed | Other instance or device creation failure | | 8 | validation_errors | Backend opened, the validation layer reported errors | | 9 | runtime_self_check_failed | Backend fine, core runtime self-check failed | | 10 | scenario_invalid | Validation failed; nothing ran | | 11 | scenario_timeout | The wall-time bound stopped the run or session | | 12 | scenario_expectation_failed | Run completed, an expectation failed | | 13 | scenario_io_error | Input unreadable or output directory unusable | | 14 | simulation_fault | A tick produced non-finite state (rolled back) | | 15 | module_startup_failed | Optional modules could not be initialized | Codes 3 to 5 all mean "no usable Vulkan device". A process killed by a signal prints no document; your supervisor must classify it as a crash. Capture exit codes are in the [CLI reference](/simengine/cli.md). ## What not to do - Do not invent commands, flags, components or modules. If `--capabilities` does not list it, it does not exist in this build. - Do not edit engine code, tests or examples to make a check pass. - Do not treat "it compiled" or exit 0 from a partial step as done. - Do not fake or reuse a capture when the GPU is unavailable. - Do not add an LLM call, network call or API key to a game or to the runtime. - Do not claim physics, collisions, input, audio, textures or a game loop: they are not supported yet. ## Links - [llms.txt](/simengine/llms.txt) and [full context](/simengine/llms-full.txt) - [CLI reference](/simengine/cli.md) - [Copy-ready agent prompt](/simengine/agent-prompt.txt) - [What SimEngine is](https://indiega.me/simengine) and [why it is an AI game engine](https://indiega.me/ai-game-engine) --- # SimEngine CLI reference > The complete command-line interface of SimEngine's public tools, taken from their `--help` text. Every command except `--help` and `--session` prints exactly one JSON document to stdout, including on failure. Diagnostics go to stderr as JSON Lines (`simengine.diagnostic`, version 1). Last updated: 2026-10-29 (pre-release build `0.0.0-dev`). ## simengine_host The runtime host: build and device report, and the headless scenario runner. ```text simengine_host --report [--no-validation] [--device INDEX] simengine_host --version simengine_host --help simengine_host --capabilities simengine_host --load FILE simengine_host --run FILE --out DIR [--ticks N] [--max-wall-ms MS] [--trace-every N] simengine_host --session [--max-wall-ms MS] ``` | Command | What it does | | --- | --- | | `--report` | Opens the Vulkan backend headless (no window), prints one JSON report of build, settings, device and a core runtime self-check (clocks, jobs, handles, telemetry), then shuts down. `runtime.modules` lists the build-selected modules. | | `--version` | Prints build identity as JSON without touching Vulkan. | | `--capabilities` | Prints the machine-readable capability document: scenario schema, units, frames, components, commands, limits, exit codes and an example scenario valid for this build's modules. No Vulkan. | | `--load FILE` | Validates a scenario (all-or-nothing) and prints its initial state. No Vulkan. | | `--run FILE --out DIR` | Loads, steps and checks expectations. DIR must be new or empty. `--ticks` overrides the scenario's run length, `--max-wall-ms` bounds the run (default 30000), `--trace-every N` records a state digest every N ticks. | | `--session` | JSON Lines control on stdin/stdout: `load`, `step`, `inspect`, `command` (dry run, expected state version), `expectations`, `capabilities`, `quit`. Bounded by `--max-wall-ms`. | Options for `--report`: `--no-validation` (do not enable `VK_LAYER_KHRONOS_validation`), `--device INDEX` (physical device by enumeration index; default selection is discrete, then integrated, virtual, CPU), `--inject-fault invalid-queue-priority` (negative control: with validation enabled the run must end with exit 8). Environment: `SIMENGINE_VULKAN_LOADER` sets an explicit Vulkan loader path (diagnostic override). ### Exit codes | Code | Name | Meaning | | --- | --- | --- | | 0 | ok | Success | | 1 | internal_error | Unexpected exception inside the host | | 2 | usage_error | Invalid command line | | 3 | vulkan_loader_unavailable | No Vulkan loader library on this machine | | 4 | vulkan_driver_unavailable | Loader present, no driver supporting Vulkan 1.3 | | 5 | no_vulkan_device | Instance created, zero physical devices | | 6 | no_suitable_device | Devices exist, none meets the requirements | | 7 | backend_initialization_failed | Other instance or device creation failure | | 8 | validation_errors | Backend opened, the validation layer reported errors | | 9 | runtime_self_check_failed | Backend fine, core runtime self-check failed | | 10 | scenario_invalid | Validation failed; nothing ran | | 11 | scenario_timeout | The wall-time bound stopped the run or session | | 12 | scenario_expectation_failed | Run completed, an expectation failed | | 13 | scenario_io_error | Input unreadable or output directory unusable | | 14 | simulation_fault | A tick produced non-finite state (rolled back) | | 15 | module_startup_failed | Optional modules could not be initialized | A process killed by a signal (crash) prints no document; supervisors classify it. ## simengine_capture Offscreen render and capture with the forward PBR path. ```text simengine_capture --scene FILE --out-dir DIR [options] simengine_capture --check-scene FILE simengine_capture --version simengine_capture --help ``` | Command | What it does | | --- | --- | | `--scene FILE` | Loads a `.sescene` file and its `.seasset` files, renders offscreen, writes `DIR/.png` and `DIR/.capture.json`, prints the manifest (`simengine.capture.manifest`, version 1). | | `--check-scene FILE` | Validates the scene and its assets only (units, axes, PBR subset, provenance, geometry). No Vulkan. Prints the manifest. | Options for `--scene`: `--out-dir DIR` (required, created if missing), `--width N --height N` (default 2560 x 1440), `--warmup N` (timed warm-up frames excluded from steady statistics, default 3), `--frames N` (measured steady frames, default 30), `--label TEXT` (free-form machine label recorded in the manifest, not verified), `--no-validation`, `--no-sync-validation`, `--device INDEX`, `--inject-fault skip-layout-barrier|black-output` (negative controls: exit 8 and exit 12 respectively). ### Exit codes | Code | Name | Meaning | | --- | --- | --- | | 0 | ok | Success | | 1 | internal_error | Unexpected exception | | 2 | usage_error | Invalid command line | | 3 | vulkan_loader_unavailable | No Vulkan loader library on this machine | | 4 | vulkan_driver_unavailable | Loader present, no driver supporting Vulkan 1.3 | | 5 | no_vulkan_device | Instance created, zero physical devices | | 6 | no_suitable_device | Devices exist, none meets the requirements | | 7 | backend_initialization_failed | Other instance or device creation failure | | 8 | validation_errors | Rendered, but the validation layer reported errors | | 10 | scene_invalid | Scene or asset rejected by the import path | | 11 | render_failed | A Vulkan call failed or the device lacks a format | | 12 | capture_content_invalid | The image is black, empty or background-only | | 13 | output_write_failed | The PNG or manifest could not be written | ## simengine_desktop (Windows only) A scene playground using the same persistent renderer: explore a generic PBR scene, select and move objects, save snapshots that `simengine_capture` reproduces. It is not a game-authoring API. ```text simengine_desktop --scene FILE [--out DIR] [--width N] [--height N] ``` `--out` must be new or empty; omitting it disables saved captures. `--width`/`--height` set the internal render resolution (default 1280 x 720). Requires Vulkan 1.3, one graphics/present queue, SRGB blit support and `VK_EXT_swapchain_maintenance1`; failures are explicit JSON. Controls: W/A/S/D and Q/E move the camera, right-drag looks, Tab selects an object, arrows and Page Up/Down move it in metres, Space pauses, R resets, F12 saves a PNG and a replayable `.sescene`, Escape exits. There is no collision response. ## first_scene.py (example driver) ```text python first_scene.py new DEST [--name NAME] [--no-skills] python first_scene.py check [--project P] [--build B | --host H --capture C] [--scenario S] python first_scene.py run [--project P] [--build B | --host H --capture C] [--scenario S] [--out DIR] [--timeout SEC] [--label TEXT] [--skip-capture] python first_scene.py verify [MANIFEST] [--project P] [--scenario S] [--expect-outcome PASS|FAIL|BLOCKED_ENVIRONMENT] ``` `run` executes the full journey (load, run twice, pose, capture, verify, manifest). With `--skip-capture` the render check is `NOT_RUN`, never `PASS`. The driver exits 21 (`incompatible_engine`) when the engine's interface versions differ from the ones its skills were written for. --- # Copy-ready agent prompt ```text Set up SimEngine and build my first verified scene with it. Read https://indiega.me/simengine/llms.txt and https://indiega.me/simengine/agent-guide.md first, then AGENTS.md in the SimEngine checkout. SimEngine is a local C++20 / Vulkan 1.3 engine driven from the command line: there is no hosted API, no API key and no MCP server. Every tool prints one JSON document per command; classify results by exit code, then by error.code. 1. Setup. On Linux run ./tools/setup.sh --install-missing; on Windows 11 (Visual Studio 2022 C++ x64, CMake 3.28+, Ninja, Python 3.12+) run tools/setup.ps1. Expect zero failures among the selected CTest tests and report skips separately. Show me the smoke summary JSON naming my Vulkan device, or the structured BLOCKED outcome. 2. Discover. Run simengine_host --capabilities and tell me the scenario schema version, the commands, components and modules this build has. Use nothing that is not listed. 3. Create. Follow the simengine-create-project skill (https://indiega.me/simengine/skills/simengine-create-project/SKILL.md): create my project OUTSIDE the engine checkout, check it, run it and verify it. Expect PASS, 3 of 3 expectations, three PNG captures and a manifest.json. 4. Change. Follow the simengine-modify-scene skill: add one object, move it with a scheduled command, add an expectation for where it ends, then check, run and verify again. Prove the change: the new final_state_sha256 differs, the new expectation passed and the new capture shows the object. 5. On failure, follow simengine-run-capture-diagnose: report the exit code, failure.code, failure.step and failure.message verbatim, fix only my project files, and say plainly if the render is BLOCKED_ENVIRONMENT (no Vulkan device). Never edit engine sources, tests or examples to get a pass, and never fake a capture. SimEngine is pre-release: there is no supported physics, collisions, gameplay input, audio, textures or game loop yet. Do not promise or attempt them. Finish with a short report: commands run, exit codes, digests, PNG paths, and what SimEngine could not do. ``` --- # Skill: simengine-create-project --- name: simengine-create-project description: Create a new SimEngine project (a scene, a scenario and a project.json) from the first_scene example, check it against the engine build, run it headless, capture it offscreen to PNG and verify the result. Use when the user asks to start, create or scaffold a SimEngine scene, project or "first game", or to check that their SimEngine build can run one. --- # Create a SimEngine project This skill ships with the SimEngine `first_scene` example. It drives only SimEngine's public tools: the `simengine_host` scenario CLI, the `simengine_capture` offscreen renderer and the example driver `examples/first_scene/first_scene.py`, which calls those two executables in separate processes and verifies what they write. ## Compatible versions Written for, and verified against, a SimEngine build that reports: | Interface | Version | Where it is reported | | --- | --- | --- | | Scenario schema `simengine.scenario` | `schema_version` 1 | `simengine_host --capabilities` → `capabilities.scenario_schema.schema_version` | | Capability document `simengine.capabilities` | 1 | `capabilities.schema_version` | | Scene state `simengine.scene_state` | 1 | `capabilities.state_schema.schema_version` | | Simulation (module extension) contract | 1 | `capabilities.simulation_contract_version` | | Module contract | 1 | `simengine_host --report` → `runtime.modules.registry.contract_version` | | Scene / asset text formats | `simengine_scene 1`, `simengine_asset 1` | first line of `.sescene` / `.seasset` files | | Capture manifest `simengine.capture.manifest` | 1 | `simengine_capture --version` | | Example driver / project file | driver 1, `simengine.example_project` 1 | `first_scene.py`, `project.json` | The driver checks every one of these before it runs anything and stops with `incompatible_engine` (exit 21) on a mismatch. Do not work around that: the skill is out of date for that engine. ## What SimEngine can do today (and what it cannot) Can: run a deterministic, seeded, headless scenario of point entities in one Cartesian frame (positions in meters, yaw orientations, scheduled `simengine.entity.set_position` commands, seeded `scatter_box` placement, end-of-run expectations), and render a static scene offscreen with a small PBR material subset to a PNG plus a JSON manifest. Cannot (do not promise or attempt it): a window or interactive camera, keyboard/mouse input, physics, collisions or contacts, audio, a game loop, textures or normal maps, glTF import, local lights or shadows. Motion without the optional `point_mass` module is a sequence of scheduled teleports. ## Before you start You need a built SimEngine checkout. If `build/linux-release/apps/host/simengine_host` does not exist yet, run the setup script first (`./tools/setup.sh` on Linux, `tools/setup.ps1` on Windows) and report its result. The commands below use three variables. Shell tools often do not keep variables between calls: prefix each command with the assignments, or substitute absolute paths. ```text SIMENGINE_ROOT=/absolute/path/to/SimEngine # the checkout SIMENGINE_BUILD=$SIMENGINE_ROOT/build/linux-release # what tools/setup.sh builds PROJECT=/absolute/path/to/my_first_scene # a NEW directory, outside the checkout ``` Use `python` instead of `python3` where that is the Python 3 interpreter (Windows). ## Steps 1. Discover what this build can do. Read `capabilities.commands`, `capabilities.components` and `capabilities.modules`: a core-only build has one command (`simengine.entity.set_position`) and no components or modules. ```sh "$SIMENGINE_BUILD/apps/host/simengine_host" --capabilities > "$SIMENGINE_BUILD/capabilities.json" python3 -c "import json,sys; c=json.load(open(sys.argv[1]))['capabilities']; print('scenario schema', c['scenario_schema']['schema_version'], '| commands', [x['id'] for x in c['commands']], '| modules', [m['id'] for m in c['modules']])" "$SIMENGINE_BUILD/capabilities.json" ``` 2. Create the project. `new` copies the example scene, scenarios, assets and these skills (into `$PROJECT/.claude/skills/`) and prints one JSON line (`outcome`, `files`, `next`). ```sh python3 "$SIMENGINE_ROOT/examples/first_scene/first_scene.py" new "$PROJECT" ``` 3. Check it without a GPU: engine compatibility, scene import path, scenario validation. Expect `"outcome": "PASS"` and exit code 0. ```sh python3 "$SIMENGINE_ROOT/examples/first_scene/first_scene.py" check --project "$PROJECT" --build "$SIMENGINE_BUILD" ``` 4. Run the whole journey: two deterministic runs, the scene posed from the initial and final simulation states, three offscreen captures (two viewpoints), verification and `manifest.json` under `$PROJECT/runs/main-NNN/`. ```sh python3 "$SIMENGINE_ROOT/examples/first_scene/first_scene.py" run --project "$PROJECT" --build "$SIMENGINE_BUILD" ``` 5. Verify it independently from the files it wrote (hashes, PNG decode, sidecars). ```sh python3 "$SIMENGINE_ROOT/examples/first_scene/first_scene.py" verify --project "$PROJECT" ``` ## Verify what you produced Report these to the user, from the `run` and `verify` JSON lines, not from memory: - `outcome` is `PASS` and the exit code is 0 for `check`, `run` and `verify`; - `expectations`: 3 passed, 0 failed; `final_state_sha256` (64 hex characters); - `checks.deterministic_repeat` and `checks.motion_visible` are `PASS`; - three PNGs under `runs/main-NNN/captures/` (`initial-front`, `final-front`, `final-side`). Open `final-front` and look at it: a table with a red ball on it, two stacked crates, three barrels on a checkered ground. If the exit code is 77 (`BLOCKED_ENVIRONMENT`), the machine has no usable Vulkan device: the simulation was verified but nothing was rendered. Say so; it is not a pass. ## On failure The driver prints `failure.code`, `failure.step`, `failure.message` and a `failure.hint`. Quote them verbatim. Then: | Exit | `failure.code` | Do this | | --- | --- | --- | | 20 | `build_not_found` | Run the setup script, or pass the build directory that contains `apps/host` and `apps/capture`. | | 21 | `incompatible_engine` | Stop. Host and capture come from different builds, or the engine changed a version this skill was written for. Report it. | | 22 | `module_missing` | The scenario needs an optional module. Use a build configured with `-DSIMENGINE_MODULES=` or pick a core-only scenario. | | 23 | `scenario_invalid` | Use `simengine-modify-scene`: fix each `failure.detail.errors[]` entry at its JSON pointer `path`. | | 24 | `scene_invalid` | Fix the `.sescene`/`.seasset` line in `failure.detail` (`code`, `file`, `line`). | | 31 | `project_invalid` | `$PROJECT` already exists and is not empty, or `project.json` is malformed. Choose a new directory. | | other | see `simengine-run-capture-diagnose` | | Never edit engine sources, tests or the example's files under `$SIMENGINE_ROOT` to make a journey pass; change only the project. --- # Skill: simengine-modify-scene --- name: simengine-modify-scene description: Modify a SimEngine project made from the first_scene example — add, move or restyle objects in the scene, change the scripted motion (scheduled commands), seeds or expectations in the scenario, or switch to the point_mass behavior variant — then re-run, re-capture and prove the change with digests, expectations and images. Use when the user asks to add an entity or object, move something, animate it, change materials or the camera, or change a SimEngine scenario's behavior. --- # Modify a scene or behavior This skill ships with the SimEngine `first_scene` example and drives only public tools: `simengine_host` (scenario CLI), `simengine_capture` (offscreen render) and `examples/first_scene/first_scene.py` (the example driver). It assumes a project created with `simengine-create-project`. ## Compatible versions Scenario schema `simengine.scenario` **schema_version 1**; capability document 1; scene state 1; simulation contract 1; module contract 1; scene/asset text formats `simengine_scene 1` / `simengine_asset 1`; capture manifest 1; example driver 1 and `simengine.example_project` 1. The driver checks all of them (`incompatible_engine`, exit 21, on a mismatch). The authoritative field list is `simengine_host --capabilities` → `capabilities.scenario_schema`; the scene and asset formats are documented in `fixtures/README.md` of the SimEngine checkout. ## How a project fits together | File | Owns | Notes | | --- | --- | --- | | `scenario.json` | simulation: entities, seed, ticks, scheduled commands, expectations | validated all-or-nothing by `simengine_host --load` | | `scene.sescene` | presentation: which asset each object uses, material, scale, camera, sun | validated by `simengine_capture --check-scene` | | `project.json` | which scene and scenarios, viewpoints, capture size | read by the driver only | | `assets/*.seasset` | meshes and materials (text, meters, +Y up) | each file records its license and tool | The rule that binds them: **every scenario entity needs a scene instance with exactly the same name.** When the driver poses the scene, each such instance takes the entity's position and yaw from the simulation state; instances without an entity (the ground) stay where they are authored. Generated entities are named `id_prefix` + an index zero-padded to the width of `count - 1` (`barrel_0` … `barrel_2` for count 3, `crate_00` … for count 11 or more). Units and frame: meters, one Cartesian frame, right-handed, `up_axis "+y"` (the scene convention; the driver refuses any other). Orientations are quaternions `[x, y, z, w]` that must be pure yaw (rotation about +Y): `[0, sin(a/2), 0, cos(a/2)]`. Placement reference (asset origins): `crate` is a 0.5 m cube with its origin at the center of its base (on the ground: y = 0; on another crate: y = 0.5); `spheres` (mesh `sphere_r0p25`) has its origin at the center (on the ground: y = 0.25; on the table: y = 1.0); `barrel` (r 0.3 m, h 0.8 m) and `table` (top at y = 0.75 m, 1.2 m × 0.8 m) have their origin on the floor. Sphere materials: `dielectric_rough`, `dielectric_smooth`, `metal_rough`, `metal_smooth`. Motion in a core-only build is `simengine.entity.set_position` at a tick (a teleport, applied at the start of that tick, before systems, in document order). There is no velocity, interpolation, collision or rotation command. Expectations compare the state at the end of the run, component-wise, within `tolerance`. ## Variables ```text SIMENGINE_ROOT=/absolute/path/to/SimEngine SIMENGINE_BUILD=$SIMENGINE_ROOT/build/linux-release PROJECT=/absolute/path/to/my_first_scene ``` Prefix each command with them if your shell tool does not keep variables between calls. ## Steps 1. Record the baseline. Run it even if an earlier `run` already passed: step 5 compares against `runs/before.json`, which only this command writes (a `check` result has no digest or captures and is not a baseline). ```sh mkdir -p "$PROJECT/runs" python3 "$SIMENGINE_ROOT/examples/first_scene/first_scene.py" run --project "$PROJECT" --build "$SIMENGINE_BUILD" > "$PROJECT/runs/before.json" ``` 2. Make the change in both files. Worked example — add a third crate that starts on the ground and is stacked on top of the other two at tick 100: - `scenario.json`, append to `entities`: `{"id": "crate_c", "position": {"value": [-1.2, 0, 0.9], "unit": "m", "frame": "world"}}` - append to `commands`: `{"at_tick": 100, "command": "simengine.entity.set_position", "version": 1, "args": {"entity": "crate_c", "position": {"value": [-2, 1, -0.5], "unit": "m", "frame": "world"}}}` - append to `expect`: `{"name": "crate_c_on_top", "entity": "crate_c", "quantity": "position", "value": {"value": [-2, 1, -0.5], "unit": "m", "frame": "world"}, "tolerance": {"value": 0, "unit": "m"}}` - `scene.sescene`, after the other crates: `instance crate_c crate * position -1.2 0 0.9` Make these edits with your editor. The block below applies exactly the same edit (the example's automated test runs it): ```sh python3 - "$PROJECT" <<'EOF' import json, pathlib, sys project = pathlib.Path(sys.argv[1]) scenario_path = project / "scenario.json" scenario = json.loads(scenario_path.read_text(encoding="utf-8")) pos = lambda v: {"value": v, "unit": "m", "frame": "world"} scenario["entities"].append({"id": "crate_c", "position": pos([-1.2, 0, 0.9])}) scenario["commands"].append({"at_tick": 100, "command": "simengine.entity.set_position", "version": 1, "args": {"entity": "crate_c", "position": pos([-2, 1, -0.5])}}) scenario["expect"].append({"name": "crate_c_on_top", "entity": "crate_c", "quantity": "position", "value": pos([-2, 1, -0.5]), "tolerance": {"value": 0, "unit": "m"}}) scenario_path.write_text(json.dumps(scenario, indent=2) + "\n", encoding="utf-8") scene_path = project / "scene.sescene" lines = scene_path.read_text(encoding="utf-8").splitlines() at = max(i for i, line in enumerate(lines) if line.startswith("instance crate_")) lines.insert(at + 1, "instance crate_c crate * position -1.2 0 0.9") scene_path.write_text("\n".join(lines) + "\n", encoding="utf-8") EOF ``` 3. Check (no GPU). A mistake shows up here as `scenario_invalid` (23) with a JSON pointer per error, or `scene_invalid` (24) with file and line. Fix and repeat. ```sh python3 "$SIMENGINE_ROOT/examples/first_scene/first_scene.py" check --project "$PROJECT" --build "$SIMENGINE_BUILD" ``` 4. Run, capture and verify the changed project. ```sh python3 "$SIMENGINE_ROOT/examples/first_scene/first_scene.py" run --project "$PROJECT" --build "$SIMENGINE_BUILD" > "$PROJECT/runs/after.json" python3 "$SIMENGINE_ROOT/examples/first_scene/first_scene.py" verify --project "$PROJECT" ``` 5. Prove the change: the state digest changed, the new expectation is evaluated and passed, and the images exist. Then open the new `final-front` PNG and look at it. ```sh python3 - "$PROJECT/runs/before.json" "$PROJECT/runs/after.json" <<'EOF' import json, sys before, after = (json.load(open(p, encoding="utf-8")) for p in sys.argv[1:3]) assert before["outcome"] == "PASS" and after["outcome"] == "PASS", (before["failure"], after["failure"]) assert after["final_state_sha256"] != before["final_state_sha256"], "the simulation state did not change" assert after["expectations"]["failed"] == 0 and after["expectations"]["passed"] == before["expectations"]["passed"] + 1 print("changed:", before["final_state_sha256"][:12], "->", after["final_state_sha256"][:12], "| expectations", after["expectations"], "| images", [c["png"] for c in after["captures"]]) EOF ``` A presentation-only change (a material, the camera, `project.json` viewpoints) leaves `final_state_sha256` unchanged and changes the captured pixels (`pixels_sha256`); check that instead. ## Behavior with velocity and gravity: the `point_mass` variant `toss.scenario.json` tosses the ball under gravity with an impulse, using the optional `point_mass` test module (point kinematics, semi-implicit Euler; no collisions, so a body falls through the ground if you run long enough). It needs a build that compiles the module in; a core-only build stops with `module_missing` (22): ```text cmake --preset linux-release -B "$SIMENGINE_ROOT/build/linux-release-point_mass" -DSIMENGINE_MODULES=point_mass cmake --build "$SIMENGINE_ROOT/build/linux-release-point_mass" SIMENGINE_BUILD_POINT_MASS=$SIMENGINE_ROOT/build/linux-release-point_mass ``` ```sh point_mass python3 "$SIMENGINE_ROOT/examples/first_scene/first_scene.py" run --project "$PROJECT" --build "$SIMENGINE_BUILD_POINT_MASS" --scenario toss python3 "$SIMENGINE_ROOT/examples/first_scene/first_scene.py" verify --project "$PROJECT" --scenario toss ``` Its components and commands (`point_mass.body`, `point_mass.world`, `point_mass.apply_impulse`, `point_mass.set_velocity`) are listed by `--capabilities` of that build only. Expected values for its expectations follow the closed form of semi-implicit Euler: after n ticks of duration dt, v = v0 + n·g·dt and x = x0 + n·v0·dt + g·dt²·n(n+1)/2 (plus impulse/mass from the tick it is applied). ## On failure | Exit | `failure.code` | Do this | | --- | --- | --- | | 23 | `scenario_invalid` | For each `failure.detail.errors[]`: open the scenario at `line`/`column`, fix the field at `path` (`code` says what: `unit_mismatch`, `unknown_entity`, `unknown_command`, `unknown_component`, `non_normalized_orientation`, …; `expected`/`found` give the values). Nothing ran. | | 24 | `scene_invalid` | Fix the line in `failure.detail` (`code`, `file`, `line`): e.g. `unknown_mesh`, `syntax_error`, `invalid_scene`. | | 25 | `pose_mapping_failed` | Add the missing scene instances listed in `failure.detail.entities_without_instance` (same names), or fix a non-yaw orientation / non-`+y` frame. | | 27 | `expectation_failed` | Compare `failure.detail.failed[].expected` and `.actual`; fix whichever is wrong (the command or the expectation), never widen a tolerance just to pass. | | 22 | `module_missing` | Use the point_mass build or a core-only scenario. | | other | see `simengine-run-capture-diagnose` | | Change only the project's files. Never edit SimEngine sources, the example under `$SIMENGINE_ROOT/examples/`, or a tolerance to make a run pass. --- # Skill: simengine-run-capture-diagnose --- name: simengine-run-capture-diagnose description: Run a SimEngine project headless, capture it offscreen, read the structured results, and diagnose any failure from its documented exit code, error code and JSON pointer or file/line — using only the public simengine_host and simengine_capture tools, the run supervisor and the first_scene example driver. Use when the user asks to run, render, capture, screenshot, test or debug a SimEngine scene or scenario, or when a SimEngine command failed or exited non-zero. --- # Run, capture and diagnose This skill ships with the SimEngine `first_scene` example. It uses only public tools: `simengine_host` (headless scenario CLI), `simengine_capture` (offscreen render and PNG), `tools/run_scenario.py` (out-of-process run supervisor) and `examples/first_scene/first_scene.py` (the example driver that chains them and verifies what they write). ## Compatible versions Scenario schema `simengine.scenario` **schema_version 1**, capabilities 1, scene state 1, host result `simengine.host.result` 1, simulation contract 1, module contract 1, capture manifest `simengine.capture.manifest` 1, scene/asset formats 1, example driver 1 and `simengine.example_manifest` 1. Exit codes below are the ones those versions document (`simengine_host --help`, `simengine_capture --help`, `first_scene.py --help`). ## Variables ```text SIMENGINE_ROOT=/absolute/path/to/SimEngine SIMENGINE_BUILD=$SIMENGINE_ROOT/build/linux-release PROJECT=/absolute/path/to/my_first_scene ``` Prefix each command with them if your shell tool does not keep variables between calls. ## Run The full journey (two runs, posing, three captures, verification), then an independent re-verification from the files: ```sh python3 "$SIMENGINE_ROOT/examples/first_scene/first_scene.py" run --project "$PROJECT" --build "$SIMENGINE_BUILD" python3 "$SIMENGINE_ROOT/examples/first_scene/first_scene.py" verify --project "$PROJECT" ``` Simulation only (no GPU needed; the render check is reported `NOT_RUN`, never passed, and a later `verify` of that run reports `"render": "NOT_RUN"`): ```sh python3 "$SIMENGINE_ROOT/examples/first_scene/first_scene.py" run --project "$PROJECT" --build "$SIMENGINE_BUILD" --skip-capture ``` Each `run` prints one JSON line (`simengine.example_result`: `outcome`, `exit_code`, `failure`, `final_state_sha256`, `expectations`, `captures`, `checks`) and writes `$PROJECT/runs/-NNN/`: | Path | Content | | --- | --- | | `manifest.json` | The build/run/capture manifest: build identity and CMake settings, dependency graph, enabled and disabled modules with absence checks, every step (argv, exit code, wall time, peak RSS where the OS reports it), simulation digests and expectations, posing, captures, checks, warnings, `not_covered` | | `logs/NN-.stdout` / `.stderr` | each engine process's JSON result and its JSON Lines diagnostics | | `run-1/`, `run-2/` | `simengine_host --run` artifacts: `scenario.json`, `initial_state.json`, `final_state.json`, `trace.jsonl`, `result.json` | | `scenes/-.sescene` | the scene posed from a simulation state | | `captures/-/` | `.png` and `.capture.json` (device, timings, validation, content check) | Look at the PNGs yourself; a passing content check only means the image is not black or empty. ## Use the engine tools directly When you need more detail than the driver gives, call the same tools it calls. Each prints one JSON document on stdout (also on failure) and JSON Lines diagnostics on stderr. ```sh "$SIMENGINE_BUILD/apps/host/simengine_host" --load "$PROJECT/scenario.json" > "$PROJECT/runs/load.json" "$SIMENGINE_BUILD/apps/capture/simengine_capture" --check-scene "$PROJECT/scene.sescene" > "$PROJECT/runs/check-scene.json" python3 "$SIMENGINE_ROOT/tools/run_scenario.py" --host "$SIMENGINE_BUILD/apps/host/simengine_host" --scenario "$PROJECT/scenario.json" --out "$PROJECT/runs/supervised-$(date +%Y%m%d%H%M%S)" --timeout 60 ``` `run_scenario.py` classifies the outcome (`completed`, `invalid`, `timeout`, `expectation_failed`, `fault`, `crash`, `supervisor_timeout`, …) even when the host crashes. `simengine_host --run FILE --out NEW_DIR --trace-every 1` records a state digest per tick to find the first tick where two runs diverge. `simengine_host --report` shows the Vulkan device and the module registry. ## Diagnose: exit codes and what to do `first_scene.py` (`failure.code`; `failure.hint` repeats the action): | Exit | Code | Meaning → action | | --- | --- | --- | | 0 | — | Everything passed. Report digests, expectations and PNG paths. | | 1 | `internal_error` | Driver defect → keep the run directory, report it with the message. | | 2 | `usage_error` | Fix the command line (`first_scene.py --help`). | | 20 | `build_not_found` | Build first (`tools/setup.sh` / `tools/setup.ps1`), or fix `--build`. | | 21 | `incompatible_engine` | Different builds for host and capture, or a version this skill does not know → stop and report. | | 22 | `module_missing` | Scenario needs an optional module → build with `-DSIMENGINE_MODULES=` or use a core-only scenario. | | 23 | `scenario_invalid` | `failure.detail.errors[]`: fix each at its JSON pointer `path` (`line`, `column`, `expected`, `found`). Nothing ran. | | 24 | `scene_invalid` | Fix the scene/asset line in `failure.detail` (`code`, `file`, `line`). | | 25 | `pose_mapping_failed` | Entities without a same-named scene instance, a non-`+y` frame or a non-yaw orientation → fix the project. | | 26 | `run_failed` | `failure.detail.host_exit_code`: 11 timeout (fewer ticks or a larger `--timeout`), 13 I/O, 14 `simulation_fault` (non-finite state; rolled back), 15 module startup, negative = crash → report crashes. | | 27 | `expectation_failed` | Compare `failure.detail.failed[].expected` with `.actual`; fix the command or the expectation, never the tolerance alone. | | 28 | `nondeterministic` | Same build, same input, different digests: an engine defect → keep both run directories and report. | | 29 | `capture_failed` | `failure.detail.error.code`: `validation_errors` (8) or `render_failed` (11) → engine defect, report; `capture_content_invalid` (12) → the camera sees nothing, fix camera/target. | | 30 | `capture_check_failed` | PNG or manifest does not verify independently → report; do not regenerate silently. | | 31 | `project_invalid` | Malformed `project.json` or a non-empty `--out` directory. | | 77 | `blocked_environment` | No usable Vulkan 1.3 device (capture exit 3–6). Simulation verified; rendering `BLOCKED_ENVIRONMENT`, not passed. Install a GPU driver or Mesa lavapipe. | `simengine_host` exit codes (in `logs/*.stdout`, `error.code`): 2 `usage_error`, 10 `scenario_invalid`, 11 `scenario_timeout`, 12 `scenario_expectation_failed`, 13 `scenario_io_error` (input unreadable, or `--out` not new/empty), 14 `simulation_fault`, 15 `module_startup_failed`; 3–9 concern `--report` only. `simengine_capture` exit codes: 3–6 no usable Vulkan device, 7 `backend_initialization_failed`, 8 `validation_errors`, 10 `scene_invalid`, 11 `render_failed`, 12 `capture_content_invalid`, 13 `output_write_failed`. ## Diagnosis drill Practise the loop on a new, uniquely named copy: break one unit and see the located error. Keep that copy and its diagnostics for inspection; existing projects are preserved. ```sh DRILL=$(python3 - "$PROJECT" <<'EOF' import pathlib, shutil, sys, tempfile project = pathlib.Path(sys.argv[1]).resolve() drill = pathlib.Path(tempfile.mkdtemp(prefix=project.name + ".drill-", dir=project.parent)) shutil.copytree(project, drill, dirs_exist_ok=True, ignore=shutil.ignore_patterns("runs", ".git")) print(drill) EOF ) python3 - "$DRILL/scenario.json" <<'EOF' import json, sys path = sys.argv[1] scenario = json.load(open(path, encoding="utf-8")) scenario["entities"][0]["position"]["unit"] = "cm" # the schema accepts meters only json.dump(scenario, open(path, "w", encoding="utf-8"), indent=2) EOF set +e python3 "$SIMENGINE_ROOT/examples/first_scene/first_scene.py" check --project "$DRILL" --build "$SIMENGINE_BUILD" > "$DRILL/check.json" status=$? set -e python3 - "$DRILL/check.json" "$status" <<'EOF' import json, sys result = json.load(open(sys.argv[1], encoding="utf-8")) error = result["failure"]["detail"]["errors"][0] assert int(sys.argv[2]) == 23 and result["failure"]["code"] == "scenario_invalid", result["failure"] assert error["code"] == "unit_mismatch" and error["path"] == "/entities/0/position/unit", error print("diagnosed:", error["code"], "at", error["path"], "line", error["line"], "expected", error["expected"], "found", error["found"]) EOF printf 'Diagnosis files retained in %s\n' "$DRILL" ``` Report failures verbatim (code, step, message, the engine's own error object). Change only the project; never edit SimEngine sources, tests or tolerances to get a pass, and never describe a `BLOCKED_ENVIRONMENT` or `NOT_RUN` render as passed.