Skip to content
justr4mziPublic
forked from c22dev/mush8

About

A native Mach-O iOS/macOS port of Mario Kart 8 Deluxe, with LAN, online and wireless multiplayer

Resources

Stars

2 stars

Watchers

0 watching

Forks

 
 

Repository files navigation

Mush8 — 6 GB iPhone fork

A fork of c22dev/mush8, the native iOS/macOS port of Mario Kart 8 Deluxe. All the hard work (the port itself) is c22dev's. This fork only adds a layer of patches so the game runs on sideloaded iPhones with 6 GB of RAM (tested on an iPhone 15, iOS 27), with built-in performance settings.

You need your own game dump. Nothing from the game is in this repository.


Why this fork?

Upstream Mush8 asks for 6 GB of RAM or more ("8 GB is comfy"). On a 6 GB iPhone with an .ipa installed through SideStore / AltStore, iOS lets one app use about 3 GB (about 3.07 GB measured on an iPhone 15), and the sideloaded build doesn't get the extended memory entitlements. On top of that, the A15/A16 GPUs can't read the Switch's BC-compressed textures, so they're decoded on the CPU into much bigger textures.

In practice, the stock build was killed by iOS when loading the character select or a race, froze on menus while shaders compiled, and showed black or invisible objects.

This fork makes it fit in about 3 GB and stay playable: a race peaks around 2.6 to 2.9 GB, at a steady 30 fps on screen (the game itself still runs at 60 updates per second).

What's added

Everything lives in patch-heap.py: it is applied on top of upstream at build time (each section finds an exact piece of upstream code and replaces it, and stops the build if upstream changed). Plus a few GitHub workflows and tools.

Memory (the 3 GB limit)

Where the memory goes on an iPhone 15, measured with the logs below: the game's own RAM (the 3 GB Switch heap, which iOS compresses in part) is about 2 GB of the footprint on the menus; our own Metal objects are the rest. A race load adds 400 to 700 MB in a few seconds, which is when iOS kills the app.

  • Smaller textures (launcher "Textures" setting): the sharpest mip levels of big BC textures are not decoded and the texture is stored smaller. Ultra low = 8x less per side for textures of 128 px and more.
  • BC1 textures as ASTC: iPhone GPUs can't read the Switch's BC textures, so they're decoded on the CPU. BC1 textures are now rewritten block by block as ASTC 4x4, a format the iPhone reads natively: 1 byte per pixel instead of 4, same colours (within 2/255, checked on 20 000 random blocks against reference decoders; a block that mixes transparent pixels with several colours gets their average colour).
  • BC3 textures as ASTC too, per texture: same colours as before (within 4/255), alpha exact on opaque, constant and cut-out (on/off) blocks. ASTC keeps only 4 alpha levels in a 4x4 block whose alpha varies, so a texture with soft alpha gradients (smoke, glass...) on more than 1% of its blocks stays RGBA (no visible loss). Measured on Mario Circuit: BC3 textures took 113 MB decoded, ~28 MB as ASTC. NX_BC3_ASTC=0 turns it off. BC4/BC5/BC7 are still decoded (BC4 to 1 byte per pixel, BC5 to 2). NX_BC1_ASTC=0 turns it off. Measured on the iPhone during a race: BC1 textures take 131 MB as ASTC, they'd take ~524 MB decoded; the race peaked at 2.57 GB with 500 MB to spare (it used to be killed).
  • Low-memory trim (NX_TRIM_MB, 900 MB by default): when iOS allows less than that, textures unused for 5 s are dropped (they come back when needed).
  • Emergency trim (NX_TRIM_EMERGENCY_MB, 400 MB by default): checked every 8 new textures while a race loads; below that, every texture not used right now is dropped. The prewarmed pipelines are kept (releasing them freed nothing measurable on the iPhone and left objects black for 2 laps while 96 of them were rebuilt); they're only released below 150 MB.
  • Purgeable textures: textures the game can rewrite, unused for ~2 s (120 frames), are marked "volatile": iOS takes that memory back first instead of killing the app, and they're decoded again if needed (NX_PURGEABLE=0 turns it off). NX_PURGEABLE_ALL=1 does it for every decoded texture: tried on the iPhone, iOS took back 1458 textures in one race and decoding them again cost FPS and memory, so it's off.
  • Draw data buffers: 4 MB pieces instead of 16 MB (less half-empty space per frame in flight), at most 4 spare pieces kept, none when less than 600 MB is left (they used 96 MB on the iPhone).
  • Upload limit: at most 96 MB of texture upload buffers in flight while a race loads, 24 MB when less than 600 MB is left.
  • Detailed memory log (every 10 s, see "Reading the log").

