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
|
||||
/conditions-play
|
||||
.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})
|
||||
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
|
||||
|
||||
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
|
||||
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. *)
|
||||
|
||||
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
|
||||
;; 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.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user