sand.flan builds for the browser, with its brush baked in
This commit is contained in:
commit
bdb3f105f2
115
BUILT.md
115
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 `<eval>` 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 `<sys/time.h>` 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.
|
||||
|
||||
33
NEXT.md
33
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 `<sys/time.h>` 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.
|
||||
|
||||
64
lib/build.ml
64
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 " "
|
||||
|
||||
40
sand.flan
40
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
|
||||
|
||||
13
test/dune
13
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/*)
|
||||
|
||||
@ -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 "<buffer>")`). 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 "<eval>" 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"
|
||||
|
||||
@ -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 ───────────────────────────────────────────────
|
||||
|
||||
69
vendor/agent/flan_agent.web.c
vendored
Normal file
69
vendor/agent/flan_agent.web.c
vendored
Normal file
@ -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 <sys/time.h> 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 <stdint.h>
|
||||
|
||||
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; }
|
||||
31
vendor/raylib/raylib.flan
vendored
31
vendor/raylib/raylib.flan
vendored
@ -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.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user