A gallery of a hundred and thirty-seven scenes, all measured running at 52 to 60 fps on an iPhone 17 Pro (one dips to 31 in its heaviest view), drawn by raylib 6.0 over SDL2, driven by Jolt (Clojure on Chez Scheme) as threaded portable bytecode. No JIT, nothing generated at run time, and no dependencies.
This is how raylib-ios, the platform these scenes run on, gets raylib onto the phone.
raylib 6.0 ships platform layers for Android, GLFW, RGFW, SDL, Win32, DRM
and web. There is no rcore_ios.c, and writing one means a
UIWindow, a GLES layer, touch handling and a loop: several hundred lines
of Objective-C.
The shortcut is SDL. Built PLATFORM=SDL with
GRAPHICS_API_OPENGL_ES2 against an SDL2 compiled for iOS,
SDL's own iOS support becomes the platform layer, which
collapses the whole problem to "build SDL". Both libraries then go into
the executable as static archives, because
jolt build --target emits the whole binary and Chez owns
main: there is no host process to dlopen
anything, and every defcfn resolves against the process
image at first call.
UIKit insists its window work happens on the main thread, so the whole design turns on who owns it. Here the answer is our own executable, and the loop is handed back to us on the thread UIKit wants.
flowchart LR main["Chez owns main()
thread 0"] run["SDL_UIKitRunApp
:blocking FFI call"] uikit["UIApplicationMain
SDLUIKitDelegate"] cb["our :collect-safe callback
= SDL_main"] loop["the raylib loop
InitWindow · BeginDrawing · EndDrawing"] main --> run --> uikit -->|"postFinishLaunch, on thread 0"| cb --> loop loop -->|"EndDrawing pumps CFRunLoop
so a blocking loop is legal"| uikit
A blocking while loop is normally wrong on iOS. It works
here only because EndDrawing reaches
SDL_PumpEvents, which spins the run loop, so UIKit gets its
turn every frame.
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.
raylib's SDL platform binds no framebuffer, so it "must" draw into framebuffer 0 and show nothing. 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 iOS since jolt#829. Read live scene state, queue work onto the main thread, and open a card without a finger.
From iOS 17.5 the simulator presents black for OpenGL ES. Pixels reach the framebuffer and nothing appears, so every picture here is hardware.
Every scene is pure .cljc that runs on a build host with no
raylib, no SDL and no device, which is how they are tested. Three come
from the
Jolt Android
experiment, identical apart from namespace names and whitespace, and
written for a different platform entirely, along with three more namespaces carrying the scene contract
itself. The other hundred and thirty-four are ports from
raylib-jolt-demo's
demos (originally raylib-jlt's).
generative 6 · fractals 3 · toys 5 · games 1 · three of them identical to the Android originals apart from names and whitespace. Every one came off the phone unattended, the moving ones recorded from a live iPhone Mirroring window. Nobody touched the device for any of them: Flappy Bird is flapped by a loop running inside the app, started from the nREPL. The bezel is the mirror's own.
| Scene | Per frame | fps |
|---|---|---|
| Kaleidoscope | 708 lines | 58 |
| Fireworks | ~120 circles | 58 |
| Penrose | 340 triangles, ~2400 FFI calls | 57 |
| Spirograph | ~1000 lines | 55 |
| Boids | 2025 distance tests | 52 |
| Flappy Bird, Following Eyes, Touch Trail | identical to Android apart from names and whitespace | 58 |
bb doctor # jolt, raylib-ios and the toolchain
bb test # every scene's tests, no device needed
UDID=... bb gallery # the 137-scene app, signed, installed, launched
UDID=... bb asteroids # one scene as an app of its own
UDID=... bb live asteroids # the same scene with an nREPL on the phone
jolt proxy # forward it over USB
Needs jolt (the README names the
version that builds device images), Chez Scheme 10.4.1 on PATH
as chez, Xcode, cmake, and a paired iPhone.