Skip to content

Latest commit

Β 

History

History
1145 lines (848 loc) Β· 54.2 KB

File metadata and controls

1145 lines (848 loc) Β· 54.2 KB

Perro CLI

Page Map

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

SteamPipe Upload

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.

Purpose

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.

Use Cases

  • Start a new game. perro new --name MyGame scaffolds project.toml, input_map.toml, deps.toml, AGENTS.md, README.md, a res/main.scn, and the .perro crates.
  • Fast edit-run loop. perro dev compiles 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, and perro new_panimtree drop templated files into res/ (or a DLC) and rebuild.
  • Cook a shippable build. perro build bakes supported assets and links a release executable into .output/; perro build --target web and perro build --target android export browser and Android bundles.
  • Package optional or paid content. perro dlc --name <name> builds one runtime-loadable .output/dlc/<name>.dlc from dlcs/<name>/.
  • Import animation and keep the project healthy. perro import_anim converts glTF/GLB clips to .panim; perro doctor, clippy, format, and test check refs and script quality; perro bench profiles real play with call chains and frame reports; perro mem-profile records process memory.

Command Choice

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.

End-to-End Example

# 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\MyGame

Quick Map

Build 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 install

Project Placement

Recommended workflow:

  1. Use shipped sample projects under demos/ for repo examples.
  2. Put temporary test/sandbox projects outside this monorepo, for example D:\GameProjects\MyGame.
  3. Open external project folders directly in VS Code.

Why:

  1. External projects keep project-local .vscode/settings.json active.
  2. demos/Demo2D and demos/Demo3D stay as known-good sample projects.
  3. perro check, perro dev, and perro build work with any project passed by --path.

Build And Run

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

check

Command:

perro check --path <project_dir>

