| Header | Link |
|---|---|
| Purpose | Purpose |
| Use Cases | Use Cases |
| End-to-End Example | End-to-End Example |
| Quick Map | Quick Map |
| Project Placement | Project Placement |
| Build And Run | Build And Run |
| SteamPipe Upload | SteamPipe Upload |
| Capture & Offline Render | Capture & Offline Render |
| New Projects And Templates | New Projects And Templates |
| Health And Maintenance | Health And Maintenance |
| Profiling | Profiling |
| Install | Install |
Run perro steampipe for main or perro steampipe --demo for demo.
Use --path <project_dir> or run within the project tree.
[steam]
enabled = true
app_id = 4999210
demo_id = 5261350
input = "fallback"
[steam.main.depots]
4999212 = "windows"
[steam.demo.depots]
5261351 = "windows"
[demo]
exclude = ["res://game/endless.rs"]Set credentials in the shell or project-root .env (shell values take precedence):
STEAMWORKS_USER=your_build_account
STEAMWORKS_PASSWORD='your_password'Keep .env out of source control. Missing or blank credentials halt before upload.
Demo uploads require an explicit demo_id and demo depots; no main fallback.
The command uploads existing bundles only. It never runs perro build; build
each target first. Missing bundle paths stop the upload before SteamCMD starts.
OS mappings use x64 targets: x86_64-pc-windows-msvc, x86_64-apple-darwin,
and x86_64-unknown-linux-gnu.
Upload roots use the compiler's native bundle path rules under .output/ or
.output/demo/, such as .output/Diceminoes-windows-x86_64 and
.output/demo/Diceminoes-windows-x86_64. Each depot maps LocalPath "*"
recursively within its ContentRoot. VDF paths use host separators.
On Windows, the CLI checks both the current and saved PATH for SteamCMD, so
an existing install works even in a terminal opened before installation. If
SteamCMD is missing, an interactive terminal offers to install Valve.SteamCMD
with WinGet. The prompt defaults to No; only y or yes starts the download
and PATH setup. After installation, the same command locates SteamCMD and
continues the upload without a terminal restart. Declining, installation
failure, or a noninteractive run stops the upload. If WinGet is unavailable,
install SteamCMD manually and add it to PATH. macOS/Linux require steamcmd
or steamcmd.sh on PATH.
The CLI invokes SteamCMD directly with +login,
+run_app_build, and +quit; stdin stays open for Steam Guard prompts.
Credentials never enter VDFs or CLI diagnostic messages. As with direct
steamcmd +login, credentials are arguments visible to OS process inspection.
The upload does not set a live branch. See Valve's upload docs.
VDFs and SteamPipe scratch output live in a unique OS temp directory, removed
on success, failure, Ctrl+C, and catchable POSIX termination signals.
Force-kill, power loss, or OS termination that skips cleanup may leave the
perro-steampipe-* temp directory; no program can guarantee cleanup in those cases.
perro is the one command you run at every stage of a project: create it, compile scripts, run a live dev loop, cook a release build, package DLC, import animations, and profile hot code. perro dev loads assets straight from disk for fast edit-run cycles, while perro build bakes assets through the static pipeline for release. The CLI wraps all compiler and setup glue, so your project folder stays plain files with no import database to babysit.
Commands use perro, assuming you ran perro_cli install and reloaded your shell profile, or installed from crates.io when available. --path defaults to the current working directory when omitted.
- Start a new game.
perro new --name MyGamescaffoldsproject.toml,input_map.toml,deps.toml,AGENTS.md,README.md, ares/main.scn, and the.perrocrates. - Fast edit-run loop.
perro devcompiles scripts, builds a project-local dev runner, and runs the game reading assets live from disk, so scene and script edits show up quickly. - Add content without hand-writing boilerplate.
perro new_script,perro new_scene,perro new_animation, andperro new_panimtreedrop templated files intores/(or a DLC) and rebuild. - Cook a shippable build.
perro buildbakes supported assets and links a release executable into.output/;perro build --target webandperro build --target androidexport browser and Android bundles. - Package optional or paid content.
perro dlc --name <name>builds one runtime-loadable.output/dlc/<name>.dlcfromdlcs/<name>/. - Import animation and keep the project healthy.
perro import_animconverts glTF/GLB clips to.panim;perro doctor,clippy,format, andtestcheck refs and script quality;perro benchprofiles real play with call chains and frame reports;perro mem-profilerecords process memory.
Use check for the shortest script/scene feedback loop, doctor for project
wiring and missing refs, dev for behavior, and build for shipped/static
behavior. Run clippy and test after structural checks pass. A successful
dev run does not replace a release build check because asset and linking paths
differ.
Generated .perro output belongs to the CLI. Fix source under res/, config,
or engine crates; do not patch generated glue as a durable solution.
# 1. Install the `perro` shell command, then open a new shell.
perro_cli install
# 2. Scaffold a new project next to your other games.
perro new --path D:\GameProjects --name MyGame
# 3. Add a behavior script and a 3D scene.
perro new_script --path D:\GameProjects\MyGame --name PlayerController --res /scripts
perro new_scene --path D:\GameProjects\MyGame --name Main --template 3D --res /scenes
# 4. Run the live dev loop with timing overlays while you edit.
perro dev --path D:\GameProjects\MyGame --timings
# 5. Cook the release executable into .output/.
perro build --path D:\GameProjects\MyGameBuild and run:
perro check [--path <project_dir>]
perro test [--path <project_dir>] [-- <cargo_test_args>]
perro dev [--path <project_dir>] [--scene res://path.scn] [--target native|web|android] [--headless] [--timings] [--profile] [--ui-profile] [--release] [--csv-profile [csv_name]] [--sim <spec>] [--host <addr>] [--port <num>]
perro capture --output <path> [--path <project_dir>] [--source main|camera2d:<name>|camera3d:<name>|ui:<name>|target:<name>] [--mode offline|realtime] [--width <px>] [--height <px>] [--aspect preserve|W:H] [--fps <fps-or-num/den>] [--duration <sec>] [--supersample <integer-scale>] [--framing fit|crop|expand|stretch] [--format png|gif|webm|mp4|webp] [--transparent]
perro build [--path <project_dir>] [--target native|web|android] [--triple <rust_target> | --universal-macos] [--headless] [--profile] [--console]
perro targets [--host windows|linux|macos]
perro dlc --name <dlc_name> [--path <project_dir>]New projects and templates:
perro new [--path <parent_dir>] [--name <project_name>]
perro new_dlc --name <dlc_name> [--path <project_dir>] [--no-open]
perro new_script --name <script_name> [--path <project_dir>] [--res <res_subdir>] [--dlc <dlc_name>] [--no-open]
perro new_scene --name <scene_name> [--path <project_dir>] [--res <res_subdir>] [--dlc <dlc_name>] [--template 2D|3D] [--no-open]
perro new_animation --name <animation_name> [--path <project_dir>] [--res <res_subdir>] [--dlc <dlc_name>] [--no-open]
perro new_panimtree --name <tree_name> [--path <project_dir>] [--res <res_subdir>] [--dlc <dlc_name>] [--no-open]
perro import_anim <model.glb|model.gltf> --output <clip.panim> [--clip <name|index>] [--fps <fps>] [--skeleton <object_name>] [--retarget-map <map.pretarget>] [--target-rig <model.glb|model.gltf>]Health and maintenance:
perro doctor [--path <project_dir>]
perro test [--path <project_dir>] [-- <cargo_test_args>]
perro format [--path <project_dir>]
perro clippy [--path <project_dir>]
perro clean [--path <project_dir>]Profiling:
perro bench [--path <project_dir>] [--output <new_run_dir>] [--scene res://path.scn] [--deep] [--cpu-samples] [--frames <N>] [--warmup-frames <N>] [--target-fps <fps>] [--sim <spec>]
perro bench compare <baseline_dir> <candidate_dir>
perro bench report <run_dir>
perro mem-profile [--path <project_dir>] [--release] [--csv [csv_name]]Install:
perro installRecommended workflow:
- Use shipped sample projects under
demos/for repo examples. - Put temporary test/sandbox projects outside this monorepo, for example
D:\GameProjects\MyGame. - Open external project folders directly in VS Code.
Why:
- External projects keep project-local
.vscode/settings.jsonactive. demos/Demo2Danddemos/Demo3Dstay as known-good sample projects.perro check,perro dev, andperro buildwork with any project passed by--path.
Use these commands for normal compile, run, export, and DLC package workflows.
| Command | Main job | Output |
|---|---|---|
check |
Compile project scripts only. | .perro/scripts build output |
test |
Sync project scripts and run their Rust tests. | cargo test result |
dev |
Compile scripts, build dev runner, run project. | running dev app |
capture |
Render exact output frames, then package them. | requested image/video output |
build |
Compile scripts, bake static assets, build release project. | .output/ executable + packed assets |
targets |
Show ready, setup-required, and unavailable build targets for a development OS. | support matrix |
dlc |
Build one runtime-loadable DLC package. | .output/dlc/<name>.dlc |
Command:
perro check --path <project_dir>What it does:
- Syncs every
*.rsfile from<project_dir>/res/**into<project_dir>/.perro/scripts/srcas generated*.gen.rs. - Regenerates module exports in
.perro/scripts/src/lib.rsfor all synced Rust files. - Regenerates runtime scripts registry in
.perro/scripts/src/lib.rsfor behavior scripts. - Builds the scripts crate at
<project_dir>/.perro/scripts.
Use this when you only need script compilation/update.
Command:
perro test --path <project_dir> [-- <cargo_test_args>]What it does:
- Syncs every
*.rsfile from<project_dir>/res/**into<project_dir>/.perro/scripts/srcas generated*.gen.rs. - Regenerates module exports and the runtime scripts registry in
.perro/scripts/src/lib.rs. - Refreshes source overrides in
.perro/scripts/Cargo.toml. - Runs
cargo testfrom<project_dir>/.perro/scripts. - Sets
CARGO_TARGET_DIR=<project_dir>/targetso script tests share the project build cache. - Enables the generated scripts crate
steamworksfeature when project Steam support is enabled.
Flags:
-- <cargo_test_args>: forwards remaining args tocargo test.
Examples:
perro test --path D:\GameProjects\MyGame
perro test --path D:\GameProjects\MyGame -- --lib -- --nocapture
perro test --path D:\GameProjects\MyGame -- player_state_testsCommand:
perro dev --path <project_dir> [--scene res://path.scn] [--target native|web|android] [--headless] [--demo] [--tools] [--timings] [--profile] [--ui-profile] [--release] [--csv-profile [csv_name]] [--sim <spec>] [--host <addr>] [--port <num>]What it does:
- Runs the same scripts build pipeline as
check. - With
--target nativeor no--target, builds the project-local dev runner at<project_dir>/.perro/dev_runner. - With
--target native, launches the generated dev runner binary with your--path. - With
--target web, builds a wasm web bundle from.perro/project. - With
--target web, starts a built-in static server and opens your browser.
Flags:
--target native|web|android: selects native runner, browser wasm bundle, or Android app target. Defaultnative.--scene res://path.scn: boots this scene instead of the project'smain_scene. Forwarded to the runner asPERRO_BOOT_SCENE. Use it to profile a heavy scene directly instead of landing on the project menu.--headless: runs the nativeperro_headlessdev path with no window, input, or GPU render loop. Native only; rejected with--target webor--target android, and cannot combine with--timingsor--ui-profile.--playtest: apply[playtest]overrides + exclusions; select Playtest App ID; splituser://saves under base name +_Playtest; enableplaytest_include!+playtest_exclude!. Reject combo with--demo.--demo: applies[demo]config overrides, skips excluded scripts/assets/scenes, strips tagged node trees, and enablesdemo_exclude!.--tools: syncs and compiles*.tool.rsproject modules and enables theperro-toolsscript feature. Off by default and native only. Normal dev/build skips their generated.perro/scriptsfiles entirely. Use for QA, capture, perf, and debug drivers that must not compile into normal dev or player builds.--timings: prints lightweight native timing averages: sim, gfx, delta, fps.--profile: enables profiling feature for the selected dev target.--ui-profile: enables native dev runnerui_profilefeature.--release: builds release dev target.--csv-profile [csv_name]: writes native dev profile metrics CSV under.output/profiling/.--sim <spec>: runs the dev runner as a weaker machine than this one, for profiling. Forwarded to the runner asPERRO_SIM. Native only; a bad spec fails before the build. See Perf Simulation.--host <addr>: web target only. Static server bind host. Default127.0.0.1.--port <num>: web target only. Static server bind port. Default8000.
Android target notes:
--timings,--ui-profile, and--csv-profileare not supported withperro dev --target androidyet.- Android dev builds require an installed Android SDK/NDK and a running emulator or device.
Web target notes:
--ui-profileis not supported withperro dev --target webyet.--timingsis not supported withperro dev --target webyet.--csv-profileis not supported withperro dev --target webyet.- web output dir:
<project_dir>/.output/web-dev/ - web path uses static embedded wasm runtime, not the native dynamic file-loading dev runner.
- see WASM / Web Target
Use this for local development runs and testing.
The dev runner keeps assets dynamic and reads from normal project files.
Dynamic scene/resource loading is optimized for development.
Perro CLI handles compiler/setup glue so day-to-day workflow stays simple while project structure stays flexible.
For release-like asset loading numbers, run perro build.
See Performance + Flexibility Philosophy.
Command:
perro capture --path <project_dir> --output <file_or_dir> [--source main|camera2d:<name>|camera3d:<name>|ui:<name>|target:<name>] [--mode offline|realtime] [--width <px>] [--height <px>] [--aspect preserve|W:H] [--fps <fps-or-num/den>] [--duration <sec>] [--supersample <integer-scale>] [--framing fit|crop|expand|stretch] [--format png|gif|webm|mp4|webp] [--transparent] [--scene res://path.scn] [--sim <spec>]capture starts the project graphics runner with a capture session. Source syntax uses main for the final compositor, or kind:name for a scene source:
For script and QA control, see the Capture Runtime Module.
camera2d:<node-name-or-id>camera3d:<node-name-or-id>ui:<node-name-or-id>(aliases:uisubview:<name>,ui_sub_view:<name>)target:<node-name-or-id>(alias:render_target:<name>)
Source names resolve against live scene nodes. Missing names, empty names, and wrong node types fail before capture output starts. target:<name> selects already-rendered pixels; --framing expand rejects that source because a raster target cannot reveal extra view. The resolved source, frame timing, dimensions, and output count belong to capture metadata.
Offline mode uses fixed simulation steps and no wall-clock pacing. --duration * --fps must resolve to a whole frame count. Frames use timestamps 0, 1/fps, ...; replay input applies before its target simulation tick. The runner exits after the requested frame count and drains GPU readbacks and encoders before final packaging.
Realtime mode samples elapsed wall time at the requested rational rate. A late render reuses the latest completed frame for missed sample slots; metadata records realtime_duplicate_frames. Offline mode never reuses a prior frame to fill its schedule.
Defaults:
--mode offline--fps 60--duration 1--supersample 2--source main--framing fit--format png
--width and --height set final output dimensions. Set one dimension to derive the other from --aspect; omit both to keep source dimensions. --aspect preserve keeps source aspect; W:H sets an explicit output aspect.
Supersampling never changes layout, scale, or look. Main capture uses the live window's logical size, exactly as perro dev: UI ratios, text sizes, nine-slice margins, pixel snapping, and camera aspect/FOV resolve there. Capture scales the same 3D, 2D, and UI frame uniformly into raster pixels. A 1920x1080 window with --width 3840 --supersample 2 renders at 7680x4320 and saves 3840x2160. The live window displays the full frame downscaled to window size, never a cropped corner. --supersample 2 remains the default; higher values change sampling quality only.
When output aspect differs, framing acts on the final image: fit keeps the entire source with letterboxing, crop fills output and trims excess, and stretch scales each axis. The scene first renders at its logical aspect; framing does not rerun layout. expand changes the view only for camera sources; main-window and UI sources use fit, and raster targets reject expand. The framed raster is output_size * supersample; it downsamples to exact output dimensions before encoding.
GIF output uses one stable clip palette, exact colors when 255 opaque colors fit, a fixed alpha cutoff, and rate-correct variable frame delays. Animated WebP uses ffmpeg lossless BGRA encoding with full alpha and infinite looping.
--transparent uses alpha-zero clear pixels for capture targets. World or IBL lighting still shades 3D objects; opaque environment background does not leak into transparent output. Omit the flag for normal opaque output.
MP4, WebM, and animated WebP stream ordered RGBA frames through a bounded worker queue to FFmpeg during capture, without raw frame files. PNG output keeps numbered frames; GIF stages raw frames for its whole-clip palette. Readback buffers are reused with a 256 MiB in-flight byte budget (one frame is allowed when a single frame exceeds that budget). Offline capture can render up to three frames ahead of encoder admission; readback, preparation, and encoding overlap. It pauses at queue pressure without advancing simulation and drains the tail at stop. Realtime capture skips readbacks when the GPU queue is full and retains only the newest completed sample; offline capture waits without advancing the output clock. Encoder input queues scale down for large frames (a 256 MiB input budget, with a minimum of one worker, one queued frame, and one pending frame; GPU buffers, resize scratch, and encoder memory are additional). Finalization runs in the background and the CLI waits for commit before exit. WebM, MP4, and animated WebP need ffmpeg on PATH; API callers can set an explicit executable with OutputSpec::with_ffmpeg. Media and metadata use temporary files and rename on success. Errors and abandoned sessions clean up staging after workers stop; final PNG sequences remain intact. Cleanup on drop is best effort if the filesystem rejects removal. See video export workflow for capture settings and output checks.
Examples:
# 360 fixed-clock 60 FPS frames, 2x render, final 1920x1080
perro capture --path D:\GameProjects\MyGame --source camera3d:MainCamera --width 1920 --height 1080 --fps 60 --duration 6 --format webm --transparent --output .output\capture.webm
# Write canonical PNG sequence at final dimensions
perro capture --path D:\GameProjects\MyGame --source ui:Hud --format png --width 1920 --height 1080 --output .output\ui-framesRuntime scripts and QA hosts use ctx.run.Capture() for the same session core. Set config.output before start; the normal API derives source dimensions and staging paths:
let output = OutputSpec::new(".output/capture", OutputFormat::PngSequence);
let mut config = CaptureConfig {
output: Some(output),
..CaptureConfig::default()
};
let mut capture = ctx.run.Capture();
capture.start(config)?;
// Optional director/replay event. Offline playback applies it before its tick.
capture.record_action(
Duration::from_secs_f32(0.5),
"input:key_down",
Some("Space".to_owned()),
)?;
capture.stop()?;state() returns Recording, Draining, or Finalized while a session exists. progress(), completed_frames(), source(), source_route(), and render_size() expose queue, route, and size state. stop() requests a safe app-boundary stop using config.output; the app drains GPU readback and encode before commit. request_stop(output) supplies an output override. Renderer hosts that own the full boundary use start_raw(...), submit_rgba(...), drain(), and finish_raw(output). Poll last_output() after the boundary; it returns FinalizedCapture with output_path, metadata_path, frame_count, exact final output_size, and pre-downsample render_size. Metadata stores the rational rate, timestamps, framing, alpha, supersample, source dimensions, output dimensions, and recorded action timeline for replay. Replay actions use input:key_down, input:key_up, input:mouse_down, input:mouse_up, input:text, or signal:<name> action names.
Command:
perro build --path <project_dir> [--target native|web|android] [--triple <rust_target> | --universal-macos] [--headless] [--profile] [--console] [--demo]--headless use native perro_headless feature path.
- rm
perro_app,perro_graphics,winitfrm final dep graph - kp scripts, scenes, timers, net, CPU physics + water physics
- force CPU particle cfg
- skip window, input device, GPU + rndr loop
- sync new + old
.perro/project+.perro/dev_runnermanifests
Steam-enabled headless builds use Steam GameServer API, not Steam client login.
- anonymous login default
PERRO_STEAM_GSLT-> token loginPERRO_STEAM_GAME_PORT-> game port; default27015PERRO_STEAM_QUERY_PORT-> query port; default27016PERRO_STEAM_SERVER_IP-> bind IPv4; default0.0.0.0PERRO_STEAM_SERVER_NAME-> browser namePERRO_STEAM_MAX_PLAYERS-> browser cap; default64PERRO_STEAM_LISTED=0-> disable browser listingPERRO_STEAM_SECURE=0-> auth w/o VAC-secure mode
Server scripts use steam::game_server for ticket auth, player stats, and server-set achievements.
What it does:
- Runs script compilation, like
check. - Packs
resassets through the static pipeline. - Generates embedded project entry files under
.perro/project. - Optimizes supported assets into match tables and preparsed compile-time statics.
- Packs unsupported/generic assets into
.perro/project/embedded/assets.perro. - Builds the generated project crate in release mode from
.perro/project. - With
--target nativeor no--target, copies the built native output to<project>/.output/. macOS builds use an unsigned.appbundle. - With
--target web, exports browser bundle files to<project>/.output/web/.
Flags:
--target native|web|android: selects native executable, browser wasm bundle, or Android app target. Defaultnative.--demo: builds only the demo-visible source and applies[demo]config overrides. Output goes to a separate<project>/.output/demo/tree (native,web/,android/) so demo exports never overwrite the full build.--triple <rust_target>: cross-compiles a native build for one Rust target triple. The CLI installs the Rust standard-library target when needed. The host still needs the target linker, SDK, and native libraries. A macOS triple emits an unsigned.appbundle.--universal-macos: on macOS, buildsaarch64-apple-darwinandx86_64-apple-darwin, then merges both slices into one unsigned.appwithlipo. Per-architecture exports are kept beside the universal export.--profile: enables profile build options for the generated project bundle.--console: enables console build options for generated native project bundle.
macOS .app exports use the standard Contents/MacOS/<game> bundle layout. Perro does not code-sign or notarize these bundles; add Apple signing and notarization as a separate release step when required.
Native export directories carry the project name, OS, and CPU architecture, while the launch artifact uses one fixed lowercase name across projects and versions: game.exe on Windows, game on Linux, and game.app on macOS. Map one platform directory to the root of its Steam depot so Steam launch-option paths stay constant.
Web target notes:
--consoleis not supported withperro build --target web.- web build uses stable
wasm32-unknown-unknown+wasm-bindgen --target web. - web output includes
index.html,boot.js,app.js, andapp_bg.wasm. - see WASM / Web Target
Android target notes:
--consoleis not supported withperro build --target android.- Android builds require an installed Android SDK/NDK; the CLI resolves them from
ANDROID_SDK_ROOT/ANDROID_HOMEandANDROID_NDK_ROOT/ANDROID_NDK_HOME/NDK_HOMEor the default platform location.
Use this to build the final executable into <project>/.output/.
Native cross-build examples:
perro build --triple x86_64-pc-windows-msvc
perro build --triple i686-pc-windows-msvc
perro build --triple aarch64-pc-windows-msvc
perro build --triple x86_64-unknown-linux-gnu
perro build --triple i686-unknown-linux-gnu
perro build --triple aarch64-unknown-linux-gnu
perro build --triple x86_64-apple-darwin
perro build --triple aarch64-apple-darwin
perro build --universal-macos
Windows MSVC architecture cross-builds need the matching Visual Studio C++ tools. Linux cross-builds need the matching GNU or compatible linker and target system libraries. macOS builds need macOS/Xcode tooling; use a Mac for release signing and notarization.
perro targets
perro targets --host windows
perro targets --host linux
perro targets --host macos
Without --host, this shows the current development OS. READY means the host can build the target directly. SETUP means the build is possible after installing the listed linker, SDK, or system libraries. NO means use another development OS.
| Development OS | Windows | Linux | macOS | Web | Android |
|---|---|---|---|---|---|
| Windows | ready/setup by architecture | setup | no | ready | setup |
| Linux | setup with GNU/LLVM target | ready/setup by architecture | no | ready | setup |
| macOS | setup with GNU/LLVM target | setup | ready, including universal | ready | setup |
The static pipeline packs all res assets.
Supported assets, such as scenes, animations, materials, particles, meshes, textures, and CSV tables, are optimized into match tables and preparsed formats as compile-time statics for efficient runtime performance.
This is main Perro trade: author normal files in dev, then let compiler pipeline reshape them for release performance.
Other res files are packed generically into assets.perro.
See Performance + Flexibility Philosophy.
Command:
perro dlc --name <dlc_name> [--path <project_dir>]What it does:
- Reads source from
<project_dir>/dlcs/<dlc_name>/. - Generates DLC scripts crate under
.perro/dlc/<dlc_name>/scripts/. - Generates DLC pack crate under
.perro/dlc/<dlc_name>/pack/. - Builds both runtime-loadable modules.
- Packs manifest, scripts module, pack module, and DLC resources into
<project_dir>/.output/dlc/<dlc_name>.dlc. - Compresses final
.dlcwhen it reduces file size. - Removes temporary
.dlc.stagingfolder after successful pack.
Name rules:
selfis reserved fordlc://self/...and is rejected as a DLC name.
Use these commands to create projects, DLC folders, scripts, scenes, animation clips, and animation trees.
Shared rules:
--pathresolves to a project root for every command exceptnew.new --pathresolves to the parent directory that receives the new project.- Commands with
--dlc <name>targetdlcs/<name>/instead of projectres/. --resacceptsres://...or/...for base game content.--resacceptsdlc://<name>/...or/...for DLC content.--no-opendisables VS Code open for generated files.
Command:
perro new [--path <parent_dir>] [--name <project_name>]What it does:
- Creates a new project directory under
<parent_dir>. - Writes default project files:
project.toml,input_map.toml,deps.toml,AGENTS.md,README.md,res/main.scn, scripts scaffold, and.perrocrates.AGENTS.mdexplains Perro's state model and links to authoring examples; the README links to it. Shared project bootstrap also adds this file when absent, preserving existing agent files and READMEs. - Prompts to open the project in VS Code.
Notes:
- If you run this inside a directory you want to contain projects, omit
--path. - Add extra script Rust crates in
deps.tomlunder[dependencies]. - Perro merges
deps.tomlinto.perro/scripts/Cargo.tomloncheck,dev, andbuild.
Examples:
perro new --path D:\GameProjects --name MyGame
perro new --name MyGameCommand:
perro new_dlc --name <dlc_name> [--path <project_dir>] [--no-open]What it does:
- Resolves
<project_dir>. - Creates
<project_dir>/dlcs/<dlc_name>/. - Creates starter directories:
scenes/,scripts/,materials/, andmeshes/. - Creates starter files:
scenes/main.scnandscripts/script.rs. - Uses
dlc://<dlc_name>/scripts/script.rsin starter scene.
Name rules:
selfis reserved fordlc://self/...and is rejected as a DLC name.
Examples:
perro new_dlc --name CosmeticsPack
perro new_dlc --name CosmeticsPack --path D:\GameProjects\MyGameCommand:
perro new_script --name <script_name> [--path <project_dir>] [--res <res_subdir>] [--dlc <dlc_name>] [--no-open]What it does:
- Resolves
<project_dir>. - Resolves target root: project
res/, or projectdlcs/<name>/with--dlc. - Resolves
<res_subdir>relative to target root. - Creates a new
*.rsscript from the empty script template. - Opens the new file in VS Code unless
--no-openis passed. - Rebuilds scripts after file creation.
Notes:
--namecan omit.rs; extension is added automatically.--namemust be a file name only.
Examples:
perro new_script --name PlayerController
perro new_script --name PlayerController --res /scripts
perro new_script --name PlayerController --path D:\GameProjects\MyGame --res res://scripts
perro new_script --name DlcController --path D:\GameProjects\MyGame --dlc ExpansionOne --res /scripts
perro new_script --name DlcController --path D:\GameProjects\MyGame --dlc ExpansionOne --res dlc://ExpansionOne/scripts
perro new_script --name PlayerController --no-openCommand:
perro new_scene --name <scene_name> [--path <project_dir>] [--res <res_subdir>] [--dlc <dlc_name>] [--template 2D|3D] [--no-open]What it does:
- Resolves
<project_dir>. - Resolves target root: project
res/, or projectdlcs/<name>/with--dlc. - Resolves
<res_subdir>relative to target root. - Creates a new
*.scnscene from the selected template. - Opens the new file in VS Code unless
--no-openis passed.
Notes:
--templatedefaults to2D.- Generated scenes use
$root = @main. $rootmarks the scene root and can be reused as a node ref.--namecan omit.scn; extension is added automatically.--namemust be a file name only.
Examples:
perro new_scene --name Main
perro new_scene --name Main3D --template 3D
perro new_scene --name Main --res /scenes
perro new_scene --name Main --path D:\GameProjects\MyGame --res res://scenes --template 2D
perro new_scene --name DlcIntro --path D:\GameProjects\MyGame --dlc ExpansionOne --res /scenes
perro new_scene --name Main --no-openCommand:
perro new_animation --name <animation_name> [--path <project_dir>] [--res <res_subdir>] [--dlc <dlc_name>] [--no-open]What it does:
- Resolves
<project_dir>. - Resolves target root: project
res/, or projectdlcs/<name>/with--dlc. - Resolves
<res_subdir>relative to target root. - Creates a new
*.panimanimation clip from the default animation template. - Opens the new file in VS Code unless
--no-openis passed.
Notes:
- Defaults to
res/animationswhen--resis omitted. --namecan omit.panim; extension is added automatically.--namemust be a file name only.
Examples:
perro new_animation --name CubeMove
perro new_animation --name HeroRun --res /animations
perro new_animation --name HeroRun --path D:\GameProjects\MyGame --res res://animations
perro new_animation --name DlcIdle --path D:\GameProjects\MyGame --dlc ExpansionOne --res /animations
perro new_animation --name HeroRun --no-openCommand:
perro new_panimtree --name <tree_name> [--path <project_dir>] [--res <res_subdir>] [--dlc <dlc_name>] [--no-open]What it does:
- Resolves
<project_dir>. - Resolves target root: project
res/, or projectdlcs/<name>/with--dlc. - Resolves
<res_subdir>relative to target root. - Creates a new
*.panimtreeanimation tree from the default animation tree template. - Opens the new file in VS Code unless
--no-openis passed.
Notes:
- Defaults to
res/animationswhen--resis omitted. --namecan omit.panimtree; extension is added automatically.--namemust be a file name only.
Examples:
perro new_panimtree --name HeroMove
perro new_panimtree --name HeroMove --res /animations
perro new_panimtree --name HeroMove --path D:\GameProjects\MyGame --res res://animations
perro new_panimtree --name DlcMove --path D:\GameProjects\MyGame --dlc ExpansionOne --res /animations
perro new_panimtree --name HeroMove --no-openCommand:
perro import_anim <model.glb|model.gltf> --output <clip.panim> [--clip <name|index>] [--fps <fps>] [--skeleton <object_name>] [--retarget-map <map.pretarget>] [--target-rig <model.glb|model.gltf>]gltf_to_panim and glb_to_panim are aliases.
Use --options <file.toml> to load saved conversion options. Explicit flags (including --in, --out, and --retarget) and a positional input override saved values. Resolve file paths from the working directory; run from the project root for portable options.
# editor_tools/animation.toml
version = 1
input = "res/models/hero.glb"
output = "res/animations/walk.panim"
clip = "Walk"
fps = 60
skeleton = "Rig"
# retarget_map = "res/animations/humanoid.pretarget"
# target_rig = "res/models/hero.glb"perro import_anim --options editor_tools/animation.toml
perro import_anim --options editor_tools/animation.toml --clip Run --fps 30Reject unknown keys, unsupported versions, and invalid value types. Conversion completes before publishing the output via a temporary sibling file, so conversion failure retains an existing output. Loading options does not trigger conversion until this command runs.
What it does:
- Loads the glTF document.
- Selects one animation by
--clipname or index. - Converts translation, rotation, and scale channels into
.panimkeyframes. - Writes node tracks as
Node3Dobjects. - Writes skin joint tracks as
Skeleton3Dbone tracks on--skeletonobject. - With
--retarget-map, bakes bone aliases, rest-pose alignment, and translation policy.
Notes:
--clipdefaults to0.--fpsdefaults to60.--skeletondefaults toRig.- Scene or script bindings still map
.panimobject names to actual scene nodes. - Bone names come from glTF node names, for example
bone["Spine"].rotation. - Joint rotations convert from glTF local rotations to Perro rest-relative pose deltas.
--target-rigreads target joint rest poses and needs--retarget-map.- Inline rest poses in the map override glTF rest poses.
- Morph target weights are ignored.
Examples:
perro import_anim res/models/hero.glb --output res/animations/idle.panim --clip Idle
perro import_anim res/models/hero.glb --output res/animations/run.panim --clip 1 --fps 30 --skeleton HeroRig
perro import_anim res/models/mocap.glb --output res/animations/run.panim --retarget-map res/animations/humanoid.pretarget --target-rig res/models/hero.glbRetarget map:
source = Rig
target = HeroRig
keep_unmapped = false
translation = root_only
root_bone = mixamorig:Hips
bone mixamorig:Hips => Hips
bone mixamorig:LeftArm => upper_arm.L
# position | rotation quaternion | scale
source_rest mixamorig:Hips = (0, 0.9, 0) | (0, 0, 0, 1) | (1, 1, 1)
target_rest Hips = (0, 1.02, 0) | (0, 0, 0, 1) | (1, 1, 1)translation values:
all: keep all bone translation tracks; default for old maps.root_only: keep onlyroot_bonetranslation tracks.none: remove all bone translation tracks.
Rest solve maps source-rest position/scale to target-rest position/scale.
Rotation keys stay rest-relative deltas, matching Skeleton3D playback.
Use these commands to check references, run user script tests, format user scripts, lint user scripts, and remove build output.
Command:
perro doctor [--path <project_dir>]What it does:
- Loads
project.toml. - Checks
project.main_scene,project.icon, andproject.startup_splash. - Scans text assets under
res/anddlcs/for quotedres://anddlc://references. - Scans user scripts for likely missing
res://anddlc://load paths. - Warns when
get_var!,set_var!,broadcast_var!,call_method!, or a signal connection references a name not found in any script state ormethods!block, or targets a member that exists but is notpub(no dispatch glue is generated); the warning names the defining file. Also warnsscene var privatewhen a scenescript_varsentry or.panimset_varevent targets a non-pub state field β that value will not apply. - Warns the reverse too: a
pubstate field orpub fnctx method that nothing references dynamically β novar!/func!/method!literal, access-macro string, signal connection, scenescript_varsentry on a node running that script, animation event, or even a plain string literal matching the name anywhere in script code β can droppubto shed its generated glue. Name matching is project-wide and never deduplicates: a shared name with any dynamic use anywhere stays quiet, so only names with zero uses are flagged. Methods without aScriptContextparameter never get glue, sopubon plain helpers is ignored; acall_method!aimed at one of those gets its own "not callable" warning instead. - Warns when those dynamic calls target
ctx.idand a typed self access path is available. - Compiles every
.wgslunderres/anddlcs/against the engine prelude and reports parse/type errors at the shader's own line and column. - Reports missing scene/config references and broken shaders as errors, and script findings as warnings.
Shader checking composes your file exactly like the renderer does, so a custom
material is checked against both the rigid and the skinned prelude, a Sky3D
pass against the sky stack, and a post-process pass against the post prelude:
err: shader res://shaders/sky_horizon_band.wgsl:9:12: [sky pass] no definition in scope for identifier: `undefined_helper`The entry function decides which prelude applies: shade_material (or
shade_vertex) means custom 3D material, sky_shader means Sky3D pass, and
post_process means post-process pass. A .wgsl with none of them cannot be
loaded by the engine, so doctor warns and skips it.
The same check runs during perro build --static, where a broken shader fails
the build instead of the first frame that draws with it.
Command:
perro format --path <project_dir> [--dedup]What it does:
- Resolves your path to that project's
resroot. - Recursively finds format targets under
res/**. - Runs
rustfmton*.rsfiles. - Formats
*.scnand*.furscene files. - Formats key/value resource files:
*.pmat,*.ppart, and*.uistyle. - With
--dedup, creates$varNvalues for large repeated scene values used 3+ times.
Command:
perro clippy --path <project_dir>What it does:
- Resolves your path to that project's
resroot. - Recursively finds all
*.rsfiles underres/**. - Syncs those files into
.perro/scripts. - Runs
cargo clippy --all-targets -- -D warningsfor the generated scripts crate.
Command:
perro clean [--path <project_dir>]What it does:
- Removes the project's
target/directory.
Use these commands to record memory samples or produce flamegraphs from the dev runner.
Answers "how does this run on a machine weaker than mine?" without a second machine.
perro dev --path <project_dir> --sim igpu
perro dev --path <project_dir> --sim half --timings
perro dev --path <project_dir> --sim "igpu,cores=2"Sets PERRO_SIM on the dev runner. Set the env var directly for a shipped build or a bench run:
$env:PERRO_SIM = "low_end"Presets:
| Preset | GPU | Cores |
|---|---|---|
off |
machine default | all |
igpu |
integrated tier + request the LowPower adapter |
all |
low_end |
integrated tier | 4 |
half |
integrated tier | half this machine's |
potato |
integrated tier + LowPower adapter + 720p scene cap |
2 |
Tokens, comma separated, later wins: cores=N, gpu=off|constrained|igpu, pixels=WxH. --sim "half,gpu=off" cuts cores only; --sim "cores=4" skips the GPU tier entirely.
What each axis really does:
- GPU: flips the same low-end quality policy an integrated adapter already trips - 1080p scene cap, MSAA off with FXAA swapped in, SSAO low, small shadow atlas, memory-usage allocator hints.
igpu/potatoalso request theLowPoweradapter, so a hybrid laptop runs on its real integrated GPU. It does not slow the silicon down: a discrete card still renders that tier fast. Use it to check the quality tier and its CPU-side cost, not to predict an iGPU's frame time. - CPU: caps the shared worker pool and every parallel work split to N threads. A real core-count cut, not an injected stall, so parallel scaling and single-thread bottlenecks show up honestly. Per-core clock speed is unchanged.
The runner prints one line at startup when a sim is on:
[perro][sim] PERF SIM ON gpu=(igpu) cores=(4/16) max_scene_pixels=(default) -- timings are NOT this machine
Check that line before trusting any timing CSV: a forgotten PERRO_SIM in your shell poisons every later measurement.
Run normal gameplay and record frame work and instrumented call chains. No bench fixture or script changes are required. Close the game to finish the report.
perro bench --path D:\GameProjects\MyGame
perro bench --path D:\GameProjects\MyGame --scene res://boss.scn --deep
perro bench --path D:\GameProjects\MyGame --frames 1200 --warmup-frames 120
perro bench compare <baseline_run_dir> <candidate_run_dir>
perro bench report <run_dir>The CLI builds the real runner and scripts together in release mode, enables GPU
timestamps and memory samples, and writes a unique .output/bench/<run-id>/.
It replaces the old spec, flamegraph, and synthetic-script bench CLI. Those
commands/flags have no aliases. Engine Criterion benches remain available through
Cargo.
Flags:
--path: project root; defaults to the enclosing project.--output: a new capture directory, resolved from the current working directory. Existing directories are rejected; repeat runs never overwrite earlier evidence. Omit it for a unique directory under the project's.output/bench/.--scene: boot an authored scene instead of the main scene.--deep: add fine-grained node reads, script state access spans, and native allocation stacks on Windows. Resolve function/source/line from local debug symbols when available; preserve unresolved addresses. State closure timing includes the closure body, not just the access overhead. Stack capture adds more overhead than default allocation counters. Native stacks focus on named script/API/runtime scopes; bare frame and unscoped/worker allocations retain counters and lifetime tracking without consuming native stack capacity.--frames N: exit cleanly after N measured gameplay frames. Splash frames and both configured and intrinsic timing warmup occur before this budget; even a short run captures gameplay regardless of monitor refresh rate.--warmup-frames N: exclude the first N gameplay frames from aggregate metrics; default 120. Raw files retain warmup and splash evidence.--target-fps N: frame budget for hitch counts; default 60. Does not change pacing.--sim: apply the same simulation policy asdev; recorded in run metadata.--cpu-samples: additionally run cargo-flamegraph on the same capture, producingcpu-flamegraph.svg. Requires an installed platform sampler and its permissions. No automatic installation or elevation. The default built-in flamegraph does not require this tool.
Long interactive captures can exceed the default 128 MiB call-trace cap. Set
PERRO_BENCH_MAX_TRACE_BYTES to a positive byte count before launching to raise
that bound (for example, 4294967296 for 4 GiB). Invalid or zero values retain
the default. This is a disk-output limit, not a preallocation. Use the same cap
for baseline and candidate, allow enough free disk space, and check
trace_status.json for the actual limit and dropped events after clean exit.
Artifacts:
| File | Meaning |
|---|---|
summary.md |
Human-readable metrics, ranked hotspots, and evidence-backed suggestions |
report.json |
Versioned aggregate data, source links, coverage, and findings |
hotspots.jsonl |
One aggregate call chain per line, with CPU/call/allocation ranks and source refs |
frame_costs.jsonl |
One measured frame per line, with additive self costs, call counts, and allocation coverage |
frames.csv |
Per-frame CPU phases, GPU timestamps when available, and draw counters |
calls.jsonl |
Named call-chain aggregates per frame; inclusive/self time and heap allocation counters |
allocations.json |
Allocation-origin retention, coverage/loss counters, and optional native stacks |
symbols.json |
Symbol names, source paths, and line locations |
flamegraph.svg |
Instrumented call-chain self-time visualization, not CPU samples |
samples.csv |
Batched process RSS measurements when available |
markers.jsonl |
Optional named gameplay markers |
run.json |
Build hashes, git state, hardware inventory, and capture settings |
trace_status.json |
Trace limits, dropped events, write errors, and scope coverage |
comparison.json |
Comparison output written into the candidate run directory |
Coverage includes script lifecycle, generated methods, cross-script dispatch, node/query, script/signal/scene, timer, animation, physics and navmesh APIs, plus selected runtime/deferred phases. Generated script libraries forward trace events to the host collector so their nested API calls share one stack. Ordinary Rust helper functions are not all instrumented. Worker-thread work and GPU execution cannot be attributed to script call chains. Deferred sync phases are shown separately; temporal proximity does not prove which script caused their cost. RSS is process memory, separate from requested heap bytes. Inherited media-capture/offline clock environment variables are cleared for bench runs. Capture adds overhead; reports do not claim a measured overhead percentage or an uninstrumented FPS improvement.
Call rows contain readable names plus stable callsite_id, category, source,
line, frame, phase, chain, count, inclusive time, self time, and max duration.
API counts include attempts/no-ops; API spans that invoke user closures include
closure work, not just engine overhead.
Raw callsite IDs identify source locations; report symbol_id values identify
whole call chains. Startup and teardown use phase = "outside_frame". JSONL permits line-by-line
processing and text search. Worst-frame chains rank current frame work; start-to-start frame intervals
describe the interval ending at a frame and include prior-frame work and pacing.
Trace and report aggregation are bounded; check
coverage and drop counters before drawing conclusions.
Allocation tracking needs no script annotations. The bench runner and generated
script/DLC libraries install matching allocator hooks. Each call chain reports
allocation count/bytes, reallocation count/requested target bytes, and free
count/bytes, with separate inclusive and self_ counters. Self counters exclude
nested instrumented calls; do not sum inclusive parent and child totals.
Vec::new() allocates no buffer. Capacity reuse produces no new allocation;
growth, String, Box, and heap work inside engine APIs appear where the actual
allocator operation occurs. Reallocation target bytes are not net memory growth.
Frees in call rows belong to the scope performing the free. The lifetime origin report instead keeps each tracked live block tied to its original allocation chain, including later or cross-thread frees. Outstanding bytes are requested live heap storage at capture end, not proof of a leak. This lifetime view includes startup and teardown; it does not apply the frame report's warmup filter. Profiler bookkeeping is excluded. External native allocators, GPU memory, and work without an active instrumented scope have no script attribution. Counts describe actual allocator operations after optimization, not Rust syntax or source-level object counts. Older captures without allocator data show unknown, not zero. Bounded tracking reports any lost blocks/stacks explicitly.
Use allocation rankings to inspect repeated growth, temporary strings, collection copies, and APIs returning owned buffers. Capacity reuse, borrowed access, or caching may help when ownership and lifetime permit it; compare matching runs after each change. Counter reductions alone do not prove higher game FPS.
For repeated agent runs, choose a finite frame count and a fresh output path. No custom benchmark implementation or gameplay annotations are required:
perro bench --frames 1200 --warmup-frames 120 --output .output/bench/baseline-01
# Inspect evidence, change one hot path, and exercise the same gameplay again.
perro bench --frames 1200 --warmup-frames 120 --output .output/bench/candidate-01
perro bench compare .output/bench/baseline-01 .output/bench/candidate-01
rg '"category":"api.node"' .output/bench/candidate-01/hotspots.jsonlStart with report.json: its artifact manifest identifies files, schema, units,
and coverage. Use hotspots.jsonl to rank self/inclusive CPU cost, call frequency,
and heap traffic; use frame_costs.jsonl to find frame spikes; join raw
calls.jsonl by frame and chain for exact evidence. Names and source locations
remain next to IDs so text search works without a separate symbol lookup.
Script/API call groups count instrumented scopes, including nested calls; they
do not represent a count of independent game actions. Sum self costs, never
nested inclusive costs. Missing or incomplete allocation data is labeled.
Use --deep to investigate a smaller troublesome scene or phase with allocation
source stacks, and bench report <run_dir> to regenerate reports without
rebuilding or rerunning the game. Keep scene, warmup, inputs, settings, and seed
consistent for comparisons. A run observes only exercised code; it does not
automatically explore every scene or invent a safe optimization. Startup and
teardown remain in raw call rows even when excluded from frame rankings.
Engine validation scene (optional):
perro bench --path demos/ScriptPatterns --scene res://bench_alloc.scn --frames 240 --warmup-frames 0 --deep
python tools/check_bench_alloc.py <run_dir>Optional phase markers:
bench_begin!("boss fight");
bench_point!("wave 2");
bench_end!("boss fight");The generated perro-bench script feature enables markers. Normal builds strip
them. Start scene, seed, input, and external state must match for useful A/B runs.
Live capture observes normal gameplay; it does not replay input or automatically
explore the game. Comparison reports flag mismatches rather than certifying a
speedup from two uncontrolled sessions. Use repeated serial runs in reversed order
and validate gameplay behavior after changing a hotspot.
Command:
perro mem-profile --path <project_dir> [--release] [--csv [csv_name]]What it does:
- Runs the same scripts build pipeline as
check. - Builds the project-local dev runner with
profilefeature enabled. - Launches dev runner with memory profiling enabled:
PERRO_MEM_PROFILE=1. - Writes batch memory samples CSV in
<project_dir>/.output/profiling/.
Flags:
--release: builds and runs release dev runner binary.--csv [csv_name]: custom output file name under.output/profiling/.
Command:
perro installWhat it does:
- Adds/updates a
perroshell function in your profile. - On Windows, updates PowerShell profiles.
- On Linux, updates POSIX shell profiles:
~/.profile,~/.bashrc,~/.zshrc. - Function builds source-mode CLI, copies it to temp, then runs args.
After running install, open a new shell or source your updated profile.
Examples:
perro new --path D:\GameProjects --name MyGame
perro check --path D:\GameProjects\MyGame