SMS Launcher for Android

SMS Launcher for Android is a native Android port of Super Mario Sunshine that supports touch controls and HD textures.

README

Join the Super Mario Sunshine Discord server
Join our Discord

Also on PC: Windows, macOS and Linux. Get SMS Launcher. Also on Switch: homebrew, up to 60 fps. Download the NRO. Also on iPhone and iPad: native port in progress, coming soon.

SMS Launcher for Android

A native Android launcher for the Super Mario Sunshine PC port. The APK includes the compiled port and SDL, but no game data: you import a disc image made from your own copy. It targets ARM64 Android 10 or newer, with GLES 3.1 or newer, and a controller or the on-screen touch controls. AYN Thor and similar modern Android handhelds are the intended hardware. On Windows, macOS and Linux, use the desktop SMS Launcher instead.

The launcher's main menu: the seaside artwork with Super Mario Sunshine, Ready when you are, a big yellow Play button, a settings button, and a Discord button in the top bar Settings, Gameplay page: frame rate, volume, language and camera inversion, with pages for Controls, Graphics, Performance, Saves and data, and About

Download

APKs are published on this repository's Releases page: full releases, starting with 1.0.0, and tester previews between them as pre-releases. Each has two: sms-android-<version>-arm64.apk plays Super Mario Sunshine, and sms-android-eclipse-<version>-arm64.apk (SMS Eclipse) plays Super Mario Eclipse. They install side by side, each with its own disc and saves. Install over the previous release or preview to keep your imported disc and saves. Previews 1 to 17 were published from the desktop launcher's repository as android-pr-107-v* pre-releases; this repository's builds update them in place. CHANGELOG.md lists what changed in each release and preview.

Status

See VALIDATION.txt for build and emulator results. Plaza gameplay is currently slow in the ARM64-translating emulator; handheld performance has not been measured on AYN Thor. An S24 Ultra tester reports near-60 FPS while recording but lower FPS during ordinary play. Performance requests and faster-core placement did not help in their comparisons. Their timing log identified an avoidable extra refresh wait in our background-work fallback; preview 12 replaces it with a short cooperative turn. The tester reports that build is much faster and supplies a sustained nominal-60 FPS 4:3 run, while widescreen still runs around 55–60 FPS. Preview 13 reduces CPU work in widescreen layout; its device FPS effect needs comparison. The two tester runs were not a controlled matching-camera benchmark.

Build

The game comes from the sms-port submodule, pinned to the sms-pc-port commit this launcher builds. Clone with submodules:

git clone --recurse-submodules https://github.com/chasem-dev/sms-android.git

To build a different port checkout, set SMS_PORT_ROOT to its location. Install a JDK (17 or newer), Python 3, CMake, Git, curl, tar, patch, and the Android SDK. Install these SDK packages:

platform-tools platforms;android-35 build-tools;36.0.0
ndk;28.2.13676358 cmake;3.22.1

Accept the SDK licenses, set ANDROID_HOME and JAVA_HOME, and run:

./build.sh

The script downloads checksum-verified SDL 2.32.10, initializes the port's decompilation submodule, and uses the checked-in Gradle wrapper to build only arm64-v8a. The installable debug APK is app/build/outputs/apk/sunshine/debug/app-sunshine-debug.apk. SMS_ECLIPSE=1 ./build.sh builds the Super Mario Eclipse app instead, app/build/outputs/apk/eclipse/debug/app-eclipse-debug.apk: the game with the Eclipse mods built in (the port's -DSMS_ECLIPSE=ON, which fetches their sources at configure time; see the port's docs/ECLIPSE.md), and checked against the mods' declarations after linking.

tests/check-input-monitor.sh, tests/check-controller-mapping.sh, tests/check-texture-pack.sh, tests/check-language-pack.sh and tests/check-update-check.sh run the input monitor's, controller mapping's, texture pack installer's, language pack installer's and update check's unit tests without the Android SDK, and tests/check-vcdiff.sh the Eclipse disc patcher's.

Releases

