diff --git a/NEXT.md b/NEXT.md index 345fe2b..0c97646 100644 --- a/NEXT.md +++ b/NEXT.md @@ -237,14 +237,30 @@ little-endian reading of the packed integer, so an identity would have passed a weaker test. That case is in the acceptance table, skipped if `libraylib` is not installed. -The bindings are 18 calls: window (`init-window`, `close-window`, +The bindings are 29 calls: window (`init-window`, `close-window`, `window-should-close?`, `set-target-fps`, `set-trace-log-level`), keyboard (`key-pressed?`/`down?`/`released?`), mouse (`mouse-button-pressed?`/`down?`/ -`released?`, `get-mouse-position`), `get-color`, and drawing (`begin-drawing`, -`end-drawing`, `draw-fps`, `clear-background`, `draw-rectangle`), plus the -`Key`, `MouseButton` and `TraceLogLevel` enums. Adding one is three lines: a +`released?`, `get-mouse-position`), `get-color`, drawing (`begin-drawing`, +`end-drawing`, `draw-fps`, `clear-background`, `draw-rectangle`), textures +(`load-texture`, `texture-valid?`, `unload-texture`, `draw-texture`, +`draw-texture-v`, `draw-texture-ex`, `draw-texture-rec`), and the shapes +texture and rectangle intersection (`set-shapes-texture`, +`get-shapes-texture`, `get-shapes-texture-rectangle`, `get-collision-rec`), +plus the `Key`, `MouseButton` and `TraceLogLevel` enums and the `Vector2`, +`Color`, `Texture2D` and `Rectangle` structs. Adding one is three lines: a `declare`, an `extern` prototype, and a one-line wrapper. +The texture calls are the first ones with no headless test, because loading +one needs a GL context. What the acceptance case does instead is pin the two +new struct layouts using the only things raylib computes from those fields +without a GPU: `GetCollisionRec`, which pins `Rectangle` completely, and +`SetShapesTexture`'s default substitution, which pins `Texture2D`'s `id` and +`format` and nothing else. Handing a struct over and reading it back proves +nothing at all — storing and returning is symmetric, so a permuted layout +comes back permuted the same way and the case passes. `width`, `height` and +`mipmaps` are therefore checked only by looking at `sand.flan` running, which +draws the brush sprite four ways for that reason. + No raylib headers are needed: `shim.c` declares the prototypes it uses, so the build depends on the shared library being linkable and not on `raylib-devel`. `vendor/raylib/link` carries `-l:libraylib.so.550` because Fedora ships the diff --git a/brush.png b/brush.png new file mode 100644 index 0000000..f2e96d4 Binary files /dev/null and b/brush.png differ diff --git a/sand.flan b/sand.flan index 8671f69..eb29636 100644 --- a/sand.flan +++ b/sand.flan @@ -28,6 +28,40 @@ (import sim "sand-sim") ; no collection prefix: relative to this file (import agent "vendor:agent") ; the dev agent: redefinitions, installed below +;; The brush sprite: a 16x8 sheet of two 8x8 frames, the ring drawn while the +;; mouse is idle and the blob while it is painting. It is here because the +;; texture calls cannot be in the acceptance table at all — loading one needs a +;; GL context — so the only way they are exercised is by running this. +(defvar brush rl/Texture2D) +(defvar brush-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. +(defn load-brush [] + (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"))) + +;; 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 +;; needs a source rectangle; the three badges in the corner are the whole +;; sheet, at integer coordinates, at a Vector2 tinted with the current sand +;; colour, and scaled up — draw-texture, draw-texture-v and draw-texture-ex. +(defn draw-brush [] + (when brush-ok + (let [m (rl/get-mouse-position) + frame (f32 (if (rl/mouse-button-down? :left) 8.0 0.0))] + (rl/draw-texture-rec brush + (rl/Rectangle {:x frame :y 0.0 :width 8.0 :height 8.0}) + (rl/Vector2 {:x (- (.x m) 4.0) :y (- (.y m) 4.0)}) + rl/white) + (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)))) + ;; Locals are assignable places (spec-memory.md); parameters are not. (defn paint [] (let [m (rl/get-mouse-position) @@ -61,6 +95,7 @@ (i32 (* row sim/cell-size)) sim/cell-size sim/cell-size (rl/get-color c)))))) + (draw-brush) (rl/draw-fps 20 20)) (defn main [] @@ -68,6 +103,10 @@ (rl/init-window sim/screen-width sim/screen-height "SAND") (defer (rl/close-window)) (rl/set-target-fps 120) + ;; 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)) ;; 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-ffi.flan b/test/programs/raylib-ffi.flan index ad0b719..f336fe7 100644 --- a/test/programs/raylib-ffi.flan +++ b/test/programs/raylib-ffi.flan @@ -1,12 +1,74 @@ (import rl "vendor:raylib") -;; No window: GetColor and the enums are pure, so this exercises the whole -;; boundary — struct out-pointer, keyword->enum, string ptr+len — headlessly. +;; 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)) + (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) - (rl/set-trace-log-level :warning) - 0)) + (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))) + 0) diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index 3670c0c..14bb5ad 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -142,14 +142,38 @@ let () = text code end; - (* The raylib FFI, headless. GetColor and the enums need no window, so the - whole boundary is exercised without a display: a struct returned through - an out-pointer, a keyword resolved against an enum, and a Flan string - crossing as ptr+len. 0x11223344 comes back as four separate bytes, which - is the check that matters — a Color is not the little-endian reading of - the packed integer, so an identity would pass a weaker test. *) - if Sys.command "ldconfig -p 2>/dev/null | grep -q libraylib" = 0 then - outputs "raylib ffi, headless" "programs/raylib-ffi.flan" "17\n34\n51\n68\n" + (* The raylib FFI, headless. GetColor, rectangle intersection and the + shapes texture need no window, so the whole boundary is exercised + without a display: a struct out of C through an out-pointer, a struct + into C through a pointer, a keyword resolved against an enum, and a + Flan string crossing as ptr+len. + + Every case is asymmetric, which is the point. 0x11223344 comes back as + four separate bytes, so a Color is not the little-endian reading of the + packed integer. The intersection of (0,0,10,4) and (6,1,10,10) is + (6,1,4,3), four numbers from four different pairs of fields, so no + permutation of Rectangle survives it. And the shapes texture is stored + or replaced by 1 1 1 1 7 depending on which field is zero, which pins + Texture2D's id and format. Handing raylib a struct and reading it back + would have passed with any of those permuted — storing and returning is + symmetric. What the last case cannot pin, because nothing raylib + computes without a GL context reads them, is width, height and mipmaps + against each other. *) + let raylib_out = + "17\n34\n51\n68\n\ + 6\n1\n4\n3\n\ + 7\n13\n17\n2\n4\n3.5\n7.25\n11.5\n13.75\n\ + 1\n1\n1\n1\n7\n\ + 7\n0\n17\n2\n4\n" + in + if Sys.command "ldconfig -p 2>/dev/null | grep -q libraylib" = 0 then begin + outputs "raylib ffi, headless" "programs/raylib-ffi.flan" raylib_out; + (* And at -O0, for the reason the rest of the table is: every struct + here crosses as (addr v) on a local, which is the alloca mem2reg + would launder before anyone noticed it was wrong. *) + outputs ~opt:"-O0" "raylib ffi, headless, -O0" "programs/raylib-ffi.flan" + raylib_out + end else print_endline "acceptance: skipping the raylib FFI case (no libraylib)"; diff --git a/vendor/raylib/raylib.flan b/vendor/raylib/raylib.flan index 0d6194d..e50822c 100644 --- a/vendor/raylib/raylib.flan +++ b/vendor/raylib/raylib.flan @@ -20,6 +20,12 @@ (defstruct Vector2 [x f32 y f32]) (defstruct Color [r u8 g u8 b u8 a u8]) +;; Texture2D is five 4-byte fields in a row, which is the layout most likely +;; to be silently wrong: permute two of them and every field still reads as a +;; plausible number. Rectangle is four floats in x/y/width/height order. +(defstruct Texture2D [id u32 width i32 height i32 mipmaps i32 format i32]) +(defstruct Rectangle [x f32 y f32 width f32 height f32]) + ;; KeyboardKey, the subset sand.flan uses. A keyword at a call site resolves ;; against these members at compile time and a typo is an error there. (defenum Key @@ -103,3 +109,130 @@ (defn draw-rectangle [x i32 y i32 width i32 height i32 color Color] (let [c color] (draw-rectangle-raw x y width height (addr c)))) + +;; ── Shapes texture ────────────────────────────────────────────────── +;; +;; raylib draws every shape from one atlas texture, and this pair sets and +;; reads it. It is bound here for a second reason: it is the only part of the +;; API that stores a Texture2D and a Rectangle and hands them back without +;; touching the GPU, so it is how the acceptance table checks both layouts +;; headlessly. Everything else that takes a texture needs a GL context. +;; +;; raylib substitutes a default ({1,1,1,1,7} / {0,0,1,1}) when the id or the +;; source's width or height is not positive, so a caller — and the test — +;; should keep clear of those values if it wants its own back. + +(declare set-shapes-texture-raw + [texture (Ptr Texture2D) source (Ptr Rectangle)] + "flan_rl_set_shapes_texture") + +(defn set-shapes-texture [texture Texture2D source Rectangle] + (let [t texture + r source] + (set-shapes-texture-raw (addr t) (addr r)))) + +(declare get-shapes-texture-raw [out (Ptr Texture2D)] "flan_rl_get_shapes_texture") + +(defn get-shapes-texture [] Texture2D + (let [t (Texture2D {})] + (get-shapes-texture-raw (addr t)) + t)) + +(declare get-shapes-texture-rectangle-raw + [out (Ptr Rectangle)] "flan_rl_get_shapes_texture_rectangle") + +(defn get-shapes-texture-rectangle [] Rectangle + (let [r (Rectangle {})] + (get-shapes-texture-rectangle-raw (addr r)) + r)) + +;; ── Shapes ────────────────────────────────────────────────────────── +;; +;; Rectangle intersection, which raylib computes from all four fields in +;; different ways. It is the one Rectangle call that needs no GPU, so it is +;; also how the acceptance table pins the layout: a store-and-return check is +;; symmetric and a permuted layout survives it untouched. + +(declare get-collision-rec-raw + [a (Ptr Rectangle) b (Ptr Rectangle) out (Ptr Rectangle)] + "flan_rl_get_collision_rec") + +(defn get-collision-rec [a Rectangle b Rectangle] Rectangle + (let [x a + y b + out (Rectangle {})] + (get-collision-rec-raw (addr x) (addr y) (addr out)) + out)) + +;; ── Textures ──────────────────────────────────────────────────────── +;; +;; Everything here needs a GL context, so a window has to be open first — +;; load-texture before init-window returns an id of 0 and raylib says so on +;; the log. texture-valid? is how that is noticed in the program rather than +;; only in the log; raylib 5.5 spells it IsTextureValid, and IsTextureReady, +;; which older code calls, does not exist in this version. + +(declare load-texture-raw [path string out (Ptr Texture2D)] "flan_rl_load_texture") + +(defn load-texture [path string] Texture2D + (let [t (Texture2D {})] + (load-texture-raw path (addr t)) + t)) + +(declare texture-valid?-raw [texture (Ptr Texture2D)] bool "flan_rl_is_texture_valid") + +(defn texture-valid? [texture Texture2D] bool + (let [t texture] + (texture-valid?-raw (addr t)))) + +(declare unload-texture-raw [texture (Ptr Texture2D)] "flan_rl_unload_texture") + +(defn unload-texture [texture Texture2D] + (let [t texture] + (unload-texture-raw (addr t)))) + +(declare draw-texture-raw + [texture (Ptr Texture2D) x i32 y i32 tint (Ptr Color)] + "flan_rl_draw_texture") + +(defn draw-texture [texture Texture2D x i32 y i32 tint Color] + (let [t texture + c tint] + (draw-texture-raw (addr t) x y (addr c)))) + +(declare draw-texture-v-raw + [texture (Ptr Texture2D) position (Ptr Vector2) tint (Ptr Color)] + "flan_rl_draw_texture_v") + +(defn draw-texture-v [texture Texture2D position Vector2 tint Color] + (let [t texture + p position + c tint] + (draw-texture-v-raw (addr t) (addr p) (addr c)))) + +(declare draw-texture-ex-raw + [texture (Ptr Texture2D) position (Ptr Vector2) rotation f32 scale f32 + tint (Ptr Color)] + "flan_rl_draw_texture_ex") + +(defn draw-texture-ex [texture Texture2D position Vector2 rotation f32 + scale f32 tint Color] + (let [t texture + p position + c tint] + (draw-texture-ex-raw (addr t) (addr p) rotation scale (addr c)))) + +;; A negative source width or height flips the sprite, which is how a sheet is +;; drawn facing the other way without a second image. +(declare draw-texture-rec-raw + [texture (Ptr Texture2D) source (Ptr Rectangle) position (Ptr Vector2) + tint (Ptr Color)] + "flan_rl_draw_texture_rec") + +(defn draw-texture-rec [texture Texture2D source Rectangle position Vector2 + tint Color] + (let [t texture + s source + p position + c tint] + (draw-texture-rec-raw (addr t) (addr s) (addr p) (addr c)))) diff --git a/vendor/raylib/shim.c b/vendor/raylib/shim.c index 11db1a6..a993548 100644 --- a/vendor/raylib/shim.c +++ b/vendor/raylib/shim.c @@ -15,10 +15,13 @@ #include #include +#include #include typedef struct { float x, y; } Vector2; typedef struct { unsigned char r, g, b, a; } Color; +typedef struct { unsigned int id; int width, height, mipmaps, format; } Texture2D; +typedef struct { float x, y, width, height; } Rectangle; extern void InitWindow(int width, int height, const char *title); extern void CloseWindow(void); @@ -38,10 +41,26 @@ extern void EndDrawing(void); extern void DrawFPS(int x, int y); extern void ClearBackground(Color color); extern void DrawRectangle(int x, int y, int width, int height, Color color); +extern void SetShapesTexture(Texture2D texture, Rectangle source); +extern Texture2D GetShapesTexture(void); +extern Rectangle GetShapesTextureRectangle(void); +extern Rectangle GetCollisionRec(Rectangle a, Rectangle b); +extern Texture2D LoadTexture(const char *fileName); +extern bool IsTextureValid(Texture2D texture); +extern void UnloadTexture(Texture2D texture); +extern void DrawTexture(Texture2D texture, int posX, int posY, Color tint); +extern void DrawTextureV(Texture2D texture, Vector2 position, Color tint); +extern void DrawTextureEx(Texture2D texture, Vector2 position, float rotation, + float scale, Color tint); +extern void DrawTextureRec(Texture2D texture, Rectangle source, + Vector2 position, Color tint); /* A Flan string arrives as ptr+len and is not NUL-terminated, so a C API that - * wants a C string needs a copy. The window title is the only one, it is short - * by nature, and truncating is better than reading past the end. */ + * wants a C string needs a copy. Two callers want one: the window title and a + * texture's file path. Each passes a buffer big enough for what it is — 256 + * for a title, PATH_MAX for a path — and truncating is better than reading + * past the end. A truncated path fails to open and LoadTexture returns an id + * of 0, which is what texture-valid? is for. */ static const char *cstr(const char *p, long long n, char *buf, size_t cap) { size_t k = (size_t)n < cap - 1 ? (size_t)n : cap - 1; memcpy(buf, p, k); @@ -81,3 +100,49 @@ void flan_rl_draw_rectangle(int x, int y, int width, int height, const Color *color) { DrawRectangle(x, y, width, height, *color); } + +void flan_rl_set_shapes_texture(const Texture2D *texture, const Rectangle *source) { + SetShapesTexture(*texture, *source); +} + +void flan_rl_get_shapes_texture(Texture2D *out) { *out = GetShapesTexture(); } + +void flan_rl_get_shapes_texture_rectangle(Rectangle *out) { + *out = GetShapesTextureRectangle(); +} + +void flan_rl_get_collision_rec(const Rectangle *a, const Rectangle *b, + Rectangle *out) { + *out = GetCollisionRec(*a, *b); +} + +void flan_rl_load_texture(const char *path, long long n, Texture2D *out) { + char buf[PATH_MAX]; + *out = LoadTexture(cstr(path, n, buf, sizeof buf)); +} + +bool flan_rl_is_texture_valid(const Texture2D *texture) { + return IsTextureValid(*texture); +} + +void flan_rl_unload_texture(const Texture2D *texture) { UnloadTexture(*texture); } + +void flan_rl_draw_texture(const Texture2D *texture, int x, int y, + const Color *tint) { + DrawTexture(*texture, x, y, *tint); +} + +void flan_rl_draw_texture_v(const Texture2D *texture, const Vector2 *position, + const Color *tint) { + DrawTextureV(*texture, *position, *tint); +} + +void flan_rl_draw_texture_ex(const Texture2D *texture, const Vector2 *position, + float rotation, float scale, const Color *tint) { + DrawTextureEx(*texture, *position, rotation, scale, *tint); +} + +void flan_rl_draw_texture_rec(const Texture2D *texture, const Rectangle *source, + const Vector2 *position, const Color *tint) { + DrawTextureRec(*texture, *source, *position, *tint); +}