b12n-raylib-jlt
  • Home
  • Docs
  • GitHub

b12n-raylib-jlt — Guide

User-facing documentation for b12n-raylib-jlt: a suite of raylib examples written in jolt (native Clojure on Chez Scheme, no JVM) over jolt.ffi. Each page below covers one FFI pattern or drawing convention, with citations to the source files that implement it.

Why this exists

The examples show you what the suite draws; these pages explain why the binding layer is shaped the way it is. Almost every non-obvious decision in net.b12n.raylib-jlt.raylib traces back to one question — how a given C struct crosses the FFI boundary — and the answer differs per struct. Read these when you want to bind a C library from Jolt yourself, or when an example does something that looks needlessly indirect and you want the ABI reason behind it.

What b12n-raylib-jlt is

A community suite of 75 raylib examples — the classic core/shapes/text demos, a handful of games (asteroids, tetris, pong, vampire-survivors), and a 3D set (orbiting cameras, waving cubes, an rlgl solar system) — each a small Clojure namespace on top of one shared binding layer, net.b12n.raylib-jlt.raylib.

It is the graphics sibling of b12n-tsj (tree-sitter from Jolt, not yet public). Both bind a real external C library directly over its C ABI with jolt.ffi — Chez foreign-procedure under the hood. They differ on how hard the library leans on by-value structs, and that difference is the whole story of the FFI pages here:

raylib hits the mild version of struct-by-value — its hot path is Color, a 4-byte struct that reduces to a uint32, and its only large structs (Camera2D/Camera3D) are by-value arguments it can fake behind a pointer on AArch64. tree-sitter hits the severe version — a 32-byte TSNode passed AND returned by value — so it needs a full C shim. raylib needs none.

Three ABI facts drive every distinctive decision in this repo:

  1. Color packs into a :uint — a 4-byte all-integer struct travels in one general-purpose register, so every draw call passes color as an int, no struct marshaling. (color-by-value.md)
  2. Camera2D/Camera3D go by pointer — a >16-byte composite is passed indirectly on AArch64, so a [:pointer] binding + a hand-built native struct works (with an x86-64 caveat). (struct-by-value-pointer-trick.md)
  3. Vector2/Vector3 geometry uses rlgl — small float structs go in FP registers, which the pointer trick does not cover, so shapes and 3D cubes are drawn with rlgl's scalar immediate mode instead. (rlgl-immediate-mode.md)

Nothing about jolt.ffi is raylib-specific: it binds any C ABI symbol. The analog-clock / digital-clock examples call plain libc time()/localtime() (via rl/local-time) for real wall-clock time — the repo's one non-raylib FFI, reading struct tm's tm_hour/tm_min/tm_sec ints straight out of native memory.

Capability pages

The FFI core (the reason this repo is interesting)

  • ✅ color-by-value.md — why raylib's Color crosses the FFI boundary as a packed :uint and not a struct, the little-endian rgba packing, and the two-by-value-Colors-in-one-call case (DrawRectangleGradientV). Source: src/net/b12n/raylib_jlt/raylib.clj (rgba, clear-background, the palette).
  • ✅ struct-by-value-pointer-trick.md — how a 24-byte Camera2D / 44-byte Camera3D is passed by value on AArch64 by allocating the struct in native memory and binding BeginMode2D/BeginMode3D as [:pointer]. The x86-64 non-portability caveat is here. Source: raylib.clj (with-camera-2d, with-camera-3d).
  • ✅ rlgl-immediate-mode.md — the fallback for by-value Vector2/Vector3 args the pointer trick can't fake: rlgl scalar immediate mode (rlBegin/rlVertex2f/rlVertex3f/rlColor4ub) for the 2D triangle and the 3D cube!, plus the rlgl matrix stack for nested transforms (the solar-system demo). Source: raylib.clj (cube!, quad-3f, the rl-* binds).

The drawing API

  • ✅ kwarg-drawing-api.md — the two-layer design: raw positional ffi/defcfn binds at the boundary (mirroring C), ergonomic keyword-argument wrappers (text!/rect!/circle!/…) on top, and the ">3 arguments → keyword args" style convention. Source: raylib.clj.

Verifying without a display

  • ✅ headless-smoke-testing.md — how a windowed example proves itself with no person at the keyboard: RAYLIB_APP_AUTO_QUIT_MS (auto-close), RAYLIB_APP_SHOT (dump one PNG), and the batched-geometry flush that makes the screenshot non-empty. Source: raylib.clj (auto-quit-deadline, keep-running?, maybe-screenshot!).

Orientation

  • ✅ example-catalog.md — a tour of all 75 examples grouped games / core / shapes / text / 3d / generative, what each demonstrates, and the four-touchpoint recipe for adding one (source ns + deps.edn alias + check.clj require + bb.edn registry row). Read this for the map; the FFI pages for the mechanics.

See also

  • raylib — the upstream C library, and the source of most examples here. Its own examples/ tree is the reference these ports are named after.
  • jolt — the native Clojure implementation whose jolt.ffi does all the binding work described on these pages.
  • b12n-tsj (not yet public) — the Jolt sibling that binds a by-value-returning C API (tree-sitter) and therefore needs a full C shim this repo avoids. struct-by-value-pointer-trick.md is the delta between "fake it with a pointer" (raylib arguments) and "you can't, write a shim" (tree-sitter returns).

Color passed by value — as a packed :uint

Every raylib draw call takes a Color. Color is a struct passed by value, and Jolt's FFI (Chez foreign-procedure) has no calling convention for a by-value struct. raylib gets away with it anyway, because Color is the one by-value struct that reduces to a scalar the ABI does pass in a register.

The ABI fact that makes it work

From raylib's raylib.h:

typedef struct Color {
    unsigned char r, g, b, a;   // 4 bytes total
} Color;

void ClearBackground(Color color);
void DrawText(const char *text, int x, int y, int fontSize, Color color);