Merging a pull request into main publishes a release automatically, through .github/workflows/release.yml, when CHANGELOG.md has lines under ## Unreleased:

  1. It numbers the next preview of smsAndroidVersion (0.2.0-preview.1, 0.2.0-preview.2, …) from the existing release tags.
  2. It builds the release APKs, Sunshine's and Eclipse's, with release.sh, signed with the keystore in the ANDROID_KEYSTORE_BASE64 repository secret.
  3. It publishes a GitHub pre-release with the Unreleased lines as its notes, then commits those lines under the new preview's heading in CHANGELOG.md.

Pull requests with nothing under Unreleased (tests, CI, docs) publish nothing. To start a new release line, raise smsAndroidVersion in gradle.properties; previews then count from 1 again. Each version allows 99 previews. Pull requests are built by .github/workflows/build.yml, which uploads the debug APK as an artifact.

To publish a full release instead of the next preview, set smsAndroidFullRelease=true and smsAndroidVersion to the release's number in the pull request that should publish it. Its version code must be above every preview testers have (major × 1000000 + minor × 10000 + patch × 100 + preview), so a full release of a version that already has previews needs a higher number (0.2.0's previews were followed by 1.0.0). The workflow publishes it as the latest release, with the Unreleased lines and every preview's since the last full release as its notes, then sets smsAndroidFullRelease=false and moves smsAndroidVersion to the next patch, whose previews the following pushes publish.

To move the game, check out the new commit in the submodule (git -C sms-port checkout <commit>), commit it with a changelog line for each change testers will notice, and merge.

./release.sh builds the same signed APKs locally into dist/. It refuses to run with uncommitted changes, so the footer names clean source revisions. It signs with ~/.android/debug.keystore unless SMS_ANDROID_KEYSTORE (and SMS_ANDROID_KEYSTORE_PASS, SMS_ANDROID_KEY_ALIAS) name another key, and refuses to sign with a certificate other than the one previous previews used, because Android won't install an update signed with a different key over an existing install. Never commit or share the keystore.

Build identification

Both landscape panels scroll independently, with fixed headings, a swipe hint and persistent scrollbars when content overflows. The launcher keeps a two-line footer visible while either panel scrolls. It shows the launcher version, preview number, debug/release variant, ARM64 architecture and the launcher/game source revisions actually built. Tracked local edits add -modified; source archives without Git show unknown. The same metadata is written to the game log at startup and survives log export. The game revision is the sms-port commit that was built.

smsAndroidVersion in gradle.properties and the preview number set the version name (0.2.0-preview.1) and version code (major × 1000000 + minor × 10000 + patch × 100 + preview), so each new preview is an identifiable update. Local builds use preview 0 and have no preview suffix. Install over the previous preview to retain data. Export a save backup before uninstalling; uninstalling also removes the imported disc.

Use