Shaders (no more endless freezes or black objects)

  • Pipeline prewarm at startup, which pauses as soon as the game needs a pipeline.
  • Prewarmed pipelines are kept in memory (NX_KEEP_PREWARMED=0 for the old behaviour): before, they were thrown away and rebuilt from the archive at the start of a race, which showed black objects for 0.5 to 10 s (up to 2 laps when memory is tight). The ones not used yet are let go only when less than 120 MB is left: letting all ~10,000 go saved just 19 MB (Oct 30; the memory the prewarm adds is the shader libraries, which stay) and the race then rebuilt them from the archive (dips to 35-45 updates/s). [mem] … letting the prewarmed pipelines go in the log.
  • Device pipeline archive (work/devarch/): every pipeline compiled on the phone is saved and found again on the next launch. It is cleared by itself when iOS or the GPU changes (iOS update, work/ copied from another iPhone), since an archive made for another iOS/GPU is useless. Limit: 1500 MB / 800 files (~70 KB per pipeline, the whole 10,000-pipeline list takes ~750 MB). It was 300 MB: full at ~6,000 pipelines, so the rest was compiled again at every launch (8 min of prewarm each time). The full prewarm also waits until the newly compiled pipelines are written to it ("Saving shaders…", at most 15 min, only when there are new ones): before, a game that ended early lost the unwritten ones and compiled them again.
  • Failed pipelines are retried after 1 s and 3 s (the iOS shader compiler sometimes dies mid-build) instead of staying black until the next launch.
  • Recorded pipeline list shipped in the app (data/pipelines.tsv, 10442 pipelines (Nov 6: +55 from Cnsw_32 / Cnsw_35; Nov 1: +220 recorded on the iPhone on Rainbow Road / Cnsw_48 / Cnsw_54; Oct 22: +6466 from the overnight VS sessions on 91 of the 96 tracks and 4 of the 8 battle arenas), from iPhone runs and the Mac smoke runs): their shaders are compiled ahead of time on the build Mac, so a phone that never ran the game doesn't have to compile them for minutes.
  • Full prewarm before playing: holds the game until every known pipeline is ready, with a progress screen.

