;;;; raylib, declared for Flan. The directory is the package (plan.org, ;;;; Modules), and (import rl "vendor:raylib") qualifies all of it as rl/… ;;;; ;;;; Every binding here is one `declare-c` line naming raylib's own function ;;;; in raylib's own signature — Color by value, Vector2 returned by value — ;;;; and the compiler writes the C that flattens it. There is no shim.c in ;;;; this directory any more, and no hand-written wrapper at all. ;;;; ;;;; The shim itself did not go away, only the typing of it. A small ;;;; aggregate's calling convention is a per-target classification rather than ;;;; part of its layout: x86-64 hands Vector2 over as <2 x float> and returns ;;;; Rectangle as {i64,i64}, and arm64 and wasm32 each do something else. ;;;; Written in C, clang classifies every one of them correctly for whichever ;;;; target the build is for; written in emit.ml it would be three calling ;;;; conventions to reimplement and then keep correct forever, and a mistake ;;;; would read as a field full of garbage rather than as a link error. This ;;;; is "one narrow host ABI, implemented twice" (plan.org, Targets). See ;;;; lib/shim.ml. ;;;; ;;;; What that means for reading this file: the `defstruct`s below are the ;;;; only statement anywhere about raylib's layouts, and the generated C ;;;; typedefs are made from them. No raylib header is consulted — a build ;;;; needs libraylib linkable, not raylib-devel — so a wrong field order here ;;;; is wrong everywhere and nothing but a test can catch it. The acceptance ;;;; cases pin the layouts by making raylib *compute* with the fields, and ;;;; they go red when a struct below is permuted. Likewise a scalar's width: ;;;; f64 where raylib says float now emits `double` in the generated ;;;; prototype, and raylib reads garbage. ;;;; ;;;; Some bindings keep a hand-written Flan wrapper, because their Flan face ;;;; is deliberately not raylib's. Three shapes of that, and every wrapper in ;;;; this file is one of them: ;;;; ;;;; - a slice where C takes a pointer and a count — collision-point-poly?, ;;;; load-image-from-memory, load-font-ex, and the eleven vector-array ;;;; drawing calls under "A slice where raylib wants a pointer and a ;;;; count"; ;;;; - an Option where C signals failure by a bool out-parameter or a ;;;; sentinel — collision-lines, get-key-pressed, get-char-pressed; ;;;; - an enum where the header says `int`. These are NOT wrappers: a C ;;;; enum parameter has an int's ABI, so the hand-written declare-c with ;;;; the Flan type on it is the whole fix, and set-exit-key, ;;;; set-mouse-cursor, key-up? and mouse-button-up? are all that. ;;;; ;;;; What is NOT here, and was asked for: with-drawing and with-mode-2d over ;;;; raylib's begin/end pairs. An unbalanced pair is a real bug and a macro ;;;; removes it, but a macro cannot live in a package — the expander collects ;;;; defmacros from the prelude and from the file being compiled, and a ;;;; defmacro in an imported package is refused by name ;;;; (test/programs/pkg-macro.flan, an acceptance case whose whole content is ;;;; the refusal). So these have to be written in the program that uses them, ;;;; or wait for macros to be importable, and neither is this file's to do. ;; Layouts are C's — no object headers anywhere — so these are exactly ;; raylib's structs and nothing marshals. ;; Vector3 sits here rather than in the 3D section below because it is a ;; vector before it is a camera's business: nothing about it is 3D-only. Three ;; floats in x/y/z order, and — like the three Vector3 fields of Camera3D — ;; nothing in the acceptance table can tell a permutation of them apart, ;; because every permutation has the same layout. (defstruct Vector2 [x f32 y f32]) (defstruct Vector3 [x f32 y f32 z 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. ;; ;; Every member carries the `key-` prefix, as every member of every other ;; enum in this file carries its own — the prefix is uniform across all ;; eleven. It is there to be READ and not to avoid a clash: a keyword ;; resolves against the expected type and nothing else, so :left could ;; already mean a key here and a button there with no trouble at all. What it ;; buys is a call site that says which closed set the name came from without ;; the reader having to know the signature. ;; ;; The prefix is declared in `bindings` (the enum line's third column), which ;; is what keeps :key-r checking against KEY_R rather than KEY_KEY_R. (defenum Key [key-space 32 key-apostrophe 39 key-comma 44 key-minus 45 key-period 46 key-slash 47 key-zero 48 key-one 49 key-two 50 key-three 51 key-four 52 key-five 53 key-six 54 key-seven 55 key-eight 56 key-nine 57 key-a 65 key-b 66 key-c 67 key-d 68 key-e 69 key-f 70 key-g 71 key-h 72 key-i 73 key-j 74 key-k 75 key-l 76 key-m 77 key-n 78 key-o 79 key-p 80 key-q 81 key-r 82 key-s 83 key-t 84 key-u 85 key-v 86 key-w 87 key-x 88 key-y 89 key-z 90 key-escape 256 key-enter 257 key-tab 258 key-backspace 259 key-right 262 key-left 263 key-down 264 key-up 265 key-left-shift 340 ;; KEY_NULL is not a key. It is the value set-exit-key takes to mean "no ;; key closes the window", which is the only thing that ever passes it. key-null 0]) ;; `mouse-` and not `button-`, which GamepadButton has: a mouse button and a ;; pad button are different sets and the prefix is where a reader is told ;; which one a keyword came out of. (defenum MouseButton [mouse-left 0 mouse-right 1 mouse-middle 2 mouse-side 3 mouse-extra 4 mouse-forward 5 mouse-back 6]) ;; `log-` rather than `trace-`: the enum is TraceLogLevel but the C names are ;; LOG_, `trace` is itself a member, and :log-warning is what the call reads ;; as. ;; ;; `warning` beside `log-warning` is the same value twice, which a defenum ;; allows, and it is the one member in this file that does not carry its ;; enum's prefix. It is an alias for sand.flan's single ;; `(rl/set-trace-log-level :warning)` — the file this lane was told not to ;; touch — and goes, with its `constant` line in `bindings`, when that call ;; is respelled. (defenum TraceLogLevel [log-all 0 log-trace 1 log-debug 2 log-info 3 log-warning 4 log-error 5 log-fatal 6 log-none 7 warning 4]) ;; ── Window ────────────────────────────────────────────────────────── (declare-c init-window [width i32 height i32 title string] "InitWindow") (declare-c close-window [] "CloseWindow") (declare-c window-should-close? [] bool "WindowShouldClose") ;; False before init-window and after close-window, true between. A game loop ;; started twice is the thing this answers — an engine that can be re-entered ;; from a REPL or a dev session asks it before opening a second window onto ;; the same context. (declare-c window-ready? [] bool "IsWindowReady") (declare-c set-target-fps [fps i32] "SetTargetFPS") (declare-c set-trace-log-level [level TraceLogLevel] "SetTraceLogLevel") ;; A bitfield, like the gestures below and for the same reason: raylib wants ;; the OR of several and a keyword can only ever name one member, so the ;; parameter is a u32 and the members are defconsts rather than a defenum. ;; ;; The part that is NOT obvious from the signature: set-config-flags has to ;; be called BEFORE init-window. raylib stores the flags and reads them while ;; creating the context, so setting them afterwards is accepted, logged at ;; INFO, and does nothing to the window that already exists — which looks ;; exactly like a binding that did not work. Afterwards is what the generated ;; set-window-state, clear-window-state and window-state? are for, and they ;; take these same bits, which is why the four sand.flan sets grew into the ;; whole of ConfigFlags: a subset has its hole exactly where the next caller ;; looks. Every value below was read off raylib.h 5.5. ;; ;; The values are NOT in bit order in the header and are not reordered here: ;; FLAG_VSYNC_HINT is 0x40 and FLAG_FULLSCREEN_MODE is 0x02. (defconst flag-vsync-hint u32 64) (defconst flag-fullscreen-mode u32 2) (defconst flag-window-resizable u32 4) (defconst flag-window-undecorated u32 8) (defconst flag-window-hidden u32 128) (defconst flag-window-minimized u32 512) (defconst flag-window-maximized u32 1024) (defconst flag-window-unfocused u32 2048) (defconst flag-window-topmost u32 4096) (defconst flag-window-always-run u32 256) (defconst flag-window-transparent u32 16) (defconst flag-window-highdpi u32 8192) (defconst flag-window-mouse-passthrough u32 16384) (defconst flag-borderless-windowed-mode u32 32768) (defconst flag-msaa-4x-hint u32 32) (defconst flag-interlaced-hint u32 65536) (declare-c set-config-flags [flags u32] "SetConfigFlags") ;; ── Input ─────────────────────────────────────────────────────────── ;; Which key closes the window, ESCAPE by default. Hand-written rather than ;; left to the generated half because the Flan face is the difference: raylib ;; declares it `void SetExitKey(int key)` and the generated line therefore ;; takes an i32, where this one takes a Key and so accepts :key-escape and refuses ;; a typo. `:key-null` is how a program says "no key does that" and takes the ;; close over itself. (declare-c set-exit-key [key Key] "SetExitKey") (declare-c key-pressed? [key Key] bool "IsKeyPressed") (declare-c key-down? [key Key] bool "IsKeyDown") (declare-c key-released? [key Key] bool "IsKeyReleased") ;; The other two halves of that family, hand-written for exactly the reason ;; above and added late: they were generated, so they took an i32, so ;; `(rl/key-up? :key-space)` did not compile while `(rl/key-down? :key-space)` did. ;; That is a hole in a family rather than a missing convenience — a caller ;; who has used key-down? has no reason to expect the sibling to be spelled ;; differently, and what they get instead of a keyword is a number nobody ;; checks. Nothing wraps these: the ABI of a C enum parameter is the ABI of ;; an int, so the declaration IS the fix and a defn around it would only be ;; a rename. (declare-c key-up? [key Key] bool "IsKeyUp") (declare-c key-pressed-repeat? [key Key] bool "IsKeyPressedRepeat") (declare-c mouse-button-pressed? [button MouseButton] bool "IsMouseButtonPressed") (declare-c mouse-button-down? [button MouseButton] bool "IsMouseButtonDown") (declare-c mouse-button-released? [button MouseButton] bool "IsMouseButtonReleased") (declare-c mouse-button-up? [button MouseButton] bool "IsMouseButtonUp") ;; ── Draining raylib's two input queues ────────────────────────────── ;; ;; Both of these answer "nothing left" with 0, and 0 is also a value the ;; caller could otherwise have to think about — KEY_NULL for one, the NUL ;; byte for the other. An Option says which of the two it is in the type, so ;; the loop that drains the queue cannot read the sentinel as a key or as a ;; character: `while (> key 0)` is a comparison a reader has to know the ;; convention to trust, and `(while-some ...)` — or the `if-let` shape the ;; examples use — is one a reader can check. ;; ;; The generated declarations are still what call C; only their names moved ;; aside, to -raw, via the `name` lines in `bindings`. Nothing about the C ;; signature was wrong, so hand-writing it would have taken the generated ;; half's agreement-by-construction with the header and given nothing back. ;; ;; get-key-pressed answers an i32 and not a Key. A Key is a *closed* set the ;; package names a subset of, and this queue reports every key on the ;; keyboard including the ones no member covers, so the enum would be a ;; promise the value does not keep. Comparing the answer against `:key-space` ;; would be the reason to want it, and that is what key-pressed? is for. (defn get-key-pressed [] (Option i32) (let [k (get-key-pressed-raw)] (if (= k 0) None (Some k)))) ;; Unicode codepoint, not a byte: raylib decodes the platform's input, so a ;; value above 127 is a real codepoint and not the first byte of one. (defn get-char-pressed [] (Option i32) (let [c (get-char-pressed-raw)] (if (= c 0) None (Some c)))) (declare-c get-mouse-position [] Vector2 "GetMousePosition") ;; One notch of the wheel is 1.0 and there are no fractional notches on an ;; ordinary mouse, but it is a float because a trackpad's two-finger scroll ;; is continuous. It is the DELTA since the last frame, not an accumulated ;; position, so it reads 0.0 on every frame the wheel did not move — which is ;; why a caller that wants a running total keeps one itself. (declare-c get-mouse-wheel-move [] f32 "GetMouseWheelMove") ;; The cursor's visibility is window state and not input, but it is read and ;; written by the same code that reads the mouse, so it sits here. ;; hide-cursor only hides it; it does not lock it to the window, which is ;; what raylib's separate DisableCursor does. Those two — DisableCursor and ;; EnableCursor — are in the generated half rather than here: they take no ;; arguments at all, so there is no Flan face for a hand-written line to ;; improve, which is the same rule that leaves GetMouseX there. ;; examples/core-3d-picking.flan toggles them from the right mouse button. (declare-c show-cursor [] "ShowCursor") (declare-c hide-cursor [] "HideCursor") (declare-c cursor-hidden? [] bool "IsCursorHidden") ;; The shape the pointer takes. raylib's SetMouseCursor says `int` and means ;; one of these eleven, so the Flan face is the enum for the same reason ;; set-exit-key takes a Key: the call is made every frame from a hover test, ;; and `(rl/set-mouse-cursor :cursor-ibeam)` is checked against the members ;; where an i32 would take any number at all — including the one off-by-one ;; that picks the arrow instead of the I-beam and looks like nothing at all ;; went wrong. ;; ;; All eleven are here and not a subset, unlike Key: the enum is closed and ;; eleven members is the whole of it. (defenum MouseCursor [cursor-default 0 cursor-arrow 1 cursor-ibeam 2 cursor-crosshair 3 cursor-pointing-hand 4 cursor-resize-ew 5 cursor-resize-ns 6 cursor-resize-nwse 7 cursor-resize-nesw 8 cursor-resize-all 9 cursor-not-allowed 10]) (declare-c set-mouse-cursor [cursor MouseCursor] "SetMouseCursor") ;; ── Colours ───────────────────────────────────────────────────────── ;; ;; A Color is four bytes in RGBA order, so it is *not* the little-endian ;; reading of the packed 0xRRGGBBAA integer — that is why get-color is a real ;; call and not a reinterpretation. (declare-c get-color [hex u32] Color "GetColor") ;; The same colour at a different alpha: raylib multiplies `a` by the factor ;; and leaves r, g and b alone. It is NOT a blend against a background, so a ;; faded colour still needs something drawn behind it to fade against. Bound ;; because the gesture examples draw every overlay through it, and it is the ;; only call in the file that takes a Color and answers one — which makes it ;; the shortest statement anywhere that the Color crossing works in both ;; directions at once. (declare-c fade [color Color alpha f32] Color "Fade") (defconst black (Color {.r 0 .g 0 .b 0 .a 255})) (defconst white (Color {.r 255 .g 255 .b 255 .a 255})) ;; raylib's own named palette, from raylib.h's CLITERAL macros. These are the ;; only entries in this file that are not a function, a layout or an enum, and ;; they are here for the reason the two above already were: every raylib ;; example is written in terms of them, so without them a port is a wall of ;; hex that cannot be diffed against the C it came from. They are values and ;; not calls — a Color is four bytes with no packing question — so unlike ;; get-color they cost nothing at run time and need no window. (defconst lightgray (Color {.r 200 .g 200 .b 200 .a 255})) (defconst gray (Color {.r 130 .g 130 .b 130 .a 255})) (defconst darkgray (Color {.r 80 .g 80 .b 80 .a 255})) (defconst yellow (Color {.r 253 .g 249 .b 0 .a 255})) (defconst gold (Color {.r 255 .g 203 .b 0 .a 255})) (defconst orange (Color {.r 255 .g 161 .b 0 .a 255})) (defconst pink (Color {.r 255 .g 109 .b 194 .a 255})) (defconst red (Color {.r 230 .g 41 .b 55 .a 255})) (defconst maroon (Color {.r 190 .g 33 .b 55 .a 255})) (defconst green (Color {.r 0 .g 228 .b 48 .a 255})) (defconst lime (Color {.r 0 .g 158 .b 47 .a 255})) (defconst darkgreen (Color {.r 0 .g 117 .b 44 .a 255})) (defconst skyblue (Color {.r 102 .g 191 .b 255 .a 255})) (defconst blue (Color {.r 0 .g 121 .b 241 .a 255})) (defconst darkblue (Color {.r 0 .g 82 .b 172 .a 255})) (defconst purple (Color {.r 200 .g 122 .b 255 .a 255})) (defconst violet (Color {.r 135 .g 60 .b 190 .a 255})) (defconst darkpurple (Color {.r 112 .g 31 .b 126 .a 255})) (defconst beige (Color {.r 211 .g 176 .b 131 .a 255})) (defconst brown (Color {.r 127 .g 106 .b 79 .a 255})) (defconst darkbrown (Color {.r 76 .g 63 .b 47 .a 255})) (defconst magenta (Color {.r 255 .g 0 .b 255 .a 255})) ;; Alpha 0, so it is invisible rather than a colour — raylib's own name for it. (defconst blank (Color {.r 0 .g 0 .b 0 .a 0})) ;; raylib's off-white background, which is what every example clears to. (defconst raywhite (Color {.r 245 .g 245 .b 245 .a 255})) ;; ── Drawing ───────────────────────────────────────────────────────── (declare-c begin-drawing [] "BeginDrawing") (declare-c end-drawing [] "EndDrawing") (declare-c draw-fps [x i32 y i32] "DrawFPS") (declare-c clear-background [color Color] "ClearBackground") (declare-c draw-rectangle [x i32 y i32 width i32 height i32 color Color] "DrawRectangle") ;; Scissor: everything drawn between these two is clipped to the rectangle, ;; in screen pixels with y down from the top — which is NOT the GL convention ;; underneath, and raylib flips it for you. It is drawing state like ;; begin-mode-2d is, so the pair is hand-written for the same reason: both ;; halves of a begin/end pair sit together, and both are on a frame's path. ;; ;; Nothing headless can assert this. A clip that is off by the window height ;; — the flip not applied — draws a perfectly plausible picture in the wrong ;; half of the screen, and only looking catches it. (declare-c begin-scissor-mode [x i32 y i32 width i32 height i32] "BeginScissorMode") (declare-c end-scissor-mode [] "EndScissorMode") ;; ── 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-c set-shapes-texture [texture Texture2D source Rectangle] "SetShapesTexture") (declare-c get-shapes-texture [] Texture2D "GetShapesTexture") (declare-c get-shapes-texture-rectangle [] Rectangle "GetShapesTextureRectangle") ;; ── Camera2D ──────────────────────────────────────────────────────── ;; ;; The 2D camera: everything drawn between begin-mode-2d and end-mode-2d is ;; transformed by it. `offset` is where the camera's target lands on screen — ;; half the window size is what centres it — `target` is the world point that ;; goes there, and rotation is in degrees. ;; ;; `zoom` of 0 makes the transform singular and both conversions below hand ;; back NaN rather than failing. raylib does not guard it and neither does ;; this; 1.0 is the identity and a fresh (Camera2D {}) is therefore NOT usable ;; as one — it has to be given a zoom. (defstruct Camera2D [offset Vector2 target Vector2 rotation f32 zoom f32]) (declare-c begin-mode-2d [camera Camera2D] "BeginMode2D") (declare-c end-mode-2d [] "EndMode2D") ;; The two conversions are pure arithmetic over every field of the camera, so ;; unlike the rest of the camera they run with no window and no GL context. ;; That is what the acceptance table uses to pin Camera2D's layout, and — via ;; a rotated camera, which is the only call here that mixes x into y — it is ;; also the only thing that pins Vector2's two fields against each other. (declare-c get-screen-to-world-2d [position Vector2 camera Camera2D] Vector2 "GetScreenToWorld2D") (declare-c get-world-to-screen-2d [position Vector2 camera Camera2D] Vector2 "GetWorldToScreen2D") ;; ── Camera3D ──────────────────────────────────────────────────────── ;; ;; raylib's `Camera` is a typedef for this, and every 3D call takes it by ;; value: position, the point it looks at, an up vector, a vertical ;; field-of-view in degrees, and which projection to build. Field order is ;; read off raylib.h 5.5 and is position/target/up/fovy/projection. ;; ;; What nothing here can pin, said the way the Texture2D note above says it: ;; the first three fields are the same type and the same size, so a permuted ;; Camera3D has the identical layout and every acceptance case that could ;; exist would pass. A camera that looks from the wrong place is a picture, ;; not a number. ;; ;; `projection` is `int` in the header and `CameraProjection` here, which is ;; the same four bytes with a face on it: a Flan defenum lowers to int32_t in ;; a struct field exactly as it does in a parameter, and the layout check ;; knows that, so it accepts an enum where the header says `int` and still ;; refuses an `f64` where the header says `float`. What it buys is at the ;; construction site — `.projection :projection-perspective` resolves against ;; the members below and a typo is a compile error there, where an i32 field ;; would have taken any number at all. ;; ;; `projection-` and not `camera-`, though both map onto raylib's CAMERA_: ;; the two enums are different questions asked of the same struct, and ;; :projection-perspective beside :camera-orbital says which one is being ;; answered where a shared prefix would not. (defenum CameraProjection [projection-perspective 0 projection-orthographic 1]) ;; UpdateCamera's mode. :camera-custom means it does nothing and the program ;; moves the camera itself. (defenum CameraMode [camera-custom 0 camera-free 1 camera-orbital 2 camera-first-person 3 camera-third-person 4]) (defstruct Camera3D [position Vector3 target Vector3 up Vector3 fovy f32 projection CameraProjection]) ;; The mode's built-in controls, applied to the camera in place — hence ;; (Ptr Camera3D), on the rule the Images section states: a call that mutates ;; takes a pointer at the Flan face. It reads the mouse and the keyboard ;; itself, which is what makes it a per-frame call and not a setup one. (declare-c update-camera [camera (Ptr Camera3D) mode CameraMode] "UpdateCamera") (declare-c begin-mode-3d [camera Camera3D] "BeginMode3D") (declare-c end-mode-3d [] "EndMode3D") ;; Where a world point lands on the screen, for the current camera and the ;; current window size. Unlike get-world-to-screen-2d this one is not pure ;; arithmetic over its arguments — it reads the window's dimensions — so it ;; needs a window, and headless it answers about a 0x0 screen. (declare-c get-world-to-screen [position Vector3 camera Camera3D] Vector2 "GetWorldToScreen") ;; The three 3D draws the examples use. All of them are immediate-mode ;; geometry and all of them need a GL context, so like the shapes above they ;; are link-checked and nothing more. (declare-c draw-cube [position Vector3 width f32 height f32 length f32 color Color] "DrawCube") (declare-c draw-cube-wires [position Vector3 width f32 height f32 length f32 color Color] "DrawCubeWires") (declare-c draw-cube-v [position Vector3 size Vector3 color Color] "DrawCubeV") ;; DrawSphere is DrawSphereEx with rings and slices fixed at 16; the wires ;; form takes them because the wireframe is the only place the tessellation is ;; visible. Both are here rather than generated for the reason above: they are ;; inside a frame, in an example that ships in examples/. (declare-c draw-sphere [center Vector3 radius f32 color Color] "DrawSphere") (declare-c draw-sphere-wires [center Vector3 radius f32 rings i32 slices i32 color Color] "DrawSphereWires") ;; `slices` squares each way from the origin, `spacing` world units apart, on ;; the XZ plane. (declare-c draw-grid [slices i32 spacing f32] "DrawGrid") ;; ── Rays and boxes ────────────────────────────────────────────────── ;; ;; Three small aggregates, added together because the picking call produces ;; all three: a Ray goes in, a BoundingBox says what to test it against, and ;; a RayCollision comes back. Every field is read off raylib.h 5.5 and every ;; one of them is a Vector3 or a scalar — there is no owned memory anywhere ;; here, which is exactly what separates these three from Model and Mesh, and ;; why these could be described and those still cannot. ;; ;; What the layout check can and cannot say about them, on the same rule the ;; Camera3D note states: BoundingBox's two Vector3s are the same type, so a ;; permuted BoundingBox has the identical layout and nothing would catch it. ;; RayCollision is the opposite and is the one worth having checked — `hit` is ;; a C `bool`, one byte, and `distance` is a float, so the three padding bytes ;; between them are a real claim about the struct that a permutation breaks. (defstruct BoundingBox [min Vector3 max Vector3]) (defstruct Ray [position Vector3 direction Vector3]) (defstruct RayCollision [hit bool distance f32 point Vector3 normal Vector3]) ;; The inverse of get-world-to-screen above, and hand-written beside it for ;; the reason stated there: the two read the same camera and the same window ;; dimensions, so they belong to the same half of the file. raylib 5.5 keeps ;; GetMouseRay as a #define onto this name; the define is not a symbol and ;; there is nothing to bind it to. (declare-c get-screen-to-world-ray [position Vector2 camera Camera3D] Ray "GetScreenToWorldRay") ;; Per-frame and inside BeginMode3D, like the cube draws. raylib draws the ;; ray as a line a thousand units long, so it is a debugging aid and not ;; geometry with an end. (declare-c draw-ray [ray Ray color Color] "DrawRay") ;; ── 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-c get-collision-rec [a Rectangle b Rectangle] Rectangle "GetCollisionRec") ;; ── Collision ─────────────────────────────────────────────────────── ;; ;; All of these are pure geometry: no window, no GL context, no state. That ;; makes them the other half of what the acceptance table can assert, and the ;; only part of the 2D surface that is tested as thoroughly as it is bound. ;; ;; Each is declared exactly as raylib declares it. The pointers and the copies ;; the crossing needs — a parameter is not an assignable place ;; (spec-memory.md), so a struct argument has no address to take without one — ;; are in the generated halves and not here. (declare-c collision-recs? [a Rectangle b Rectangle] bool "CheckCollisionRecs") (declare-c collision-circles? [c1 Vector2 r1 f32 c2 Vector2 r2 f32] bool "CheckCollisionCircles") (declare-c collision-circle-rec? [center Vector2 radius f32 rec Rectangle] bool "CheckCollisionCircleRec") (declare-c collision-circle-line? [center Vector2 radius f32 p1 Vector2 p2 Vector2] bool "CheckCollisionCircleLine") (declare-c collision-point-rec? [point Vector2 rec Rectangle] bool "CheckCollisionPointRec") (declare-c collision-point-circle? [point Vector2 center Vector2 radius f32] bool "CheckCollisionPointCircle") (declare-c collision-point-triangle? [point Vector2 a Vector2 b Vector2 c Vector2] bool "CheckCollisionPointTriangle") ;; `threshold` is in pixels, and it is not optional in practice: raylib's test ;; is a distance comparison in floats, so a point exactly on the line fails at ;; a threshold of 0. 1 is the useful smallest value. (declare-c collision-point-line? [point Vector2 p1 Vector2 p2 Vector2 threshold i32] bool "CheckCollisionPointLine") ;; The one binding whose Flan face is not raylib's, and one of only two in ;; this file with a hand-written wrapper on top. A Flan slice crosses as ;; ptr+len with an i64 length; raylib wants a pointer and an `int` count, and ;; the generator refuses to guess what integer type a C count parameter is — ;; so the declaration says (Ptr Vector2) and a count, and the wrapper takes ;; the slice apart. The polygon is not closed explicitly; raylib joins the ;; last point to the first. ;; ;; Empty is answered here rather than passed on: (at points 0) would be an ;; out-of-bounds read, and raylib answers false for a polygon with no points ;; anyway. (declare-c collision-point-poly?-raw [point Vector2 points (Ptr Vector2) count i32] bool "CheckCollisionPointPoly") (defn collision-point-poly? [point Vector2 points [Vector2]] bool (if (= (len points) 0) false (collision-point-poly?-raw point (addr (at points 0)) (len points)))) ;; The one that answers with more than yes or no: where the two segments meet. ;; None is "they do not", so the point cannot be read when there isn't one — ;; raylib's own signature leaves the out-parameter untouched in that case and ;; a caller that forgets reads whatever was there. (declare-c collision-lines-raw [a1 Vector2 a2 Vector2 b1 Vector2 b2 Vector2 out (Ptr Vector2)] bool "CheckCollisionLines") (defn collision-lines [a1 Vector2 a2 Vector2 b1 Vector2 b2 Vector2] (Option Vector2) (let [out (Vector2 {})] (if (collision-lines-raw a1 a2 b1 b2 (addr out)) (Some out) None))) ;; ── 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-c load-texture [path string] Texture2D "LoadTexture") (declare-c texture-valid? [texture Texture2D] bool "IsTextureValid") (declare-c unload-texture [texture Texture2D] "UnloadTexture") ;; Refilling a texture's pixels from a Color buffer. ;; ;; UpdateTexture takes a `const void *` — raylib does not care what the buffer ;; is an array *of*, only that it is the right number of bytes in the format ;; the texture was made with. The importer has to render that as *something* ;; and renders it `(Ptr u8)`, which is the generated `update-texture` above. ;; ;; That face cannot be the only one, because the commonest source of pixels is ;; LoadImageColors and it answers a `(Ptr Color)`. It also cannot be replaced ;; by a `(Ptr Color)` one: `(.data im-copy)` is already a `(Ptr u8)` over the ;; same bytes and is the shorter route, and `(Ptr u8)` → `(Ptr Color)` is not ;; expressible while `(Ptr Color)` → `(Ptr u8)` is. So the byte face is the ;; declaration and the typed face is this. ;; ;; A second `declare-c` was the obvious shape and lib/shim.ml refuses it, for a ;; reason that is right: a shim emits one C prototype per declaration, and two ;; prototypes for one symbol that disagree about a parameter type is a C file ;; that does not compile. Its message says what to write instead — "another ;; Flan name for it is a defn" — and this is that defn. docs/PORTING.md §A.2 ;; expected the header check to be the only thing in the way; it was not. ;; ;; `(addr (.r pixels))` is the whole of the conversion. `.` auto-derefs one ;; level and `r` is Color's first field, so this is the address the pointer ;; already held, said in the only way the language has of saying it. It used ;; to be written at the call site as ;; `(addr (.r (at (slice-from-ptr pixels n) 0)))`, which is the same address ;; with a slice built and indexed on the way past. Once, here, with a name on ;; it is better than once per caller. ;; ;; Neither call checks that the buffer is big enough, because neither can: the ;; length raylib wants is width * height * bytes-per-pixel of the *texture*, ;; and a pointer has no length. That is the deal every raylib pointer ;; parameter offers. (defn update-texture-colors [texture Texture2D pixels (Ptr Color)] () (update-texture texture (addr (.r pixels)))) ;; How a texture is sampled when it is drawn at anything other than its own ;; size. The header says `int` on SetTextureFilter and means one of these six. ;; ;; The value of the enum face here is not a typo caught at the call site so ;; much as a *readable* one: the fog-of-war example renders its fog into a ;; 25x15 render texture and scales it to 800x450, and the entire visual point ;; of the example is that `:filter-bilinear` smooths the tile edges where the ;; default `:filter-point` would show 32-pixel squares. A bare `1` in that ;; call says nothing; the member name says the whole thing. ;; ;; TEXTURE_FILTER_ANISOTROPIC_* are the mipmapped modes and need a texture ;; with mipmaps generated, which nothing here makes — they are listed because ;; the enum is closed, not because anything calls them. (defenum TextureFilter [filter-point 0 filter-bilinear 1 filter-trilinear 2 filter-anisotropic-4x 3 filter-anisotropic-8x 4 filter-anisotropic-16x 5]) (declare-c set-texture-filter [texture Texture2D filter TextureFilter] "SetTextureFilter") (declare-c draw-texture [texture Texture2D x i32 y i32 tint Color] "DrawTexture") (declare-c draw-texture-v [texture Texture2D position Vector2 tint Color] "DrawTextureV") (declare-c draw-texture-ex [texture Texture2D position Vector2 rotation f32 scale f32 tint Color] "DrawTextureEx") ;; 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-c draw-texture-rec [texture Texture2D source Rectangle position Vector2 tint Color] "DrawTextureRec") ;; The two above, in one call, and the only one of the four that both takes a ;; source rectangle and scales: `source` picks a cell out of an atlas, `dest` ;; says where on the screen it lands and how big, so a 16px tile drawn at 4x is ;; a dest four times the source. draw-texture-rec has the source and no scale; ;; draw-texture-ex has the scale and no source. Neither half is usable alone ;; for a tilemap, which is why this is the draw call a grid-based game makes ;; every frame and for every tile. ;; ;; `origin` is the point within *dest* that lands on dest's x,y and that ;; `rotation` (degrees, clockwise) turns about — {0 0} draws from the corner, ;; and half the dest size spins a tile about its middle. A negative source ;; width or height flips, the same as in draw-texture-rec. (declare-c draw-texture-pro [texture Texture2D source Rectangle dest Rectangle origin Vector2 rotation f32 tint Color] "DrawTexturePro") ;; ── 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]) ;; The codes that `format` field carries, named. The header says `int format` ;; everywhere one is passed, and there is nothing in an `int` to say that 7 is ;; the one a texture upload requires — which is exactly the trade ;; TextureFilter and MouseCursor already made, and the reason this is a ;; defenum rather than a row of defconsts. ;; ;; The set is closed and is here whole, compressed members included, because a ;; subset would put the hole where the next caller looks: `format` is a field ;; programs *read* off an Image they did not make, and a loaded .ktx or .dds ;; answers with one of the compressed codes. Nothing here converts to one — ;; raylib's own ImageFormat only moves between the uncompressed formats — so ;; the compressed half is for reading rather than for asking. ;; ;; `pixel-uncompressed-r8g8b8a8` is 7, the one GenImageColor makes and the one ;; LoadTextureFromImage and UpdateTexture want. The `x` in the two ASTC names ;; is lowercase in raylib.h where every other letter in that enum is upper, so ;; those two are mapped by name in `bindings` — the same narrow exception ;; GESTURE_DOUBLETAP already has, and for the same reason. (defenum PixelFormat [pixel-uncompressed-grayscale 1 pixel-uncompressed-gray-alpha 2 pixel-uncompressed-r5g6b5 3 pixel-uncompressed-r8g8b8 4 pixel-uncompressed-r5g5b5a1 5 pixel-uncompressed-r4g4b4a4 6 pixel-uncompressed-r8g8b8a8 7 pixel-uncompressed-r32 8 pixel-uncompressed-r32g32b32 9 pixel-uncompressed-r32g32b32a32 10 pixel-uncompressed-r16 11 pixel-uncompressed-r16g16b16 12 pixel-uncompressed-r16g16b16a16 13 pixel-compressed-dxt1-rgb 14 pixel-compressed-dxt1-rgba 15 pixel-compressed-dxt3-rgba 16 pixel-compressed-dxt5-rgba 17 pixel-compressed-etc1-rgb 18 pixel-compressed-etc2-rgb 19 pixel-compressed-etc2-eac-rgba 20 pixel-compressed-pvrt-rgb 21 pixel-compressed-pvrt-rgba 22 pixel-compressed-astc-4x4-rgba 23 pixel-compressed-astc-8x8-rgba 24]) ;; Reformats the pixels in place, reallocating the buffer, so the Image's ;; `data`, `format` and — for a compressed source — its size all change under ;; the caller. Hand-written rather than generated for the enum: the header's ;; `int newFormat` takes any integer at all and only one of twenty-four is the ;; conversion a given program meant. (declare-c image-format [image (Ptr Image) new-format PixelFormat] "ImageFormat") (declare-c load-image [path string] Image "LoadImage") ;; raylib 5.5 spells this IsImageValid. IsImageReady, which older code calls, ;; does not exist here — the same rename that took IsTextureReady. (declare-c image-valid? [image Image] bool "IsImageValid") ;; The same decode, from bytes already in memory rather than from a path. This ;; is what an (embed "brush.png") is for, and it is the only route to a texture ;; on a target with no filesystem: a bare relative path has no meaning in a ;; browser, so LoadImage there opens nothing and hands back an image with a ;; null buffer. ;; ;; `fileType` is the extension *with* the dot — ".png" — because that is what ;; raylib compares against (rtextures.c, strcmp(fileType, ".png")). It is how ;; the decoder is chosen; there is no sniffing of the bytes. ;; ;; Declared with (Ptr u8) and an explicit count for the reason ;; collision-point-poly?-raw is: a slice crosses as ptr+len with an i64 length, ;; raylib wants a pointer and an `int`, and the shim generator refuses to guess ;; which integer type a C count parameter is. The Flan wrapper below takes the ;; slice apart, which is where that idiom lives everywhere else in this file. (declare-c load-image-from-memory-raw [file-type string file-data (Ptr u8) data-size i32] Image "LoadImageFromMemory") ;; Empty is answered here rather than passed on, exactly as in ;; collision-point-poly?: (at data 0) on an empty slice is an out-of-bounds ;; read, and raylib's own answer to a zero-length buffer is an image with a ;; null buffer — which is what a zeroed one already is. image-valid? reports ;; false for it either way, so a caller that checks sees the same thing. (defonce no-image Image) (defn load-image-from-memory [file-type string data [u8]] Image (if (= (len data) 0) no-image (load-image-from-memory-raw file-type (addr (at data 0)) (len data)))) ;; 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-c unload-image [image Image] "UnloadImage") ;; The format is taken from the path's extension, so ".png" writes a PNG. ;; False means it could not be written. (declare-c export-image [image Image path string] bool "ExportImage") (declare-c gen-image-color [width i32 height i32 color Color] Image "GenImageColor") ;; 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-c image-resize [image (Ptr Image) width i32 height i32] "ImageResize") (declare-c image-resize-nn [image (Ptr Image) width i32 height i32] "ImageResizeNN") (declare-c image-crop [image (Ptr Image) crop Rectangle] "ImageCrop") ;; The non-mutating form of the line above, and the reason it is worth having ;; both: image-crop changes the image it is given, so carving a sheet into ;; twenty tiles with it destroys the sheet on the first one. This returns a ;; fresh Image and leaves the original alone. A rec covering the whole image ;; duplicates it, which is what this was originally reached for — though ;; image-copy in the generated half says that in one argument and is the ;; better call for it. (An earlier comment here said 5.5 had no ImageCopy. It ;; does — raylib-5.5.h line 1348 — and generated.flan has bound it all along.) ;; ;; The result owns its own buffer: unload-image it, like anything else that ;; allocated. (declare-c image-from-image [image Image rec Rectangle] Image "ImageFromImage") (declare-c image-flip-horizontal [image (Ptr Image)] "ImageFlipHorizontal") (declare-c image-flip-vertical [image (Ptr Image)] "ImageFlipVertical") (declare-c image-draw-pixel [image (Ptr Image) x i32 y i32 color Color] "ImageDrawPixel") ;; 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-c get-image-color [image Image x i32 y i32] Color "GetImageColor") ;; 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-c load-texture-from-image [image Image] Texture2D "LoadTextureFromImage") ;; ── 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-c draw-pixel [x i32 y i32 color Color] "DrawPixel") (declare-c draw-pixel-v [position Vector2 color Color] "DrawPixelV") (declare-c draw-line [x1 i32 y1 i32 x2 i32 y2 i32 color Color] "DrawLine") (declare-c draw-line-v [start Vector2 end Vector2 color Color] "DrawLineV") ;; 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-c draw-line-ex [start Vector2 end Vector2 thick f32 color Color] "DrawLineEx") (declare-c draw-circle [x i32 y i32 radius f32 color Color] "DrawCircle") (declare-c draw-circle-v [center Vector2 radius f32 color Color] "DrawCircleV") (declare-c draw-circle-lines [x i32 y i32 radius f32 color Color] "DrawCircleLines") (declare-c draw-circle-lines-v [center Vector2 radius f32 color Color] "DrawCircleLinesV") ;; Two radii, horizontal then vertical. Equal radii is a circle, so a binding ;; that exchanged them would be invisible unless they differ — which is why ;; sand.flan's ellipse is deliberately wider than it is tall. (declare-c draw-ellipse [x i32 y i32 radius-h f32 radius-v f32 color Color] "DrawEllipse") (declare-c draw-ellipse-lines [x i32 y i32 radius-h f32 radius-v f32 color Color] "DrawEllipseLines") ;; 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-c draw-ring [center Vector2 inner f32 outer f32 start f32 end f32 segments i32 color Color] "DrawRing") (declare-c draw-ring-lines [center Vector2 inner f32 outer f32 start f32 end f32 segments i32 color Color] "DrawRingLines") ;; Counter-clockwise, and raylib means it: the clockwise winding is culled and ;; draws nothing at all, which looks exactly like a broken binding. (declare-c draw-triangle [v1 Vector2 v2 Vector2 v3 Vector2 color Color] "DrawTriangle") (declare-c draw-triangle-lines [v1 Vector2 v2 Vector2 v3 Vector2 color Color] "DrawTriangleLines") (declare-c draw-rectangle-v [position Vector2 size Vector2 color Color] "DrawRectangleV") (declare-c draw-rectangle-rec [rec Rectangle color Color] "DrawRectangleRec") (declare-c draw-rectangle-lines [x i32 y i32 width i32 height i32 color Color] "DrawRectangleLines") ;; 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-c draw-rectangle-lines-ex [rec Rectangle thick f32 color Color] "DrawRectangleLinesEx") ;; `roundness` is 0 to 1 as a fraction of the shorter side, so 0 is a plain ;; rectangle and 1 is a stadium. (declare-c draw-rectangle-rounded [rec Rectangle roundness f32 segments i32 color Color] "DrawRectangleRounded") ;; No thickness here — see the section note. The `-ex` form below is the one ;; that takes it. (declare-c draw-rectangle-rounded-lines [rec Rectangle roundness f32 segments i32 color Color] "DrawRectangleRoundedLines") (declare-c draw-rectangle-rounded-lines-ex [rec Rectangle roundness f32 segments i32 thick f32 color Color] "DrawRectangleRoundedLinesEx") ;; ── A slice where raylib wants a pointer and a count ───────────────── ;; ;; Eleven entry points take an array of vectors as a pointer plus an `int` ;; count. A Flan slice already carries both, so every call site that does not ;; go through a wrapper has to take the slice apart itself — `(addr (at pts ;; 0))` and `(len pts)`, twice, in the right order — and the compiler cannot ;; check that the two halves came from the same slice. The wrapper is where ;; that idiom lives, which is the rule collision-point-poly? set. ;; ;; It also guards the empty case, which is the part a hand-written call site ;; gets wrong rather than merely writes out. raylib takes a count of 0 and ;; draws nothing, but `(at pts 0)` on an empty slice is out of bounds before ;; raylib is ever reached: the safe call is "do not call at all", and it is ;; written once here instead of at every use. ;; ;; All eleven and not the three anybody has called. A subset would have its ;; hole exactly where the next caller looks, which is the argument this file ;; already makes about ConfigFlags, and the eleven are one family — there is ;; no line to draw between DrawSplineLinear and DrawSplineBasis that a reader ;; would predict. ;; ;; `bindings` makes the opposite argument a few lines above its own list — ;; that hand-writing the variants of a family "would widen the half that has ;; to be maintained by hand for nothing the examples ask for" — and it is ;; right there and does not reach here. That paragraph is about hand-written ;; `declare-c` lines, which are exactly the half a header change can falsify. ;; None of these eleven is one: each is a `name` directive, so the generated ;; declaration keeps the C symbol and its checked signature and gives up only ;; its Flan name. The hand-maintained half does not widen at all — what is ;; written below is Flan calling Flan, and it cannot disagree with raylib. They sit together here rather than each in its own section ;; for the same reason: the justification above is one argument about a shape ;; that cuts across Shapes, Images and 3D, and splitting the family would ;; mean writing it three times or leaving two thirds of it unexplained. ;; ;; Each -raw below is a generated declaration whose name moved aside; see the ;; `name` lines at the foot of `bindings`. (defn draw-line-strip [points [Vector2] color Color] () (when (> (len points) 0) (draw-line-strip-raw (addr (at points 0)) (len points) color))) (defn draw-triangle-fan [points [Vector2] color Color] () (when (> (len points) 0) (draw-triangle-fan-raw (addr (at points 0)) (len points) color))) (defn draw-triangle-strip [points [Vector2] color Color] () (when (> (len points) 0) (draw-triangle-strip-raw (addr (at points 0)) (len points) color))) (defn draw-triangle-strip-3d [points [Vector3] color Color] () (when (> (len points) 0) (draw-triangle-strip-3d-raw (addr (at points 0)) (len points) color))) ;; The five spline drawers. raylib reads the same point array five different ;; ways; the only difference between these wrappers is which one it calls. (defn draw-spline-linear [points [Vector2] thick f32 color Color] () (when (> (len points) 0) (draw-spline-linear-raw (addr (at points 0)) (len points) thick color))) (defn draw-spline-basis [points [Vector2] thick f32 color Color] () (when (> (len points) 0) (draw-spline-basis-raw (addr (at points 0)) (len points) thick color))) (defn draw-spline-catmull-rom [points [Vector2] thick f32 color Color] () (when (> (len points) 0) (draw-spline-catmull-rom-raw (addr (at points 0)) (len points) thick color))) (defn draw-spline-bezier-quadratic [points [Vector2] thick f32 color Color] () (when (> (len points) 0) (draw-spline-bezier-quadratic-raw (addr (at points 0)) (len points) thick color))) (defn draw-spline-bezier-cubic [points [Vector2] thick f32 color Color] () (when (> (len points) 0) (draw-spline-bezier-cubic-raw (addr (at points 0)) (len points) thick color))) ;; The same two into an Image rather than the frame. `dst` stays a pointer: ;; it is the thing being written, not an array, and raylib's convention for ;; an in-place Image is the whole Image* family in this file. (defn image-draw-triangle-fan [dst (Ptr Image) points [Vector2] color Color] () (when (> (len points) 0) (image-draw-triangle-fan-raw dst (addr (at points 0)) (len points) color))) (defn image-draw-triangle-strip [dst (Ptr Image) points [Vector2] color Color] () (when (> (len points) 0) (image-draw-triangle-strip-raw dst (addr (at points 0)) (len points) color))) ;; ── 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. ;; ;; Fonts ARE bound now — see the section at the end of this file. The reason ;; they were not is worth keeping: a Font is three ints beside a Texture2D, a ;; Rectangle* and a GlyphInfo*, and 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 generator takes all of that now, and the ;; headless test turned out to exist after all. (declare-c draw-text [text string x i32 y i32 font-size i32 color Color] "DrawText") (declare-c measure-text [text string font-size i32] i32 "MeasureText") ;; ── 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-c get-frame-time [] f32 "GetFrameTime") ;; The measured rate, as an integer, which is not 1/get-frame-time: raylib ;; averages the last handful of frames so the read-out does not flicker. ;; draw-fps was already bound and puts the same number on the screen itself; ;; this is the one that hands it back, for a program that wants to compare it ;; against a target it set. (declare-c get-fps [] i32 "GetFPS") (declare-c get-time [] f64 "GetTime") (declare-c get-screen-width [] i32 "GetScreenWidth") (declare-c get-screen-height [] i32 "GetScreenHeight") ;; ── Gamepads ──────────────────────────────────────────────────────── ;; ;; Nothing here can be asserted headlessly and nothing here can be asserted ;; *at all* without a pad plugged in: with no gamepad, gamepad-available? is ;; false, every button predicate is false and every axis reads 0.0, which is ;; also exactly what a wrapper with its two int arguments exchanged would ;; report. So these are bound, wired into sand.flan's HUD, and honestly ;; described as untested — the only check they get is that a pad moves the ;; read-out. ;; ;; `pad` is an index from 0, not an enum: raylib's own parameter is an int and ;; how many are attached is a run-time question. ;; `button-` and `axis-` rather than a shared `gamepad-` stem. The two enums ;; are never in the same position, the shorter prefix is the one that keeps ;; :button-left-face-up and :axis-left-trigger readable, and `gamepad-` on ;; both would have said the part the surrounding call already says. (defenum GamepadButton [button-unknown 0 button-left-face-up 1 button-left-face-right 2 button-left-face-down 3 button-left-face-left 4 button-right-face-up 5 button-right-face-right 6 button-right-face-down 7 button-right-face-left 8 button-left-trigger-1 9 button-left-trigger-2 10 button-right-trigger-1 11 button-right-trigger-2 12 button-middle-left 13 button-middle 14 button-middle-right 15 button-left-thumb 16 button-right-thumb 17]) ;; The triggers read -1 at rest and 1 fully pressed, unlike the sticks, which ;; are centred at 0. raylib does not normalise that and neither does this. (defenum GamepadAxis [axis-left-x 0 axis-left-y 1 axis-right-x 2 axis-right-y 3 axis-left-trigger 4 axis-right-trigger 5]) (declare-c gamepad-available? [pad i32] bool "IsGamepadAvailable") (declare-c gamepad-button-pressed? [pad i32 button GamepadButton] bool "IsGamepadButtonPressed") (declare-c gamepad-button-down? [pad i32 button GamepadButton] bool "IsGamepadButtonDown") (declare-c gamepad-button-released? [pad i32 button GamepadButton] bool "IsGamepadButtonReleased") (declare-c gamepad-button-up? [pad i32 button GamepadButton] bool "IsGamepadButtonUp") ;; -1 when nothing is pressed, so the answer is not a GamepadButton: raylib ;; returns an int outside the enum and the checker would have to be lied to. (declare-c get-gamepad-button-pressed [] i32 "GetGamepadButtonPressed") (declare-c get-gamepad-axis-count [pad i32] i32 "GetGamepadAxisCount") (declare-c get-gamepad-axis-movement [pad i32 axis GamepadAxis] f32 "GetGamepadAxisMovement") ;; An INTEGER-faced version of the call above is wanted and cannot be had, and ;; the pair of refusals that stops it is worth recording here because it is ;; structural rather than incidental. ;; ;; get-gamepad-axis-count answers how many axes the pad reports, and ;; core_input_gamepad's read-out walks 0..count-1 and asks for each. That loop ;; cannot go through the binding above: the index is an i32 and ;; ;; expected rl/GamepadAxis, found i32 ;; ;; — an integer does not convert to an enum, and a keyword names exactly one ;; member so it cannot come from a loop variable either. The obvious fix, a ;; second declare-c of the same symbol with an i32 parameter, is refused too: ;; ;; rl/get-gamepad-axis-movement and rl/get-gamepad-axis-movement-by-index ;; both bind the C function GetGamepadAxisMovement — one declare-c per C ;; function, and another Flan name for it is a defn ;; ;; and a `defn` cannot help, because what has to change is the parameter's ;; TYPE and a wrapper can only rename. Nothing here is wrong: one declaration ;; per symbol is what keeps the generated prototype unique, and an enum that ;; silently accepted integers would give up what makes a keyword argument ;; checkable. The consequence is simply that an enum parameter cannot be ;; indexed, and the caller spells the loop as a cond over the members it ;; knows — which is what examples/core-input-gamepad.flan does. ;; SetGamepadVibration is NOT bound, and the reason is not the usual one. Two ;; things are wrong with it at once. Its arity changed — 5.1-dev takes three ;; floats and 6.1-dev takes four, there is no 5.5 header here to settle which, ;; and the generated prototype is what fixes the call, so a guess is a ;; corrupted stack frame rather than a link error. And it would not matter if ;; it were guessed right: the symbol in libraylib.so.550 disassembles to a ;; single TraceLog call and a jump — it is a stub that reports "not ;; implemented" and touches no motor. Binding it would be binding a warning. ;; ── Touch and gestures ────────────────────────────────────────────── ;; ;; Touch is a superset of the mouse on a desktop: with no touchscreen, ;; get-touch-point-count is 0 but get-touch-position 0 still tracks the mouse, ;; which is what makes it visible in sand.flan at all. ;; ;; The gesture system is fed by raylib's own event polling, so a gesture is ;; only ever detected inside a window's frame loop — nothing here is headless ;; material either. ;; A bitfield, not an ordinary enum: set-gestures-enabled takes the OR of ;; several and gesture-detected? tests against one. That is why the enabling ;; call below takes a u32 and not a Gesture — a keyword can only ever name one ;; member, and `gesture-all` is spelled out below so the common case still ;; reads. The defconst and the members now share the `gesture-` stem, which ;; is the prefix reading its way out of the enum and into the constant beside ;; it. (defenum Gesture [gesture-none 0 gesture-tap 1 gesture-double-tap 2 gesture-hold 4 gesture-drag 8 gesture-swipe-right 16 gesture-swipe-left 32 gesture-swipe-up 64 gesture-swipe-down 128 gesture-pinch-in 256 gesture-pinch-out 512]) (defconst gesture-all u32 1023) (declare-c get-touch-position [index i32] Vector2 "GetTouchPosition") (declare-c get-touch-x [] i32 "GetTouchX") (declare-c get-touch-y [] i32 "GetTouchY") (declare-c get-touch-point-count [] i32 "GetTouchPointCount") (declare-c get-touch-point-id [index i32] i32 "GetTouchPointId") (declare-c set-gestures-enabled [flags u32] "SetGesturesEnabled") (declare-c gesture-detected? [gesture Gesture] bool "IsGestureDetected") (declare-c get-gesture-detected [] Gesture "GetGestureDetected") ;; Degrees, and only meaningful while a drag is in progress. (declare-c get-gesture-drag-vector [] Vector2 "GetGestureDragVector") (declare-c get-gesture-drag-angle [] f32 "GetGestureDragAngle") (declare-c get-gesture-pinch-vector [] Vector2 "GetGesturePinchVector") (declare-c get-gesture-pinch-angle [] f32 "GetGesturePinchAngle") (declare-c get-gesture-hold-duration [] f32 "GetGestureHoldDuration") ;; ── Render textures ───────────────────────────────────────────────── ;; ;; A framebuffer with two textures hanging off it: draw into it between ;; begin-texture-mode and end-texture-mode, then draw *it* like any other ;; texture. That is how a post-process pass and a pixel-perfect integer ;; upscale are both done. ;; ;; None of it is assertable here — LoadRenderTexture makes a GL framebuffer ;; object, so with no context it answers an id of 0 and every draw into it is ;; a no-op. `depth` is a renderbuffer rather than a real texture in raylib's ;; default configuration, so its id is the only field of it worth reading. ;; ;; The one thing the layout gets for free: RenderTexture2D is a u32 and two ;; Texture2Ds, and Texture2D is already pinned as far as anything headless can ;; pin it, so the only new claim here is the order of the three members. ;; sand.flan draws through one, which is where a wrong order shows up. (defstruct RenderTexture2D [id u32 texture Texture2D depth Texture2D]) (declare-c load-render-texture [width i32 height i32] RenderTexture2D "LoadRenderTexture") ;; raylib 5.5 spells this IsRenderTextureValid; there is no IsRenderTextureReady ;; in this version, the same rename that took IsTextureReady and IsImageReady. (declare-c render-texture-valid? [target RenderTexture2D] bool "IsRenderTextureValid") (declare-c unload-render-texture [target RenderTexture2D] "UnloadRenderTexture") ;; Everything drawn between these two lands in the target instead of the ;; screen, and the target's texture comes out of the GPU upside down — raylib ;; renders it bottom-up — so drawing it back with a NEGATIVE source height is ;; not a flourish, it is the correction. sand.flan does exactly that. (declare-c begin-texture-mode [target RenderTexture2D] "BeginTextureMode") (declare-c end-texture-mode [] "EndTextureMode") ;; ── Audio ─────────────────────────────────────────────────────────── ;; ;; The device first, and the split that matters for testing runs right ;; through this section: a **Wave** is samples in RAM and needs no device at ;; all, while a **Sound** is a buffer the mixer owns and a **Music** is a ;; decoder feeding one, and both of those are nothing without ;; init-audio-device having succeeded. ;; ;; That makes Wave the audio equivalent of the Image family — raylib ;; *computes* with it, headlessly — and it is the only part of this section ;; the acceptance table asserts. Everything from Sound down is exercised by ;; running sand.flan with a working sound server, and a machine without one ;; gets silence rather than a crash: init-audio-device logs a warning, every ;; load answers a zeroed struct and every play is a no-op. (declare-c init-audio-device [] "InitAudioDevice") (declare-c close-audio-device [] "CloseAudioDevice") (declare-c audio-device-ready? [] bool "IsAudioDeviceReady") ;; 0 to 1, and it is a listener gain applied after every per-sound volume. (declare-c set-master-volume [volume f32] "SetMasterVolume") (declare-c get-master-volume [] f32 "GetMasterVolume") ;; A Wave is the CPU side: `data` is frame-count × channels samples of ;; sample-size bits each, and raylib reads every one of the four integers to ;; decide what those bytes mean. `data` is (Ptr u8) rather than a typed ;; pointer because its element type is `sample-size`, which is a run-time ;; number — 8, 16 or 32 bits — and there is no Flan type that says that. ;; ;; The consequence for a caller building one by hand: the bytes are written ;; as bytes, in the host's order. That is the shape the acceptance case uses, ;; and it is deliberate — it means the case says what it means about ;; little-endian 16-bit PCM instead of hiding it behind a cast. (defstruct Wave [frame-count u32 sample-rate u32 sample-size u32 channels u32 data (Ptr u8)]) (declare-c load-wave [path string] Wave "LoadWave") ;; raylib 5.5 spells this IsWaveValid; IsWaveReady is gone, as everywhere else. (declare-c wave-valid? [wave Wave] bool "IsWaveValid") (declare-c unload-wave [wave Wave] "UnloadWave") ;; The extension picks the format, and raylib writes .wav and .qoa. This is ;; external ground truth for the layout: the header it writes carries ;; sample-rate, sample-size and channels, and the payload length carries ;; frame-count, so a permuted defstruct writes a file that reads back ;; differently — the same argument the PNG round trip makes for Image. (declare-c export-wave [wave Wave path string] bool "ExportWave") ;; Allocates a copy of the buffer; the copy is unloaded on its own. (declare-c wave-copy [wave Wave] Wave "WaveCopy") ;; In FRAMES, not samples — raylib renamed the parameters for 5.5 without ;; changing the signature, so the name is the only thing that says which. On ;; a mono wave the two readings coincide, which is what the acceptance case ;; uses, so nothing here depends on having guessed right. (declare-c wave-crop [wave (Ptr Wave) init-frame i32 final-frame i32] "WaveCrop") ;; Resamples in place. This is the strongest headless shape available in this ;; section and the same one gen-image-color has: three scalars go in and four ;; fields come out, with frame-count *computed* from the sample-rate ratio, so ;; a permuted layout has nothing to cancel against. (declare-c wave-format [wave (Ptr Wave) sample-rate i32 sample-size i32 channels i32] "WaveFormat") ;; Every sample as a float in [-1, 1], frame-count × channels of them, ;; whatever the wave's own sample-size. That is the one call that reads ;; *through* `data`, so it is what pins the pointer as a pointer rather than ;; as two integers that happen to sit at the end. (declare-c load-wave-samples [wave Wave] (Ptr f32) "LoadWaveSamples") (declare-c unload-wave-samples [samples (Ptr f32)] "UnloadWaveSamples") ;; A Sound is an AudioStream plus a frame count. The two leading pointers are ;; miniaudio's and Flan never reads through them — they are (Ptr u8) so the ;; struct is the right size and the three integers land at the right offsets, ;; exactly as Image's `data` is. (defstruct AudioStream [buffer (Ptr u8) processor (Ptr u8) sample-rate u32 sample-size u32 channels u32]) (defstruct Sound [stream AudioStream frame-count u32]) (declare-c load-sound [path string] Sound "LoadSound") ;; Note what this does to the frame count: the mixer resamples to the device's ;; own rate, so a sound made from an 8 kHz wave on a 48 kHz device reports six ;; times as many frames. Nothing should read `frame-count` expecting the ;; wave's. (declare-c load-sound-from-wave [wave Wave] Sound "LoadSoundFromWave") (declare-c sound-valid? [sound Sound] bool "IsSoundValid") (declare-c unload-sound [sound Sound] "UnloadSound") (declare-c play-sound [sound Sound] "PlaySound") (declare-c stop-sound [sound Sound] "StopSound") (declare-c pause-sound [sound Sound] "PauseSound") (declare-c resume-sound [sound Sound] "ResumeSound") (declare-c sound-playing? [sound Sound] bool "IsSoundPlaying") ;; Volume is a gain from 0, pitch is a rate multiplier where 1 is unchanged, ;; and pan is 0 hard left to 1 hard right with 0.5 centred — raylib's own ;; convention, and the one place in this file where 0 is not the neutral ;; value. (declare-c set-sound-volume [sound Sound volume f32] "SetSoundVolume") (declare-c set-sound-pitch [sound Sound pitch f32] "SetSoundPitch") (declare-c set-sound-pan [sound Sound pan f32] "SetSoundPan") ;; A second voice over the same samples, so one sound can overlap itself. It ;; does NOT own the data, so unloading an alias must not unload the original — ;; which is why raylib has a separate call for it and why this one is bound. (declare-c load-sound-alias [source Sound] Sound "LoadSoundAlias") (declare-c unload-sound-alias [alias Sound] "UnloadSoundAlias") ;; Music is streamed rather than resident, which is the whole difference: the ;; buffer is refilled from the decoder and update-music-stream is what does ;; the refilling. Miss it for a frame and the music stops. (defstruct Music [stream AudioStream frame-count u32 looping bool ctx-type i32 ctx-data (Ptr u8)]) (declare-c load-music-stream [path string] Music "LoadMusicStream") (declare-c music-valid? [music Music] bool "IsMusicValid") (declare-c unload-music-stream [music Music] "UnloadMusicStream") (declare-c play-music-stream [music Music] "PlayMusicStream") ;; Called once per frame, every frame, for as long as the music is meant to ;; play. This is the one binding in the section whose absence is silent. (declare-c update-music-stream [music Music] "UpdateMusicStream") (declare-c stop-music-stream [music Music] "StopMusicStream") (declare-c pause-music-stream [music Music] "PauseMusicStream") (declare-c resume-music-stream [music Music] "ResumeMusicStream") (declare-c music-stream-playing? [music Music] bool "IsMusicStreamPlaying") (declare-c set-music-volume [music Music volume f32] "SetMusicVolume") (declare-c set-music-pitch [music Music pitch f32] "SetMusicPitch") (declare-c set-music-pan [music Music pan f32] "SetMusicPan") ;; Seconds, both of them. (declare-c seek-music-stream [music Music position f32] "SeekMusicStream") (declare-c get-music-time-length [music Music] f32 "GetMusicTimeLength") (declare-c get-music-time-played [music Music] f32 "GetMusicTimePlayed") ;; AudioStream itself — the raw callback-fed stream — is NOT bound. Its point ;; is set-audio-stream-callback, which takes a C function pointer, and a ;; callback is refused by the shim generator by name: `%s is a function type, ;; and a C callback is not implemented`. Binding the rest of the family ;; without it would be binding a stream that can only ever be fed by ;; update-audio-stream from the main thread, which is a worse Sound. ;; ── Fonts ─────────────────────────────────────────────────────────── ;; ;; A previous pass refused this whole family by name, and the reason was that ;; a Font drags in two more aggregates and two owned arrays and there was ;; nothing headless to check them against. Both halves of that have changed. ;; ;; The generator takes it: a struct held by value is emitted after everything ;; it contains, a struct held by POINTER is forward-declared, and both the ;; Flan struct and the C typedef come from the same `defstruct`. Font holds a ;; Texture2D by value and points at Rectangle and GlyphInfo; GlyphInfo holds ;; an Image by value. Nothing here needed a generator change. ;; ;; And the test exists. raylib's text measuring is pure CPU arithmetic over ;; every field of a Font — it walks the glyph array looking for a codepoint, ;; reads the advance out of the glyph or the width out of the atlas rectangle, ;; and scales by the base size. The catch was that the calls that MAKE a font ;; all need something a headless run does not have: get-font-default needs ;; init-window, load-font-ex needs a TTF on disk. So the acceptance case does ;; not make one — it *builds* one, field by field, out of Flan arrays, and ;; hands it to raylib to compute with. Scalars in, numbers out, with no input ;; struct raylib produced for a permutation to cancel against. ;; ;; One trap found while doing that, and it is in raylib rather than here: ;; MeasureTextEx returns (0,0) immediately when `texture.id` is 0. A ;; hand-built font therefore has to claim a nonzero texture id even though ;; there is no texture — which is also what makes the case pin where the ;; Texture2D sits inside the Font. ;; `image` is the glyph's own pixels, and raylib owns them; it is here so the ;; four ints in front of it are at the right offsets and so a GlyphInfo is 40 ;; bytes rather than 16. offset-x and offset-y shift the glyph when drawn; ;; advance-x is how far the pen moves after it, and when it is 0 raylib falls ;; back to the atlas rectangle's width plus offset-x. (defstruct GlyphInfo [value i32 offset-x i32 offset-y i32 advance-x i32 image Image]) ;; `recs` and `glyphs` are parallel arrays of glyph-count entries each: recs ;; says where the glyph is in the atlas texture, glyphs says what it is. A ;; codepoint raylib cannot find falls back to index 0 rather than reading out ;; of bounds. (defstruct Font [base-size i32 glyph-count i32 glyph-padding i32 texture Texture2D recs (Ptr Rectangle) glyphs (Ptr GlyphInfo)]) ;; Needs a window: the default font is loaded as part of init-window and ;; LoadFontDefault is not exported, which is the same fact that makes ;; measure-text answer 0 headless. (declare-c get-font-default [] Font "GetFontDefault") (declare-c load-font [path string] Font "LoadFont") ;; The codepoint set is a C array plus an int count, so — like ;; collision-point-poly? — the declaration says (Ptr i32) and the Flan wrapper ;; below takes a slice apart. A slice parameter in a declare-c is refused by ;; name, because the C count's own type is not recoverable from [T]. ;; ;; raylib's own convention is that a NULL pointer with a count of 0 means the ;; default ASCII set, and the wrapper keeps it — but Flan has no null pointer ;; literal, so the null comes from the one place the language does hand out ;; zeroed bytes: a `defonce` with no initialiser is BSS (plan.org, zero ;; values), and a zeroed (Ptr i32) is exactly a null one. It is never written ;; to and never read through; raylib only ever compares it against NULL. (defonce default-codepoints (Ptr i32)) (declare-c load-font-ex-raw [path string font-size i32 codepoints (Ptr i32) count i32] Font "LoadFontEx") (defn load-font-ex [path string font-size i32 codepoints [i32]] Font (if (= (len codepoints) 0) (load-font-ex-raw path font-size default-codepoints 0) (load-font-ex-raw path font-size (addr (at codepoints 0)) (len codepoints)))) ;; raylib 5.5 spells this IsFontValid. It reads the texture id and both ;; arrays, so a font that loaded but could not upload its atlas — which is ;; every font loaded without a GL context — is NOT valid by this test. (declare-c font-valid? [font Font] bool "IsFontValid") (declare-c unload-font [font Font] "UnloadFont") ;; `spacing` is extra pixels between glyphs, added per gap and not per glyph, ;; so a one-character string is unaffected by it. raylib's own DrawTextEx adds ;; it the same way measure-text-ex counts it, which is why the two agree. (declare-c draw-text-ex [font Font text string position Vector2 font-size f32 spacing f32 tint Color] "DrawTextEx") ;; Pure arithmetic over the font — no GL, no window — and therefore the one ;; thing in this section the acceptance table can assert. See the note above: ;; it refuses to measure anything at all when the font's texture id is 0. (declare-c measure-text-ex [font Font text string font-size f32 spacing f32] Vector2 "MeasureTextEx") ;; `recs` and `glyphs` are the two places in this whole package where a C ;; pointer's length is knowable and the language could not say it. Both arrays ;; hold exactly `glyph-count` entries — raylib allocates them that way in ;; LoadFontData and every one of its own loops uses that bound — and ;; `glyph-count` is a sibling *field*, which is why naming a count argument in ;; `bindings` could never have covered these two. `slice-from-ptr` can. ;; ;; **These two wrappers are where the promise is made, and they are the reason ;; the promise is safe to make**: a caller of `font-recs` is trusting raylib's ;; own invariant rather than remembering a number, and there is one place to ;; fix if raylib ever changes it. Prefer them to writing `slice-from-ptr` at a ;; call site. ;; ;; The one way to break them is to call either on a Font that was unloaded, or ;; on a zeroed one: `unload-font` frees both arrays and does not clear the ;; pointers, so the slice would be a promise about freed memory. That is the ;; ordinary use-after-free a (Ptr T) already had; the slice does not own the ;; storage and freeing through one is not expressible. (defn font-recs [font Font] [Rectangle] (slice-from-ptr (.recs font) (.glyph-count font))) (defn font-glyphs [font Font] [GlyphInfo] (slice-from-ptr (.glyphs font) (.glyph-count font))) ;; The index into `recs` and `glyphs`, by linear search over glyph-count. Also ;; pure CPU, and it is what pins glyph-count as the loop bound. (declare-c get-glyph-index [font Font codepoint i32] i32 "GetGlyphIndex") (declare-c get-glyph-info [font Font codepoint i32] GlyphInfo "GetGlyphInfo") (declare-c get-glyph-atlas-rec [font Font codepoint i32] Rectangle "GetGlyphAtlasRec") (declare-c draw-text-codepoint [font Font codepoint i32 position Vector2 font-size f32 tint Color] "DrawTextCodepoint") ;; DrawTextCodepoints and LoadFontData are not bound. The first is the slice ;; problem again and adds nothing draw-text-ex does not already do from a ;; string; the second hands back a raw GlyphInfo array whose length is the ;; caller's to remember and whose lifetime is UnloadFontData's, and Flan has ;; no owning array type to give that to — a (Ptr GlyphInfo) with a separate ;; count is what the language would force, which is the C API with the safety ;; removed rather than a binding. ;; ── One codepoint backwards ───────────────────────────────────────── ;; ;; The one entry point in raylib that reads *backwards* from the pointer it is ;; handed, and therefore the one that a Flan `string` must never reach. The ;; shim crosses a string as ptr+len in and a NUL-terminated *copy* out, so the ;; bytes in front of what C receives belong to the allocator; GetCodepointNext ;; beside it never notices, because it only reads forwards. GetCodepointPrevious ;; reads the copy's prefix — which is somebody else's memory — and answers 0, ;; which is also its answer for genuinely malformed UTF-8. Nothing in the ;; result tells the two apart. docs/PORTING.md §A.1 is the whole story. ;; ;; So the declaration says `(Ptr u8)` and means it. That is a hand-written line ;; over a `const char *` the importer would have rendered as `string`, which is ;; exactly the case lib/cimport.ml's [ptr_agrees] exists for, and the generated ;; half no longer carries the string-faced version at all — a binding that is ;; wrong for the only direction it reads in is worse than no binding. (declare-c get-codepoint-previous-raw [text (Ptr u8) codepoint-size (Ptr i32)] i32 "GetCodepointPrevious") ;; The face a caller wants: the bytes and an offset into them, rather than an ;; interior pointer they had to build. `at` bounds-checks the offset, which is ;; the one thing the raw call cannot do for itself. ;; ;; `offset` is where the *next* codepoint starts; the answer is the codepoint ;; before it and `codepoint-size` is that one's length in bytes, so the ;; previous offset is `offset` minus what comes back through the pointer. At ;; offset 0 there is nothing behind it and raylib is not asked. (defn get-codepoint-previous [text [u8] offset i32 codepoint-size (Ptr i32)] i32 (if (<= offset 0) (do (set (deref codepoint-size) 0) 0) (get-codepoint-previous-raw (addr (at text offset)) codepoint-size)))