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.
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.
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 auint32, 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-byteTSNodepassed AND returned by value — so it needs a full C shim. raylib needs none.
Three ABI facts drive every distinctive decision in this repo:
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)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)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.
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).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.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!).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.examples/ tree is the reference these ports are named after.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 :uintEvery 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.
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.
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"]
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.
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)))
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).
struct-by-value-pointer-trick.md — the16-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.Every example at full size — linked from the example catalog's preview thumbnails.
the classic vector shooter (rotate/thrust/fire)

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

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

auto-fire survival: move, waves chase you

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

paddle + ball + brick grid (mouse paddle)

marching aliens (arrows + SPACE to shoot)

flap through the pipe gaps (SPACE)

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

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

the minimal raylib window + text

steer a ball with the arrow keys

a ball follows the mouse; click to recolor

scroll a box with the mouse wheel

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

per-frame vs delta-time movement

a scissor rectangle clips a grid

a LOGO/TITLE/GAMEPLAY/ENDING flow

a new random value every two seconds

a ball bouncing around the window

shape primitives + an rlgl triangle

every named raylib color in a grid

a vertical two-color gradient

two eyes track the mouse

a twinkling starfield

the raylib logo from rectangles + text

a fading trail follows the cursor

a binary fractal tree

a live unit-circle trig visualization

a rotating bullet spiral

a rainbow strip via rlgl immediate mode

AABB collision between two boxes

a dashed line follows the mouse

chaotic double-pendulum motion + trail

strokes mirrored with 6-fold symmetry

a rainbow Hilbert space-filling curve

fixed spokes + a spinning line

2D balls under gravity, SPACE respawns

a cubic Bézier that follows the mouse

an HSV color wheel (rlgl triangle fan)

labelled pie slices via rl/sector!

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

the angle between two vectors (arc + readout)

a grid of balls, each on a different easing curve

a P3 Penrose rhombus tiling (deflation)

a live analog clock (libc local time)

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

an animated annulus via rl/ring!

rounded rects via sector! corners

drag the corner handle to resize a rect

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

font sizes + MeasureText centering

a message types itself out

padded score + MM:SS timer readouts

align a word inside a box (MeasureText)

type into a text box (GetCharPressed)

an orbiting 3D camera (Camera3D by value)

an NxN grid of cubes rippling in 3D

walk a yard of columns in first person

a rotating 4D hypercube projected to 2D

pyramid/octahedron/torus/helix in 3D lines

Sun/Earth/Moon via the rlgl matrix stack

a player cube colliding with 3D boxes

a single cube spinning via the rlgl matrix stack

a row of cubes each spinning with a phase offset

perspective vs orthographic (SPACE toggles)

~1500 points as tiny rlgl cubes, rotating

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

Conway's Game of Life (SPACE reseeds)

flocking birds (separation/alignment/cohesion)

rockets + fading particle bursts

rotating circles trace a square wave

animated hypotrochoid roulette curves

an L-system fractal plant (grows + regrows)

particles steered by a flow field (trails)

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)
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).
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.
The suite is deliberately mechanical to grow. One new example touches four places:
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.deps.edn alias — :<name> {:main-opts ["-m" "net.b12n.raylib-jlt.<name>"]} so joltc -M:<name> works.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.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.
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.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.
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-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.
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).
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)"]
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
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.
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))))
example-catalog.md — every example inherits these guards through the shared loop shape.kwarg-drawing-api.md — the rl/*! calls inside the loop.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.
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!, text!, fps!, rect!, rect-lines!, rect-gradient!, circle!, circle-lines!, ellipse!, line!, pixel!.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.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 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/defcfnbinds 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 [] …))
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.
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.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.
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.
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_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.
cube! is 12 rlgl trianglesThe 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.
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).
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.
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.
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.
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.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.
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.
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 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 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.
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.
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.
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.