Merge branch 'worktree-agent-a065a2101ee7d8007' into dev-loop

This commit is contained in:
Joseph Ferano 2026-09-12 03:55:50 +07:00
commit 71877a5baa
6 changed files with 1216 additions and 12 deletions

2
.gitignore vendored
View File

@ -43,3 +43,5 @@ old-ocaml/
/sand
/conditions-play
.claude/
probe
probe.c

299
sand.flan
View File

@ -207,6 +207,136 @@
(rl/draw-texture-ex brush-mirrored (rl/Vector2 {:x 110.0 :y 46.0})
0.0 2.0 rl/white)))
;; ── Sound ──────────────────────────────────────────────────────────
;;
;; Nothing in this section can be in the acceptance table and the reason is
;; sharper than "it needs a GL context": a Sound is a buffer the miniaudio
;; mixer owns, so it does not exist until init-audio-device has found a
;; device, and a machine with no sound server gets a zeroed struct and a
;; warning. What CAN be asserted headlessly is the Wave the sound is made
;; from — test/programs/raylib-audio.flan does exactly that — so the split
;; here is real and not an excuse: the samples are checked, the playing is
;; only listened to.
;;
;; The tone is generated rather than loaded because a .wav in the repository
;; would be an asset to maintain for one plink, and because generating it is
;; what gives load-sound-from-wave and export-wave a call site that is not a
;; test.
(defconst tone-rate 22050)
(defconst tone-frames 4410) ; 0.2 s
(defconst tone-path "/tmp/flan-sand-tone.wav")
;; Little-endian signed 16-bit PCM, two bytes per frame, written as bytes
;; because that is what a Wave's `data` is: the element width is `sample-size`,
;; a run-time number, and no Flan type says what the buffer holds.
(defvar tone-pcm [8820 u8])
(defn write-sample [i i32 v i32]
(set (at tone-pcm (* i 2)) (u8 (bit-and v 255)))
(set (at tone-pcm (+ (* i 2) 1)) (u8 (bit-and (>> v 8) 255))))
;; A square wave that decays to nothing over its length, which is the
;; cheapest thing that sounds like a plink rather than a click. `period` is
;; the half-period in frames, so a smaller one is a higher note.
(defn build-tone [period i32]
(dotimes [i tone-frames]
(let [amp (/ (* 9000 (- tone-frames i)) tone-frames)]
(write-sample i (if (= 0 (% (/ i period) 2)) amp (- 0 amp))))))
(defvar audio-ok bool)
(defvar tone rl/Sound)
(defvar tone-ok bool)
(defvar music rl/Music)
(defvar music-ok bool)
(defvar music-on bool)
(defn start-audio []
(rl/init-audio-device)
(set audio-ok (rl/audio-device-ready?))
(unless audio-ok
(print-line "sand: no audio device — the grains are silent"))
(build-tone 40)
(let [w (rl/Wave {:frame-count (u32 tone-frames) :sample-rate (u32 tone-rate)
:sample-size 16 :channels 1
:data (addr (at tone-pcm 0))})]
(when audio-ok
(set tone (rl/load-sound-from-wave w))
(set tone-ok (rl/sound-valid? tone))
(rl/set-sound-volume tone 0.25)
(rl/set-sound-pan tone 0.5)
;; The same samples out to a file and straight back in as a stream, so
;; the Music side has a call site without an asset in the repository.
;; A Sound is resident and a Music is decoded as it plays, which is the
;; whole difference, and update-music-stream in the loop below is the
;; visible consequence of it.
(when (rl/export-wave w tone-path)
(set music (rl/load-music-stream tone-path))
(set music-ok (rl/music-valid? music))
(when music-ok
(set (.looping music) true)
(rl/set-music-volume music 0.15)
(rl/set-music-pitch music 0.5)))))
(rl/set-master-volume 0.6))
(defn stop-audio []
(when music-ok (rl/unload-music-stream music))
(when tone-ok (rl/unload-sound tone))
(rl/close-audio-device))
;; Every plink goes through here, so a silent build — no device, or a wave
;; that would not load — is one branch and not a guard at every call site.
(defn plink [pitch f32]
(when tone-ok
(rl/set-sound-pitch tone pitch)
(rl/play-sound tone)))
(defn toggle-music []
(when music-ok
(set music-on (not music-on))
(if music-on
(rl/play-music-stream music)
(rl/pause-music-stream music))))
;; ── The scene, off-screen ───────────────────────────────────────────
;;
;; The world is drawn into a render texture and the render texture is drawn to
;; the screen, which is what a post-process pass or a pixel-perfect upscale
;; would hang off. There is nothing to assert here either — LoadRenderTexture
;; makes a GL framebuffer object and answers an id of 0 with no context — so
;; the fallback below is not defensive padding, it is what keeps the program
;; honest when the framebuffer is not there.
;;
;; The source rectangle's height is NEGATIVE on purpose. raylib renders into a
;; framebuffer bottom-up, so drawing the result the right way up needs the
;; flip, and getting it wrong is a world that is upside down rather than an
;; error.
(defvar scene rl/RenderTexture2D)
(defvar scene-ok bool)
(defn load-scene []
(set scene (rl/load-render-texture screen-width screen-height))
(set scene-ok (rl/render-texture-valid? scene))
(unless scene-ok
(print-line "sand: no render texture — drawing straight to the screen")))
;; ── The font ────────────────────────────────────────────────────────
;;
;; get-font-default needs a window: the default font is loaded as part of
;; init-window and LoadFontDefault is not exported, which is the same fact
;; that makes the plain measure-text answer 0 headless. The Font family IS
;; assertable — test/programs/raylib-font.flan builds one by hand and has
;; raylib measure with it — so what is left here is the drawing, and the
;; drawing is looked at.
(defvar hud-font rl/Font)
(defvar hud-font-ok bool)
(defn load-hud-font []
(set hud-font (rl/get-font-default))
(set hud-font-ok (rl/font-valid? hud-font)))
;; ── The view ────────────────────────────────────────────────────────
;;
;; begin-mode-2d and end-mode-2d were bound along with Camera2D and then
@ -279,8 +409,27 @@
(defn game-update []
(when (rl/key-pressed? :r) (clear-grid))
(move-view)
;; key-released? and mouse-button-pressed? were bound and called by nothing
;; at all, which is the same as not having bound them. They are the two
;; halves nothing else here uses: space cycles the colour when it comes back
;; UP, and the right button resets the view the instant it goes DOWN, so the
;; two edges are told apart by eye rather than only in the source.
(when (rl/key-released? :space) (next-color) (plink 1.4))
(when (rl/mouse-button-pressed? :right) (reset-view) (plink 0.6))
(when (rl/mouse-button-pressed? :left) (plink 1.0))
(when (rl/key-pressed? :m) (toggle-music))
;; The pad, when there is one. Pressed and released are separate events here
;; too, for the same reason.
(when (rl/gamepad-available? 0)
(when (rl/gamepad-button-pressed? 0 :right-face-down) (plink 1.2))
(when (rl/gamepad-button-released? 0 :right-face-down) (next-color))
(when (rl/gamepad-button-down? 0 :middle-right) (clear-grid)))
(when (rl/mouse-button-down? :left) (paint))
(when (rl/mouse-button-released? :left) (next-color))
;; Once a frame, every frame, or the stream runs dry and the music stops
;; without saying anything. This is the whole difference between a Music and
;; a Sound.
(when music-on (rl/update-music-stream music))
(step))
@ -342,11 +491,20 @@
;; visible rather than merely different.
(defn draw-hud []
(let [title "SAND"
keys "arrows pan , . zoom 0 reset r clear"
keys "arrows pan , . zoom 0 reset r clear space colour m music"
;; measure-text is what sizes the panel, so the box fits the string
;; rather than a number somebody guessed. Headless it answers 0 for
;; everything, which is why it is not in the acceptance table.
w (max (rl/measure-text title 30) (rl/measure-text keys 20))
;;
;; The title is measured with the Font form instead, which takes a
;; float size and a spacing the integer form has no way to express —
;; and then drawn with the matching draw-text-ex, so the box and the
;; letters agree. That agreement is the check: measure with one and
;; draw with the other and the panel is visibly the wrong width.
title-w (if hud-font-ok
(i32 (.x (rl/measure-text-ex hud-font title 30.0 4.0)))
(rl/measure-text title 30))
w (max title-w (rl/measure-text keys 20))
h 72
x 24
y (- (rl/get-screen-height) (+ h 24))
@ -358,7 +516,10 @@
(rl/draw-rectangle-rounded-lines panel (f32 0.2) 8 (rl/get-color 0x404060FF))
(rl/draw-rectangle-rounded-lines-ex panel (f32 0.2) 8 (f32 2.0)
(rl/get-color 0x6060A0FF))
(rl/draw-text title x y 30 rl/white)
(if hud-font-ok
(rl/draw-text-ex hud-font title (rl/Vector2 {:x (f32 x) :y (f32 y)})
30.0 4.0 rl/white)
(rl/draw-text title x y 30 rl/white))
(rl/draw-text keys x (+ y 40) 20 (rl/get-color 0xA0A0B0FF))
;; The palette, along the bottom right. The selected colour is the one
@ -372,6 +533,18 @@
(when (= i current-color)
(rl/draw-circle-lines cx sh (f32 22.0) rl/white))))
;; The selected index as a digit, drawn one codepoint at a time. There
;; is no string formatting in the language yet, so this is the only way
;; a number reaches the screen at all — and get-glyph-index is what says
;; whether the default font has the digit before it is asked for.
(when hud-font-ok
(let [cp (+ 48 current-color)]
(when (> (rl/get-glyph-index hud-font cp) 0)
(rl/draw-text-codepoint hud-font cp
(rl/Vector2 {:x (f32 (- sw 24))
:y (f32 (- sh 96))})
30.0 rl/white))))
;; A zoom read-out with no number in it, because there is no string
;; formatting yet: the bar's length is the zoom. draw-rectangle-rec,
;; draw-rectangle-v and draw-rectangle-lines are the three remaining
@ -407,16 +580,118 @@
(f32 30.0) (f32 34.0) spin (+ spin (f32 270.0)) 32
rl/white)))))
(defn game-draw []
(rl/clear-background rl/black)
;; ── Gamepads, touch and gestures ────────────────────────────────────
;;
;; None of this can be asserted anywhere, and the reason is worth stating
;; rather than assuming. With no pad attached, gamepad-available? is false,
;; every button predicate is false and every axis reads 0.0 — which is exactly
;; what a wrapper with its two int arguments exchanged would report. Touch and
;; gestures are the same: they are fed by raylib's own event polling inside a
;; frame loop, so they answer nothing at all headless. A test would be
;; asserting that two zeroes are equal.
;;
;; So they are drawn, and the only check they get is that moving a stick moves
;; the marker. Every read-out below is asymmetric on purpose — the stick dot
;; is offset by x and y separately, and the trigger bars are different lengths
;; — so a crossed wrapper is visible rather than merely different.
(defn draw-input-state []
(let [ox (f32 200.0)
oy (f32 90.0)
r (f32 34.0)]
(when (rl/gamepad-available? 0)
;; The ring only appears when raylib says a pad is there, which is what
;; separates "centred stick" from "no pad" — both of which are a dot in
;; the middle otherwise.
(rl/draw-circle-lines (i32 ox) (i32 oy) r (rl/get-color 0x6060A0FF))
(let [p (rl/Vector2 {:x (+ ox (* (rl/get-gamepad-axis-movement 0 :left-x) r))
:y (+ oy (* (rl/get-gamepad-axis-movement 0 :left-y) r))})]
(rl/draw-circle-v p (f32 5.0) rl/white))
;; The two triggers as bars of different lengths, so exchanging them is
;; visible. They rest at -1 and not at 0, which raylib does not
;; normalise and neither does this — hence the +1.
(let [lt (+ (f32 1.0) (rl/get-gamepad-axis-movement 0 :left-trigger))
rt (+ (f32 1.0) (rl/get-gamepad-axis-movement 0 :right-trigger))]
(rl/draw-rectangle-v (rl/Vector2 {:x (+ ox (f32 46.0)) :y (- oy (f32 12.0))})
(rl/Vector2 {:x (* lt (f32 30.0)) :y (f32 8.0)})
(rl/get-color 0x8080C0FF))
(rl/draw-rectangle-v (rl/Vector2 {:x (+ ox (f32 46.0)) :y (+ oy (f32 4.0))})
(rl/Vector2 {:x (* rt (f32 50.0)) :y (f32 8.0)})
(rl/get-color 0x8080C0FF)))
;; One pip per axis the pad reports, and one per face button that is
;; NOT up — gamepad-button-up? rather than -down? so the negative form
;; has a call site of its own.
(dotimes [i (rl/get-gamepad-axis-count 0)]
(rl/draw-pixel (+ (i32 ox) (* i 4)) (+ (i32 oy) 44) rl/white))
(unless (rl/gamepad-button-up? 0 :right-face-down)
(rl/draw-circle (+ (i32 ox) 110) (i32 oy) (f32 6.0) rl/white))
;; -1 when nothing is pressed, which is why this is an i32 and not a
;; GamepadButton: the answer is outside the enum.
(when (>= (rl/get-gamepad-button-pressed) 0)
(rl/draw-circle (+ (i32 ox) 130) (i32 oy) (f32 6.0)
(rl/get-color 0xFFC000FF))))
;; Touch. On a desktop the count is 0 but point 0 still follows the
;; mouse, so the ring below is what says a real touchscreen is there.
(dotimes [i (rl/get-touch-point-count)]
(rl/draw-circle-lines-v (rl/get-touch-position i) (f32 18.0)
(rl/get-color 0xA0A0FFFF))
(rl/draw-pixel (rl/get-touch-x) (rl/get-touch-y) rl/white)
(rl/draw-pixel-v (rl/Vector2 {:x (f32 (rl/get-touch-point-id i))
:y (f32 4.0)})
rl/white))
;; And the gesture, if any: a bar as long as the hold has lasted, and the
;; drag vector drawn from the centre of the ring.
(unless (= (rl/get-gesture-detected) :none)
(rl/draw-rectangle (i32 ox) (+ (i32 oy) 54)
(i32 (* (f32 60.0) (rl/get-gesture-hold-duration)))
8 rl/white)
(when (rl/gesture-detected? :drag)
(let [d (rl/get-gesture-drag-vector)]
(rl/draw-line-v (rl/Vector2 {:x ox :y oy})
(rl/Vector2 {:x (+ ox (* (.x d) (f32 200.0)))
:y (+ oy (* (.y d) (f32 200.0)))})
(rl/get-color 0xFFC000FF))))
(when (rl/gesture-detected? :pinch-in)
(let [q (rl/get-gesture-pinch-vector)]
(rl/draw-circle-v (rl/Vector2 {:x (+ ox (* (.x q) (f32 200.0)))
:y (+ oy (* (.y q) (f32 200.0)))})
(f32 4.0) rl/white))))))
(defn draw-world []
;; Everything between these two is in world space and moves with the camera.
(rl/begin-mode-2d view)
(draw-grid)
(draw-world-cursor)
(rl/end-mode-2d)
(rl/end-mode-2d))
(defn game-draw []
;; The world goes into the render texture first, if there is one. Note the
;; clear inside the texture mode: the framebuffer keeps last frame's pixels
;; otherwise, which looks like a trail and not like a bug.
(when scene-ok
(rl/begin-texture-mode scene)
(rl/clear-background rl/black)
(draw-world)
(rl/end-texture-mode))
(rl/clear-background rl/black)
;; Then the texture, upside down on purpose: raylib renders into a
;; framebuffer bottom-up, so a negative source height is the correction and
;; not a trick. Without a framebuffer the world is drawn straight to the
;; screen and nothing else changes.
(if scene-ok
(rl/draw-texture-rec (.texture scene)
(rl/Rectangle {:x 0.0 :y 0.0
:width (f32 screen-width)
:height (f32 (- 0 screen-height))})
(rl/Vector2 {:x 0.0 :y 0.0})
rl/white)
(draw-world))
;; And everything after it is in screen pixels again.
(draw-brush)
(draw-hud)
(draw-input-state)
(rl/draw-fps 20 20))
(defn main []
@ -431,6 +706,18 @@
(load-brush)
(defer (rl/unload-texture brush))
(defer (rl/unload-texture brush-mirrored))
;; The same rule for the framebuffer and the default font, and a third one
;; for the mixer: the audio device is its own subsystem and does not come
;; with the window, so it is opened and closed on its own.
(load-scene)
(defer (rl/unload-render-texture scene))
(load-hud-font)
(start-audio)
(defer (stop-audio))
;; Gestures are off until something asks for them. Everything, because the
;; read-out draws whichever one arrives, and because the parameter is a set
;; of flags rather than one member — see the note on the Gesture enum.
(rl/set-gestures-enabled rl/gesture-all)
;; The dev agent listens on a socket for redefinitions and hands them over;
;; (agent/poll) below is where they are installed. Building without --dev is
;; fine — nothing has cells to install into, so a module is refused on the

View File

@ -0,0 +1,202 @@
(import rl "vendor:raylib")
;; raylib's Wave family, headless — and the point of this file is that it IS
;; headless, which the brief for this lane said audio could not be.
;;
;; The split inside raylib's audio module runs exactly along that line. A
;; Sound is a buffer the miniaudio mixer owns and a Music is a decoder feeding
;; one, so neither exists until init-audio-device has found a device. A
;; **Wave** is samples in RAM and four integers describing them, and every
;; call that operates on one — copy, crop, reformat, export, load, decode to
;; floats — runs on the CPU with no device open at all. That is the Image
;; family's situation exactly, and it gets the Image family's kind of test:
;; raylib *computes*, and the answer is a number.
;;
;; Four separate claims are pinned here, and they are different claims:
;;
;; 1. **Wave's layout, from scalars in and fields out.** wave-format is
;; handed three plain integers and rewrites all four u32 fields, one of
;; them — frame-count — *computed* from the ratio of the old and new
;; sample rates. The answers are 16, 16000, 8 and 2: all distinct, and
;; the 16 is named by no argument at all, so a permuted defstruct has
;; nothing to cancel against. This is gen-image-color's argument, which
;; is the strongest shape a headless FFI test has.
;;
;; 2. **The file, as external ground truth.** export-wave writes a RIFF
;; header carrying sample-rate, sample-size and channels; load-wave reads
;; it back. Both ends are dr_wav's and agree with each other, not with
;; whatever field order Flan believes in — the same reason the PNG round
;; trip in raylib-image.flan is not the symmetric trap a store-and-return
;; check is.
;;
;; Its reach is narrower than it looks and the narrowness is worth
;; knowing: exchanging sample-size and channels turns the loaded line
;; into "8 8000 16 16", while exchanging frame-count and sample-rate
;; leaves every line of the round trip untouched. What catches THAT pair
;; is the crop and the reformat, above. Neither claim covers the other.
;;
;; 3. **`data` as a pointer, and the bytes behind it.** load-wave-samples
;; decodes through the buffer. Move `data` up among the integers and
;; every number in this file reads half of a pointer instead.
;;
;; 4. **Crop's byte arithmetic, which is the axis discriminator.** raylib
;; crops at `init-frame * channels * sample-size/8` bytes into the
;; buffer, so cropping one frame out of the middle and decoding it says
;; which frame it was. That is what distinguishes sample-size from
;; channels: exchange them and a one-frame crop lands two bytes off and
;; the sample that comes back is a different number, not the same one
;; mirrored. Axis-aligned geometry could never do this and neither could
;; a round trip.
;;
;; The four integers describing the source wave are 8, 8000, 16 and 1 — all
;; different, for the same reason the test image in raylib-image.flan is 4 by
;; 2 and not square.
;; The samples, as bytes, little-endian 16-bit signed PCM. They are bytes
;; rather than i16 because `data` is (Ptr u8): a wave's element width is
;; `sample-size`, a run-time number, so no Flan type says what the buffer
;; holds. Writing them out is the honest spelling — the file then says what it
;; means about 16-bit PCM instead of hiding it behind a cast.
;;
;; 0, 1000, 2000, 3000, -3000, -2000, -1000, 0 — one cycle, every sample
;; different from its neighbours, both signs present, so a decode that lost
;; the byte order or the sign is a wrong number rather than merely a
;; different one.
(defvar pcm [16 u8])
(defn load-pcm []
(set (at pcm 0) 0x00) (set (at pcm 1) 0x00) ; 0
(set (at pcm 2) 0xE8) (set (at pcm 3) 0x03) ; 1000
(set (at pcm 4) 0xD0) (set (at pcm 5) 0x07) ; 2000
(set (at pcm 6) 0xB8) (set (at pcm 7) 0x0B) ; 3000
(set (at pcm 8) 0x48) (set (at pcm 9) 0xF4) ; -3000
(set (at pcm 10) 0x30) (set (at pcm 11) 0xF8) ; -2000
(set (at pcm 12) 0x18) (set (at pcm 13) 0xFC) ; -1000
(set (at pcm 14) 0x00) (set (at pcm 15) 0x00)) ; 0
(defconst wav-path "/tmp/flan-raylib-audio.wav")
(defn show-wave [name string w rl/Wave]
(print-str name)
(print-str " ") (print-i64 (i64 (.frame-count w)))
(print-str " ") (print-i64 (i64 (.sample-rate w)))
(print-str " ") (print-i64 (i64 (.sample-size w)))
(print-str " ") (print-i64 (i64 (.channels w)))
(newline))
(defn show-bool [name string b bool]
(print-str name) (print-str " ")
(print-line (if b "yes" "no")))
;; A decoded sample is compared with a tolerance and the verdict is printed,
;; not the number. 1000 over a 15- or 16-bit full scale is 0.0305185 or
;; 0.0305176, and this table compares stdout byte for byte at two optimisation
;; levels, so printing the float would pin raylib's choice of divisor rather
;; than Flan's field order. Nothing is lost: a permuted layout is not out by a
;; rounding step, it reads a different buffer.
(defn near? [a f32 b f32] bool
(let [d (- a b)]
(< (if (< d 0.0) (- 0.0 d) d) 0.0005)))
;; One frame out of a wave, decoded to a float.
;;
;; This is shaped the way it is because of a real limit in Flan, not for
;; effect: load-wave-samples answers a (Ptr f32), a pointer cannot be indexed
;; — `at` takes an array, a slice or a string, and there is no pointer
;; arithmetic — so `deref` reaches sample 0 and nothing reaches sample 3. So
;; the wave is cropped to the single frame wanted FIRST, and then sample 0 is
;; the one being asked about.
;;
;; The detour pays for itself. Reading through an index would have exercised
;; only the decoder; cropping first puts raylib's byte-offset arithmetic —
;; init-frame times channels times sample-size over 8 — in front of the
;; decoder, and that arithmetic reads two of the fields this file is trying
;; to pin.
(defn frame-at [w rl/Wave i i32] f32
(let [one (rl/wave-copy w)]
(rl/wave-crop (addr one) i (+ i 1))
(let [s (rl/load-wave-samples one)
v (deref s)]
(rl/unload-wave-samples s)
(rl/unload-wave one)
v)))
(defn show-frame [name string w rl/Wave i i32 want f32]
(show-bool name (near? (frame-at w i) want)))
(defn main [] i32
(rl/set-trace-log-level :warning)
(load-pcm)
;; ── The wave, built by hand ─────────────────────────────────────────
;;
;; Nothing raylib made: four integers Flan chose and a buffer Flan owns. So
;; every number below is raylib reading THIS struct, and there is no
;; raylib-produced struct anywhere for a permutation to hide inside.
(let [src (rl/Wave {:frame-count 8 :sample-rate 8000 :sample-size 16
:channels 1 :data (addr (at pcm 0))})]
(show-bool "valid" (rl/wave-valid? src))
(show-wave "source" src)
;; ── Crop ────────────────────────────────────────────────────────────
;;
;; Frames 2 up to 6, so four of the eight survive and nothing else
;; changes. This is the one call that moves frame-count on its own, which
;; is what separates that field from sample-rate: exchange the two and the
;; crop is asked for frames 2..6 of a wave claiming 8000 of them, and the
;; 4 lands in the sample-rate slot instead.
;;
;; The wave is mono, so raylib 5.5's rename of these parameters from
;; samples to frames is not something this depends on having guessed
;; right — at one channel the two readings coincide.
(let [cropped (rl/wave-copy src)]
(rl/wave-crop (addr cropped) 2 6)
(show-wave "cropped" cropped)
(rl/unload-wave cropped))
;; ── Reformat ────────────────────────────────────────────────────────
;;
;; The strongest case in the file. Three scalars go in — 16000 Hz, 8 bits,
;; 2 channels — and four fields come out: 16, 16000, 8, 2. The 16 is
;; computed, twice the original eight frames because the rate doubled, and
;; it is the only one of the four no argument named. Eight bits rather
;; than sixteen so that sample-size cannot be confused with the frame
;; count it would otherwise equal.
(let [reformatted (rl/wave-copy src)]
(rl/wave-format (addr reformatted) 16000 8 2)
(show-wave "reformatted" reformatted)
(rl/unload-wave reformatted))
;; ── Through the pointer, one frame at a time ────────────────────────
;;
;; See frame-at: each of these crops to a single frame and decodes it, so
;; raylib's byte offset into `data` is what selects the answer. Frames 1,
;; 3 and 4 are +1000, +3000 and -3000 — three different magnitudes and
;; both signs, so neither an offset that is out by a frame nor a decode
;; that read the bytes backwards survives.
(show-frame "frame 1 is +1000" src 1 0.030518)
(show-frame "frame 3 is +3000" src 3 0.091553)
(show-frame "frame 4 is -3000" src 4 -0.091553)
(show-frame "frame 0 is zero" src 0 0.0)
;; ── Out to a file and back ──────────────────────────────────────────
;;
;; dr_wav writes the header from three of the four fields and the payload
;; length from the fourth, and reads all four back out of the file. Both
;; ends are external to Flan and agree with each other, so this is not the
;; round trip that passes for any field order — it is the PNG argument,
;; for audio. It also crosses a path as ptr+len.
;;
;; It does not catch everything, and the header note says which: exchange
;; frame-count and sample-rate and all three lines below stay green. The
;; crop and the reformat are what go red for that pair.
(show-bool "exported" (rl/export-wave src wav-path))
(let [back (rl/load-wave wav-path)]
(show-bool "loaded valid" (rl/wave-valid? back))
(show-wave "loaded" back)
;; And the samples off the file's own copy, so the bytes are checked to
;; have survived the encoder, the decoder and the layout at once.
(show-frame "loaded frame 1 is +1000" back 1 0.030518)
(show-frame "loaded frame 4 is -3000" back 4 -0.091553)
(rl/unload-wave back)))
0)

View File

@ -0,0 +1,174 @@
(import rl "vendor:raylib")
;; raylib's Font family, headless.
;;
;; A previous lane refused this whole family by name and gave a real reason: a
;; Font is three ints beside a Texture2D, a Rectangle* and a GlyphInfo*, a
;; GlyphInfo embeds an Image, and that was two more aggregates and two owned
;; arrays "for something with no headless test at the end of it". The second
;; half of that turned out to be wrong, and this file is the counter-example.
;;
;; **Nothing here makes a font.** That is the whole trick. Every raylib call
;; that produces one needs something a headless run has not got —
;; get-font-default needs init-window, load-font and load-font-ex need a TTF
;; on disk and a GL context to upload the atlas to — so this file *builds* a
;; Font out of Flan arrays, field by field, and hands it to raylib to compute
;; with. raylib's text measuring is pure CPU arithmetic: it walks the glyph
;; array for a codepoint, takes the advance out of the glyph or the width out
;; of the atlas rectangle, and scales by the base size.
;;
;; Which makes this the scalars-in/fields-out shape rather than the
;; store-and-return one. There is no raylib-produced struct anywhere in the
;; file for a permutation to cancel against: Flan chose every field, and what
;; comes back is a number raylib worked out from them.
;;
;; What is pinned, and by what:
;;
;; - **base-size** by the scale factor. Measuring "AB" at size 10 against a
;; base size of 10 is scale 1 and at size 20 is scale 2, and both are
;; asserted, so the field cannot be a constant that happens to fit.
;; - **glyph-count** by the search bound. 'Z' is not in the font, and
;; raylib's miss falls back to index 0 rather than reading past the array.
;; - **recs and glyphs** as pointers, by being read through at all.
;; - **advance-x against offset-x**, because glyph C deliberately has an
;; advance of 0: raylib then falls back to the atlas rectangle's width
;; plus offset-x, so measuring "ABC" reaches three more fields than
;; measuring "AB" does.
;; - **where the Texture2D sits inside the Font**, by a trap in raylib
;; rather than a choice here: MeasureTextEx returns (0,0) at once when
;; `texture.id` is 0. The hand-built font claims an id of 1 — there is no
;; texture — and if the texture landed anywhere else in the struct the id
;; would read 0 and every measurement below would collapse to zero.
;;
;; And what is NOT pinned, which matters as much: `glyph-padding` is read by
;; nothing raylib computes on the CPU, `offset-y` only moves a glyph when one
;; is actually drawn, and of each atlas rectangle only `width` is ever looked
;; at — `x`, `y` and `height` come back from get-glyph-atlas-rec exactly as
;; they were stored, which is the symmetric trap and proves nothing. Those
;; four rest on raylib's header and on sand.flan looking right. This is the
;; same limit raylib-ffi.flan records for Texture2D's width, height and
;; mipmaps, and it is written down for the same reason.
;;
;; The numbers are chosen so that no two are equal: base size 10, glyph count
;; 3, padding 2, advances 11 and 13, offsets 1, 2 and 3, atlas widths 5, 7
;; and 9. Everything prints exactly — no trigonometry, so unlike the rotated
;; camera in raylib-ffi.flan this can be compared as text.
;; The atlas rectangles, one per glyph. x and width differ per glyph so that
;; a fallback to the wrong index is a wrong number; height is the same 10 for
;; all three because raylib never reads it here.
(defvar glyph-recs [3 rl/Rectangle])
;; The glyphs themselves. `image` is raylib's own pixels for the glyph and is
;; left zeroed — it is present so the four ints in front of it are at the
;; right offsets and so a GlyphInfo is 40 bytes rather than 16. That claim is
;; checked: moving `image` to the front of the defstruct shifts the four ints
;; by 24 bytes, and the glyph search then finds nothing — every index reads 0
;; and glyph C answers with A's numbers.
(defvar glyphs [3 rl/GlyphInfo])
(defn build-glyphs []
(set (at glyph-recs 0) (rl/Rectangle {:x 0.0 :y 0.0 :width 5.0 :height 10.0}))
(set (at glyph-recs 1) (rl/Rectangle {:x 5.0 :y 0.0 :width 7.0 :height 10.0}))
(set (at glyph-recs 2) (rl/Rectangle {:x 12.0 :y 0.0 :width 9.0 :height 10.0}))
;; 65 66 67 are A B C. Advances 11 and 13 for the first two; C's advance is
;; 0 on purpose, which is what sends raylib down the other branch.
(set (at glyphs 0) (rl/GlyphInfo {:value 65 :offset-x 1 :offset-y 0
:advance-x 11 :image (rl/Image {})}))
(set (at glyphs 1) (rl/GlyphInfo {:value 66 :offset-x 2 :offset-y 0
:advance-x 13 :image (rl/Image {})}))
(set (at glyphs 2) (rl/GlyphInfo {:value 67 :offset-x 3 :offset-y 0
:advance-x 0 :image (rl/Image {})})))
(defn show-bool [name string b bool]
(print-str name) (print-str " ")
(print-line (if b "yes" "no")))
(defn show-i [name string v i32]
(print-str name) (print-str " ") (print-i64 (i64 v)) (newline))
(defn show-v [name string v rl/Vector2]
(print-str name)
(print-str " ") (print-f64 (f64 (.x v)))
(print-str " ") (print-f64 (f64 (.y v)))
(newline))
(defn show-rect [name string r rl/Rectangle]
(print-str name)
(print-str " ") (print-f64 (f64 (.x r)))
(print-str " ") (print-f64 (f64 (.y r)))
(print-str " ") (print-f64 (f64 (.width r)))
(print-str " ") (print-f64 (f64 (.height r)))
(newline))
(defn main [] i32
(rl/set-trace-log-level :warning)
(build-glyphs)
;; The font. `texture` is a lie in every field but `id`, and the id is 1 for
;; the reason in the header note: at 0, raylib refuses to measure anything.
;;
;; It is built here rather than by a helper because a `defn` cannot say it
;; returns one: the parser decides whether the form after a parameter list
;; is a return type or the first body expression by looking the name up in
;; the set of types THIS FILE declares, and a package's types are not in it
;; — imports are resolved after parsing. `(defn the-font [] rl/Font ...)`
;; therefore parses rl/Font as an expression and fails with "unknown name".
;; That is a parser limit and not something this file wanted; it applies to
;; rl/Vector2 just as much as to rl/Font.
(let [f (rl/Font {:base-size 10 :glyph-count 3 :glyph-padding 2
:texture (rl/Texture2D {:id 1 :width 32 :height 16
:mipmaps 1 :format 7})
:recs (addr (at glyph-recs 0))
:glyphs (addr (at glyphs 0))})]
;; font-valid? reads the texture id and both array pointers, so it is a
;; null check on the three things everything below dereferences.
(show-bool "valid" (rl/font-valid? f))
;; ── The search ──────────────────────────────────────────────────────
;;
;; A linear walk of `glyph-count` entries comparing `value`. Three hits at
;; three different indices, and one miss: 'Z' is not in the font and
;; raylib answers index 0 rather than running off the end. Shrink
;; glyph-count — or land the field somewhere else — and 'C' misses too.
(show-i "index A" (rl/get-glyph-index f 65))
(show-i "index B" (rl/get-glyph-index f 66))
(show-i "index C" (rl/get-glyph-index f 67))
(show-i "index Z" (rl/get-glyph-index f 90))
;; ── Reading back through the two pointers ───────────────────────────
;;
;; The atlas rectangle for B is the second entry of `recs`, and the glyph
;; for C is the third of `glyphs`. Both are indexed by the search above,
;; so an exchange of the two pointers is a Rectangle read as a GlyphInfo.
(show-rect "atlas B" (rl/get-glyph-atlas-rec f 66))
(let [g (rl/get-glyph-info f 67)]
(show-i "glyph C value" (.value g))
(show-i "glyph C offset" (.offset-x g))
(show-i "glyph C advance" (.advance-x g)))
;; ── Measuring, which is where the arithmetic is ─────────────────────
;;
;; "AB" is 11 + 13 = 24 wide and base-size tall, at scale 1.
(show-v "measure AB" (rl/measure-text-ex f "AB" 10.0 0.0))
;; "ABC" adds C, whose advance is 0, so raylib falls back to the atlas
;; width plus the offset: 9 + 3 = 12, for 36. This line and the one above
;; disagree by exactly the amount that proves the fallback branch ran, and
;; that branch is the only thing in the file that reads Rectangle.width
;; out of the recs array — exchange x and width in the Rectangle defstruct
;; and this line reads 39 while the one above stays 24.
(show-v "measure ABC" (rl/measure-text-ex f "ABC" 10.0 0.0))
;; The same string at twice the size and a spacing of 3. Scale is 20/10,
;; so 24 doubles to 48, and spacing is added once per gap and not once per
;; glyph: 48 + 3 = 51. Exchange base-size and glyph-count and the scale
;; becomes 20/3 and the search bound becomes 10 — both wrong, in different
;; directions, on the same line.
(show-v "measure AB big" (rl/measure-text-ex f "AB" 20.0 3.0))
;; One glyph, so there is no gap for the spacing to land in: 11 at scale
;; 1, and the 3 does not appear. Without this line a spacing added per
;; glyph rather than per gap would pass everything above.
(show-v "measure A spaced" (rl/measure-text-ex f "A" 10.0 3.0)))
0)

View File

@ -353,6 +353,138 @@ let () =
else
print_endline "acceptance: skipping the raylib Image case (no libraylib)";
(* raylib's Wave family, headless — and the first claim to make about it
is that it exists. The received wisdom in this repository was that
audio needs a device and so cannot be in this table at all. That is
true of Sound, Music and AudioStream, every one of which is a handle
the miniaudio mixer owns, and false of Wave: samples in RAM and four
integers describing them, with copy, crop, reformat, export, load and
decode all running on the CPU. So it gets the Image family's treatment.
Three shapes, and they pin different things.
wave-format is the scalars-in/fields-out case gen-image-color is: three
plain integers go in and all four u32 fields come out, with
frame-count *computed* from the ratio of the sample rates 16, from
eight frames at twice the rate, and no argument named it. Eight bits
rather than sixteen in that call so sample-size cannot be confused
with the frame count it would otherwise equal.
export-wave then load-wave is external ground truth, the PNG argument
transposed: dr_wav writes the header from three fields and reads them
back, agreeing with itself rather than with Flan's field order. Be
precise about its reach, because it is narrower than it looks it
catches sample-size against channels (the "loaded" line reads
"8 8000 16 16" when those two are exchanged) and NOT frame-count
against sample-rate, which leaves every line of the round trip green.
The crop and the reformat are what catch that pair.
And the decoded samples are the axis discriminator this section needed.
load-wave-samples answers a (Ptr f32), which Flan cannot index [at]
takes an array, a slice or a string so the program crops to a single
frame first and dereferences sample 0. That detour is what makes the
case strong rather than weak: raylib's crop offset is init-frame times
channels times sample-size over 8, so asking for frame 3 and getting
+3000 pins sample-size against channels. Exchange those two and the
crop lands two bytes off and the sample is a different number, not the
same one mirrored, which is the failure mode axis-aligned geometry
could never produce.
Verified red by permuting the Wave defstruct three ways: frame-count
with sample-rate (cropped reads "8 4 16 1" and reformatted
"16000 16000000 8 2", while the file round trip stays green see
above), sample-size with channels (every frame read turns to "no" and
the loaded line reads "8 8000 16 16"), and data moved to the front
(the run dies after two lines). *)
let raylib_audio_out =
"valid yes\n\
source 8 8000 16 1\n\
cropped 4 8000 16 1\n\
reformatted 16 16000 8 2\n\
frame 1 is +1000 yes\n\
frame 3 is +3000 yes\n\
frame 4 is -3000 yes\n\
frame 0 is zero yes\n\
exported yes\n\
loaded valid yes\n\
loaded 8 8000 16 1\n\
loaded frame 1 is +1000 yes\n\
loaded frame 4 is -3000 yes\n"
in
if Sys.command "ldconfig -p 2>/dev/null | grep -q libraylib" = 0 then begin
outputs "raylib audio, headless" "programs/raylib-audio.flan"
raylib_audio_out;
outputs ~opt:"-O0" "raylib audio, headless, -O0" "programs/raylib-audio.flan"
raylib_audio_out
end
else
print_endline "acceptance: skipping the raylib Wave case (no libraylib)";
(* raylib's Font family, headless, which the package refused to bind at
all until now. The stated reason was that a Font drags in two more
aggregates and two owned arrays "for something with no headless test at
the end of it". The generator takes all of it unchanged — a struct held
by value is emitted after what it contains, one held by pointer is
forward-declared and the test turned out to exist.
What makes it exist is that the program does not ASK raylib for a font.
Every call that makes one needs a window or a TTF and a GL context.
So it builds one out of Flan arrays, field by field, and hands it over
to be computed with: three glyphs, three atlas rectangles, a base size
of 10 and a texture that is a lie in every field but [id]. Text
measuring is pure arithmetic over exactly those fields, so this is
scalars in and numbers out with no raylib-produced struct anywhere for
a permutation to cancel against.
The one raylib trap worth recording: MeasureTextEx returns (0,0)
immediately when font.texture.id is 0. The hand-built font claims an id
of 1, and that guard is what pins where the Texture2D sits inside the
Font land it elsewhere and every measurement collapses to zero.
Glyph C carries an advance of 0 deliberately, which sends raylib down
its other branch: the atlas rectangle's width plus the glyph's offset,
9 + 3 = 12, which is why "ABC" is 36 and "AB" is 24. And "A" at a
spacing of 3 measures 11 and not 14, because spacing is added per gap
and not per glyph without that line, a wrapper that added it per
glyph would pass everything else.
Verified red by six permutations. In Font: base-size with glyph-count
(the measurements become 80, 120, 163 and 36.6667), the recs and glyphs
pointers (floats in the 1e9 range and a garbage atlas rectangle), and
[texture] moved to the end (the run dies after the first line). In
GlyphInfo: offset-x with advance-x (3, 6, 9, 1), and [image] moved to
the FRONT, which shifts the four ints by 24 bytes the glyph search
collapses, every index reads 0 and glyph C answers with A's fields.
In Rectangle: x with width, which moves "measure ABC" to 39 and leaves
"measure AB" at 24, since only the advance-0 fallback reads a width out
of the recs array. Two of the six were a crash rather than a wrong
number, which still counts.
What this case does NOT pin, said here for the same reason the
Texture2D notes above say it: glyph-padding is read by nothing raylib
computes on the CPU, offset-y only moves a glyph when it is drawn, and
of each atlas rectangle only `width` is ever looked at. Those four
fields rest on the header agreeing with raylib's and on sand.flan
looking right, and on nothing else. *)
let raylib_font_out =
"valid yes\n\
index A 0\nindex B 1\nindex C 2\nindex Z 0\n\
atlas B 5 0 7 10\n\
glyph C value 67\nglyph C offset 3\nglyph C advance 0\n\
measure AB 24 10\n\
measure ABC 36 10\n\
measure AB big 51 20\n\
measure A spaced 11 10\n"
in
if Sys.command "ldconfig -p 2>/dev/null | grep -q libraylib" = 0 then begin
outputs "raylib fonts, headless" "programs/raylib-font.flan"
raylib_font_out;
outputs ~opt:"-O0" "raylib fonts, headless, -O0" "programs/raylib-font.flan"
raylib_font_out
end
else
print_endline "acceptance: skipping the raylib Font case (no libraylib)";
(* Again at -O0. Everything above runs through mem2reg, which launders a
sloppy alloca; -O0 tests the IR actually emitted, so a disagreement
between the two points at undefined behaviour rather than a typo. *)

View File

@ -459,12 +459,12 @@
;; libraylib.so.550 and is why it is NOT in the acceptance table despite
;; looking like exactly the kind of call that could be.
;;
;; Font loading is not bound, deliberately. A Font is baseSize, glyphCount and
;; glyphPadding beside a Texture2D, a Rectangle* and a GlyphInfo* — and a
;; GlyphInfo embeds an Image. Binding it means binding two more aggregates and
;; two owned arrays for something with no headless test at the end of it, so
;; load-font, load-font-ex, unload-font, get-font-default, draw-text-ex and
;; measure-text-ex are all absent rather than half-done.
;; Fonts ARE bound now — see the section at the end of this file. The reason
;; they were not is worth keeping: a Font is three ints beside a Texture2D, a
;; Rectangle* and a GlyphInfo*, and a GlyphInfo embeds an Image, and that was
;; "two more aggregates and two owned arrays for something with no headless
;; test at the end of it". The generator takes all of that now, and the
;; headless test turned out to exist after all.
(declare-c draw-text
[text string x i32 y i32 font-size i32 color Color]
@ -483,3 +483,410 @@
(declare-c get-time [] f64 "GetTime")
(declare-c get-screen-width [] i32 "GetScreenWidth")
(declare-c get-screen-height [] i32 "GetScreenHeight")
;; ── Gamepads ────────────────────────────────────────────────────────
;;
;; Nothing here can be asserted headlessly and nothing here can be asserted
;; *at all* without a pad plugged in: with no gamepad, gamepad-available? is
;; false, every button predicate is false and every axis reads 0.0, which is
;; also exactly what a wrapper with its two int arguments exchanged would
;; report. So these are bound, wired into sand.flan's HUD, and honestly
;; described as untested — the only check they get is that a pad moves the
;; read-out.
;;
;; `pad` is an index from 0, not an enum: raylib's own parameter is an int and
;; how many are attached is a run-time question.
(defenum GamepadButton
[unknown 0
left-face-up 1 left-face-right 2 left-face-down 3 left-face-left 4
right-face-up 5 right-face-right 6 right-face-down 7 right-face-left 8
left-trigger-1 9 left-trigger-2 10
right-trigger-1 11 right-trigger-2 12
middle-left 13 middle 14 middle-right 15
left-thumb 16 right-thumb 17])
;; The triggers read -1 at rest and 1 fully pressed, unlike the sticks, which
;; are centred at 0. raylib does not normalise that and neither does this.
(defenum GamepadAxis
[left-x 0 left-y 1 right-x 2 right-y 3
left-trigger 4 right-trigger 5])
(declare-c gamepad-available? [pad i32] bool "IsGamepadAvailable")
(declare-c gamepad-button-pressed?
[pad i32 button GamepadButton] bool
"IsGamepadButtonPressed")
(declare-c gamepad-button-down?
[pad i32 button GamepadButton] bool
"IsGamepadButtonDown")
(declare-c gamepad-button-released?
[pad i32 button GamepadButton] bool
"IsGamepadButtonReleased")
(declare-c gamepad-button-up?
[pad i32 button GamepadButton] bool
"IsGamepadButtonUp")
;; -1 when nothing is pressed, so the answer is not a GamepadButton: raylib
;; returns an int outside the enum and the checker would have to be lied to.
(declare-c get-gamepad-button-pressed [] i32 "GetGamepadButtonPressed")
(declare-c get-gamepad-axis-count [pad i32] i32 "GetGamepadAxisCount")
(declare-c get-gamepad-axis-movement
[pad i32 axis GamepadAxis] f32
"GetGamepadAxisMovement")
;; SetGamepadVibration is NOT bound, and the reason is not the usual one. Two
;; things are wrong with it at once. Its arity changed — 5.1-dev takes three
;; floats and 6.1-dev takes four, there is no 5.5 header here to settle which,
;; and the generated prototype is what fixes the call, so a guess is a
;; corrupted stack frame rather than a link error. And it would not matter if
;; it were guessed right: the symbol in libraylib.so.550 disassembles to a
;; single TraceLog call and a jump — it is a stub that reports "not
;; implemented" and touches no motor. Binding it would be binding a warning.
;; ── Touch and gestures ──────────────────────────────────────────────
;;
;; Touch is a superset of the mouse on a desktop: with no touchscreen,
;; get-touch-point-count is 0 but get-touch-position 0 still tracks the mouse,
;; which is what makes it visible in sand.flan at all.
;;
;; The gesture system is fed by raylib's own event polling, so a gesture is
;; only ever detected inside a window's frame loop — nothing here is headless
;; material either.
;; A bitfield, not an ordinary enum: set-gestures-enabled takes the OR of
;; several and gesture-detected? tests against one. That is why the enabling
;; call below takes a u32 and not a Gesture — a keyword can only ever name one
;; member, and `all` is spelled out here so the common case still reads.
(defenum Gesture
[none 0 tap 1 double-tap 2 hold 4 drag 8
swipe-right 16 swipe-left 32 swipe-up 64 swipe-down 128
pinch-in 256 pinch-out 512])
(defconst gesture-all u32 1023)
(declare-c get-touch-position [index i32] Vector2 "GetTouchPosition")
(declare-c get-touch-x [] i32 "GetTouchX")
(declare-c get-touch-y [] i32 "GetTouchY")
(declare-c get-touch-point-count [] i32 "GetTouchPointCount")
(declare-c get-touch-point-id [index i32] i32 "GetTouchPointId")
(declare-c set-gestures-enabled [flags u32] "SetGesturesEnabled")
(declare-c gesture-detected? [gesture Gesture] bool "IsGestureDetected")
(declare-c get-gesture-detected [] Gesture "GetGestureDetected")
;; Degrees, and only meaningful while a drag is in progress.
(declare-c get-gesture-drag-vector [] Vector2 "GetGestureDragVector")
(declare-c get-gesture-drag-angle [] f32 "GetGestureDragAngle")
(declare-c get-gesture-pinch-vector [] Vector2 "GetGesturePinchVector")
(declare-c get-gesture-pinch-angle [] f32 "GetGesturePinchAngle")
(declare-c get-gesture-hold-duration [] f32 "GetGestureHoldDuration")
;; ── Render textures ─────────────────────────────────────────────────
;;
;; A framebuffer with two textures hanging off it: draw into it between
;; begin-texture-mode and end-texture-mode, then draw *it* like any other
;; texture. That is how a post-process pass and a pixel-perfect integer
;; upscale are both done.
;;
;; None of it is assertable here — LoadRenderTexture makes a GL framebuffer
;; object, so with no context it answers an id of 0 and every draw into it is
;; a no-op. `depth` is a renderbuffer rather than a real texture in raylib's
;; default configuration, so its id is the only field of it worth reading.
;;
;; The one thing the layout gets for free: RenderTexture2D is a u32 and two
;; Texture2Ds, and Texture2D is already pinned as far as anything headless can
;; pin it, so the only new claim here is the order of the three members.
;; sand.flan draws through one, which is where a wrong order shows up.
(defstruct RenderTexture2D [id u32 texture Texture2D depth Texture2D])
(declare-c load-render-texture
[width i32 height i32] RenderTexture2D
"LoadRenderTexture")
;; raylib 5.5 spells this IsRenderTextureValid; there is no IsRenderTextureReady
;; in this version, the same rename that took IsTextureReady and IsImageReady.
(declare-c render-texture-valid?
[target RenderTexture2D] bool
"IsRenderTextureValid")
(declare-c unload-render-texture
[target RenderTexture2D]
"UnloadRenderTexture")
;; Everything drawn between these two lands in the target instead of the
;; screen, and the target's texture comes out of the GPU upside down — raylib
;; renders it bottom-up — so drawing it back with a NEGATIVE source height is
;; not a flourish, it is the correction. sand.flan does exactly that.
(declare-c begin-texture-mode [target RenderTexture2D] "BeginTextureMode")
(declare-c end-texture-mode [] "EndTextureMode")
;; ── Audio ───────────────────────────────────────────────────────────
;;
;; The device first, and the split that matters for testing runs right
;; through this section: a **Wave** is samples in RAM and needs no device at
;; all, while a **Sound** is a buffer the mixer owns and a **Music** is a
;; decoder feeding one, and both of those are nothing without
;; init-audio-device having succeeded.
;;
;; That makes Wave the audio equivalent of the Image family — raylib
;; *computes* with it, headlessly — and it is the only part of this section
;; the acceptance table asserts. Everything from Sound down is exercised by
;; running sand.flan with a working sound server, and a machine without one
;; gets silence rather than a crash: init-audio-device logs a warning, every
;; load answers a zeroed struct and every play is a no-op.
(declare-c init-audio-device [] "InitAudioDevice")
(declare-c close-audio-device [] "CloseAudioDevice")
(declare-c audio-device-ready? [] bool "IsAudioDeviceReady")
;; 0 to 1, and it is a listener gain applied after every per-sound volume.
(declare-c set-master-volume [volume f32] "SetMasterVolume")
(declare-c get-master-volume [] f32 "GetMasterVolume")
;; A Wave is the CPU side: `data` is frame-count × channels samples of
;; sample-size bits each, and raylib reads every one of the four integers to
;; decide what those bytes mean. `data` is (Ptr u8) rather than a typed
;; pointer because its element type is `sample-size`, which is a run-time
;; number — 8, 16 or 32 bits — and there is no Flan type that says that.
;;
;; The consequence for a caller building one by hand: the bytes are written
;; as bytes, in the host's order. That is the shape the acceptance case uses,
;; and it is deliberate — it means the case says what it means about
;; little-endian 16-bit PCM instead of hiding it behind a cast.
(defstruct Wave [frame-count u32 sample-rate u32 sample-size u32
channels u32 data (Ptr u8)])
(declare-c load-wave [path string] Wave "LoadWave")
;; raylib 5.5 spells this IsWaveValid; IsWaveReady is gone, as everywhere else.
(declare-c wave-valid? [wave Wave] bool "IsWaveValid")
(declare-c unload-wave [wave Wave] "UnloadWave")
;; The extension picks the format, and raylib writes .wav and .qoa. This is
;; external ground truth for the layout: the header it writes carries
;; sample-rate, sample-size and channels, and the payload length carries
;; frame-count, so a permuted defstruct writes a file that reads back
;; differently — the same argument the PNG round trip makes for Image.
(declare-c export-wave [wave Wave path string] bool "ExportWave")
;; Allocates a copy of the buffer; the copy is unloaded on its own.
(declare-c wave-copy [wave Wave] Wave "WaveCopy")
;; In FRAMES, not samples — raylib renamed the parameters for 5.5 without
;; changing the signature, so the name is the only thing that says which. On
;; a mono wave the two readings coincide, which is what the acceptance case
;; uses, so nothing here depends on having guessed right.
(declare-c wave-crop
[wave (Ptr Wave) init-frame i32 final-frame i32]
"WaveCrop")
;; Resamples in place. This is the strongest headless shape available in this
;; section and the same one gen-image-color has: three scalars go in and four
;; fields come out, with frame-count *computed* from the sample-rate ratio, so
;; a permuted layout has nothing to cancel against.
(declare-c wave-format
[wave (Ptr Wave) sample-rate i32 sample-size i32 channels i32]
"WaveFormat")
;; Every sample as a float in [-1, 1], frame-count × channels of them,
;; whatever the wave's own sample-size. That is the one call that reads
;; *through* `data`, so it is what pins the pointer as a pointer rather than
;; as two integers that happen to sit at the end.
(declare-c load-wave-samples [wave Wave] (Ptr f32) "LoadWaveSamples")
(declare-c unload-wave-samples [samples (Ptr f32)] "UnloadWaveSamples")
;; A Sound is an AudioStream plus a frame count. The two leading pointers are
;; miniaudio's and Flan never reads through them — they are (Ptr u8) so the
;; struct is the right size and the three integers land at the right offsets,
;; exactly as Image's `data` is.
(defstruct AudioStream [buffer (Ptr u8) processor (Ptr u8)
sample-rate u32 sample-size u32 channels u32])
(defstruct Sound [stream AudioStream frame-count u32])
(declare-c load-sound [path string] Sound "LoadSound")
;; Note what this does to the frame count: the mixer resamples to the device's
;; own rate, so a sound made from an 8 kHz wave on a 48 kHz device reports six
;; times as many frames. Nothing should read `frame-count` expecting the
;; wave's.
(declare-c load-sound-from-wave [wave Wave] Sound "LoadSoundFromWave")
(declare-c sound-valid? [sound Sound] bool "IsSoundValid")
(declare-c unload-sound [sound Sound] "UnloadSound")
(declare-c play-sound [sound Sound] "PlaySound")
(declare-c stop-sound [sound Sound] "StopSound")
(declare-c pause-sound [sound Sound] "PauseSound")
(declare-c resume-sound [sound Sound] "ResumeSound")
(declare-c sound-playing? [sound Sound] bool "IsSoundPlaying")
;; Volume is a gain from 0, pitch is a rate multiplier where 1 is unchanged,
;; and pan is 0 hard left to 1 hard right with 0.5 centred — raylib's own
;; convention, and the one place in this file where 0 is not the neutral
;; value.
(declare-c set-sound-volume [sound Sound volume f32] "SetSoundVolume")
(declare-c set-sound-pitch [sound Sound pitch f32] "SetSoundPitch")
(declare-c set-sound-pan [sound Sound pan f32] "SetSoundPan")
;; A second voice over the same samples, so one sound can overlap itself. It
;; does NOT own the data, so unloading an alias must not unload the original —
;; which is why raylib has a separate call for it and why this one is bound.
(declare-c load-sound-alias [source Sound] Sound "LoadSoundAlias")
(declare-c unload-sound-alias [alias Sound] "UnloadSoundAlias")
;; Music is streamed rather than resident, which is the whole difference: the
;; buffer is refilled from the decoder and update-music-stream is what does
;; the refilling. Miss it for a frame and the music stops.
(defstruct Music [stream AudioStream frame-count u32 looping bool
ctx-type i32 ctx-data (Ptr u8)])
(declare-c load-music-stream [path string] Music "LoadMusicStream")
(declare-c music-valid? [music Music] bool "IsMusicValid")
(declare-c unload-music-stream [music Music] "UnloadMusicStream")
(declare-c play-music-stream [music Music] "PlayMusicStream")
;; Called once per frame, every frame, for as long as the music is meant to
;; play. This is the one binding in the section whose absence is silent.
(declare-c update-music-stream [music Music] "UpdateMusicStream")
(declare-c stop-music-stream [music Music] "StopMusicStream")
(declare-c pause-music-stream [music Music] "PauseMusicStream")
(declare-c resume-music-stream [music Music] "ResumeMusicStream")
(declare-c music-stream-playing? [music Music] bool "IsMusicStreamPlaying")
(declare-c set-music-volume [music Music volume f32] "SetMusicVolume")
(declare-c set-music-pitch [music Music pitch f32] "SetMusicPitch")
(declare-c set-music-pan [music Music pan f32] "SetMusicPan")
;; Seconds, both of them.
(declare-c seek-music-stream [music Music position f32] "SeekMusicStream")
(declare-c get-music-time-length [music Music] f32 "GetMusicTimeLength")
(declare-c get-music-time-played [music Music] f32 "GetMusicTimePlayed")
;; AudioStream itself — the raw callback-fed stream — is NOT bound. Its point
;; is set-audio-stream-callback, which takes a C function pointer, and a
;; callback is refused by the shim generator by name: `%s is a function type,
;; and a C callback is not implemented`. Binding the rest of the family
;; without it would be binding a stream that can only ever be fed by
;; update-audio-stream from the main thread, which is a worse Sound.
;; ── Fonts ───────────────────────────────────────────────────────────
;;
;; A previous pass refused this whole family by name, and the reason was that
;; a Font drags in two more aggregates and two owned arrays and there was
;; nothing headless to check them against. Both halves of that have changed.
;;
;; The generator takes it: a struct held by value is emitted after everything
;; it contains, a struct held by POINTER is forward-declared, and both the
;; Flan struct and the C typedef come from the same `defstruct`. Font holds a
;; Texture2D by value and points at Rectangle and GlyphInfo; GlyphInfo holds
;; an Image by value. Nothing here needed a generator change.
;;
;; And the test exists. raylib's text measuring is pure CPU arithmetic over
;; every field of a Font — it walks the glyph array looking for a codepoint,
;; reads the advance out of the glyph or the width out of the atlas rectangle,
;; and scales by the base size. The catch was that the calls that MAKE a font
;; all need something a headless run does not have: get-font-default needs
;; init-window, load-font-ex needs a TTF on disk. So the acceptance case does
;; not make one — it *builds* one, field by field, out of Flan arrays, and
;; hands it to raylib to compute with. Scalars in, numbers out, with no input
;; struct raylib produced for a permutation to cancel against.
;;
;; One trap found while doing that, and it is in raylib rather than here:
;; MeasureTextEx returns (0,0) immediately when `texture.id` is 0. A
;; hand-built font therefore has to claim a nonzero texture id even though
;; there is no texture — which is also what makes the case pin where the
;; Texture2D sits inside the Font.
;; `image` is the glyph's own pixels, and raylib owns them; it is here so the
;; four ints in front of it are at the right offsets and so a GlyphInfo is 40
;; bytes rather than 16. offset-x and offset-y shift the glyph when drawn;
;; advance-x is how far the pen moves after it, and when it is 0 raylib falls
;; back to the atlas rectangle's width plus offset-x.
(defstruct GlyphInfo [value i32 offset-x i32 offset-y i32 advance-x i32
image Image])
;; `recs` and `glyphs` are parallel arrays of glyph-count entries each: recs
;; says where the glyph is in the atlas texture, glyphs says what it is. A
;; codepoint raylib cannot find falls back to index 0 rather than reading out
;; of bounds.
(defstruct Font [base-size i32 glyph-count i32 glyph-padding i32
texture Texture2D recs (Ptr Rectangle)
glyphs (Ptr GlyphInfo)])
;; Needs a window: the default font is loaded as part of init-window and
;; LoadFontDefault is not exported, which is the same fact that makes
;; measure-text answer 0 headless.
(declare-c get-font-default [] Font "GetFontDefault")
(declare-c load-font [path string] Font "LoadFont")
;; The codepoint set is a C array plus an int count, so — like
;; collision-point-poly? — the declaration says (Ptr i32) and the Flan wrapper
;; below takes a slice apart. A slice parameter in a declare-c is refused by
;; name, because the C count's own type is not recoverable from [T].
;;
;; raylib's own convention is that a NULL pointer with a count of 0 means the
;; default ASCII set, and the wrapper keeps it — but Flan has no null pointer
;; literal, so the null comes from the one place the language does hand out
;; zeroed bytes: a `defvar` with no initialiser is BSS (plan.org, zero
;; values), and a zeroed (Ptr i32) is exactly a null one. It is never written
;; to and never read through; raylib only ever compares it against NULL.
(defvar default-codepoints (Ptr i32))
(declare-c load-font-ex-raw
[path string font-size i32 codepoints (Ptr i32) count i32] Font
"LoadFontEx")
(defn load-font-ex [path string font-size i32 codepoints [i32]] Font
(if (= (len codepoints) 0)
(load-font-ex-raw path font-size default-codepoints 0)
(load-font-ex-raw path font-size (addr (at codepoints 0)) (len codepoints))))
;; raylib 5.5 spells this IsFontValid. It reads the texture id and both
;; arrays, so a font that loaded but could not upload its atlas — which is
;; every font loaded without a GL context — is NOT valid by this test.
(declare-c font-valid? [font Font] bool "IsFontValid")
(declare-c unload-font [font Font] "UnloadFont")
;; `spacing` is extra pixels between glyphs, added per gap and not per glyph,
;; so a one-character string is unaffected by it. raylib's own DrawTextEx adds
;; it the same way measure-text-ex counts it, which is why the two agree.
(declare-c draw-text-ex [font Font text string position Vector2
font-size f32 spacing f32 tint Color] "DrawTextEx")
;; Pure arithmetic over the font — no GL, no window — and therefore the one
;; thing in this section the acceptance table can assert. See the note above:
;; it refuses to measure anything at all when the font's texture id is 0.
(declare-c measure-text-ex
[font Font text string font-size f32 spacing f32] Vector2
"MeasureTextEx")
;; The index into `recs` and `glyphs`, by linear search over glyph-count. Also
;; pure CPU, and it is what pins glyph-count as the loop bound.
(declare-c get-glyph-index [font Font codepoint i32] i32 "GetGlyphIndex")
(declare-c get-glyph-info [font Font codepoint i32] GlyphInfo "GetGlyphInfo")
(declare-c get-glyph-atlas-rec
[font Font codepoint i32] Rectangle
"GetGlyphAtlasRec")
(declare-c draw-text-codepoint [font Font codepoint i32 position Vector2
font-size f32 tint Color] "DrawTextCodepoint")
;; DrawTextCodepoints and LoadFontData are not bound. The first is the slice
;; problem again and adds nothing draw-text-ex does not already do from a
;; string; the second hands back a raw GlyphInfo array whose length is the
;; caller's to remember and whose lifetime is UnloadFontData's, and Flan has
;; no owning array type to give that to — a (Ptr GlyphInfo) with a separate
;; count is what the language would force, which is the C API with the safety
;; removed rather than a binding.