Say what each case does not catch, not only what it does

Two claims in these comments were stronger than the permutation runs
behind them. The WAV round trip catches sample-size against channels
and leaves frame-count against sample-rate entirely green — the crop
and the reformat are what catch that pair, and a reader who trusted the
round trip would drop exactly the wrong case. The font file listed what
it pins and never said that glyph-padding, offset-y and three of each
atlas rectangle's four fields are read by nothing here at all.

Two more permutations run and recorded while fixing it: GlyphInfo's
image moved to the front, which shifts the four ints 24 bytes and
collapses the glyph search, and Rectangle's x with width, which moves
"measure ABC" to 39 and confirms the advance-0 fallback is the only
thing reading a width out of the recs array.
This commit is contained in:
Joseph Ferano 2026-09-12 03:55:06 +07:00
parent 245ad60fd8
commit a53a3603ee
3 changed files with 59 additions and 20 deletions

View File

@ -23,12 +23,17 @@
;; is the strongest shape a headless FFI test has. ;; is the strongest shape a headless FFI test has.
;; ;;
;; 2. **The file, as external ground truth.** export-wave writes a RIFF ;; 2. **The file, as external ground truth.** export-wave writes a RIFF
;; header carrying sample-rate, sample-size and channels, and a payload ;; header carrying sample-rate, sample-size and channels; load-wave reads
;; whose length says frame-count; load-wave reads that header back. Both ;; it back. Both ends are dr_wav's and agree with each other, not with
;; ends are dr_wav's and agree with each other, not with whatever field ;; whatever field order Flan believes in — the same reason the PNG round
;; order Flan believes in — the same reason the PNG round trip in ;; trip in raylib-image.flan is not the symmetric trap a store-and-return
;; raylib-image.flan is not the symmetric trap a store-and-return check ;; check is.
;; 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 ;; 3. **`data` as a pointer, and the bytes behind it.** load-wave-samples
;; decodes through the buffer. Move `data` up among the integers and ;; decodes through the buffer. Move `data` up among the integers and
@ -181,6 +186,10 @@
;; ends are external to Flan and agree with each other, so this is not the ;; 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, ;; round trip that passes for any field order — it is the PNG argument,
;; for audio. It also crosses a path as ptr+len. ;; 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)) (show-bool "exported" (rl/export-wave src wav-path))
(let [back (rl/load-wave wav-path)] (let [back (rl/load-wave wav-path)]
(show-bool "loaded valid" (rl/wave-valid? back)) (show-bool "loaded valid" (rl/wave-valid? back))

View File

@ -40,6 +40,15 @@
;; texture — and if the texture landed anywhere else in the struct the id ;; texture — and if the texture landed anywhere else in the struct the id
;; would read 0 and every measurement below would collapse to zero. ;; 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 ;; 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 ;; 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 ;; and 9. Everything prints exactly — no trigonometry, so unlike the rotated
@ -52,7 +61,10 @@
;; The glyphs themselves. `image` is raylib's own pixels for the glyph and is ;; 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 ;; 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. ;; 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]) (defvar glyphs [3 rl/GlyphInfo])
(defn build-glyphs [] (defn build-glyphs []
@ -144,7 +156,8 @@
;; width plus the offset: 9 + 3 = 12, for 36. This line and the one above ;; 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 ;; 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 ;; that branch is the only thing in the file that reads Rectangle.width
;; out of the recs array. ;; 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)) (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, ;; The same string at twice the size and a spacing of 3. Scale is 20/10,

View File

@ -371,9 +371,13 @@ let () =
with the frame count it would otherwise equal. with the frame count it would otherwise equal.
export-wave then load-wave is external ground truth, the PNG argument export-wave then load-wave is external ground truth, the PNG argument
transposed: dr_wav writes the header from three fields and the payload transposed: dr_wav writes the header from three fields and reads them
length from the fourth, and reads all four back, agreeing with itself back, agreeing with itself rather than with Flan's field order. Be
rather than with Flan's field order. 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. And the decoded samples are the axis discriminator this section needed.
load-wave-samples answers a (Ptr f32), which Flan cannot index [at] load-wave-samples answers a (Ptr f32), which Flan cannot index [at]
@ -388,9 +392,10 @@ let () =
Verified red by permuting the Wave defstruct three ways: frame-count Verified red by permuting the Wave defstruct three ways: frame-count
with sample-rate (cropped reads "8 4 16 1" and reformatted with sample-rate (cropped reads "8 4 16 1" and reformatted
"16000 16000000 8 2"), sample-size with channels (every frame read "16000 16000000 8 2", while the file round trip stays green see
turns to "no"), and data moved to the front (the run dies after two above), sample-size with channels (every frame read turns to "no" and
lines). *) the loaded line reads "8 8000 16 16"), and data moved to the front
(the run dies after two lines). *)
let raylib_audio_out = let raylib_audio_out =
"valid yes\n\ "valid yes\n\
source 8 8000 16 1\n\ source 8 8000 16 1\n\
@ -443,12 +448,24 @@ let () =
and not per glyph without that line, a wrapper that added it per and not per glyph without that line, a wrapper that added it per
glyph would pass everything else. glyph would pass everything else.
Verified red by four permutations: base-size with glyph-count (the Verified red by six permutations. In Font: base-size with glyph-count
measurements become 80, 120, 163 and 36.6667), offset-x with advance-x (the measurements become 80, 120, 163 and 36.6667), the recs and glyphs
(3, 6, 9, 1), the recs and glyphs pointers (floats in the 1e9 range and pointers (floats in the 1e9 range and a garbage atlas rectangle), and
a garbage atlas rectangle), and moving [texture] to the end of the [texture] moved to the end (the run dies after the first line). In
Font (the run dies after the first line). Two of the four were a crash GlyphInfo: offset-x with advance-x (3, 6, 9, 1), and [image] moved to
rather than a wrong number, which still counts. *) 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 = let raylib_font_out =
"valid yes\n\ "valid yes\n\
index A 0\nindex B 1\nindex C 2\nindex Z 0\n\ index A 0\nindex B 1\nindex C 2\nindex Z 0\n\