improved measuraments ,cursors, zooming and user interactions
This commit is contained in:
1 parent
0eceaf537b
commit
02620ca946
12 files changed
+1488
-171
No files matched your search
@@ -35,8 +35,12 @@ drawn at a data position. `generate_ticks()` also stores the tick at the origin
|
||||
exactly `0.0` — the walk starts at `ceil(min/stride)*stride` and so never lands on zero
|
||||
when the stride is not a power of two, which labelled it `-139a`. All three carry
|
||||
upstream tests — `the_atlas_is_filtered_not_point_sampled`,
|
||||
`grid_hairlines_land_on_whole_pixels` and `the_tick_at_the_origin_reads_zero`; README
|
||||
records what they cured.
|
||||
`grid_hairlines_land_on_whole_pixels` and `the_tick_at_the_origin_reads_zero`. The minor
|
||||
grid's look is set there too, not here: `grid_minor` carries 50% alpha, `grid_minor_width` is
|
||||
0.75 (sub-pixel, expressed as coverage at 1x) and `grid_minor_dash_on`/`_off` are 3/3 — a
|
||||
fragment discard, no extra geometry — so the subdivisions read as texture under the labelled
|
||||
grid instead of as a second set of ticks (`the_minor_grid_is_thinner_dashed_and_dimmer_than_the_major`).
|
||||
README records what they cured.
|
||||
|
||||
Changing any of this means committing in the submodule and moving its pin, and running
|
||||
the library's own suite as well as the udpscope probes below:
|
||||
@@ -76,8 +80,58 @@ xvfb-run -a env UDPSCOPE_EXIT_AFTER_MS=8000 UDPSCOPE_ENABLE_ALL_MS=1000 \
|
||||
with, so a synchronous sequence would read every position one action stale. It clicks by
|
||||
emitting `::clicked`, never `gtk_widget_activate()`, which holds a `GtkButton` for its
|
||||
250 ms `ACTIVATE_TIMEOUT` and then emits into a button the rebuild has already destroyed.
|
||||
- `[measure-probe] N measurements on 'X'` (stderr, `UDPSCOPE_PROBE_MEASURE_MS`) — adds one
|
||||
measurement of each statistical type (average, top, peak to peak, amplitude) on the first
|
||||
*channel* and forces a recompute. It must name a **channel**, not `signals().front()`: the
|
||||
first signal of a stream is its timestamp, which is not acquired, and naming it adds four
|
||||
measurements that all read `--` *and* makes a performance run look free (each bails before
|
||||
collecting anything -- that is how the first baseline of the stall below read 0.4 ms).
|
||||
The values are in the exit dump as `measurements: <type>=<value>`; on the fast mock
|
||||
(`tools/mock_udpstreamer.py --rate 1300000 --batch 2000`) a unit sine must read
|
||||
`peak to peak=2`, `amplitude=1`, `average=0` and **`top=0.636620`** -- 2/pi, the half-wave
|
||||
mean of a sine, which is the check that the threshold types are not merely plausible.
|
||||
`[measure-trace] view=[..] <type>=<value>... marks=N level:V/time:V...` is a repeating line
|
||||
(250 ms) from the probe: the pane's numbers beside the *current* window and the marks actually
|
||||
placed, which is the only way a stale result or a mark that should not be drawn shows up as a
|
||||
number. It must never print `marks=0` while measurements with `show_on_plot` are on and the
|
||||
data is there -- that was the rulers blinking off at a mode change.
|
||||
`measure cost: N recomputes, X ms total, Y ms each` follows it: **read it against a run with
|
||||
`UDPSCOPE_PROBE_MEASURE_MS` unset**, because a frame mean cannot attribute a cost.
|
||||
|
||||
- `[pan-probe] asked=[..] was=[..]`, `[pan-probe] after=[..] held=D asked=[..]`, then
|
||||
`[pan-probe] zoom asked=[..] after=[..] span=S window=W limited=L`
|
||||
(stderr, `UDPSCOPE_PROBE_PAN_MS`) — **`held` must be 1**: a gesture must not move a window
|
||||
that is pinned (trigger mode, or a paused view). The probe writes the panned range the way a
|
||||
gesture does — on the *member* axis, which `Figure::layout()` adopts into the group — and
|
||||
reports the axis the frame after it produced. Time it *inside* one pinned state: a pan during
|
||||
a capture whose check lands after the capture has ended is re-framed by the mode change, not
|
||||
by the pin, and reads `held=1` with the hold removed (that is how the first version of this
|
||||
probe fooled me). The pause probe makes a stable pin, so run them together:
|
||||
`UDPSCOPE_PROBE_PAUSE_MS=1500 UDPSCOPE_PROBE_PAN_MS=2200` puts the pan and its check inside
|
||||
the pause. With `hold_pinned_window()` disabled it reads `held=0` and the axis sits at the
|
||||
asked range. It then asks for a **zoom-in** (a window a quarter of what is on screen) and
|
||||
reports `group=`/`member=` beside it: **`allowed` must be 1, with both equal to the ask** --
|
||||
that is the single-trigger-mode case, where the wheel has to keep working, and where reading
|
||||
the wrong one of the two made a working clamp look broken. All three verdicts are read from the
|
||||
*tick*, one frame after the ask: a `g_timeout` was the first version and it drifted past the
|
||||
paused/resumed boundary under load. The probe then asks for a **zoom-out** ten times the current span and reports
|
||||
the span the next frame produced: **`limited` must be 1**, i.e. `span <= window`. Run it with
|
||||
a ring that holds *more* than the window (`--buffer-mb 64` past the window's own seconds) —
|
||||
that is the state the old `max(retained, window_s_)` bound leaked in, and it showed up here as
|
||||
`span=2.502550 window=2.000000 limited=0` against `span=2.000000` after the fix.
|
||||
|
||||
- `UDPSCOPE_PROBE_PAUSE_MS` — pause acquisition mid-run so layout numbers are
|
||||
measured on a still picture.
|
||||
measured on a still picture. It now also asserts what a pause *is*
|
||||
(`[pause-probe] <action> paused=D ring_produced=N event_size=M frozen_x=[..] axis=[..]` on
|
||||
stderr):
|
||||
it presses the **button**, not `Acquisition::set_paused()`, because pausing is a display
|
||||
state as well as an acquisition one and the view snapshot and axis pin live in the
|
||||
handler. Across the three lines **`ring_produced` must grow** (the producer never
|
||||
stops — it fills the live ring exactly as it does with the trigger off), **`event_size`
|
||||
must be non-zero exactly while paused** (the plot is bound to the frozen snapshot, not
|
||||
the ring) and **`frozen_x` must not move between the pause and the resume**; after the
|
||||
resume `event_size` is 0 and the window is back on the newest data. Pausing used to stop
|
||||
the producer, which is why the counts grew by nothing and the resume left a hole.
|
||||
- `UDPSCOPE_EXPORT_MS` (+ optional `UDPSCOPE_EXPORT_PATH`, default
|
||||
`/tmp/udpscope-export`) — collect the export table and write it both ways, skipping
|
||||
only the file chooser.
|
||||
@@ -132,12 +186,17 @@ Two lines of the self-check output are assertions in disguise:
|
||||
- `[trig-probe/idx] n=.. bounds=[..] dx=.. dx_real=.. dx_ratio=R got=[a..b) want=[c..d)`
|
||||
(stderr, same probe) — **`got` must contain `want`** (`a <= c` and `b >= d`), and
|
||||
`dx_ratio` must stay within about a percent of `1.0`. `got` is the index window
|
||||
`Renderer::draw_traces` gathers, recomputed here from its own `(x - x0()) / dx()`
|
||||
*uniform grid* formula; `want` is the same window found by binary search over the real
|
||||
time ring, which is what `x_at()`/`neighbours()` — and so the crosshair — use. A
|
||||
`dx_ratio` above 1 scales every index up, `a` lands past the first visible sample, and
|
||||
the samples in between sit in the buffer, readable by the crosshair, and are never
|
||||
drawn: the live view's older part goes blank while its newest part looks fine. Also
|
||||
`Renderer::draw_traces` gathers *today* — `neighbours()`, one sample of slack either side —
|
||||
and `want` is the same window from the same call, so read this line for the **rate**:
|
||||
`dx_ratio` is the nominal `dx()` against the real last-spacing, and it is the number that
|
||||
used to matter. It above 1 (every index scaled up, `a` past the first visible sample, the
|
||||
samples in between in the buffer and readable by the crosshair but never drawn) was the
|
||||
live view whose older part went blank while its newest part looked fine. That is a *fixed*
|
||||
defect: the renderer no longer walks the grid at all, because on a ring-backed series it
|
||||
also lost the two end segments and left the left and right margins of a zoomed-in view
|
||||
empty. The guard for it is the library's own pixel test
|
||||
(`a_sparse_window_between_samples_still_inks_both_margins`), not this line — a probe that
|
||||
recomputes the renderer's formula cannot see a renderer that changed it. Also
|
||||
read `axis=` in the companion line against the `window control:` line in the dump —
|
||||
a live axis narrower than the requested window is the same symptom from the other
|
||||
cause (see the `clamp_to_data()` invariant below). Both need a *saturated* ring to
|
||||
@@ -201,6 +260,13 @@ Two lines of the self-check output are assertions in disguise:
|
||||
2 kHz at about 6%. **A `y=[-1, 1]` there means the filter is gone** and the 2 kHz sine has
|
||||
aliased to a full-amplitude 125 Hz one, which looks entirely plausible on screen. This is
|
||||
the only check that distinguishes averaging from striding.
|
||||
- `[cursors-probe] gestureless plot move with no gesture: stored A -> B ignored=D` — a
|
||||
plot-level `set_cursor_x()` with **no gesture behind it must be ignored** (`ignored=1`, and
|
||||
`A == B`): the tick reads a row only while its controller holds a cursor, and this line is the
|
||||
guard for the defect where it adopted its own echo and a dragged cursor sprang back. (The
|
||||
*adoption* half -- a gesture that does move a cursor, and the shared value reaching every row
|
||||
-- is asserted in the `check_host` harness, which can call `sync_cursors(row)` directly; a
|
||||
synthetic press here cannot reach the adapter's own event pump.)
|
||||
- `[cursors-probe] <action> mode=.. on=D pane=D t0=.. t1=.. dt=.. T=[..] lines=N [..]`
|
||||
(stderr, `UDPSCOPE_PROBE_CURSORS_MS`) — walks `vertical` → `horizontal` → `both` →
|
||||
`mix`, then moves a marker *through the plot* (`Plot::set_cursor_x(..., Free)`, which is
|
||||
@@ -272,6 +338,20 @@ Two lines of the self-check output are assertions in disguise:
|
||||
*not* every channel, and never a stream's timestamp signal, which is what an acquired-but-
|
||||
unplotted list put first and auto-selected), and `drop_h` must be a combo box's height
|
||||
(tens of pixels — a scrolled-window wrapper asked it to grow to 160).
|
||||
- `[timelabels-probe] <action> relative=D window=.. origin=.. note=[..] ticks=[..]
|
||||
first_value=.. first_rel=.. range=[..]` + the dump's `time labels: relative=D origin=..
|
||||
note=[..] x_origin_set=.. ticks=[..] first_rel=..` (stderr,
|
||||
`UDPSCOPE_PROBE_TIMELABELS_MS`) — the x label form across a zoom, driven through
|
||||
`apply_time_window()`: **under the figure's x link group the group owns the window, so a
|
||||
row-level `set_time_window()` does nothing** (a probe that changes the window must use the
|
||||
app's path). Absolute at 2 s and at 500 ms, `relative=1` below a second, and the caption
|
||||
must be non-empty exactly then. The probe *races the paint* — it reads the axis on a timer,
|
||||
and the ticks it sees are the previous layout's — so its `first_rel` (the first major minus
|
||||
the axis' origin) is a fraction of a step; the dump runs after the last paint and is the
|
||||
race-free reading. Either way the assertion that matters is the *sum*: `first_value` must
|
||||
equal `origin + first_rel` as a *tick label* plus its caption, i.e. the labels and the
|
||||
caption must share one anchor. `x_origin_set` is read back out of the plot; it must be 0 in
|
||||
the absolute form.
|
||||
- `[fit-probe] <action> window=.. x=[..] span=.. y=[..]` (stderr, `UDPSCOPE_PROBE_FIT_MS`) —
|
||||
the two fits, through the same `RowSet` calls the right-click menu wires. It first puts
|
||||
the y scale and the window where the data is *not* (a fixed `[-100, 100]`, a 50 ms
|
||||
@@ -455,6 +535,17 @@ far the largest file.
|
||||
again until the next latch. `PlotArea::create()` is handed `App::placeholder_plot`,
|
||||
never a row: the area keeps a reference to the plot it was created with, and a rebuild
|
||||
destroys every row.
|
||||
- **The cursors are read back from the plots only while a gesture is on that row.** `Cursor::x`
|
||||
is the *free* pointer position while a drag runs and the snapped value otherwise -- which is
|
||||
what the app itself wrote a frame earlier, or the snap of it. A tick that reads a row on
|
||||
every frame therefore adopts its own echo back, and -- because the vertical pair is one value
|
||||
shared by every row -- a row that has not been re-written yet hands the *previous* shared
|
||||
time straight back. That is the "drag it and it springs back" defect, and the fix is
|
||||
`RowSet::sync_cursors(int drag_row)`: the tick passes the row whose controller reports
|
||||
`grabbed_cursor() != kInvalid`, and every other frame does nothing. The values are echoed
|
||||
back with `CursorPlacement::Free` for the same reason -- the controller snaps at the end of
|
||||
the gesture (`Controller::end_drag()`), and snapping the echo fights the hand that is still
|
||||
moving the cursor.
|
||||
- **The cursors' on/off is one flag on the `RowSet`, never a copy per row.** It is one
|
||||
piece of state (the toolbar toggle) and the mode is one value; a per-row copy is something
|
||||
`insert_row()`'s `clear_row()`, or any rebuild, can clear behind the user's back, which is
|
||||
@@ -525,11 +616,24 @@ far the largest file.
|
||||
- **The trigger sees the stored (filtered) signal, not the wire signal.** `engine.process()`
|
||||
is fed the averaged value, inside the `if` that pushes it. An edge found on a sample that
|
||||
is not in the buffer would be cut from data that does not contain it.
|
||||
- **The renderer maps the visible range onto indices through the series' own x, never the
|
||||
`dx()` grid.** `deps/plotter`'s `Renderer::draw_traces` asks `Series::neighbours()` for the
|
||||
bracketing samples. A grid walk is wrong for exactly the series this app plots: `dx()` here is
|
||||
a nominal spacing (the LOD level, a coarse snap) while the real stamps are the time ring, so
|
||||
the window it computes lands inside the visible range and the two end segments — the ones that
|
||||
draw into the left and right margins — are never emitted. That is the "empty margin when
|
||||
zoomed in" defect. The library test is
|
||||
`a_sparse_window_between_samples_still_inks_both_margins` (a stub whose `dx()` lies, with the
|
||||
window strictly between two samples). It is the guard, not the probe: `[trig-probe/idx]` now
|
||||
computes `got` from the same call the renderer makes, so it cannot catch a renderer that went
|
||||
back to the grid — what it still reports is the drift that made the walk wrong (`dx_ratio`).
|
||||
|
||||
- **The `Series` grid contract does not hold for `ChannelBuffer`.** x is a streamed
|
||||
timestamp, so `dx()`/`x0()` report a nominal grid (LOD level, coarse snap) while
|
||||
`gather()`, `x_at()`, `neighbours()` and `x_bounds()` use the real time ring.
|
||||
`gather()` emits x relative to `x0()` because absolute epoch seconds do not survive
|
||||
`f32`.
|
||||
Nothing may walk that grid to find *which* samples a range covers — the renderer did,
|
||||
and it lost the two end segments (see the margin invariant above). `gather()` emits x
|
||||
relative to `x0()` because absolute epoch seconds do not survive `f32`.
|
||||
- **`gather()` emits x relative to `x0()` in *every* series here**, `EventSeries`
|
||||
included, and the renderer adds `x0()` back. An event's stamps are small (edge
|
||||
relative, `[-pre, +post]`), so emitting them absolute does not overflow anything and
|
||||
@@ -564,12 +668,18 @@ far the largest file.
|
||||
every live view. The library's `StreamSeries` is immune for a reason worth copying:
|
||||
its x is `x0 + i*dx`, a function of the absolute index, so an overwritten slot costs a
|
||||
*y* value, never the geometry. Clamping buys the same bargain here.
|
||||
- **`clamp_to_data()` may not shrink the view below the requested window.** It runs
|
||||
every frame and cannot distinguish "the user zoomed out past the data" from "the ring
|
||||
has only just started filling", so it clamps against `max(retained, window_s_)`: a
|
||||
partly filled window is left empty on its left, which is what a scope that just
|
||||
started should look like. Clamping to `retained` alone turned a requested 2 s window
|
||||
into the ~0.02 s the first frames held, permanently — nothing there grows a span back.
|
||||
- **`clamp_to_data()` caps the span at the *requested window*, never at the retained
|
||||
span.** It runs every frame and cannot distinguish "the user zoomed out past the data"
|
||||
from "the ring has only just started filling", so `window_s_` — what the window control
|
||||
states, capped by `--max-window` — is the bound. Clamping to `retained` alone turned a
|
||||
requested 2 s window into the ~0.02 s the first frames held, permanently: nothing there
|
||||
grows a span back. Loosening it to `max(retained, window_s_)` fixed that and broke the
|
||||
other state instead — while the ring is filling the two are equal, but once it holds
|
||||
more than the window the bound becomes the whole ring, and a wheel zoom-out walked the
|
||||
view into data the window was never set to show. `window_s_` is right in both: it never
|
||||
shrinks a window a caller asked for, and a partly filled buffer is left empty on its
|
||||
left, which is what a scope that just started should look like. `UDPSCOPE_PROBE_PAN_MS`
|
||||
reports it as `zoom ... span=.. window=.. limited=1`.
|
||||
Its pan clamping is likewise gated on the data being wider than the view, or the empty
|
||||
left-hand side drags the live edge off the right-hand edge.
|
||||
- **Layout must not depend on content.** The y gutter is reserved at the widest label
|
||||
@@ -596,6 +706,61 @@ far the largest file.
|
||||
out empty: any stream whose time signal advances in large steps drove it to its
|
||||
1 S/s floor, the count came out as 1, and the engine latched on the edge sample
|
||||
itself. **Do not reintroduce a rate estimate on this path.**
|
||||
- **A pinned window is *clamped* into the event every frame, and a zoom in is allowed.**
|
||||
While an event is held, a capture is filling, or the view is paused, `tick()` calls
|
||||
`RowSet::hold_pinned_window()`, which caps the span at `[-pinned_pre_, +pinned_post_]`'s and
|
||||
clamps the position into it (and states `Off`, since a following group would re-derive from a
|
||||
live edge). **A zoom in must keep working** -- "not outside the event" is not "not readable",
|
||||
and this function's first version *re-stated* the pin every frame, which made the wheel a no-op
|
||||
in single trigger mode: an acquired event could not be examined at all.
|
||||
**The request is read from the row's own axis, not from the group.** A member's `set_x_range`
|
||||
-- what the controller writes for a gesture -- is *not* adopted into the group while the follow
|
||||
is off, so a clamp that read the group only ever saw its own previous answer (the probe showed
|
||||
`group` and `member` both equal to the pin while the ask was gone). `pinned_stated_` remembers
|
||||
what the clamp last wrote: a member that matches it is our own answer coming back, one that
|
||||
differs is a gesture to honour -- which is also what keeps a *fresh* pin
|
||||
(`pin_event_window()`, `pin_pause_window()`, both of which write the group in the same tick)
|
||||
from being reverted to the member's stale window. Without it nothing bounds a drag or
|
||||
a zoom in those states: the library clamps a *following* group's offset only, and the live
|
||||
clamp is deliberately skipped here — so a gesture walked the frozen view off its axis, over
|
||||
data the scope never had. The two are one branch in `tick()` for that reason. A paused view
|
||||
pins the same way (`pin_pause_window()` sets `pinned_` from the frozen range), so the frozen
|
||||
picture is equally immovable; resuming or leaving trigger mode clears it.
|
||||
|
||||
- **Every instrument this app *computed* is out of the pointer's reach.** The trigger marker and
|
||||
the measurement marks are cursors created with `Plot::add_cursor()`/`add_h_ruler()`, and a cursor
|
||||
is grabbable by default -- so each takes `set_cursor_selectable(id, false)`. **Parking it outside
|
||||
the axis is not enough**: the controller holds a marker for the gesture's life and follows the
|
||||
pointer between frames, so a readout parked at `kParked` could still be dragged. The trigger
|
||||
marker says where the scope put the edge and a measurement mark says what a measurement measured;
|
||||
neither is something a user may move. The dump prints `readout instruments locked: N`, which must
|
||||
be 1 -- a drag is not something a probe can perform, so the state is what is asserted.
|
||||
|
||||
- **A pinned view is navigable *within its data*, and that is the bound -- not the request.**
|
||||
Two things had to agree for this, and both were wrong in the same direction:
|
||||
the **pause** pinned on the window it asked for rather than on the snapshot it got (a cut
|
||||
clamped to the ring is shorter), and the **held event** pinned on the snapshot's recorded
|
||||
`pre_s`/`post_s` -- the *request* again -- which a capture shorter than the request exceeds.
|
||||
Either way the shortfall was drawn as an empty margin, and a zoom-in moved the view *into* it,
|
||||
which is what made it look as if zooming caused it. `tick()` now re-states the pin from
|
||||
`RowSet::event_data_window()` (the bound **event series'** own stamps) while an event is held,
|
||||
from the snapshot's stamps on a pause, and `pin_pause_window()`/the pause handler set the axis
|
||||
to it as well; `[pause-probe]` prints `axis=` beside `frozen_x=` and the axis must lie inside.
|
||||
A `Capturing` view is the exception and stays on the *requested* `[-pre, +post]`: its post side
|
||||
is empty on purpose, and that is the capture filling in view, not a margin.
|
||||
|
||||
- **A pause is pinned from its snapshot's own stamps too, not from the window it requested.** The
|
||||
pause freezes the *visible window*, but `publish_pause_view()` cuts that window out of the ring
|
||||
and the cut is clamped to what the ring holds -- so a window reaching back past the oldest
|
||||
retained sample produces a *shorter* snapshot. Pinning the axis to the request left the shortfall
|
||||
as an **empty margin on the left**, visible before any zoom and made worse by one: zooming in
|
||||
moved the view *into* the empty part, which is what made it look as if zooming caused it. The pin
|
||||
is the snapshot's own stamps now. `[pause-probe]` prints `axis=` beside `frozen_x=`, and while
|
||||
paused **the axis must lie inside `frozen_x`** -- equal at the moment of the pause, a subset
|
||||
afterwards if the probe zoomed in. Before the fix the two disagreed outright:
|
||||
`axis=[-1.44..0.56]` over data that started at `0.0`. (The event pin already worked this way; the
|
||||
pause now agrees.)
|
||||
|
||||
- **The event axis is pinned from the snapshot's own stamps, not from the request.** A
|
||||
capture may legitimately be shorter than asked for — the ring need not reach back a
|
||||
full pre-trigger share on the first latch after arming, so `snap=[-0.339..1.000]`
|
||||
@@ -640,6 +805,39 @@ far the largest file.
|
||||
- **The export is collected at click time, not in the file-dialog callback.** The live
|
||||
view keeps scrolling while the chooser is up, so a late collection would write a
|
||||
window the user never saw. The writer is picked by the chosen path's extension.
|
||||
- **The measurements pane is refreshed on a cadence, and its cost is the *window's*, not the
|
||||
frame's.** A measurement is a statistic of the visible window and a live view moves that window
|
||||
every frame, so "recompute when the window moves" is "recompute every frame" at O(visible
|
||||
samples) *per measurement*. With the pane holding four measurements over a 4.7M-sample window
|
||||
that measured **13.66 ms of tick mean** (baseline 0.41 ms) and cost the run a third of its
|
||||
frames. Three things fixed it, and all three matter: `update_measures()` recomputes at most
|
||||
every `kMeasureRefreshUs` (100 ms; 400 ms when a pass costs more than `kMeasureHeavyUs`), once
|
||||
when the window settles and once per mode change -- and reading *that* backwards is what made
|
||||
it worse first, 457 recomputes in one run, because a pinned window then recomputed every
|
||||
frame; the window is materialized in bounded chunks (never two whole-window f32 temporaries)
|
||||
and the loop holds it **by reference** (copying both vectors per measurement was 300 MB of
|
||||
memcpy a frame); and `measure_stats()` is one shared pass carrying both extremes with their
|
||||
first positions, the sum and the count, so average/min/max/peak-to-peak/amplitude need no
|
||||
pass of their own. **Adding a type must not add a pass** -- that is what `compute_measure()`'s
|
||||
`stats` argument is for.
|
||||
The marks' *ink* is set where they are created (`install()` in `plot_host.cpp`): 0.75 px and
|
||||
dashed 4/4 through `Plot::set_cursor_stroke()`, which is per cursor because the trigger marker
|
||||
(also a cursor) keeps the solid `line_width_px` stroke. A new measure mark must take that
|
||||
stroke -- it is created in the same loop, so it does; a new *instrument* should not.
|
||||
The **marks** the pane draws (`set_measure_marks()` -- full-width `h_ruler`s and full-height
|
||||
cursors, i.e. the "lines across the plot") are re-placed every frame from the result, so a
|
||||
throttled pass is a *stepped* ruler: acceptable, because the ruler and the number beside it
|
||||
come from the same pass and agree. What is *not* acceptable is a ruler that appears and
|
||||
disappears, so an invalid pass falls back to the last valid result for its **level** marks and
|
||||
only drops its **time** marks: a level is a property of the signal (a mean, a top, an
|
||||
amplitude) and outlives the window it was measured in, while a time mark is a position *inside*
|
||||
that window and may not. Both are emitted in **one pass after the numbers, never inside the
|
||||
loop that computes them**: that loop has three `continue`s (an unresolvable signal, a channel
|
||||
with no window) and each one was a way for the rulers to blink off and back. Fixing one and not
|
||||
the others is how that bug survived a round of "fixes". `clamp fires: N` in the dump is the other half of this: `clamp_to_data()`
|
||||
compares with a relative tolerance, because clamping to exactly `window_s_` can leave the
|
||||
group's span an ULP above it and a clamp that fires every frame is a re-frame loop, not a
|
||||
clamp.
|
||||
- **The status line's per-second fields are measured once a second, not per frame.**
|
||||
`App::stored_rate`/`declared_rate` and the label's tooltip are refreshed on the same
|
||||
tick as the channel dump; the status line only formats what is already there. The
|
||||
|
||||
Reference in new issue
Block a user