Skip to content

Android port: build an arm64 APK from your own disc (Vulkan, touch controls, on-device training) - #13

Open
LiquidAzir wants to merge 6 commits into
elliotttate:windows-releasefrom
LiquidAzir:android-port
Open

LiquidAzir wants to merge 6 commits into
elliotttate:windows-releasefrom
LiquidAzir:android-port

Conversation

@LiquidAzir

@LiquidAzir LiquidAzir commented Oct 1, 2026 •

Copy link
Copy Markdown

Hi! I got Wind Waker Recomp running natively on Android and wanted to offer the port back in case it's useful.
It follows the project's model exactly: the player builds their own APK from their own disc, and nothing
game-derived is in this PR.

What it is

  • scripts/android/build.py builds on top of scripts/windows/build.py: the same RecompCore/DolRecomp pins, disc
    checks, translation, verified digest (54f54434), mods and source steps run on the Windows PC, then the game
    module and the app are compiled for arm64 Android with the NDK's clang. Aurora already supports Android, so it
    draws through Dawn on Vulkan (Aurora's prebuilt dawn-android-aarch64), with SDL3 statically linked into
    libmain.so. The APK is assembled without Gradle (javac, d8, aapt2, apksigner from the SDK).
  • android/: an entry shim (default paths in the app's storage, a session log mirrored to logcat, an optional
    launch.env), a touch overlay (stick, A/B/X/Y, L/R/Z, START, D-pad, drag on the right half for the C-stick)
    that feeds Aurora's virtual pad the way the iOS overlay does and hides when a controller connects, and the
    activity (a 60 Hz display request).
  • scripts/android/install.py installs the APK and pushes main.dol, the RELs and the disc image to the app's
    folder over adb.
  • --device SERIAL runs the local optimization training on the phone: an instrumented module plays the
    Windows builder's training route headless to player control on Outset (plain, then widescreen + Better Wind
    Waker), and the counts guide the real compile. The profile stays local, as on the other platforms.
  • --train-app (with --device) also trains libmain.so on the phone. The module's training is headless, so
    it never runs the GX worker or the render thread; this one plays the opening once, drawn and paced, with an
    instrumented app and the real module. With libmain.so linked -Bsymbolic-functions (its exported functions
    were called through the PLT even from within), the GX worker went from 51 to 47 M cycles a frame on the
    title's sea.
  • Shared code changes are small: Android's Back opens the options menu (settings_menu.cpp, one line), and
    cmake/composite accepts optional extra sources (used only for the training module's profile flush).

Two things Android needed that might be worth knowing

  • Aurora's Android window code waits for SDLSurface to call auroraNativeSetSurfaceReady, which stock SDL
    never does, so the game hangs before its first frame. The builder patches SDL's Java as it copies it (along with
    a 64 MB stack for SDL's game thread, matching the Windows link).
  • On phones, sustained speed is set by temperature. On a Galaxy Z Fold 7, Samsung's thermal manager lowers the
    CPU limit from 4.47 GHz to about 2.0/1.7/1.4 GHz as the surface passes 38/40/42 °C. Work per frame and memory
    stay flat (measured with simpleperf stat), so late-session slowdowns are the frequency cap rather than a leak.
    docs/ANDROID.md has the numbers.

Tested on one device, a Galaxy Z Fold 7 (Snapdragon 8 Elite, Adreno 830, Android 16), from a Redump-verified
disc. This branch, rebased on windows-release 8cc06de (RecompCore 9618e9d), generates the verified composite
(54f54434), builds with the default options and, freshly installed with install.py, plays the scripted route
to player control on Outset. Before the rebase the port was played on the phone for some hours: it boots and plays
on both screens with touch, and runs at 30 FPS in most of the opening, with dips in the
heaviest views once the phone throttles. Not yet tried: other devices and GPUs, controllers on Android, saving and reloading, the
new save states and climbing on Android, the HD packs and the later game. See docs/ANDROID.md.

Things you may want to change

  • The application id defaults to the profile's bundle id (dev.bluewake.BlueWake) and the label to "Wind Waker
    Recomp". Both are builder options.
  • The build host is Windows only (it reuses the Windows builder); a Mac/Linux host would need the same
    subclassing of scripts/builder.
  • The activity honors an adb-only --ez showWhenLocked true extra, for testing and training on a locked phone.
    Drop it if you'd rather not have it.
  • Docs and README wording are yours to adapt; docs/ANDROID.md is written in the style of docs/WINDOWS.md.

This was developed with substantial AI assistance (Claude), like the rest of the project.

🤖 Generated with Claude Code


Summary by cubic

Adds a native Android (arm64) port: scripts/android/build.py builds a signed APK on a Windows PC from your own disc, reusing the Windows builder's pins, disc checks, translation, and verified composite digest. The game module and app are compiled with the NDK's clang, drawing through Aurora on Vulkan with SDL3, and the optimization profile can be trained on the phone itself.

What's in it

  • New android/ app: entry shim, touch overlay (stick, buttons, D-pad, C-stick drag) feeding Aurora's virtual pad, activity, and docs.
  • scripts/android/install.py installs the APK and pushes main.dol, the RELs, and the disc image over adb.
  • --device SERIAL trains the game module on the phone; --train-app also trains libmain.so (-Bsymbolic-functions cut the GX worker from 51 to 47 M cycles a frame on the title's sea).

Noteworthy details

  • The build patches SDL's Java: a 64 MB game thread stack, and telling Aurora when SDLSurface is ready (stock SDL never does, so the game would hang before its first frame).
  • Android's Back opens the options menu; a bluewake_settings_is_open hook (an atomic flag for cross-thread reads) leaves a touch away from the controls to the menu; cmake/composite accepts optional extra sources for the training module's profile flush.
  • Climbing and save states are included (the host's climb.c, save_state.c and zlib, as on iOS), and install.py makes the pushed game folders 2775 so a new app can read them.
  • Follow-up fixes: touch pushes track pointer ids 16–31; the adb-only lock-screen switch applies only while launch.env is present; the manifest requires Vulkan 1.1; install.py skips existing files only on size plus SHA-1 match.
  • Tested on a Galaxy Z Fold 7; docs/ANDROID.md documents the phone's thermal throttling that sets sustained speed.

Written for commit b66adcb. Summary will update on new commits.

Review in cubic

…on-device training

scripts/android/build.py builds BlueWake for arm64 Android on top of the
Windows builder: the same pins, disc checks, translation, verified digest,
mods and source steps, then the game module and the app compiled with the
NDK's clang (Aurora through Dawn on Vulkan, SDL3 static in libmain.so) and an
APK assembled without Gradle. --device trains the optimization profile on the
phone itself (an instrumented module played headless to player control on
Outset), as the Windows builder trains on the PC.

android/ holds the app: the entry shim (paths in the app's storage, the
session log, launch.env), the touch overlay and its bridge to Aurora's
virtual pad, and the activity (a 60 Hz display request). Two changes are made
to SDL's Java as the builder copies it: the game thread's stack (64 MB, as on
Windows) and SDLSurface telling Aurora when its surface is ready, which
Aurora's Android window code waits for.

Shared code: Android's Back opens the options menu (settings_menu.cpp), and
cmake/composite accepts extra sources (the training module's profile flush).

Verified on a Galaxy Z Fold 7 (Snapdragon 8 Elite): the verified digest, boot
to play on Outset with touch, the trained module; docs/ANDROID.md has the
numbers, including the phone's thermal frequency limit that sets sustained
speed.
--train-app (with --device) trains libmain.so as --device trains the game
module: an instrumented app (the host, GXRuntime, Aurora and SDL) installed
with the real module plays the opening once, drawn and at the game's pace,
and the app is compiled with the counts (build/android/pgo-app, used by every
later app build; --no-app-profile leaves it out). The module's training is
headless, so it never runs the GX worker or the render thread. The entry shim
writes the app's counts itself after the host returns, naming the file again
(the profiling runtime read LLVM_PROFILE_FILE before launch.env set it).

libmain.so is linked with -Bsymbolic-functions: its exported functions were
called through the PLT even from within it.

On the Fold 7, over the title's sea, the GX worker went from 51 to 47 M
cycles a frame (docs/ANDROID.md).
The folders adb makes in the app's storage are shell's (group ext_data_rw,
2770). On the Fold 7 (Android 16) a newly installed app could not search
them and stopped with "the prepared game executable is missing"; the files
themselves were readable. install.py now makes the two folders 2775.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 14 files

Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.

Re-trigger cubic

Comment thread android/java/dev/bluewake/android/BlueWakeActivity.java Outdated
Comment thread android/src/android_entry.c Outdated
Comment thread android/CMakeLists.txt Outdated
Comment thread scripts/android/install.py Outdated
Comment thread scripts/android/install.py Outdated
Comment thread scripts/android/build.py Outdated
Comment thread android/AndroidManifest.xml Outdated
Comment thread scripts/android/build.py
Comment thread android/src/android_entry.c Outdated
Comment thread scripts/android/install.py Outdated
…the builder and installer)

- Touch: pointer ids 16-31 are tracked; an earlier short tap's timer no longer
  ends a newer tap's latch; while the options menu is open, a touch away from
  the controls is the menu's (bluewake_settings_is_open), not the C-stick.
- The adb-only showWhenLocked switch counts only while files/launch.env exists
  (written over adb; no other app can), and every launch decides again
  (onNewIntent), so an ordinary launch turns it off.
- The manifest requires Vulkan 1.1, the renderer's API, and not OpenGL ES.
- The session log's pump starts before stdout and stderr are redirected, and a
  training run's exit waits for it to drain instead of a fixed 300 ms.
- build.py: the app's profile goes to CMake as a path (a path with spaces
  works); SDL's Java is found beside the app build being packaged; the label is
  escaped for the manifest; the module training's check run is inside the
  cleanup; --train-app reuses its profile while its inputs are unchanged.
- install.py: main.dol and the disc image are skipped only when the size and
  SHA-1 match, and every REL is checked.
@LiquidAzir

Copy link
Copy Markdown
Author

Thanks for the review. e3f0947 addresses all 15 comments (replies in each thread).

Checked so far: the branch builds (Java, libmain.so and the APK); the manifest now requires Vulkan 1.1 and makes OpenGL ES optional; nativeMenuOpen and bluewake_settings_is_open are exported; and a profile in a folder with a space in its name reaches the compiler as one argument and compiles.

Still to do: an on-device check of the touch and launch changes (the menu's right side by touch, rapid taps, and the lock-screen switch only with launch.env). The phone wasn't available today; I'll follow up here once it's done.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 10 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread runtime/host/src/settings_menu.cpp Outdated
@LiquidAzir

Copy link
Copy Markdown
Author

On-device check of e3f0947 done on the Galaxy Z Fold 7 (cover screen), from a fresh install.py install with the default application id:

  • install.py: a fresh install pushes main.dol, the RELs and the disc image and opens the folders; a second run finds all three identical (size and SHA-1) and skips them in about 3 seconds.
  • Options menu by touch: with the menu open, a tap on the right half of the screen (on the Render resolution combo) opens the combo instead of moving the C-stick.
  • Short taps: quick taps on the touch START take the title through to the memory card prompt.
  • showWhenLocked: read from the keyguard state with the screen locked:
    • With launch.env present and the extra, the game shows over the lock screen (occluded).
    • A following ordinary launch turns that off.
    • The extra without launch.env is refused.

The Android touch overlay reads it from its own thread
(bluewake_settings_is_open); every access now goes through the atomic.
@chrissotraidis

Copy link
Copy Markdown
Collaborator

Hi @LiquidAzir, thanks for all the work on this. Wind Waker Recomp is moving into BlueWake, which Elliott and I now maintain together. Could you open this against https://github.com/chrissotraidis/bluewake? An Android port is new platform work, so we'd review it there as its own project, with your authorship. Building the APK from the player's own disc fits how BlueWake handles phones and tablets.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants