A gallery of seventeen scenes in a NativeActivity, drawn by raylib and driven by Jolt (Clojure on Chez Scheme) as native arm64 code. No JVM, no Kotlin, no Java anywhere in the app, and no dependencies.
Work in progress: this has not yet been run on Android hardware. What follows is read off the pinned raylib source and the code in this tree, not measured on a phone.
These docs are also a moving target: Jolt and the libraries this project sits on (raylib-jlt, jolt-lang/nrepl, the pinned raylib revision) are still evolving, so a detail here can go stale between one release and the next.
src/platforms/rcore_android.c is the platform layer: EGL,
GLES2, touch, the activity lifecycle and the ALooper pump, all upstream.
What it asks of an app is extern int main(int, char **),
which android_main calls after storing the
android_app — and then it calls
ANativeActivity_finish, so returning from
main quits the app.
That main is twenty lines of C. It dlopens the library
jolt build --library produced, calls
jolt_library_init, resolves one name published with
ffi/export! through jolt_lookup, and calls it.
raylib is compiled from pinned source into the same
libmain.so, statically, so every defcfn
resolves against the process image — the one :jolt/native
entry in deps.edn names libmain.so itself.
raylib and GLES may only be touched from the thread
android_main runs on, because that is where the ALooper is
pumped and the EGL context is current. Every arrow below stays on it, so
the Clojure loop inherits the right thread by construction rather than by
hopping onto it.
flowchart LR na["NativeActivity
android.app.NativeActivity"] am["android_main
raylib's rcore_android.c"] mc["our main()
20 lines of C"] jl["libjoltraylib.so
jolt_library_init · jolt_lookup"] loop["the raylib loop
InitWindow · BeginDrawing · EndDrawing"] na -->|"lib_name = main"| am -->|"main(1, {raylib})"| mc -->|"dlopen + jolt_lookup"| jl --> loop loop -->|"Back at the top: return the frame count"| mc mc -->|"return from main"| am -->|ANativeActivity_finish| na
An nREPL eval lands on jolt.nrepl's accept thread instead, which is why
reads are free and anything calling raylib goes through
raylib.host/on-next-frame! — a queue the loop drains at the
top of a frame, on the owner thread.
Penrose makes ~2400 FFI calls a frame at 59 fps, while a scene doing 1800 draws managed 18. The cost was partition and map-indexed in the draw loop, not the calls. Fixing that was 3.5x.
A helper returning [x y] allocates a vector per line. Removing a lazy sequence changed nothing until the allocation went too: 22 fps, then 47.
This project once believed for a day that raylib drew into framebuffer 0 and showed nothing, because that is what the code said. The device disagreed, and an upstream bug report was withdrawn before it was filed.
A release build inlines across call sites, so a var redefined over the nREPL updates what the REPL sees while the running loop keeps calling the original. Nothing announces the split.
jolt's built-in server binds on Android loopback and adb forward brings it here. Read live scene state, queue work onto the owner thread, and open a card without a finger.
An Android app's stdout goes to /dev/null, so every println writes into nothing. Diagnostics go through __android_log_write instead, and jolt log is the console this project has.
raylib records AKEYCODE_BACK and then eats the event, so it never reaches Android and never sets shouldClose. The gallery reads it as one level up, and quitting means returning from main.
Every scene is pure .cljc that runs on a build host with no
raylib, no NDK and no device, which is how they are tested. Three come
byte-identical from the
Jolt Android
experiment, sha256 verified, and were written for a different platform
entirely, along with three more namespaces carrying the scene contract
itself. The other fourteen are ports from
raylib-jlt.
There are no screenshots here yet, for the same reason there are none in the README: nothing has run on Android to photograph honestly. Every capture that stood here before was a real screen recording, phone status bar and all, but of raylib-ios, the sibling project this tree was ported from, not of this build. That project's own site has the captures and frame rates for what the scenes draw, and this section gets real Android ones once this build has actually met a phone.
jolt test # 35 tests on the host, no device needed
jolt deps # the pinned raylib source
jolt pack # Chez, cross-built for Bionic. once, ~15 min
jolt release # raylib.gallery, no nREPL
jolt deploy
jolt log # the console this app has
jolt live # the gallery with an nREPL, on --dev
tools/android/nrepl repl 7888
Needs jolt 0.8.1+, JDK 17 with Gradle 8.9+, an Android SDK and NDK, and
an arm64 device. Chez Scheme is not a prerequisite: pack.sh
clones the pinned 10.4.1 and cross-builds it. No private repositories,
and the default build has no dependencies at all.