The raylib examples, on an iPhone, from Clojure

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.

raylib has no iOS backend, and does not need one

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.

Who owns thread 0

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.

What the device taught us

The FFI is not the bottleneck

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.

Allocation costs more than the call

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.

Reading source is a hypothesis

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.

Build --dev, or redefinition lies

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.

An nREPL, on the phone

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.

The simulator cannot show this

From iOS 17.5 the simulator presents black for OpenGL ES. Pixels reach the framebuffer and nothing appears, so every picture here is hardware.

The gallery

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).

A hypotrochoid drawing itself in rainbow lines
spirograph · 55 fps
A stroke mirrored into six-fold symmetry
kaleidoscope · 58 fps
Rockets bursting into falling sparks
fireworks · 58 fps
Forty-five boids flocking
boids · 52 fps
Flappy Bird, flapped by a loop inside the app
flappy-bird · identical to Android apart from names
A chaotic double pendulum tracing its path
double-pendulum · 59 fps
Fourier epicycles drawing a square wave
fourier-epicycles · 59 fps
A binary fractal tree swaying
fractal-tree · 59 fps
A starfield flying toward the viewer
starfield · 58 fps
A Hilbert space-filling curve
hilbert-curve · 1023 lines at 60 fps
An L-system plant growing segment by segment
l-system · 1488 segments at 59 fps
Particles following a drifting flow field
flow-field · 54 fps, tuned down from 32
The Lorenz attractor, camera orbiting a 450-point trail
lorenz · 58 fps, and where the budget rule broke
A rotating 4D hypercube projected to the screen
tesseract · 32 edges, the cheapest scene here
Conway's Game of Life on a 48 by 93 grid
life · 1470 rectangles at 59 fps
Elementary cellular automata scrolling down the screen
automata · 2551 rectangles, and the budget rule rewritten
An HSV colour wheel drawn as an rlgl triangle fan
colour wheel · the one scene that fills
Sine and cosine traced from a rotating radius
sine & cosine · the projections, and their traces
A seven-segment clock showing the time
clock · libc time, through the FFI
A rotating pie chart
pie chart · 62 triangles, sized per wedge
raylib's logo assembling itself
raylib logo · nothing eased, on purpose
Fifteen easing curves plotted and running on one clock
easings · all fifteen curves, one clock
Balls falling and bouncing under gravity
ball physics · restarts when everything settles
A shuffled sequence of coloured bars
random sequence · a permutation, not draws
An analog clock face with bezel, sixty ticks and three sweeping hands
analog · rlgl stands in for DrawRing
A green ball under a finger
touch ball · follows a finger, green while held
A triangle shaded red, green and blue at its corners, one corner mid-drag
rlgl triangle · a colour per vertex, corners you drag
A plume of grey smoke rising from a finger, 324 particles
particles · smoke, 324 live at 59 fps
Breakout mid-game, one brick gone and the ball in flight
breakout · the paddle follows a finger
Two small boxes, one moved per frame and one by delta time, already apart
delta time · apart at 58 fps without being asked
A large number 84 with the recent rolls listed below it
random values · seeded, the LCG's high bits
A zero-padded score and an MM:SS timer
formatted text · padded without format
Sixteen coloured bands running from cyan through yellow to near black
triangle strip · 32 triangles, winding fixed by draw-triangle
Vampire Survivors mid-wave, the hero in a corner with red enemies closing in and green gems on the floor
vampire survivors · a thumb-stick, 58 fps
Pac-Man in a blue maze with four ghosts, score 120
pac-man · swipes queue the turn, 58 fps
One dark grey line, Congrats! You created your first window!
basic window · one line, fitted to the width
A red ball moved down from the caption with a thumb-stick
keyboard ball · a thumb-stick for the arrow keys
A red square dragged down the screen
mouse wheel · a vertical drag for the wheel
A grid with a grey trail ending in a blue square, the history strip below and undo and redo buttons at the bottom
undo redo · buttons for the shortcuts
Three text particles, a sentence cut and shaken apart, with shatter, shake and case buttons
strings management · cut, then shaken
A rectangle resized by dragging its corner handle
resize · a sticky grab
One word placed left, centre and right inside three boxes
text alignment · from MeasureText
A coloured grid visible only inside a moving box
scissor · a scene clipping inside the safe area
Sixteen coloured spokes, each a different thickness
line widths · sixteen, thinnest at the top
A cubic Bezier with its control polygon, following a finger
bezier · follows your finger
Five bars with different rounding on each end, each filled with a gradient
rounded bars · one loop, square to lozenge
Two arms from one origin with the signed angle between them filled
vector angle · the sign is visible
A rectangle with quarter-circle corners
rounded rect · decomposed, not bound
Three spline bases drawn over the same five control points
splines · three bases, same points
A stroked annulus whose sweep and inner radius breathe
ring drawing · an rlgl annulus
Four gradient bands, vertical, horizontal, four-corner and turning
gradients · a colour at every corner
The time spelled out by a grid of little clock faces
clock of clocks · 144 faces, 288 hands
All twenty-five colours raylib names, as labelled swatches
colours · every one raylib names
A pie slice drawn with eight segments, showing its polygon edges
circle sector · raylib's segment floor
Four fingers on the screen, each drawn in its own colour with its raylib id
multitouch · four real fingers, four ids
A three-armed spiral of gold bullets leaving a red emitter
bullets · straight lines from a turning source
A blue box and a yellow box, their overlap picked out in red
collision · follows a finger
A dashed line from a red hub to a moving target
dashed · equal dashes along the unit vector
A ring of spokes with one turning
angles · the two lines the rest are built from
A message typing itself out one character at a time
writing · the only scene that animates text
A Penrose P3 tiling by deflation
penrose · 57 fps
Two eyes whose pupils follow a finger
following-eyes · identical to Android apart from names
A fading trail following the primary pointer
touch-trail · identical to Android apart from names
The category screen, showing Generative, Fractals, Toys and Games
the gallery · four categories
The scene list inside Toys, twenty cards in two columns
and inside one · twenty scenes under Toys

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.

ScenePer framefps
Kaleidoscope708 lines58
Fireworks~120 circles58
Penrose340 triangles, ~2400 FFI calls57
Spirograph~1000 lines55
Boids2025 distance tests52
Flappy Bird, Following Eyes, Touch Trailidentical to Android apart from names and whitespace58

Quickstart

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.