flan/test/programs/raylib-ffi.flan
Joseph Ferano 856f7dad7b A camera whose layout a round trip could not have pinned
GetScreenToWorld2D and GetWorldToScreen2D are pure arithmetic over every
field of a Camera2D, so they run with no window at all — the best headless
material the package has had. Both directions are asserted as absolute
answers rather than as a round trip, because an inverse cancels a permuted
layout exactly the way store-and-return does.

The rotated case earns its awkwardness: exchanging x and y in Vector2
mirrors every component-wise formula and the answer comes back mirrored
too, so nothing until now could tell the two floats apart. A rotation mixes
them. It reports ok/bad against a tolerance because 90 degrees goes through
sinf and the answer is 27.9999981, and the table compares stdout byte for
byte at -O0 and -O2.
2026-09-11 18:48:57 +07:00

140 lines
7.0 KiB
Plaintext

(import rl "vendor:raylib")
;; The raylib boundary, headless. GetColor, the shapes texture and rectangle
;; intersection all need no window, so the whole crossing — a struct out of C
;; through an out-pointer, a struct into C through a pointer, a keyword
;; resolved against an enum — is exercised without a display.
;;
;; What is being checked is that a struct's FIELDS mean the same thing on both
;; sides. Note what does not check that: handing raylib a struct and reading it
;; back, because storing and returning is symmetric and a permuted layout
;; survives it unchanged. Every case below is asymmetric — raylib does
;; something to the fields that depends on which is which.
(defn show-texture [t rl/Texture2D]
(print-i64 (i64 (.id t))) (newline)
(print-i64 (i64 (.width t))) (newline)
(print-i64 (i64 (.height t))) (newline)
(print-i64 (i64 (.mipmaps t))) (newline)
(print-i64 (i64 (.format t))) (newline))
(defn show-rect [r rl/Rectangle]
(print-f64 (f64 (.x r))) (newline)
(print-f64 (f64 (.y r))) (newline)
(print-f64 (f64 (.width r))) (newline)
(print-f64 (f64 (.height r))) (newline))
;; ── Camera2D ────────────────────────────────────────────────────────
;;
;; The two conversions are pure arithmetic and need no window, which makes
;; them the strongest headless material in the package: each one reads every
;; field of a Camera2D and every field of two Vector2s.
;;
;; They are asserted in both directions separately and never as a round trip.
;; world->screen->world is the store-and-return trap wearing a different hat:
;; the inverse cancels a permuted layout exactly, so it passes for any order.
;;
;; The camera below is chosen so that no field is silently unpinned — offset
;; and target differ, zoom is 2.0 and not the identity 1.0, and every
;; component is a distinct dyadic value that prints exactly.
(defconst cam (rl/Camera2D {:offset (rl/Vector2 {:x 100.0 :y 50.0})
:target (rl/Vector2 {:x 8.0 :y 4.0})
:rotation 0.0
:zoom 2.0}))
;; Absolute values, not a round trip: screen (140,90) is world (28,24) because
;; ((140-100)/2)+8 = 28 and ((90-50)/2)+4 = 24. Swap offset and target in the
;; defstruct and this reads (143,-16); swap rotation and zoom and the zoom
;; becomes 0, the transform is singular, and both come back NaN.
(defn show-v [v rl/Vector2]
(print-f64 (f64 (.x v))) (newline)
(print-f64 (f64 (.y v))) (newline))
;; What no geometric call can pin on its own is Vector2's own two fields:
;; exchange x and y everywhere and every component-wise formula is simply
;; mirrored, so the answer comes back mirrored too and compares equal. A
;; *rotated* camera is the exception — it mixes x into y — so the case below
;; is the one thing in this file that fixes which float is which.
;;
;; It cannot be compared as text: 90 degrees goes through sinf and cosf and
;; the answer is 27.9999981, not 28, and the table compares stdout byte for
;; byte at -O0 and -O2. So the comparison happens here, with a tolerance, and
;; what is printed is the verdict. A wrong-but-close value passing is not a
;; risk worth naming: a permuted layout is out by whole units. Swapping x and
;; y in Vector2 makes this print "rotated bad" — the same camera then reads
;; back as (-12,24).
(defn near? [a f32 b f32] bool
(let [d (- a b)]
(< (if (< d 0.0) (- 0.0 d) d) 0.0001)))
(defn show-near [name string v rl/Vector2 x f32 y f32]
(print-str name)
(print-line (if (and (near? (.x v) x) (near? (.y v) y)) " ok" " bad")))
(defn main [] i32
(rl/set-trace-log-level :warning)
;; A Color is four bytes in RGBA order, so 0x11223344 is 17 34 51 68 and not
;; the little-endian reading of the packed integer. An identity would pass a
;; weaker test than this one.
(let [c (rl/get-color 0x11223344)]
(print-i64 (i64 (.r c))) (newline)
(print-i64 (i64 (.g c))) (newline)
(print-i64 (i64 (.b c))) (newline)
(print-i64 (i64 (.a c))) (newline))
;; Rectangle, pinned completely. The intersection of (0,0,10,4) and
;; (6,1,10,10) is (6,1,4,3) — four different numbers, each derived from a
;; different pair of fields, so swapping any two fields changes the answer.
(show-rect (rl/get-collision-rec (rl/Rectangle {:x 0.0 :y 0.0 :width 10.0 :height 4.0})
(rl/Rectangle {:x 6.0 :y 1.0 :width 10.0 :height 10.0})))
;; Texture2D, as far as a machine with no GPU can go. raylib keeps the
;; shapes texture without touching GL, and substitutes a default when
;; `texture.id`, `source.width` or `source.height` is zero — that guard is
;; the only asymmetry a headless test gets.
(let [rect (rl/Rectangle {:x 3.5 :y 7.25 :width 11.5 :height 13.75})]
;; (A) Valid, five distinct values: they come back, so the struct crosses
;; intact in both directions and raylib stored it rather than defaulting.
(rl/set-shapes-texture (rl/Texture2D {:id 7 :width 13 :height 17 :mipmaps 2 :format 4}) rect)
(show-texture (rl/get-shapes-texture))
(show-rect (rl/get-shapes-texture-rectangle))
;; (B) id zero, everything else positive: the default 1 1 1 1 7 comes
;; back. The 7 is the only distinct field in it, so this pins `format` as
;; the last field, and the substitution happening at all pins `id` as the
;; field the guard reads.
(rl/set-shapes-texture (rl/Texture2D {:id 0 :width 13 :height 17 :mipmaps 2 :format 4}) rect)
(show-texture (rl/get-shapes-texture))
;; (C) width zero, id positive: still stored, because the guard does not
;; look at the texture's width. Without this case, (B) would pass just as
;; well with `id` and `width` swapped — the zero would land in the guarded
;; slot either way.
;;
;; That is the limit of what is checkable here: nothing raylib computes
;; without a GL context reads width, height or mipmaps, so their order
;; among themselves is not pinned by this test. A swap there shows up as a
;; visibly wrong sprite in the interactive run, and nowhere else.
(rl/set-shapes-texture (rl/Texture2D {:id 7 :width 0 :height 17 :mipmaps 2 :format 4}) rect)
(show-texture (rl/get-shapes-texture)))
;; Camera2D, each direction on its own. See the note above show-v for why
;; this is not a round trip.
(show-v (rl/get-screen-to-world-2d (rl/Vector2 {:x 140.0 :y 90.0}) cam))
(show-v (rl/get-world-to-screen-2d (rl/Vector2 {:x 28.0 :y 24.0}) cam))
;; And the rotated camera, which is what pins Vector2's own two fields.
(let [spun (rl/Camera2D {:offset (rl/Vector2 {:x 100.0 :y 50.0})
:target (rl/Vector2 {:x 8.0 :y 4.0})
:rotation 90.0
:zoom 2.0})]
(show-near "rotated screen-to-world"
(rl/get-screen-to-world-2d (rl/Vector2 {:x 140.0 :y 90.0}) spun)
28.0 -16.0)
(show-near "rotated world-to-screen"
(rl/get-world-to-screen-2d (rl/Vector2 {:x 28.0 :y 24.0}) spun)
60.0 90.0))
0)