Install the APK and open the landscape SMS Launcher. Its big yellow button does the next step until the game is ready: first Import disc image. Pick a US revision 0 Sunshine ISO or GCM using Android's document picker. Import copies it into private app storage; allow enough free space for that copy. The button then reads Play. The gear beside it (or Start on a controller) opens Settings: Gameplay, Controls, Graphics, Performance, Saves & data and About. Changes save as you make them. A controller's D-pad or stick moves through the launcher, A selects and B goes back. The Discord button in the top bar opens an invite to the Discord server. Language under Gameplay plays the game in English (the default, the disc's own text) or in any language pack the game's tools/mods/language-packs.json lists: Deutsch, Español, Français, Italiano and Polski. Choosing Play downloads the chosen language's pack (about 1 MB, from the HD texture extras releases), checks it against its MD5 and installs it in private app storage, as the desktop SMS Launcher does. Saves work in every language. The Eclipse app has no Language setting: Super Mario Eclipse plays in English. Android doesn't let the launcher update itself, so it checks this repository's GitHub releases when it opens (at most every six hours) and, when a newer version is out, shows a Get update button in the top bar, which opens the release page. A full release hears of full releases; a preview also hears of newer previews, as the desktop launcher's Stable and Beta channels do. The new APK installs over the old one, keeping the disc and saves, because every release is signed with the same key. Picture sharpness is the internal resolution: Original (1×, the GameCube's 640×528, the default), Sharper (2×), Very sharp (3×) or Sharpest (4×). Higher settings need more of the graphics chip. HD textures defaults off. Download HD textures fetches the Super Mario Sunshine UHD Texture Pack v2.1.1 (about 941 MB, from its authors' release page) and the port's HD texture extras (about 61 MB), the releases the game's tools/mods/get.py pins. Each download is checked against its MD5 and unpacked into private app storage (about 3.1 GB installed; allow 5 GB free during setup, since the archive is kept until it is unpacked). A stopped or dropped download continues where it left off. The switch turns on once both are installed, and Remove HD textures deletes them. GPUs that sample the pack's BC7 images, such as the Adreno 750, get every image at full size: Delfino Plaza's take about 175 MiB. Other GPUs get them decoded to RGBA, four times their size, so the launcher halves those with more texels than 1024×1024 and caps texture memory at 1 GiB; in the emulator, whose GPU takes no BC7, Delfino Plaza then needs about 560 MiB. Loading screens take longer while the pack's images are prepared. Connect a USB/Bluetooth controller or use the handheld's built-in gamepad. Touch screen controls (off by default) draws a GameCube controller over the game: both sticks are analog, the D-pad takes diagonals, and L and R press fully. The game shows GameCube button prompts while it is on. The controls hide while a real controller is used and come back when the screen is touched. Android performance hints defaults on and lets supported devices adjust CPU scheduling to the frame workload. It does not force clock speeds or bypass thermal limits. Toggle it off to compare the same scene and settings, then back on: saved choices carry over between updates, so a comparison left off stays off. On a Snapdragon 8 Gen 3 phone, 0.2.0-preview.6 holds 60 FPS with hints on or off; low clocks there came from the game stalling mid-frame on the GPU, which Android read as idle time. Use performance CPU cores defaults on (from 0.2.0-preview.7; players who changed it keep their choice). It restricts the game's frame-producing thread and optional renderer worker to a detected group of big/prime cores. It uses the current thread's allowed CPU mask, prefers complete scheduler-capacity metadata, and falls back to complete maximum frequency metadata from one source. It never guesses from core numbering, never restricts to one core, and leaves placement unchanged if a distinct multi-core tier cannot be identified. Android still controls clock speeds and thermal policy. This works independently of ADPF: compare it on/off with performance hints, headroom and display matching off, restarting Play between runs. Game logs report requested/effective core masks or the reason for falling back. Testers found it smoother on older builds, and on 0.2.0-preview.6 still a little smoother than without it, so it is now the default; switch it off to save battery or to compare. Request performance headroom defaults off and asks ADPF to finish frame work within 80% of the frame interval (about 13.35 ms at nominal 60 FPS), leaving a margin for presentation and scheduling variation. It changes the requested deadline, not game speed or reported measurements. Switch it off to restore the v8 full-frame target. This is a tuning experiment, not a confirmed fix for a manufacturer's clock policy. The S24 Ultra tester reported v9 was slower, so both headroom and display matching are opt-in from v10. Existing explicitly saved choices are preserved; turn these two switches off when comparing with v8. Supported Android 15+ sessions explicitly disable power-efficiency preference, which is already the platform default; that call alone does not imply a boost. Match display refresh to game FPS defaults off and sends a Surface frame rate hint on Android 11+. Android chooses the actual display refresh rate. The game also reports interruptible gameplay while its activity is resumed and no gameplay on pause using Android 13+ Game State. The selected Android Game Mode is read on resume; the app cannot select Performance mode for you. No game-mode configuration overrides the manufacturer's interventions. Log performance timings defaults off. Enable it to export steady frame and presentation timing, SDK pacing waits and main-thread CPU time without recording or touching the screen during the sample. A current CPU frequency sample is included when Android allows reading it; -1 means unavailable. SDK pacing also reports background-handoff average/max wall time. These logs include a GX breakdown for vertex decoding, draw submission, textures, copies and peeks to help investigate the extra widescreen cost. These timers add some overhead, and swap/wait times are CPU wall time, not asynchronous GPU measurements. Startup logs include Game Mode, reported Game State, battery saver, current display refresh and the frame rate request. Show performance overlay reports the port's game/shown FPS, frame time, game work, GX submission, presentation/swap and idle time. This helps separate game timing from an external SurfaceFlinger FPS counter. Android Back returns to the launcher. Each game session uses a fresh process.

Preview 15 fixes the unsafe memory fallback reported by an Adreno 740 tester. If Android occupies the preferred game-arena address, the port reserves another compatible range and updates both SDK and renderer address conversions. It fails startup safely if no compatible range is available. The APK remains ARM64; some legacy game data still stores addresses in 32-bit fields. Confirm a successful title/save/stage load on the affected device before treating the physical-device crash as resolved.

Input test under Settings → Controls opens a landscape monitor without starting the game. It lists Android input devices and USB adapters, and shows live key/button down/up events, scan codes, modifiers, held keys and stick/trigger/hat axes. Bluetooth/wireless keyboards use the same monitor. Both panels scroll; Refresh updates device inventory, Clear resets the sample, and Done returns to the launcher. Controller Back/B is captured for testing instead of closing the screen; system Back still exits. Capture runs only while the test screen is active, with a bounded event history. Export writes sunshine-input.log through the document picker, including phone/build metadata, connected devices and the last 256 events.

For a generic adapter with no game input, open Input test, connect it, Refresh, and press buttons/move sticks. Events here show what Android receives before SDL mapping or game bindings. A USB adapter listed without an Android input device suggests a different problem from a controller producing unmapped keys. Export the sample to identify the device IDs and received codes before choosing a mapping; the monitor itself does not fix or guess unknown controller layouts.

Super Mario Eclipse

SMS Eclipse, the second APK, plays Super Mario Eclipse v1.1.0 by the Eclipse Team. Choose Import disc image and pick the same US revision 0 Sunshine ISO; it must be the unmodified 1:1 image (MD5 0c6d2edae9fdf40dfc410ff1623e4119), since Eclipse's patch is made for that disc. Then choose Download Eclipse: the launcher downloads the Eclipse Team's release (about 851 MB, from SMS Launcher's mirror of its GameBanana release, or GameBanana itself), checks it against its MD5 and patches your disc with it, giving the same GMSE04 image Eclipse's own patcher makes. Allow 3 GB of free space besides the imported disc; the download and your Sunshine copy are deleted once the Eclipse disc is checked. A stopped download continues where it left off. Unpacking the release needs 256 MB of the launcher's memory; on a device that grants less, make the image with Eclipse's patcher on a computer and import that instead (it is checked against MD5 caa546309e0443f7b47632b040a250d7). Its save backups hold the GMSE04_ card files.

Settings → Saves & data has your saves and the game log. Back up your save writes a ZIP you can keep or move to the desktop port's card-a folder. Restore a backup accepts a ZIP containing the desktop port's index.txt, super_mario_sunshine.dat and super_mario_sunshine.stat, at the root or beneath card-a/. Import keeps the prior card in a private saves/previous folder. Use Export before uninstalling: uninstalling deletes the disc, saves, settings and caches. The import does not support Dolphin .gci or whole memory card images.

Game log uses the document picker to save diagnostic output. Disc and save import need no broad storage permission, and gameplay is offline. The Android launchers supply the base game, Super Mario Eclipse and the optional HD textures.

Emulator validation

On an x86 Linux host, the Android 35 Google APIs x86_64 image includes libndk_translation and advertises arm64-v8a. Use the host GPU, KVM, and a GLES 3.1/3.2-capable host driver. Software renderers that expose only GLES 3.0 cannot run this renderer. Verify the emulator's ABI list and GPU before installing; a successful x86 APK test would not validate this ARM64 APK. The Android 30 image's older translator crashes on a valid ARM64 floating-point conversion instruction emitted by the compiler; use the newer image.

adb shell getprop ro.product.cpu.abilist
adb shell getprop ro.dalvik.vm.native.bridge
adb shell dumpsys SurfaceFlinger
adb install -r a