Skip to content

Build and Deploy

Alex Coulombe edited this page Jun 8, 2026 · 3 revisions

Build and Deploy

This is the loop you'll run dozens of times: change code → see it on the headset. Once the engine is built, a full round-trip is about 3–4 minutes.

One-time setup (building the engine, ~30–90 min) is covered in the repo README → Building the engine. This page is the repeating loop afterward.

⚡ The easy way: ./build.sh sim or ./build.sh device

The companion godot-visionos-simulator-kit (extracted from this project — it pairs great with Cascade Countdown) ships a build.sh switcher that runs the whole loop with one command and flips between the Simulator and a real device for you (they differ in the xcodebuild -destination, code-signing, and install tool — exactly the tedium it hides). It also bundles the simulator input + hand-tracking tooling:

./build.sh sim      # build + run in the visionOS Simulator (recommended for prototyping)
./build.sh device   # build signed + install on a paired Apple Vision Pro
./build.sh export   # just re-export the .pck

Project-specific values (device/sim UDID, signing team, bundle id, paths) live in the CONFIG block at the top of build.sh — override via env var or a sibling build.config — so the script is reusable across Godot visionOS projects. The four steps below are what it does under the hood (and what to run by hand when you want to understand or tweak a step).

🥽 Simulator vs device — prototype in the Simulator

Iterate in the visionOS Simulator (./build.sh sim); reach for a device only when you need hand tracking, final perf, or TestFlight. The Simulator shows the real spatial render — unlike running the project in desktop Godot, which renders broken XR visuals (audio only) and misleads. GDScript/.pck changes need no engine rebuild — just re-export + rebuild the app for the sim. The differences the switcher handles for you:

sim device
xcodebuild -destination platform=visionOS Simulator,id=… platform=visionOS,id=…
Code signing CODE_SIGNING_ALLOWED=NO CODE_SIGN_IDENTITY + DEVELOPMENT_TEAM
Install / launch simctl install + simctl launch devicectl device install + tap by hand
Engine slice (auto-picked from the xcframework) xros-arm64-simulator (Clancey, renders) xros-arm64 (your hand-tracking lib)
Hand input none in-sim (a round auto-starts so audio is audible) real ARKit hands

Gotcha: an immersive sim app suspends when backgrounded — keep the Simulator window frontmost or the scene (and its audio) pauses.

The four steps

 1. edit GDScript     2. export .pck        3. xcodebuild        4. install + tap
   (main_v2.gd)   ──►  "pack the game"  ──►  "wrap in an app" ──►  "put on headset"

Step 1 — Edit

Change test-project/main_v2.gd (or a sandbox/ actor, or project.godot).

Step 2 — Export the .pck (the "cartridge")

This asks the Godot editor to pack your project into one file the app will run.

~/godot-visionos-pilot/Godot.app/Contents/MacOS/Godot --headless \
  --path /Users/you/godot-visionos-pilot/test-project \
  --export-pack "visionOS" \
  /Users/you/godot-visionos-pilot/out/xcode-visionos/GodotVisionPilot.pck
Piece ELI5
--headless run the editor with no window — just do the job and quit
--path …/test-project which project to export
--export-pack "visionOS" use the export preset named "visionOS"
the last path where to write the .pck

🔥 The #1 footgun here: the output path must be absolute. A relative path resolves from the project directory, not your shell — so the new .pck lands somewhere you're not looking, Xcode rebuilds the old one, and you swear your change "did nothing." Always pass a full /Users/... path.

Step 3 — Build the app with Xcode

This wraps the .pck + libgodot.a into a signed Vision Pro app.

xcodebuild \
  -project out/xcode-visionos/GodotVisionPilot.xcodeproj \
  -scheme GodotVisionPilot \
  -configuration Debug \
  -destination "platform=visionOS,id=<YOUR_DEVICE_UDID>" \
  CODE_SIGN_IDENTITY="Apple Development" \
  DEVELOPMENT_TEAM="<YOUR_TEAM_ID>" \
  build

Replace <YOUR_DEVICE_UDID> (find it in Xcode → Window → Devices and Simulators) and <YOUR_TEAM_ID> (your Apple Developer team). Signing is required — Vision Pro won't run an unsigned app.

Step 4 — Install, then tap

xcrun devicectl device install app \
  --device <YOUR_DEVICE_UDID> \
  $(ls -d ~/Library/Developer/Xcode/DerivedData/GodotVisionPilot-*/Build/Products/Debug-xros/GodotVisionPilot.app)

Then put on the headset and tap the app icon. You can't remote-launch an immersive app (see Gotcha #6) — so the loop always ends with a human tap.

Did it actually work? (the 90 FPS check)

The app writes a diagnostic frame count you can pull back off the device:

xcrun devicectl device copy from \
  --device <YOUR_DEVICE_UDID> \
  --source "Documents/xr_diag.txt" \
  --destination /tmp/xr_diag.txt \
  --domain-type appDataContainer \
  --domain-identifier com.agilelens.godotvisionpilot

The per-5-second frame delta should be exactly 450 — that's 90 FPS × 5 s, locked. Anything less means a frame-rate wall was hit; back off whatever you changed last.

When the build misbehaves

Symptom Likely cause Do this
App runs old code relative .pck path (Step 2) use an absolute output path, re-export
devicectl install → "unable to locate device" the AVP sleeps when you take it off put it on, retry 2–3×
App opens to passthrough only, no scene script failed to load, or editor/lib version mismatch run headless first to catch script errors; see Gotcha #5
Renders nothing at 90 FPS an XR-setup gotcha run the pre-flight checklist

Capturing what you see

To record or screenshot the headset's view at full quality, attach the Developer Strap and use Xcode → Window → Devices and Simulators → (select AVP) → Take Screenshot / Record Screen. (The hero clip and stills in this wiki were captured that way.) Without the strap, AirPlay-mirror to a Mac and record in QuickTime — but you'll get one eye at ~30 fps.


Back to Getting Started · or browse Architecture, Hand Tracking, Procedural Audio.