On both the AArch64 and x86-64 ABIs, a 4-byte struct made entirely of integers travels in a single general-purpose register — bit-for-bit identical to how a uint32_t travels. So a Jolt binding can declare the parameter as :uint and pass an ordinary integer; the C side reads the same four bytes back as {r,g,b,a}. No struct marshaling, no shim, no native allocation.

Contrast struct-by-value-pointer-trick.md: Camera2D is 24 bytes, too big for a register, so it needs the pointer approach. Color is the easy case precisely because it fits in a register.

The packing

Color is little-endian in memory: r at the lowest byte, then g, b, a. So the uint32 is r | g<<8 | b<<16 | a<<24. That is exactly rgba (src/net/b12n/raylib_jlt/raylib.clj):

(defn rgba
  "Pack an RGBA color into the little-endian uint32 that raylib's `Color` struct
  is (r | g<<8 | b<<16 | a<<24), so it can cross the FFI boundary as a :uint."
  [r g b a]
  (bit-or (int r) (bit-shift-left (int g) 8)
          (bit-shift-left (int b) 16) (bit-shift-left (int a) 24)))

Every binding that takes a Color declares it :uint:

(ffi/defcfn clear-background "ClearBackground" [:uint] :void)
(ffi/defcfn draw-text        "DrawText" [:string :int :int :int :uint] :void)
(ffi/defcfn draw-rectangle   "DrawRectangle" [:int :int :int :int :uint] :void)

The whole named palette is just rgba calls with the values from raylib.h:

(def RAYWHITE (rgba 245 245 245 255))
(def RED      (rgba 230 41 55 255))
(def BLUE     (rgba 0 121 241 255))
;; … 25 named colors total
flowchart LR
  rgba["(rgba 230 41 55 255)"] -->|"r | g<<8 | b<<16 | a<<24"| u["uint32 0xFF3729E6"]
  u -->|":uint arg, one GP register"| c["DrawText(…, Color color)"]
  c -.->|"C reads the 4 bytes back as {r,g,b,a}"| ok["red text"]

Two by-value Colors in one call

DrawRectangleGradientV takes two Color values by value (top and bottom of the gradient). Because each is an independent :uint, this needs nothing special — just two :uint parameters:

(ffi/defcfn draw-rectangle-grad-v "DrawRectangleGradientV"
  [:int :int :int :int :uint :uint] :void)   ; x y w h topColor bottomColor

The gradient example (net.b12n.raylib-jlt.gradient) uses it directly. Two by-value structs in one signature would be a real problem if they didn't each collapse to a register — this is a second dividend of the register-fit fact.

Getting the color back out (for rlgl)

rlgl's immediate mode (see rlgl-immediate-mode.md) wants the four components as separate u8 args to rlColor4ub, not a packed int. So rl-color! unpacks the same :uint, keeping one Color representation across the whole API:

(defn rl-color! [color]
  (rl-color-4ub (bit-and color 0xff)
                (bit-and (bit-shift-right color 8) 0xff)
                (bit-and (bit-shift-right color 16) 0xff)
                (bit-and (bit-shift-right color 24) 0xff)))

Why this generalizes

Any Chez/Jolt FFI against a C library with a small all-integer by-value struct can use this: pack the fields little-endian into the matching-width integer and bind the parameter as that integer type. It works for structs up to 8 bytes (a register); the moment the struct is >16 bytes or contains floats, the ABI stops passing it in a GP register and you need struct-by-value-pointer-trick.md (large structs) or rlgl-immediate-mode.md (float structs).

See also

  • struct-by-value-pointer-trick.md — the

    16-byte case (Camera2D/Camera3D) that a register can't hold.

  • b12n-tsj (the Jolt tree-sitter sibling, not yet public) — its by-value struct (TSNode) is both large and returned, so no register trick saves it and it must ship a C shim.

Full-size demo gallery

Every example at full size — linked from the example catalog's preview thumbnails.

games (10)

asteroids

the classic vector shooter (rotate/thrust/fire)

asteroids

tetris

the block-stacking puzzle (move/rotate/drop)

tetris

pong

two-paddle classic, you (W/S) vs a CPU

pong

vampire-survivors

auto-fire survival: move, waves chase you

vampire-survivors

snake

