diff --git a/BUILT.md b/BUILT.md index 1005959..5f882f8 100644 --- a/BUILT.md +++ b/BUILT.md @@ -1927,3 +1927,118 @@ and node runs the emitted JS and gets `ok`. For raylib it builds `core-basic-win module for the two things that would be false if the mechanism were wrong: an `asyncify_start_unwind` export, and a `glViewport` import that can only have come from raylib's web platform. Import and export names are plain strings in the binary, so this needs no wasm reader. + +## sand.flan in a browser + +**The flagship program builds for the browser and the artifact opens.** Three things were between it and the target, +and none of them was the `#include` the earlier note named. + +**The brush was a path, and a path is what cannot work.** `(rl/load-texture "brush.png")` hands raylib a filename to +open; a bare relative path has no meaning where there is no filesystem, so raylib would have opened nothing and the +cursor would simply have been missing. It is `(embed "brush.png")` now, decoded by a new binding — +`LoadImageFromMemory`, which completes the chain embedded bytes -> `Image` -> `Texture2D` that `LoadTextureFromImage` +already had the other half of. The declaration is `(Ptr u8)` plus an explicit `i32` count, because `shim.ml` refuses a +slice parameter and its refusal says exactly that; the Flan wrapper beside it takes the slice apart, which is the +idiom `collision-point-poly?` and `load-font-ex` already established. One decode serves both textures now: the +unflipped upload first, then `ImageFlipHorizontal` in place, then the mirrored upload — **the order is load-bearing**, +and if the two badges look the same it was swapped. + +`load-texture` and `load-image` lose their only call site in this repository by this change. That is deliberate rather +than an accident of editing: a path-based load is the one shape the browser cannot have, and the bindings stay for the +desktop programs that will want them. + +The embedded path resolves **relative to the file the form is written in**, which is why +`test/programs/sand-headless.flan` still works: it reaches `sand.flan` through `../../` out of a sandboxed `_build`, +and the PNG is found beside `sand.flan` and not beside the working directory. `test/dune` therefore lists `brush.png` +as a dependency of every stanza that builds sand — an embed is read by the *checker*, so it is a build input and not a +run-time one. The same fact reached `test_session`'s `C-c C-k` case, which re-evaluates sand.flan's whole text: it now +passes `~origin`, which is the buffer path both editor paths already send, because the default `` origin would +resolve the embed against the working directory instead. + +**A package's C may be addressed to one target, the way a `link` line already could.** `Load` collects a package's +`.c` files by listing the directory, and there is nowhere in a directory listing to put a tag except the name, so the +tag goes there, before the extension: + +``` +vendor/agent/flan_agent.c compiled everywhere, unless displaced +vendor/agent/flan_agent.web.c compiled for the browser, and displaces the above +``` + +One rule: **a tagged file is compiled only on its own target, and there it replaces the untagged file of the same base +name.** Untagged is the default and every existing package is untagged, so nothing that did not opt in changed. +Replacement rather than the pure tagging Go's `_windows.go` and Odin's `file_js.odin` use, and the difference is the +point — pure tagging would mean renaming `flan_agent.c` to `flan_agent.native.c` to teach the package about a target +it had never heard of, and this way a package gains a target by gaining a file. The selection is in `Build` and not in +`Load`, for the reason `select_lflags` gives: `Load` resolves imports before a target is chosen. + +**The dev agent on the web is a no-op, and it is not the `barf` decision being contradicted.** The compile error that +led here — `struct timeval` incomplete, because emscripten's headers do not pull `` in transitively — is +the surface. Underneath: *the agent is a socket server and a browser has no sockets*. Adding the include produces an +agent that compiles, links, starts and can never accept a connection. + +Refusing `vendor:agent` on a web target was the other candidate and is ruled out by arithmetic, not taste. Flan has no +conditional compilation, `sand.flan` calls `(agent/start ...)` unconditionally, and `Reach` cannot prune a package +something reachable calls into — so a build-time refusal means the flagship program does not build for the browser at +all without being edited into a second program. **A refusal is only honest when the caller has a way to not ask.** + +The two decisions look contradictory and are not, and the difference is *what the caller loses*. `barf` is asked to +make something durable; a no-op returns success to a program that now believes the bytes are on disk, and the loss is +real, is the user's, and is discovered later or never. The agent is asked to accept redefinitions from an editor; on +the web there is no editor, no socket and no session — `--dev` is refused by name on every wasm target, so a web build +has no cells to install a redefinition into even if one arrived. **Nothing is lost because there was never anything +there.** `sand.flan` already says the same about a *native* release build, at the call site: "Building without `--dev` +is fine — nothing has cells to install into, so a module is refused on the listener thread and the loop never +notices." A web build reaches that outcome by a shorter route. `start` returns `-1`, which is what `flan_agent.c` +returns for a path it cannot bind; `poll` and `wait` return 0, which is what the native build returns on every frame +nothing arrived on. The whole argument is written at the top of `vendor/agent/flan_agent.web.c`, where the next reader +will meet it. + +### Building it and opening it + +The raylib archive is built once and is not in the tree. From the repository root: + +```sh +sh vendor/raylib/build-web.sh # clones raylib 5.5 and compiles it with emcc +export FLAN_RAYLIB_WEB=$PWD/vendor/raylib/web/libraylib-5.5.a +``` + +`build-web.sh` prints that `export` line itself. Then: + +```sh +flan build sand.flan --target=web -o sand.html +``` + +**The output must be named `.html`.** `--shell-file` is passed only when it is, because emcc accepts and ignores it +otherwise — so `-o sand` produces a module with no shell, no canvas, and a page that looks like it built fine and +paints nothing. Three files land beside it, in whatever directory `-o` names: `sand.html`, `sand.js`, `sand.wasm`. + +**A `file://` URL will not work.** The page fetches `sand.wasm`, and a browser refuses that from the filesystem. Serve +the directory holding the three files: + +```sh +python3 -m http.server 8000 +``` + +and open **`http://localhost:8000/sand.html`**. Left mouse paints; the keys are the ones the native build has. + +### What only a human opening it can settle + +Verified headlessly, by `test/test_web.ml`: the three files exist, the module carries `asyncify_start_unwind` and +`glViewport`, and **brush.png's own bytes are in the module, whole** — the assertion that keeps the embed from +rotting. Not `IHDR`: stb_image, linked in from raylib, carries that string itself, so an `IHDR` check would pass on a +build where the embed emitted nothing. + +Not verified, and not verifiable here. `node sand.js` instantiates the module, runs `main`, and dies inside `glfwInit` +on `window is not defined` — which says the module is live and says nothing about the canvas. + +- **Whether it paints at all.** Nothing in CI has ever seen a pixel of this. +- **Audio.** `start-audio` generates a tone, `ExportWave`s it to `/tmp/flan-sand-tone.wav` and loads it back as a + music stream. That is raylib's own `fopen`, not Flan's `barf`, so the `#ifdef` in `flan_rt.c` does not cover it and + emscripten's MEMFS may well give it a writable `/tmp`. Every use is behind `music-ok`/`tone-ok`, so a failure is + silence and not a crash. Separately, browsers suspend the audio context until a user gesture, so `audio-ok` may be + true while nothing is heard until the first click. +- **The loop never exits.** `WindowShouldClose()` on `PLATFORM_WEB` is an `emscripten_sleep` that returns false, so + `until` never terminates and **none of `main`'s `defer`s ever run** — no `CloseWindow`, no `UnloadTexture`. That is + correct for a page, which is torn down by the tab closing, and it is worth knowing before reading anything into it. +- **Canvas size against `screen-width`/`screen-height`.** The shell is a string in `Build` and its canvas is not sized + from the program, so 900x600 may be letterboxed or cropped. diff --git a/NEXT.md b/NEXT.md index c52b684..f5eb2fe 100644 --- a/NEXT.md +++ b/NEXT.md @@ -412,6 +412,29 @@ array with a struct element, a 2-D struct array, and `[N string]` as both `defco "The browser is the third target", for the mechanism and why asyncify rather than `emscripten_set_main_loop`. Four things it does not cover. +~~**1. `sand.flan` has no web build, and the cause is one missing `#include`.**~~ **Built. It opens.** See BUILT.md, +"sand.flan in a browser", for the whole of it. Three summary lines, because the diagnosis below was right about the +structure and wrong about the cause: + +- The `#include` was never the fix. **The agent is a socket server and a browser has no sockets**, so an agent that + compiles there is an agent that can never accept a connection. `vendor/agent/flan_agent.web.c` is three no-ops, and + `Build` selects it over `flan_agent.c` on `--target=web` and nowhere else. +- **Refusing `vendor:agent` on web was the honest-looking option and is ruled out by arithmetic.** There is no + conditional compilation, `sand.flan` calls `agent/start` unconditionally, `Reach` cannot prune a package something + reachable calls into — so a refusal means the flagship program does not build for the browser at all. A refusal is + only honest when the caller has a way to not ask. This does **not** reverse decision 2 above: `barf`'s no-op loses a + file the program believed it wrote, and there is nothing for the agent to lose because `--dev` is already refused by + name on every wasm target. The argument is written out at the top of `flan_agent.web.c`. +- **A package's `.c` files can now be addressed to a target**, by a tag in the name before the extension, and a tagged + file *replaces* the untagged file of the same base name on that target. This is the C-source half of the + `@native`/`@wasi`/`@web` link-line mechanism decision 2 pointed at for per-package target isolation. + +The brush is `(embed "brush.png")` decoded through a new `LoadImageFromMemory` binding. `load-texture` and +`load-image` now have no call site anywhere in this repository — deliberately, because a path-based load is the one +shape the browser cannot have, and said here so it is not read later as an accident. + +The original entry follows. + **1. `sand.flan` has no web build, and the cause is one missing `#include`.** `vendor/agent/flan_agent.c` does not compile under emcc: *variable has incomplete type 'struct timeval'* at line 426, because emscripten's headers do not pull `` in transitively the way glibc's do. `sand.flan`'s `main` calls `(agent/start ...)` @@ -440,6 +463,16 @@ The original text follows. `sand.flan` does `(rl/load-texture "brush.png")` agai all. Answering this means either giving a single-file program a way to carry build arguments, or making assets their own declaration rather than a linker flag. No flag was invented for it here. +**3. Nothing has been opened in a browser.** *Still true, and now it is the only thing left between here and +"someone played with it".* `sand.flan` builds for the web, the module carries asyncify, raylib's GL imports and +brush.png's own bytes whole, and `node sand.js` gets as far as `glfwInit` before dying on `window is not defined` — +which proves the module is live and proves nothing about the canvas. BUILT.md carries the exact commands to serve and +open it, and the list of what only a human will discover: whether it paints, whether the audio round trip through +MEMFS survives, and the canvas size. The `until` loop never exits on the web, so none of `main`'s `defer`s run — +expected, and worth knowing before reading anything into it. + +The original entry follows. + **3. Nothing has been opened in a browser.** The test is headless and permanently so: it asserts the artifact's shape, the `asyncify_start_unwind` export and the `glViewport` import, and that node runs the emitted JS. Whether the canvas actually paints is unverified by anything in CI, and a human should look once. diff --git a/lib/build.ml b/lib/build.ml index af575e3..209106e 100644 --- a/lib/build.ml +++ b/lib/build.ml @@ -517,6 +517,65 @@ let select_lflags opts flags = else Some (expand_vars ~where:"link" f)) flags +(* ── A package's C may be addressed to one target too ───────────────── + [link] lines carry a @tag; a package's .c files had no such channel, and + [Load] collects every one of them by listing the directory. There is nowhere + in a directory listing to put a tag except the name, so the tag goes there, + before the extension: + + flan_agent.c compiled everywhere, unless displaced + flan_agent.web.c compiled for the browser, and displaces the above + + The rule is one sentence: **a tagged file is compiled only on its own + target, and there it replaces the untagged file of the same base name.** + Untagged is the default and every existing package is untagged, so nothing + that does not opt in changes. + + Replacement rather than pure tagging, which is what Go's `_windows.go` and + Odin's `file_js.odin` do, and the difference is deliberate: pure tagging + would mean renaming the file that already works — [flan_agent.c] becoming + [flan_agent.native.c] — to teach a package about a target it had never + heard of. Making the browser's answer *additive* means a package gains a + target by gaining a file, and the file that was right on three targets is + not touched to say so. + + The selection is here and not in [Load] for the reason [select_lflags] is: + [Load] resolves imports before a target is chosen, and the same program is + built for both. + + The base name is what is matched, not the path: two packages each with a + [flan_agent.c] would already collide at the link, so there is nothing new + to disambiguate here. *) +let split_csrc_tag path = + let base = Filename.basename path in + let stem = Filename.remove_extension base in + match Filename.extension stem with + | "" -> (stem, None) + | dot_tag -> + let tag = String.sub dot_tag 1 (String.length dot_tag - 1) in + if List.mem tag link_tags then (Filename.remove_extension stem, Some tag) + else (stem, None) + +let select_csrcs opts csrcs = + let want = target_tag opts in + (* The base names a tagged file speaks for on *this* target. Only these + displace; a [foo.wasi.c] is invisible to a native build in both + directions, so it neither compiles nor hides [foo.c]. *) + let displaced = + List.filter_map + (fun c -> + match split_csrc_tag c with + | (base, Some tag) when String.equal tag want -> Some base + | _ -> None) + csrcs + in + List.filter + (fun c -> + match split_csrc_tag c with + | (_, Some tag) -> String.equal tag want + | (base, None) -> not (List.mem base displaced)) + csrcs + (* What the compiler itself is, cheaply: its path, size and mtime. A clang upgrade changes one of those, so the key changes with it — without paying a [clang --version] subprocess on every build, which would cost most of what @@ -663,7 +722,10 @@ let executable ?(opts = default) ?(csrcs = []) ?(lflags = []) ?(pnames = []) | [] -> [] | parts -> [ cc (String.concat "" (List.map snd parts)) "flan_shim.c" ]) - @ List.map (fun c -> cc (read_file c) (Filename.basename c)) csrcs + (* Per-target selection, the same shape [select_lflags] applies to a + package's linker arguments. See [select_csrcs]. *) + @ List.map (fun c -> cc (read_file c) (Filename.basename c)) + (select_csrcs opts csrcs) in let cmd = String.concat " " diff --git a/sand.flan b/sand.flan index afd5d3c..fe731fd 100644 --- a/sand.flan +++ b/sand.flan @@ -165,22 +165,44 @@ (defvar brush-mirrored rl/Texture2D) (defvar brush-mirrored-ok bool) -;; A missing file is not a crash and not silence: LoadTexture hands back a +;; The sheet itself, baked into the binary at compile time. This used to be +;; (rl/load-texture "brush.png") against a bare relative path, and that is the +;; one line that kept this program off the browser: a relative path has no +;; meaning on a target with no filesystem, so raylib would have opened nothing +;; and the cursor would have been missing with no way to say why. +;; +;; The path is resolved relative to *this file*, not to wherever flan was +;; invoked from, which is what lets test/programs/sand-headless.flan import +;; this file as a package from _build and still find the PNG. Nothing is read +;; at run time on either target, so there is one code path and not two. +(defconst brush-png (embed "brush.png")) + +;; A brush that does not load is not a crash and not silence: an Image that +;; failed to decode has a null buffer, LoadTextureFromImage hands back a ;; texture with an id of 0, every draw with it is a no-op, and the program ;; looks like it has a drawing bug. So it is asked and said once, here. +;; +;; Note what this can no longer mean: the file is missing. A missing +;; brush.png is a compile error now, at the embed, which is the whole point of +;; embedding it. What is left is a decode that failed or an upload with no GL +;; context behind it. +;; +;; One decode serves both textures. The unflipped upload happens first, +;; because ImageFlipHorizontal rewrites the buffer in place and the second +;; upload has to see the changed pixels — if the two badges look the same, +;; either the flip did nothing or the order here was swapped. (defn load-brush [] - (set brush (rl/load-texture "brush.png")) - (set brush-ok (rl/texture-valid? brush)) - (unless brush-ok - (println "sand: cannot load brush.png — drawing the cursor is off")) - ;; The other route to a texture: the file into RAM, changed there, and only - ;; then uploaded. An Image that failed to load has a null buffer and - ;; unloading it is still safe, so there is one unload and not two. - (let [sheet (rl/load-image "brush.png")] + (let [sheet (rl/load-image-from-memory ".png" brush-png)] (when (rl/image-valid? sheet) + (set brush (rl/load-texture-from-image sheet)) (rl/image-flip-horizontal (addr sheet)) (set brush-mirrored (rl/load-texture-from-image sheet))) + ;; An Image that failed to decode has a null buffer and unloading it is + ;; still safe, so there is one unload and not two. (rl/unload-image sheet)) + (set brush-ok (rl/texture-valid? brush)) + (unless brush-ok + (println "sand: brush.png would not decode — drawing the cursor is off")) (set brush-mirrored-ok (rl/texture-valid? brush-mirrored))) ;; Four draws, one per shape the call comes in, because none of them can be in diff --git a/test/dune b/test/dune index b87d43e..446f9ba 100644 --- a/test/dune +++ b/test/dune @@ -12,6 +12,12 @@ (deps (file %{workspace_root}/calc-me.flan) (file %{workspace_root}/sand.flan) + ; The brush sheet, which sand.flan now (embed ...)s rather than opening by + ; path. An embed is read by the *checker*, relative to the file the form is + ; written in, so it is a dependency of every build of sand.flan including the + ; headless one — which reaches sand.flan through ../../ from programs/ and + ; would otherwise find nothing at _build/default/brush.png. + (file %{workspace_root}/brush.png) ; The raylib bindings, because sand.flan and the FFI case import them and an ; import reads the directory at build time. sand.flan itself is above: the ; headless case imports it as a single-file package. @@ -59,6 +65,12 @@ ; example imports examples/digits.flan, so the directory comes whole. (glob_files %{workspace_root}/vendor/raylib/*) (glob_files %{workspace_root}/examples/*) + ; sand.flan for the browser, with the sheet it embeds and the dev agent it + ; imports — the agent's directory has to be whole, because the file that + ; makes a web build possible is the one Build selects out of it. + (file %{workspace_root}/sand.flan) + (file %{workspace_root}/brush.png) + (glob_files %{workspace_root}/vendor/agent/*) ; flan run --target=web is refused by the CLI, so the CLI has to be here. (file %{workspace_root}/bin/main.exe))) @@ -82,6 +94,7 @@ test_sanitize.exe (file %{workspace_root}/calc-me.flan) (file %{workspace_root}/sand.flan) + (file %{workspace_root}/brush.png) (glob_files %{workspace_root}/vendor/raylib/*) (glob_files %{workspace_root}/vendor/agent/*) (glob_files %{workspace_root}/vendor/edn/*) diff --git a/test/test_session.ml b/test/test_session.ml index 8221b20..7339dbd 100644 --- a/test/test_session.ml +++ b/test/test_session.ml @@ -181,7 +181,14 @@ let () = place rather than appended a second time and rejected as duplicates. *) let t, _ = Session.create ~file:"../sand.flan" () in let src = In_channel.with_open_bin "../sand.flan" In_channel.input_all in - (match Session.eval t src with + (* [~origin] is the buffer's own path and both editor paths send it + (flan-dev.el's `:file (or buffer-file-name "")`). It became + load-bearing here when sand.flan started embedding brush.png: an embedded + path is resolved relative to the file the form is written in, so the + default origin of "" would look for ./brush.png beside the test's + working directory and find nothing. Omitting it here was testing a request + the editor never sends. *) + (match Session.eval ~origin:"../sand.flan" t src with | c -> if not (List.mem "game-draw" c.Session.fns) then fail "reloading sand.flan did not include its own functions" diff --git a/test/test_web.ml b/test/test_web.ml index 3f6b0e9..73e3e67 100644 --- a/test/test_web.ml +++ b/test/test_web.ml @@ -208,6 +208,59 @@ let () = browser"; if not (contains bytes "glViewport") then fail "no GL imports in the module — raylib did not link"; + cleanup out); + + (* ── sand.flan, the flagship, for the browser ────────────────── + The program the whole target was wanted for, and the last web build + in this process on purpose — see [raylib_web] on why anything after + it would pick the archive up silently. + + Two things had to change for this to build at all, and this case + watches both of them rather than only watching the exit code: + + - The brush was (rl/load-texture "brush.png"), a bare relative path, + which means nothing on a target with no filesystem. It is an + (embed "brush.png") now, decoded through LoadImageFromMemory. The + assertion is the PNG's *own bytes*, whole, found in the module: a + build that succeeded while the embed silently emitted nothing + would pass every other check here, and "IHDR" would not do it + either — stb_image's decoder, linked in from raylib, carries that + string itself. + + - The dev agent is a socket server and a browser has no sockets. + vendor/agent/flan_agent.web.c is selected over flan_agent.c here + and nowhere else. It needs no assertion of its own: flan_agent.c + does not compile under emcc at all, so a build that reached this + point compiled the stub. + + Not run, and this is the ceiling rather than an omission. node has + no DOM and no WebGL, so the module instantiates, main runs, and + InitWindow dies inside glfwInit on `window is not defined` — which + says the module is live but says nothing about whether the canvas + paints. Only a human opening it can say that; BUILT.md carries the + commands. *) + let out = Filename.concat scratch "flan-web-sand.html" in + (match web_build "../sand.flan" out with + | exception Failure m -> fail "sand.flan for the browser: %s" m + | () -> + let html, js, wasm = parts out in + List.iter + (fun (what, f) -> + if not (Sys.file_exists f) then + fail "the sand web build made no %s (%s)" what f) + [ ("page", html); ("JS", js); ("module", wasm) ]; + let bytes = read wasm in + if not (contains bytes "asyncify_start_unwind") then + fail + "no asyncify in sand's module — its `until` loop would block the browser"; + if not (contains bytes "glViewport") then + fail "no GL imports in sand's module — raylib did not link"; + let png = read "../brush.png" in + if String.length png = 0 then + fail "brush.png is empty, so the embed assertion below proves nothing" + else if not (contains bytes png) then + fail + "brush.png's bytes are not in sand's module — the embed did not reach the browser"; cleanup out))); (* ── Refused by name ─────────────────────────────────────────────── diff --git a/vendor/agent/flan_agent.web.c b/vendor/agent/flan_agent.web.c new file mode 100644 index 0000000..7745284 --- /dev/null +++ b/vendor/agent/flan_agent.web.c @@ -0,0 +1,69 @@ +/* The dev agent in a browser: the three calls, doing nothing, and saying so. + * + * Build selects this file over flan_agent.c on --target=web, and only there + * (see Build.select_csrcs). The reason is not the compile error that led here + * — emscripten's headers do not pull in transitively, so + * flan_agent.c fails on `struct timeval` at line 426 — because an #include + * would have fixed that and fixed nothing real. The reason is structural: + * + * THE AGENT IS A SOCKET SERVER, AND A BROWSER HAS NO SOCKETS. + * + * flan_agent_start binds an AF_UNIX socket and hands it to a listener thread. + * There is no such address family under emscripten, nothing to listen on, and + * nothing that could connect if there were. Adding the include produces an + * agent that compiles, links, starts, and can never accept a connection. + * + * ── Why a no-op here, when `barf` on the web signals instead ──────────── + * + * NEXT.md decision 2 rejected a silent no-op for `barf` in as many words: a + * no-op write is how a save file disappears with nothing said. The two look + * contradictory and are not, and the difference is what the caller loses. + * + * `barf` is asked to make something durable. A no-op returns success to a + * program that now believes the bytes are on disk; the loss is real, it is the + * user's, and it is discovered later or never. So the web signals a condition + * and the program decides. + * + * The agent is asked to accept redefinitions from an editor. On the web there + * is no editor, no socket, and no session — `--dev` is refused by name on + * every wasm target, so a web build has no cells to install a redefinition + * into even if one arrived. There is nothing to lose because there was never + * anything there. sand.flan already says the same thing about a *native* + * release build, at the call site: + * + * "Building without --dev is fine — nothing has cells to install into, so a + * module is refused on the listener thread and the loop never notices." + * + * A web build reaches that same outcome by a shorter route. The no-op is not + * hiding a failure; it is the truthful implementation of "nothing is + * available here", which is the case where Odin's own `.Unsupported` stubs are + * right and the case decision 2 was careful to say it was not. + * + * ── Why not refuse vendor:agent on a web target ───────────────────────── + * + * It was the other candidate and it is ruled out by arithmetic, not taste. + * Flan has no conditional compilation, so a program cannot say "skip this on + * web". sand.flan calls (agent/start ...) unconditionally, Reach cannot prune + * a package something reachable calls into, and a build-time refusal would + * therefore mean sand.flan does not build for the browser at all without being + * edited into a second program. Refusing is only honest when the caller has a + * way to not ask; here it has none. + * + * ── What the return values say ────────────────────────────────────────── + * + * start returns -1, which is the same "could not listen" flan_agent.c returns + * for a path it cannot bind, so a caller that checks gets the answer it + * already knows how to read. poll and wait return 0 — no redefinitions + * installed — which is the truth and is what the native build returns on every + * frame that nothing arrived on. */ + +#include + +int32_t flan_agent_start(const uint8_t *path, int64_t len) { + (void)path; (void)len; + return -1; +} + +int32_t flan_agent_poll(void) { return 0; } + +int32_t flan_agent_wait(int32_t ms) { (void)ms; return 0; } diff --git a/vendor/raylib/raylib.flan b/vendor/raylib/raylib.flan index d6b792c..6275b72 100644 --- a/vendor/raylib/raylib.flan +++ b/vendor/raylib/raylib.flan @@ -378,6 +378,37 @@ ;; 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. +(defvar 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.