Compare commits
3
Commits
b505706d92
...
2f4530c128
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f4530c128 | ||
|
|
39f30d913f | ||
|
|
e2985aa159 |
No files matched your search
@@ -435,20 +435,31 @@ 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-
|
*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
|
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).
|
(tens of pixels — a scrolled-window wrapper asked it to grow to 160).
|
||||||
- `[timelabels-probe] <action> relative=D window=.. origin=.. note=[..] ticks=[..]
|
- `[timelabels-probe] <action> relative=D distinct=D window=.. origin=.. note=[..] ticks=[..]
|
||||||
first_value=.. first_rel=.. range=[..]` + the dump's `time labels: relative=D origin=..
|
first_value=.. first_rel=.. range=[..]` + the dump's `time labels: relative=D distinct=D
|
||||||
note=[..] x_origin_set=.. ticks=[..] first_rel=..` (stderr,
|
origin=.. note=[..] x_origin_set=.. ticks=[..] first_rel=..` (stderr,
|
||||||
`UDPSCOPE_PROBE_TIMELABELS_MS`) — the x label form across a zoom, driven through
|
`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
|
`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
|
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
|
app's path). **`distinct` must be 1 on every line, and that is the assertion** — it says no two
|
||||||
must be non-empty exactly then. The probe *races the paint* — it reads the axis on a timer,
|
major labels on the axis are the same string, which is the one thing the relative form exists to
|
||||||
|
buy and the only reading that holds whatever the stream's uptime is. `relative=D` deliberately
|
||||||
|
is *not* fixed per window: the form is decided by the tick step against the label resolution at
|
||||||
|
the axis' **magnitude** (see the invariant below), so the same 50 ms window reads absolute at
|
||||||
|
2 s of uptime (`2.13s, 2.14s, 2.15s`) and relative at 17 s (`100µs, 10.1ms, 20.1ms`, with the
|
||||||
|
caption) — both correct, and a probe pinned to one of them would assert the stream's clock
|
||||||
|
rather than the app. The **caption must be non-empty exactly when `relative=1`**. Run it after
|
||||||
|
`UDPSCOPE_ARM_TRIGGER_MS` as well, which is the case the span rule got wrong: a triggered 50 ms
|
||||||
|
window must read `relative=0 note=[] ticks=[-20ms,-10ms,0,10ms]` — a `t0` caption on an axis
|
||||||
|
that is already edge-relative re-anchors the ticks away from the trigger, which is the one
|
||||||
|
position the axis exists to show. 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
|
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
|
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
|
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
|
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
|
caption must share one anchor. `x_origin_set` is read back out of the plot; it must be 0 in
|
||||||
the absolute form.
|
the absolute form. Note the last two steps both report `window=0.05`: `RowSet::set_time_window()`
|
||||||
|
floors the window there, so the 20 ms step is clamped.
|
||||||
- `[fit-probe] <action> pinned=D window=.. event_span=.. x=[..] span=.. y=[..]` (stderr,
|
- `[fit-probe] <action> pinned=D window=.. event_span=.. x=[..] span=.. y=[..]` (stderr,
|
||||||
`UDPSCOPE_PROBE_FIT_MS`) — the two fits, through the **menu handlers** rather than the bare
|
`UDPSCOPE_PROBE_FIT_MS`) — the two fits, through the **menu handlers** rather than the bare
|
||||||
`RowSet` calls, because the notice and the control are the handler's half. A fit is the **undo
|
`RowSet` calls, because the notice and the control are the handler's half. A fit is the **undo
|
||||||
@@ -597,10 +608,12 @@ Two lines of the self-check output are assertions in disguise:
|
|||||||
lines and the disarm probe's `axis=`/`snap=` need a fast source to say anything —
|
lines and the disarm probe's `axis=`/`snap=` need a fast source to say anything —
|
||||||
`tools/mock_udpstreamer.py --rate 1300000 --batch 20000` — since at the default 10 kS/s a
|
`tools/mock_udpstreamer.py --rate 1300000 --batch 20000` — since at the default 10 kS/s a
|
||||||
whole window fits in the raw path and the publish is too short to lose a race.
|
whole window fits in the raw path and the publish is too short to lose a race.
|
||||||
- `trace ink: N paints checked, C collapsed, S staircase, H short, worst cover P%` plus a
|
- `trace ink: N paints checked, C collapsed, S staircase, H short, worst cover P%, worst
|
||||||
|
drawn-vs-quoted D px over R raw paints` plus a
|
||||||
`[ink] paint N <flags> mode=.. axis=[..] n=.. idx=[..) spp=.. level=.. pts=.. kept=..
|
`[ink] paint N <flags> mode=.. axis=[..] n=.. idx=[..) spp=.. level=.. pts=.. kept=..
|
||||||
ink=[..] cover=..% x0=.. x_at=[..] bounds=[..] rel=[..] dx=.. kept_fixed_x0=.. drops=[..]
|
ink=[..] cover=..% x0=.. x_at=[..] bounds=[..] rel=[..] dx=.. drops=[..]
|
||||||
back=..` line per anomalous frame (stderr) — **`collapsed` and `staircase` must both be 0**.
|
back=..` line per anomalous frame (stderr) — **`collapsed` and `staircase` must both be 0**,
|
||||||
|
and **`drawn-vs-quoted` must stay far under a pixel** (0.0000 is what a clean run reads).
|
||||||
This is the only check that can see a frame whose *waveform* vanished: `staged_segments`
|
This is the only check that can see a frame whose *waveform* vanished: `staged_segments`
|
||||||
counts geometry including the grid and the instruments, and `lod_traces` counts a trace whose
|
counts geometry including the grid and the instruments, and `lod_traces` counts a trace whose
|
||||||
points are then collapsed, so both read healthy through the flicker. The probe runs on every
|
points are then collapsed, so both read healthy through the flicker. The probe runs on every
|
||||||
@@ -610,11 +623,22 @@ Two lines of the self-check output are assertions in disguise:
|
|||||||
`collapsed` is fewer than two distinct positions left (nothing to draw at all), `staircase`
|
`collapsed` is fewer than two distinct positions left (nothing to draw at all), `staircase`
|
||||||
is the filter dropping over three quarters of the points, `short` is ink reaching under a
|
is the filter dropping over three quarters of the points, `short` is ink reaching under a
|
||||||
quarter of the axis (reported only once the data spans the window, since a filling ring is
|
quarter of the axis (reported only once the data spans the window, since a filling ring is
|
||||||
*meant* to leave its left side empty). `kept_fixed_x0` is the same filter with `x0()` hoisted
|
*meant* to leave its left side empty). `drawn-vs-quoted` is the other reading, and it is the
|
||||||
out of the loop: it exceeding `kept` means `x0()` moved *during* the walk. **`back` ≈ one ring
|
one that names a *defect of position* rather than of presence: at a raw level a gathered pair
|
||||||
span is the signature** of the torn `x0()` read (see the cached-first-stamp invariant); so is
|
**is** sample `first + k`, so the line differences where a sample is drawn
|
||||||
an `ink=` range disjoint from `axis=`, or an `idx=` covering the whole ring with `cover` far
|
(`axis.min + gather[2k]`, the renderer's own reconstruction) against the x a readout quotes
|
||||||
over 100%. It is a race, so it needs load and a reason for long frames —
|
for it (`x_at()`, exact f64) and reports the worst of them in pixels. That is the
|
||||||
|
gather-origin invariant below read from the screen's side; it was over a hundred pixels at a
|
||||||
|
deep zoom before the origin became the caller's. It is measured on the raw levels only — above
|
||||||
|
them a gathered point is a bucket's envelope and has no single sample to be off from — so `R`
|
||||||
|
is legitimately below `N`. **`back` ≈ one ring span is the signature** of the torn `x0()` read
|
||||||
|
(see the cached-first-stamp invariant); so is an `ink=` range disjoint from `axis=`, or an
|
||||||
|
`idx=` covering the whole ring with `cover` far over 100%. There was a `kept_fixed_x0` field
|
||||||
|
beside `kept` — the same filter with `x0()` hoisted out of the loop, since the renderer used to
|
||||||
|
re-read `x0()` per point and a ring-backed one advances with the producer. The origin is a
|
||||||
|
local of the renderer's now, so that failure mode is structurally impossible and the field is
|
||||||
|
gone; the `[ink]` walk and the renderer still agree because both add `axis.min`. It is a race,
|
||||||
|
so it needs load and a reason for long frames —
|
||||||
`tools/mock_udpstreamer.py --rate 1300000 --batch 2000 --clock-jitter 0.4` with the
|
`tools/mock_udpstreamer.py --rate 1300000 --batch 2000 --clock-jitter 0.4` with the
|
||||||
measurements *and* cursors panes up:
|
measurements *and* cursors panes up:
|
||||||
`UDPSCOPE_ENABLE_ALL_MS=1000 UDPSCOPE_PROBE_MEASURES_MS=2000 UDPSCOPE_PROBE_CURSORS_MS=3500
|
`UDPSCOPE_ENABLE_ALL_MS=1000 UDPSCOPE_PROBE_MEASURES_MS=2000 UDPSCOPE_PROBE_CURSORS_MS=3500
|
||||||
@@ -935,15 +959,32 @@ far the largest file.
|
|||||||
`gather()`, `x_at()`, `neighbours()` and `x_bounds()` use the real time ring.
|
`gather()`, `x_at()`, `neighbours()` and `x_bounds()` use the real time ring.
|
||||||
Nothing may walk that grid to find *which* samples a range covers — the renderer did,
|
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
|
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`.
|
relative to an origin because absolute epoch seconds do not survive `f32`.
|
||||||
- **`gather()` emits x relative to `x0()` in *every* series here**, `EventSeries`
|
- **`gather()` emits x relative to an origin *the caller names*, in every series here**,
|
||||||
included, and the renderer adds `x0()` back. An event's stamps are small (edge
|
`EventSeries` included, and the consumer adds that same origin back in `f64`. Two separate
|
||||||
relative, `[-pre, +post]`), so emitting them absolute does not overflow anything and
|
rules live here and both were learned from a defect.
|
||||||
does not look like a bug — it just draws the whole capture `x0()` = one pre-trigger
|
(1) **It must be relative at all.** An event's stamps are small (edge relative,
|
||||||
window to the left of the axis. That was the "half the plot is empty / the marker sits
|
`[-pre, +post]`), so emitting them absolute does not overflow anything and does not look
|
||||||
at the end of the data / nothing is drawn but the crosshair reads values" defect: the
|
like a bug — it just draws the whole capture `x0()` = one pre-trigger window to the left of
|
||||||
crosshair reads `x_at()`, which was right all along. The probe's `data=` vs `drawn=`
|
the axis. That was the "half the plot is empty / the marker sits at the end of the data /
|
||||||
is the guard.
|
nothing is drawn but the crosshair reads values" defect: the crosshair reads `x_at()`, which
|
||||||
|
was right all along. `[trig-probe]`'s `data=` vs `drawn=` is the guard.
|
||||||
|
(2) **The origin is the *view's* minimum, not `x0()`.** `x0()` is the start of the
|
||||||
|
**retained window**, so the f32 had to resolve 2^-24 of the whole ring however little of it
|
||||||
|
was on screen: 1.9 µs over 20 s of retained data, which at a view 10 µs wide across a 716 px
|
||||||
|
axis is over a hundred pixels between where a sample is drawn and the x `x_at()` quotes for
|
||||||
|
it — and it halved on every zoom out, so it read as "zooming in makes the crosshair select a
|
||||||
|
point beside the data". `Renderer::draw_traces` passes `x_range.min`, making the error a fixed
|
||||||
|
4e-5 of a pixel at any zoom. The parameter sits **before** `out_xy` deliberately, so a call
|
||||||
|
site that was not thought about is a compile error rather than silent drift. A caller that has
|
||||||
|
no view in mind may still pass `x0()`, and `[trig-probe]`'s `drawn=`/`data=` does exactly that
|
||||||
|
on purpose: it reads the series' *whole* span, which is the widest relative value a gather can
|
||||||
|
be asked to stage. The two guards are upstream's
|
||||||
|
`a_gather_origin_at_the_view_keeps_a_deep_zoom_on_the_sample_it_quotes` and the
|
||||||
|
`trace ink:` line's `drawn-vs-quoted` figure.
|
||||||
|
**A probe that recomputes what the renderer gathers must pass the same origin the renderer
|
||||||
|
does** (`[ink]` and `[zoom-probe]` both pass the axis minimum), or its reconstruction stops
|
||||||
|
being the renderer's and its pixel readings mean nothing.
|
||||||
- **Every series bound to a trace must carry a real `MinMaxPyramid`**, including an
|
- **Every series bound to a trace must carry a real `MinMaxPyramid`**, including an
|
||||||
immutable one. `MinMaxPyramid::level_for()` descends to level 0 — raw samples — on a
|
immutable one. `MinMaxPyramid::level_for()` descends to level 0 — raw samples — on a
|
||||||
pyramid with no levels, whatever `n` is, so a series that answers `pyramid()` with an
|
pyramid with no levels, whatever `n` is, so a series that answers `pyramid()` with an
|
||||||
@@ -999,14 +1040,19 @@ far the largest file.
|
|||||||
sample 0 is the ring's oldest, the very slot the producer is about to overwrite. `time_of()`
|
sample 0 is the ring's oldest, the very slot the producer is about to overwrite. `time_of()`
|
||||||
checks the bounds and then copies, and when the producer laps the slot in between the stamp it
|
checks the bounds and then copies, and when the producer laps the slot in between the stamp it
|
||||||
returns is **one whole ring-span too new**. Everything else on a `ChannelBuffer` is read at
|
returns is **one whole ring-span too new**. Everything else on a `ChannelBuffer` is read at
|
||||||
paint time with no lock, and `x0()` is the origin the renderer reconstructs every trace point
|
paint time with no lock, and `x0()` *was* the origin the renderer reconstructed every trace
|
||||||
from (`gather[i] + series.x0()`), read both inside its increasing-suffix filter and again at
|
point from (`gather[i] + series.x0()`), read both inside its increasing-suffix filter and again
|
||||||
`stage_xy()` — so one torn read of that one slot produced three pictures, all of them "the
|
at `stage_xy()` — so one torn read of that one slot produced three pictures, all of them "the
|
||||||
waveform is gone while the grid and the axes stay": `neighbours()` tests against `x_at(0)` and
|
waveform is gone while the grid and the axes stay": `neighbours()` tests against `x_at(0)` and
|
||||||
refused the whole visible range (the renderer then gathered the *entire* ring at a coarse level
|
refused the whole visible range (the renderer then gathered the *entire* ring at a coarse level
|
||||||
and drew it off the axis — `idx=[0..8388608) level=7 cover=320%`); `x0()` changing *during* the
|
and drew it off the axis — `idx=[0..8388608) level=7 cover=320%`); `x0()` changing *during* the
|
||||||
filter loop dropped three quarters of the points; and `stage_xy()` shifted the finished polyline
|
filter loop dropped three quarters of the points; and `stage_xy()` shifted the finished polyline
|
||||||
a ring-span off its axis. That is the "with measurements and cursors active the plot flickers as
|
a ring-span off its axis. The gather origin is the *view's* minimum now (see the `gather()`
|
||||||
|
invariant), a local of the renderer's, so the second and third of those are structurally
|
||||||
|
impossible — but **the first is not**, `x_at(0)` is still what `neighbours()` tests the visible
|
||||||
|
range against, and that is the one that gathered the whole ring. The cache is load-bearing for
|
||||||
|
that reason; do not read the slot at paint time because two of its three symptoms are gone.
|
||||||
|
That is the "with measurements and cursors active the plot flickers as
|
||||||
if there were no data, at no constant frequency" defect. It is **live only** (a snapshot is
|
if there were no data, at no constant frequency" defect. It is **live only** (a snapshot is
|
||||||
immutable, so `x0()` is constant for `Held`/`Paused`), it is a race against packet arrival so it
|
immutable, so `x0()` is constant for `Held`/`Paused`), it is a race against packet arrival so it
|
||||||
is irregular, and the measurements pane makes it both likelier and larger: the pane holds
|
is irregular, and the measurements pane makes it both likelier and larger: the pane holds
|
||||||
@@ -1406,6 +1452,41 @@ far the largest file.
|
|||||||
reasons (an event pin) call `sync_window_display()` alone, which only restates the control and
|
reasons (an event pin) call `sync_window_display()` alone, which only restates the control and
|
||||||
never moves the plot — calling `apply_time_window()` there would clamp a legitimately short
|
never moves the plot — calling `apply_time_window()` there would clamp a legitimately short
|
||||||
capture up to the requested window.
|
capture up to the requested window.
|
||||||
|
- **The x labels go relative on the tick *step against the label resolution at the axis'
|
||||||
|
magnitude*, never on the span.** `RowSet::update_time_labels()`'s question is only ever "can
|
||||||
|
this step be written at this magnitude": `TickFormat::Time` carries three significant figures
|
||||||
|
below a minute, **whole seconds** through `m:ss`/`h:mm:ss` up to a day, and three figures of
|
||||||
|
days above — so the resolution is a step function of the magnitude, which `time_label_resolution()`
|
||||||
|
states and nothing else may re-derive. The old test was `span < 1.0` and it was wrong in both
|
||||||
|
directions: a 2 s window at twenty minutes of uptime reads `20:00` on every tick and stayed
|
||||||
|
absolute, and a **triggered** sub-second window — whose axis is already `[-25ms, +25ms]` about
|
||||||
|
the edge — went relative and so re-anchored its ticks *away from the trigger*, with a `t0`
|
||||||
|
caption that had nothing to explain. Three details hold it up. The step is read back from the
|
||||||
|
majors the **last layout** produced, which is not a feedback loop (a tick's value depends on the
|
||||||
|
range and the axis' pixel width, never on the label form) but *is* absent before the first
|
||||||
|
layout — and with no ticks the form must be **left alone**, because a step guessed from the span
|
||||||
|
would be latched by the hysteresis below. The hysteresis is a 1.05 band on *leaving* the relative
|
||||||
|
form. And *entering* it needs a ULP of slack (`1 - 1e-9`): a step differenced from two tick
|
||||||
|
values and a resolution from `pow(10, n)` differ in their last bits when they are the same
|
||||||
|
number, and `step == res` is a readable axis (`1.73s`, `1.74s`, `1.75s` — one in the last digit
|
||||||
|
is exactly enough), so the bare comparison picked a different form from one frame to the next.
|
||||||
|
The guard is `[timelabels-probe]`'s `distinct=1`, which asserts what the form is *for* rather
|
||||||
|
than which form a given window gets.
|
||||||
|
- **The `t0 = …` caption is chrome, never an annotation.** It is `Plot::set_x_note()` (added to
|
||||||
|
`deps/plotter` for this), drawn by `Renderer::draw_x_note()` on the legend's own
|
||||||
|
`draw_readout_panel()` backplate in the bottom-left corner of the axes, in **pixels**. It was an
|
||||||
|
`Annotation` first, and an annotation's anchor is a *data* coordinate: the caption therefore
|
||||||
|
crawled across the plot under a pan and drifted under a zoom, which is what "[t0=...s] move
|
||||||
|
slightly around when user zoom/pan" was. Three things follow. The **text is the host's**, because
|
||||||
|
the caption has to name the anchor precisely enough that label + caption is the exact absolute
|
||||||
|
time and `format_time()` would round it to three figures (`"t0 = %.6g s"`); only the **bottom
|
||||||
|
row** of the figure carries one, every other row being set to `""`; and the renderer measures it
|
||||||
|
rather than `Plot::layout()` — it reserves no space and nothing can be picked on it, so there is
|
||||||
|
no rect to read back, and a caption explaining the x labels must not be able to move the layout
|
||||||
|
those labels were measured into. The guards are the library's
|
||||||
|
`the_x_note_is_chrome_that_a_pan_or_a_zoom_does_not_move` (the footprint is the pixels a frame
|
||||||
|
with the caption differs from one without, and a pan plus a 64× zoom must give the same box to
|
||||||
|
the pixel) and `[timelabels-probe]`'s `note=`, which reads `Plot::x_note()` back.
|
||||||
- **`fit_x()` restores the *whole of what the view may show*, which is not always `window_s_`.**
|
- **`fit_x()` restores the *whole of what the view may show*, which is not always `window_s_`.**
|
||||||
Live, that is the newest `window_s_` at the live edge (`set_time_window(window_s_)`, the same
|
Live, that is the newest `window_s_` at the live edge (`set_time_window(window_s_)`, the same
|
||||||
path a resume takes). Pinned — a held event, a filling capture or a paused view — it is the
|
path a resume takes). Pinned — a held event, a filling capture or a paused view — it is the
|
||||||
|
|||||||
@@ -74,15 +74,36 @@ so the times on the axis do not change when the pause begins. Resuming drops the
|
|||||||
re-frames on the newest data, so the seconds that passed while paused are *there*, not a hole —
|
re-frames on the newest data, so the seconds that passed while paused are *there*, not a hole —
|
||||||
which is what pausing used to leave, because it stopped the producer outright.
|
which is what pausing used to leave, because it stopped the producer outright.
|
||||||
|
|
||||||
**Sub-second windows read relative.** The x ticks are absolute until the window's span goes
|
**The x ticks read relative when, and only when, they could not be written absolutely.** A
|
||||||
under a second (with a band to 1.05 s, so the form cannot flicker while a zoom is nudged), and
|
`TickFormat::Time` label carries three significant digits, so the test is the tick **step**
|
||||||
relative below it: the ticks are then generated on a grid anchored at the window's left edge —
|
against the label's own **resolution at the axis' magnitude**: `12.345671s` and `12.345672s` are
|
||||||
so they read `0`, `5ms`, `10ms` instead of `12.345671s`, `12.345672s`, which three significant
|
the same string, and there the ticks are re-anchored on a grid at the window's left edge — so
|
||||||
digits cannot even tell apart — and a caption names that anchor in the axes' top-left corner:
|
they read `0`, `5ms`, `10ms` — with a caption naming that anchor, `t0 = 12.3456 s`. Adding the
|
||||||
`t0 = 12.3456 s`. The labels scale through the prefixed units the SI formatter uses — `500ms`,
|
two is the tick's absolute time. The labels scale through the prefixed units the SI formatter
|
||||||
`500us`, `5us`, `5ns` — so a microsecond window reads in microseconds instead of `0.005ms`. Adding the two is the tick's absolute time. The caption is an annotation
|
uses — `500ms`, `500us`, `5us`, `5ns` — so a microsecond window reads in microseconds instead of
|
||||||
inside the axes rather than the axis title on purpose: its text changes with the window, and a
|
`0.005ms`.
|
||||||
title of a changing width would relayout the plot every frame. Everything a user copies out —
|
|
||||||
|
The rule used to be the *span* alone (relative under a second, with a band to 1.05 s), and a
|
||||||
|
span says nothing about whether a label can be written: it was wrong in **both** directions. A
|
||||||
|
2 s window at twenty minutes of uptime is labelled `20:00` on every tick — `m:ss` carries whole
|
||||||
|
seconds — and was left absolute and unreadable. And a **triggered** sub-second window, whose
|
||||||
|
axis is already `[-25ms, +25ms]` about the edge, reads `-20ms`, `-10ms`, `0`, `10ms` perfectly
|
||||||
|
well, yet was re-anchored anyway: the one position a triggered axis exists to show was displaced
|
||||||
|
by a `t0` caption that had nothing to explain. The step is read back from the majors the last
|
||||||
|
layout produced (a function of the range and the axis width, not of the label form, so no
|
||||||
|
feedback loop), the magnitude is the larger of the two endpoints, and the comparison carries a
|
||||||
|
1.05 band leaving the relative form plus a ULP of slack entering it — a step *equal* to the
|
||||||
|
resolution is readable (`1.73s`, `1.74s`, `1.75s`: one in the last digit is exactly enough), so a
|
||||||
|
decision taken on the last bits of two differently computed doubles picked a different form from
|
||||||
|
one frame to the next. The caption is **chrome** — `Plot::set_x_note()`, added to the plotter for
|
||||||
|
it, drawn on the legend's own backplate in the bottom-left corner of the axes — rather than the
|
||||||
|
axis title or an annotation. Not the title, because its text changes with the window and a title
|
||||||
|
of a changing width would relayout the plot every frame; not an annotation, because an
|
||||||
|
annotation's anchor is a *data* coordinate, so the caption crawled across the plot under a pan and
|
||||||
|
drifted under a zoom, which is what it was reported as. The library test is
|
||||||
|
`the_x_note_is_chrome_that_a_pan_or_a_zoom_does_not_move`: the footprint is measured as the pixels
|
||||||
|
a frame with the caption differs from the same frame without one, and a pan to another decade plus
|
||||||
|
a 64× zoom must produce the same box to the pixel. Everything a user copies out —
|
||||||
the cursors' `T0`/`T1`/`dT`, a measurement, an export — stays in absolute stream seconds.
|
the cursors' `T0`/`T1`/`dT`, a measurement, an export — stays in absolute stream seconds.
|
||||||
|
|
||||||
The anchor is taken by the *axis* on every layout (`Axis::origin_follows_range`), not written
|
The anchor is taken by the *axis* on every layout (`Axis::origin_follows_range`), not written
|
||||||
@@ -1173,6 +1194,31 @@ those are two places. The library's hover crosshair snaps the same way and draws
|
|||||||
since a snapped mark lands among those dots and at their size would read as one more sample
|
since a snapped mark lands among those dots and at their size would read as one more sample
|
||||||
instead of as the answer to the question the hover asks.
|
instead of as the answer to the question the hover asks.
|
||||||
|
|
||||||
|
**A zoomed-in trace is drawn on the x its readout quotes, which needed the gather's origin to
|
||||||
|
become the caller's.** Separately from the half-sample offset above — which is a property of
|
||||||
|
*which datum* is quoted — there was an offset in *where the polyline was drawn*, and it grew
|
||||||
|
with the zoom. `plot::data::Series::gather()` stages x as `f32`, which cannot hold an absolute
|
||||||
|
epoch time, so it emits a value relative to an origin the consumer adds back in `f64`. That
|
||||||
|
origin was fixed at `x0()`, the start of the **retained window**: the f32 then had to resolve
|
||||||
|
2^-24 of the whole ring however little of it was on screen — 1.9 µs over 20 s of retained data
|
||||||
|
— while a readout reads `x_at()`, exact `f64`. At a view 10 µs wide across a 716 px axis
|
||||||
|
(14 ns a pixel) that is over a hundred pixels between the drawn sample and the x reported for
|
||||||
|
it, and it halves with every zoom *out*, which is why it read as "zooming in makes the
|
||||||
|
crosshair pick the wrong point".
|
||||||
|
|
||||||
|
The origin is now an argument, and `Renderer::draw_traces` passes the **visible range's
|
||||||
|
minimum** — so the quantisation is 2^-24 of the span on screen, a fixed 4e-5 of a pixel, at
|
||||||
|
every zoom. The fix is upstream (`deps/plotter`), where the parameter was deliberately placed
|
||||||
|
*before* the output buffer so that every one of the library's and this app's call sites was a
|
||||||
|
compile error rather than a silent change of behaviour. One simplification fell out of it:
|
||||||
|
`stage_xy()` took a gather origin and a draw origin, those are now always the same value, and
|
||||||
|
the linear case hands the gathered buffer straight to the line batch with no per-point copy.
|
||||||
|
Two guards: the library's
|
||||||
|
`a_gather_origin_at_the_view_keeps_a_deep_zoom_on_the_sample_it_quotes`, which measures the
|
||||||
|
error in pixels both ways (under 0.05 px with the view's minimum, over 10 px with `x0()`), and
|
||||||
|
this app's `trace ink:` line, which now reports `worst drawn-vs-quoted P px` over the raw-level
|
||||||
|
paints of a whole run.
|
||||||
|
|
||||||
The readouts are a **table**: a header naming the columns, then one row per instrument —
|
The readouts are a **table**: a header naming the columns, then one row per instrument —
|
||||||
`| who | A | B | delta |`. The first row is the two times and their difference, and the
|
`| who | A | B | delta |`. The first row is the two times and their difference, and the
|
||||||
rows below it are the rulers (one per row, in `horizontal`/`both`) or the traces (in
|
rows below it are the rulers (one per row, in `horizontal`/`both`) or the traces (in
|
||||||
|
|||||||
Vendored
+1
-1
Submodule deps/plotter updated: 03c41adbec...c2c8c596f3.
+5
-5
@@ -189,7 +189,7 @@ void ChannelBuffer::raw(u64 begin, u64 end, f32* out) const {
|
|||||||
for (u64 i = count; i < end - begin; ++i) out[i] = 0.0f;
|
for (u64 i = count; i < end - begin; ++i) out[i] = 0.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChannelBuffer::gather(u32 level, u64 begin, u64 end, std::vector<f32>& out_xy,
|
void ChannelBuffer::gather(u32 level, u64 begin, u64 end, f64 x_origin, std::vector<f32>& out_xy,
|
||||||
plot::data::GatherMode mode) const {
|
plot::data::GatherMode mode) const {
|
||||||
const u64 n = pyramid_.size();
|
const u64 n = pyramid_.size();
|
||||||
const u64 b = std::min(begin, n);
|
const u64 b = std::min(begin, n);
|
||||||
@@ -197,7 +197,7 @@ void ChannelBuffer::gather(u32 level, u64 begin, u64 end, std::vector<f32>& out_
|
|||||||
if (b >= e) return;
|
if (b >= e) return;
|
||||||
|
|
||||||
const u64 origin = pyramid_.window_base();
|
const u64 origin = pyramid_.window_base();
|
||||||
const f64 base = x0(); // x is emitted relative to this (see the header)
|
const f64 base = x_origin; // x is emitted relative to this (see the header)
|
||||||
|
|
||||||
const auto emit_raw = [&](u64 from, u64 to) {
|
const auto emit_raw = [&](u64 from, u64 to) {
|
||||||
for (u64 i = from; i < to; ++i) {
|
for (u64 i = from; i < to; ++i) {
|
||||||
@@ -302,13 +302,13 @@ u64 SlotSeries::snap_index(f64 x) const {
|
|||||||
return buffer_ != nullptr ? buffer_->snap_index(x) : 0;
|
return buffer_ != nullptr ? buffer_->snap_index(x) : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void SlotSeries::gather(u32 level, u64 begin, u64 end, std::vector<f32>& out_xy,
|
void SlotSeries::gather(u32 level, u64 begin, u64 end, f64 x_origin, std::vector<f32>& out_xy,
|
||||||
plot::data::GatherMode mode) const {
|
plot::data::GatherMode mode) const {
|
||||||
if (event_ != nullptr) {
|
if (event_ != nullptr) {
|
||||||
event_->gather(level, begin, end, out_xy, mode);
|
event_->gather(level, begin, end, x_origin, out_xy, mode);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (buffer_ != nullptr) buffer_->gather(level, begin, end, out_xy, mode);
|
if (buffer_ != nullptr) buffer_->gather(level, begin, end, x_origin, out_xy, mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
void SlotSeries::copy_raw(u64 begin, u64 end, f32* out) const {
|
void SlotSeries::copy_raw(u64 begin, u64 end, f32* out) const {
|
||||||
|
|||||||
+9
-6
@@ -80,9 +80,11 @@ class ChannelBuffer final : public plot::data::Series {
|
|||||||
plot::Range x_bounds() const override;
|
plot::Range x_bounds() const override;
|
||||||
plot::Range y_bounds() const override;
|
plot::Range y_bounds() const override;
|
||||||
const plot::data::MinMaxPyramid& pyramid() const override { return pyramid_; }
|
const plot::data::MinMaxPyramid& pyramid() const override { return pyramid_; }
|
||||||
/// x is emitted **relative to `x0()`** (the library's contract): staging an
|
/// x is emitted **relative to `x_origin`** (the library's contract): staging an
|
||||||
/// absolute epoch time as f32 would quantise it far coarser than a pixel.
|
/// absolute epoch time as f32 would quantise it far coarser than a pixel, and
|
||||||
void gather(u32 level, u64 begin, u64 end, std::vector<f32>& out_xy,
|
/// staging it relative to the whole *retained window* quantises it coarser than a
|
||||||
|
/// pixel of a zoomed view -- see `plot::data::Series::gather()`.
|
||||||
|
void gather(u32 level, u64 begin, u64 end, f64 x_origin, std::vector<f32>& out_xy,
|
||||||
plot::data::GatherMode mode = plot::data::GatherMode::Envelope) const override;
|
plot::data::GatherMode mode = plot::data::GatherMode::Envelope) const override;
|
||||||
|
|
||||||
/// `copy_raw`, published so the retargetable trace series can forward the
|
/// `copy_raw`, published so the retargetable trace series can forward the
|
||||||
@@ -109,8 +111,9 @@ class ChannelBuffer final : public plot::data::Series {
|
|||||||
/// slot cannot be overwritten under the read. Everything else on this object is read at
|
/// slot cannot be overwritten under the read. Everything else on this object is read at
|
||||||
/// *paint* time with no lock, and this one slot is the ring's oldest: the one the producer is
|
/// *paint* time with no lock, and this one slot is the ring's oldest: the one the producer is
|
||||||
/// about to overwrite. Reading it there returned a stamp a whole ring-span too new whenever
|
/// about to overwrite. Reading it there returned a stamp a whole ring-span too new whenever
|
||||||
/// the producer lapped it between the bounds check and the copy -- and `x0()` is the origin
|
/// the producer lapped it between the bounds check and the copy -- and `x0()` is what
|
||||||
/// the renderer reconstructs every trace point from (`gather[i] + series.x0()`), so one such
|
/// `neighbours()` tests the visible range against, as well as the origin the renderer
|
||||||
|
/// reconstructed every trace point from before the caller named one, so one such
|
||||||
/// read moved the entire polyline a ring-span off its axis (an invisible trace) or, caught
|
/// read moved the entire polyline a ring-span off its axis (an invisible trace) or, caught
|
||||||
/// inside the renderer's increasing-suffix filter, made it drop most of the points. That is
|
/// inside the renderer's increasing-suffix filter, made it drop most of the points. That is
|
||||||
/// the "live plot flickers as if there were no data" defect. The cached value is also the
|
/// the "live plot flickers as if there were no data" defect. The cached value is also the
|
||||||
@@ -163,7 +166,7 @@ class SlotSeries final : public plot::data::Series {
|
|||||||
plot::Range x_bounds() const override;
|
plot::Range x_bounds() const override;
|
||||||
plot::Range y_bounds() const override;
|
plot::Range y_bounds() const override;
|
||||||
const plot::data::MinMaxPyramid& pyramid() const override;
|
const plot::data::MinMaxPyramid& pyramid() const override;
|
||||||
void gather(u32 level, u64 begin, u64 end, std::vector<f32>& out_xy,
|
void gather(u32 level, u64 begin, u64 end, f64 x_origin, std::vector<f32>& out_xy,
|
||||||
plot::data::GatherMode mode = plot::data::GatherMode::Envelope) const override;
|
plot::data::GatherMode mode = plot::data::GatherMode::Envelope) const override;
|
||||||
bool streaming() const noexcept override { return event_ == nullptr && buffer_ != nullptr; }
|
bool streaming() const noexcept override { return event_ == nullptr && buffer_ != nullptr; }
|
||||||
|
|
||||||
|
|||||||
+12
-12
@@ -108,7 +108,7 @@ class EventSeries final : public plot::data::Series {
|
|||||||
/// Indices need no rebasing: the pyramid is built from sample 0 and nothing is ever
|
/// Indices need no rebasing: the pyramid is built from sample 0 and nothing is ever
|
||||||
/// dropped from it, so `window_base()` is 0 and a window index *is* the absolute one
|
/// dropped from it, so `window_base()` is 0 and a window index *is* the absolute one
|
||||||
/// (unlike the ring, whose window slides).
|
/// (unlike the ring, whose window slides).
|
||||||
void gather(u32 level, u64 begin, u64 end, std::vector<f32>& out_xy,
|
void gather(u32 level, u64 begin, u64 end, f64 x_origin, std::vector<f32>& out_xy,
|
||||||
plot::data::GatherMode mode =
|
plot::data::GatherMode mode =
|
||||||
plot::data::GatherMode::Envelope) const override {
|
plot::data::GatherMode::Envelope) const override {
|
||||||
if (channel_ >= snap_->values.size()) return;
|
if (channel_ >= snap_->values.size()) return;
|
||||||
@@ -118,17 +118,17 @@ class EventSeries final : public plot::data::Series {
|
|||||||
const u64 b = std::min(begin, n);
|
const u64 b = std::min(begin, n);
|
||||||
const u64 e = std::min(std::max(end, b), n);
|
const u64 e = std::min(std::max(end, b), n);
|
||||||
if (b >= e) return;
|
if (b >= e) return;
|
||||||
// x **relative to x0()**, which is the library's gather contract: the renderer
|
// x **relative to `x_origin`**, which is the library's gather contract: the
|
||||||
// adds `x0()` back before mapping to the screen. Emitting the absolute stamp made
|
// renderer adds that same origin back before mapping to the screen. Emitting the
|
||||||
// it add x0 twice, so the whole capture was drawn one `x0()` -- a full
|
// absolute stamp made it add an origin twice, so the whole capture was drawn one
|
||||||
// pre-trigger window -- to the left of the axis it had just been measured
|
// `x0()` -- a full pre-trigger window -- to the left of the axis it had just been
|
||||||
// against. The event's own stamps are small (edge-relative, so [-pre, +post]),
|
// measured against. The event's own stamps are small (edge-relative, so
|
||||||
// which is why that shift looked like a plausible picture: with a 50/50 split the
|
// [-pre, +post]), which is why that shift looked like a plausible picture: with a
|
||||||
// right half of the plot was simply empty, zooming out showed the trace ending at
|
// 50/50 split the right half of the plot was simply empty, zooming out showed the
|
||||||
// the x = 0 marker, and zooming in past the shift showed nothing at all while the
|
// trace ending at the x = 0 marker, and zooming in past the shift showed nothing
|
||||||
// crosshair -- which reads `x_at()`, never `gather()` -- went on reporting the
|
// at all while the crosshair -- which reads `x_at()`, never `gather()` -- went on
|
||||||
// right values.
|
// reporting the right values.
|
||||||
const f64 base = x0();
|
const f64 base = x_origin;
|
||||||
const auto rel = [&](u64 i) {
|
const auto rel = [&](u64 i) {
|
||||||
return static_cast<f32>(stamps[static_cast<std::size_t>(i)] - base);
|
return static_cast<f32>(stamps[static_cast<std::size_t>(i)] - base);
|
||||||
};
|
};
|
||||||
|
|||||||
+68
-26
@@ -30,10 +30,23 @@ constexpr plot::u32 kRulerBColor = 0x8FA8C8u;
|
|||||||
/// The measurements' own colour, distinct from both cursor rulers so a readout mark cannot be
|
/// The measurements' own colour, distinct from both cursor rulers so a readout mark cannot be
|
||||||
/// mistaken for something the user placed.
|
/// mistaken for something the user placed.
|
||||||
constexpr plot::u32 kMeasureColor = 0x9BE8A0u;
|
constexpr plot::u32 kMeasureColor = 0x9BE8A0u;
|
||||||
/// The window-start caption's colour: quiet chrome, brighter than a grid line.
|
|
||||||
constexpr plot::u32 kTimeNoteColor = 0xC8C8C8u;
|
|
||||||
constexpr plot::u32 kTriggerColor = 0xFF5B5Bu;
|
constexpr plot::u32 kTriggerColor = 0xFF5B5Bu;
|
||||||
|
|
||||||
|
/// The smallest difference a `TickFormat::Time` label can *write* at the magnitude `t` -- what
|
||||||
|
/// the last digit of a label at that magnitude is worth. `format_time` writes three significant
|
||||||
|
/// figures below a minute (of the SI-prefixed mantissa under a second, of the seconds above it),
|
||||||
|
/// whole seconds through `m:ss`/`h:mm:ss` up to a day, and three figures of days above. So the
|
||||||
|
/// resolution is a step function of the *magnitude* and not of the span -- which is the whole
|
||||||
|
/// reason a relative axis is ever needed, and the reason a span threshold alone gets it wrong in
|
||||||
|
/// both directions.
|
||||||
|
double time_label_resolution(double t) {
|
||||||
|
t = std::fabs(t);
|
||||||
|
if (!std::isfinite(t) || !(t > 0.0)) return 0.0;
|
||||||
|
if (t >= 86400.0) return 86400.0 * std::pow(10.0, std::floor(std::log10(t / 86400.0)) - 2.0);
|
||||||
|
if (t >= 60.0) return 1.0;
|
||||||
|
return std::pow(10.0, std::floor(std::log10(t)) - 2.0);
|
||||||
|
}
|
||||||
|
|
||||||
constexpr double kParked = -1.0e12; ///< hides an instrument without removing it
|
constexpr double kParked = -1.0e12; ///< hides an instrument without removing it
|
||||||
/// The measurement marks' stroke: thinner than the chrome that frames the plot (a trace is 1.5,
|
/// The measurement marks' stroke: thinner than the chrome that frames the plot (a trace is 1.5,
|
||||||
/// the axis lines and the trigger marker `Style::line_width_px`), and broken, so a readout over
|
/// the axis lines and the trigger marker `Style::line_width_px`), and broken, so a readout over
|
||||||
@@ -155,11 +168,6 @@ void RowSet::install(Row& r, int /*row_index*/, plot::Plot& p) {
|
|||||||
r.plot->set_cursor_stroke(r.measure_v[mi], kMeasureStrokeWidth,
|
r.plot->set_cursor_stroke(r.measure_v[mi], kMeasureStrokeWidth,
|
||||||
plot::Vec2{kMeasureDashOn, kMeasureDashOff});
|
plot::Vec2{kMeasureDashOn, kMeasureDashOff});
|
||||||
}
|
}
|
||||||
// The window-start caption for the relative x labels: an annotation, hidden until the
|
|
||||||
// labels are relative. Inside the axes on purpose -- its text changes with the window, and
|
|
||||||
// an axis title of a changing width would relayout the plot every frame.
|
|
||||||
r.time_origin_note = r.plot->add_annotation(
|
|
||||||
plot::Annotation{0.0, 0.0, "", plot::Color::hex(kTimeNoteColor), r.axis_x, r.axis_y, false});
|
|
||||||
r.trigger_marker = r.plot->add_cursor(kParked, plot::Color::hex(kTriggerColor));
|
r.trigger_marker = r.plot->add_cursor(kParked, plot::Color::hex(kTriggerColor));
|
||||||
// A marker, not an instrument: it must sit exactly on the edge's time, so its
|
// A marker, not an instrument: it must sit exactly on the edge's time, so its
|
||||||
// snap is Free and the controller never rounds it onto a sample.
|
// snap is Free and the controller never rounds it onto a sample.
|
||||||
@@ -509,12 +517,48 @@ bool RowSet::update_time_labels() {
|
|||||||
const double span = view.span();
|
const double span = view.span();
|
||||||
if (!(span > 0.0) || !std::isfinite(span)) return false;
|
if (!(span > 0.0) || !std::isfinite(span)) return false;
|
||||||
|
|
||||||
// Sub-second windows read relative: `TickFormat::Time` writes three significant digits, so
|
// A relative axis is the answer to one problem only: a tick step the label form cannot write.
|
||||||
// `12.345671s` and `12.345672s` are the same string and a 1 ms step around a multi-second
|
// `TickFormat::Time` carries three significant digits, so a 1 ms step around a 12 s stamp
|
||||||
// time cannot be labelled at all. The band is the switch's hysteresis -- entering below a
|
// writes `12.3s` on every tick -- there the ticks have to be re-anchored and the anchor named.
|
||||||
// second, leaving only above 1.05 -- so a span sitting on the boundary cannot flicker
|
// The test is therefore the *step against the resolution at the axis' own magnitude*, never the
|
||||||
// between two forms while a zoom is nudged.
|
// span: the two part company in both directions, and both cases are real here. A 2 s window at
|
||||||
const bool want_relative = relative_x_ ? span < 1.05 : span < 1.0;
|
// twenty minutes of uptime is labelled `20:00` throughout and needs the anchor although its
|
||||||
|
// span is wide; a 50 ms trigger window spanning [-25ms, +25ms] is labelled `-20ms .. +20ms`
|
||||||
|
// perfectly well and needs nothing, although its span is narrow. That second case is what made
|
||||||
|
// a triggered sub-second view carry a `t0` caption *and* re-anchor its ticks away from the
|
||||||
|
// edge, which is the one position a triggered axis exists to show.
|
||||||
|
// The 1.05 band is the switch's hysteresis, so a step sitting on the boundary cannot flicker
|
||||||
|
// between two forms while a zoom is nudged, and the `1 - 1e-9` is a slack the *entry* needs for
|
||||||
|
// its own reason: a step read back from two tick values and a resolution from `pow(10, n)`
|
||||||
|
// differ in their last bits even when they are the same number, and `step == res` is a readable
|
||||||
|
// axis (`1.73s, 1.74s, 1.75s` -- one in the last digit is exactly enough), so a decision taken
|
||||||
|
// on those bits picked a different form from one frame to the next.
|
||||||
|
const double mag = std::max(std::fabs(view.min), std::fabs(view.max));
|
||||||
|
const double res = time_label_resolution(mag);
|
||||||
|
// The step the labels are actually written at: the majors the *last* layout produced. They are
|
||||||
|
// a function of the range and the axis' pixel width, not of the label form, so reading them
|
||||||
|
// back here is no feedback loop.
|
||||||
|
double step = 0.0;
|
||||||
|
{
|
||||||
|
const plot::Axis& ax = first.plot->x_axis(first.axis_x);
|
||||||
|
double prev = 0.0;
|
||||||
|
bool have = false;
|
||||||
|
for (const plot::Tick& t : ax.ticks) {
|
||||||
|
if (!t.major) continue;
|
||||||
|
if (have) {
|
||||||
|
step = t.value - prev;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
prev = t.value;
|
||||||
|
have = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// No ticks yet -- the frames before the first layout. The form is a property of the labels, so
|
||||||
|
// with no labels there is nothing to decide, and keeping the current one is right where
|
||||||
|
// guessing a step from the span is not: a guess that entered the relative form would be
|
||||||
|
// *latched* there by the hysteresis above.
|
||||||
|
const bool want_relative =
|
||||||
|
step > 0.0 ? step < res * (relative_x_ ? 1.05 : 1.0 - 1.0e-9) : relative_x_;
|
||||||
const bool changed = want_relative != relative_x_;
|
const bool changed = want_relative != relative_x_;
|
||||||
relative_x_ = want_relative;
|
relative_x_ = want_relative;
|
||||||
|
|
||||||
@@ -530,17 +574,12 @@ bool RowSet::update_time_labels() {
|
|||||||
r.plot->set_x_axis_origin_from_range(r.axis_x, relative_x_);
|
r.plot->set_x_axis_origin_from_range(r.axis_x, relative_x_);
|
||||||
if (!relative_x_) r.plot->set_x_axis_origin(r.axis_x, 0.0);
|
if (!relative_x_) r.plot->set_x_axis_origin(r.axis_x, 0.0);
|
||||||
// The caption belongs with the labels, which a figure draws once, on the bottom row (the
|
// The caption belongs with the labels, which a figure draws once, on the bottom row (the
|
||||||
// one whose x axis is the figure's). Every row gets it parked.
|
// one whose x axis is the figure's). Every other row gets an empty one, which draws nothing.
|
||||||
const bool bottom = i == rows_ - 1;
|
const bool bottom = i == rows_ - 1;
|
||||||
if (r.time_origin_note >= r.plot->annotation_count()) continue;
|
if (!relative_x_ || !bottom) {
|
||||||
plot::Annotation note = r.plot->annotation(r.time_origin_note);
|
r.plot->set_x_note("");
|
||||||
note.visible = relative_x_ && bottom;
|
continue;
|
||||||
if (note.visible) {
|
}
|
||||||
// Inside the axes, just above the axis line, at their left edge: the reader adds this
|
|
||||||
// to a tick, so it sits beside the ticks.
|
|
||||||
const plot::Range yr = r.plot->y_range(r.axis_y);
|
|
||||||
note.x = view.min;
|
|
||||||
note.y = yr.min + 0.04 * (yr.span() > 0.0 ? yr.span() : 1.0);
|
|
||||||
char text[64];
|
char text[64];
|
||||||
// The anchor the *drawn* labels were built with: the axis' own origin, which the last
|
// The anchor the *drawn* labels were built with: the axis' own origin, which the last
|
||||||
// layout set from its range. Reading it back (rather than keeping our own copy) is what
|
// layout set from its range. Reading it back (rather than keeping our own copy) is what
|
||||||
@@ -548,9 +587,12 @@ bool RowSet::update_time_labels() {
|
|||||||
const double shown = r.plot->x_axis_origin(r.axis_x);
|
const double shown = r.plot->x_axis_origin(r.axis_x);
|
||||||
x_origin_ = shown;
|
x_origin_ = shown;
|
||||||
std::snprintf(text, sizeof(text), "t0 = %.6g s", shown);
|
std::snprintf(text, sizeof(text), "t0 = %.6g s", shown);
|
||||||
note.text = text;
|
// Chrome, not an annotation: an annotation is anchored to a *data* coordinate, so the caption
|
||||||
}
|
// crawled across the plot as the view panned and drifted as it zoomed -- and it came with the
|
||||||
r.plot->set_annotation(r.time_origin_note, note);
|
// annotation's own pin cross, which pointed at nothing. `set_x_note()` places it in the axes'
|
||||||
|
// bottom-left corner in pixels, on the legend's backplate, which is where the labels it
|
||||||
|
// qualifies are.
|
||||||
|
r.plot->set_x_note(text);
|
||||||
}
|
}
|
||||||
return changed;
|
return changed;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,11 +106,6 @@ class RowSet {
|
|||||||
/// it keeps its position whatever the cursor toggle says.
|
/// it keeps its position whatever the cursor toggle says.
|
||||||
plot::u32 trigger_marker = 0;
|
plot::u32 trigger_marker = 0;
|
||||||
|
|
||||||
/// The caption naming the window's start while the x labels are relative: an annotation
|
|
||||||
/// inside the axes, so its text can change every frame without moving the layout (an axis
|
|
||||||
/// *title* would, and it would be a different width each time).
|
|
||||||
plot::u32 time_origin_note = 0;
|
|
||||||
|
|
||||||
/// The measurements' readout marks: a pool created with the plot (a `Plot` cannot add or
|
/// The measurements' readout marks: a pool created with the plot (a `Plot` cannot add or
|
||||||
/// drop a cursor after the fact) and parked unless a measurement is using the slot.
|
/// drop a cursor after the fact) and parked unless a measurement is using the slot.
|
||||||
std::array<plot::u32, kMeasureMarks> measure_h{};
|
std::array<plot::u32, kMeasureMarks> measure_h{};
|
||||||
|
|||||||
+99
-65
@@ -418,6 +418,14 @@ struct App {
|
|||||||
int dbg_ink_staircase = 0; ///< the filter dropped over three quarters of the points
|
int dbg_ink_staircase = 0; ///< the filter dropped over three quarters of the points
|
||||||
int dbg_ink_short = 0; ///< the ink reached under a quarter of the axis' width
|
int dbg_ink_short = 0; ///< the ink reached under a quarter of the axis' width
|
||||||
double dbg_ink_worst_cover = 1.0;
|
double dbg_ink_worst_cover = 1.0;
|
||||||
|
/// The worst distance, in pixels, between where a sample is *drawn* and the x a readout
|
||||||
|
/// quotes for the same sample. `gather()` stages x as an f32 relative to an origin the
|
||||||
|
/// renderer names, while `Plot::crosshair_point()` reads `x_at()` in exact f64, so this is
|
||||||
|
/// the quantisation of that f32 expressed in the unit the user sees it in. Measured on the
|
||||||
|
/// raw levels only, where a gathered point *is* a sample; above them a point is a bucket's
|
||||||
|
/// envelope and has no single sample to be off from.
|
||||||
|
double dbg_ink_worst_offset_px = 0.0;
|
||||||
|
int dbg_ink_raw_checked = 0; ///< frames the figure above was measured over
|
||||||
int dbg_ink_reports = 0;
|
int dbg_ink_reports = 0;
|
||||||
/// Frames the badge spent in each `BadgePhase`. Both `lit` and `dim` have to be non-zero
|
/// Frames the badge spent in each `BadgePhase`. Both `lit` and `dim` have to be non-zero
|
||||||
/// over a run that acquired anything -- that is what "it blinks" means, and a badge
|
/// over a run that acquired anything -- that is what "it blinks" means, and a badge
|
||||||
@@ -1093,21 +1101,19 @@ void check_frame_ink(App& app) {
|
|||||||
const plot::u32 level = spp >= 64.0 ? s->pyramid().level_for(n, spp) : 0u;
|
const plot::u32 level = spp >= 64.0 ? s->pyramid().level_for(n, spp) : 0u;
|
||||||
std::vector<plot::f32>& xy = app.measure_ink_scratch;
|
std::vector<plot::f32>& xy = app.measure_ink_scratch;
|
||||||
xy.clear();
|
xy.clear();
|
||||||
s->gather(level, first, last, xy);
|
// The same origin the renderer names (`x_range.min`), so the reconstruction below is its
|
||||||
|
// reconstruction: see `plot::data::Series::gather()` for why the caller owns it.
|
||||||
|
s->gather(level, first, last, axis.min, xy);
|
||||||
const std::size_t pts = xy.size() / 2;
|
const std::size_t pts = xy.size() / 2;
|
||||||
|
|
||||||
std::size_t kept = 0;
|
std::size_t kept = 0;
|
||||||
double lo = 0.0;
|
double lo = 0.0;
|
||||||
double hi = 0.0;
|
double hi = 0.0;
|
||||||
// Where the filter's drops fall, and how far back each one steps. The renderer reconstructs
|
// Where the filter's drops fall, and how far back each one steps. The origin is a constant
|
||||||
// every point's absolute x as `gather[i] + series.x0()` **inside** this loop, so an `x0()`
|
// for the walk's duration *by construction* now -- the renderer reconstructs
|
||||||
// that is not constant for the loop's duration shifts the comparison under itself -- and on a
|
// `gather[i] + x_range.min`, a local of its own, where it used to re-read `series.x0()` on
|
||||||
// ring-backed series `x0()` is `time_of(window_base)`, which *clamps to the oldest readable
|
// every point and a ring-backed `x0()` advances with the producer. So a drop here is a
|
||||||
// slot* and therefore advances with the producer. The two readings that separate that from a
|
// genuinely out-of-order gather, and `back` says by how much.
|
||||||
// genuinely out-of-order gather are the drops' *positions* (spread through the points, or
|
|
||||||
// bunched at the front where the clamp lives) and `back` (one sample's spacing, or a packet's
|
|
||||||
// worth). So the loop is run twice: once the renderer's way, and once with `x0()` read a
|
|
||||||
// single time.
|
|
||||||
std::size_t drop_first = 0;
|
std::size_t drop_first = 0;
|
||||||
std::size_t drop_last = 0;
|
std::size_t drop_last = 0;
|
||||||
double worst_back = 0.0;
|
double worst_back = 0.0;
|
||||||
@@ -1115,7 +1121,7 @@ void check_frame_ink(App& app) {
|
|||||||
{
|
{
|
||||||
double next = std::numeric_limits<double>::infinity();
|
double next = std::numeric_limits<double>::infinity();
|
||||||
for (std::size_t k = pts; k > 0; --k) {
|
for (std::size_t k = pts; k > 0; --k) {
|
||||||
const double x = double(xy[2 * (k - 1)]) + s->x0();
|
const double x = double(xy[2 * (k - 1)]) + axis.min;
|
||||||
if (!(x < next)) {
|
if (!(x < next)) {
|
||||||
worst_back = std::max(worst_back, x - next);
|
worst_back = std::max(worst_back, x - next);
|
||||||
if (!any_drop) drop_last = k - 1;
|
if (!any_drop) drop_last = k - 1;
|
||||||
@@ -1129,18 +1135,24 @@ void check_frame_ink(App& app) {
|
|||||||
++kept;
|
++kept;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
std::size_t kept_fixed = 0;
|
|
||||||
{
|
|
||||||
const double base = s->x0(); // one read, held for the whole walk
|
|
||||||
double next = std::numeric_limits<double>::infinity();
|
|
||||||
for (std::size_t k = pts; k > 0; --k) {
|
|
||||||
const double x = double(xy[2 * (k - 1)]) + base;
|
|
||||||
if (!(x < next)) continue;
|
|
||||||
next = x;
|
|
||||||
++kept_fixed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
++app.dbg_ink_checked;
|
++app.dbg_ink_checked;
|
||||||
|
// Drawn against quoted, in pixels. At level 0 a gathered pair *is* sample `first + k`, so
|
||||||
|
// this is the one place the two numbers the user compares by eye -- the polyline's position
|
||||||
|
// and the crosshair's readout -- can be differenced. The origin is what decides it: the f32
|
||||||
|
// resolves 2^-24 of whatever span it is relative to, so a view narrower than that fraction
|
||||||
|
// of the *retained window* drew a sample visibly away from the x quoted for it, which is
|
||||||
|
// what "when zooming there is a visible offset between the real data point and the crosshair
|
||||||
|
// selected one" was.
|
||||||
|
if (level == 0 && pts > 0) {
|
||||||
|
const double px_per_x = axes_w / axis.span();
|
||||||
|
double worst = 0.0;
|
||||||
|
for (std::size_t k = 0; k < pts; ++k) {
|
||||||
|
const double drawn = axis.min + double(xy[2 * k]);
|
||||||
|
worst = std::max(worst, std::abs(drawn - s->x_at(first + k)) * px_per_x);
|
||||||
|
}
|
||||||
|
app.dbg_ink_worst_offset_px = std::max(app.dbg_ink_worst_offset_px, worst);
|
||||||
|
++app.dbg_ink_raw_checked;
|
||||||
|
}
|
||||||
const double cover = kept >= 2 ? (hi - lo) / axis.span() : 0.0;
|
const double cover = kept >= 2 ? (hi - lo) / axis.span() : 0.0;
|
||||||
app.dbg_ink_worst_cover = std::min(app.dbg_ink_worst_cover, cover);
|
app.dbg_ink_worst_cover = std::min(app.dbg_ink_worst_cover, cover);
|
||||||
const bool collapse = kept < 2;
|
const bool collapse = kept < 2;
|
||||||
@@ -1155,22 +1167,23 @@ void check_frame_ink(App& app) {
|
|||||||
if ((collapse || staircase || short_ink) && app.dbg_ink_reports < 40) {
|
if ((collapse || staircase || short_ink) && app.dbg_ink_reports < 40) {
|
||||||
++app.dbg_ink_reports;
|
++app.dbg_ink_reports;
|
||||||
// The series' own answers beside the reconstruction, because the two disagreeing is itself
|
// The series' own answers beside the reconstruction, because the two disagreeing is itself
|
||||||
// a reading: `rel` is what `gather()` emitted (relative to the `x0()` *it* read) and `x0`
|
// a reading: `rel` is what `gather()` emitted (relative to `axis.min`, the origin it was
|
||||||
// is the `x0()` read here one call later, so a trace whose ink sits a whole ring-span off
|
// given) while `x0`/`x_at`/`bounds` are the window the series says it holds, so a trace
|
||||||
// its axis separates into "the stamps moved under us" and "the window is the wrong one".
|
// whose ink sits a whole ring-span off its axis separates into "the stamps moved under us"
|
||||||
|
// and "the window is the wrong one".
|
||||||
const plot::Range xb = s->x_bounds();
|
const plot::Range xb = s->x_bounds();
|
||||||
std::fprintf(stderr,
|
std::fprintf(stderr,
|
||||||
"[ink] paint %d %s%s%s mode=%s axis=[%.6f..%.6f] n=%llu idx=[%llu..%llu) "
|
"[ink] paint %d %s%s%s mode=%s axis=[%.6f..%.6f] n=%llu idx=[%llu..%llu) "
|
||||||
"spp=%.1f level=%u pts=%zu kept=%zu ink=[%.6f..%.6f] cover=%.1f%% "
|
"spp=%.1f level=%u pts=%zu kept=%zu ink=[%.6f..%.6f] cover=%.1f%% "
|
||||||
"x0=%.6f x_at=[%.6f..%.6f] bounds=[%.6f..%.6f] rel=[%.6f..%.6f] dx=%.3g "
|
"x0=%.6f x_at=[%.6f..%.6f] bounds=[%.6f..%.6f] rel=[%.6f..%.6f] dx=%.3g "
|
||||||
"kept_fixed_x0=%zu drops=[%zu..%zu] back=%.3g\n",
|
"drops=[%zu..%zu] back=%.3g\n",
|
||||||
app.dbg_paint_n, collapse ? "collapse " : "", staircase ? "staircase " : "",
|
app.dbg_paint_n, collapse ? "collapse " : "", staircase ? "staircase " : "",
|
||||||
short_ink ? "short " : "", display_mode_name(app.dbg_display), axis.min, axis.max,
|
short_ink ? "short " : "", display_mode_name(app.dbg_display), axis.min, axis.max,
|
||||||
(unsigned long long)n, (unsigned long long)first, (unsigned long long)last, spp,
|
(unsigned long long)n, (unsigned long long)first, (unsigned long long)last, spp,
|
||||||
level, pts, kept, lo, hi, cover * 100.0, s->x0(), s->x_at(0), s->x_at(n - 1),
|
level, pts, kept, lo, hi, cover * 100.0, s->x0(), s->x_at(0), s->x_at(n - 1),
|
||||||
xb.min, xb.max, pts > 0 ? double(xy[0]) : 0.0,
|
xb.min, xb.max, pts > 0 ? double(xy[0]) : 0.0,
|
||||||
pts > 0 ? double(xy[2 * (pts - 1)]) : 0.0, s->dx(), kept_fixed, drop_first,
|
pts > 0 ? double(xy[2 * (pts - 1)]) : 0.0, s->dx(), drop_first, drop_last,
|
||||||
drop_last, worst_back);
|
worst_back);
|
||||||
std::fflush(stderr);
|
std::fflush(stderr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4681,24 +4694,29 @@ void report_event_zoom(App& a, const char* what) {
|
|||||||
const double spp = double(last - first) / axes_w;
|
const double spp = double(last - first) / axes_w;
|
||||||
const plot::u32 level = spp >= 64.0 ? s->pyramid().level_for(n, spp) : 0u;
|
const plot::u32 level = spp >= 64.0 ? s->pyramid().level_for(n, spp) : 0u;
|
||||||
std::vector<plot::f32> xy;
|
std::vector<plot::f32> xy;
|
||||||
s->gather(level, first, last, xy);
|
// `axis.min` is the origin the renderer names, so every reconstruction below is its own --
|
||||||
|
// and at the deep end of this sweep that choice is the whole point: relative to the retained
|
||||||
|
// window instead, the f32's ULP would be a large fraction of these spans.
|
||||||
|
s->gather(level, first, last, axis.min, xy);
|
||||||
double ink_lo = 0.0, ink_hi = 0.0;
|
double ink_lo = 0.0, ink_hi = 0.0;
|
||||||
if (xy.size() >= 2) {
|
if (xy.size() >= 2) {
|
||||||
ink_lo = s->x0() + double(xy[0]);
|
ink_lo = axis.min + double(xy[0]);
|
||||||
ink_hi = s->x0() + double(xy[xy.size() - 2]);
|
ink_hi = axis.min + double(xy[xy.size() - 2]);
|
||||||
}
|
}
|
||||||
// The renderer's own increasing-suffix filter, counted rather than applied: it drops every
|
// The renderer's own increasing-suffix filter, counted rather than applied: it drops every
|
||||||
// point that is not strictly older than the one kept after it, which for a *run* of equal x
|
// point that is not strictly older than the one kept after it, which for a *run* of equal x
|
||||||
// keeps the last of the run. The stamps here are f32, so once the sample spacing falls below
|
// keeps the last of the run. The stamps here are f32, so once the sample spacing falls below
|
||||||
// the f32 step at the stream's magnitude the gathered x has such runs -- and `kept` is how
|
// the f32 step at the *gather origin's* magnitude the gathered x has such runs -- and `kept`
|
||||||
// many distinct positions the polyline is left with. A `kept` far below `pts` is a staircase,
|
// is how many distinct positions the polyline is left with. A `kept` far below `pts` is a
|
||||||
// and a `kept` of a handful is a trace with nothing to draw.
|
// staircase, and a `kept` of a handful is a trace with nothing to draw. This is the other
|
||||||
|
// reading the origin change buys: the step is now 2^-24 of the span on screen, so a sweep
|
||||||
|
// that used to grind `kept` down as it zoomed keeps every point.
|
||||||
std::size_t kept = 0;
|
std::size_t kept = 0;
|
||||||
double worst_back = 0.0; // the largest backwards step, in data units: non-monotonic gather
|
double worst_back = 0.0; // the largest backwards step, in data units: non-monotonic gather
|
||||||
{
|
{
|
||||||
double next = std::numeric_limits<double>::infinity();
|
double next = std::numeric_limits<double>::infinity();
|
||||||
for (std::size_t k = xy.size() / 2; k > 0; --k) {
|
for (std::size_t k = xy.size() / 2; k > 0; --k) {
|
||||||
const double x = double(xy[2 * (k - 1)]) + s->x0();
|
const double x = double(xy[2 * (k - 1)]) + axis.min;
|
||||||
if (!(x < next)) {
|
if (!(x < next)) {
|
||||||
worst_back = std::max(worst_back, x - next);
|
worst_back = std::max(worst_back, x - next);
|
||||||
continue;
|
continue;
|
||||||
@@ -6247,23 +6265,27 @@ gboolean trig_trace_probe(gpointer user) {
|
|||||||
if (found) break;
|
if (found) break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Where that slot's samples are *drawn*, reconstructed exactly as the renderer
|
// Where that slot's samples are *drawn*, reconstructed the way the renderer does it:
|
||||||
// does it: `Series::gather()` emits x relative to `x0()`, and the renderer adds
|
// `Series::gather()` emits x relative to an origin **the caller names**, and the
|
||||||
// `x0()` back before mapping to the screen. So `x0() + gathered` must equal
|
// consumer adds that same origin back before mapping to the screen. So
|
||||||
// `x_at()`, and `drawn=` must equal `data=` to the f32's precision.
|
// `origin + gathered` must equal `x_at()`, and `drawn=` must equal `data=` to the
|
||||||
|
// f32's precision. The origin named here is `x0()`, deliberately -- the renderer
|
||||||
|
// passes the visible range's minimum, but this line reads the series' *whole* span,
|
||||||
|
// which is the widest relative value a gather can be asked to stage and so the worst
|
||||||
|
// case for the f32.
|
||||||
//
|
//
|
||||||
// This is the one defect in this area the picture alone shows and no other probe
|
// This is the one defect in this area the picture alone shows and no other probe
|
||||||
// line does: a series that emits absolute x instead makes the renderer add `x0()`
|
// line does: a series that emits absolute x instead makes the consumer add the origin
|
||||||
// twice, so the trace lands a whole `x0()` away from the axis it was measured
|
// twice, so the trace lands a whole origin away from the axis it was measured
|
||||||
// against -- off screen entirely once the view is narrower than that shift, while
|
// against -- off screen entirely once the view is narrower than that shift, while
|
||||||
// the crosshair (which reads `x_at()`) goes on reporting correct values.
|
// the crosshair (which reads `x_at()`) goes on reporting correct values.
|
||||||
double drawn_lo = 0.0, drawn_hi = 0.0, data_lo = 0.0, data_hi = 0.0;
|
double drawn_lo = 0.0, drawn_hi = 0.0, data_lo = 0.0, data_hi = 0.0;
|
||||||
if (shown != nullptr && shown->size() > 0) {
|
if (shown != nullptr && shown->size() > 0) {
|
||||||
const u64 n = shown->size();
|
const u64 n = shown->size();
|
||||||
std::vector<f32> xy;
|
std::vector<f32> xy;
|
||||||
shown->gather(0u, 0u, 1u, xy);
|
shown->gather(0u, 0u, 1u, shown->x0(), xy);
|
||||||
const std::size_t head = xy.size();
|
const std::size_t head = xy.size();
|
||||||
shown->gather(0u, n - 1, n, xy);
|
shown->gather(0u, n - 1, n, shown->x0(), xy);
|
||||||
if (head >= 2 && xy.size() >= head + 2) {
|
if (head >= 2 && xy.size() >= head + 2) {
|
||||||
drawn_lo = shown->x0() + double(xy[0]);
|
drawn_lo = shown->x0() + double(xy[0]);
|
||||||
drawn_hi = shown->x0() + double(xy[head]);
|
drawn_hi = shown->x0() + double(xy[head]);
|
||||||
@@ -7246,18 +7268,25 @@ gboolean timelabels_probe_step(gpointer user) {
|
|||||||
const RowSet::Row& last = a->rows->row(a->rows->row_count() - 1);
|
const RowSet::Row& last = a->rows->row(a->rows->row_count() - 1);
|
||||||
std::string ticks;
|
std::string ticks;
|
||||||
std::string note;
|
std::string note;
|
||||||
|
// Whether *every* major label on the axis is a different string. That, and not a span
|
||||||
|
// threshold, is what the relative form exists to buy: two adjacent ticks reading `12.3s` are
|
||||||
|
// not labels. It is therefore the one assertion that holds whatever the stream's uptime is --
|
||||||
|
// which decides where the switch actually falls, since the label resolution is a function of
|
||||||
|
// the axis' magnitude.
|
||||||
|
bool distinct = true;
|
||||||
if (last.plot != nullptr) {
|
if (last.plot != nullptr) {
|
||||||
int n = 0;
|
int n = 0;
|
||||||
|
std::vector<std::string> seen;
|
||||||
for (const plot::Tick& t : last.plot->x_axis(last.axis_x).ticks) {
|
for (const plot::Tick& t : last.plot->x_axis(last.axis_x).ticks) {
|
||||||
if (!t.major) continue;
|
if (!t.major) continue;
|
||||||
if (n++ != 0) ticks += ",";
|
if (std::find(seen.begin(), seen.end(), t.label) != seen.end()) distinct = false;
|
||||||
|
seen.emplace_back(t.label);
|
||||||
|
if (n++ < 4) {
|
||||||
|
if (n != 1) ticks += ",";
|
||||||
ticks += t.label;
|
ticks += t.label;
|
||||||
if (n >= 4) break;
|
|
||||||
}
|
}
|
||||||
if (last.time_origin_note < last.plot->annotation_count()) {
|
|
||||||
const plot::Annotation& an = last.plot->annotation(last.time_origin_note);
|
|
||||||
if (an.visible) note = an.text;
|
|
||||||
}
|
}
|
||||||
|
note = last.plot->x_note();
|
||||||
}
|
}
|
||||||
double first_value = 0.0;
|
double first_value = 0.0;
|
||||||
plot::Range shown{};
|
plot::Range shown{};
|
||||||
@@ -7271,9 +7300,10 @@ gboolean timelabels_probe_step(gpointer user) {
|
|||||||
shown = last.plot->x_range(last.axis_x);
|
shown = last.plot->x_range(last.axis_x);
|
||||||
}
|
}
|
||||||
std::fprintf(stderr,
|
std::fprintf(stderr,
|
||||||
"[timelabels-probe] %-12s relative=%d window=%.6g origin=%.6f note=[%s] "
|
"[timelabels-probe] %-12s relative=%d distinct=%d window=%.6g origin=%.6f "
|
||||||
"ticks=[%s] first_value=%.6f first_rel=%.6f range=[%.6f..%.6f]\n",
|
"note=[%s] ticks=[%s] first_value=%.6f first_rel=%.6f range=[%.6f..%.6f]\n",
|
||||||
what, a->rows->time_labels_relative() ? 1 : 0, a->rows->time_window(),
|
what, a->rows->time_labels_relative() ? 1 : 0, distinct ? 1 : 0,
|
||||||
|
a->rows->time_window(),
|
||||||
a->rows->time_labels_origin(), note.c_str(), ticks.c_str(), first_value,
|
a->rows->time_labels_origin(), note.c_str(), ticks.c_str(), first_value,
|
||||||
first_value - (last.plot != nullptr ? last.plot->x_axis_origin(last.axis_x) : 0.0),
|
first_value - (last.plot != nullptr ? last.plot->x_axis_origin(last.axis_x) : 0.0),
|
||||||
shown.min, shown.max);
|
shown.min, shown.max);
|
||||||
@@ -8057,9 +8087,10 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
|
|||||||
app.dbg_notrace_frames, app.dbg_notrace_after_bound, app.dbg_notrace_last);
|
app.dbg_notrace_frames, app.dbg_notrace_after_bound, app.dbg_notrace_last);
|
||||||
std::printf("[udpscope] worst side/pane min width: %d px\n", app.dbg_min_w_worst);
|
std::printf("[udpscope] worst side/pane min width: %d px\n", app.dbg_min_w_worst);
|
||||||
std::printf("[udpscope] trace ink: %d paints checked, %d collapsed, %d staircase, %d short, "
|
std::printf("[udpscope] trace ink: %d paints checked, %d collapsed, %d staircase, %d short, "
|
||||||
"worst cover %.1f%%\n",
|
"worst cover %.1f%%, worst drawn-vs-quoted %.4f px over %d raw paints\n",
|
||||||
app.dbg_ink_checked, app.dbg_ink_collapse, app.dbg_ink_staircase,
|
app.dbg_ink_checked, app.dbg_ink_collapse, app.dbg_ink_staircase,
|
||||||
app.dbg_ink_short, app.dbg_ink_worst_cover * 100.0);
|
app.dbg_ink_short, app.dbg_ink_worst_cover * 100.0, app.dbg_ink_worst_offset_px,
|
||||||
|
app.dbg_ink_raw_checked);
|
||||||
std::printf("[udpscope] blank view frames: %d of %d\n", app.dbg_blank_frames,
|
std::printf("[udpscope] blank view frames: %d of %d\n", app.dbg_blank_frames,
|
||||||
app.dbg_axes_frames);
|
app.dbg_axes_frames);
|
||||||
std::printf("[udpscope] trigger events: %llu total, %llu at the disarm (no event after it "
|
std::printf("[udpscope] trigger events: %llu total, %llu at the disarm (no event after it "
|
||||||
@@ -8306,24 +8337,27 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
|
|||||||
}
|
}
|
||||||
{
|
{
|
||||||
// The x label form: relative to the window's own start (with the caption that names
|
// The x label form: relative to the window's own start (with the caption that names
|
||||||
// that start) while the span is sub-second, absolute otherwise. `x_origin_set` is read
|
// that start) when the labels could not carry the tick step absolutely, absolute
|
||||||
// back out of the plot, so this is what the axis is actually formatting with.
|
// otherwise. `x_origin_set` is read back out of the plot, so this is what the axis is
|
||||||
|
// actually formatting with, and `note` is the plot's own caption string.
|
||||||
const RowSet::Row& rlt = rows.row(rows.row_count() - 1);
|
const RowSet::Row& rlt = rows.row(rows.row_count() - 1);
|
||||||
std::string note;
|
std::string note;
|
||||||
if (rlt.plot != nullptr && rlt.time_origin_note < rlt.plot->annotation_count()) {
|
if (rlt.plot != nullptr) note = rlt.plot->x_note();
|
||||||
const plot::Annotation& a = rlt.plot->annotation(rlt.time_origin_note);
|
|
||||||
if (a.visible) note = a.text;
|
|
||||||
}
|
|
||||||
// The labels themselves, from the axis the reader looks at: the assertion is what the
|
// The labels themselves, from the axis the reader looks at: the assertion is what the
|
||||||
// ticks *say*, not only which mode is on.
|
// ticks *say*, not only which mode is on.
|
||||||
std::string ticks;
|
std::string ticks;
|
||||||
|
bool distinct = true;
|
||||||
if (rlt.plot != nullptr) {
|
if (rlt.plot != nullptr) {
|
||||||
int shown = 0;
|
int shown = 0;
|
||||||
|
std::vector<std::string> seen;
|
||||||
for (const plot::Tick& t : rlt.plot->x_axis(rlt.axis_x).ticks) {
|
for (const plot::Tick& t : rlt.plot->x_axis(rlt.axis_x).ticks) {
|
||||||
if (!t.major) continue;
|
if (!t.major) continue;
|
||||||
if (shown++ != 0) ticks += ",";
|
if (std::find(seen.begin(), seen.end(), t.label) != seen.end()) distinct = false;
|
||||||
|
seen.emplace_back(t.label);
|
||||||
|
if (shown++ < 4) {
|
||||||
|
if (shown != 1) ticks += ",";
|
||||||
ticks += t.label;
|
ticks += t.label;
|
||||||
if (shown >= 4) break;
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// The labels against the anchor they were built with: the dump runs after the last
|
// The labels against the anchor they were built with: the dump runs after the last
|
||||||
@@ -8340,10 +8374,10 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
std::printf("[udpscope] time labels: relative=%d origin=%.6f note=[%s] x_origin_set=%.6f "
|
std::printf("[udpscope] time labels: relative=%d distinct=%d origin=%.6f note=[%s] "
|
||||||
"ticks=[%s] first_rel=%.9f\n",
|
"x_origin_set=%.6f ticks=[%s] first_rel=%.9f\n",
|
||||||
rows.time_labels_relative() ? 1 : 0, rows.time_labels_origin(), note.c_str(),
|
rows.time_labels_relative() ? 1 : 0, distinct ? 1 : 0, rows.time_labels_origin(),
|
||||||
origin_set, ticks.c_str(), first_value - origin_set);
|
note.c_str(), origin_set, ticks.c_str(), first_value - origin_set);
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(model.mutex());
|
std::lock_guard<std::mutex> lock(model.mutex());
|
||||||
|
|||||||
Reference in new issue
Block a user