the classic snake (arrow keys, grow, don't crash)

snake

breakout

paddle + ball + brick grid (mouse paddle)

breakout

space-invaders

marching aliens (arrows + SPACE to shoot)

space-invaders

flappy-bird

flap through the pipe gaps (SPACE)

flappy-bird

game-2048

2048: 4x4 tile-merge puzzle (arrow keys)

game-2048

minesweeper

reveal/flag grid (mouse L reveal, R flag)

minesweeper

core (9)

basic-window

the minimal raylib window + text

basic-window

input-keys

steer a ball with the arrow keys

input-keys

input-mouse

a ball follows the mouse; click to recolor

input-mouse

input-mouse-wheel

scroll a box with the mouse wheel

input-mouse-wheel

camera-2d

a 2D camera over a skyline (struct-by-value)

camera-2d

delta-time

per-frame vs delta-time movement

delta-time

scissor-test

a scissor rectangle clips a grid

scissor-test

basic-screen-manager

a LOGO/TITLE/GAMEPLAY/ENDING flow

basic-screen-manager

random-values

a new random value every two seconds

random-values

shapes (32)

bouncing-ball

a ball bouncing around the window

bouncing-ball

basic-shapes

shape primitives + an rlgl triangle

basic-shapes

colors-palette

every named raylib color in a grid

colors-palette

gradient

a vertical two-color gradient

gradient

following-eyes

two eyes track the mouse

following-eyes

starfield

a twinkling starfield

starfield

logo-raylib

the raylib logo from rectangles + text

logo-raylib

mouse-trail

a fading trail follows the cursor

mouse-trail

recursive-tree

a binary fractal tree

recursive-tree

math-sine-cosine

a live unit-circle trig visualization

math-sine-cosine

bullet-hell

a rotating bullet spiral

bullet-hell

triangle-strip

a rainbow strip via rlgl immediate mode

triangle-strip

collision-area

AABB collision between two boxes

collision-area

dashed-line

a dashed line follows the mouse

dashed-line

double-pendulum

chaotic double-pendulum motion + trail

double-pendulum

kaleidoscope

strokes mirrored with 6-fold symmetry

kaleidoscope

hilbert-curve

a rainbow Hilbert space-filling curve

hilbert-curve

math-angle-rotation

fixed spokes + a spinning line

math-angle-rotation

ball-physics

2D balls under gravity, SPACE respawns

ball-physics

lines-bezier

a cubic Bézier that follows the mouse

lines-bezier

color-wheel

an HSV color wheel (rlgl triangle fan)

color-wheel

pie-chart

labelled pie slices via rl/sector!

pie-chart

splines

Catmull-Rom / Bezier / B-spline (SPACE cycles)

splines

vector-angle

the angle between two vectors (arc + readout)

vector-angle

easings

a grid of balls, each on a different easing curve

easings

penrose-tiling

a P3 Penrose rhombus tiling (deflation)

penrose-tiling

analog-clock

a live analog clock (libc local time)

analog-clock

digital-clock

a seven-segment HH:MM:SS clock (libc time)

digital-clock

ring-drawing

an animated annulus via rl/ring!

ring-drawing

rounded-rectangle

rounded rects via sector! corners

rounded-rectangle

rectangle-scaling

drag the corner handle to resize a rect

rectangle-scaling

lines-drawing

a rotating fan of thick lines (line-ex!)

lines-drawing

text (5)

font-sizes

font sizes + MeasureText centering

font-sizes

writing-anim

a message types itself out

writing-anim

format-text

padded score + MM:SS timer readouts

format-text

words-alignment

align a word inside a box (MeasureText)

words-alignment

input-box

type into a text box (GetCharPressed)

input-box

3d (12)

camera-3d

an orbiting 3D camera (Camera3D by value)

camera-3d

waving-cubes

an NxN grid of cubes rippling in 3D

waving-cubes

camera-3d-first-person

walk a yard of columns in first person

camera-3d-first-person

tesseract-view

a rotating 4D hypercube projected to 2D

tesseract-view

wireframe-shapes

pyramid/octahedron/torus/helix in 3D lines

wireframe-shapes

rlgl-solar-system

Sun/Earth/Moon via the rlgl matrix stack

rlgl-solar-system

box-collisions

a player cube colliding with 3D boxes

box-collisions

rotating-cube

a single cube spinning via the rlgl matrix stack

rotating-cube

spinning-cubes

a row of cubes each spinning with a phase offset

spinning-cubes

orthographic-projection

perspective vs orthographic (SPACE toggles)

orthographic-projection

point-cloud

~1500 points as tiny rlgl cubes, rotating

point-cloud

bouncing-spheres

spheres bouncing in a 3D box (rl/sphere!)

bouncing-spheres

generative (7)

game-of-life

Conway's Game of Life (SPACE reseeds)

game-of-life

boids

flocking birds (separation/alignment/cohesion)

boids

fireworks

rockets + fading particle bursts

fireworks

fourier-epicycles

rotating circles trace a square wave

fourier-epicycles

spirograph

animated hypotrochoid roulette curves

spirograph

l-system

an L-system fractal plant (grows + regrows)

l-system

flow-field

particles steered by a flow field (trails)

flow-field

The example catalog — 75 raylib demos in jolt

A map of the whole suite. Each example is one namespace under src/net/b12n/raylib_jlt/, runnable by a friendly bb <name> task or the underlying joltc -M:<alias>. bb info prints this grouping live; this page adds the "how it's wired" recipe at the end.

Run one, list them, or reel through all of them:

bb <name>          # e.g. bb asteroids   (opens a window)
bb examples        # flat list with descriptions
bb info            # the grouped cheat-sheet below
bb run-all [secs]  # every example, N seconds each (unattended)

games (10)

previewbb nameshows
asteroidsthe classic vector shooter — rotate / thrust / fire, splitting asteroids
tetris10×20 well, 7 tetrominoes, rotation, line-clearing, levels
pongtwo-paddle classic, you (W/S) vs a ball-tracking CPU
vampire-survivorsauto-firing survivors-like — chasing waves, XP gems, leveling
snakethe classic snake (arrow keys, grow, don't crash)
breakoutpaddle + ball + brick grid (mouse paddle)
space-invadersmarching aliens (arrows + SPACE to shoot)
flappy-birdflap through the pipe gaps (SPACE)
game-20482048: 4x4 tile-merge puzzle (arrow keys)
minesweeperreveal/flag grid (mouse L reveal, R flag)

core (9)

previewbb nameshows
basic-windowthe minimal window + text (the default, joltc -M:run)
input-keysIsKeyDown, steer a ball with the arrow keys
input-mouseGetMouseX/Y, IsMouseButtonDown, click to recolor
input-mouse-wheelGetMouseWheelMove (a float return) scrolls a box
camera-2da 2D camera over a skyline — struct-by-value (see below)
delta-timeper-frame vs GetFrameTime movement
scissor-testBeginScissorMode clips a grid
basic-screen-managera LOGO / TITLE / GAMEPLAY / ENDING state flow
random-valuesGetRandomValue, a new value every 2s

shapes (32)

previewbb nameshows
bouncing-ballanimation, IsKeyPressed pause, DrawFPS
basic-shapesrect / circle / ellipse / line + an rlgl triangle
colors-paletteevery named raylib color in a grid
gradientDrawRectangleGradientV (two by-value Colors)
following-eyespupils track the mouse (scalar trig)
starfieldGetRandomValue + bulk draw, per-star twinkle
logo-raylibrectangles + text, positioned with MeasureText
mouse-traila fading cursor trail (alpha)
recursive-treea binary fractal tree (lines + trig)
math-sine-cosinea live unit-circle sine/cosine visualization
bullet-hella rotating bullet spiral
triangle-stripa rainbow strip via rlgl immediate mode
collision-areaAABB collision between two boxes (computed in Clojure)
dashed-linea dashed line follows the mouse
double-pendulumchaotic double-pendulum motion + trail
kaleidoscopestrokes mirrored with 6-fold symmetry
hilbert-curvea rainbow Hilbert space-filling curve
math-angle-rotationfixed spokes + a spinning line
ball-physics2D balls under gravity, SPACE respawns
lines-beziera cubic Bézier that follows the mouse
color-wheelan HSV color wheel drawn as an rlgl triangle fan (per-vertex hue)
pie-chartlabelled pie slices via rl/sector! + a legend
splinesCatmull-Rom / cubic-Bézier / uniform-B-spline through animated points (SPACE cycles)
vector-anglethe signed angle between two vectors, filled arc + degrees readout (atan2)
easingsa 3×4 grid of balls, each animating on a different easing curve
penrose-tilinga P3 Penrose rhombus tiling built by golden-ratio deflation
analog-clocka live analog clock (bezel ring!, ticks + hands line-ex!, libc local time)
digital-clocka seven-segment HH:MM:SS display (libc local time)
ring-drawingan animated annulus via rl/ring! + a stroked outline
rounded-rectanglerounded rectangles built from sector! corners + rects
rectangle-scalingdrag the bottom-right corner handle to resize a rectangle
lines-drawinga rotating fan of thick lines via rl/line-ex! (rlgl quads)

text (5)

previewbb nameshows
font-sizesfont sizes + MeasureText centering
writing-anima self-typing message
format-textformat padded score + MM:SS timer
words-alignmentalign a word inside a box with MeasureText
input-boxtype into a text box (GetCharPressed)

3d (12)

previewbb nameshows
camera-3dan orbiting 3D camera — Camera3D by value + rlgl cube
waving-cubes196 cubes rippling in 3D (shared rl/cube!)
camera-3d-first-personWASD + mouse-look walk through columns
tesseract-viewa rotating 4D hypercube projected 4D→3D→2D
wireframe-shapespyramid / octahedron / torus / helix as rlgl 3D lines
rlgl-solar-systemSun / Earth / Moon via the rlgl matrix stack
box-collisionsa player cube vs. boxes, 3D AABB highlight
rotating-cubea single cube spinning via the rlgl matrix stack
spinning-cubesa row of cubes each spinning with a phase offset
orthographic-projectionperspective vs orthographic (SPACE toggles)
point-cloud~1500 points as tiny rlgl cubes, rotating
bouncing-spheresspheres bouncing in a 3D box (rl/sphere!)

The 3D set stands entirely on two building blocks from rlgl-immediate-mode.md and struct-by-value-pointer-trick.md: with-camera-3d (the camera, by pointer) and cube! (the geometry, by rlgl vertices).

generative (7)

previewbb nameshows
game-of-lifeConway's Game of Life (SPACE reseeds)
boidsflocking birds (separation/alignment/cohesion)
fireworksrockets + fading particle bursts
fourier-epicyclesrotating circles trace a square wave
spirographanimated hypotrochoid roulette curves
l-systeman L-system fractal plant (grows + regrows)
flow-fieldparticles steered by a flow field (trails)

All seven are pure math + the drawing API — no new bindings. They showcase the suite as a generative-art canvas: cellular automata, agent flocking, particle systems, rotating-vector Fourier series, parametric roulette curves, string-rewrite fractals, and noise-steered flow fields.

Adding an example — the four touchpoints

The suite is deliberately mechanical to grow. One new example touches four places:

  1. Source — src/net/b12n/raylib_jlt/<name>.clj, a namespace with a -main that runs the canonical loop (see headless-smoke-testing.md) against the net.b12n.raylib-jlt.raylib API.
  2. deps.edn alias — :<name> {:main-opts ["-m" "net.b12n.raylib-jlt.<name>"]} so joltc -M:<name> works.
  3. check.clj require — add net.b12n.raylib-jlt.<name> to the :require list in net.b12n.raylib-jlt.check, so the headless compile-check covers it.
  4. bb.edn registry row — add ["<display-name>" "<alias>" "<group>" "<desc>"] to the examples vector and a matching bb <name> task, so it shows in bb info / bb examples / bb run-all.
    flowchart LR
      src["src/…/<name>.clj<br/>the example"] --> deps["deps.edn<br/>:<name> alias"]
      src --> chk["check.clj<br/>require (headless compile)"]
      src --> bb["bb.edn<br/>registry row + task"]
    

Filenames use underscores (basic_screen_manager.clj); the namespace uses hyphens (net.b12n.raylib-jlt.basic-screen-manager) — Clojure's standard file↔ns mapping.

One ordering rule applies inside every file: since jolt 0.4.0 an unresolved symbol is a compile error rather than a late-bound reference, so a definition must appear before its first use — in a fn body and in an :or destructuring default just as much as at top level. It bites hardest in the shared raylib.clj binding layer, where one misordered symbol stops every example from loading and only the first offender is reported. joltc -M:check is the quick confirmation; see the jolt note in the README.

See also

  • kwarg-drawing-api.md — the API every example is written against.
  • headless-smoke-testing.md — the loop shape and how bb run-all / bb check verify the catalog.

Headless smoke testing — proving a window rendered with no one watching

A raylib example opens a window and runs until you close it. That's fine for a human, useless for CI or an agent: nothing here can click a close button, and a test that blocks forever is worse than no test. Two environment variables turn every windowed example into something a machine can drive and verify — without changing a line of the example itself.

The two knobs

Both are read in net.b12n.raylib-jlt.raylib and consumed by the shared loop guards, so every example inherits them for free.

  • RAYLIB_APP_AUTO_QUIT_MS=<n> — close the window after n milliseconds. The example runs, renders real frames, then exits on its own.
  • RAYLIB_APP_SHOT=<name> — dump one frame to a PNG. Headless visual proof that a frame actually rendered, not just that the process didn't crash.

Auto-quit: a deadline the loop checks

auto-quit-deadline turns the env var into an absolute wall-clock deadline (or nil); keep-running? ANDs it with raylib's own close signal:

;; src/net/b12n/raylib_jlt/raylib.clj
(defn auto-quit-deadline []
  (when-let [v (System/getenv "RAYLIB_APP_AUTO_QUIT_MS")]
    (try (let [ms (Integer/parseInt v)]
           (when (pos? ms) (+ (System/currentTimeMillis) ms)))
         (catch Exception _ nil))))

(defn keep-running? [deadline]
  (and (not (window-should-close?))
       (or (nil? deadline) (< (System/currentTimeMillis) deadline))))

With the var unset, deadline is nil and keep-running? reduces to "while the window is open" — normal interactive behavior. Set it, and the loop ends on its own.

The canonical loop

Every example is the same shape (net.b12n.raylib-jlt.core, the basic window):

(defn -main [& _]
  (rl/window! :width 800 :height 450 :title "raylib [core] example - basic window")
  (rl/set-target-fps 60)
  (let [deadline (rl/auto-quit-deadline)]
    (loop [frame 0]
      (when (rl/keep-running? deadline)
        (rl/begin-drawing)
        (rl/clear-background rl/RAYWHITE)
        (rl/text! "Congrats! You created your first window!" :x 190 :y 200 …)
        (rl/maybe-screenshot! frame 10)
        (rl/end-drawing)
        (recur (inc frame)))))
  (rl/close-window))

frame is threaded through the loop so maybe-screenshot! can fire on a specific frame (here frame 10 — a few frames in, so the first render has settled).

Screenshot: flush the batch first, or you get an empty PNG

The subtle part. raylib batches geometry — DrawText, shapes, everything — and doesn't actually submit it to the framebuffer until EndDrawing. A naive TakeScreenshot mid-frame would capture the framebuffer before this frame's geometry lands, producing a blank or stale image. maybe-screenshot! flushes the active render batch first:

(defn maybe-screenshot! [frame at]
  (when (and shot-path (= frame at))
    (flush-batch)                 ; rlDrawRenderBatchActive — submit deferred geometry
    (take-screenshot shot-path)
    (binding [*out* *err*] (println "[net.b12n.raylib-jlt] SHOT" shot-path))))

Note: raylib writes the file's basename into the current working directory — it ignores any directory component of the path. So RAYLIB_APP_SHOT=out/x.png still writes x.png in CWD. Plan for that when collecting artifacts.

flowchart LR
  d["DrawText / shapes<br/>(batched, deferred)"] --> f["flush-batch<br/>rlDrawRenderBatchActive"]
  f --> t["TakeScreenshot(name)"]
  t --> p["name.png in CWD<br/>(real frame, not blank)"]

Driving it

One example, auto-quit + shot:

RAYLIB_APP_AUTO_QUIT_MS=2000 RAYLIB_APP_SHOT=shot.png joltc -M:run
# opens, renders ~2s, writes shot.png, exits

The whole suite as a demo reel / smoke test — bb run-all sets RAYLIB_APP_AUTO_QUIT_MS per example so each runs N seconds then advances:

bb run-all 3     # every example, 3s each, unattended

And the display-free check that belongs in CI — it compiles every example namespace without opening a window at all:

joltc -M:check   # "net.b12n.raylib-jlt: all example namespaces compiled OK"
bb check         # same, via babashka

One environment caveat

The screenshot path needs an active display — raylib/GLFW initializes a real GL context. On a Mac whose display has slept, window creation can fail with "Failed to determine Monitor" and then crash. joltc -M:check needs no display and always works; RAYLIB_APP_SHOT needs a live (awake) display.

A C-truthiness footnote

raylib's boolean-returning functions (WindowShouldClose, IsKeyDown) return a C int whose truth lives in the low byte; the upper bytes are unspecified. The predicates mask before testing so a dirty high byte can't read as "true":

(defn window-should-close? [] (not (zero? (bit-and (should-close-raw) 0xff))))
(defn key-down? [k] (not (zero? (bit-and (key-down-raw k) 0xff))))

See also

  • example-catalog.md — every example inherits these guards through the shared loop shape.
  • kwarg-drawing-api.md — the rl/*! calls inside the loop.

The keyword-argument drawing API

raylib's C functions are positional and often long: DrawText(text, x, y, fontSize, color), DrawRectangle(x, y, width, height, color). Bound literally, example code becomes a wall of bare numbers where the fifth argument's meaning is anyone's guess. This repo keeps the raw binds positional (they mirror C) and layers an ergonomic keyword-argument API on top, so example call sites read as self-describing.

Two layers, one boundary

The FFI boundary stays a faithful, positional mirror of the C signature — that's the contract with the library and the thing to check against raylib.h:

;; src/net/b12n/raylib_jlt/raylib.clj — the FFI boundary (positional, mirrors C)
(ffi/defcfn draw-text      "DrawText"      [:string :int :int :int :uint] :void)
(ffi/defcfn draw-rectangle "DrawRectangle" [:int :int :int :int :uint] :void)
(ffi/defcfn draw-circle    "DrawCircle"    [:int :int :float :uint] :void)

On top sits a thin wrapper per call that names the arguments and supplies defaults:

(defn text!
  "DrawText. :x :y :size :color."
  [s & {:keys [x y size color] :or {x 0 y 0 size 20 color BLACK}}]
  (draw-text s x y size color))

(defn rect!
  "DrawRectangle. :x :y :width :height :color."
  [& {:keys [x y width height color] :or {x 0 y 0 width 10 height 10 color BLACK}}]
  (draw-rectangle x y width height color))

(defn circle!
  "DrawCircle. :x :y :radius :color."
  [& {:keys [x y radius color] :or {x 0 y 0 radius 10 color BLACK}}]
  (draw-circle x y (double radius) color))

The wrappers also absorb small coercions the raw bind is strict about — e.g. circle! calls (double radius) so a caller can pass an int radius without a type error at the :float boundary.

The payoff at the call site (net.b12n.raylib-jlt.core):

(rl/text! "Congrats! You created your first window!"
          :x 190 :y 200 :size 20 :color rl/LIGHTGRAY)
;; vs. the positional (draw-text "…" 190 200 20 rl/LIGHTGRAY)

The ! suffix marks these as side-effecting calls. Sixteen wrappers exist, in three groups:

  • Window + 2D primitives, straight over a scalar bind — window!, text!, fps!, rect!, rect-lines!, rect-gradient!, circle!, circle-lines!, ellipse!, line!, pixel!.
  • 2D shapes rlgl has to draw — sector!, ring!, line-ex!. Their raylib originals (DrawCircleSector, DrawRing, DrawLineEx) take a Vector2 by value and are unbindable, so these emit triangles instead. Same keyword-arg surface; see rlgl-immediate-mode.md.
  • 3D — cube! and sphere!, likewise rlgl vertex streams standing in for the by-value-Vector3 DrawCube / DrawSphere.

(rl-color! and maybe-screenshot! also end in ! but are not part of this wrapper layer — one unpacks a Color for rlgl, the other is smoke-test plumbing.)

The style convention: >3 arguments → keyword args

The rule the whole suite follows:

A function with more than three arguments takes keyword args (or groups scalars into vectors to get to ≤3). Raw ffi/defcfn binds stay positional because they mirror C; recursive math kernels may stay positional with grouped-vector args.

DrawText has five arguments → text! is keyword. cube! would have a long positional list (pos, size, color) → keyword args, and pos/size are themselves grouped [x y z] vectors so each stays one argument:

(defn cube! [& {:keys [pos size color] :or {pos [0.0 0.0 0.0] size 1.0 color BLACK}}]
  …)

(rl/cube! :pos [x 0 z] :size 0.8 :color rl/BLUE)

The camera wrappers take a single map argument for the same reason — a Camera2D has six scalars, well past three:

(rl/with-camera-2d {:offset-x 400 :offset-y 225 :target-x px :target-y py :zoom 1.5}
  (fn [] …))

Why not just bind everything keyword?

Because the FFI boundary should stay a 1:1, positional mirror of the C signature — that's what you diff against the header when a call misbehaves, and what keeps the "is this binding correct?" question answerable. Keyword ergonomics are a caller concern, so they live in a caller-facing layer above the boundary, never in the ffi/defcfn itself. Same reason rgba (packing) and rl-color! (unpacking) wrap the raw :uint/rlColor4ub binds rather than replacing them.

See also

  • color-by-value.md — the packed-Color :uint the draw wrappers pass through unchanged.
  • rlgl-immediate-mode.md — cube! is the keyword-arg wrapper over the positional rl-vertex-3f stream.
  • example-catalog.md — every example is written against this wrapper API.

rlgl immediate mode — for the by-value float structs the pointer trick can't fake

Some raylib calls take a Vector2 or Vector3 by value: DrawTriangle(Vector2, Vector2, Vector2, Color), DrawCube(Vector3, …), DrawSphere(Vector3, …). These are the one FFI case that neither color-by-value.md (packed :uint) nor struct-by-value-pointer-trick.md (pointer for >16 bytes) can handle. The way out is to not call them at all — draw the same geometry with rlgl's scalar immediate mode.

Why the earlier tricks don't apply

A Vector2 is 8 bytes of two floats; a Vector3 is 12 bytes of three floats. Both are ≤16-byte homogeneous float aggregates (HFAs), and the AArch64 ABI passes those in floating-point registers, one field per register — not as an integer (so the Color :uint trick is out) and not indirectly through a pointer (so the Camera2D [:pointer] trick is out — that only applies to composites larger than 16 bytes). There is no scalar or pointer binding that reproduces "three floats in three FP registers." So these functions are simply unbindable from Jolt.

rlgl is all scalars

raylib ships rlgl, a thin immediate-mode layer over the GPU batch. Its vertex API takes individual floats, never a vector struct:

;; src/net/b12n/raylib_jlt/raylib.clj
(ffi/defcfn rl-begin     "rlBegin"     [:int] :void)        ; RL-LINES / RL-TRIANGLES
(ffi/defcfn rl-end       "rlEnd"       [] :void)
(ffi/defcfn rl-vertex-2f "rlVertex2f"  [:float :float] :void)
(ffi/defcfn rl-vertex-3f "rlVertex3f"  [:float :float :float] :void)
(ffi/defcfn rl-color-4ub "rlColor4ub"  [:int :int :int :int] :void)  ; u8 args
(def ^:const RL-LINES 1)
(def ^:const RL-TRIANGLES 4)

Every argument is a scalar the FFI passes cleanly. So instead of DrawTriangle(v1, v2, v3, color) you emit rlBegin(RL&#95;TRIANGLES), one rlColor4ub, three rlVertex2f, rlEnd. The shapes example (net.b12n.raylib-jlt.shapes) draws its triangle exactly this way; rl-color! (see color-by-value.md) unpacks the shared packed Color into the four u8 args.

3D: cube! is 12 rlgl triangles

The same move scales to 3D. with-camera-3d gets the camera active (via the pointer trick), but DrawCube takes a Vector3 by value — unbindable — so cube! builds a box out of rl-vertex-3f. Each face is a quad = two triangles, and faces are shaded by darkening the packed color so the cube reads as 3D without a lighting pass:

(defn- quad-3f [color [a b c d]]
  (rl-color! color)
  (let [[ax ay az] a [bx by bz] b [cx cy cz] c [dx dy dz] d]
    (rl-vertex-3f ax ay az) (rl-vertex-3f bx by bz) (rl-vertex-3f cx cy cz)
    (rl-vertex-3f ax ay az) (rl-vertex-3f cx cy cz) (rl-vertex-3f dx dy dz)))

(defn cube! [& {:keys [pos size color] :or {pos [0.0 0.0 0.0] size 1.0 color BLACK}}]
  ;; … compute the 8 corners …
  (rl-begin RL-TRIANGLES)
  (quad-3f (shade-color color 1.0)  [a001 a101 a111 a011])   ; front  +z
  (quad-3f (shade-color color 0.5)  [a100 a000 a010 a110])   ; back   -z
  ;; … four more faces at 0.7 / 0.85 / 1.0 / 0.4 brightness …
  (rl-end))

with-camera-3d + cube! are the two 3D building blocks the whole 3D example set stands on (camera-3d, waving-cubes, box-collisions, …). DrawGrid is scalar ([:int :float]) so it's bound and used directly.

sphere! is the same idea for a ball — lat/long rings of RL_TRIANGLES, faces shaded by latitude (brighter toward +y) — and drives the bouncing-spheres example. DrawSphere takes a Vector3 by value too, so it gets the same rlgl treatment as the cube.

The rlgl matrix stack — nested transforms for free

rlgl also exposes its transform stack, and it applies the current transform to each rlVertex* at submit time. So wrapping push/rotate/translate/scale around a cube! call moves that cube — the same nested-transform model as BeginMode2D, but in scalars:

(ffi/defcfn rl-push-matrix "rlPushMatrix" [] :void)
(ffi/defcfn rl-pop-matrix  "rlPopMatrix"  [] :void)
(ffi/defcfn rl-translatef  "rlTranslatef" [:float :float :float] :void)
(ffi/defcfn rl-rotatef     "rlRotatef"    [:float :float :float :float] :void)
(ffi/defcfn rl-scalef      "rlScalef"     [:float :float :float] :void)
flowchart TD
  s["rlPushMatrix"] --> r["rlRotatef sun-spin"]
  r --> c1["cube! (Sun)"]
  c1 --> s2["rlPushMatrix"]
  s2 --> t["rlTranslatef orbit-radius"]
  t --> c2["cube! (Earth)"]
  c2 --> p2["rlPopMatrix"]
  p2 --> p1["rlPopMatrix"]

The rlgl-solar-system example (net.b12n.raylib-jlt.rlgl-solar-system) uses exactly this to make Earth orbit the Sun and the Moon orbit Earth — nested push/translate/ rotate around three cube! calls, no matrix math in Clojure. It's also the portable substitute for the AArch64-only camera pointer trick (see the x86-64 caveat in struct-by-value-pointer-trick.md).

2D: filled arcs with sector!

The same wall stops DrawCircleSector / DrawRing / DrawCircleV: they take a Vector2 center by value (a float pair in FP registers), so they're unbindable by the pointer trick. The fix is the same — draw the arc as an rlgl triangle fan. sector! (in raylib.clj) builds one:

(rl/sector! :cx 270 :cy 235 :radius 165
            :start-deg a0 :end-deg a1 :segments 60 :color slice-color)

Each sub-triangle is emitted rim → center → rim with the angle increasing (rim = (sin θ, -cos θ), so 0° points up and grows clockwise). That order carries raylib's front-facing winding — a fan wound the other way is silently backface-culled (raylib enables GL_CULL_FACE). Two examples consume it: pie-chart (one fan per slice) and vector-angle (a translucent fan for the measured angle). color-wheel draws its own fan directly because it needs a per-vertex hue (a different rl-color! before each rim vertex), which a single-color helper can't express. The showpiece penrose-tiling fills ~900 deflation triangles of mixed winding, so it normalizes each to negative signed area (the proven front face) before the batch — the general form of the winding rule sector! bakes in.

2D: ring! and line-ex!

Two more by-value casualties get the same rlgl treatment:

  • ring! — a filled annulus (donut sector), the stand-in for DrawRing. Instead of a center-anchored fan it walks a quad strip between an inner and an outer radius: each angular step emits two triangles spanning inner → outer across dθ, wound front-facing. analog-clock's bezel and ring-drawing use it.
  • line-ex! — a thick line, the stand-in for DrawLineEx (both Vector2 endpoints by value). It offsets the two endpoints by ±thick/2 along the unit perpendicular (dy,-dx)/len and fills the resulting quad as two triangles. That perpendicular convention keeps the quad front-wound at every line direction — so a rotating fan of them (lines-drawing) or a sweeping clock hand (analog-clock) never flips to a culled back face. line-ex! also draws the clock ticks and the ring-drawing outline stroke.

Both are single-color (rl-color! once, then the vertices) and, like sector!, live in raylib.clj beside the raw rl-* binds.

Why this generalizes

When a C graphics API forces geometry through by-value small-float-vector arguments, look for a scalar immediate-mode or builder API on the same library and drive that instead of fighting the ABI. rlgl is raylib's; many GPU libraries ship an equivalent. The cost is you re-express shapes as vertex streams — cheap, and it keeps the whole FFI boundary scalar.

See also

  • struct-by-value-pointer-trick.md — the camera side (>16-byte structs) and why its matrix-op alternative lives here.
  • color-by-value.md — rl-color! shares the packed-Color representation with the rest of the API.
  • kwarg-drawing-api.md — cube! follows the keyword-arg convention; the raw rl-* binds stay positional.

The struct-by-value pointer trick (Camera2D / Camera3D)

raylib's cameras are structs passed by value: BeginMode2D(Camera2D) takes 24 bytes, BeginMode3D(Camera3D) takes 44 bytes. Chez foreign-procedure (what jolt.ffi/defcfn lowers to) has no by-value-aggregate calling convention. Unlike Color (see color-by-value.md) these are far too big to fake as a register-width int. But on AArch64 there is still a way — and it comes straight out of the ABI.

The ABI fact

typedef struct Camera2D {   // 24 bytes
    Vector2 offset;         //  8  (float x, y)
    Vector2 target;         //  8  (float x, y)
    float   rotation;       //  4
    float   zoom;           //  4
} Camera2D;

void BeginMode2D(Camera2D camera);

On the AArch64 (Apple silicon) procedure-call standard, a composite larger than 16 bytes is passed indirectly: the caller allocates the struct somewhere, and what actually goes in the argument register is a pointer to it. So at the machine level, BeginMode2D(Camera2D) and BeginMode2D(Camera2D*) are the same call — the callee receives a pointer either way.

That means a Jolt binding can declare the parameter as [:pointer], build the 24 bytes in native memory itself, and pass the pointer. The C side never knows the difference.

The Jolt side

src/net/b12n/raylib_jlt/raylib.clj binds BeginMode2D as a pointer taker and builds the struct with ffi/alloc + six little-endian ffi/write :floats:

(ffi/defcfn ^:private begin-mode-2d-ptr "BeginMode2D" [:pointer] :void)
(ffi/defcfn end-mode-2d "EndMode2D" [] :void)

(defn with-camera-2d
  [{:keys [offset-x offset-y target-x target-y rotation zoom]
    :or {offset-x 0 offset-y 0 target-x 0 target-y 0 rotation 0 zoom 1.0}} f]
  (let [p (ffi/alloc 24)]
    (try
      (ffi/write p :float 0  (double offset-x))
      (ffi/write p :float 4  (double offset-y))
      (ffi/write p :float 8  (double target-x))
      (ffi/write p :float 12 (double target-y))
      (ffi/write p :float 16 (double rotation))
      (ffi/write p :float 20 (double zoom))
      (begin-mode-2d-ptr p)
      (f)
      (end-mode-2d)
      (finally (ffi/free p)))))

The struct layout is the whole game: the field byte-offsets must match the C struct exactly (0, 4, 8, 12 for the four Vector2 floats, then 16 = rotation, 20 = zoom), and the buffer size must equal sizeof(Camera2D) = 24. Get either wrong and you read garbage or corrupt the stack.

flowchart LR
  j["Jolt: (ffi/alloc 24)<br/>write 6 floats"]
  j -->|"[:pointer] p"| b["BeginMode2D(Camera2D*)<br/>(= by-value on AArch64)"]
  b --> f["(f) draws in camera space"]
  f --> e["EndMode2D · (ffi/free p)"]

Camera3D is the same recipe, bigger

Camera3D is 44 bytes — three Vector3 (position, target, up) + a float fovy + an int projection. with-camera-3d allocates 44 and writes nine floats then one int:

(let [p (ffi/alloc 44)]
  ;; … nine (ffi/write p :float …) at offsets 0..36 …
  (ffi/write p :float 36 (double fovy))
  (ffi/write p :int   40 (int projection))   ; 0 = perspective
  (begin-mode-3d-ptr p) (f) (end-mode-3d))

Both wrappers use try/finally so the native buffer is freed even if f throws — the same discipline the sibling project applies to its node buffers.

The x86-64 caveat — this is not portable as written

The trick works because AArch64 passes >16-byte structs indirectly. On the x86-64 System V ABI, those 24 bytes are classified and passed on the stack (in pieces), which a [:pointer] binding does not do — it would put a pointer where the callee expects 24 bytes of struct data, and the call reads garbage (typically an "invalid memory reference" crash).

So camera2d/camera-3d are AArch64-only as written. The portable alternative is to skip BeginMode2D entirely and apply the same transform with rlgl's scalar matrix ops (rlPushMatrix/rlTranslatef/rlRotatef/rlScalef) — which is exactly what BeginMode2D does internally. See rlgl-immediate-mode.md for the matrix stack. The source flags this inline:

;; NOTE: this is AArch64-specific — on the x86-64 SysV ABI the 24 bytes are passed
;; on the stack, which a [:pointer] binding does NOT do (see README). For a portable
;; alternative, apply the same transform with the scalar rlgl matrix ops instead.

By-value returns are a different, harder problem

This trick fakes a by-value struct argument. It does nothing for a function that returns a struct by value — on AArch64 that uses the x8 sret register, which Chez doesn't expose. raylib's hot path never needs a by-value struct return, so this repo never hits it. Its Jolt sibling b12n-tsj (not yet public) is not so lucky: tree-sitter's ts_tree_root_node returns a 32-byte TSNode by value and ts_node_type takes one, so it can't fake either direction with a pointer binding and must ship a C shim. That contrast is the reason both projects exist side by side.

Why this generalizes

For any Chez/Jolt FFI on AArch64 that needs to pass a >16-byte struct by value: allocate sizeof(T) with ffi/alloc, write each field at its C offset, bind the function as [:pointer], and free in finally. Confirm sizeof and every offset against the C header. Do not assume it ports to x86-64 — reach for rlgl matrix ops (or a real shim) there.

See also

  • color-by-value.md — the small-struct case that fits in a register and needs none of this.
  • rlgl-immediate-mode.md — the portable matrix-stack alternative, and the fallback for by-value float structs (Vector2/Vector3).
  • b12n-tsj (not yet public) — the sibling case where the pointer trick isn't enough (by-value returns) and you must write a C shim.

b12n-raylib-jlt — raylib examples in Jolt. A community suite of raylib examples written in native Clojure, calling libraylib directly over jolt.ffi.