fix: the plot's two side margins are balanced, and the dump outlives its widgets
With the axis titles gone the left gutter was 200 px of an 870 px viewport and the right side had no margin at all, so the newest tick's label was drawn half outside the plot and the newest sample's ink landed on the frame. Two causes on the left. The reservation covered `format_si()`'s `%.3g` fallback (`-1.23e-45`), which no signal on an `Si` axis produces -- the widest real label for a unitless axis is `-1.23m` -- and `note_y_gutter()` added 6 px of slack to a figure that is already the maximum of the labels and last frame's floor, making the gutter a ratchet that climbed to its 200 px cap in ~19 frames. It settles at 104 px now, with `axes.w changes: 1` where it read 17. The right side did not exist: a plot's `axes` rect reached the viewport's right edge by construction. `deps/plotter` gains `Plot::set_right_gutter()` and `Figure::set_right_gutter()` -- a want rather than a measurement, since nothing is drawn there, imposed figure-wide so the rows end on one column as they already start on one. udpscope asks for half the widest time label plus the label padding (59 px), x labels being centred on their tick. Separately: the exit dump asked two destroyed widgets for their size, which is where the "exactly two Gtk-CRITICALs" baseline came from. The pointer is freed memory by then, so a run that had destroyed other windows first (UDPSCOPE_PROBE_HELP_MS opens three) segfaulted in the middle of the dump instead. Both sizes are captured on the tick; a clean run now carries no critical. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
811c6d80f0
commit
62df049f53
7 files changed
+153
-39
No files matched your search
@@ -62,7 +62,11 @@ app's plain `GtkApplicationWindow` is supported and warning-free; it was checked
|
||||
|
||||
`deps/plotter` is upstream's own repository and this app's figure support was added
|
||||
there rather than worked around here: `Figure::set_min_y_gutter()` (a floor under the
|
||||
common left gutter), and in the GTK4 adapter `PlotArea`'s **figure mode** —
|
||||
common left gutter), `Figure::set_right_gutter()` / `Plot::set_right_gutter()` (an empty
|
||||
band on the *other* side, which the layout had no concept of — `axes` reached the
|
||||
viewport's right edge, so the rightmost x tick label was centred on a line it had half of
|
||||
itself outside of; the guard is `figure_rows_share_one_right_gutter`), and in the GTK4
|
||||
adapter `PlotArea`'s **figure mode** —
|
||||
`set_figure()`, one `input::Controller` per row, nearest-row event routing, a
|
||||
figure-wide crosshair — plus `PlotArea::row_at()` for naming the row a pointer is over.
|
||||
|
||||
@@ -110,9 +114,18 @@ Two baselines every run is read against. A clean build emits **five** warnings a
|
||||
`-Woverlength-strings` on **both** generated GResource `.c` files — the help pages' and the
|
||||
icons', whose blob crossed C99's 4095-byte string-literal limit when the two app icons joined the
|
||||
four action ones (a note about a generated file, which GCC itself supports). And an exit-dump run
|
||||
emits exactly **two** `Gtk-CRITICAL`s, `gtk_widget_get_height` and `gtk_widget_get_width`, from
|
||||
the dump asking a destroyed widget for its size. A sixth warning, or a third critical, is a
|
||||
finding.
|
||||
emits **no** `Gtk-CRITICAL` at all. A sixth warning, or any critical, is a finding.
|
||||
|
||||
That second baseline used to be "exactly two, `gtk_widget_get_height` and `gtk_widget_get_width`,
|
||||
from the dump asking a destroyed widget for its size", and tolerating them was a mistake: the dump
|
||||
runs from the destroy handler, so the widget pointer is **freed memory**, and a critical is only
|
||||
the lucky reading of it. A run that had built and destroyed other windows first —
|
||||
`UDPSCOPE_PROBE_HELP_MS`, which opens three — has had that memory reused, and the same two calls
|
||||
then **SIGSEGV in the middle of the dump**, which reads as a truncated dump rather than as a
|
||||
crash in a debug line. Every size the dump prints is therefore captured on the *tick*, while the
|
||||
window is alive (`App::dbg_*`), and **no `gtk_widget_*` call may appear in the dump** — the
|
||||
`panel controls:` line was already written that way for the same reason, and that comment is the
|
||||
precedent, not an exception.
|
||||
|
||||
- `UDPSCOPE_EXIT_AFTER_MS` — run the trigger-on → go-live probe, close the window,
|
||||
then dump the whole pipeline state to stdout (frame/sample counts, per-plot painted
|
||||
@@ -251,10 +264,18 @@ Two lines of the self-check output are assertions in disguise:
|
||||
monotone (`plot_host.cpp`), its one-shot widest-label reservation can land a frame or
|
||||
two after the first measured frame, and the figure serves its floor out of the label
|
||||
strip (`y_width = max(measured, floor - y_title_w)`, `plot.cpp`) so the reservation
|
||||
converges over two layouts rather than one. A `y-gutter 44..74` dragging
|
||||
`axes.x 67..80`/`axes.w 546..559` with it is that settle, not a twitch — confirm it
|
||||
converges over two layouts rather than one. A `y-gutter 88..104` dragging
|
||||
`axes.x 88..104`/`axes.w 707..782` with it is that settle, not a twitch — confirm it
|
||||
with the `changes` line below. A range that keeps moving is the "twitching axis" bug:
|
||||
the plot being re-measured every frame. **`x-desync` must read 0**: it counts frames
|
||||
the plot being re-measured every frame. **A range that keeps *climbing* to 200 is the
|
||||
other one**: `note_y_gutter()` is handed the gutter the row was laid out with, which
|
||||
already includes the floor imposed last frame, so slack added to it is added again every
|
||||
frame and the gutter ratchets to its own 200 px cap — `axes.x 88..200 / y-gutter 88..200
|
||||
/ axes.w changes: 17` on an 870 px viewport is what that reads as, and 17 changes looks
|
||||
like a settle. The reservation's own padding is the only slack there may be.
|
||||
`axes.w` also no longer reaches the viewport's right edge: `apply_gutters()` reserves an
|
||||
empty band there (~59 px of 870), so `viewport.w - axes.x - axes.w` is a *constant*, not
|
||||
a zero. **`x-desync` must read 0**: it counts frames
|
||||
in which any row's x range differed from row 0's by more than a pixel of data, i.e.
|
||||
frames the x link group did not reach. `x-ticks` is content, not layout, and
|
||||
legitimately varies by one as a live
|
||||
@@ -1175,8 +1196,18 @@ far the largest file.
|
||||
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
|
||||
`TickFormat::Si` can produce and only ever grows; the status label is ellipsized so
|
||||
`TickFormat::Si` can produce *for an axis with no unit* — a sign, three significant
|
||||
figures and a one-glyph prefix, measured and not counted in characters — and only ever
|
||||
grows; the status label is ellipsized so
|
||||
its text cannot drive the paned divider. Both exist to stop per-frame relayout.
|
||||
The reservation deliberately does **not** cover `format_si()`'s `%.3g` fallback
|
||||
(`-1.23e-45`), which it used to: that form needs a value past 1e24, so it bought a third
|
||||
of the gutter for a label no stream produces, and the floor being a monotone maximum over
|
||||
the *measured* need means one that did appear widens the gutter for good anyway. Erring
|
||||
wide is not free here — it is a third of the plot's width.
|
||||
And **nothing may add slack to the gutter it is told**: `note_y_gutter()`'s argument is
|
||||
`layout().y_axis_width`, which is `max(measured, floor)` — partly our own answer coming
|
||||
back — so a constant added to it compounds. See the ratchet reading under Verification.
|
||||
The same rule is why the activity badge is one glyph in three colours rather than
|
||||
three glyphs, why the `rate` field collapses to one figure or a range instead of a
|
||||
per-channel list, and why `format_elapsed()` steps to a coarser unit instead of
|
||||
@@ -1186,6 +1217,16 @@ far the largest file.
|
||||
plotter for this), not on the rows: `Figure::layout()` clears every row's
|
||||
`set_min_y_gutter()` to measure what its own labels need, so a floor written onto the
|
||||
rows survives exactly until the next layout.
|
||||
- **The right-hand band is a want, and the left gutter is a measurement.** The two sides of
|
||||
the plot are not symmetric in kind: the left holds the y labels, so it is measured and
|
||||
floored; the right holds nothing, so there is nothing to measure and `RowSet` simply asks
|
||||
for half the widest time label plus the label padding (`right_gutter_px_`, reserved in the
|
||||
same one-shot block). Half a label is the figure that matters: an x tick label is
|
||||
*centred* on its tick and a live axis is `[-window, 0]`, so there is always a tick exactly
|
||||
on the axes' right edge, and with `axes` flush to the viewport half of that label was
|
||||
drawn outside the plot. Both go on the **figure** and for the same reason — the rows have
|
||||
to end on one column as well as start on one — which is why `apply_gutters()` applies the
|
||||
pair and `rebuild_all()` calls it: a new `Figure` carries neither.
|
||||
- **Signals start disabled** — enabling is what starts acquisition. A trigger on a
|
||||
signal nobody acquires can never fire, so selecting/arming enables it.
|
||||
- **The trigger's event window is separate from the display window**, and
|
||||
|
||||
Reference in new issue
Block a user