The renderer was one binding away, and the import was not allowed to be the answer
This commit is contained in:
parent
54027ca942
commit
a5fa83fe87
36
BUILT.md
36
BUILT.md
@ -3897,3 +3897,39 @@ composite renderer and ghost text want the same form, for different reasons. `fl
|
||||
things ghost text would need — overlay invalidation as the buffer is edited, and a rule for a watch inside a loop,
|
||||
which the buffer sidesteps by showing the last value written and which inline has no obvious answer that does not
|
||||
become the query UI this design exists to avoid.
|
||||
|
||||
## The four raylib lines siam-farmer needed
|
||||
|
||||
`PORTING.md` measured the author's game against the binding and found the renderer unwritable in a default build.
|
||||
Four lines closed it, and the interesting part is not the lines.
|
||||
|
||||
`draw-texture-pro` is the one that mattered. It is the only call in the package that takes both a source rectangle and
|
||||
a destination rectangle, which is what a tilemap is: `source` picks a cell out of an atlas, `dest` says where it lands
|
||||
and how big, and 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 draws a tile.
|
||||
|
||||
It *was* reachable — with `FLAN_RAYLIB_H` exported the importer brings it in with 250-odd others — and that is the
|
||||
finding worth keeping. `vendor/raylib/headers` keeps the import opt-in on purpose, so a build needs libraylib linkable
|
||||
and not raylib-devel installed. That property is worth keeping and it means **the default build had no draw call for a
|
||||
grid-based game**. The rule that follows: *a raylib function on a game's per-frame path is hand-written in
|
||||
`raylib.flan` and checked against the header; it is not left to the import.* The import widens the surface; it must
|
||||
not be load-bearing.
|
||||
|
||||
The other three: `image-from-image` (the non-mutating `image-crop` — carving a sheet into twenty tiles with
|
||||
`image-crop` destroys the sheet on the first one, and 5.5 has no `ImageCopy`), `window-ready?` (an engine's "am I
|
||||
already running" guard), and `left-shift 340` in the `Key` `defenum`. Nothing else went into the enum: it carries the
|
||||
keys that have a customer, and `PORTING.md` §5 checked every other value the game touches and found them all present.
|
||||
|
||||
**What could be tested, and what could not.** `PORTING.md` asked for an acceptance case making raylib compute with the
|
||||
source rect so a permuted `Rectangle` goes red. That cannot exist for `DrawTexturePro` — it needs a GL context, and
|
||||
`raylib.flan`'s Shapes comment already says none of the drawing calls can be in the table. So `raylib-ffi.flan` links
|
||||
it instead: the call sits behind `(when (rl/window-ready?) …)`, false headless, so the shim is generated and the
|
||||
symbol resolves at link time and the body never runs. That catches a name or an arity libraylib does not have. It does
|
||||
**not** catch the argument order, and three structs in a row is where an argument order goes wrong. Only looking at
|
||||
the screen catches that, and saying so is better than a test that implies otherwise.
|
||||
|
||||
The computed case moved to `image-from-image`, which is CPU-side and is exactly what the Images section says is
|
||||
assertable. `raylib-image.flan` carves one 6×3 sheet twice at two different `y`s and then re-reads the sheet: `x`,
|
||||
`y`, `width` and `height` are each pinned by an answer that moves if they do, and the sheet surviving both carves is
|
||||
what distinguishes this from `image-crop`. Bind it to `ImageCrop` by mistake and the second carve reads out of a 2×1
|
||||
image and the case goes red.
|
||||
|
||||
13
NEXT.md
13
NEXT.md
@ -2,13 +2,14 @@
|
||||
|
||||
Three things, in order. The first two are one line each and unblock a real game.
|
||||
|
||||
**1. `DrawTexturePro` is not bound, and it is the one true blocker for `siam-farmer`.** See `PORTING.md`, written
|
||||
against both the Clojure implementation and the WIP Common Lisp port. Every tile in both goes through it;
|
||||
`DrawTextureRec` does not scale and `DrawTextureEx` takes no source rect, so the renderer **cannot be written at all**
|
||||
in a default build. It is reachable only through the opt-in `FLAN_RAYLIB_H` import, which `vendor/raylib/headers`
|
||||
deliberately keeps optional. One `declare-c` line.
|
||||
**1. `DrawTexturePro` is not bound — DONE.** It is `draw-texture-pro` in `vendor/raylib/raylib.flan` now, hand-written
|
||||
beside `draw-texture-rec` and read off a raylib header rather than remembered. `image-from-image` and `window-ready?`
|
||||
went in with it. The rule the gap exposed is written down in `BUILT.md` and `PORTING.md` §1: *a raylib function on a
|
||||
game's per-frame path is hand-written and header-checked, not left to the opt-in import* — the import widens the
|
||||
surface and must not be load-bearing, because the default build has no `FLAN_RAYLIB_H` and still has to draw.
|
||||
|
||||
**2. `Key` has no `left-shift`.** Both implementations use shift+1..5 to pick the tilemap. One enum member.
|
||||
**2. `Key` has no `left-shift` — DONE.** `left-shift 340`, and nothing else: `PORTING.md` §5 checked every other enum
|
||||
value the game touches and they were all already right.
|
||||
|
||||
**3. An out-of-bounds index should signal a condition, not `exit(134)`.** `PORTING.md`'s own first recommendation
|
||||
after the binding lines. The game indexes grids straight from mouse coordinates — `game.lisp` had to add an
|
||||
|
||||
78
PORTING.md
78
PORTING.md
@ -27,6 +27,11 @@ hypothetical.
|
||||
|
||||
## 1. The one thing that cannot be written at all
|
||||
|
||||
> **Bound, 2026-09-13.** `draw-texture-pro`, `image-from-image`, `window-ready?` and
|
||||
> `Key/left-shift` are all hand-written in `vendor/raylib/raylib.flan` now. The section
|
||||
> is kept as it was written, because the reasoning is the part worth having; what the
|
||||
> fix cost, and what could and could not be tested, is in the box at the end of it.
|
||||
|
||||
### `DrawTexturePro` is not bound
|
||||
|
||||
Every tile in this game is drawn by one call:
|
||||
@ -77,6 +82,40 @@ And one enum member, not a function: **`raylib.flan`'s `Key` has no `left-shift`
|
||||
`KEY_LEFT_SHIFT` (340). The `defenum` carries a deliberate subset — the keys `sand.flan`
|
||||
uses — and this is one member short of what this game needs. One entry.
|
||||
|
||||
### What the four lines actually cost, and what a test could say about them
|
||||
|
||||
All four landed together: `draw-texture-pro` beside `draw-texture-rec`,
|
||||
`image-from-image` beside `image-crop`, `window-ready?` beside `window-should-close?`,
|
||||
and `left-shift 340` at the end of the `Key` `defenum`. The three signatures were read
|
||||
off a raylib header rather than remembered, and all three are unchanged across 5.1, 5.5
|
||||
and the 6.0 this game vendors.
|
||||
|
||||
**The test suggestion further down this file was wrong and is corrected here.** Tier 0
|
||||
asked for "an acceptance case that makes raylib compute with the source rect so a
|
||||
permuted `Rectangle` goes red". `DrawTexturePro` cannot have one: it needs a GL context,
|
||||
and `raylib.flan`'s own Shapes comment already says none of the drawing calls can be in
|
||||
the acceptance table. What `raylib-ffi.flan` does instead is link it — the call sits
|
||||
behind `(when (rl/window-ready?) …)`, which is false headless, so the shim is generated
|
||||
and the symbol is resolved at link time and the body never runs. That catches a name or
|
||||
an arity that does not exist in libraylib. It does **not** catch the argument order, and
|
||||
three structs in a row is exactly where an argument order goes wrong. Only looking at
|
||||
the screen catches that.
|
||||
|
||||
The computed test the suggestion wanted does exist — on `image-from-image`, which is
|
||||
CPU-side and is what that Images section comment says is assertable.
|
||||
`raylib-image.flan` carves the same 6×3 sheet twice, at two different `y`s, and then
|
||||
re-reads the sheet: the rectangle's `x`, `y`, `width` and `height` are each pinned by an
|
||||
answer that changes if they move, and the source surviving both carves is what
|
||||
distinguishes this from `image-crop`. Bind it to `ImageCrop` by mistake and the second
|
||||
carve reads out of a 2×1 image and the case goes red.
|
||||
|
||||
**And the rule Tier 0 item 4 asked for, written down:** *a raylib function on a game's
|
||||
per-frame path is hand-written in `raylib.flan` and checked against the header; it is
|
||||
not left to the opt-in import.* The import widens the surface and is worth having, but a
|
||||
build that does not have `FLAN_RAYLIB_H` set is the default build, and the default build
|
||||
has to be able to draw. The test that goes with the rule is a link check, which is cheap
|
||||
and is all a GL-context call can have.
|
||||
|
||||
---
|
||||
|
||||
## 2. What looks like a gap and is not
|
||||
@ -405,15 +444,19 @@ path; the remainder is unchecked and should not be read as verified.
|
||||
through `KEY_NINE`=57, `KEY_SPACE`=32, `KEY_E`=69, `KEY_LEFT_SHIFT`=340,
|
||||
`MOUSE_BUTTON_LEFT`/`RIGHT`/`MIDDLE`=0/1/2, and `LOG_WARNING` as the fifth
|
||||
`TraceLogLevel` member (4) are all unchanged in 6.0 and all match `raylib.flan` — except
|
||||
that `left-shift` is absent from the `Key` `defenum` entirely, which is the §1 note, not a
|
||||
skew.
|
||||
that `left-shift` was absent from the `Key` `defenum` entirely, which is the §1 note and
|
||||
not a skew. **It is `left-shift 340` there now**, and nothing else went in beside it: the
|
||||
`defenum`'s own comment says it carries the keys that have a customer, and of the
|
||||
modifier siblings only this one does. This paragraph is the record that every other enum
|
||||
value the game touches was already present and already right, so "check the surrounding
|
||||
enum for other omissions" has an answer and the answer is none.
|
||||
|
||||
So the version skew is not a correctness problem for this game today. It is still worth
|
||||
closing, because the whole point of `headers` is that a silent disagreement is the failure
|
||||
mode, and "I checked by hand once" is exactly the state `headers` was built to replace.
|
||||
|
||||
The binding's real gap against `rl.clj` is §1: `DrawTexturePro`, then `ImageFromImage` and
|
||||
`IsWindowReady`.
|
||||
The binding's real gap against `rl.clj` was §1: `DrawTexturePro`, then `ImageFromImage`
|
||||
and `IsWindowReady`. All three are bound.
|
||||
|
||||
---
|
||||
|
||||
@ -422,22 +465,25 @@ The binding's real gap against `rl.clj` is §1: `DrawTexturePro`, then `ImageFro
|
||||
Split into two tiers, because a one-line binding fix and a month of language work should
|
||||
not compete for the same slot.
|
||||
|
||||
### Tier 0 — bindings. Hours, not weeks. Do these first.
|
||||
### Tier 0 — bindings. Hours, not weeks. **Done, 2026-09-13.**
|
||||
|
||||
1. **`declare-c draw-texture-pro`.** Unblocks the entire renderer. Without it this game
|
||||
has no draw call. One line, plus an acceptance case that makes raylib compute with the
|
||||
source rect so a permuted `Rectangle` goes red.
|
||||
1. ~~**`declare-c draw-texture-pro`.**~~ Bound. The renderer is writable. The acceptance
|
||||
case this asked for could not be written as described — see §1's closing box — so it
|
||||
is a link check behind a `window-ready?` guard, and the computed test moved to
|
||||
`image-from-image` where raylib does the arithmetic on the CPU.
|
||||
|
||||
2. **`left-shift` in the `Key` `defenum`.** Shift+1..5 picks the tilemap in both
|
||||
implementations, and the member is not there. One entry, and without it that input is
|
||||
unwritable.
|
||||
2. ~~**`left-shift` in the `Key` `defenum`.**~~ One entry, `340`. §5 records that nothing
|
||||
else in the enum was missing.
|
||||
|
||||
3. **`declare-c image-from-image` and `window-ready?`.** The atlas tool and the engine's
|
||||
reentrancy guard. Two lines.
|
||||
3. ~~**`declare-c image-from-image` and `window-ready?`.**~~ Both bound.
|
||||
`image-from-image` carries the strongest new assertion in the table: two carves at
|
||||
different `y`s out of one sheet, and the sheet re-read afterwards to show it was not
|
||||
destroyed.
|
||||
|
||||
4. **Decide the rule the first item exposes.** A function the game calls every frame
|
||||
should be hand-written and header-checked, not left to the opt-in import. Worth writing
|
||||
down, because the next game-shaped program will find the next `DrawTexturePro`.
|
||||
4. ~~**Decide the rule the first item exposes.**~~ Decided and written into §1: *a raylib
|
||||
function on a game's per-frame path is hand-written in `raylib.flan` and checked
|
||||
against the header, not left to the opt-in import.* The default build has no
|
||||
`FLAN_RAYLIB_H` and the default build has to be able to draw.
|
||||
|
||||
### Tier 1 — language and tooling, in the order that unblocks the most of this game
|
||||
|
||||
|
||||
@ -288,4 +288,33 @@
|
||||
(print (rl/get-gamepad-axis-movement 0 (rl/GamepadAxis 9)))
|
||||
(println "")
|
||||
|
||||
;; ── window-ready?, and the one draw call that has to link ───────────
|
||||
;;
|
||||
;; No window was opened, so this is false — which is the answer an engine's
|
||||
;; "am I already running" guard needs, and the only thing about it that can
|
||||
;; be asserted without a display.
|
||||
(show-bool "window ready" (rl/window-ready?))
|
||||
|
||||
;; draw-texture-pro needs a GL context, so it cannot be *called* here and
|
||||
;; cannot be in the assertion table at all — the whole Shapes and Textures
|
||||
;; section is in that position. What it can be is *linked*, which is what
|
||||
;; this branch is for: the shim is generated, the symbol is resolved at link
|
||||
;; time, and a signature that does not exist in libraylib fails the build
|
||||
;; rather than the frame a game first draws in. The guard is
|
||||
;; window-ready?, which has just printed no, so the body never runs.
|
||||
;;
|
||||
;; This is the honest limit of what a headless table says about a draw call,
|
||||
;; and it is worth saying out loud: nothing here checks the argument order.
|
||||
;; The source rect, the dest rect and the origin are three structs in a row
|
||||
;; and permuting them links perfectly. Only looking at the screen catches
|
||||
;; that, which is why the file's header says so about the Shapes family.
|
||||
(when (rl/window-ready?)
|
||||
(rl/draw-texture-pro (rl/Texture2D {.id 0 .width 0 .height 0
|
||||
.mipmaps 0 .format 0})
|
||||
(rl/Rectangle {.x 0.0 .y 0.0 .width 16.0 .height 16.0})
|
||||
(rl/Rectangle {.x 0.0 .y 0.0 .width 64.0 .height 64.0})
|
||||
(rl/Vector2 {.x 0.0 .y 0.0})
|
||||
0.0
|
||||
rl/white))
|
||||
|
||||
0)
|
||||
|
||||
@ -157,4 +157,44 @@
|
||||
(show-pixel "cropped at 0,0" img 0 0)
|
||||
(rl/unload-image img))
|
||||
|
||||
;; ── image-from-image, which is crop without the destruction ─────────
|
||||
;;
|
||||
;; The same 6 x 3 image and the same two marks, carved twice. The point that
|
||||
;; matters is the one the assertions below make in three parts:
|
||||
;;
|
||||
;; 1. It reads the rectangle the same way image-crop does — (4,0,2,1)
|
||||
;; picks two pixels of the first row and the mark at (5,0) lands at
|
||||
;; (1,0) of a 2 x 1 result. Exchange width and height and the result is
|
||||
;; 1 x 2 with nothing in it.
|
||||
;;
|
||||
;; 2. It reads `y`. The second carve is (4,2,2,1), one row lower than
|
||||
;; anything image-crop's case reaches, and the mark it finds is the
|
||||
;; *other* colour. A binding that ignored y would answer mark-a twice.
|
||||
;;
|
||||
;; 3. **The source survives.** That is the whole reason this exists beside
|
||||
;; image-crop: crop mutates in place, so carving a sheet into twenty
|
||||
;; tiles with it destroys the sheet on the first one. The original is
|
||||
;; re-read after both carves and still reports 6 x 3 with both marks
|
||||
;; where they were put. Bind this to ImageCrop by mistake and the second
|
||||
;; carve reads out of a 2 x 1 image and the source check goes red.
|
||||
(let [sheet (rl/gen-image-color 6 3 bg)]
|
||||
(rl/image-draw-pixel (addr sheet) 5 0 mark-a)
|
||||
(rl/image-draw-pixel (addr sheet) 4 2 mark-b)
|
||||
(let [top (rl/image-from-image
|
||||
sheet (rl/Rectangle {.x 4.0 .y 0.0 .width 2.0 .height 1.0}))]
|
||||
(show-image "piece-top" top)
|
||||
(show-pixel "piece-top at 1,0" top 1 0)
|
||||
(show-pixel "piece-top at 0,0" top 0 0)
|
||||
(rl/unload-image top))
|
||||
(let [bottom (rl/image-from-image
|
||||
sheet (rl/Rectangle {.x 4.0 .y 2.0 .width 2.0 .height 1.0}))]
|
||||
(show-image "piece-bottom" bottom)
|
||||
(show-pixel "piece-bottom at 0,0" bottom 0 0)
|
||||
(show-pixel "piece-bottom at 1,0" bottom 1 0)
|
||||
(rl/unload-image bottom))
|
||||
(show-image "sheet after" sheet)
|
||||
(show-pixel "sheet at 5,0" sheet 5 0)
|
||||
(show-pixel "sheet at 4,2" sheet 4 2)
|
||||
(rl/unload-image sheet))
|
||||
|
||||
0)
|
||||
|
||||
@ -774,7 +774,8 @@ let () =
|
||||
point in poly yes\npoint outside poly no\n\
|
||||
in square, four corners yes\nout of triangle, three no\n\
|
||||
no crossing\n\
|
||||
axes 0 0 0 0 -1 -1 past-end 0\n"
|
||||
axes 0 0 0 0 -1 -1 past-end 0\n\
|
||||
window ready no\n"
|
||||
in
|
||||
if Sys.command "ldconfig -p 2>/dev/null | grep -q libraylib" = 0 then begin
|
||||
outputs "raylib ffi, headless" "programs/raylib-ffi.flan" raylib_out;
|
||||
@ -876,7 +877,16 @@ let () =
|
||||
resized 2 6 1 7\n\
|
||||
cropped 2 1 1 7\n\
|
||||
cropped at 1,0 200 0 0 255\n\
|
||||
cropped at 0,0 10 20 30 255\n"
|
||||
cropped at 0,0 10 20 30 255\n\
|
||||
piece-top 2 1 1 7\n\
|
||||
piece-top at 1,0 200 0 0 255\n\
|
||||
piece-top at 0,0 10 20 30 255\n\
|
||||
piece-bottom 2 1 1 7\n\
|
||||
piece-bottom at 0,0 0 200 0 255\n\
|
||||
piece-bottom at 1,0 10 20 30 255\n\
|
||||
sheet after 6 3 1 7\n\
|
||||
sheet at 5,0 200 0 0 255\n\
|
||||
sheet at 4,2 0 200 0 255\n"
|
||||
in
|
||||
if Sys.command "ldconfig -p 2>/dev/null | grep -q libraylib" = 0 then begin
|
||||
outputs "raylib images, headless" "programs/raylib-image.flan"
|
||||
|
||||
38
vendor/raylib/raylib.flan
vendored
38
vendor/raylib/raylib.flan
vendored
@ -52,7 +52,8 @@
|
||||
j 74 k 75 l 76 m 77 n 78 o 79 p 80 q 81 r 82
|
||||
s 83 t 84 u 85 v 86 w 87 x 88 y 89 z 90
|
||||
escape 256 enter 257 tab 258 backspace 259
|
||||
right 262 left 263 down 264 up 265])
|
||||
right 262 left 263 down 264 up 265
|
||||
left-shift 340])
|
||||
|
||||
(defenum MouseButton
|
||||
[left 0 right 1 middle 2 side 3 extra 4 forward 5 back 6])
|
||||
@ -65,6 +66,12 @@
|
||||
(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")
|
||||
|
||||
@ -355,6 +362,23 @@
|
||||
(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
|
||||
@ -435,6 +459,18 @@
|
||||
|
||||
(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. There is no ImageCopy in 5.5, so
|
||||
;; this is also how a whole image is duplicated — a rec covering all of it.
|
||||
;;
|
||||
;; 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")
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user