BareDroidNX#
Run Android games natively on Nintendo Switch Horizon OS — without Android
Made by aaronworld.uk · Give it a star ⭐ if you find it interesting!
Disclaimer — Please Read
Approximately 99.8% or more of the code in this project was written by Anthropic's Claude AI (claude-sonnet-4-6). I (the author) am not a developer and am not capable of writing this myself. Claude and I are working on this together as an experiment. I handle testing on real hardware, describe problems, and decide direction; Claude writes and debugs the code.
This project is in extremely early stages. It may not run anything at all reliably yet. Use entirely at your own risk.
What Is This?#
BareDroidNX is a compatibility / translation layer that lets Android native (NDK) games run directly on the Nintendo Switch's Horizon OS — the Switch's real operating system.
This is NOT Android running on the Switch. There is no Android OS, no emulator, no virtual machine. The game's ARM64 machine code runs directly on the Switch's Tegra X1 processor, the same chip Android phones use. A thin shim layer fakes just enough of the Android runtime (libc, OpenGL ES, JNI, asset management) that the game's native .so library doesn't notice it isn't on Android.
Think of it like Wine on Linux — not emulation, but translation.
Current Focus#
We are currently only attempting simple, old, 2D Android games — specifically games that:
- Ship an
arm64-v8anative library (.sofile) - Use OpenGL ES 2.0 or 3.0 for rendering
- Have no Google Play Services dependency
- Save locally (no cloud saves required)
- Do not require network/online connectivity
We are not planning support for online-only games (such as Roblox, Fortnite, etc.) anytime soon — if ever. The project is focused on proving that even the simplest games can run, which is still a significant technical challenge.
Our test target is Hill Climb Racing 1.x by Fingersoft — a simple 2D physics game with no online requirement, widely used in compatibility testing.
What We've Achieved So Far#
- The NRO launches from hbmenu and shows a working APK browser UI
- The Switch correctly extracts the APK — unpacking the game's native libraries and assets from the
.apkfile onto the SD card. This works reliably. - The Switch loads and parses the ELF binary (
libgame.so) and attempts to link it against our shim table - The on-screen progress display shows each launch stage in real time
- Docked mode is detected — the footer warns you when launched in docked mode, since games require touch screen input (handheld only)
- The launcher no longer crashes the Switch when
svcSetMemoryPermissionfails — instead it shows a clear diagnostic screen explaining the blocker - Full diagnostic output is written to
sdmc:/BareDroidNX/compat_log.txt
The game does not yet run — there are outstanding blockers (see Current Blockers) — but the groundwork is there.
Controls#
Handheld mode only. Android games use touch screen input. The Switch touchscreen only works in handheld mode — in docked mode there is no touch input, so games will be uncontrollable. BareDroidNX now detects docked mode and shows a warning in the footer.
BareDroidNX launcher controls (in the APK browser):
| Button | Action |
|---|---|
| D-pad / Left stick | Navigate APK list |
| A | Launch selected APK |
| Y | Rescan APK folder |
| + | Quit |
| B | Back (on result screen) |
Setup#
- Copy
BareDroidNX.nrotosdmc:/switch/ - Place
.apkfiles insdmc:/BareDroidNX/apks/ - Launch from hbmenu (Atmosphere CFW required)
- Navigate with D-pad or left stick, press A to launch
- If a launch fails, check
sdmc:/BareDroidNX/compat_log.txtfor the full error log
Building#
Requires devkitPro with devkitA64 and libnx installed.
export DEVKITPRO=/opt/devkitpro
make
Output: BareDroidNX.nro
Dependencies (via pacman/devkitPro)#
switch-sdl2 switch-sdl2_image switch-sdl2_ttf
switch-libpng switch-libjpeg-turbo switch-minizip
switch-mesa switch-glad
Current Blockers#
These are the known issues preventing Hill Climb Racing from running:
1. Code pages not executable — RESOLVED#
The ELF loader now uses libnx's jitCreate / jitTransitionToExecutable API instead of memalign + svcSetMemoryPermission. This creates a dual-view mapping (writable side for loading, executable side for running) that the Switch kernel allows. The old 0xD801 error no longer occurs.
2. Many unresolved symbols — RESOLVED (120+ symbols shimmed)#
All symbols that appeared in the Hill Climb Racing compat log are now shimmed:
setjmp/longjmp— forwarded to newlibsem_init,sem_wait,sem_post,sem_destroy— single-threaded stubsclock_gettime,nanosleep,gettimeofday,gmtime,localtime,mktime,strftime— newlib passthrough- All wide char/locale functions (
wcslen,wmemcpy,iswupper,setlocale,newlocale, etc.) - Bionic fortified wrappers (
__strlen_chk,__memcpy_chk,__strcat_chk, etc.) - Networking:
socket,connect,recv,send, etc. — all stub returningENOTSUP pthread_mutexattr_init/destroy/settype,sched_yield,syscall,getcwdandroid_set_abort_message,dl_iterate_phdr,sincosf,__sF,__stack_chk_guardstrtoll_l,strtoull_l,strtold_l,vasprintf,stpcpy,strerror,vsscanf
3. Background threads not supported#
pthread_create is stubbed to return a fake handle and never actually spawn a thread. Games that rely on a separate render or physics thread will appear frozen or crash.
4. Game may crash at runtime#
Even with all symbols resolved and code executable, the game could crash during initialisation (NULL deref, bad GLES call, unimplemented JNI method, etc.). The next step is to get a crash address from the compat log and identify which code path is failing.
Performance Expectations (Hill Climb Racing 1.67.0)#
We're testing the .apk release of Hill Climb Racing 1.67.0 specifically — the current Play Store release ships as a .xapk (a zip-of-zips wrapper some third-party distributors use for split/expansion APKs). BareDroidNX's APK parser only understands plain .apk (zip) files right now, so .xapk support is out of scope until a later phase. Pinning to 1.67.0 keeps testing on a format we can actually ingest.
There's no measured frame rate yet — the game hasn't booted far enough to render a single frame. Here's the theoretical ceiling based on the hardware alone:
- Hill Climb Racing is a simple 2D vector-style physics game, originally tuned to hit 60 FPS on 2012-era phones with GPUs far weaker than the Switch's (Adreno 200/203, Mali-400MP class hardware).
- The Switch's Tegra X1 (4× Cortex-A57 @ ~1020 MHz in handheld mode, Maxwell-based GPU) has roughly an order of magnitude more compute than HCR's original minimum-spec target. Raw rendering throughput should not be the bottleneck.
- Theoretical ceiling: a locked 60 FPS — the same cap the game's own engine uses on Android — assuming it boots and renders at all.
- The real risk to frame rate isn't the silicon, it's BareDroidNX's compat layer:
pthread_createis currently stubbed (no real background thread), so any physics/render thread split the game relies on will serialize onto one thread; GLES calls are translated through switch-mesa rather than a native Android GPU driver, adding some per-draw-call overhead. - Docked vs handheld GPU clocks differ a lot on Switch, but it's moot here — the game requires touchscreen input, which only works in handheld mode, so handheld is the only mode worth benchmarking once it runs.
This section gets replaced with real measured numbers once the game boots far enough to render a frame.
TODO / Roadmap#
Items are roughly ordered by priority. "Phase 0" is the current work.
Phase 0 — Make any game do something (in progress)#
- APK browser UI with icon extraction
- APK extraction (libs + assets) onto SD card
- Custom ARM64 ELF loader with RELA relocation
- JNI environment shim (fake JavaVM / JNIEnv)
- EGL setup (GLES 2 + 3 shim table passthrough)
- On-screen progress display during launch stages
- Full diagnostic result screen with error details
- Docked mode detection — footer warns when not in handheld mode
- Early abort when code pages are not executable — shows diagnostic screen instead of crashing Switch
- Fix code-page permissions — ELF loader now uses libnx
jitCreate/jitTransitionToExecutablefor dual-view RW+Rx mapping - All 120+ unresolved symbols shimmed —
sem_*,clock_gettime,nanosleep,gettimeofday, wide-char, locale, Bionic fortified wrappers, networking stubs,android_set_abort_message,sincosf,setjmp/longjmp,__sF,__stack_chk_guard - Per-constructor logging — each of the 417
DT_INIT_ARRAYconstructors is logged (address + index) with an immediate flush before it's called, so the crash site shows incompat_log.txtwhen the Switch dies mid-constructor - Load all .so files — all three libs loaded smallest-first so cross-library symbols are available before any constructors run
- 40+ new shims — signal handling (
sigaction,sigemptyset/fill/add/del,pthread_sigmask), thread naming (pthread_setname_np,prctl), process info (gettid,getpid,getauxval), memory (mprotect,mmap/munmap), barriers,sleep/usleep,strtod_l/strtof_l,access,lstat,chmod,ioctl,pipe,dup/dup2,raise,kill,pthread_kill,__register_atfork,clock_nanosleep - Heap staging buffer for ELF loading — segment copy, relocation, and dynamic parsing now happen on a plain heap buffer; only a single bulk
memcpytouches JIT-writable memory, right beforejitTransitionToExecutable. This fixed a hard crash on the very first write to JIT memory. - Per-relocation-entry logging + bounds checks in
applyRela— every RELA/JMPREL entry now logs its index, type, symbol index, and resolved name before being applied;sym.st_nameis now bounds-checked againstDT_STRSZbefore being dereferenced as a string (the gap-derivedsym_countheuristic can overrun into.gnu.versiondata sitting between.dynsymand.dynstr, producing garbage string pointers — this is the leading suspect for the current crash).R_AARCH64_COPYsizes are now capped at 64KB to guard against a bogusst_size. PT_LOAD segment copies and thePT_DYNAMICpointer are now bounds-checked against the staging buffer too. - Identify relocation crash — need a new compat_log run; the last logged
RELA[n/3060]/JMPREL[n/...]entry before the crash is the culprit - Real touch input delivery via
AInputQueue/ALooper
Phase 1 — Touch input#
- Map Switch touchscreen events to
AInputQueuetouch events delivered to the game - Docked-mode detection — footer shows warning when not in handheld mode
- Block launch in docked mode with a hard block + clear message ("Controls only work in handheld mode — the game requires touch input")
Phase 2 — Stability#
- Real
pthreadsupport using libnxThread(for games with background render/physics threads) - Load all
.sofiles in dependency order (smallest-first) — was previously only loading the largest - Implement
dl_iterate_phdrso stack unwinders work - Save/load state via proper
internalDataPathon the SD card - Catch and display fatal signal info on crash instead of hard-locking
Phase 3 — Polish#
- Add NRO icon (author: aaronworld.uk)
- Version bump system — each build increments
APP_VERSION - Per-APK settings overlay (resolution, framerate cap)
- APK delete / manage from the UI
- WebP icon decoding (
IMG_INIT_WEBP) + WebP fallback candidates — many modern app icons ship as WebP, not PNG - Linear texture scaling (
SDL_HINT_RENDER_SCALE_QUALITY) for smoother icon downscaling - Colored monogram placeholder (Android-style initial + hashed color) replaces the flat gray box when no icon is found
- Larger APK list icons (72px → 84px) and result-screen icon (96px → 112px)
- APK file size shown in the list (e.g. "42.1 MB")
Not Planned (for now)#
- Online / multiplayer games (Roblox, Fortnite, etc.)
- Google Play Services (GMS) stub
- ARM32 (
armeabi-v7a) game support — Switch is 64-bit only
Changelog / Change Checklist#
This section is updated with every significant change. Most recent first.
[Unreleased / Current Build]#
- Performance Expectations section — theoretical 60 FPS ceiling for Hill Climb Racing 1.67.0 based on Tegra X1 vs. the game's original minimum-spec hardware, plus the
.apkvs.xapknote explaining why we test 1.67.0 instead of the latest release - APK chooser QoL: WebP icon decoding, linear icon scaling, colored monogram placeholders for missing icons, larger icons, file size shown per APK
- Heap staging buffer for ELF loading — fixed a hard crash on the first write to JIT-writable memory by doing all segment copy / relocation / dynamic parsing on a heap buffer, then a single bulk
memcpyinto JIT memory right beforejitTransitionToExecutable - Per-relocation-entry logging + bounds checks in
applyRela()— logs every RELA/JMPREL entry's index/type/symbol before applying it, bounds-checkssym.st_nameagainstDT_STRSZ(guards against the gap-derivedsym_countoverrunning into.gnu.versiondata), and capsR_AARCH64_COPYsize at 64KB - Per-constructor logging —
elfRunCtors()logs each constructor address + index and flushes tocompat_log.txtbefore calling it; the last logged entry before a Switch crash pinpoints the culprit - Load all .so files — replaced
findMainSowithfindAllSos; all three libs are loaded smallest-first so cross-library symbol resolution works before any constructors run - 40+ new shims —
sigaction,sigemptyset,sigfillset,sigaddset,sigdelset,sigismember,pthread_sigmask,sigprocmask,prctl,gettid,getpid,getuid,getgid,getauxval,mprotect,mmap,munmap,pipe,dup,dup2,ioctl,access,chmod,fchmod,lstat,pthread_setname_np,pthread_getname_np,pthread_attr_setstack,pthread_barrier_*,kill,raise,pthread_kill,sleep,usleep,clock_nanosleep,strtod_l,strtof_l,__register_atfork - ELF loader: replace
memalign+svcSetMemoryPermissionwith libnx JIT API —jitCreate/jitTransitionToWritable/jitTransitionToExecutablegive a dual-view RW+Rx mapping; the 0xD801 blocker is resolved - JIT dual-mapping: relocations are written to the writable (
rw_addr) side; GOT entries store exec (rx_addr) addresses; symtab/strtab are copied to heap before the JIT transition unmaps the write side - 120+ new shims:
setjmp/longjmp,sem_*, all time functions, all wide-char and locale functions, all Bionic fortified string wrappers, networking stubs (returnENOTSUP),sched_yield,syscall,sysconf,getcwd,dl_iterate_phdr,android_set_abort_message,sincosf,stpcpy,vasprintf,vsscanf,strerror,strtold,puts/putchar,rename/remove,__sFdata stub,__stack_chk_guardaddress,pthread_mutexattr_*,__cxa_finalize - Capture
svcSetMemoryPermissionresult code from ELF loader and surface it on the diagnostic screen - Abort launch early when code pages are not executable — shows diagnostic screen instead of hard-crashing the Switch
- Add "Checking code permissions" progress step so the user sees where the launch stopped
- Docked mode detection — footer turns amber and warns that games need handheld (touch screen) mode
- Add
LaunchResultstruct —launchApk()now returns structured error info instead of a bare bool - Add
ProgressCbcallback — UI shows each launch stage on-screen in real time - Replace "(loader not yet implemented)" stub screen with a proper diagnostic result screen showing failure stage, unresolved symbol count, and svc error codes
- Track and display unresolved ELF symbol count after each load
- Change NRO author to
aaronworld.uk - Improve README with badges, profile link, AI disclaimer, in-depth TODO
0.1.0 — Initial release#
- APK browser UI (SDL2, 1280×720)
- APK icon + metadata extraction (AndroidManifest.xml + resources.arsc parser)
- Full APK extraction to SD card (libs + assets)
- Custom ARM64 ELF loader with RELA relocation processing
- JNI / JavaVM fake environment
- GLES 2 + GLES 3 shim table (400+ functions)
- EGL setup via switch-mesa
- SD card logging (
log.txt+compat_log.txt)
License#
This project is licensed under the MIT License — see LICENSE for the full text.
In plain English: You can use, copy, modify, share, and even sell this code freely, as long as you keep the copyright notice. There is no warranty — if it breaks your Switch, that's on you (please use CFW responsibly). You do not need to open-source any modifications you make, but you must include the original copyright line.
The MIT license does not cover the Android games themselves — those belong to their respective developers. BareDroidNX only provides the compatibility layer.
About#
Made by Aaron — a non-developer who wanted to see Android games on their modded Switch, and is figuring it out one step at a time with the help of Claude AI.
~99.8% of all code in this repo was written by Claude (claude-sonnet-4-6 model). I describe the problem, test on real hardware, and point out what's broken. Claude writes the fixes. This is an honest experiment in AI-assisted hardware hacking.
If this interests you, star the repo and check back. Progress will be slow but real.