Speed

  • Graphics on a separate thread (on by default): the game and the translation of its graphics to Metal run on two CPU cores at the same time instead of one after the other. Measured on an iPhone 15: ~45 game fps instead of ~36 on heavy scenes.
  • 30 fps lock (frameskip): one frame out of two is drawn, evenly paced, while the game still updates at 60 Hz (multiplayer timing unchanged). Render targets drawn rarely are never skipped.
  • Speed first, adaptive 20-60 fps (Nov 1: above 30 too, up to NX_FPS_MAX, default 60, 2 fps at a time, as long as the race stays at full speed; above 30 fps frames are shown at the next 1/60 s) (on with the 30 fps lock, NX_SPEED_FIRST=0 for the plain lock): the game's own speed comes before the frame rate (MK8 moves the race one step per update: 30 updates/s = slow motion at 50 %). While the game makes fewer than ~58 updates/s, the drawn frame rate goes down 1 fps at a time (30, 29, 28 ... down to NX_FPS_MIN, default 20), but only if that really makes the game faster: the game speed is measured over ~1.5 s at each rate; a lower rate is kept only if the game got at least 2 % faster, otherwise it goes back up (Oct 30: the game made 56-59 updates/s at 30 and at 20 fps alike, and the first version sat at 20 fps for nothing). It goes down only while the game waits for the GPU (more than 2.5 ms per update, 2 fps at a time over 6 ms); if the game is slow without waiting for the GPU, the CPU is the limit and fewer frames would not help, so it goes back up. At full speed it tries one fps higher. Frames drawn are spread evenly. [speed] game at … updates/s with N fps drawn: now M fps in the log.
  • Frame pacing catches up: skipped frames keep a fixed 60 Hz grid; after a late frame (GPU fence wait, slow update) the next ones don't sleep until the game is back on time, instead of losing that time for good (the game averaged 55-58 updates/s with the CPU mostly idle). NX_SKIP_MAX=1 = always 30 fps. (Oct 22: steps of 30/20/15 fps; before that the "smart 30 lock" drew every frame when the game was slow and slowed it down further.)
  • Shake = trick: with the touch controls, shaking the phone (more than 2.6 g) presses R for 150 ms: on a ramp it does the trick and its small boost, like shaking a Joy-Con in the original game. NX_SHAKE=0 off, NX_SHAKE_G strength (default 2.6). [input] shake in the log.
  • Texture change checks every 2 frames instead of every frame.
  • Archive writer does not block the game: the device archive writer compiles pipelines into its own new archive file without the archive lock; only the game's lookups take it (Oct 31: track loads stalled up to 15 s, "no frame for 6 s", while the writer held the lock during its compiles).
  • GPU starts earlier: a game submit is split into several Metal command buffers, every NX_SPLIT_DRAWS draws (default 80, 0 = off) between render passes, so the GPU works on the first part while the rest is encoded (the GPU was ~50 % busy yet the game waited 5-8 ms per update for it).
  • Archive lookups in parallel: pipelines found in the archives are created on all shader threads at once (Oct 31: at a track start one lookup at a time under a lock, 5-15 s stalls, buttons answered 20 s late). NX_ARCHIVE_LOCK=1 puts the lock back.
  • Smooth menus with preview videos: when the menu video decoder is behind, the game waits at most 8 ms (NX_MOVIE_WAIT_MS, was 250 ms) and shows the previous video frame (the mode menu had a 250 ms hitch every second).
  • GPU: relaxed shader math: shaders are compiled with Metal's relaxed math mode (Apple's default on Apple silicon; still honors Inf/NaN) instead of safe, so the compiler can simplify floating-point math. Position invariance stays on. The shader cache key changed with it (every shader compiled again once).
  • GPU: anisotropic filtering capped at 4x (NX_MAX_ANISO, 1-16, the game asks up to 16x): less texture bandwidth per pixel, the difference is only visible on surfaces seen at a very low angle.
  • GPU profile (with NX_FPS): [prof] gpu passes per drawn frame = render passes, attachments loaded from memory and MB loaded/stored per drawn frame (an A16 is a tile GPU: each pass moves its attachments through memory), and how many passes use the same targets as the pass just before (candidates to merge).
  • Null index buffers (from upstream, found by 184904): indexed draws with a null or overflowing index buffer are skipped instead of making the GPU read address 0 ([nvn] skipping draw …, logged once).
  • Memory as without extra RAM: the memory left used by every trim is the smaller of what iOS says and 3070 MB minus what the app uses (NX_MEM_LIMIT_MB, 0 = iOS only), so the app behaves the same with or without SideStore's "Get more RAM" (with it, the trims never ran and render targets kept growing).
  • Graphics thread profile: [prof] graphics thread (10 s): Draw 61 % (… ms), … = which game commands take the graphics thread's time (it turns the game's commands into Metal; ~12 ms per update in the Oct 31 race, more than the GPU).
  • GPU errors: [gpu] GPU error on a frame: … with Metal's reason when a command buffer fails on the GPU (iOS can end an app whose GPU work keeps failing without any crash report), [gpu] a frame took the GPU N s for very slow ones.
  • Crash clues: [mem] … heat now … at every change of the phone's heat state, and [mem] … only N MB left every second once less than 300 MB is left (iOS kills without a report leave nothing else).
  • Fewer scheduler passes: the game makes thousands of unlock/signal system calls per second, most with nobody waiting; after those, the second scheduling check (global lock + walk over all threads) is skipped.
  • Game thread priorities: the game's main thread (game logic) and the thread that presents frames get "interactive" priority (like our graphics thread), so iOS keeps them on the 2 fast cores of the A15/A16 when the phone is hot (NX_STOCK_QOS=1 for the old priorities). The phone logs showed the FPS drops come from the game's own CPU work when iOS slows a hot phone down, not from the GPU or the graphics translation.

