diff --git a/NEXT.md b/NEXT.md index e4388f6..2f278d7 100644 --- a/NEXT.md +++ b/NEXT.md @@ -295,10 +295,36 @@ turned out to check nothing. computes four numbers from four different field pairs, and the point/rect predicates turn the wrong way when width and height are exchanged. +- **Scalars in, fields out is the strongest shape there is**, and the Image + family is where it was finally available. `GenImageColor(4, 2, colour)` is + handed two integers and answers with a struct reading 4, 2, 1, 7 — four + distinct values in four adjacent `i32` slots, with no input struct for a + permutation to cancel against. That pins `Image` completely, including that + `data` is present and first; `Texture2D` could never be pinned that way + because nothing without a GPU reads its width, height or mipmaps at all. +- **A non-square image is an axis discriminator.** `GetImageColor` indexes + `y*width + x`, so on a 4-wide, 2-tall image `(3,0)` exists and its transpose + does not: exchange the wrapper's `x` and `y` and the read goes out of bounds + and answers transparent black. `ImageFlipHorizontal` against + `ImageFlipVertical` says the same thing twice more. +- **A file is external ground truth**, so `ExportImage` then `LoadImage` is not + the symmetric round trip the rest of the package has to avoid — stb's encoder + and decoder agree with each other, not with Flan's field order. Verified + red by the `x`/`y` permutation above. + The rule that falls out: make raylib **compute** something whose answer differs per axis, then verify the test can fail by permuting the fields and watching it go red. A case not verified that way is decoration. +One correction to an assumption that has now cost two lanes a guess: +**`MeasureText` is not headless material.** It measures with the default font, +`LoadFontDefault` is not exported, and nothing but `InitWindow` loads it — so +with no window it answers 0 for every string. Measured against +`libraylib.so.550`, not reasoned about. `GetFrameTime`, `GetTime` and +`GetScreenWidth`/`Height` are all 0 headless for the same kind of reason. All +five are bound, and all five are exercised by running `sand.flan` and looking, +which is the whole of what can be claimed for them. + ## Packages `lib/load.ml` resolves `(import rl "vendor:raylib")` before the checker runs. diff --git a/sand.flan b/sand.flan index eb29636..dd3c080 100644 --- a/sand.flan +++ b/sand.flan @@ -35,6 +35,13 @@ (defvar brush rl/Texture2D) (defvar brush-ok bool) +;; The same sheet a second time, mirrored on the CPU before the GPU ever sees +;; it. That is what the Image family is for, and this is its only call site: +;; nothing headless can make a texture, so load-texture-from-image would +;; otherwise be bound and never called, which is the same as not bound. +(defvar brush-mirrored rl/Texture2D) +(defvar brush-mirrored-ok bool) + ;; A missing file is not a crash and not silence: LoadTexture hands back a ;; texture with an id of 0, every draw with it is a no-op, and the program ;; looks like it has a drawing bug. So it is asked and said once, here. @@ -42,7 +49,16 @@ (set brush (rl/load-texture "brush.png")) (set brush-ok (rl/texture-valid? brush)) (unless brush-ok - (print-line "sand: cannot load brush.png — drawing the cursor is off"))) + (print-line "sand: cannot load brush.png — drawing the cursor is off")) + ;; The other route to a texture: the file into RAM, changed there, and only + ;; then uploaded. An Image that failed to load has a null buffer and + ;; unloading it is still safe, so there is one unload and not two. + (let [sheet (rl/load-image "brush.png")] + (when (rl/image-valid? sheet) + (rl/image-flip-horizontal (addr sheet)) + (set brush-mirrored (rl/load-texture-from-image sheet))) + (rl/unload-image sheet)) + (set brush-mirrored-ok (rl/texture-valid? brush-mirrored))) ;; Four draws, one per shape the call comes in, because none of them can be in ;; the acceptance table. The cursor picks one frame out of the sheet and so @@ -60,11 +76,69 @@ (rl/draw-texture brush 20 50 rl/white) (rl/draw-texture-v brush (rl/Vector2 {:x 44.0 :y 50.0}) (rl/get-color (nth sim/colors sim/current-color))) - (rl/draw-texture-ex brush (rl/Vector2 {:x 72.0 :y 46.0}) 0.0 2.0 rl/white)))) + (rl/draw-texture-ex brush (rl/Vector2 {:x 72.0 :y 46.0}) 0.0 2.0 rl/white))) + ;; The mirrored one beside them, scaled up so the flip is visible rather + ;; than eight pixels wide. If the two badges look the same, either the flip + ;; did nothing or the upload took the unedited buffer. + (when brush-mirrored-ok + (rl/draw-texture-ex brush-mirrored (rl/Vector2 {:x 110.0 :y 46.0}) + 0.0 2.0 rl/white))) + +;; ── The view ──────────────────────────────────────────────────────── +;; +;; begin-mode-2d and end-mode-2d were bound along with Camera2D and then +;; called by nothing at all, which is the same as not having bound them. They +;; are load-bearing here now: the grid is drawn through this camera, the arrow +;; keys pan it, comma and period zoom, and `paint` has to undo the transform +;; with get-screen-to-world-2d or the sand lands somewhere other than the +;; cursor. That last part is what makes this a check rather than decoration — +;; a camera plumbed in wrongly shows up as grains appearing in the wrong +;; place, at once, while zoomed. +(defvar view rl/Camera2D) + +;; A fresh (Camera2D {}) has a zoom of 0, which is singular: both conversions +;; hand back NaN and nothing draws. 1.0 is the identity. +(defn reset-view [] + (set view (rl/Camera2D {:offset (rl/Vector2 {:x 0.0 :y 0.0}) + :target (rl/Vector2 {:x 0.0 :y 0.0}) + :rotation 0.0 + :zoom 1.0}))) + +(defn set-view [target-x f32 target-y f32 zoom f32] + (set view (rl/Camera2D {:offset (.offset view) + :target (rl/Vector2 {:x target-x :y target-y}) + :rotation (.rotation view) + :zoom zoom}))) + +;; Panning is world units per second and zooming is a factor per second, so +;; neither changes with the frame rate. That is the whole of what +;; get-frame-time is for, and a loop that assumed it hit its target fps would +;; be a loop that moves differently on a slower machine. +(defn move-view [] + (let [dt (rl/get-frame-time) + pan (* (f32 600.0) dt) + tx (.x (.target view)) + ty (.y (.target view)) + zoom (.zoom view)] + (when (rl/key-down? :left) (set tx (- tx pan))) + (when (rl/key-down? :right) (set tx (+ tx pan))) + (when (rl/key-down? :up) (set ty (- ty pan))) + (when (rl/key-down? :down) (set ty (+ ty pan))) + (when (rl/key-down? :comma) (set zoom (- zoom (* zoom dt)))) + (when (rl/key-down? :period) (set zoom (+ zoom (* zoom dt)))) + ;; Clamped away from 0 for the reason above, and away from the far end + ;; because a cell is 5 pixels and there is no point past a screenful of + ;; one of them. + (set-view tx ty (min (f32 8.0) (max (f32 0.125) zoom))) + (when (rl/key-pressed? :zero) (reset-view)))) ;; Locals are assignable places (spec-memory.md); parameters are not. +;; +;; The mouse is in screen pixels and the grid is in world cells, and with a +;; camera in the way those stopped being the same thing — so this is the one +;; place get-screen-to-world-2d is not a test case but a requirement. (defn paint [] - (let [m (rl/get-mouse-position) + (let [m (rl/get-screen-to-world-2d (rl/get-mouse-position) view) row (/ (i32 (.y m)) sim/cell-size) col (/ (i32 (.x m)) sim/cell-size)] (sim/paint-at row col))) @@ -81,12 +155,12 @@ ;; reload rejects it. See plan.org "What redefinition cannot do". (defn game-update [] (when (rl/key-pressed? :r) (sim/clear-grid)) + (move-view) (when (rl/mouse-button-down? :left) (paint)) (when (rl/mouse-button-released? :left) (sim/next-color)) (sim/step)) -(defn game-draw [] - (rl/clear-background rl/black) +(defn draw-grid [] (dotimes [row sim/rows] (dotimes [col sim/cols] (let [c (at sim/grid row col)] @@ -94,8 +168,131 @@ (rl/draw-rectangle (i32 (* col sim/cell-size)) (i32 (* row sim/cell-size)) sim/cell-size sim/cell-size - (rl/get-color c)))))) + (rl/get-color c))))))) + +;; Drawn inside the camera, in world units, so every one of these moves and +;; scales with the grid. That is the point: a shape binding that is subtly +;; wrong is easiest to see when it is supposed to sit exactly on the cursor +;; and does not. +(defn draw-world-cursor [] + (let [p (rl/get-screen-to-world-2d (rl/get-mouse-position) view) + tint (rl/get-color (nth sim/colors sim/current-color)) + x (.x p) + y (.y p) + r (f32 (* sim/brush-size sim/cell-size))] + ;; The brush's actual reach, as a ring, plus a thinner circle outside it. + (rl/draw-ring p (* r (f32 0.9)) r (f32 0.0) (f32 360.0) 48 tint) + (rl/draw-circle-lines-v p (+ r (f32 6.0)) tint) + ;; A crosshair: two thin lines and one thick one, which is three separate + ;; raylib calls with three different shapes of argument. + (rl/draw-line-v (rl/Vector2 {:x (- x r) :y y}) + (rl/Vector2 {:x (+ x r) :y y}) tint) + (rl/draw-line-v (rl/Vector2 {:x x :y (- y r)}) + (rl/Vector2 {:x x :y (+ y r)}) tint) + (rl/draw-line-ex (rl/Vector2 {:x (- x (f32 4.0)) :y y}) + (rl/Vector2 {:x (+ x (f32 4.0)) :y y}) (f32 3.0) rl/white) + ;; The exact world point: a filled dot, and one pixel of white on top of + ;; it. Both are the Vector2 forms, so they land where the ring's centre + ;; is and not somewhere an integer cast put them. + (rl/draw-circle-v p (f32 5.0) tint) + (rl/draw-pixel-v p rl/white) + ;; A pointer above the cursor. Counter-clockwise, because raylib culls the + ;; other winding and draws nothing — which looks exactly like a broken + ;; binding and is why the outline is drawn over it as a control. + (let [tip (rl/Vector2 {:x x :y (- y (+ r (f32 26.0)))}) + left (rl/Vector2 {:x (- x (f32 12.0)) :y (- y (+ r (f32 6.0)))}) + rght (rl/Vector2 {:x (+ x (f32 12.0)) :y (- y (+ r (f32 6.0)))})] + (rl/draw-triangle tip left rght tint) + (rl/draw-triangle-lines tip left rght rl/white)) + ;; And the world's own edge, so panning has something to pan against. + (rl/draw-rectangle-lines-ex + (rl/Rectangle {:x 0.0 :y 0.0 + :width (f32 sim/screen-width) + :height (f32 sim/screen-height)}) + (f32 2.0) (rl/get-color 0x303030FF)))) + +;; Drawn outside the camera, in screen pixels, so it stays put while the world +;; moves underneath it. Nothing here can be asserted — it needs a GL context — +;; so it is built to be *looked* at: every shape binding appears once, and each +;; one is asymmetric enough that a wrapper with its arguments crossed is +;; visible rather than merely different. +(defn draw-hud [] + (let [title "SAND" + keys "arrows pan , . zoom 0 reset r clear" + ;; 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)) + h 72 + x 24 + y (- (rl/get-screen-height) (+ h 24)) + panel (rl/Rectangle {:x (f32 (- x 12)) :y (f32 (- y 12)) + :width (f32 (+ w 24)) :height (f32 (+ h 24))})] + (rl/draw-rectangle-rounded panel (f32 0.2) 8 (rl/get-color 0x101018E0)) + ;; Both outline forms, one inside the other: the plain one has no + ;; thickness in raylib 5.5 and the -ex one is where thickness went. + (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) + (rl/draw-text keys x (+ y 40) 20 (rl/get-color 0xA0A0B0FF)) + + ;; The palette, along the bottom right. The selected colour is the one + ;; with a ring round it, so `current-color` is readable off the screen. + (let [sw (- (rl/get-screen-width) 40) + sh (- (rl/get-screen-height) 40)] + (dotimes [i (len sim/colors)] + (let [cx (- sw (* (- (len sim/colors) (+ i 1)) 46)) + c (rl/get-color (nth sim/colors i))] + (rl/draw-circle cx sh (f32 16.0) c) + (when (= i sim/current-color) + (rl/draw-circle-lines cx sh (f32 22.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 + ;; rectangle shapes, so all three are here rather than invented + ;; elsewhere. + (let [bx (f32 (- sw 220)) + by (f32 (- sh 60)) + fill (* (f32 200.0) (min (f32 1.0) (/ (.zoom view) (f32 8.0))))] + (rl/draw-rectangle-rec (rl/Rectangle {:x bx :y by :width (f32 200.0) + :height (f32 10.0)}) + (rl/get-color 0x202028FF)) + (rl/draw-rectangle-v (rl/Vector2 {:x bx :y by}) + (rl/Vector2 {:x fill :y (f32 10.0)}) + (rl/get-color 0x6060A0FF)) + (rl/draw-rectangle-lines (- sw 220) (- sh 60) 200 10 + (rl/get-color 0x8080C0FF)) + ;; The tick at the identity zoom, and a single pixel marking its left + ;; end — the integer forms of the line and pixel calls. + (rl/draw-line (+ (- sw 220) 25) (- sh 66) (+ (- sw 220) 25) (- sh 46) + rl/white) + (rl/draw-pixel (- sw 220) (- sh 66) rl/white)) + + ;; An ellipse, deliberately wider than it is tall so that exchanging its + ;; two radii would be obvious, and a ring whose sweep is driven by + ;; get-time so that something on screen proves the clock is running. + (let [ex (- sw 320) + ey (- sh 20) + spin (f32 (* 60.0 (rl/get-time)))] + (rl/draw-ellipse ex ey (f32 26.0) (f32 12.0) (rl/get-color 0x303040FF)) + (rl/draw-ellipse-lines ex ey (f32 26.0) (f32 12.0) + (rl/get-color 0x8080C0FF)) + (rl/draw-ring-lines (rl/Vector2 {:x (f32 ex) :y (f32 ey)}) + (f32 30.0) (f32 34.0) spin (+ spin (f32 270.0)) 32 + rl/white))))) + +(defn game-draw [] + (rl/clear-background rl/black) + ;; 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) + ;; And everything after it is in screen pixels again. (draw-brush) + (draw-hud) (rl/draw-fps 20 20)) (defn main [] @@ -103,10 +300,13 @@ (rl/init-window sim/screen-width sim/screen-height "SAND") (defer (rl/close-window)) (rl/set-target-fps 120) + ;; Before anything draws: a zero zoom is singular and nothing would appear. + (reset-view) ;; After the window, never before: LoadTexture uploads to the GPU and there ;; is no GPU to upload to until InitWindow has made a context. (load-brush) (defer (rl/unload-texture brush)) + (defer (rl/unload-texture brush-mirrored)) ;; 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 diff --git a/test/programs/raylib-image.flan b/test/programs/raylib-image.flan new file mode 100644 index 0000000..7bfe9fd --- /dev/null +++ b/test/programs/raylib-image.flan @@ -0,0 +1,160 @@ +(import rl "vendor:raylib") + +;; raylib's Image family, headless. An Image is pixels in RAM: no window, no +;; GL context, and — unlike every other struct in the package — raylib will +;; *compute* with it. That is what makes this the strongest FFI case in the +;; project rather than another link check. +;; +;; Two things are being pinned here and they are different things: +;; +;; 1. **Flan's Image layout.** gen-image-color is handed two scalars and +;; answers with a struct whose four ints are 4, 2, 1 and 7 — all +;; distinct, so exchanging any two of width/height/mipmaps/format shows +;; up immediately. Scalars in, fields out: a permuted layout cannot +;; cancel itself the way store-and-return does, which is why this pins +;; more than the shapes texture ever could. It also pins `data`: drop it +;; from the defstruct and `width` reads the low half of raylib's pointer. +;; +;; 2. **The shim's argument order and raylib's own index arithmetic.** +;; get-image-color reads pixel y*width + x out of the buffer, so on a +;; 4-wide, 2-tall image the pixel at (3,0) exists and (0,3) does not. +;; Exchange x and y in the wrapper and the answer is transparent black. +;; This is the axis discriminator that no axis-aligned geometry can be: +;; the image is not square, so a reflection has nowhere to hide. +;; +;; The image is deliberately 4 x 2 throughout. A square one would let a +;; transposed read pass, and that is exactly the trap the collision cases fell +;; into. + +(defconst bg (rl/Color {:r 10 :g 20 :b 30 :a 255})) +(defconst mark-a (rl/Color {:r 200 :g 0 :b 0 :a 255})) +(defconst mark-b (rl/Color {:r 0 :g 200 :b 0 :a 255})) + +;; Where the export goes and comes back from. The two optimisation levels +;; write identical bytes, so sharing one path between runs is harmless. +(defconst png-path "/tmp/flan-raylib-image.png") + +(defn show-image [name string i rl/Image] + (print-str name) + (print-str " ") (print-i64 (i64 (.width i))) + (print-str " ") (print-i64 (i64 (.height i))) + (print-str " ") (print-i64 (i64 (.mipmaps i))) + (print-str " ") (print-i64 (i64 (.format i))) + (newline)) + +(defn show-color [name string c rl/Color] + (print-str name) + (print-str " ") (print-i64 (i64 (.r c))) + (print-str " ") (print-i64 (i64 (.g c))) + (print-str " ") (print-i64 (i64 (.b c))) + (print-str " ") (print-i64 (i64 (.a c))) + (newline)) + +(defn show-bool [name string b bool] + (print-str name) (print-str " ") + (print-line (if b "yes" "no"))) + +;; Every pixel read names its coordinates in the label, so a failure says +;; which one moved rather than only that something did. +(defn show-pixel [name string i rl/Image x i32 y i32] + (show-color name (rl/get-image-color i x y))) + +(defn main [] i32 + (rl/set-trace-log-level :warning) + + ;; ── The layout, from a struct raylib built ────────────────────────── + ;; + ;; 4 wide, 2 tall, 1 mipmap level, format 7 (uncompressed R8G8B8A8). Four + ;; different numbers in four adjacent i32 slots is the case Texture2D never + ;; got: there, nothing without a GPU read width, height or mipmaps at all. + (let [img (rl/gen-image-color 4 2 bg)] + (show-image "generated" img) + + ;; ── The axes, from raylib's own indexing ─────────────────────────── + ;; + ;; (3,0) is the last pixel of the first row and (0,1) the first of the + ;; second. On a 4 x 2 image neither coordinate pair is valid with x and y + ;; exchanged, so a wrapper with its arguments the wrong way round reads out + ;; of bounds and answers 0 0 0 0. + (rl/image-draw-pixel (addr img) 3 0 mark-a) + (rl/image-draw-pixel (addr img) 0 1 mark-b) + (show-pixel "at 3,0" img 3 0) + (show-pixel "at 0,1" img 0 1) + ;; And the two corners nothing was written to, because a get that ignored + ;; its coordinates and returned the last-written colour would pass above. + (show-pixel "at 0,0" img 0 0) + (show-pixel "at 3,1" img 3 1) + + ;; ── Flip horizontal, then vertical ───────────────────────────────── + ;; + ;; Horizontal moves x and leaves y: (3,0) becomes (0,0) and (0,1) becomes + ;; (3,1). Bind the two flips to each other's wrappers and this reads + ;; unchanged at (3,0) instead, because a vertical flip of a 2-row image + ;; would put the marks on the other rows entirely. + (rl/image-flip-horizontal (addr img)) + (show-pixel "flipped-h at 0,0" img 0 0) + (show-pixel "flipped-h at 3,1" img 3 1) + (show-pixel "flipped-h at 3,0" img 3 0) + + ;; Vertical moves y and leaves x, so the two marks swap rows: (0,0) goes to + ;; (0,1) and (3,1) to (3,0). + (rl/image-flip-vertical (addr img)) + (show-pixel "flipped-v at 0,1" img 0 1) + (show-pixel "flipped-v at 3,0" img 3 0) + (show-pixel "flipped-v at 0,0" img 0 0) + + ;; ── Out to a PNG and back ────────────────────────────────────────── + ;; + ;; The file is external ground truth, which is what stops this being the + ;; symmetric round trip the rest of the package has to avoid: the encoder + ;; and the decoder are stb's, they agree with each other and not with + ;; whatever field order Flan believes in. A path crosses as ptr+len and + ;; the shim NUL-terminates a copy, so this exercises the string half of + ;; the boundary too. + (show-bool "exported" (rl/export-image img png-path)) + (let [back (rl/load-image png-path)] + (show-bool "loaded valid" (rl/image-valid? back)) + (show-image "loaded" back) + (show-pixel "loaded at 0,1" back 0 1) + (show-pixel "loaded at 3,0" back 3 0) + (show-pixel "loaded at 0,0" back 0 0) + + ;; ── Nearest-neighbour resize ───────────────────────────────────── + ;; + ;; 4 x 2 to 8 x 2 doubles each pixel across, and leaves the rows alone. + ;; The colours survive exactly, which bicubic's would not, so this is + ;; the resize that can be asserted on content: the mark at (0,1) spreads + ;; to (0,1) and (1,1), the one at (3,0) to (6,0) and (7,0), and (2,1) is + ;; background between them. New width and height are 8 and 2 — distinct, + ;; so a wrapper that swapped them answers 2 and 8. + (rl/image-resize-nn (addr back) 8 2) + (show-image "resized-nn" back) + (show-pixel "nn at 0,1" back 0 1) + (show-pixel "nn at 1,1" back 1 1) + (show-pixel "nn at 6,0" back 6 0) + (show-pixel "nn at 7,0" back 7 0) + (show-pixel "nn at 2,1" back 2 1) + + ;; Bicubic. Its pixels are interpolated and not worth asserting, but the + ;; dimensions are, and 2 x 6 is asymmetric in both directions at once. + (rl/image-resize (addr back) 2 6) + (show-image "resized" back) + (rl/unload-image back)) + (rl/unload-image img)) + + ;; ── Crop, which is where Rectangle meets Image ────────────────────── + ;; + ;; A 6 x 3 image with one mark at (5,0), cropped to (x 4, y 0, w 2, h 1). + ;; The result is 2 x 1 and the mark has moved to (1,0) — it survived, so the + ;; crop's x really is 4 and not its width, and the region really is two wide + ;; and one tall and not the other way about. Exchange width and height in + ;; the Rectangle and the result is 1 x 2 with the mark gone. + (let [img (rl/gen-image-color 6 3 bg)] + (rl/image-draw-pixel (addr img) 5 0 mark-a) + (rl/image-crop (addr img) (rl/Rectangle {:x 4.0 :y 0.0 :width 2.0 :height 1.0})) + (show-image "cropped" img) + (show-pixel "cropped at 1,0" img 1 0) + (show-pixel "cropped at 0,0" img 0 0) + (rl/unload-image img)) + + 0) diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index 52eb110..068ff1a 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -282,6 +282,73 @@ let () = else print_endline "acceptance: skipping the raylib FFI case (no libraylib)"; + (* raylib's Image family, headless, and the strongest FFI case here: an + Image is pixels in RAM, so raylib *computes* with it rather than + storing and returning it. + + Two separate things are pinned. gen-image-color is handed two scalars + and answers with a struct reading 4, 2, 1, 7 — four distinct values in + four adjacent i32 slots, so exchanging any two of width, height, + mipmaps and format is visible, and dropping `data` makes width the low + half of raylib's pointer. Scalars in and fields out is what makes that + work: a permuted layout has nothing to cancel against, unlike the + shapes texture, where nothing without a GPU read width, height or + mipmaps at all. + + The other is the axis, which the collision cases could not get. raylib + indexes a pixel as y*width + x, and the image is 4 wide by 2 tall, so + (3,0) exists and its transpose does not — exchange x and y in the shim + and the read is out of bounds and answers transparent black. The + horizontal and vertical flips are the same argument twice more: on two + rows, one of them moves a mark that the other leaves alone. + + The PNG round trip is not the symmetric trap either: stb's encoder and + decoder are external ground truth and agree with each other rather than + with whatever field order Flan believes in. It also crosses a path as + ptr+len. /tmp is written to, and both optimisation levels write the + same bytes, so the shared name is harmless. + + Trace logging stays at :warning and no read here is out of bounds, so + a warning appearing in this output is a real failure — [run] folds + stderr in. *) + let raylib_image_out = + "generated 4 2 1 7\n\ + at 3,0 200 0 0 255\n\ + at 0,1 0 200 0 255\n\ + at 0,0 10 20 30 255\n\ + at 3,1 10 20 30 255\n\ + flipped-h at 0,0 200 0 0 255\n\ + flipped-h at 3,1 0 200 0 255\n\ + flipped-h at 3,0 10 20 30 255\n\ + flipped-v at 0,1 200 0 0 255\n\ + flipped-v at 3,0 0 200 0 255\n\ + flipped-v at 0,0 10 20 30 255\n\ + exported yes\n\ + loaded valid yes\n\ + loaded 4 2 1 7\n\ + loaded at 0,1 200 0 0 255\n\ + loaded at 3,0 0 200 0 255\n\ + loaded at 0,0 10 20 30 255\n\ + resized-nn 8 2 1 7\n\ + nn at 0,1 200 0 0 255\n\ + nn at 1,1 200 0 0 255\n\ + nn at 6,0 0 200 0 255\n\ + nn at 7,0 0 200 0 255\n\ + nn at 2,1 10 20 30 255\n\ + resized 2 6 1 7\n\ + cropped 2 1 1 7\n\ + cropped at 1,0 200 0 0 255\n\ + cropped at 0,0 10 20 30 255\n" + in + if Sys.command "ldconfig -p 2>/dev/null | grep -q libraylib" = 0 then begin + outputs "raylib images, headless" "programs/raylib-image.flan" + raylib_image_out; + outputs ~opt:"-O0" "raylib images, headless, -O0" "programs/raylib-image.flan" + raylib_image_out + end + else + print_endline "acceptance: skipping the raylib Image 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. *) diff --git a/vendor/raylib/raylib.flan b/vendor/raylib/raylib.flan index 6e06e40..5b0bb55 100644 --- a/vendor/raylib/raylib.flan +++ b/vendor/raylib/raylib.flan @@ -392,3 +392,349 @@ p position c tint] (draw-texture-rec-raw (addr t) (addr s) (addr p) (addr c)))) + +;; ── Images ────────────────────────────────────────────────────────── +;; +;; An Image is pixels in RAM. Nothing here touches the GPU, which makes it the +;; one corner of the 2D surface a headless test can assert properly — raylib +;; *computes* with these, and a wrong answer is a wrong number rather than the +;; struct handed back unchanged. +;; +;; `data` is raylib's buffer and Flan never reads through it; it is here so +;; the struct is the right size and the four ints that follow are at the right +;; offsets. `format` is a PixelFormat code — GenImageColor makes 7, which is +;; uncompressed R8G8B8A8, one byte per channel. +;; +;; The split between by-value and by-pointer here is raylib's own and worth +;; keeping: a call that *mutates* the image takes (Ptr Image) at the Flan +;; level too, so a caller can see which ones change what they are given. +(defstruct Image [data (Ptr u8) width i32 height i32 mipmaps i32 format i32]) + +(declare load-image-raw [path string out (Ptr Image)] "flan_rl_load_image") + +(defn load-image [path string] Image + (let [i (Image {})] + (load-image-raw path (addr i)) + i)) + +;; raylib 5.5 spells this IsImageValid. IsImageReady, which older code calls, +;; does not exist here — the same rename that took IsTextureReady. +(declare image-valid?-raw [image (Ptr Image)] bool "flan_rl_is_image_valid") + +(defn image-valid? [image Image] bool + (let [i image] + (image-valid?-raw (addr i)))) + +;; By value, as raylib has it. The caller's copy is dangling afterwards — +;; `data` pointed at the buffer this just freed — so an Image is used or +;; unloaded, never both. +(declare unload-image-raw [image (Ptr Image)] "flan_rl_unload_image") + +(defn unload-image [image Image] + (let [i image] + (unload-image-raw (addr i)))) + +;; The format is taken from the path's extension, so ".png" writes a PNG. +;; False means it could not be written. +(declare export-image-raw [image (Ptr Image) path string] bool + "flan_rl_export_image") + +(defn export-image [image Image path string] bool + (let [i image] + (export-image-raw (addr i) path))) + +(declare gen-image-color-raw + [width i32 height i32 color (Ptr Color) out (Ptr Image)] + "flan_rl_gen_image_color") + +(defn gen-image-color [width i32 height i32 color Color] Image + (let [c color + i (Image {})] + (gen-image-color-raw width height (addr c) (addr i)) + i)) + +;; Bicubic, so the pixels that come out are interpolated and only the new +;; width and height are exactly predictable. image-resize-nn is the +;; nearest-neighbour one, and it is the one to reach for when the colours +;; have to survive. +(declare image-resize [image (Ptr Image) width i32 height i32] + "flan_rl_image_resize") +(declare image-resize-nn [image (Ptr Image) width i32 height i32] + "flan_rl_image_resize_nn") + +(declare image-crop-raw [image (Ptr Image) crop (Ptr Rectangle)] + "flan_rl_image_crop") + +(defn image-crop [image (Ptr Image) crop Rectangle] + (let [r crop] + (image-crop-raw image (addr r)))) + +(declare image-flip-horizontal [image (Ptr Image)] + "flan_rl_image_flip_horizontal") +(declare image-flip-vertical [image (Ptr Image)] + "flan_rl_image_flip_vertical") + +(declare image-draw-pixel-raw + [image (Ptr Image) x i32 y i32 color (Ptr Color)] + "flan_rl_image_draw_pixel") + +(defn image-draw-pixel [image (Ptr Image) x i32 y i32 color Color] + (let [c color] + (image-draw-pixel-raw image x y (addr c)))) + +;; Out of bounds is not an error: raylib logs a warning and hands back a +;; transparent black, so a caller that is off by one gets zeroes rather than +;; somebody else's memory. +(declare get-image-color-raw + [image (Ptr Image) x i32 y i32 out (Ptr Color)] + "flan_rl_get_image_color") + +(defn get-image-color [image Image x i32 y i32] Color + (let [i image + c (Color {})] + (get-image-color-raw (addr i) x y (addr c)) + c)) + +;; The one call in this section that does need a GL context — it uploads. An +;; image loaded and edited on the CPU becomes something draw-texture can use. +(declare load-texture-from-image-raw [image (Ptr Image) out (Ptr Texture2D)] + "flan_rl_load_texture_from_image") + +(defn load-texture-from-image [image Image] Texture2D + (let [i image + t (Texture2D {})] + (load-texture-from-image-raw (addr i) (addr t)) + t)) + +;; ── Shapes ────────────────────────────────────────────────────────── +;; +;; Immediate-mode drawing: each of these needs a GL context, so a window has +;; to be open and none of them can be in the acceptance table. They are +;; exercised by running sand.flan and looking at it, which is the honest +;; description — "it links" is not "it draws the right thing". +;; +;; raylib's own naming is kept: a plain name fills, `-lines` outlines, and a +;; `-v` suffix takes Vector2s where the plain form takes integers. +;; +;; The one signature worth calling out is draw-rectangle-rounded-lines, which +;; in raylib 5.5 has NO thickness — it moved to the `-ex` form. The 5.1 header +;; still shows the five-argument version, and getting it wrong links cleanly +;; and draws nonsense, so this was read off the library with nm rather than +;; remembered. + +(declare draw-pixel-raw [x i32 y i32 color (Ptr Color)] "flan_rl_draw_pixel") + +(defn draw-pixel [x i32 y i32 color Color] + (let [c color] (draw-pixel-raw x y (addr c)))) + +(declare draw-pixel-v-raw [position (Ptr Vector2) color (Ptr Color)] + "flan_rl_draw_pixel_v") + +(defn draw-pixel-v [position Vector2 color Color] + (let [p position c color] (draw-pixel-v-raw (addr p) (addr c)))) + +(declare draw-line-raw [x1 i32 y1 i32 x2 i32 y2 i32 color (Ptr Color)] + "flan_rl_draw_line") + +(defn draw-line [x1 i32 y1 i32 x2 i32 y2 i32 color Color] + (let [c color] (draw-line-raw x1 y1 x2 y2 (addr c)))) + +(declare draw-line-v-raw + [start (Ptr Vector2) end (Ptr Vector2) color (Ptr Color)] + "flan_rl_draw_line_v") + +(defn draw-line-v [start Vector2 end Vector2 color Color] + (let [a start b end c color] (draw-line-v-raw (addr a) (addr b) (addr c)))) + +;; The thick one is built from triangles rather than GL lines, which is why it +;; is a separate call and not a parameter on the one above. +(declare draw-line-ex-raw + [start (Ptr Vector2) end (Ptr Vector2) thick f32 color (Ptr Color)] + "flan_rl_draw_line_ex") + +(defn draw-line-ex [start Vector2 end Vector2 thick f32 color Color] + (let [a start b end c color] + (draw-line-ex-raw (addr a) (addr b) thick (addr c)))) + +(declare draw-circle-raw [x i32 y i32 radius f32 color (Ptr Color)] + "flan_rl_draw_circle") + +(defn draw-circle [x i32 y i32 radius f32 color Color] + (let [c color] (draw-circle-raw x y radius (addr c)))) + +(declare draw-circle-v-raw + [center (Ptr Vector2) radius f32 color (Ptr Color)] "flan_rl_draw_circle_v") + +(defn draw-circle-v [center Vector2 radius f32 color Color] + (let [p center c color] (draw-circle-v-raw (addr p) radius (addr c)))) + +(declare draw-circle-lines-raw [x i32 y i32 radius f32 color (Ptr Color)] + "flan_rl_draw_circle_lines") + +(defn draw-circle-lines [x i32 y i32 radius f32 color Color] + (let [c color] (draw-circle-lines-raw x y radius (addr c)))) + +(declare draw-circle-lines-v-raw + [center (Ptr Vector2) radius f32 color (Ptr Color)] + "flan_rl_draw_circle_lines_v") + +(defn draw-circle-lines-v [center Vector2 radius f32 color Color] + (let [p center c color] (draw-circle-lines-v-raw (addr p) radius (addr c)))) + +;; Two radii, horizontal then vertical. Equal radii is a circle, so a wrapper +;; that exchanged them would be invisible unless they differ — which is why +;; sand.flan's ellipse is deliberately wider than it is tall. +(declare draw-ellipse-raw + [x i32 y i32 radius-h f32 radius-v f32 color (Ptr Color)] + "flan_rl_draw_ellipse") + +(defn draw-ellipse [x i32 y i32 radius-h f32 radius-v f32 color Color] + (let [c color] (draw-ellipse-raw x y radius-h radius-v (addr c)))) + +(declare draw-ellipse-lines-raw + [x i32 y i32 radius-h f32 radius-v f32 color (Ptr Color)] + "flan_rl_draw_ellipse_lines") + +(defn draw-ellipse-lines [x i32 y i32 radius-h f32 radius-v f32 color Color] + (let [c color] (draw-ellipse-lines-raw x y radius-h radius-v (addr c)))) + +;; Angles are degrees, clockwise from the +x axis, and `segments` is how many +;; straight pieces the arc is made of — 0 lets raylib pick from the radius. +(declare draw-ring-raw + [center (Ptr Vector2) inner f32 outer f32 start f32 end f32 + segments i32 color (Ptr Color)] + "flan_rl_draw_ring") + +(defn draw-ring [center Vector2 inner f32 outer f32 start f32 end f32 + segments i32 color Color] + (let [p center c color] + (draw-ring-raw (addr p) inner outer start end segments (addr c)))) + +(declare draw-ring-lines-raw + [center (Ptr Vector2) inner f32 outer f32 start f32 end f32 + segments i32 color (Ptr Color)] + "flan_rl_draw_ring_lines") + +(defn draw-ring-lines [center Vector2 inner f32 outer f32 start f32 end f32 + segments i32 color Color] + (let [p center c color] + (draw-ring-lines-raw (addr p) inner outer start end segments (addr c)))) + +;; Counter-clockwise, and raylib means it: the clockwise winding is culled and +;; draws nothing at all, which looks exactly like a broken binding. +(declare draw-triangle-raw + [v1 (Ptr Vector2) v2 (Ptr Vector2) v3 (Ptr Vector2) color (Ptr Color)] + "flan_rl_draw_triangle") + +(defn draw-triangle [v1 Vector2 v2 Vector2 v3 Vector2 color Color] + (let [a v1 b v2 d v3 c color] + (draw-triangle-raw (addr a) (addr b) (addr d) (addr c)))) + +(declare draw-triangle-lines-raw + [v1 (Ptr Vector2) v2 (Ptr Vector2) v3 (Ptr Vector2) color (Ptr Color)] + "flan_rl_draw_triangle_lines") + +(defn draw-triangle-lines [v1 Vector2 v2 Vector2 v3 Vector2 color Color] + (let [a v1 b v2 d v3 c color] + (draw-triangle-lines-raw (addr a) (addr b) (addr d) (addr c)))) + +(declare draw-rectangle-v-raw + [position (Ptr Vector2) size (Ptr Vector2) color (Ptr Color)] + "flan_rl_draw_rectangle_v") + +(defn draw-rectangle-v [position Vector2 size Vector2 color Color] + (let [p position s size c color] + (draw-rectangle-v-raw (addr p) (addr s) (addr c)))) + +(declare draw-rectangle-rec-raw [rec (Ptr Rectangle) color (Ptr Color)] + "flan_rl_draw_rectangle_rec") + +(defn draw-rectangle-rec [rec Rectangle color Color] + (let [r rec c color] (draw-rectangle-rec-raw (addr r) (addr c)))) + +(declare draw-rectangle-lines-raw + [x i32 y i32 width i32 height i32 color (Ptr Color)] + "flan_rl_draw_rectangle_lines") + +(defn draw-rectangle-lines [x i32 y i32 width i32 height i32 color Color] + (let [c color] (draw-rectangle-lines-raw x y width height (addr c)))) + +;; The one-pixel outline above is drawn with GL lines and sits *on* the +;; rectangle's edge; this one is drawn with quads and sits inside it, so the +;; two do not agree at thickness 1 and that is raylib's doing, not a bug here. +(declare draw-rectangle-lines-ex-raw + [rec (Ptr Rectangle) thick f32 color (Ptr Color)] + "flan_rl_draw_rectangle_lines_ex") + +(defn draw-rectangle-lines-ex [rec Rectangle thick f32 color Color] + (let [r rec c color] (draw-rectangle-lines-ex-raw (addr r) thick (addr c)))) + +;; `roundness` is 0 to 1 as a fraction of the shorter side, so 0 is a plain +;; rectangle and 1 is a stadium. +(declare draw-rectangle-rounded-raw + [rec (Ptr Rectangle) roundness f32 segments i32 color (Ptr Color)] + "flan_rl_draw_rectangle_rounded") + +(defn draw-rectangle-rounded [rec Rectangle roundness f32 segments i32 + color Color] + (let [r rec c color] + (draw-rectangle-rounded-raw (addr r) roundness segments (addr c)))) + +;; No thickness here — see the section note. The `-ex` form below is the one +;; that takes it. +(declare draw-rectangle-rounded-lines-raw + [rec (Ptr Rectangle) roundness f32 segments i32 color (Ptr Color)] + "flan_rl_draw_rectangle_rounded_lines") + +(defn draw-rectangle-rounded-lines [rec Rectangle roundness f32 segments i32 + color Color] + (let [r rec c color] + (draw-rectangle-rounded-lines-raw (addr r) roundness segments (addr c)))) + +(declare draw-rectangle-rounded-lines-ex-raw + [rec (Ptr Rectangle) roundness f32 segments i32 thick f32 + color (Ptr Color)] + "flan_rl_draw_rectangle_rounded_lines_ex") + +(defn draw-rectangle-rounded-lines-ex [rec Rectangle roundness f32 + segments i32 thick f32 color Color] + (let [r rec c color] + (draw-rectangle-rounded-lines-ex-raw (addr r) roundness segments thick + (addr c)))) + +;; ── Text ──────────────────────────────────────────────────────────── +;; +;; Both of these use raylib's built-in font, and both therefore need +;; init-window — not for the GPU in measure-text's case, but because the +;; default font is only loaded as part of opening a window. Called headless, +;; measure-text answers 0 for every string, which was measured against +;; 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. + +(declare draw-text-raw + [text string x i32 y i32 font-size i32 color (Ptr Color)] + "flan_rl_draw_text") + +(defn draw-text [text string x i32 y i32 font-size i32 color Color] + (let [c color] (draw-text-raw text x y font-size (addr c)))) + +(declare measure-text [text string font-size i32] i32 "flan_rl_measure_text") + +;; ── Timing and window state ───────────────────────────────────────── +;; +;; All four read state that init-window creates, so all four answer 0 before +;; there is a window — again measured, not assumed. get-frame-time is the +;; delta the last frame took, in seconds, which is what a simulation should +;; scale by instead of assuming the target fps was met. + +(declare get-frame-time [] f32 "flan_rl_get_frame_time") +(declare get-time [] f64 "flan_rl_get_time") +(declare get-screen-width [] i32 "flan_rl_get_screen_width") +(declare get-screen-height [] i32 "flan_rl_get_screen_height") diff --git a/vendor/raylib/shim.c b/vendor/raylib/shim.c index 1cd7e13..b9f9e1e 100644 --- a/vendor/raylib/shim.c +++ b/vendor/raylib/shim.c @@ -232,3 +232,245 @@ bool flan_rl_check_collision_lines(const Vector2 *a1, const Vector2 *a2, Vector2 *out) { return CheckCollisionLines(*a1, *a2, *b1, *b2, out); } + +/* ── Images ───────────────────────────────────────────────────────── + * + * An Image is pixels in RAM, so all of this runs with no window and no GL + * context — which is why it is the part of the package that a headless test + * can actually assert rather than merely link. `data` is the pixel buffer + * raylib owns; `format` is a PixelFormat enum and GenImageColor makes 7 + * (uncompressed R8G8B8A8). + * + * raylib 5.5 spells the predicate IsImageValid. IsImageReady, which the 5.1 + * header still had, is gone — checked with nm -D, not remembered. + */ +typedef struct { void *data; int width, height, mipmaps, format; } Image; + +extern Image LoadImage(const char *fileName); +extern bool IsImageValid(Image image); +extern void UnloadImage(Image image); +extern bool ExportImage(Image image, const char *fileName); +extern Image GenImageColor(int width, int height, Color color); +extern void ImageResize(Image *image, int newWidth, int newHeight); +extern void ImageResizeNN(Image *image, int newWidth, int newHeight); +extern void ImageCrop(Image *image, Rectangle crop); +extern void ImageFlipHorizontal(Image *image); +extern void ImageFlipVertical(Image *image); +extern void ImageDrawPixel(Image *dst, int posX, int posY, Color color); +extern Color GetImageColor(Image image, int x, int y); +extern Texture2D LoadTextureFromImage(Image image); + +void flan_rl_load_image(const char *path, long long n, Image *out) { + char buf[PATH_MAX]; + *out = LoadImage(cstr(path, n, buf, sizeof buf)); +} + +bool flan_rl_is_image_valid(const Image *image) { return IsImageValid(*image); } +void flan_rl_unload_image(const Image *image) { UnloadImage(*image); } + +bool flan_rl_export_image(const Image *image, const char *path, long long n) { + char buf[PATH_MAX]; + return ExportImage(*image, cstr(path, n, buf, sizeof buf)); +} + +void flan_rl_gen_image_color(int width, int height, const Color *color, + Image *out) { + *out = GenImageColor(width, height, *color); +} + +void flan_rl_image_resize(Image *image, int w, int h) { ImageResize(image, w, h); } +void flan_rl_image_resize_nn(Image *image, int w, int h) { ImageResizeNN(image, w, h); } + +void flan_rl_image_crop(Image *image, const Rectangle *crop) { + ImageCrop(image, *crop); +} + +void flan_rl_image_flip_horizontal(Image *image) { ImageFlipHorizontal(image); } +void flan_rl_image_flip_vertical(Image *image) { ImageFlipVertical(image); } + +void flan_rl_image_draw_pixel(Image *dst, int x, int y, const Color *color) { + ImageDrawPixel(dst, x, y, *color); +} + +void flan_rl_get_image_color(const Image *image, int x, int y, Color *out) { + *out = GetImageColor(*image, x, y); +} + +void flan_rl_load_texture_from_image(const Image *image, Texture2D *out) { + *out = LoadTextureFromImage(*image); +} + +/* ── Shapes, text and timing ───────────────────────────────────────── + * + * All of the drawing below needs a GL context and therefore a window, so none + * of it can be in the acceptance table — it is exercised by running sand.flan + * and looking. The three prototypes most likely to be remembered wrong were + * checked against nm -D on libraylib.so.550 rather than against a header: + * raylib 5.5 moved the line thickness off DrawRectangleRoundedLines onto + * DrawRectangleRoundedLinesEx, so the four-argument form here is the 5.5 one + * and not the 5.1 one. + * + * MeasureText, GetFrameTime, GetTime and GetScreenWidth/Height need no GL + * context but do need InitWindow: the first reads the default font, which + * only InitWindow loads, and the others read window state. Headless they all + * answer 0 — measured, not assumed — so they are no more assertable than the + * drawing is. + */ + +extern void DrawPixel(int posX, int posY, Color color); +extern void DrawPixelV(Vector2 position, Color color); +extern void DrawLine(int startPosX, int startPosY, int endPosX, int endPosY, + Color color); +extern void DrawLineV(Vector2 startPos, Vector2 endPos, Color color); +extern void DrawLineEx(Vector2 startPos, Vector2 endPos, float thick, Color color); +extern void DrawCircle(int centerX, int centerY, float radius, Color color); +extern void DrawCircleV(Vector2 center, float radius, Color color); +extern void DrawCircleLines(int centerX, int centerY, float radius, Color color); +extern void DrawCircleLinesV(Vector2 center, float radius, Color color); +extern void DrawEllipse(int centerX, int centerY, float radiusH, float radiusV, + Color color); +extern void DrawEllipseLines(int centerX, int centerY, float radiusH, + float radiusV, Color color); +extern void DrawRing(Vector2 center, float innerRadius, float outerRadius, + float startAngle, float endAngle, int segments, Color color); +extern void DrawRingLines(Vector2 center, float innerRadius, float outerRadius, + float startAngle, float endAngle, int segments, + Color color); +extern void DrawTriangle(Vector2 v1, Vector2 v2, Vector2 v3, Color color); +extern void DrawTriangleLines(Vector2 v1, Vector2 v2, Vector2 v3, Color color); +extern void DrawRectangleV(Vector2 position, Vector2 size, Color color); +extern void DrawRectangleRec(Rectangle rec, Color color); +extern void DrawRectangleLines(int posX, int posY, int width, int height, + Color color); +extern void DrawRectangleLinesEx(Rectangle rec, float lineThick, Color color); +extern void DrawRectangleRounded(Rectangle rec, float roundness, int segments, + Color color); +extern void DrawRectangleRoundedLines(Rectangle rec, float roundness, + int segments, Color color); +extern void DrawRectangleRoundedLinesEx(Rectangle rec, float roundness, + int segments, float lineThick, + Color color); +extern void DrawText(const char *text, int posX, int posY, int fontSize, + Color color); +extern int MeasureText(const char *text, int fontSize); +extern float GetFrameTime(void); +extern double GetTime(void); +extern int GetScreenWidth(void); +extern int GetScreenHeight(void); + +void flan_rl_draw_pixel(int x, int y, const Color *c) { DrawPixel(x, y, *c); } + +void flan_rl_draw_pixel_v(const Vector2 *p, const Color *c) { + DrawPixelV(*p, *c); +} + +void flan_rl_draw_line(int x1, int y1, int x2, int y2, const Color *c) { + DrawLine(x1, y1, x2, y2, *c); +} + +void flan_rl_draw_line_v(const Vector2 *a, const Vector2 *b, const Color *c) { + DrawLineV(*a, *b, *c); +} + +void flan_rl_draw_line_ex(const Vector2 *a, const Vector2 *b, float thick, + const Color *c) { + DrawLineEx(*a, *b, thick, *c); +} + +void flan_rl_draw_circle(int x, int y, float radius, const Color *c) { + DrawCircle(x, y, radius, *c); +} + +void flan_rl_draw_circle_v(const Vector2 *center, float radius, const Color *c) { + DrawCircleV(*center, radius, *c); +} + +void flan_rl_draw_circle_lines(int x, int y, float radius, const Color *c) { + DrawCircleLines(x, y, radius, *c); +} + +void flan_rl_draw_circle_lines_v(const Vector2 *center, float radius, + const Color *c) { + DrawCircleLinesV(*center, radius, *c); +} + +void flan_rl_draw_ellipse(int x, int y, float rh, float rv, const Color *c) { + DrawEllipse(x, y, rh, rv, *c); +} + +void flan_rl_draw_ellipse_lines(int x, int y, float rh, float rv, const Color *c) { + DrawEllipseLines(x, y, rh, rv, *c); +} + +void flan_rl_draw_ring(const Vector2 *center, float inner, float outer, + float start, float end, int segments, const Color *c) { + DrawRing(*center, inner, outer, start, end, segments, *c); +} + +void flan_rl_draw_ring_lines(const Vector2 *center, float inner, float outer, + float start, float end, int segments, + const Color *c) { + DrawRingLines(*center, inner, outer, start, end, segments, *c); +} + +void flan_rl_draw_triangle(const Vector2 *a, const Vector2 *b, const Vector2 *d, + const Color *c) { + DrawTriangle(*a, *b, *d, *c); +} + +void flan_rl_draw_triangle_lines(const Vector2 *a, const Vector2 *b, + const Vector2 *d, const Color *c) { + DrawTriangleLines(*a, *b, *d, *c); +} + +void flan_rl_draw_rectangle_v(const Vector2 *position, const Vector2 *size, + const Color *c) { + DrawRectangleV(*position, *size, *c); +} + +void flan_rl_draw_rectangle_rec(const Rectangle *rec, const Color *c) { + DrawRectangleRec(*rec, *c); +} + +void flan_rl_draw_rectangle_lines(int x, int y, int w, int h, const Color *c) { + DrawRectangleLines(x, y, w, h, *c); +} + +void flan_rl_draw_rectangle_lines_ex(const Rectangle *rec, float thick, + const Color *c) { + DrawRectangleLinesEx(*rec, thick, *c); +} + +void flan_rl_draw_rectangle_rounded(const Rectangle *rec, float roundness, + int segments, const Color *c) { + DrawRectangleRounded(*rec, roundness, segments, *c); +} + +/* Four arguments, not five: 5.5's DrawRectangleRoundedLines has no thickness + * and the Ex variant below is where it went. Getting this wrong links fine. */ +void flan_rl_draw_rectangle_rounded_lines(const Rectangle *rec, float roundness, + int segments, const Color *c) { + DrawRectangleRoundedLines(*rec, roundness, segments, *c); +} + +void flan_rl_draw_rectangle_rounded_lines_ex(const Rectangle *rec, + float roundness, int segments, + float thick, const Color *c) { + DrawRectangleRoundedLinesEx(*rec, roundness, segments, thick, *c); +} + +void flan_rl_draw_text(const char *text, long long n, int x, int y, + int font_size, const Color *c) { + char buf[512]; + DrawText(cstr(text, n, buf, sizeof buf), x, y, font_size, *c); +} + +int flan_rl_measure_text(const char *text, long long n, int font_size) { + char buf[512]; + return MeasureText(cstr(text, n, buf, sizeof buf), font_size); +} + +float flan_rl_get_frame_time(void) { return GetFrameTime(); } +double flan_rl_get_time(void) { return GetTime(); } +int flan_rl_get_screen_width(void) { return GetScreenWidth(); } +int flan_rl_get_screen_height(void) { return GetScreenHeight(); }