A port of the Visual Pinball X - Steam Frame Edition installer (steam-frame/installer/, in Go). It finds the Frame on the network, checks Developer Mode, signs in over its own ssh connection (ssh key, or the Developer Mode password asked once), checks the Frame is ready, shows the installed and latest versions, and installs, updates or removes CitraVR by running the release's install.sh on the Frame. tools/make-installer.sh builds it; make-release.sh attaches the four builds to each release. install.sh now records the installed release in ~/.local/share/citravr/VERSION. The README makes the app the main way to install. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(Beta)
Play 3DS homebrew and personal game backups in 3D on the go with your Quest.
CitraVR is a GPL-licensed, engineless OpenXR application with all source code publicly available.
Steam Frame Edition | Installing on Steam Frame | Compatibillty | Releases | Known Issues | How to Install and Run | Building | Discord | Need Help? | Support | License
This is a fork of amwatson/CitraVR, focused on making CitraVR run on the Valve Steam Frame. It was built with extensive help from AI and uses AI-generated content. It is a Steam Frame edition, installed into the Frame's Steam library as Citra VR - Steam Frame Edition, and doesn't target Quest. See the Steam Frame guide to build and install it. For Quest, use the original repository.
Steam Frame Edition
Click the image to watch the trailer on YouTube.
The primary goal of this fork is Steam Frame compatibility. The Frame runs Android apps through Lepton, Valve's Android container on SteamOS, with SteamVR as the OpenXR runtime. Stock CitraVR doesn't start there, because it depends on Meta-only OpenXR extensions that SteamVR doesn't offer. This fork makes those optional, replaces the ones CitraVR can't do without, and adds what's needed to use CitraVR comfortably on the Frame.
Status: experimental, running on a Steam Frame. It has been tested with SteamVR/OpenXR 2.18.2 and Lepton (Android 11):
- the app installs into the Steam library;
- games boot in VR, with the Frame controllers;
- the in-game menu and the in-VR game list work;
- the ROM upload server is reachable from the network;
- passthrough and the immersive modes work.
The APK targets Steam Frame only, not Quest.
Quick start (Steam Frame): turn on Developer Mode on the headset (with a password), then download the installer app for your computer (Windows, macOS, Linux) and open it. It finds the Frame on the network and installs or updates CitraVR in its Steam library, with its artwork. Details in Installing on Steam Frame.
What this fork adds
- Full Steam Frame compatibility: runs on SteamVR through Lepton without Meta's OpenXR extensions, with native Steam Frame controller bindings, passthrough, sharp panels and installation into the Steam library.
- Quality of life improvements:
- Controller input configuration: remap every 3DS button to any controller button, globally or per game, from the VR settings or in game.
- Controller models: your Steam Frame controllers are drawn in VR, with diagrams of the mapping in use.
- Beam to click: a pointer ray shows where you point, and pulling the trigger clicks right where you aimed.
- ROM upload server: add and delete games from any browser on your network, with drag-and-drop uploads.
The full list, with the technical details, is in What this fork adds.
Known limitations on Steam Frame:
- The curved screen and immersive modes fall back to the flat screen (no cylinder layers in SteamVR).
- There's no CPU/GPU clock control and no performance overlay.
- The library title keeps its own Android data, separate from Lepton Development.
Controls
Drawings made for CitraVR (steam-frame/make_diagram_art.py), labelled with its defaults by steam-frame/make_controls_image.py.
The 3DS-style button layout, for comparison: X at the top, A on the right, B at the bottom and Y on the left, as on a 3DS. In VR, this drawing is shown to the right of the game, opposite the controller diagram, and in the menu bar's Controls tab.
| Steam Frame | 3DS / CitraVR |
|---|---|
| A, B, X, Y (right) | B, A, Y, X (the Nintendo layout) |
| D-pad (left) | D-pad |
| Left stick / click | Circle Pad / Select |
| Right stick / click | C-Stick / Start |
| Bumpers | L, R |
| Triggers | Pointer click, or ZL, ZR (see below) |
| Grips | not mapped |
| View (left) | Home |
| Menu (right) | Show/hide the menu bar |
A/B and X/Y are swapped on purpose: the Frame's buttons follow the Xbox layout (A at the bottom, B on the right), while the 3DS has A on the right and B at the bottom (and X at the top, Y on the left). Swapping them puts each 3DS button where it sits on a 3DS, so the Frame's B presses the 3DS's A, A presses B, Y presses X and X presses Y.
All of this can be changed in Settings → VR → Controller mapping, also in game from the menu bar's Settings tab (see Controller mapping).
Controller diagram in VR
In VR, turn your head to the left: a panel shows the controllers labelled with the current mapping, and it follows any change you make (per game too). Turn your head to the right for the 3DS-style button layout. Show controller diagram in the VR settings hides both.
The panel as the app lays it out with the default mapping (rendered by steam-frame/make_panel_image.py).
Pointing and clicking
One controller is the pointer: it shows a ray and a dot where it points. The triggers do two jobs, depending on where their controller points when the trigger is pulled (the choice holds until it's released):
- On the bottom screen: the trigger touches the 3DS touchscreen where the dot is. Holding it and moving is a stylus drag.
- On a menu (the menu bar beside the bottom screen, the game list, the keyboard, a message): the trigger clicks. It clicks as soon as it's pulled, where you aimed just before the pull (the pull tilts the controller). Lists scroll with the thumbstick, not by dragging.
- Anywhere else (the top screen, the room): the trigger is a game button, ZL on the left and ZR on the right by default. It doesn't click.
Pulling the other controller's trigger while it points at the bottom screen or a menu makes that controller the pointer; pulled anywhere else, it doesn't take over the pointer.
Installing on Steam Frame
The installer app, run on any computer on the same network, installs the latest release on the headset: nothing to build, nothing else to install on the computer. The app, your ROMs and your saves then live on the headset.
1. Prepare the headset
- Turn on Developer Mode: on the Frame, open Settings > System and turn on Enable Developer Mode.
- Set a password: in the Developer section that appears, choose Set User Password and pick a password. The installer asks for it.
- Keep the headset awake while installing: when it sleeps it drops off the network.
2. Install with the installer app
| Computer | Download |
|---|---|
| Windows 10, 11 | CitraVR-Frame-Installer-Windows.exe |
| macOS 12 or later | CitraVR-Frame-Installer-macOS.zip |
| Linux (PC, Steam Deck) | CitraVR-Frame-Installer-Linux-x86_64.tar.gz |
| Linux (ARM64) | CitraVR-Frame-Installer-Linux-arm64.tar.gz |
The links always give the latest version; they're also on the Releases page. The app doesn't need updating with each new version of CitraVR: it always installs the latest one.
The app isn't signed with a paid certificate, so Windows and macOS warn the first time it's opened:
- Windows: double-click
CitraVR-Frame-Installer-Windows.exe. If Windows says "Windows protected your PC", choose More info, then Run anyway. (If the browser warns that the file isn't commonly downloaded, choose to keep it.) - macOS: open the
.zip(Safari may already have done it), then double-click CitraVR Installer. If macOS says it can't verify the app ("Not Opened"), choose Done, open System Settings > Privacy & Security, scroll down to Security, choose Open Anyway next to the app's name and confirm. When the app asks to find devices on local networks, choose Allow: it's how it reaches the Frame. - Linux: unpack the download and run the app, for example in a terminal:
tar -xzf CitraVR-Frame-Installer-Linux-x86_64.tar.gz && ./CitraVR-Frame-Installer. It opens in your web browser and quits when the page is closed.
The app has two steps:
- Connect to your Steam Frame. The app looks for the Frame on the network and shows it as found (
frame, with its address). Type the password from step 1 and choose Connect. Four checks turn green: Frame found on the network, Developer Mode is on, Signed in to the Frame and Frame ready for CitraVR (Steam is running, with enough free space). If one fails, the app says what to do under it, then choose Check again. If the Frame isn't found, type its IP address, shown on the Frame in Settings > Network (for example192.168.1.42). - Install or update CitraVR. The app shows the version on the Frame and the latest one. Choose Install (or Update to …). The Frame downloads the release from GitHub, checks it against the release's
SHA256SUMS, registers the APK with Steam as a title on the Lepton (Android) runtime (the same request Valve's devkit Title Upload sends), installs the library artwork, and names the entry Citra VR - Steam Frame Edition.
The app signs in to the Frame over ssh by itself, with your ssh key if the Frame accepts it, otherwise with the password; it remembers the Frame between runs. Its log, for help when something goes wrong, is shown on its page when an install fails.
Other ways to install
From a terminal on any computer (macOS, Linux, or Windows PowerShell), with the headset awake:
ssh steamos@frame.local "curl -fsSL https://github.com/SPD13/CitraVR-steam-frame/releases/latest/download/install.sh | bash"
Enter the Developer Mode password when asked. If frame.local doesn't resolve, use the Frame's IP address instead (ssh steamos@192.168.1.42 "…"). The installer runs on the headset and does what the app's step 2 does:
Downloading CitraVR (v1.1.1)...
registered as Devkit Game: citravr (Android runtime, Lepton)
Devkit Game: citravr: artwork installed for appid … in …/config/grid
steam-library: app id … is now 'Citra VR - Steam Frame Edition', 4 artwork images set
Done: 'Citra VR - Steam Frame Edition' is in your Steam library. …
Windows script: if the app can't run, download install-windows.cmd and double-click it (or paste irm https://raw.githubusercontent.com/SPD13/CitraVR-steam-frame/master/steam-frame/install-windows.ps1 | iex in PowerShell). It asks whether to install, update or uninstall, then for the headset's address (Enter for frame.local) and the Developer Mode password, and runs the same installer on the headset through Windows' ssh client. It offers to add the ssh client (OpenSSH Client) when it's missing.
3. First launch
-
In the headset, open Library → Non-Steam → Citra VR - Steam Frame Edition and press Play. The first start takes a little longer while Lepton prepares the app. If Steam warns that Valve is still learning about the title's compatibility, continue.
-
CitraVR's 2D window opens with a single question: Do you want to manually configure Citra directories? Answer No, use default folders. CitraVR creates its folders on the headset and opens the game list:
/storage/emulated/0/CitraVR: its data, saves and settings;/storage/emulated/0/CitraVR/games: your games.
Answer Yes, configure manually only if you want other folders. You then pick them in Android's folder picker.
-
Add your games. In CitraVR's left menu, open ROM server and turn on Enable ROM upload server. Server address shows a URL such as
http://192.168.1.42:8338. Open it in a browser on your computer, and drag your ROMs (.3ds,.cci,.cxi,.3dsx, …) onto the page. They appear in CitraVR's game list as soon as each upload finishes. Turn the server off when you're done: anyone on the network can use it while it's on. -
Play: pick a game in the list. It opens in VR.
In a game: press the right controller's Menu button to show or hide the menu bar below the screens. Close Game opens the Choose a game panel, where you can start another game, go back to the current one, return to the 2D window, or quit CitraVR. The controls are in Controls.
Updating and uninstalling
- Update: open the installer app again and choose Update to … (or run the install command again). It replaces the app and keeps its library entry, your folders, games and saves. If CitraVR is running, quit it and start it again. CitraVR uses its folders as plain paths on the headset's storage, so updates don't lose them (Lepton drops folder grants made with Android's picker on each update; folders you picked manually are switched to plain paths automatically when they're on the headset's storage).
- A given version: add
-s -- --version v1.0.0afterbash(releases are listed on the Releases page). - Uninstall: in the installer app, Uninstall CitraVR from the Frame… (or the install command with
bash -s -- --uninstall) removes the library entry, its artwork and the files in~/devkit-game/citravr/. Your data (folders, saves) stays on the headset.
Building from source
For development. On macOS, Linux, or Windows with Git Bash or WSL, with Git, JDK 17, the Android SDK (platform 35, build-tools, CMake 3.25+, NDK 27.3.13750724) and about 10 GB of free space:
git clone --recurse-submodules https://github.com/SPD13/CitraVR-steam-frame.git
cd CitraVR-steam-frame/src/android
export ANDROID_HOME=/path/to/android/sdk # or put sdk.dir=... in local.properties
./gradlew assembleRelease # ~10 minutes the first time
cd ../.. && ./register-frame.sh # installs it on frame.local (FRAME_HOST=<ip> otherwise)
register-frame.sh copies the APK and the files in steam-frame/ to the headset and runs the same installer there; ./register-frame.sh --uninstall removes it. To avoid typing the password at each step, copy your ssh key once: ssh-copy-id steamos@frame.local. Switching between your own builds and releases needs nothing special: Lepton installs the APK afresh at each launch and keeps the app's data.
Alternative: install with adb
For development, you can sideload the APK into Lepton Development instead: start Lepton Development from the Steam library, then run adb connect frame.local:5555 and adb install -r app-release.apk. Apps installed this way run inside the Lepton Development window and have separate data from the library entry. See STEAM_FRAME.md.
Troubleshooting
| Problem | Fix |
|---|---|
ssh: Could not resolve hostname frame.local, or (Windows installer) frame.local wasn't found / no answer |
The headset is asleep or on another network. Wake it, or use its IP address. |
(Windows installer) doesn't accept ssh logins |
Developer Mode is off. Turn it on in Steam Settings → System. |
(Windows installer) no ssh client |
Add OpenSSH Client in Settings → System → Optional features → Add a feature, then run the installer again. |
| macOS: the installer app is "Not Opened" | System Settings > Privacy & Security > Open Anyway (see Install with the installer app). |
| macOS: the installer app finds no Frame, or gets no answer from it | Allow it in System Settings > Privacy & Security > Local Network, then choose Check again. |
Permission denied (publickey,password) |
Developer Mode or its password isn't set. Turn it on in Steam Settings → System. |
could not download … |
The headset isn't online, or GitHub is unreachable. Check its Wi-Fi and run the command again. |
Steam is not running on the Frame |
Put the headset on so Steam is running, then run the installer again. |
Steam did not answer / Steam refused |
Developer Mode is off. Turn it on and run the installer again. |
| The entry is called Devkit Game: citravr | The renaming step couldn't reach Steam. Run the installer again. |
| No artwork in the library | Run the installer again, or restart the headset. |
| The ROM upload page doesn't load | Check the server is on and the address matches the one on the ROM server screen. The computer must be on the same network. |
| After Play, only the Steam interface shows | The SteamVR dashboard is in front: choose Resume, or close the dashboard. |
Releases (maintainers)
tools/make-release.sh vX.Y.Z tags the commit, builds the APK signed with the project's release key, checks the signature, and packages the release files (the APK, install.sh, install-windows.cmd, the steam-frame/ files, the installer apps and SHA256SUMS) in build/release/vX.Y.Z; with --publish it pushes the tag and creates the GitHub release. The installer apps are built by tools/make-installer.sh (Go, on a Mac) from steam-frame/installer/, adapted from the Visual Pinball X - Steam Frame Edition installer; the same apps serve every release, since they install the latest one. The release key's certificate SHA-256 fingerprint is 08:AF:DA:F9:C8:B2:0E:4D:9F:01:CF:1A:D4:78:0D:2A:28:E2:DD:36:69:B6:29:EB:C6:E8:6A:F5:E8:C3:98:84.
Every release must be signed with this same key, or it won't install as an update over the previous one where Android checks it (Lepton Development, Quest; the Steam library entry reinstalls at each launch). The key (~/.config/citravr-steam-frame/release.keystore, with its password in the macOS Keychain item citravr-steam-frame-release-key) is never committed: keep a backup of both outside this computer, as described in STEAM_FRAME.md.
Original CitraVR README
Everything below is the original upstream README from amwatson/CitraVR. It describes the Quest version, and its releases, guides and support links lead to the upstream project. Only the modification notice under License was added.
Introduction
I originally created this project to a be a proof-of-concept of some techniques XR developers were curious about. Specifically:
- Building a 2D/3D hybrid app
- Creating a VR app to render 2D interactive windows without an engine
- Using VR layers to get sharp, crisp text and visuals.
CitraVR began as a Quest-native (i.e. OpenXR, without a third-party game engine) port of the Citra 3DS emulator. It seemed like a great and fun way to demonstrate all these things at once.
The original Citra project is now defunct. Azahar was built on Citra's foundation and continues its development. CitraVR has since incorporated the changes from Azahar and now uses Azahar as its upstream emulator project.
The project is still small, but I'm looking for ways to improve it as time goes on.
Features
- Stereoscopic rendering
- Broad controller support
- Large, moveable/resizeable screen
- Playable in mixed reality
- Low-overhead port
- Fully GPL-licensed, 100% independent of the Meta SDK
Compatibillty
HMDs
CitraVR supports the following devices:
- Meta Quest 2
- Meta Quest Pro
- Meta Quest 3
- Meta Quest 3S
Games
For a full list of games that work well on CitraVR, please visit the CitraVR Game Compatability List
Controllers/Input
Touch Controller Input Bindings Diagram
For games that need access to more inputs, or if a player needs to access more inputs faster, CitraVR also supports a multitude of 3rd party wired USB and wireless bluetooth controllers.
Releases
Grab the latest release here
Known Issues
See the CitraVR Known Issues
How to Install and Run
Building
Discord
Join the Flat2VR discord and from there join cvr-join to get access to the CitraVR community and support forums
Need Help?
Please check our Troubleshooting and Known Issues pages to see if your issue is listed. To file a bug report or a feature request, please submit an issue. Otherwise, follow the instructions for Discord and post in cvr-support
Support
License
CitraVR is licensed under the GPLv3 (or any later version). Refer to the LICENSE.txt file.
Modified by SPD13 for Steam Frame, 2026. This fork is distributed under the same license.



