diff options
| author | user <user@clank> | 2026-08-14 22:06:57 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-08-14 22:06:57 +0200 |
| commit | 2ae507a8a331799875e07c327730a1a4ca0363f4 (patch) | |
| tree | 685b167c2a87f5e9f877cca31d31f43fd4e71734 /HANDOVER.md | |
| parent | sl0ppty: expand ~ in a path that a config file wrote (diff) | |
| download | slosh-2ae507a8a331799875e07c327730a1a4ca0363f4.tar.gz slosh-2ae507a8a331799875e07c327730a1a4ca0363f4.tar.xz slosh-2ae507a8a331799875e07c327730a1a4ca0363f4.zip | |
sl0ppty: shaders can be loaded from shared libraries
Adding an effect meant editing shader.c and rebuilding, which is fine for us
and useless to anyone else. Now any `*.so` in the shader directory that
exports a plugin table is loaded at startup, and what it declares is named in
the config exactly like a built-in -- same syntax, same three parameters,
same pass. Default location is a `shaders` directory beside the config file,
so dropping the .so next to config.kdl is the whole installation.
The interesting part is what a plugin can see, which is one header.
shader_abi.h holds cell_t, color_t, shade_ctx_t, shader_t and nothing else:
no libghostty-vt, no pty, no layout, no session. A shader is therefore
unable to reach anything but the cell it was handed, not by rule but because
nothing else is declared where it can find it. cell_t and color_t moved
there from sl0ppty.h for the same reason -- that is the one place they are a
contract with code compiled outside this tree rather than an internal type.
Loading is deliberately narrow:
- refused unless the library reports our ABI version *and* matching struct
sizes. The version says which contract; the sizes catch someone editing
their copy of the header, which a version number alone calls fine and a
running session calls garbled colours;
- refused if it redefines an existing name, because a config naming `dim`
means the `dim` it was written against;
- all-or-nothing per file, and per file: a bad library is a line on stderr
and everything else still loads (D9 fail-open);
- nothing is ever unloaded. A shade_fn may be sitting in a live config or on
a pane, so dlclose() is a jump into an unmapped page. A reload picks up
newly *added* plugins; replacing one needs a new session.
This is the single exception to D9's "a plugin is a subprocess", and it is
argued in D15 rather than drifted into: a shader is called per cell, per
pass, per frame, which no subprocess can be in the path of, and the measured
alternative -- interpreting bytecode per cell -- is 56ns against 8ns for the
compiled function. On trust, `shell` and a layout's `command=` already let a
file in the config directory run anything as you, so a .so beside them is
not a new boundary. The docs say so plainly rather than implying a sandbox.
contrib/shader-plugin is a working skeleton: Makefile, two commented effects
(one positional, one animated), and the three rules a shader has to keep. It
is built and loaded by the test suite, so a broken example is a failing test
rather than someone else's afternoon.
Diffstat (limited to 'HANDOVER.md')
| -rw-r--r-- | HANDOVER.md | 30 |
1 files changed, 30 insertions, 0 deletions
diff --git a/HANDOVER.md b/HANDOVER.md index 03d8a8d..422b04d 100644 --- a/HANDOVER.md +++ b/HANDOVER.md @@ -118,6 +118,31 @@ before changing anything near it. blocking `waitpid` would hang the entire session on one program that closes its terminal and keeps running. Losing the race costs the words "status 0". +## Shaders: what is measured, and what is not worth doing + +Before optimising anything here, read the numbers rather than reasoning about +them — `.scratch/shbench.c` and `.scratch/shprof.c` produce them in a second +(they are gitignored; rebuild with the command in their headers). + +What they say, per cell per pass on this box: + +- the **arithmetic is free**. `vignette` — squared distances, clamp, divide — + is *cheaper* than `dim`, which computes nothing. So precomputing an amount + per cell, the obvious optimisation, buys approximately zero. +- the cost is per-cell overhead and the colour mixing. Removing `screen_at()` + from the inner loop took the pass from 8.0ns to 6.1ns for ten lines, and + that is done. +- the next real win is **per-row dispatch** (an indirect call per row rather + than per cell, which also lets each shader hoist its row-invariant work): + measured at 4.7ns in a prototype. Not done, because it is a rewrite of every + shader and nothing needs the 1.4ns yet. +- an **interpreted** shader is 56ns against a compiled 8ns, which is the whole + argument for D15 loading native code instead of inventing a language. + +All of this is 0.24ms per frame on a 200x50 pane, under 3% of a 120Hz frame. +It has never been the bottleneck; do not spend a day here without a profile +saying otherwise. + ## Things left on the table - **A `reload` keybinding.** The config watcher made it less pressing. @@ -133,6 +158,11 @@ before changing anything near it. something you are looking at; less fine once you have six of them put away. - **Nothing re-runs a whole tab**, which is the obvious next want once one pane can be re-run. +- **Shader plugins cannot be replaced without a new session** (D15): loading + is additive on purpose, because a `shade_fn` may be live in a config or on a + pane. Hot-swapping would need every shader reference to be indirected + through the registry, which is a real change and not obviously worth it. +- **Per-row shader dispatch** — see the numbers above. - **Corner crossings**: found for both nestings now, but if more turn up missing, ask *how the layout was built* — the split order decides the tree shape, and that is where any remaining blind spot will be. |
