flan/test/programs/raylib-font.flan
Joseph Ferano 34b6543e58 Assert audio and fonts headlessly, which both were said to be impossible
Audio was written off as needing a device. That is true of Sound and
Music and false of Wave: copy, crop, reformat, export, load and decode
are all CPU work, and wave-format is the same scalars-in/fields-out
shape gen-image-color is, with the frame count computed rather than
handed over. Cropping to a single frame before decoding puts raylib's
byte-offset arithmetic in front of the decoder, which is what tells
sample-size from channels — an axis discriminator, not a mirror.

Fonts were said to have no headless test. They do, once the program
stops asking raylib for a font and builds one out of Flan arrays: text
measuring reads every field and computes. Both cases were verified red
by permuting the defstructs; the permutations are recorded in the
comments so the next reader need not rediscover which ones bite.
2026-09-12 03:45:41 +07:00

162 lines
8.5 KiB
Plaintext

(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.
;;
;; 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.
(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.
(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)