Fixes

  • Menu freeze after "Single Player": the video decoder waited for its next frame while holding a lock the game needed; it now waits at most 250 ms and shows the previous frame.
  • An open render pass dropped by an early exit no longer kills the app ("Command encoder released without endEncoding", seen on a macOS 14 Mac).
  • Crash while looking up a pipeline: Metal threw an exception ("-[OS_dispatch_data data]: unrecognized selector") when a pipeline was looked up in the archives while another thread wrote to an archive; all archive use now holds one lock. That was not enough (still 45 crashes overnight on the Macs): after an archive miss the full compile still had the archives attached, so Metal searched them again outside the lock. The archives are now detached before compiling a miss. Objective-C exceptions are logged with our call stack.
  • Mii GetIndex answers "not found" instead of a bogus index; SignalToAddress (kind 2) follows Atmosphère/yuzu; iOS link fix ("compact unwind ... doesn't fit in 24 bits").
  • Watchdog: if no frame is shown for 6 s, every thread's position is written to the log.

Launcher options

Option Default What it does
Console mode Handheld (720p) Handheld is lighter on the GPU and the battery
30 fps lock (frameskip) On Draws 1 frame out of 2, evenly paced
Textures Ultra low Ultra low: least memory (recommended). Very low: a bit sharper, a few hundred MB more. Low: only huge textures (2048 px+) shrunk. Full: no reduction (runs out of memory)
Background texture loading Off Experimental. Fewer small hitches, but it can run out of memory while a race loads
Graphics on a separate thread On Game and graphics on two CPU cores at once (more FPS)
Unlocked save Off Plays with a save packed in the IPA (everything unlocked) in work/save-unlocked; your own save in work/save is never touched. Uses ~200 MB more in races
Show FPS / Show memory On Two small counters at the top of the screen
Music from other apps On Keeps Spotify & co playing, the game sound is mixed in
Game volume 100 % Game sound only
Vibration / Motion controls / iPhone controllers On / Off / Off
Prewarm pipelines On Builds known pipelines at startup
Full prewarm before playing On Waits until all of them are ready (seconds once the archive is filled)
Full prewarm on first launch only Off Same, only while work/devarch is empty (new phone)
Background shader compiling On Off = compile while the game waits (more freezes)
Quiet mode (no log) Off No log, no watchdog: a bit less CPU. Turn it off to send a log

Tools: Share the log / Share the previous log, Share the pipeline list (work/pipelines.tsv, to merge into data/pipelines.tsv), Clean up unneeded files, Clear the shader cache, Reset settings to default (asks first).

env.txt (advanced settings)

A text file named env.txt in the app's folder (Files → On My iPhone → Mush8, next to prod.keys) sets NX_* options, one per line, NAME=value; lines starting with # are ignored. It is read at each launch. Example:

# warn earlier when memory gets low
NX_TRIM_EMERGENCY_MB=800
# a crash test without the ASTC textures
NX_BC1_ASTC=0

Order: env.txt is read first, then the launcher settings. Most launcher settings only fill in what env.txt didn't set, so env.txt wins. A few are always taken from the launcher (marked launcher wins below). Delete the file (or the line) to go back to the defaults.

Memory

