Docs /Guides
Agent guide
How a coding agent sets up, drives and verifies SimEngine: the loop, the contract, scenarios, sessions and exit codes.
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.
- Discover:
simengine_host --capabilitiesprints 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. - 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. - Run headless:
simengine_host --run scenario.json --out RUN_DIRloads, steps and checks expectations, then writes the result and artifacts. - See:
simengine_capture --scene scene.sescene --out-dir CAPTURE_DIRrenders 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. - Prove: a change is done only when its expectations pass, the
final_state_sha256changed 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-missingdetects the environment, installs only missing recorded prerequisites (Ubuntu/Debian), configures and builds thelinux-releasepreset, 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
- Start from capabilities. Run
--capabilitiesbefore writing a scenario. Use only the commands, components and modules it lists; a disabled module’s commands do not exist and are rejected. - Keep your project outside the engine checkout. Never edit engine sources, tests or
examples/to get a pass. - Be explicit. Positions are
{ "value": [x, y, z], "unit": "m", "frame": "world" }. Frames are right-handed,+yup. Durations are nanoseconds ("unit": "ns"). - Validate before you run.
--load FILEvalidates all-or-nothing and prints the initial state;simengine_capture --check-scene FILEvalidates a scene without a GPU. - Use new or empty output directories. Existing content is never overwritten.
- Classify outcomes by exit code, then by
error.code. Do not parse stderr prose. Diagnostics on stderr are JSON Lines (simengine.diagnostic, version 1). - 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-msbound was reached. - Missing GPU is BLOCKED, not PASS. Exit codes 3 to 5 mean no usable Vulkan device. Report
BLOCKED_ENVIRONMENT; never fake a capture. - Report evidence. Finish with the commands run, exit codes,
final_state_sha256values, PNG paths and what SimEngine could not do.
Scenario format (simengine.scenario v1)
This is the scenario of the first_scene example, abridged:
{
"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 inputinitial_state.jsonandfinal_state.json(simengine.scene_state); the result carries the SHA-256 of the final statetrace.jsonlwhen--trace-every Nis setresult.json: the samesimengine.host.resultdocument 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 DESTcreates a project (and copies the three skills intoDEST/.claude/skills)python first_scene.py check --project P --build Bvalidates engine compatibility, scene and scenario without a GPUpython first_scene.py run --project P --build Bruns the full journey: load, run twice, capture, verify, manifestpython 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: create, check, run and verify a project
- simengine-modify-scene: add an object, move it, add an expectation, prove the change
- simengine-run-capture-diagnose: 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.
What not to do
- Do not invent commands, flags, components or modules. If
--capabilitiesdoes 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.