Author SHA1 Message Date
simplysanatoriandClaude Opus 5.5 004f33aa0d README: release path confirmed on the headset; item box, controller models and grips as found
The second release built and ran on the Frame from a clean start. The item box
change does not hide the empty box; the menus show Quest Touch models because
Lepton gets no SteamVR render models; grips do not grab the wheel because hand
steering is off on purpose for this layout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N7dBRsvawKKn1F9gf68SYv
2026-10-08 12:14:08 -04:00
simplysanatoriandClaude Fable 5.1 45d71a37df Build the kit objects with NDK r29 and reject new C++ imports
The first release's objects were compiled with NDK r30, whose libc++ headers
emit std::__hash_memory; the kit's r29 libc++ has no such symbol and the
on-device link failed. Use HomuHomu833's r29 build on arm64 (Google's r29 on
x86_64) and fail the build when an object imports a mangled symbol the kit's
original does not.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N7dBRsvawKKn1F9gf68SYv
2026-10-08 11:53:54 -04:00
simplysanatoriandClaude Fable 5.1 cfa6aa1ded Ignore __pycache__
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N7dBRsvawKKn1F9gf68SYv
2026-10-08 11:39:19 -04:00
simplysanatoriandClaude Fable 5.1 ed7c28f37c Launch wrapper: start the game only when the built game matches this APK's kit
Lepton keeps the app's private data across an APK update, so a game built
against upstream's kit survives; the app refuses it as stale and the wrapper
used to start the game activity anyway (black screen). Compare game.json's
kitFingerprint with the APK's kit.json and leave the launcher up otherwise.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N7dBRsvawKKn1F9gf68SYv
2026-10-08 11:39:11 -04:00
5 changed files with 91 additions and 45 deletions

No files matched your search