Variable Default Effect
NX_TRIM_MB 900 Below this much memory left (MB), drop textures unused for 5 s
NX_TRIM_EMERGENCY_MB 400 Below this, while loading: release prewarmed pipelines and every texture not used right now. 0 = off
NX_BC1_ASTC 1 0 = decode BC1 textures to RGBA like before (4x more memory)
NX_BC3_ASTC 1 0 = decode BC3 textures to RGBA like before
NX_PURGEABLE 1 0 = don't let iOS reclaim unused textures
NX_PURGEABLE_ALL 0 1 = every decoded texture can be reclaimed by iOS (costs FPS)
NX_KEEP_PREWARMED 1 0 = drop prewarmed pipelines after the prewarm (less memory, black objects for a moment)
NX_TEX_DROP 3 (Ultra low) Mip levels not decoded on big BC textures (0 to 3). Setting it here replaces the launcher's Textures setting
NX_TEX_DROP_SHRINK 1 Store the texture smaller (with NX_TEX_DROP), instead of an enlarged copy
NX_TEX_DROP_MIN 128 (Ultra low), 256 Don't shrink below this size (px)
NX_TEX_DROP_ALL off Also shrink cube maps and BC6 lighting (can turn tracks dark)
NX_TEX_SHRINK_MIN 2048 (Low) Only shrink textures at least this big
NX_TEX_16BIT 0 1/2 = 16-bit colour textures (half the memory, visible banding)
NX_SYNC_TEXTURES 1 Load textures on the game's thread (launcher "Background texture loading" off)
NX_NO_TEX_CACHE 1 1 = no decoded-texture cache on disk (work/texcache)
NX_TEX_CACHE_MB 1024 Size of that cache when it is on
NX_NO_MEMLOG off 1 = no memory lines in the log

Graphics and speed

Variable Default Effect
NX_ASYNC_GFX 1 Graphics on a separate thread (0 = off)
NX_FRAMESKIP 1 Frames skipped between two drawn ones (0 = none, max 4)
NX_SPEED_FIRST 1 0 = plain 30 fps lock (1 frame out of 2 even when the game is slow)
NX_MEM_LIMIT_MB 3070 Memory limit the trims assume (0 = only what iOS says)
NX_MAX_ANISO 4 Highest anisotropic filtering (the game asks up to 16)
NX_ARCHIVE_LOCK 0 1 = one pipeline archive lookup at a time (old behaviour)
NX_MOVIE_WAIT_MS 8 Longest wait for a menu video frame before showing the previous one
NX_FPS_MAX 60 Speed first: highest frame rate drawn (20-60; 30 = never above 30)
NX_SPLIT_DRAWS 80 Commit the GPU work every N draws inside a submit (0 = one command buffer per submit)
NX_AA on (launcher: Anti-aliasing; NX_AA=0 turns it off) 1 = anti-aliasing (FXAA + light sharpening) run on the game's 720p picture before it is scaled to the screen (first version ran at screen resolution and slowed the Grand Prix menu by ~20 s when hot). Off by default
NX_COMPILE_WAIT_MS 250 Longest a frame waits for a shader/pipeline compile needed by a new render target; after that the game goes on and the effect pops in when ready (was: wait until done, up to 29 s freezes on Rainbow Road when RAM is full). 100000 = old behaviour
NX_SPEED_TARGET 57 (58 in LAN / Online / Wireless) Game updates/s counted as full speed by the adaptive fps (60 = exact, 50 = more frames, slower race when hot). Multiplayer defaults a bit higher so a phone never lags behind the others
NX_FPS_MIN 20 Speed first: lowest frame rate drawn when the game is slow (15-30)
NX_SKIP_MAX - 1 = always 30 fps (game may slow down when hot)
NX_SHAKE / NX_SHAKE_G 1 / 2.6 Shake the phone = trick (R), strength needed in g
NX_AUTOSKIP off 1 = skip frames only when late
NX_HASH_EVERY 2 Check textures for changes every N frames (1 to 30)
NX_HANDHELD 1 Handheld (720p) / docked — launcher wins
NX_FPS 1 FPS measurement for the counter and the log
NX_HIDE_FPS, NX_HIDE_MEM off Hide the counters — launcher wins

Shaders and pipelines

Variable Default Effect
NX_PREWARM 1 Prewarm at startup — launcher wins
NX_PREWARM_FULL 1 Wait for the full prewarm before playing
NX_SYNC_SHADERS off Compile while the game waits — launcher wins
NX_ASYNC_ALL off Never wait for a pipeline, even on a brand-new render target
NX_NO_DEVICE_ARCHIVE off Don't use work/devarch
NX_PIPELINE_LIST the app's list Pipeline list to prewarm
NX_RECORD_PIPELINES off Record new pipelines into the shipped list instead of work/pipelines.tsv
NX_SKIP_SHADERS — Comma-separated shader hashes not to draw (debugging)

Sound, controls, network

