diff --git a/.gitignore b/.gitignore index 4b6b173..67b9111 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,5 @@ old-ocaml/ /sand /conditions-play .claude/ +probe +probe.c diff --git a/vendor/raylib/raylib.flan b/vendor/raylib/raylib.flan index a2d4d9a..a406210 100644 --- a/vendor/raylib/raylib.flan +++ b/vendor/raylib/raylib.flan @@ -459,12 +459,12 @@ ;; libraylib.so.550 and is why it is NOT in the acceptance table despite ;; looking like exactly the kind of call that could be. ;; -;; Font loading is not bound, deliberately. A Font is baseSize, glyphCount and -;; glyphPadding beside a Texture2D, a Rectangle* and a GlyphInfo* — and a -;; GlyphInfo embeds an Image. Binding it means binding two more aggregates and -;; two owned arrays for something with no headless test at the end of it, so -;; load-font, load-font-ex, unload-font, get-font-default, draw-text-ex and -;; measure-text-ex are all absent rather than half-done. +;; 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] @@ -483,3 +483,410 @@ (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. + +(defenum GamepadButton + [unknown 0 + left-face-up 1 left-face-right 2 left-face-down 3 left-face-left 4 + right-face-up 5 right-face-right 6 right-face-down 7 right-face-left 8 + left-trigger-1 9 left-trigger-2 10 + right-trigger-1 11 right-trigger-2 12 + middle-left 13 middle 14 middle-right 15 + left-thumb 16 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 + [left-x 0 left-y 1 right-x 2 right-y 3 + left-trigger 4 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") + +;; 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 `all` is spelled out here so the common case still reads. +(defenum Gesture + [none 0 tap 1 double-tap 2 hold 4 drag 8 + swipe-right 16 swipe-left 32 swipe-up 64 swipe-down 128 + pinch-in 256 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 `defvar` 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. +(defvar 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") + +;; 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.