---
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/<scenario>-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-<step>.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/<pose>-<view>.sescene` | the scene posed from a simulation state |
| `captures/<pose>-<view>/` | `<scene_id>.png` and `<scene_id>.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=<id>` 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.