Variable Default Effect
NX_VOLUME 1.0 Game volume, 0.0 to 1.0
NX_RUMBLE, NX_MOTION, NX_PHONE_PADS 1, 0, 0 Vibration, motion controls, iPhone controllers — launcher wins
NX_EXTRA_PADS 0 Extra virtual controllers
NX_LANGUAGE iPhone's language Game language (e.g. fr, en-US)
NX_NET, NX_LAN, NX_LANPLAY_SERVER launcher Multiplayer mode / server — launcher wins
NX_SAVE_DIR work/save Save folder (the Unlocked save option sets it)

Debugging (for logs, slow)

NX_TRACE_ROMFS (game files read), NX_TRACE_IPC, NX_TRACE_SVC, NX_TRACE_BSD, NX_TRACE_MOVIE, NX_TRACE_THREADS, NX_TRACE_TRACKS (track names, on by default), NX_MUTEX_LOG, NX_NVN_LOG=<n> (graphics API calls logged, 400 by default), NX_DUMP_EVERY=<s> / NX_DUMP_AT=<s,s,...> (screenshots into work/), NX_DUMP_FRAME, NX_DUMP_MSL, NX_DUMP_SHADERS, NX_SOFT_BC=1 (decode BC on the CPU like an iPhone, on a Mac), NX_NO_WATCHDOG, NX_AUTOINPUT (scripted buttons, below), NX_STOCK_QOS, NX_NO_SCHED, NX_COMPACT_MEMORY.

Reading the log

Share it from the launcher (Share the log; after a crash, Share the previous log).

  • [mem] 43 s: app uses 1973 MB, iOS still allows 1098 MB — every 10 s. The app is killed when "still allows" reaches ~0. jump to X MB = a big rise in 1 s (race loading).
  • [mem] detail: game RAM … Metal … | resident … compressed … footprint … — game RAM = the Switch heap pages in memory; Metal = everything given to the GPU (includes the 3 GB heap); compressed = what iOS squeezed (it slows everything down when it grows fast).
  • [mem] Metal besides the game RAM: X MB = textures …, draw scratch …, block copies …, uploads in flight …, other … — our own GPU memory, by kind.
  • [mem] textures: … and [mem] BC by format … — every ~10 s, texture count and size, BC textures by format.
  • [game] track: Gu_WaterPark (app uses 2300 MB) — a track loaded (NX_TRACE_TRACKS=0 to turn off).
  • [cpu] …% of a core in all: guest-0x51 60%, gfx-submit 25%, … and [cpu] system calls: … — every 10 s, the CPU used by each thread and the game's most frequent system calls.
  • [mem] phone: heat … — serious/critical = iOS slows the CPU/GPU down (lower FPS, slower shader builds).
  • [gfx] pipeline …: not in the archive, compiling / building took N ms — a pipeline seen for the first time; long times = memory or heat pressure.
  • [fps] … / [prof] … — frame rate and where the time goes. [prof] game thread: graphics calls X ms (of which waiting for GPU fences Y ms) = time the game spends in the graphics calls we translate; every 10 s [prof] slowest graphics calls lists the 6 that took the most time. With the 30 fps lock, 30 fps on screen needs the game itself at 60 (one frame out of two is drawn).
  • [hang] no frame for 6 s — the watchdog: what every thread was doing.

Building the IPA with GitHub Actions

No Mac needed: the Build Mush8 IPA workflow builds on a GitHub Mac and publishes an AES-encrypted zip (Mush8-ipa) as an artifact.

Repository secrets:

  • KEYS_URL, BASE_URL, UPDATE_URL: private links to your prod.keys, base game and update 4.0.1 (Google Drive or Dropbox)
  • IPA_PASSWORD: password of the zip
  • SAVE_URL (optional): zip / rar / 7z of a save folder, packed in the IPA for the "Unlocked save" option

Inputs: runner (macos-26 by default, macos-15 works too: the newest Xcode is selected). The game code and the iOS shader cache are kept in the Actions cache, so later builds skip the 12 GB download unless the pipeline list changes. Build logs go to the ci-logs branch.

