frame-controller-models.py converts the Frame controller render models SteamVR keeps on every Frame into the game's WCC1 controller format (grip space, baked AO texture, nine animated controls, tutorial landmarks), and the launch wrapper puts them in place of the Touch Plus files. They looked right in the headset but cost too much: ~7x the vertices, re-animated on the CPU each frame, and the menus fell to 45-60 fps. install.sh therefore only converts them with WCF_FRAME_MODELS=1. README documents that and change 8 (grips hold the wheel in the cockpit). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N7dBRsvawKKn1F9gf68SYv
Mario Kart Wii VR on the Steam Frame
This is a fork of heurazy/Wiicompiled_VR-PLUS, the Mario Kart Wii VR port, that runs standalone on Valve's Steam Frame through Lepton (the Frame's Android compatibility tool). Upstream's Quest APK installs on the Frame but crashes the moment the game starts; with the changes here it runs at 90 Hz with the Frame controllers laid out like a normal gamepad, straight from the Steam library, no PC involved.
The branch to use is steam-frame. Everything the fork adds lives in steam-frame/
plus six small commits to runtime/ and aurora-main/. Upstream's own README is kept as
README.upstream.md; read it for what the port itself does, this file is only
about the Frame.
Status, October 2026: working on my Steam Frame (SteamOS 0.4.5, Lepton 2.8.11, SteamVR), in the original and first-person cameras, single player. See What works and what doesn't before you spend an afternoon on it.
The honest part: an AI wrote this
I did not write the code in this fork, and I did not write this README either. Both were produced by Anthropic's Claude (the models called Claude Opus 5.5 and Claude Fable 5.1), running through Claude Code on the Steam Frame itself over SSH. What I did was decide what to try, launch every build in the headset, play it, and report back what I saw: "it's a flickering mess", "the menus are a slideshow", "the item box stays on screen", "that resolution change made it stutter, roll it back". The AI read the logs, read the code, made the change, and we went round again. Everything in the status and testing sections below comes from those rounds on real hardware, not from reasoning about the code.
So: this is AI-generated code, by someone who could not have written it by hand, published because it works and because I think it proves something worth proving, that a Wii game can run natively in VR on a Steam Frame at 90 Hz without streaming. Treat it accordingly. Read the diffs before you trust them. Nobody from the upstream project has reviewed any of it, and I asked the AI to say so plainly rather than dress it up. The How this was made section at the end has the longer version, including the things it got wrong along the way.
If you want a project that is honest about the same thing in a much funnier way, read the README of baijiSteamVR; it is where I took the idea of just saying it out loud.
What is different from upstream
Upstream ships three things: a Windows build, a Quest APK, and the design where your game is
compiled on your own device from your own disc: the APK carries a "game kit" (precompiled runtime
objects, headers, a toolchain) and the headset translates and links libmain.so from your disc
against that kit. No Nintendo code is ever in the APK or in this repository, and that stays true
here.
This fork changes the kit, not the design. The Steam Frame APK is upstream's v1.3.1 Quest APK with
four kit files swapped for ones compiled from the patched sources, one patched classes.dex, and a
new kit fingerprint. When the headset builds your game it links it against the Frame objects, so you
get the Frame version from the same disc import and build that upstream documents.
What the patches do, in the order they were needed:
-
It starts. Upstream's app registers a clipboard listener when SDL comes up. Lepton's Android image has no clipboard service, the lookup returns null, and the game process dies with a
NullPointerExceptionbefore it draws anything. The ten bytes of that call are replaced with nops inclasses.dex(steam-frame/dexpatch.py). That is the whole reason the stock APK "doesn't work" on the Frame. -
90 Hz. Upstream deliberately disables VR frame interpolation on Android (its README says so: it "makes the standalone presentation unstable" on Quest). On the Frame that leaves the game presenting its 60 fps simulation into a 90 Hz headset, which looked like judder and ghosting whenever I turned my head. The Android runtime overrides that forced it off are removed, so Aurora's stereo replay renders the eyes at the headset's refresh rate with a fresh head pose each frame, from the game's 60 fps. SteamVR's OpenXR inside Lepton has no
XR_KHR_convert_timespec_time, which the pacing needs, so there is aCLOCK_MONOTONICfallback that anchors on the first frame. (runtime/src/vr/openxr_integration.cpp;setprop debug.wiicompiled.vr_interp 0|1|72|90|120overrides it per launch.) Races hold 90 fps on the counter, with frames submitted well inside the budget. Not perfectly smooth, see below. -
Frame controllers. The game's actions are hand-specific OpenXR actions written for Quest Touch and SteamVR had auto-mapped them onto the Frame controllers so that A did three things and B, X and Y did nothing. The runtime now also creates hand-independent actions (
frame_item,frame_drift,frame_look_back,frame_start,frame_back, a D-pad), a SteamVR binding file assigns the Frame's buttons to them, and when SteamVR reports them bound the game switches to a gamepad-style layout, the same in every camera (no cockpit "A becomes drift" swap). The layout latches for the session, because SteamVR briefly reports the actions inactive when a controller dozes and you do not want the controls changing mid-race. (runtime/src/vr/openxr_input.cpp,steam-frame/bindings_frame_controller.json.) -
Menus at 90 instead of 40. Races were fine but the menus, which upstream draws on a spatial panel with an animated "dielectric" raymarched background, ran at 37 to 45 fps with each frame taking 15 to 24 ms to submit. Turning the background quality to 0 upstream would have moved the menus to a quad layer, which under Lepton's SteamVR bridge is a slow submission of its own. On Android the menus are now always drawn into the eye images, and quality 0 means a plain dark background, which costs nothing. (
openxr_integration.cpp;menu_shader_quality = 0in the config.) -
The item box (attempted, does not work yet). In VR the item roulette slides up after use and stays visible. Aurora's virtual-screen HUD draws ignore the game's scissor rectangle, so the port now skips a 2D draw that lies entirely outside a scissor the game narrowed, which is what hides it on a TV. (
aurora-main/lib/gfx/common.cpp.) In the headset the empty box still shows, so either the box is not drawn the way that check assumes or the scissor is not what hides it. The change is harmless and stays in until someone finds the real cause. -
Settings that suit the Frame, seeded into
Config.tomlon the first launch by the wrapper (steam-frame/Config.frame.toml): interpolation on auto, menu background quality 0, hand steering on (see change 8), render scale 1.0 and resolution multiplier 1.0. In SteamVR the app getspreferredRefreshRate 90,framesToThrottle 0andadditionalFramesToPredict 0; those went in together with the interpolation and I have not tested them one at a time. -
A launch wrapper, because the app's 2D launcher never shows up in the headset on the Frame (SteamVR does not pick up Lepton's flat window for this app). Once a game built against this APK's kit is installed (it compares the kit fingerprints), the wrapper waits for the launcher to come up and starts the game activity itself, so a launch from the Steam library goes straight to the title screen in about ten seconds.
-
Grips in the cockpit. With upstream's hand steering on, a grip squeeze in first person both grabbed the wheel and drifted, because the grips are drift on this layout. Now the grips drift in the original and diorama cameras and only hold the wheel in first person; the right bumper drifts in every camera. (
openxr_input.cpp; the binding no longer sends a grip click to drift.) Withhand_steering = falsethe grips drift everywhere and you steer with the stick.
Everything else, the cameras, the hand IK, the HUD, Retro Rewind support, is upstream's work and is unchanged.
What works and what doesn't
Confirmed on my Frame, in the headset:
- Launching from the Steam library into the game, title screen to race, at 90 Hz.
- Racing in the original camera; the first-person cockpit, briefly; the menus at full rate; the VR settings panel; the controller layout in the table below, including pause and back.
- The release path itself: the APK from the Releases page installed with
install.sh, the game built on the headset against its kit (about 13 minutes on my Frame), and launched from the library. That is what I play now. The first release (v1.3.1-frame.1) failed at the final link on the headset because its objects were compiled with the wrong NDK; see "Building" below.
Known problems and things I have not tested:
- The empty item box stays on screen after the roulette slides up. The attempted fix (change 5) does not do it. It is cosmetic, so I live with it.
- The controllers drawn in the menus are Quest Touch controllers, not the Frame's. Upstream
draws SteamVR's own model of whatever controller you hold when SteamVR offers one and falls back
to its built-in Touch Plus models otherwise; inside Lepton the app gets no SteamVR render models,
so it always falls back. The buttons the tutorials point at are the Quest ones too; the table
below is what the Frame buttons actually do. I tried fixing this: SteamVR keeps the Frame
controller models on every Frame, and
steam-frame/frame-controller-models.pyconverts them into the game's format, with the buttons, trigger, grip and stick animated. They looked right in my hands, but they are about seven times the Touch Plus models' vertex count, the game re-animates every vertex on the CPU each frame a menu is up, and the menus dropped to 45 to 60 fps, with the background flickering when the VR settings opened mid-race. So it is off. To try it anyway, run the installer withWCF_FRAME_MODELS=1. A decimated version of the models would probably fix the cost; that is a good pull request for someone. - Steering with the grips in first person works, roughly. Change 8 is newer than the rest and
has had one session; grabbing the wheel works but upstream's grab range and steering response
were tuned for Quest controllers and I have not adjusted them. Set
hand_steering = falseunder[vr]inConfig.toml(path under "Where things are" below) to steer with the stick instead. - Subtle slowdowns remain. Races are a solid 90 on the counter, but there are brief hitches now and then that I notice and cannot pin to anything in the logs. Good enough that I play it; not perfect.
- The 2D launcher is invisible in the headset, so the one-time disc import and build are done from a terminal with two commands (below). Upstream's "Select disc image" button needs a document picker Lepton does not have anyway.
- Resolution. I tried 1.5x game resolution with 2160x2160 eyes for sharper menu text. It looked better and dropped frames all over the place (10 to 24 ms late, 30 to 50 ms stalls), so it went back to 1.0x and 1728x1728, which is what the installer sets. If you want sharper menus, make the menu screen bigger or closer in VR settings instead.
- Not tested: Retro Rewind, online play, the diorama camera for more than a minute, USB wheels, multiplayer, passthrough (there is nothing to pass through to; the Frame shows SteamVR's void around the menus), hand tracking.
- Not for Quest. The kit objects are compiled for Android arm64 and would run on a Quest, but the interpolation change is exactly what upstream turned off there because it misbehaved on Quest, and the controller layout only activates when SteamVR binds the Frame actions. Use upstream's APK on a Quest.
- Updates. This is built on upstream v1.3.1. A newer upstream APK has a different kit, and
build.shrefuses to make an APK when the objects it compiled do not export the same symbols as the ones in the APK; the patches will need porting when upstream moves.
Controls
The binding is installed per app by install.sh; the game picks the layout up by itself.
| Steam Frame | In the game |
|---|---|
| Left stick | Steer, navigate menus |
| Right trigger, or A | Accelerate / confirm |
| Left trigger, or B | Brake, reverse / cancel |
| X, or left bumper | Use or hold item |
| Y | Look back |
| Right bumper | Hop and drift |
| Grips | Hop and drift; in first person, hold the wheel instead |
| D-pad | D-pad (up is a trick) |
| Right stick directions | Tricks and wheelies |
| Right stick click | Cycle the three cameras |
| Menu (right) | Start / pause |
| View (left), tap | Back |
| View (left), hold half a second | VR settings panel (press again to close) |
The left View tap is sent on release so that a hold never also goes back. Steam Input and SteamVR's binding UI are the right place to change any of this: the file is a normal SteamVR binding and shows up under Manage Controller Bindings for the app.
Setting it up on a Steam Frame
You need: a Steam Frame; Lepton installed from Steam (Library > Tools); your own clean PAL
RMCP01 Mario Kart Wii disc image (I used an ISO; upstream's launcher also accepts RVZ, WBFS and
the others it lists); about 5 GB free; and a terminal on the Frame: Konsole in desktop mode, or SSH
from another machine with developer mode on. Nothing needs root and nothing is installed with pacman.
-
Get the repository and the APK:
git clone -b steam-frame https://github.com/simplysanatori/Wiicompiled_VR-PLUS-steam-frame.git cd Wiicompiled_VR-PLUS-steam-frameDownload
WiiCompiled-VR-SteamFrame-*.apkfrom the Releases page, or build it yourself (next section). The APK contains no game data, exactly like upstream's. -
Install it, with Steam running:
steam-frame/install.sh ~/Downloads/WiiCompiled-VR-SteamFrame-*.apkThis copies the APK and the launch wrapper to
~/Games/WiiCompiledVR-SteamFrame/, adds a Mario Kart Wii VR entry to the Steam library set to the Lepton compatibility tool (through Steam's own client API, so no editing ofshortcuts.vdfbehind Steam's back), and writes the controller binding and the 90 Hz settings into SteamVR for that entry. It prints the entry's appid; you need it for the next step. Running it again later updates the same entry. -
Put the disc image in
~/Documents/on the Frame (Lepton shows that folder to the app as/sdcard/Documents). -
Put the headset on and launch Mario Kart Wii VR from the library. The first launch takes a minute while Lepton installs the APK. You will see SteamVR's empty space and nothing else: the app's launcher is running but cannot be seen. Leave it running.
-
From the terminal, start the disc import, then the build.
<appid>is the number from step 2 andMarioKart.isois your file's name:LEPTON=~/.local/share/Steam/steamapps/common/Lepton/lepton $LEPTON exec steamlaunch-<appid> am start-foreground-service -n org.wiicompiled.quest/.launcher.GameSetupService \ -d file:///sdcard/Documents/MarioKart.iso --es task ExtractDiscThis checks the disc (it must be the clean PAL game; anything else is rejected) and extracts it into the app's data folder, a few minutes. Then:
$LEPTON exec steamlaunch-<appid> am start-foreground-service -n org.wiicompiled.quest/.launcher.GameSetupService \ --es task BuildGameThe headset now translates and compiles the game against the Frame kit. It took about a quarter of an hour on my Frame (upstream quotes half an hour on a Quest 3). Progress goes to
~/.local/share/Steam/steamapps/compatdata/<appid>/external/Android/data/org.wiicompiled.quest/files/WiiCompiledOpenXRVR/Logs/build_*.log, and the extraction and build both log throughadb:/usr/lib/android-sdk/platform-tools/adb connect localhost:5555 /usr/lib/android-sdk/platform-tools/adb -s localhost:5555 logcat | grep -iE 'GameSetup|DiscExtract|GameBuild|WiiCompiled' -
When the build log ends with the game installed, quit the app (Steam button > exit) and launch it again. From now on every launch goes straight into the game. The first time, the game asks you to pick a camera with the pointer; that choice and everything in the VR settings panel are saved to
Config.toml.
If you already had upstream's APK set up on the Frame under the same library entry name, step 2
repoints that entry. On the next launch Lepton reinstalls the app because the APK changed; the
extracted disc files and the previously built game both survive, but that game was built against
upstream's kit and the app will not run it (the launcher calls it stale, and the wrapper sees the
fingerprint mismatch and leaves the launcher up). Run the BuildGame command from step 5; the
extraction is not needed again.
Building the APK yourself
steam-frame/build.sh # downloads upstream's v1.3.1 APK and checks its SHA-256
steam-frame/build.sh path/to/WiiCompiled-VR-Quest3-v1.3.1.apk
On the Frame this takes about 20 seconds once the toolchain is there, and produces
steam-frame/release/WiiCompiled-VR-SteamFrame-<git tag>.apk. It needs curl, tar, python3
and openssl, all on SteamOS already, and no root. What it does:
- Fetches Android NDK r29 into
~/.local/opt/android-build/(Google ships no arm64 Linux NDK, so on the Frame it uses HomuHomu833's r29 build; on x86_64 it would use Google's r29, untried). It has to be r29, the NDK the kit was built with: the first release APK was compiled with r30, whose libc++ headers emit a helper (std::__hash_memory) that r29's library lacks, and the headset's link failed on it. The build now also refuses any object that imports a C++ symbol the kit's original does not. - Compiles the two runtime unity groups that contain the patched files and Aurora's
common.cppwith the same flags upstream's Android build uses. The third-party headers those three units need are vendored insteam-frame/build-deps/(see itsNOTICE.md), because upstream fetches its dependencies with CMake on Windows and this build runs no CMake. - Extracts the matching objects from the upstream APK's kit and checks that each new object exports exactly the same symbols and imports no C++ symbol the original did not. If not, it stops: the sources and the APK are out of step, or the toolchain is wrong.
make-apk.pyrewrites the APK: the four kit files (007_unity_runtime_4,001_unity_runtime_6,020_libaurora_gx.awith itscommon.cpp.oswapped, andinclude/vr/openxr_input.h), the new kit fingerprint inkit.json, the patchedclasses.dex, zipalign-equivalent alignment, and an APK Signature Scheme v2 signature made withopenssl. No Java, apksigner or zipalign. The signing key is generated insteam-frame/signing/on first use; keep it, an APK signed with another key will not install over the previous one.make-apk.py verify file.apkchecks any APK's signature.
The game itself (libmain.so) is never built here. It needs upstream's translator and your disc,
and the headset does that part. For what it is worth, during development the AI also linked
libmain.so on the Frame directly against the patched objects, using a translation produced by
upstream's .NET translator on the Frame; that is how the changes were iterated on in minutes. Those
tools and that translation output are not in this repository (the translator is upstream's, the
translation is Nintendo's code), so the on-device build is the path for everyone else.
Where things are, for digging
- Steam library entry: a non-Steam shortcut whose executable is the APK and whose compatibility
tool is
lepton;lepton-show-flatscreennext to the APK would show the 2D window if SteamVR ever picked it up. Launch options run~/Games/WiiCompiledVR-SteamFrame/wiicompiled-launch.sh. - The app's data:
~/.local/share/Steam/steamapps/compatdata/<appid>/.external/Android/data/org.wiicompiled.quest/files/WiiCompiledOpenXRVR/hasConfig.toml,DATA(the extracted disc), saves andLogs/(one folder per run with the game'sconsole.log, plus the build logs).internal/org.wiicompiled.quest/files/game/base/has the builtlibmain.soand itsgame.json. - Lepton's log for the launch:
~/.local/share/Steam/logs/lepton-steamlaunch-<appid>.log; the wrapper's:/tmp/wiicompiled-launch.log. - SteamVR's per-app settings are the
steam.app.<appid>section of~/.config/openvr/config/steamvr.vrsettings;/opt/steamvr/bin/linuxarm64/vrcmd --set-settings-int steam.app.<appid>.preferredRefreshRate 90is how the installer writes them while SteamVR runs. [diagnostics] openxr_logging = trueinConfig.tomlmakes the game log frame timing to itsconsole.log(late frames, stalls);setprop debug.wiicompiled.fpslog 1over adb before the game starts logs the frame rate. Both were how the menu and resolution problems above were found.- Lepton's Android reaches the host's
~/Documents,~/Downloadsand~/Videosas/sdcard/Documents,/sdcard/Downloadand/sdcard/Movies.lepton exec steamlaunch-<appid> <command>runs a command inside the running container;adb connect localhost:5555reaches it too.
How this was made
Over about a week in October 2026, in a series of Claude Code sessions on the Frame. The rough order:
- The first attempt, a few days earlier, got upstream's v1.3 Quest APK installed through Lepton,
found the clipboard crash in a logcat dump and patched it, drove the invisible launcher over adb
to import the disc, and got the game running. It was, in my words at the time, "a flickering
mess with shaky head tracking, vomit inducing". The AI tried a long list of SteamVR settings
(refresh rates, motion smoothing on and off, resolution overrides); one of those left the image
warped in a corner, and the actual cause of a lot of it turned out to be that the in-game panel
had quietly written foveation, adaptive resolution and passthrough into
Config.toml. - I then asked whether the PC version through Proton would be better. The AI cross-compiled upstream's Windows build on the Frame and ran it under Proton for ARM64 in VR: it worked and flickered because x86 emulation could not keep up (27 ms frames). It then wrote a native Linux ARM64 OpenXR port, which also worked and which I did not want either. Both were deleted at my request once the Android build was good; this fork is the Android path only.
- The real fix was reading upstream's runtime and finding that frame interpolation was compiled out on Android. The AI rebuilt the two runtime objects with that enabled, found that SteamVR under Lepton has no clock conversion extension and wrote the fallback, re-signed the APK, and the result was "very good". Then the controls, in code rather than only in the binding file because the game reads X and Y as the left hand's buttons. Then the menus, after the frame-timing diagnostics showed exactly which draw was slow. Then the item box, which did not work.
- Publishing went wrong once too. The first release APK carried objects compiled with NDK r30,
whose C++ headers call a helper that the kit's NDK r29 runtime does not have, so the headset's
build got all the way to the last step and failed to link. The fix was to build with r29, plus a
check in
build.shthat refuses any object importing a C++ symbol the kit's original does not. The second release then built and ran on the headset from a clean start. - The last change before publishing was a mistake: raising the resolution for sharper menus, which I reported as a performance regression the next day and which was rolled back. The numbers in the "Resolution" note above are from that log.
- This README was written by the AI from the session logs and its memory notes, at my request and
in my voice, and I read it before publishing. If something in it is wrong, it is because I
missed it; open an issue with the Lepton log and the game's
console.logattached.
I will look at issues and pull requests, but I am one person with one headset and no ability to fix emulation-level problems; the useful contribution here, as the Baiji README puts it better than I can, is testing. If you make a change, say what you tested it on and how.
Licence and credits
GPL-3.0, as upstream (LICENSE). The fork's own additions are under the same licence. Third-party
headers vendored for the build are listed with their licences in
steam-frame/build-deps/NOTICE.md.
The port is heurazy's Wiicompiled VR PLUS, itself built on iChris4's Wiicompiled VR and the WiiCompiled static recompilation project, on Aurora, Dawn, SDL3 and OpenXR; the credits in README.upstream.md are theirs and they did the hard part. Lepton is Valve's. Mario Kart Wii is Nintendo's; nothing of theirs is in this repository or in the APK, and you need your own disc.