Merge branch 'worktree-agent-a065a2101ee7d8007' into dev-loop
This commit is contained in:
commit
71877a5baa
2
.gitignore
vendored
2
.gitignore
vendored
@ -43,3 +43,5 @@ old-ocaml/
|
|||||||
/sand
|
/sand
|
||||||
/conditions-play
|
/conditions-play
|
||||||
.claude/
|
.claude/
|
||||||
|
probe
|
||||||
|
probe.c
|
||||||
|
|||||||
299
sand.flan
299
sand.flan
@ -207,6 +207,136 @@
|
|||||||
(rl/draw-texture-ex brush-mirrored (rl/Vector2 {:x 110.0 :y 46.0})
|
(rl/draw-texture-ex brush-mirrored (rl/Vector2 {:x 110.0 :y 46.0})
|
||||||
0.0 2.0 rl/white)))
|
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 ────────────────────────────────────────────────────────
|
;; ── The view ────────────────────────────────────────────────────────
|
||||||
;;
|
;;
|
||||||
;; begin-mode-2d and end-mode-2d were bound along with Camera2D and then
|
;; begin-mode-2d and end-mode-2d were bound along with Camera2D and then
|
||||||
@ -279,8 +409,27 @@
|
|||||||
(defn game-update []
|
(defn game-update []
|
||||||
(when (rl/key-pressed? :r) (clear-grid))
|
(when (rl/key-pressed? :r) (clear-grid))
|
||||||
(move-view)
|
(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-down? :left) (paint))
|
||||||
(when (rl/mouse-button-released? :left) (next-color))
|
(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))
|
(step))
|
||||||
|
|
||||||
|
|
||||||
@ -342,11 +491,20 @@
|
|||||||
;; visible rather than merely different.
|
;; visible rather than merely different.
|
||||||
(defn draw-hud []
|
(defn draw-hud []
|
||||||
(let [title "SAND"
|
(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
|
;; measure-text is what sizes the panel, so the box fits the string
|
||||||
;; rather than a number somebody guessed. Headless it answers 0 for
|
;; rather than a number somebody guessed. Headless it answers 0 for
|
||||||
;; everything, which is why it is not in the acceptance table.
|
;; 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
|
h 72
|
||||||
x 24
|
x 24
|
||||||
y (- (rl/get-screen-height) (+ h 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 panel (f32 0.2) 8 (rl/get-color 0x404060FF))
|
||||||
(rl/draw-rectangle-rounded-lines-ex panel (f32 0.2) 8 (f32 2.0)
|
(rl/draw-rectangle-rounded-lines-ex panel (f32 0.2) 8 (f32 2.0)
|
||||||
(rl/get-color 0x6060A0FF))
|
(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))
|
(rl/draw-text keys x (+ y 40) 20 (rl/get-color 0xA0A0B0FF))
|
||||||
|
|
||||||
;; The palette, along the bottom right. The selected colour is the one
|
;; The palette, along the bottom right. The selected colour is the one
|
||||||
@ -372,6 +533,18 @@
|
|||||||
(when (= i current-color)
|
(when (= i current-color)
|
||||||
(rl/draw-circle-lines cx sh (f32 22.0) rl/white))))
|
(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
|
;; 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,
|
;; formatting yet: the bar's length is the zoom. draw-rectangle-rec,
|
||||||
;; draw-rectangle-v and draw-rectangle-lines are the three remaining
|
;; 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
|
(f32 30.0) (f32 34.0) spin (+ spin (f32 270.0)) 32
|
||||||
rl/white)))))
|
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.
|
;; Everything between these two is in world space and moves with the camera.
|
||||||
(rl/begin-mode-2d view)
|
(rl/begin-mode-2d view)
|
||||||
(draw-grid)
|
(draw-grid)
|
||||||
(draw-world-cursor)
|
(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.
|
;; And everything after it is in screen pixels again.
|
||||||
(draw-brush)
|
(draw-brush)
|
||||||
(draw-hud)
|
(draw-hud)
|
||||||
|
(draw-input-state)
|
||||||
(rl/draw-fps 20 20))
|
(rl/draw-fps 20 20))
|
||||||
|
|
||||||
(defn main []
|
(defn main []
|
||||||
@ -431,6 +706,18 @@
|
|||||||
(load-brush)
|
(load-brush)
|
||||||
(defer (rl/unload-texture brush))
|
(defer (rl/unload-texture brush))
|
||||||
(defer (rl/unload-texture brush-mirrored))
|
(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;
|
;; 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
|
;; (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
|
;; fine — nothing has cells to install into, so a module is refused on the
|
||||||
|
|||||||
202
test/programs/raylib-audio.flan
Normal file
202
test/programs/raylib-audio.flan
Normal 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)
|
||||||
174
test/programs/raylib-font.flan
Normal file
174
test/programs/raylib-font.flan
Normal 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)
|
||||||
@ -353,6 +353,138 @@ let () =
|
|||||||
else
|
else
|
||||||
print_endline "acceptance: skipping the raylib Image case (no libraylib)";
|
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
|
(* Again at -O0. Everything above runs through mem2reg, which launders a
|
||||||
sloppy alloca; -O0 tests the IR actually emitted, so a disagreement
|
sloppy alloca; -O0 tests the IR actually emitted, so a disagreement
|
||||||
between the two points at undefined behaviour rather than a typo. *)
|
between the two points at undefined behaviour rather than a typo. *)
|
||||||
|
|||||||
419
vendor/raylib/raylib.flan
vendored
419
vendor/raylib/raylib.flan
vendored
@ -459,12 +459,12 @@
|
|||||||
;; libraylib.so.550 and is why it is NOT in the acceptance table despite
|
;; libraylib.so.550 and is why it is NOT in the acceptance table despite
|
||||||
;; looking like exactly the kind of call that could be.
|
;; looking like exactly the kind of call that could be.
|
||||||
;;
|
;;
|
||||||
;; Font loading is not bound, deliberately. A Font is baseSize, glyphCount and
|
;; Fonts ARE bound now — see the section at the end of this file. The reason
|
||||||
;; glyphPadding beside a Texture2D, a Rectangle* and a GlyphInfo* — and a
|
;; they were not is worth keeping: a Font is three ints beside a Texture2D, a
|
||||||
;; GlyphInfo embeds an Image. Binding it means binding two more aggregates and
|
;; Rectangle* and a GlyphInfo*, and a GlyphInfo embeds an Image, and that was
|
||||||
;; two owned arrays for something with no headless test at the end of it, so
|
;; "two more aggregates and two owned arrays for something with no headless
|
||||||
;; load-font, load-font-ex, unload-font, get-font-default, draw-text-ex and
|
;; test at the end of it". The generator takes all of that now, and the
|
||||||
;; measure-text-ex are all absent rather than half-done.
|
;; headless test turned out to exist after all.
|
||||||
|
|
||||||
(declare-c draw-text
|
(declare-c draw-text
|
||||||
[text string x i32 y i32 font-size i32 color Color]
|
[text string x i32 y i32 font-size i32 color Color]
|
||||||
@ -483,3 +483,410 @@
|
|||||||
(declare-c get-time [] f64 "GetTime")
|
(declare-c get-time [] f64 "GetTime")
|
||||||
(declare-c get-screen-width [] i32 "GetScreenWidth")
|
(declare-c get-screen-width [] i32 "GetScreenWidth")
|
||||||
(declare-c get-screen-height [] i32 "GetScreenHeight")
|
(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.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user