+1
View File
@@ -92,3 +92,4 @@ output.txt
/steam-frame/out/
/steam-frame/release/
/steam-frame/signing/
__pycache__/
+46 -26
View File
@@ -87,10 +87,12 @@ What the patches do, in the order they were needed:
background, which costs nothing. (`openxr_integration.cpp`; `menu_shader_quality = 0` in the
config.)
5. **The item box.** In VR the item roulette slid up after use and stayed visible. Aurora's
virtual-screen HUD draws ignore the game's scissor rectangle; 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`.) During the slide itself a sliver can still show.
5. **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.
6. **Settings that suit the Frame**, seeded into `Config.toml` on the first launch by the wrapper
([`steam-frame/Config.frame.toml`](steam-frame/Config.frame.toml)): interpolation on auto, menu
@@ -100,9 +102,10 @@ What the patches do, in the order they were needed:
together with the interpolation and I have not tested them one at a time.
7. **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 is built, 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.
(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.
Everything else, the cameras, the hand IK, the HUD, Retro Rewind support, is upstream's work and is
unchanged.
@@ -114,17 +117,26 @@ 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 item box fix is in the build I play and I have not seen the box stuck since; it had the
least testing of the five changes.
- 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 release APK has not yet been through a clean run.** What I play is the same four objects,
linked into `libmain.so` on the Frame by the AI's host-side build. The APK on the Releases page
was made by `build.sh` from those objects and passes the same archive, alignment and signature
checks as upstream's APK, but the full install, disc import and on-headset build with it is the
next thing I do, and this line changes when it has happened. If you get there first, an issue
with the build log would be welcome.
- **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.
- **Grips do not grab the wheel in first person.** That is on purpose: on this layout the grips
are hop and drift, and with upstream's hand steering on, squeezing a grip to drift also grabbed
the wheel and steering jumped to your hand. So `hand_steering = false`, and you steer with the
stick in every camera. If you would rather steer with your hands in the cockpit, set
`hand_steering = true` under `[vr]` in `Config.toml` (path under "Where things are" below) and
drift with the right bumper instead. I have not tried that combination on the Frame.
- **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.
@@ -135,7 +147,6 @@ Known problems and things I have not tested:
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.
- **Item roulette** can still show a sliver while sliding.
- **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.
@@ -240,9 +251,11 @@ from another machine with developer mode on. Nothing needs root and nothing is i
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, and Lepton clears the old app's data on the next launch because the APK
changed; the extracted disc files survive in `external/`, the built game does not, so you repeat
the `BuildGame` command (not the extraction).
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
@@ -255,17 +268,19 @@ On the Frame this takes about 20 seconds once the toolchain is there, and produc
`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:
1. Fetches an Android NDK into `~/.local/opt/android-build/` (Google ships no arm64 Linux NDK, so
on the Frame it uses [HomuHomu833's r30 build](https://github.com/HomuHomu833/android-ndk-custom);
on x86_64 it would use Google's r29, untried). If you have built
[Moonlight XR for the Frame](https://github.com/simplysanatori/moonlight-android-xr-steam-frame) the
NDK is already there.
1. 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](https://github.com/HomuHomu833/android-ndk-custom);
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.
2. Compiles the two runtime unity groups that contain the patched files and Aurora's `common.cpp`
with the same flags upstream's Android build uses. The third-party headers those three units need
are vendored in [`steam-frame/build-deps/`](steam-frame/build-deps/) (see its `NOTICE.md`),
because upstream fetches its dependencies with CMake on Windows and this build runs no CMake.
3. Extracts the matching objects from the upstream APK's kit and checks that each new object exports
exactly the same symbols. If not, it stops: the sources and the APK are out of step.
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.
4. `make-apk.py` rewrites the APK: the four kit files (`007_unity_runtime_4`, `001_unity_runtime_6`,
`020_libaurora_gx.a` with its `common.cpp.o` swapped, and `include/vr/openxr_input.h`), the new
kit fingerprint in `kit.json`, the patched `classes.dex`, zipalign-equivalent alignment, and an
@@ -322,7 +337,12 @@ Over about a week in October 2026, in a series of Claude Code sessions on the Fr
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.
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.sh` that 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.
Binary file not shown.
+23 -14
View File
@@ -35,15 +35,18 @@ if ! echo "$UPSTREAM_APK_SHA256 $APK" | sha256sum -c --quiet 2>/dev/null; then
echo "warning: $APK is not the upstream v1.3.1 APK this build was made for; carrying on, the symbol check below decides"
fi
# 1. Toolchain. Google ships no arm64 Linux NDK; HomuHomu833's build of r30 fills in on the Frame.
# 1. Toolchain: NDK r29, the one the kit itself was built with and the one the headset links with.
# A newer NDK's libc++ headers emit helpers (std::__hash_memory in r30) that r29's libc++ does
# not have, and the on-device link then fails. Google ships no arm64 Linux NDK; HomuHomu833's
# build of r29 fills in on the Frame.
ARCH=$(uname -m)
if [ -n "${WCF_NDK:-}" ]; then
NDK=$WCF_NDK
elif [ "$ARCH" = aarch64 ]; then
NDK=$TOOLS/ndk/android-ndk-r30
NDK=$TOOLS/ndk/android-ndk-r29
if [ ! -d "$NDK/toolchains/llvm/prebuilt" ]; then
fetch https://github.com/HomuHomu833/android-ndk-custom/releases/download/r30/android-ndk-r30-aarch64-linux-gnu.tar.xz "$TOOLS/dl/ndk-r30-aarch64.tar.xz"
mkdir -p "$TOOLS/ndk" && tar xJf "$TOOLS/dl/ndk-r30-aarch64.tar.xz" -C "$TOOLS/ndk"
fetch https://github.com/HomuHomu833/android-ndk-custom/releases/download/r29/android-ndk-r29-aarch64-linux-gnu.tar.xz "$TOOLS/dl/ndk-r29-aarch64.tar.xz"
mkdir -p "$TOOLS/ndk" && tar xJf "$TOOLS/dl/ndk-r29-aarch64.tar.xz" -C "$TOOLS/ndk"
fi
elif [ "$ARCH" = x86_64 ]; then
NDK=$TOOLS/ndk/android-ndk-r29
@@ -86,7 +89,7 @@ for entry in 007:runtime_4 001:runtime_6; do
"$CXX" "${TARGET[@]}" "${RUNTIME_DEFS[@]}" "${RUNTIME_INC[@]}" "${COMMON[@]}" -include mkw_pch.h \
-c "$OUT/obj/unity_${g}_cxx.cxx" -o "$OUT/kit/objects/${n}_unity_${g}_cxx.cxx.o" &
done
# fmt's format.h uses malloc without including <cstdlib> itself; NDK r30's libc++ no longer does it for it.
# fmt's format.h uses malloc without including <cstdlib> itself; a newer libc++ no longer does it for it.
"$CXX" "${TARGET[@]}" "${DEFS[@]}" "${AURORA_INC[@]}" "${COMMON[@]}" -include cstdlib \
-c "$SRC/aurora-main/lib/gfx/common.cpp" -o "$OUT/obj/common.cpp.o" &
wait
@@ -98,20 +101,26 @@ unzip -q -o "$APK" 'assets/game_kit/objects/007_unity_runtime_4_cxx.cxx.o' 'asse
'assets/game_kit/libs/020_libaurora_gx.a' 'assets/game_kit/include/vr/openxr_input.h' -d "$OUT/upstream-kit"
UK=$OUT/upstream-kit/assets/game_kit
exported() { "$NM" --defined-only "$1" | awk '$2 ~ /[TDBR]/ {print $3}' | sort -u; }
imported() { "$NM" -u "$1" | awk '{print $2}' | sort -u; }
ok=1
for o in 007_unity_runtime_4_cxx.cxx.o 001_unity_runtime_6_cxx.cxx.o; do
if diff <(exported "$UK/objects/$o") <(exported "$OUT/kit/objects/$o") >/dev/null; then
echo "$o: exports the kit's symbols"
# Same exports as the kit's object, and no new C++ imports: a new mangled import means a different
# libc++ than the kit's, which the on-device link cannot satisfy. New plain C imports (bionic) are fine.
check() { # label kit-object new-object
if diff <(exported "$2") <(exported "$3") >/dev/null; then
echo "$1: exports the kit's symbols"
else
echo "$o: exported symbols DIFFER from the kit's (sources out of step with this APK?)"; ok=0
echo "$1: exported symbols DIFFER from the kit's (sources out of step with this APK?)"; ok=0
fi
new_cxx=$(comm -13 <(imported "$2") <(imported "$3") | grep '^_Z' || true)
if [ -n "$new_cxx" ]; then
echo "$1: imports C++ symbols the kit's object does not (wrong NDK/libc++?):"; echo "$new_cxx" | head -5; ok=0
fi
}
for o in 007_unity_runtime_4_cxx.cxx.o 001_unity_runtime_6_cxx.cxx.o; do
check "$o" "$UK/objects/$o" "$OUT/kit/objects/$o"
done
t=$(mktemp -d); (cd "$t" && "$AR" x "$UK/libs/020_libaurora_gx.a" common.cpp.o)
if diff <(exported "$t/common.cpp.o") <(exported "$OUT/obj/common.cpp.o") >/dev/null; then
echo "020_libaurora_gx.a(common.cpp.o): exports the kit's symbols"
else
echo "020_libaurora_gx.a(common.cpp.o): exported symbols DIFFER from the kit's"; ok=0
fi
check "020_libaurora_gx.a(common.cpp.o)" "$t/common.cpp.o" "$OUT/obj/common.cpp.o"
rm -rf "$t"
[ $ok = 1 ] || { echo "not making an APK from objects that do not match the kit"; exit 1; }
cp "$UK/libs/020_libaurora_gx.a" "$OUT/kit/libs/020_libaurora_gx.a"
+21 -5
View File
@@ -14,17 +14,33 @@ LOG=/tmp/wiicompiled-launch.log
SAVED_LD_PRELOAD=$LD_PRELOAD
unset LD_PRELOAD # Steam's overlay preload only spams errors for adb
GAME=
READY= # set once a game built against this APK's kit is installed
REASON="no STEAM_COMPAT_DATA_PATH"
if [ -n "$STEAM_COMPAT_DATA_PATH" ]; then
# Lepton mounts $STEAM_COMPAT_DATA_PATH/external as the container's shared storage and
# internal/<package> as the app's private data.
# internal/<package> as the app's private data. The latter survives an APK update, so a game
# built against another kit can be sitting there: the app calls it stale and will not run it.
CFG=$STEAM_COMPAT_DATA_PATH/external/Android/data/$PKG/files/WiiCompiledOpenXRVR
if [ ! -f "$CFG/Config.toml" ] && [ -f "$HERE/Config.frame.toml" ]; then
mkdir -p "$CFG"
cp "$HERE/Config.frame.toml" "$CFG/Config.toml" && chmod 664 "$CFG/Config.toml"
echo "$(date +%T) seeded $CFG/Config.toml with the Frame settings" >> "$LOG"
fi
GAME=$STEAM_COMPAT_DATA_PATH/internal/$PKG/files/game/base/libmain.so
MANIFEST=$STEAM_COMPAT_DATA_PATH/internal/$PKG/files/game/base/game.json
APK=$(ls "$HERE"/app/*.apk 2>/dev/null | head -1)
if [ ! -f "$MANIFEST" ]; then
REASON="no game built yet"
elif [ -z "$APK" ]; then
REASON="no APK next to this script to compare the kit with"
else
KIT=$(unzip -p "$APK" assets/game_kit/kit.json 2>/dev/null | python3 -c 'import json,sys; print(json.load(sys.stdin)["fingerprint"])' 2>/dev/null)
BUILT=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get("kitFingerprint", ""))' "$MANIFEST" 2>/dev/null)
if [ -n "$KIT" ] && [ "$KIT" = "$BUILT" ]; then
READY=1
else
REASON="the built game is for another kit (${BUILT:0:12} vs this APK's ${KIT:0:12}); it needs building again"
fi
fi
fi
(
@@ -34,8 +50,8 @@ fi
timeout 5 "$ADB" connect localhost:5555 >/dev/null 2>&1
if timeout 5 "$ADB" -s localhost:5555 shell dumpsys window 2>/dev/null \
| grep -q "mCurrentFocus=.*$PKG/.*LauncherActivity"; then
if [ -n "$GAME" ] && [ ! -f "$GAME" ]; then
echo "$(date +%T) no game built yet ($GAME missing): leaving the launcher up"
if [ -z "$READY" ]; then
echo "$(date +%T) leaving the launcher up: $REASON"
exit 0
fi
if [ -n "$WCF_STAY_IN_LAUNCHER" ]; then