raylib-ios
A gallery of graphics scenes running on an iPhone, drawn by raylib 6.0 over SDL2, driven from Clojure compiled by Jolt onto Chez Scheme — no JVM, no JIT, and nothing generated at run time. EPL-2.0, part of the jlt-commons fleet, first commit 2026-09-03, 3 stars when this page was written.
It matters to this spoke as evidence rather than as software: it is the first artifact here where a host-swapped language ships on a platform its original host cannot reach, with frame times published.
How it gets onto the phone
raylib has no iOS backend and the project does not write one. src/platforms/ in raylib 6.0 holds
Android, four desktop variants, DRM, web and a template, and the project reports that raylib issue
#330, "Try to support iOS platform," was closed in 2018 without one. Instead: build raylib with
PLATFORM=SDL and GRAPHICS_API_OPENGL_ES2 against an SDL2 built for iOS, and SDL's own iOS support
becomes the platform layer. The problem collapses to "build SDL," which is well trodden.
Both libraries link in as static archives, -force_loaded rather than -led. jolt build --target emits the whole binary and Chez owns main, so there is no host process to dlopen a
shared library and every defcfn resolves against the process image at first call. The project
therefore declares no native dependencies at all — there is no libraylib.dylib to find.
Two pins are load-bearing and the README says why: Chez 10.4.1 exactly (a target pack's xpatch is
compiled Scheme that loads only into the Chez that produced it) and jolt ≥ 0.8.0 (ffi/write
swapped its value and offset arguments at 0.8.0; both are integers, so an older jolt would write the
wrong byte to the wrong place in silence).
The numbers, and how to read them
Measured on an iPhone 17 Pro, iOS 26.6.1, running the gallery with Flappy Bird open, every draw call going through libffi:
host: 600 frames, mean 17.03 ms, worst 17.4 ms
host: 1200 frames, mean 17.02 ms, worst 17.1 ms
host: 5400 frames, mean 17.02 ms, worst 17.1 ms
Ninety seconds with a worst case 0.1 ms off the mean and no outliers. The project reads this as a
loop that finishes its work and waits for vsync every frame, with the interpreter, the reducer, about
twenty FFI calls per frame and the collector all fitting inside the budget. The 279 ms spike in the
first window is InitWindow compiling shaders and building the default font.
Two arithmetic notes this page keeps. A 60 Hz frame is 16.67 ms; a 17.02 ms mean is 58.8 frames per second, so the repository description's "60 fps" is a rounding of a number that is consistently above one frame period, and nothing in the README explains the 0.35 ms. And that description says seventeen scenes where the README says forty-eight — the repo was pushed the morning this page was written, so the headline is simply behind. Neither gap changes the finding; both are the sort of thing a reader should not have to discover.
There is also a useful negative result about the measurement itself: raylib's GetFPS is a stateful
sampler over a 30-slot ring, so it only returns a frame rate if called every frame. Called once
per 300 frames it decays — 1757, 887, 590, … 98 — while GetFrameTime sat still at 17.02 ms.
DrawFPS is the only place raylib itself calls it, and nothing documents the requirement.
Provenance of the code
The recipe came from Larry Staton's glimmer-ios-demo, two literate org notebooks that keep the
mistakes as well as the answers. The scenes are lifted byte for byte from
jasalt/jolt-android-experiment at commit 6d2b291, and the project ships a tool that re-checks the
six pure namespaces against the sha256 the notebooks record and refuses to overwrite a file edited
since. Pure .cljc simulations, an iOS owner loop of about thirty lines, and a scene contract where
a scene is a reducer over frames — nothing in a scene polls, draws, or holds a native value.
Live coding survives the trip: jolt live starts the gallery with an nREPL, an editor drives the
running app on the device, and evaluated work is handed to raylib.host/on-next-frame! because an
eval lands on the nREPL thread and the toolkit is not thread-safe. Flappy Bird in the recorded GIF is
flapped by a loop started from that REPL, not by a finger.
Tier: T3
The load-bearing claim is a benchmark the author ran on their own device, which is the maker reporting how well their own thing works. The artifact facts around it — licence, pins, link strategy, the absent iOS backend — are first-party and checkable, and the repository is the project's own. Read the frame times as unreplicated: one device, one build, no independent run.
Cross-spoke context
../game-engines-wiki owns raylib's own territory (it holds no raylib page yet; this one is a
consumer of raylib, not an account of it). The mobile-deployment and toolchain detail is real but
subordinate: what is being demonstrated is a language runtime, which is why it files here.
Related
jolt-clojure · clojure · jank · hosted-language · jlt-commons · scheme