No description
  • Shell 95.7%
  • Just 2.3%
  • CMake 2%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-09-28 15:56:43 -04:00
docs Snapshot before Monado shell work 2026-09-28 12:26:30 -04:00
initramfs Initial platform import 2026-09-27 15:04:46 -04:00
rootfs Bind the Frame radio safely and grant active-seat SPI access 2026-09-28 15:44:44 -04:00
scripts Bind the Frame radio safely and grant active-seat SPI access 2026-09-28 15:44:44 -04:00
.gitignore Snapshot before Monado shell work 2026-09-28 12:26:30 -04:00
config.zsh Build Frame Monado runtime from workspace submodule 2026-09-28 12:48:22 -04:00
justfile Boot Monado and install the tracked shell environment 2026-09-28 13:04:39 -04:00
README.md Record verified controller handedness and button events 2026-09-28 15:56:43 -04:00

AmbitOS

A custom Linux distribution for the Steam Frame. It reuses Valve's kernel, device trees, modules and firmware unchanged and owns everything from the initramfs onward:

  • Initramfs: initramfs/init, a busybox script that finds the root filesystem by UUID and hands over to systemd.
  • Userspace: Arch Linux ARM + systemd; greetd starts Monado and the ambit-compositor OpenXR overlay/Wayland shell, with foot and the ARM64 Flutter dashboard. Monado owns Vulkan/Turnip panel presentation; Ambit uses libseat/logind and libinput for keyboard/pointer input. No outer Sway or software-renderer fallback.
  • Boot: Valve's boot firmware boots AmbitOS from a USB drive through its built-in "Boot from USB" menu entry: it starts Valve's U-Boot from the drive's esp partition, which loads Valve's kernel and the AmbitOS initramfs from rootfs-A. The Frame's internal storage is not touched.

Platform notes and how the boot chain works: docs/frame-platform.md.

XR runtime status

Monado's frame driver reads the calibrated head IMU from Valve's root frame-dsp.service; SteamVR is not required. This is 3DoF orientation tracking: no tracked translation or controller poses. IPD defaults to 64 mm (FRAME_IPD_MM). Ambit's desktop and dock are world-locked OpenXR quad overlays, re-centred upright in front of the head whenever shown. The headset Steam button (KEY_SELECT, 353) or F6 toggles the dashboard; F5 toggles the dock. Controller Steam clicks are wired through Monado's buttons-only simple-controller profile and XR_MNDX_system_buttons. Live handedness and Steam/select/menu events passed an ARM64 driver probe with SteamVR stopped; on-headset shell toggling remains unverified.

The session selects /usr/share/openxr/1/openxr_monado.json through XR_RUNTIME_JSON. It starts Monado before Ambit and stops it when Ambit exits. The default Vulkan display-mode index is 4 (90 Hz on the measured Frame); AMBITOS_DISPLAY_MODE overrides it. Check Monado's printed mode list rather than assuming another device enumerates modes identically.

Launch Ambit Environments from the dock. It is a normal OpenXR session beneath Ambit. libmonado primary-session monitoring hides the shell when an app becomes primary and shows/re-centres it when the last primary app exits. Manual Steam button toggles remain available while an app runs. No simulated tracking fallback is built into the ARM64 runtime.

frame-radio.service binds the specific spi0.1 device to spidev before login, without flashing firmware, touching GPIO or unbinding another kernel driver. The narrow udev uaccess rule grants the active seat access to spidev0.1. This uses Roy firmware already provisioned by stock SteamVR; cold-boot radio operation still needs hardware verification. An unavailable radio leaves the tracked HMD usable, with no fabricated controller input or poses.

Layout

Path Purpose
config.zsh Pinned Valve package versions and hashes, user, hostname, image size
initramfs/init The initramfs /init
rootfs/packages.txt Packages added to the Arch Linux ARM base
rootfs/setup-chroot.sh Package and user setup, run inside the aarch64 rootfs
rootfs/overlay/ Files copied into the rootfs (greetd, standalone session launcher, ...)
justfile Native platform module; default lists recipes without running them
scripts/ Executable host zsh tooling; frame-nopasswd.sh remains target-side Bash

