feat: a dense trace's columns are graded by dwell, so density reads as brightness

Stage 1 cured the moire by collapsing a dense gathered window onto one vertical
segment per pixel column; this is the information that collapse throws away. A
column's min and max are the same whether its samples are spread evenly or piled
at its two ends, so the flat parts of a square wave and the edges between them
drew as one ink. Style::intensity_grading, which the library leaves off and this
app turns on at the first tick (the renderer does not exist until the area is
realized), grades each column by the time the signal spent at each row -- the
digital-phosphor look.

frame path: gains a graded= field, which must equal columns=: grading is a way of
drawing the reduction, not a third path. UDPSCOPE_NO_PHOSPHOR=1 puts the style
back to the library's default and is the A/B, since a frame mean cannot attribute
its own cost -- measured on the fast mock, draw 3.24 ms graded against 2.30 ms,
with segments 1340/4157 against 131/4306 over the same lod=453 frames. That A/B
is also what found the level-floor regression now fixed upstream.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Martino FerrariandClaude Opus 4.6 committed 2026-10-06 03:02:48 +02:00
1 parent 5f5b87ca60
commit b4b26d3e31
5 files changed
+112 -4

No files matched your search

+28 -2
View File
@@ -311,6 +311,9 @@ struct App {
unsigned signal_generation = ~0u;
bool updating_panels = false; ///< guards signal handlers during a rebuild
bool relayout_pending = false;
/// True once the renderer's `Style` has been stated (see `tick()`): the renderer does not
/// exist until the area is realized, so the one-shot has to be a tick's job.
bool style_stated = false;
bool signals_panel_initialised = false;
/// Set once the user edits the trigger level, so the panel stops defaulting it
/// into the middle of the declared range.
@@ -417,6 +420,11 @@ struct App {
/// produced them -- and it is the only reading that distinguishes "the segment count
/// collapsed because the reduction ran" from "the window got narrower".
int dbg_column_frames = 0;
/// Frames on which at least one reduced column was drawn **graded by dwell** rather than
/// as one flat band (`Style::intensity_grading`, which this app turns on). A subset of
/// `columns`: grading is a way of drawing the reduction, not a third path, so the two
/// counts being equal is what says the look is reaching every reduced frame.
int dbg_graded_frames = 0;
/// Painted frames on which `draw_traces` drew **no trace at all**, and the last one's index.
/// This is the observable for "the plot flickers as if there were no data": `staged_segments`
/// cannot see it, because the grid and the instruments stage geometry of their own, so a frame
@@ -1254,6 +1262,7 @@ int on_render_probe(GtkGLArea*, GdkGLContext*, gpointer user) {
if (s.raw_traces > 0) ++app->dbg_raw_frames;
if (s.lod_traces > 0) ++app->dbg_lod_frames;
if (s.column_traces > 0) ++app->dbg_column_frames;
if (s.graded_traces > 0) ++app->dbg_graded_frames;
if (s.raw_traces > 0 || s.lod_traces > 0) check_frame_ink(*app);
if (s.raw_traces == 0 && s.lod_traces == 0) {
++app->dbg_notrace_frames;
@@ -4872,6 +4881,22 @@ gboolean tick(gpointer user) {
}
}
// The trace look, stated on the first tick that finds a renderer -- there is none until
// the area is realized, and this is the same "claim it when it exists" shape as the frame
// clock above. `intensity_grading` is off in the library by default (it is a change of
// look, not a correction, and other hosts must not be restyled by an upgrade); an
// oscilloscope is exactly the host it is for, so udpscope turns it on. A one-shot rather
// than a per-frame write because `Style` is plain data and nothing else touches it.
if (!app->style_stated && app->area != nullptr) {
if (plot::Renderer* r = app->area->renderer()) {
// `UDPSCOPE_NO_PHOSPHOR` puts it back to the library's default, which is the only way
// to read `frame cost:` and `frame path:` against the ungraded path -- a frame mean
// cannot attribute its own cost, so the A/B is the measurement.
r->style().intensity_grading = g_getenv("UDPSCOPE_NO_PHOSPHOR") == nullptr;
app->style_stated = true;
}
}
if (app->relayout_pending) {
app->relayout_pending = false;
relayout_areas(*app);
@@ -8219,9 +8244,10 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
app.dbg_draw_ms_max, app.dbg_draw_max_frame,
app.dbg_paint_n > 0 ? app.dbg_paint_ms_sum / app.dbg_paint_n : 0.0,
app.dbg_paint_ms_max, app.dbg_paint_n);
std::printf("[udpscope] frame path: raw=%d lod=%d columns=%d frames, segments %llu/%u "
"(mean/max), visible max %llu samples\n",
std::printf("[udpscope] frame path: raw=%d lod=%d columns=%d graded=%d frames, segments "
"%llu/%u (mean/max), visible max %llu samples\n",
app.dbg_raw_frames, app.dbg_lod_frames, app.dbg_column_frames,
app.dbg_graded_frames,
static_cast<unsigned long long>(app.dbg_paint_n > 0
? app.dbg_seg_sum / app.dbg_paint_n
: 0),