Other workflows:

  • Check patch: compiles with the patches for macOS and iOS, no game files, ~10 min.
  • The long Mac workflows (Mac smoke test: hours of the game running on GitHub Macs to collect pipelines; A16 lab) were removed: GitHub disabled Actions on the first repository because of them. Only short jobs now (IPA build ~20 min, Check patch ~10 min); new pipelines come from the phone (Share the pipeline list).
  • Game files for the workflows can come from Google Drive through a service account (GDRIVE_SA_JSON), which avoids Drive's "too many downloads" limit.

Install the .ipa with SideStore / AltStore.


Upstream: how Mush8 works

The Switch and Apple Silicon both run arm64, so the game's code can almost run as is. A few instructions can't run outside a Switch (the x18 register, system calls, system registers), so they're rewritten by nxpatch. nxhost plays the role of the Switch's OS (Horizon) and its services (files, saves, controllers, audio, network...) and turns its graphics API (NVN) into Metal. Game shaders are translated to Metal and compiled ahead of time. On iOS (no JIT) the game code is linked into the app at build time.

Game files

Your own dump: prod.keys, the base game (.nsp), update 4.0.1 (.nsp), optionally the Booster Course Pass (.nsp) and a Firmware folder. Import them from the app or copy them in Files → On My iPhone → Mush8.

Building locally (Mac)

./build.sh, or ./make-app.sh (macOS), ./make-ios.sh --install / ./make-ios.sh --ipa (iOS); run python3 patch-heap.py first to apply this fork's patches. Needs an Apple Silicon Mac, Rust, Xcode 26+, xcodegen.

Multiplayer

In the launcher, under Multiplayer: LAN (same network, a Switch can join), Online (any lan-play.com server) or Wireless (nearby Mush8 players, no router). The 30 fps lock doesn't change the game's timing, so it's fine in multiplayer; keep both phones' shaders ready (full prewarm) to avoid freezes.

  • iOS app compiled for the A15 and newer (-C target-cpu=apple-a15).
  • Touch + and − must be held 250 ms (NX_PAUSE_HOLD_MS, 0 = instant): no accidental pause in races.
  • Nov 6 fixes: sound restarted if iOS stops it (interruption), at most 2 urgent compiles at once (low-RAM crash with 8 at once).
  • Nov 5 fixes: shake detection moved off the input thread (a blocking CoreMotion call froze the controls), touch controls reset when the app leaves/returns to the foreground, Anti-aliasing toggle in the launcher.

Two iPhones together (this fork)

Same IPA on both phones. Before the first race together:

  1. Launch the game once on each phone, alone, with Full prewarm on, and let it finish (on a phone that never ran this build, e.g. an iPhone 14 / A15, the first prewarm compiles every pipeline on the phone: plug it in, it is long only the first time; it is saved in work/devarch). The GPU cache of one phone cannot be copied to a different chip.
  2. On both phones choose the same Multiplayer mode in the launcher: LAN if both are on the same Wi-Fi, or Wireless with no router (Wi-Fi and Bluetooth on, both phones close). Allow Local Network when iOS asks (Settings → Privacy & Security → Local Network → Mush8 if you said no).
  3. In the game: LAN Play (shows by itself on the main menu when the launcher is in LAN mode). One phone creates the room, the other joins. The 30 fps lock / adaptive fps only skips drawing frames, the game logic stays at 60 updates/s, so phones at different fps play together fine. Our patches don't touch the network code (it is upstream's: Bonjour discovery + unicast copies of broadcasts, so it works without Apple's multicast entitlement).

Controls

Any controller (Switch Pro, Joy-Con, DualSense, Xbox...), touch controls on iOS when no controller is connected, optional motion controls. An iPhone can also be a controller for a Mac ("Use as a controller").

Upstream limitations

  • sideloaded .ipa / free Apple IDs don't get extended virtual addressing (smaller memory layout)
  • Nintendo's online play is gone, use Online (lan-play)
  • 5 to 8 players split screen is Switch 2 only
  • no amiibo, WebM videos play as a slideshow on iOS

Credits

  • c22dev — Mush8
  • This fork: memory, shader, frame pacing and launcher patches for 6 GB iPhones

About

A native Mach-O iOS/macOS port of Mario Kart 8 Deluxe, with LAN, online and wireless multiplayer

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages