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.
162 lines
8.5 KiB
Plaintext
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)
|