aboutsummaryrefslogtreecommitdiffstats
path: root/src/config.c
diff options
context:
space:
mode:
authoruser <user@clank>2026-08-14 22:39:18 +0200
committeruser <user@clank>2026-08-14 22:39:18 +0200
commitc0b88416fc9543fa348b63be07905f7a33fdbcc3 (patch)
treef669fdf1066b2ffeed7d61baae572f4834b7ce62 /src/config.c
parentsl0ppty: a tab is something you can name and move (diff)
downloadslosh-c0b88416fc9543fa348b63be07905f7a33fdbcc3.tar.gz
slosh-c0b88416fc9543fa348b63be07905f7a33fdbcc3.tar.xz
slosh-c0b88416fc9543fa348b63be07905f7a33fdbcc3.zip
sl0ppty: a shader's strength can be an expression
Adding an effect meant writing C -- in the binary, or since D15 in a shared library. Both are a lot of ceremony for "dim every other row". So: `amount` may be an expression, compiled to bytecode when the config is read and evaluated per cell. shaders { dim amount="(y % 2) * 40" // scanlines dim amount="(x > cols - 10) * 120" // a right margin tint amount="255 - dist(x, y, curx, cury) * 12" color="#ff5fd7" } The scope is the argument, and that is the whole design. Every built-in here is already colour_op(amount) with the amount a pure function of position -- vignette, zebra, ruler, margin and spotlight differ only in that function -- so the part worth handing to a config is the *number*, not the colour maths. Three things follow at once: the mixing stays in C where it is fast and correct; an expression cannot produce an invalid cell, because it produces one byte; and that byte is cacheable. The cache is what makes an interpreter affordable, and the numbers are why this exists at all. Measured on a 200x50 pane, one pass: native vignette 6.1 ns/cell constant expression, folded 5.6 ns/cell positional expression, mapped 5.2 ns/cell animated (reads the clock) 35.0 ns/cell A shader you wrote in your config costs what a compiled one does. A program that does not read the clock is evaluated once per cell into a byte map and reused until something it reads changes; one that does read the clock is interpreted per cell, per frame, which is the honest price of animation and is opt-in. (The earlier measurement of 56ns for interpretation was per evaluation -- correct, and the reason the map exists rather than an argument against the language.) What a program reads is derived by the compiler rather than declared, and that dependency set *is* the cache key. A stale map is therefore unrepresentable rather than merely unlikely -- the same rule the layout follows, arrived at from the other direction. A unit test caught the first violation of it before anything else did: `cols` and `rows` carried no dependency flag, on the theory that the key always contains the rect, and so a program reading only the size looked constant and was folded at compile time, where every size is zero. The language is deliberately total. No loops, no recursion, no jumps: even `?:` compiles to evaluating both sides and selecting, because nothing in it has an effect. Evaluation is O(program length), so a config cannot spin, and there is nothing to sandbox. Division by zero is 0 rather than a trap: an expression that is wrong should look wrong, not take the session with it. One that does not compile drops that one shader with a warning -- not the config, and not the half-strength version of what you asked for, which is what the number path did with garbage and is now also fixed. shader_t gained the compiled program, so the plugin ABI goes to 2. A plugin built against 1 is refused with "shader ABI 1, we speak 2" and needs nothing but a rebuild -- the check earning its keep the first time it was asked to.
Diffstat (limited to 'src/config.c')
-rw-r--r--src/config.c36
1 files changed, 35 insertions, 1 deletions
diff --git a/src/config.c b/src/config.c
index 80d9936..e06e25a 100644
--- a/src/config.c
+++ b/src/config.c
@@ -126,7 +126,33 @@ static size_t parse_shader_list(config_t *c, const kdl_node_t *node,
const kdl_node_t *k = node->kids[i];
if (!k || !k->name) continue;
- long amount = kdl_prop_int(k, "amount", 128);
+ /* `amount` is a number, or an expression that produces one per cell.
+ * Same key either way: `amount=90` and `amount="(y % 2) * 40"` are the
+ * same idea, one of them constant, and the compiler folds a constant
+ * expression back to a number so nothing downstream can tell. */
+ expr_prog_t *aexpr = NULL;
+ long amount = 128;
+ const char *as = kdl_prop(k, "amount", NULL);
+ if (as) {
+ char *end = NULL;
+ long v = strtol(as, &end, 10);
+ while (end && (*end == ' ' || *end == '\t')) end++;
+ if (end && !*end) {
+ amount = v;
+ } else {
+ char eerr[128] = {0};
+ aexpr = expr_compile(as, eerr, sizeof eerr);
+ if (!aexpr) {
+ /* The shader is dropped, not run at its default strength: an
+ * expression that did not compile leaves the strength *unknown*,
+ * and half-dimming a pane is a worse answer to that than doing
+ * nothing and saying why. */
+ if (err && !err[0])
+ snprintf(err, errcap, "bad amount for %s: %s", k->name, eerr);
+ continue;
+ }
+ }
+ }
if (amount < 0) amount = 0;
if (amount > 255) amount = 255;
/* Every shader that takes a number calls it something different, so accept
@@ -146,8 +172,14 @@ static size_t parse_shader_list(config_t *c, const kdl_node_t *node,
if (!shader_make_p(&out[n], k->name, col, (uint8_t)amount,
(uint16_t)param)) {
if (err && !err[0]) snprintf(err, errcap, "unknown shader: %s", k->name);
+ expr_free(aexpr);
continue;
}
+ out[n].amount_expr = aexpr;
+ if (aexpr) {
+ c->exprs = realloc(c->exprs, (c->nexprs + 1) * sizeof *c->exprs);
+ c->exprs[c->nexprs++] = aexpr; /* the config owns every program */
+ }
n++;
}
return n;
@@ -321,6 +353,8 @@ void config_defaults(config_t *c) {
}
void config_free(config_t *c) {
+ for (size_t i = 0; i < c->nexprs; i++) expr_free(c->exprs[i]);
+ free(c->exprs);
free(c->binds);
free(c->shell);
free(c->shader_dir);