nandi/jolt-nativepublic Fork 0
db7ce9f0dec25938d439969e528906a35ad2d68c
Commits
Clone
git clone https://git.rickub.com/nandi/jolt-native.git
git clone ssh://git@rickub.com/nandi/jolt-native.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Play three games on the gfx backend e98a184 · on db7ce9f0dec25938d439969e528906a35ad2d68c · nandi · 11d ago
README.md · 92 lines · 4.0 KBmarkdown
Blame HistoryOpen raw

glimmer-gfx

glimmer's software backend: no toolkit, no shared object, no GPU. The
rasterizer, the layout and the font are jolt; the only foreign code is Xlib,
and Xlib draws nothing — it opens a window and takes a finished framebuffer.

It is glimmer-vidya with nothing underneath it. The
reconciler does not know what it is patching, so the same hiccup that egui
paints as a window and glimmer-tui paints as cells is painted
here, a pixel at a time, by code in this directory:

(ns myapp
  (:require [glimmer.ratom :as ra]
            [glimmer.core :as ui]
            [glimmer-gfx.core]))          ; installs this backend

(defn app []
  [:card {:spacing 8}
   [:title {:label "Counter"}]
   [:label {:label (str "Count: " (ra/deref count))}]
   [:button {:label "+ 1" :kind :primary :on-click #(ra/swap! count inc)}]])

(defn -main [& _] (ui/run app :title "myapp"))

Running

jolt test                        # headless: no window, no display
LD_LIBRARY_PATH=/path/to/libX11 jolt counter
LD_LIBRARY_PATH=/path/to/libX11 jolt tictactoe   # a game, in the same widgets
LD_LIBRARY_PATH=/path/to/libX11 jolt maze        # WASD, under the widgets
LD_LIBRARY_PATH=/path/to/libX11 jolt asteroids   # vector primitives, same two files

Xlib is a system library here, not one of this repo's crates, and it must be
one the jolt binary can load — on a nix-built jolt, the host /usr/lib copy is
a different glibc and fails before dlopen returns.

maze is the other half of the story: sixty frames a second whatever you do,
so there is nothing for a reconciler to reconcile. It skips glimmer and talks
to raster and x11 directly — run-window hands it a framebuffer and the
keys held this frame, which is all a game loop ever wanted.

asteroids is what those primitives are actually for: every shape is a list of
points rotated and translated per frame and stroked with raster/poly!, so
there is no sprite, no bitmap and no asset — the ship is four points and some
trigonometry. step is pure, which is why the tests play a whole game without
a display.

What a backend has to do

Read this one first. It is the smallest complete backend in the repo, and the
only one where every part of the answer is visible rather than behind an ABI.

src/glimmer_gfx/raster.clj   framebuffer, rect/line, a 3x5 font   no FFI
src/glimmer_gfx/core.clj     the backend: tree, layout, paint     no FFI
src/glimmer_gfx/x11.clj      the window, and nothing else

glimmer.backend's map is mostly trivial — create! allocates an atom,
append-child! conjes onto a vector. The work is what a toolkit would have
done for you:

  • A tree to patch. The reconciler needs somewhere to hold widgets between
    frames, which an immediate-mode painter has not got. Here it is atoms of
    {:tag :props :children}, about 60 lines. glimmer-vidya needs the same thing
    and keeps it in Rust behind a second C ABI, because egui hands the
    reconciler nothing to hold.
  • Layout. measure! sizes bottom-up, place! positions top-down. This is
    most of the file. :spacing, :max-width and container padding are
    honoured; :align and :grow are not, so children keep their measured
    width, left-aligned.
  • Hit testing. Deepest interactive node under the cursor, last match wins.
    A press claims a widget and holds it until release, so a slider keeps
    tracking when the pointer leaves its rect.

Tags: :page :card :vbox :hbox :title :label :button :checkbox
:slider :spacer. Props are the shared vocabulary — :label, :spacing,
:max-width, :kind, :checked, :value/:min/:max, :on-click,
:on-change.

Text

There is no font stack. raster.clj carries a 3x5 bitmap font for ASCII,
authored as art and editable in place:

"A.#.|#.#|###|#.#|#.#"

It scales by whole pixels, which is why the UI looks like it does. A real font
means FreeType, which means a crate — at which point this stops being the
backend that runs anywhere.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# glimmer-gfx

glimmer's **software** backend: no toolkit, no shared object, no GPU. The
rasterizer, the layout and the font are jolt; the only foreign code is Xlib,
and Xlib draws nothing — it opens a window and takes a finished framebuffer.

It is [glimmer-vidya](../glimmer-vidya) with nothing underneath it. The
reconciler does not know what it is patching, so the same hiccup that egui
paints as a window and [glimmer-tui](../glimmer-tui) paints as cells is painted
here, a pixel at a time, by code in this directory:

```clojure
(ns myapp
  (:require [glimmer.ratom :as ra]
            [glimmer.core :as ui]
            [glimmer-gfx.core]))          ; installs this backend

(defn app []
  [:card {:spacing 8}
   [:title {:label "Counter"}]
   [:label {:label (str "Count: " (ra/deref count))}]
   [:button {:label "+ 1" :kind :primary :on-click #(ra/swap! count inc)}]])

(defn -main [& _] (ui/run app :title "myapp"))
```

## Running

```bash
jolt test                        # headless: no window, no display
LD_LIBRARY_PATH=/path/to/libX11 jolt counter
LD_LIBRARY_PATH=/path/to/libX11 jolt tictactoe   # a game, in the same widgets
LD_LIBRARY_PATH=/path/to/libX11 jolt maze        # WASD, under the widgets
LD_LIBRARY_PATH=/path/to/libX11 jolt asteroids   # vector primitives, same two files
```

Xlib is a *system* library here, not one of this repo's crates, and it must be
one the jolt binary can load — on a nix-built jolt, the host `/usr/lib` copy is
a different glibc and fails before `dlopen` returns.

`maze` is the other half of the story: sixty frames a second whatever you do,
so there is nothing for a reconciler to reconcile. It skips glimmer and talks
to `raster` and `x11` directly — `run-window` hands it a framebuffer and the
keys held this frame, which is all a game loop ever wanted.

`asteroids` is what those primitives are actually for: every shape is a list of
points rotated and translated per frame and stroked with `raster/poly!`, so
there is no sprite, no bitmap and no asset — the ship is four points and some
trigonometry. `step` is pure, which is why the tests play a whole game without
a display.

## What a backend has to do

Read this one first. It is the smallest complete backend in the repo, and the
only one where every part of the answer is visible rather than behind an ABI.

    src/glimmer_gfx/raster.clj   framebuffer, rect/line, a 3x5 font   no FFI
    src/glimmer_gfx/core.clj     the backend: tree, layout, paint     no FFI
    src/glimmer_gfx/x11.clj      the window, and nothing else

`glimmer.backend`'s map is mostly trivial — `create!` allocates an atom,
`append-child!` conjes onto a vector. The work is what a toolkit would have
done for you:

* **A tree to patch.** The reconciler needs somewhere to hold widgets between
  frames, which an immediate-mode painter has not got. Here it is atoms of
  `{:tag :props :children}`, about 60 lines. glimmer-vidya needs the same thing
  and keeps it in Rust behind a second C ABI, because egui hands the
  reconciler nothing to hold.
* **Layout.** `measure!` sizes bottom-up, `place!` positions top-down. This is
  most of the file. `:spacing`, `:max-width` and container padding are
  honoured; `:align` and `:grow` are not, so children keep their measured
  width, left-aligned.
* **Hit testing.** Deepest interactive node under the cursor, last match wins.
  A press claims a widget and holds it until release, so a slider keeps
  tracking when the pointer leaves its rect.

Tags: `:page` `:card` `:vbox` `:hbox` `:title` `:label` `:button` `:checkbox`
`:slider` `:spacer`. Props are the shared vocabulary — `:label`, `:spacing`,
`:max-width`, `:kind`, `:checked`, `:value`/`:min`/`:max`, `:on-click`,
`:on-change`.

## Text

There is no font stack. `raster.clj` carries a 3x5 bitmap font for ASCII,
authored as art and editable in place:

    "A.#.|#.#|###|#.#|#.#"

It scales by whole pixels, which is why the UI looks like it does. A real font
means FreeType, which means a crate — at which point this stops being the
backend that runs anywhere.