From 5e321995978a93ad6927ba51eced410a1e11ad85 Mon Sep 17 00:00:00 2001 From: Martino Ferrari Date: Fri, 16 Oct 2026 17:38:06 +0200 Subject: [PATCH] fix: the density's ceiling comes from the fade, so the ramp is visible The library's `full_scale` now defaults to the default fade's steady state and `gamma` to 1.0 (its own commit); this side states the ceiling for the fade *it* configured, because that is what the ceiling means: the ink the display settles at, so the newest acquisition is a fraction of full ink and white is reached only where acquisitions agree. - **acquisitions**: `1/(1 - factor)` -- the geometric sum, 2.0 at the panel's 0.5, and 1.0 at 0 (only the newest sweep, at full ink). - **time**: the library's default of two sweeps. A time fade has no fixed steady state -- it settles where the trigger's rate and the constant meet, which is the *point* of it -- so a fast trigger reads brighter than a slow one the way a phosphor's does. - `gamma` is left at the library's 1.0: a cell is a count of sweeps, so half as many must read as half the ink. With the ceiling at one sweep and gamma at 0.45 (the library's first defaults) one sweep was already full white and every display looked the same -- which is exactly what the user reported seeing, and why the assertion for it is a framebuffer one in the library's case (`empty=21 full=83 quarter=54`) rather than anything `ink`/`peak` can say. The probe's own figures are unchanged in kind (they are the grid's): `on` `peak=1.0000`, `step-acquisitions` `peak=1.5000`, `step-time` `ratio=0.367879`, `off` all zeros -- re-read on this build, with the frame cost `tick 0.53 -> 7.49 ms`, `draw 1.89 -> 16.78 ms` and `gtk warnings: 0` in the same run. The trigger/pinning invariants carry the ceiling rule and why the eye found it. --- deps/plotter | 2 +- docs/claude/invariants-trigger-and-pinning.md | 11 +++++++++++ src/ui.cpp | 12 ++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/deps/plotter b/deps/plotter index 6a6915e..7382052 160000 --- a/deps/plotter +++ b/deps/plotter @@ -1 +1 @@ -Subproject commit 6a6915e64e9a9bd5e7b8c93cfa600334bf192b10 +Subproject commit 738205293eca6e77d6876dbd0ad2df1a42f26791 diff --git a/docs/claude/invariants-trigger-and-pinning.md b/docs/claude/invariants-trigger-and-pinning.md index 4723572..1c8d7bb 100644 --- a/docs/claude/invariants-trigger-and-pinning.md +++ b/docs/claude/invariants-trigger-and-pinning.md @@ -403,6 +403,17 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear * **The switch deposits what is already on screen.** `on && !density_was_on` sets the same pending flag, so a user who turns persistence on while an event is held sees it instead of waiting for a latch that `single` mode may never send. + * **The brightness ceiling is the fade's steady state, and the ramp is linear in sweeps.** The app + states `DensityConfig::full_scale` from the fade it configured -- `1/(1 - factor)` for the + acquisition fade, the library's default two sweeps for the time one, which has no fixed steady + state (it settles where the rate and the constant meet) -- and leaves `gamma` at 1.0. A ceiling + at *one* sweep is the defect a user reported as "the colour fading is not applied, it looks + white": a single sweep was already full ink, everything above it clamped to the same pixel, and + a display of one sweep and a display of five were the same white. It is also the one half that + the *grid's* own numbers cannot see -- `ink`/`peak` were right while the picture was flat -- so + the assertion is a **framebuffer** one: `a_phosphor_density_is_drawn_under_the_traces` measures a + cell at a quarter of the scale against one at the ceiling and against the background + (`empty=21 full=83 quarter=54`), which is the reading the eye asked for. * **It is the plot's, so a figure rebuild resets it** -- the grid lives on each `Plot`, and `rebuild_all()` builds new ones. That is the one state a structural change (a row added, a signal switched off, a trace recoloured) does not carry: it costs the accumulated image and nothing else, diff --git a/src/ui.cpp b/src/ui.cpp index bc77641..0f85ce4 100644 --- a/src/ui.cpp +++ b/src/ui.cpp @@ -3396,6 +3396,18 @@ void apply_density(App& app, DisplayMode display, bool live_display) { : plot::DensityDecay::PerAcquisition; cfg.time_constant_s = static_cast(tc.density_tau_s); cfg.per_acquisition = static_cast(tc.density_factor); + // The brightness ceiling: the ink this fade settles at, so the newest acquisition is a + // *fraction* of full ink and the display reaches white only where the acquisitions agree. For a + // per-acquisition fade that is the geometric sum `1/(1-f)` exactly. A time fade has no fixed + // one -- it settles where the trigger's rate and the constant meet, which is the point of it -- + // so it keeps the library's default of two sweeps, and a fast trigger then reads brighter than + // a slow one the way a phosphor's does. The gamma is left at the library's 1.0: the cell is a + // count of sweeps, so half as many sweeps must read as half the ink. (With the ceiling at one + // sweep -- the library's first default -- one sweep was already full white and the fade was + // invisible, which is exactly what a user reported seeing.) + cfg.full_scale = tc.density_decay == PersistDecay::Time + ? 2.0f + : static_cast(1.0 / std::max(1.0 - tc.density_factor, 0.05)); // The plot drops what it held when this turns the display off, so the step below only ever // speaks about an image that is on screen. p->set_density(cfg);