The platform repository can be checked out independently. Commands and relative paths below are from its checkout. Build output defaults to <platform>/build/ (ignored by git), overridden with AMBITOS_WORK_DIR. Private calibration, Wi-Fi profiles and firmware backups default to <platform>/storage/, overridden with STORAGE_DIR; they are not disposable build output. Relative overrides resolve against the invocation directory. AMBITOS_DIR points to the platform source.

Run just here to list recipes, or just platform from the repository root. For example, just -- build-rootfs --frame-secrets "/path/with spaces/frame" here is equivalent to just -- platform build-rootfs --frame-secrets "/path/with spaces/frame" at the root. Arguments are forwarded positionally, without shell interpolation. The executable scripts/*.zsh entry points also work from any working directory; relative input/output arguments remain relative to the invocation directory. just check checks host zsh and target Bash/POSIX syntax only: no sudo, downloads, builds, image writes, GUI, or hardware access. Target helpers and the initramfs retain their existing Bash/BusyBox shells; zsh is only a workstation dependency.

Requirements

On the workstation (Arch Linux x86_64): just with native modules and set default-list (1.58.0), zsh (5.9.2), passwordless sudo, qemu-user-static with the aarch64 binfmt handler, systemd-nspawn, curl, bsdtar, zstd, uboot-tools (mkimage), dosfstools, mtools, e2fsprogs, jq, python-pillow, and qemu-system-aarch64 for the boot smoke test. Building ambit-compositor also requires Rust/Cargo with the aarch64-unknown-linux-gnu target, aarch64-linux-gnu-gcc, and glslangValidator. Monado additionally needs the matching cross C++ compiler, CMake 3.24+, Ninja, Git, Python, and pkg-config. The isolated Flutter builder needs rsync, readelf, flock, and an enabled qemu-aarch64 binfmt handler with the F (fix-binary) flag.

On the Frame: SSH access through the frame host alias. Root-only steps use passwordless sudo, toggled by a deployed copy of scripts/frame-nopasswd.sh. Deploy the current helper explicitly before using it. The historical copy at ~/Projects/xer-os/frame-nopasswd.sh on the Frame and its old sudoers rule remain unchanged; use that historical copy to disable its own rule rather than expecting the renamed helper to manage it.

Building

Fetch the stock Frame's private DSP binaries and per-device calibration:

just fetch-frame-vendor

This uses FRAME_HOST (default frame) over SSH, without sudo, and stores dsp/{dsp_service,adsp_sm8650.mbn,cdsp_sm8650.mbn}, device_config.json, xrservice.json, and SHA256SUMS under $STORAGE_DIR/secrets/frame. Controller investigation inputs are staged under controller/: the complete frame_controller/ resource tree, roy/ radio firmware/scripts, and driver_cv.so. Stock frame_controller is resource-only; the actual Roy SPI implementation is in driver_cv.so, not a separate controller library. Files are mode 0600; keep them out of Git. Changed inputs are refused unless --force is passed, with every file checked before replacement. Verify them with sha256sum -c SHA256SUMS in that directory. Fetching never executes vendor scripts, flashes the radio or changes pairing. The rootfs builder requires the five DSP/calibration files before any build work; controller artifacts are private analysis inputs, not installed runtime support. Use --frame-secrets /path/to/frame to select another private input directory.

just backup-stock-firmware           # once per SteamOS build: reference copy of the Frame
just -f ../justfile environments export-frame # Godot 4.7 ARM64 executable and PCK
just -- build-rootfs --wifi-from-frame
just make-image                      # <platform>/build/ambitos.img
just qemu-test                       # optional boot/SSH smoke; not VR rendering
just -- write-usb /dev/sdX            # erases the drive, grows rootfs-A to fill it
  • build-rootfs.zsh downloads the Arch Linux ARM tarball and Valve's pinned kernel/firmware/U-Boot packages (fetch-valve-packages.zsh; just -- fetch-valve-packages --latest shows newer pins), installs packages under qemu-user, cross-compiles ambit-compositor and the workspace Monado fork against that userspace, and builds the ARM64 Flutter bundle. It installs private DSP firmware/calibration, the exported Environments app and session overlay, authorizes your ~/.ssh/id_*.pub keys for xer, and builds /boot/initrd.uImage.
  • make-image.zsh lays out esp (Valve U-Boot at /uboot/u-boot.img plus its firmware files), efi-A, and rootfs-A.
  • --wifi-from-frame copies the Frame's active NetworkManager Wi-Fi profile (export-frame-wifi.zsh) so AmbitOS joins the same network. The profile holds the Wi-Fi password; it is stored under $STORAGE_DIR/secrets/ and in the image, never in git.
  • The default login remains xer / xer (AMBITOS_PASSWORD in config.zsh); this is user identity, not OS branding. Configuration overrides use AMBITOS_*; old XEROS_* names are not aliases.
  • qemu-test.zsh boots the image with a generic Arch Linux ARM kernel (Valve's has no virtio drivers) and checks SSH login. It masks greetd in that VM: virtio-gpu/TCG cannot exercise the Frame's calibrated Vulkan direct-display path. New screenshots go to build/qemu/screen.png. Existing historical boot-console evidence remains at ~/Projects/steamframe/storage/xer-os/qemu/screen.png, not at the new path. Neither is a compositor test; rendering, input, and VT lifecycle require a Frame run.

The rootfs builder runs $AMBITOS_COMPOSITOR_DIR/scripts/build-frame.zsh. AMBITOS_COMPOSITOR_DIR defaults to a sibling compositor/ checkout; set it explicitly when the repositories are elsewhere. This dependency is validated before downloads or rootfs replacement. Cross-compilation uses AMBITOS_SYSROOT=$WORK_DIR/rootfs and Cargo output at <compositor>/build (overridden with CARGO_TARGET_DIR), then installs /usr/local/bin/ambit-compositor. For a standalone compositor build against the existing historical rootfs, use AMBITOS_SYSROOT=$HOME/Projects/steamframe/storage/xer-os/rootfs.

scripts/build-monado.zsh cross-builds the workspace's openxr/monado submodule (override with MONADO_SOURCE_DIR for standalone builds), using only target libraries and host build tools. It enables the Frame driver and OpenXR overlays and stages the runtime under $WORK_DIR/monado/install. Source changes are built in place; the script never fetches or switches revisions. The rootfs builder uses the workspace submodule and does not select a separate revision.

scripts/build-systemui.zsh uses a separate $WORK_DIR/systemui/rootfs copy for native ARM64 compilation under QEMU/nspawn. It bootstraps the official Flutter source tag from System UI's .fvmrc, enforces the application's lockfile, and keeps the compiler, SDK and package cache out of the shipped rootfs. The complete bundle is installed at /usr/local/lib/ambit-systemui; the login launcher passes its executable with --system-ui. The workstation SDK and x64 build are untouched. AMBITOS_SYSTEMUI_DIR defaults to the workspace's apps/systemui; AMBITOS_TOMEUI_DIR defaults to the sibling tomeui checkout beside the workspace. Both helpers accept --sysroot PATH --output EMPTY_DIRECTORY; the Flutter helper requires root. The full rootfs builder manages their staging directories.

Existing private files are not moved automatically. Use --frame-secrets for an existing Frame input directory, or set STORAGE_DIR to a private storage tree containing secrets/frame/. Changing AMBITOS_WORK_DIR only changes build output.

New images use hostname/OS ID ambitos, label ambitos-root, and image ambitos.img. The root UUID and optional early modules are stored under /etc/ambitos/; initramfs overrides are ambitos.root= and ambitos.rootwait=. Hardware boot ABI names esp, efi-A, rootfs-A, and Valve's steamos.efi= remain unchanged.

This source rename does not rename the deployed headset, existing images, rootfs trees, calibration, or backups under ~/Projects/steamframe/storage/. The new identity and session paths require an explicit new build and redeploy; there are no legacy compatibility shims.

Stock firmware reference

backup-stock-firmware.zsh stores the Frame's stock software under storage/stock-firmware/<id>-<version>-<build>/: /boot (Valve kernel, DTBs, initrd, U-Boot images, boot firmware tarball), zstd raw images of the boot firmware LUNs (sdb, sdc), syspersist (sdd), esp, efi-A/B, var-A and rootfs-A, plus system info (live FDT, kernel config, dmesg, GPT dumps, package list). The LUN and syspersist images contain device-specific data (calibration, keys); keep them private. Existing reference copies remain under ~/Projects/steamframe/storage/stock-firmware/; new backups default to this platform repository's storage/stock-firmware/, overridden with STORAGE_DIR.

Booting on the Frame

  1. Attach the USB drive to the Frame (use a hub if the charger shares the port).

  2. Power the Frame on while holding the button the device tree labels Select to reach the Deckard boot menu, then choose Boot from USB and the drive (U-Boot lists it as USB: <vendor> <product> [<size> GB]).

  3. A newly built and deployed AmbitOS comes up on Wi-Fi as ambitos.local (mDNS): ssh xer@ambitos.local. Monado owns the panel; Ambit starts on tty1. The session requests 90 Hz and Monado applies calibrated optical distortion. Read both logs over SSH:

    ssh xer@ambitos.local 'cat ~/.local/state/ambitos/ambit-compositor.log'
    ssh xer@ambitos.local 'cat ~/.local/state/ambitos/monado-service.log'
    

    Ctrl+Alt+Escape exits to the text greeter; Ctrl+Alt+Fn switches VT. To restart the autologin session from SSH, stop greetd, remove its /run/greetd.run once-per-boot marker, and start greetd again. See ambit-compositor's recovery instructions. Ordinary grim screenshots are not supported; there is no outer compositor.

    greetd launches /usr/local/bin/ambitos-session with XDG_CURRENT_DESKTOP=AmbitOS. Both calibration files are installed under ~/.config/ambitos/frame (respecting XDG_CONFIG_HOME). The dashboard is /usr/local/lib/ambit-systemui/ambit_systemui; Environments is /usr/local/lib/ambit-environments/ambit-environments.

Removing the drive and rebooting returns to SteamOS.

For subsequent iterations, just deploy copies built artifacts and invokes interactive sudo on AMBITOS_HOST (default xer@ambitos.local). It installs the DSP/radio units, their login ordering, the radio helper and seat-access rule, and calibration as well as binaries. It updates Godot's Wayland dependencies, reloads the targeted radio device's udev rules, restarts DSP/radio setup, and restarts greetd after clearing its autologin marker. Use the same AMBITOS_WORK_DIR, CARGO_TARGET_DIR, and STORAGE_DIR overrides used for the builds/fetch. Environments always comes from the workspace's storage/environments/frame, matching its export script, not the platform's private STORAGE_DIR. Deployment does not write partitions or the stock OS.

The menu and USB boot flow come from decompiling Valve's SPL and U-Boot; see docs/frame-platform.md.

When a boot fails

Watch the headset while it boots:

  • Stock blue/purple logo, then SteamOS: the boot firmware did not start U-Boot from the drive (stage 1).
  • Orange/red logo (the drive's recoloured copy): U-Boot from the drive is running. If SteamOS follows, U-Boot could not load the kernel files or the kernel did not start.
  • Text lines starting with ambitos-initramfs:: the AmbitOS initramfs is running. On failure it lists block devices, the USB-C data role and USB devices, holds that for 5 minutes, then reboots.

After a failed boot and warm reboot into SteamOS, the AmbitOS kernel log may be in /sys/fs/pstore/ (root only). Once the root filesystem is mounted, the journal persists on the drive: mount rootfs-A on the PC and run journalctl -D <mountpoint>/var/log/journal. Session logs are in /home/xer/.local/state/ambitos/ (respecting XDG_STATE_HOME).