aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authoruser <user@clank>2026-08-16 15:37:16 +0200
committeruser <user@clank>2026-08-16 15:37:16 +0200
commita5114a9e923814878cd9c331adfe7a4a3ae5113a (patch)
treef06a3e780969eaadc59508503a06fb1f096eaf55 /src
parentsl0ppty: three ways back from a painted pane (diff)
downloadslosh-a5114a9e923814878cd9c331adfe7a4a3ae5113a.tar.gz
slosh-a5114a9e923814878cd9c331adfe7a4a3ae5113a.tar.xz
slosh-a5114a9e923814878cd9c331adfe7a4a3ae5113a.zip
sl0ppty: zero strength is not a pass
`shader_abi.h` has said `0 is identity` from the start, and the pass did not honour it: it materialised a cell's default colours -- resolving "whatever the terminal draws" into our `default_fg`/`default_bg` -- and only then called a shader that was contractually going to change nothing. So a chain that is currently *off* still repainted. `focused * 200` on an unfocused pane, `(since < 500) * 255` half a second later, `(x < 10) * 200` from column ten on: every one of those wrote our idea of the default over the terminal's own. Visible whenever the two differ, which for a real terminal theme is always, and invisible in the test suite because the tests compared against the materialised value. Found by measuring, not by reading: I was checking that each example I was about to hand over actually changed the pane, and one that should have changed nothing changed eight style runs. The fix is an ordering: work out the strength, and if it is zero leave the cell before touching it. A constant-zero shader now skips its whole rect before the loops. Cheaper too -- a pass that is off costs no materialisation and no call -- though that is a side effect and not the reason. Three test changes, all of them the same correction. `tests/shader_test.c`'s traversal probe was built `{.kind, .fn}` with `amount` defaulting to 0, so it was asking to be called for every cell at zero strength; it now asks for 255, which is what those checks are about. A new block pins the rule: a zero-strength pass visits nothing and leaves an unset cell unset. And four checks in `tests/test_shader_expr.py` asserted `== "#ffffff"` for cells outside an effect's band -- the materialised default. They now assert the cell has no colour of its own at all, which is the stronger claim and the one that would have caught this.
Diffstat (limited to 'src')
-rw-r--r--src/shader.c30
-rw-r--r--src/shader_abi.h6
2 files changed, 27 insertions, 9 deletions
diff --git a/src/shader.c b/src/shader.c
index ba2888a..56a0a2b 100644
--- a/src/shader.c
+++ b/src/shader.c
@@ -367,6 +367,15 @@ void shade_apply(screen_t *s, const shader_t *shaders, size_t n, rect_t r,
* once per pass rather than per cell, because copying a shader_t for every
* cell would cost more than evaluating the expression. */
shader_t local = *sh;
+ /* `0 is identity` (shader_abi.h), so a pass at zero strength is not a pass
+ * that runs and changes nothing -- it is one that does not run. The
+ * difference is visible: the loop below materialises a cell's default
+ * colours before calling a shader, so an identity pass would leave the
+ * terminal's own background written out as ours. A chain like
+ * `focused * 200` on an unfocused pane, or `(since < 500) * 255` half a
+ * second later, would faintly repaint the pane it is meant to be leaving
+ * alone. */
+ if (!sh->amount_expr && !sh->amount) continue;
const uint8_t *map = NULL;
expr_env_t env = {0};
bool per_cell = false;
@@ -416,6 +425,19 @@ void shade_apply(screen_t *s, const shader_t *shaders, size_t n, rect_t r,
* it would be computing a colour nothing can display. */
if (!c->width) continue;
+ /* Strength first, because zero means this cell is not this pass's
+ * business and materialising it below would be the whole of the
+ * effect. */
+ ctx.x = x;
+ if (map) {
+ local.amount = map[(size_t)y * w + x];
+ } else if (per_cell) {
+ env.x = x;
+ env.y = y;
+ local.amount = (uint8_t)clamp255(expr_eval(sh->amount_expr, &env));
+ }
+ if (!local.amount) continue;
+
/* What the cell was, for whichever colour this pass does not keep.
* Taken before materialising, so a colour the terminal was drawing in
* its own default goes back to *unset* rather than to our idea of it —
@@ -429,14 +451,6 @@ void shade_apply(screen_t *s, const shader_t *shaders, size_t n, rect_t r,
if (!c->fg.set) c->fg = ctx.default_fg;
if (!c->bg.set) c->bg = ctx.default_bg;
- ctx.x = x;
- if (map) {
- local.amount = map[(size_t)y * w + x];
- } else if (per_cell) {
- env.x = x;
- env.y = y;
- local.amount = (uint8_t)clamp255(expr_eval(sh->amount_expr, &env));
- }
local.fn(&local, &ctx, c);
if (!keep_fg) c->fg = was_fg;
diff --git a/src/shader_abi.h b/src/shader_abi.h
index dbd840d..96b7069 100644
--- a/src/shader_abi.h
+++ b/src/shader_abi.h
@@ -111,7 +111,11 @@ struct shader {
shade_fn fn;
expr_prog_t *amount_expr; /* NULL when `amount` is just a number */
color_t color; /* the target colour, for shaders that have one */
- uint8_t amount; /* strength, 0..255; 0 is identity, 255 is fully applied */
+ /* Strength, 0..255; 0 is identity, 255 is fully applied. A cell whose
+ * strength works out to 0 is skipped by the pass rather than handed to the
+ * shader, so a plugin is never called to do nothing -- and a cell the
+ * terminal was drawing in its own default colour stays that way. */
+ uint8_t amount;
/* One number whose meaning is the shader's own, because a second parameter
* that is a column for one effect and a radius for another is not really
* two things: