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:
1 parent
5f5b87ca60
commit
b4b26d3e31
5 files changed
+112
-4
No files matched your search
@@ -671,7 +671,7 @@ Two lines of the self-check output are assertions in disguise:
|
||||
blinking green. The `mode ...` lines carry the rest of the status line, so `rate` and
|
||||
`last event ... ago` are checked there.
|
||||
- `frame cost: tick A/B ms over N, draw C/D ms (max at frame F), paint E/G ms over M` and
|
||||
`frame path: raw=R lod=L columns=C frames, segments S/T (mean/max), visible max V samples`
|
||||
`frame path: raw=R lod=L columns=C graded=G frames, segments S/T (mean/max), visible max V samples`
|
||||
— the
|
||||
drawing cost, split into the frame tick, `PlotArea`'s own draw (timed from the frame
|
||||
clock's `before-paint` to the end of the host's `render` handler) and the whole GTK
|
||||
@@ -689,6 +689,16 @@ Two lines of the self-check output are assertions in disguise:
|
||||
`--rate 1300000 --batch 2000 --freq 3000` over 20 s: `raw=2 lod=1198 columns=1200 frames,
|
||||
segments 2041/2237` over 1201 painted frames, against `columns=0 ... segments
|
||||
10960/216093` over 263 before it existed — the same run at 4.6× the frame rate.
|
||||
**`graded` must equal `columns`**: this app turns `Style::intensity_grading` on, and grading is
|
||||
a way of *drawing* the reduction rather than a third path, so a `graded` below `columns` is the
|
||||
style not reaching a frame. **`UDPSCOPE_NO_PHOSPHOR=1` is the A/B** and the only way to read
|
||||
either of these two lines against the ungraded path, since a frame mean cannot attribute its own
|
||||
cost: measured on `--rate 1300000 --batch 2000`, `columns=1166 graded=1166` with
|
||||
`draw 3.24/21.43 ms` and `segments 1340/4157` against `graded=0`, `draw 2.30/25.76 ms` and
|
||||
`segments 131/4306` over the same `lod=453` frames. The segment rise *is* the gradient (alpha is
|
||||
quantised into steps, one `LineBatch` and one draw call each, because the line pipeline's colour
|
||||
is a uniform) and the ~1 ms is its cost; a `raw` that *grows* when the grading is on is the
|
||||
level-floor invariant below gone, which measured `raw=1154 lod=0` and 6.36 ms.
|
||||
**`draw`'s max must sit
|
||||
at a frame near 0** — that is the warm-up (glyph atlas, first vertex-buffer growth); a
|
||||
late `F` is a frame whose own geometry was expensive and worth tracing. `tick`'s max is
|
||||
@@ -1151,6 +1161,39 @@ far the largest file.
|
||||
column), and `frame path:`'s `columns=` field here. `trace ink:` deliberately cannot see this
|
||||
path: it recomputes the **gathered** points, which the reduction is downstream of, so its
|
||||
`drawn-vs-quoted` reads 0.0000 either way and remains the guard for the gather origin alone.
|
||||
- **A reduced column is graded by dwell, and the grading may not undo the reduction.** The
|
||||
column above is honest about *where* the signal went and silent about *how long it was there*
|
||||
— a column's min and max are identical whether its samples fill the span or pile at its two
|
||||
ends, so a ramp and a pulse are one flat band. `Style::intensity_grading` grades it instead
|
||||
(bright where the signal sits, faint where it only passes through, the digital-phosphor look),
|
||||
and **udpscope turns it on; the library's default is off**, so other hosts are not restyled by
|
||||
an upgrade. The switch is a one-shot in `tick()` guarded by `App::style_stated`, not a line in
|
||||
the window build: `PlotArea::renderer()` is null until the area is realized, so there is no
|
||||
earlier place to state a style. Three things to know here, the rest upstream in
|
||||
`docs/data-and-performance.md` (the `1/height` dwell weighting, the one-column accumulator, the
|
||||
per-column-peak normalisation, the quantised alpha steps):
|
||||
* **The finer gather level is floored at 1 whenever the flat path would have used the
|
||||
pyramid.** Grading asks for `samples_per_pixel / intensity_sub_columns`, and
|
||||
`MinMaxPyramid::level_for()` answers **0 — raw samples** — as soon as that falls under one
|
||||
bucket (`kBucket` is 64). Unfloored, every moderately dense frame became an O(visible)
|
||||
gather: at 1.3 MS/s over a 2 s window, 172 samples a pixel, `raw=1154 lod=0` against the flat
|
||||
path's `raw=710 lod=453`, and 2.30 → 6.36 ms of draw mean. The floor degrades the *look*
|
||||
(fewer sub-columns than asked for, down to what the coarsest buckets give — still two or
|
||||
three per column at 64 samples a bucket) and never the path. It is also why
|
||||
`intensity_sub_columns` past about 8 buys nothing: the bucket size is the limit.
|
||||
* **`graded=` must equal `columns=`** in `frame path:`, and `UDPSCOPE_NO_PHOSPHOR=1` is the A/B
|
||||
that makes either line readable against the ungraded path (see Verification).
|
||||
* **The steps are draw calls, not per-segment alpha.** `LineInstance` is four floats of
|
||||
geometry and the colour is a **uniform** (only `TextInstance` carries `a_colour`), so the
|
||||
gradient is `intensity_steps` `LineBatch`es flushed at different alphas. That is why the
|
||||
segment mean rises from 131 to 1340 on the same frames — the rise is the tiers, not extra
|
||||
geometry per column, and the bound is still `axes.w` (measured 14.5 runs a column in the
|
||||
library's own guard, 8303 segments over a 572 px axis).
|
||||
The guard is upstream's `a_dense_trace_can_be_graded_by_dwell`, which draws one frame both ways
|
||||
and profiles the band's outer fifths against its middle: flat reads **191/191**, graded
|
||||
**191/131**. That 0.69 is a *tier* ratio, not the dwell ratio — `intensity_gamma` (0.45) and
|
||||
`intensity_floor` (0.3) compress it deliberately, the floor so that a fast riser, one
|
||||
sub-bucket deep everywhere, is never dimmed out of existence.
|
||||
- **The time ring is `f64` and the value rings are `f32`, and the asymmetry is the point.**
|
||||
A value's precision is a property of the signal (24 bits is plenty for a sample); a
|
||||
timestamp's is a property of *how long the scope has been running*, because the stamps are
|
||||
|
||||
@@ -971,6 +971,39 @@ staircase, 0 short, worst drawn-vs-quoted 0.0000 px` either way. `frame path:`'s
|
||||
`columns=` field is the reading that separates "the segment count collapsed because the
|
||||
reduction ran" from "the window got narrower".
|
||||
|
||||
**Those columns are then graded by dwell, which is the rest of the answer.** A reduced column
|
||||
is honest about *where* the signal went and silent about *how long it was there*: its min and
|
||||
max are the same whether its samples fill the span evenly or pile at its two ends, so a ramp
|
||||
and a pulse are one flat band. `Style::intensity_grading` — off in the library, which udpscope
|
||||
turns on at the first tick, since the renderer does not exist until the area is realized —
|
||||
draws each column graded instead: bright where the signal sits, faint where it only passes
|
||||
through, the way a digital-phosphor scope reads. The distribution cannot be recovered from the
|
||||
band, so the gather is asked at a level finer than the pixel grid and the dwell read from those
|
||||
sub-buckets; the measure is dwell *per row*, each segment depositing one unit spread over the
|
||||
rows it spans, and normalisation is against the column's own peak. See the upstream
|
||||
`docs/data-and-performance.md` for the five things that carry it and for the two that were
|
||||
wrong first (counting rows at +1 each inverts the pattern, grading a sine's fast middle as
|
||||
brightly as its rails; a frame-wide scale needs a 33 MB density image *and* fades the trace out
|
||||
exactly where it moves fastest).
|
||||
|
||||
It costs about a millisecond a frame and the A/B is the only way to read that, because a frame
|
||||
mean cannot attribute its own cost: **`UDPSCOPE_NO_PHOSPHOR=1` puts the style back to the
|
||||
library's default**, and the same probe run on the fast mock (`--rate 1300000 --batch 2000`)
|
||||
reads `draw 3.24/21.43 ms` with the grading against `draw 2.30/25.76 ms` without, over the same
|
||||
`lod=453` frames. **`frame path:`'s `graded=` must equal `columns=`** — grading is a way of
|
||||
drawing the reduction and not a third path, so the two counts agreeing is what says the look
|
||||
reaches every reduced frame (`columns=1166 graded=1166` in that run, `graded=0` under the env
|
||||
var). The segment count rises with it, from a mean of 131 to 1340, which is the gradient's
|
||||
tiers: alpha is quantised into steps because the line pipeline's colour is a uniform and a
|
||||
per-segment alpha is inexpressible, and each step is its own draw call.
|
||||
|
||||
The one performance trap here is worth stating because only the A/B found it. Asking for a
|
||||
finer level drops straight to **raw samples** once a sub-column falls under one pyramid bucket
|
||||
(64 samples), which at the run's 172 samples a pixel it did: `raw=1154 lod=0` and 6.4 ms a
|
||||
frame, i.e. grading undoing the very reduction it draws. The finer ask is floored at level 1
|
||||
whenever the flat path would have used the pyramid, so what degrades is the *look* — fewer
|
||||
sub-columns than asked for — and never the path.
|
||||
|
||||
**The panels and the toolbar.** The header bar carries only what you reach for
|
||||
*while watching a signal*: the time window, pause/resume, the trigger toggle, the
|
||||
side-panel toggle and an overflow menu. Those
|
||||
|
||||
Vendored
+1
-1
Submodule deps/plotter updated: e81e84645f...cc908efa36.
@@ -42,6 +42,12 @@ The full table is in the shortcut overview (<tt>Ctrl+/</tt>).
|
||||
pixel grid — which reads as bands of light and dark that <i>crawl</i> as you pan. One line per
|
||||
column removes the beat, and it is also much faster: a 1 MS/s view costs a couple of thousand
|
||||
line segments a frame instead of a couple of hundred thousand.
|
||||
• Those columns are <i>graded by brightness</i>, the way a digital-phosphor scope reads: bright
|
||||
where the signal spent its time, faint where it only passed through. A column's highest and
|
||||
lowest value are the same whether the samples in it are spread evenly or piled at its two ends,
|
||||
so the grading is what tells the two apart — the flat parts of a square wave come out solid and
|
||||
the edges between them faint. It is read from a finer division of each column rather than from
|
||||
the line that is drawn, and it costs about a millisecond a frame.
|
||||
|
||||
<b>Example</b>
|
||||
|
||||
|
||||
+28
-2
@@ -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),
|
||||
|
||||
Reference in new issue
Block a user