What it does:

  1. Syncs every *.rs file from <project_dir>/res/** into <project_dir>/.perro/scripts/src as generated *.gen.rs.
  2. Regenerates module exports in .perro/scripts/src/lib.rs for all synced Rust files.
  3. Regenerates runtime scripts registry in .perro/scripts/src/lib.rs for behavior scripts.
  4. Builds the scripts crate at <project_dir>/.perro/scripts.

Use this when you only need script compilation/update.

test

Command:

perro test --path <project_dir> [-- <cargo_test_args>]

What it does:

  1. Syncs every *.rs file from <project_dir>/res/** into <project_dir>/.perro/scripts/src as generated *.gen.rs.
  2. Regenerates module exports and the runtime scripts registry in .perro/scripts/src/lib.rs.
  3. Refreshes source overrides in .perro/scripts/Cargo.toml.
  4. Runs cargo test from <project_dir>/.perro/scripts.
  5. Sets CARGO_TARGET_DIR=<project_dir>/target so script tests share the project build cache.
  6. Enables the generated scripts crate steamworks feature when project Steam support is enabled.

Flags:

  • -- <cargo_test_args>: forwards remaining args to cargo test.

Examples:

perro test --path D:\GameProjects\MyGame
perro test --path D:\GameProjects\MyGame -- --lib -- --nocapture
perro test --path D:\GameProjects\MyGame -- player_state_tests

dev

Command:

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:

  1. Runs the same scripts build pipeline as check.
  2. With --target native or no --target, builds the project-local dev runner at <project_dir>/.perro/dev_runner.
  3. With --target native, launches the generated dev runner binary with your --path.
  4. With --target web, builds a wasm web bundle from .perro/project.
  5. 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. Default native.
  • --scene res://path.scn: boots this scene instead of the project's main_scene. Forwarded to the runner as PERRO_BOOT_SCENE. Use it to profile a heavy scene directly instead of landing on the project menu.
  • --headless: runs the native perro_headless dev path with no window, input, or GPU render loop. Native only; rejected with --target web or --target android, and cannot combine with --timings or --ui-profile.
  • --playtest: apply [playtest] overrides + exclusions; select Playtest App ID; split user:// saves under base name + _Playtest; enable playtest_include! + playtest_exclude!. Reject combo with --demo.
  • --demo: applies [demo] config overrides, skips excluded scripts/assets/scenes, strips tagged node trees, and enables demo_exclude!.
  • --tools: syncs and compiles *.tool.rs project modules and enables the perro-tools script feature. Off by default and native only. Normal dev/build skips their generated .perro/scripts files 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 runner ui_profile feature.
  • --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 as PERRO_SIM. Native only; a bad spec fails before the build. See Perf Simulation.
  • --host <addr>: web target only. Static server bind host. Default 127.0.0.1.
  • --port <num>: web target only. Static server bind port. Default 8000.

Android target notes:

  • --timings, --ui-profile, and --csv-profile are not supported with perro dev --target android yet.
  • Android dev builds require an installed Android SDK/NDK and a running emulator or device.

Web target notes:

  • --ui-profile is not supported with perro dev --target web yet.
  • --timings is not supported with perro dev --target web yet.
  • --csv-profile is not supported with perro dev --target web yet.
  • 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.

Capture & Offline Render

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-frames

Perro Capture API

Runtime 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.

build

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, winit frm 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_runner manifests

Steam-enabled headless builds use Steam GameServer API, not Steam client login.

  • anonymous login default
  • PERRO_STEAM_GSLT -> token login
  • PERRO_STEAM_GAME_PORT -> game port; default 27015
  • PERRO_STEAM_QUERY_PORT -> query port; default 27016
  • PERRO_STEAM_SERVER_IP -> bind IPv4; default 0.0.0.0
  • PERRO_STEAM_SERVER_NAME -> browser name
  • PERRO_STEAM_MAX_PLAYERS -> browser cap; default 64
  • PERRO_STEAM_LISTED=0 -> disable browser listing
  • PERRO_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:

  1. Runs script compilation, like check.
  2. Packs res assets through the static pipeline.
  3. Generates embedded project entry files under .perro/project.
  4. Optimizes supported assets into match tables and preparsed compile-time statics.
  5. Packs unsupported/generic assets into .perro/project/embedded/assets.perro.
  6. Builds the generated project crate in release mode from .perro/project.
  7. With --target native or no --target, copies the built native output to <project>/.output/. macOS builds use an unsigned .app bundle.
  8. 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. Default native.
  • --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 .app bundle.
  • --universal-macos: on macOS, builds aarch64-apple-darwin and x86_64-apple-darwin, then merges both slices into one unsigned .app with lipo. 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:

  • --console is not supported with perro build --target web.
  • web build uses stable wasm32-unknown-unknown + wasm-bindgen --target web.
  • web output includes index.html, boot.js, app.js, and app_bg.wasm.
  • see WASM / Web Target

Android target notes:

  • --console is not supported with perro build --target android.
  • Android builds require an installed Android SDK/NDK; the CLI resolves them from ANDROID_SDK_ROOT/ANDROID_HOME and ANDROID_NDK_ROOT/ANDROID_NDK_HOME/NDK_HOME or 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.

targets

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.

dlc

Command:

perro dlc --name <dlc_name> [--path <project_dir>]

What it does:

  1. Reads source from <project_dir>/dlcs/<dlc_name>/.
  2. Generates DLC scripts crate under .perro/dlc/<dlc_name>/scripts/.
  3. Generates DLC pack crate under .perro/dlc/<dlc_name>/pack/.
  4. Builds both runtime-loadable modules.
  5. Packs manifest, scripts module, pack module, and DLC resources into <project_dir>/.output/dlc/<dlc_name>.dlc.
  6. Compresses final .dlc when it reduces file size.
  7. Removes temporary .dlc.staging folder after successful pack.

Name rules:

  • self is reserved for dlc://self/... and is rejected as a DLC name.

New Projects And Templates

Use these commands to create projects, DLC folders, scripts, scenes, animation clips, and animation trees.

Shared rules:

  • --path resolves to a project root for every command except new.
  • new --path resolves to the parent directory that receives the new project.
  • Commands with --dlc <name> target dlcs/<name>/ instead of project res/.
  • --res accepts res://... or /... for base game content.
  • --res accepts dlc://<name>/... or /... for DLC content.
  • --no-open disables VS Code open for generated files.

new

Command:

perro new [--path <parent_dir>] [--name <project_name>]

What it does:

  1. Creates a new project directory under <parent_dir>.
  2. Writes default project files: project.toml, input_map.toml, deps.toml, AGENTS.md, README.md, res/main.scn, scripts scaffold, and .perro crates. AGENTS.md explains 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.
  3. 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.toml under [dependencies].
  • Perro merges deps.toml into .perro/scripts/Cargo.toml on check, dev, and build.

Examples:

perro new --path D:\GameProjects --name MyGame
perro new --name MyGame

new_dlc

Command:

perro new_dlc --name <dlc_name> [--path <project_dir>] [--no-open]

What it does:

  1. Resolves <project_dir>.
  2. Creates <project_dir>/dlcs/<dlc_name>/.
  3. Creates starter directories: scenes/, scripts/, materials/, and meshes/.
  4. Creates starter files: scenes/main.scn and scripts/script.rs.
  5. Uses dlc://<dlc_name>/scripts/script.rs in starter scene.

Name rules:

  • self is reserved for dlc://self/... and is rejected as a DLC name.

Examples:

perro new_dlc --name CosmeticsPack
perro new_dlc --name CosmeticsPack --path D:\GameProjects\MyGame

new_script

Command:

perro new_script --name <script_name> [--path <project_dir>] [--res <res_subdir>] [--dlc <dlc_name>] [--no-open]

What it does:

  1. Resolves <project_dir>.
  2. Resolves target root: project res/, or project dlcs/<name>/ with --dlc.
  3. Resolves <res_subdir> relative to target root.
  4. Creates a new *.rs script from the empty script template.
  5. Opens the new file in VS Code unless --no-open is passed.
  6. Rebuilds scripts after file creation.

Notes:

  • --name can omit .rs; extension is added automatically.
  • --name must 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-open

new_scene

Command:

perro new_scene --name <scene_name> [--path <project_dir>] [--res <res_subdir>] [--dlc <dlc_name>] [--template 2D|3D] [--no-open]

What it does:

  1. Resolves <project_dir>.
  2. Resolves target root: project res/, or project dlcs/<name>/ with --dlc.
  3. Resolves <res_subdir> relative to target root.
  4. Creates a new *.scn scene from the selected template.
  5. Opens the new file in VS Code unless --no-open is passed.

Notes:

  • --template defaults to 2D.
  • Generated scenes use $root = @main.
  • $root marks the scene root and can be reused as a node ref.
  • --name can omit .scn; extension is added automatically.
  • --name must 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-open

new_animation

Command:

perro new_animation --name <animation_name> [--path <project_dir>] [--res <res_subdir>] [--dlc <dlc_name>] [--no-open]

What it does:

  1. Resolves <project_dir>.
  2. Resolves target root: project res/, or project dlcs/<name>/ with --dlc.
  3. Resolves <res_subdir> relative to target root.
  4. Creates a new *.panim animation clip from the default animation template.
  5. Opens the new file in VS Code unless --no-open is passed.

Notes:

  • Defaults to res/animations when --res is omitted.
  • --name can omit .panim; extension is added automatically.
  • --name must 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-open

new_panimtree

Command:

perro new_panimtree --name <tree_name> [--path <project_dir>] [--res <res_subdir>] [--dlc <dlc_name>] [--no-open]

What it does:

  1. Resolves <project_dir>.
  2. Resolves target root: project res/, or project dlcs/<name>/ with --dlc.
  3. Resolves <res_subdir> relative to target root.
  4. Creates a new *.panimtree animation tree from the default animation tree template.
  5. Opens the new file in VS Code unless --no-open is passed.

Notes:

  • Defaults to res/animations when --res is omitted.
  • --name can omit .panimtree; extension is added automatically.
  • --name must 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-open

import_anim

Command:

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 30

Reject 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:

  1. Loads the glTF document.
  2. Selects one animation by --clip name or index.
  3. Converts translation, rotation, and scale channels into .panim keyframes.
  4. Writes node tracks as Node3D objects.
  5. Writes skin joint tracks as Skeleton3D bone tracks on --skeleton object.
  6. With --retarget-map, bakes bone aliases, rest-pose alignment, and translation policy.

Notes:

  • --clip defaults to 0.
  • --fps defaults to 60.
  • --skeleton defaults to Rig.
  • Scene or script bindings still map .panim object 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-rig reads 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.glb

Retarget 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 only root_bone translation 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.

Health And Maintenance

Use these commands to check references, run user script tests, format user scripts, lint user scripts, and remove build output.

doctor

Command:

perro doctor [--path <project_dir>]

What it does:

  1. Loads project.toml.
  2. Checks project.main_scene, project.icon, and project.startup_splash.
  3. Scans text assets under res/ and dlcs/ for quoted res:// and dlc:// references.
  4. Scans user scripts for likely missing res:// and dlc:// load paths.
  5. Warns when get_var!, set_var!, broadcast_var!, call_method!, or a signal connection references a name not found in any script state or methods! block, or targets a member that exists but is not pub (no dispatch glue is generated); the warning names the defining file. Also warns scene var private when a scene script_vars entry or .panim set_var event targets a non-pub state field β€” that value will not apply.
  6. Warns the reverse too: a pub state field or pub fn ctx method that nothing references dynamically β€” no var!/func!/method! literal, access-macro string, signal connection, scene script_vars entry on a node running that script, animation event, or even a plain string literal matching the name anywhere in script code β€” can drop pub to 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 a ScriptContext parameter never get glue, so pub on plain helpers is ignored; a call_method! aimed at one of those gets its own "not callable" warning instead.
  7. Warns when those dynamic calls target ctx.id and a typed self access path is available.
  8. Compiles every .wgsl under res/ and dlcs/ against the engine prelude and reports parse/type errors at the shader's own line and column.
  9. 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.

format

Command:

perro format --path <project_dir> [--dedup]

What it does:

  1. Resolves your path to that project's res root.
  2. Recursively finds format targets under res/**.
  3. Runs rustfmt on *.rs files.
  4. Formats *.scn and *.fur scene files.
  5. Formats key/value resource files: *.pmat, *.ppart, and *.uistyle.
  6. With --dedup, creates $varN values for large repeated scene values used 3+ times.

clippy

Command:

perro clippy --path <project_dir>

What it does:

  1. Resolves your path to that project's res root.
  2. Recursively finds all *.rs files under res/**.
  3. Syncs those files into .perro/scripts.
  4. Runs cargo clippy --all-targets -- -D warnings for the generated scripts crate.

clean

Command:

perro clean [--path <project_dir>]

What it does:

  1. Removes the project's target/ directory.

Profiling

Use these commands to record memory samples or produce flamegraphs from the dev runner.

Perf Simulation

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/potato also request the LowPower adapter, 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.

bench

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 as dev; recorded in run metadata.
  • --cpu-samples: additionally run cargo-flamegraph on the same capture, producing cpu-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.jsonl

Start 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.

mem-profile

Command:

perro mem-profile --path <project_dir> [--release] [--csv [csv_name]]

What it does:

  1. Runs the same scripts build pipeline as check.
  2. Builds the project-local dev runner with profile feature enabled.
  3. Launches dev runner with memory profiling enabled: PERRO_MEM_PROFILE=1.
  4. 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/.

Install

install

Command:

perro install

What it does:

  1. Adds/updates a perro shell function in your profile.
  2. On Windows, updates PowerShell profiles.
  3. On Linux, updates POSIX shell profiles: ~/.profile, ~/.bashrc, ~/.zshrc.
  4. 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