The tick queued a GL render unconditionally. In `Held` and `Paused` the rows
are bound to an immutable snapshot on a pinned axis, so a frame in which none
of the picture's own terms moved is the same frame again. The gate keys on a
coarse signature of what a frozen view can be re-framed by, opens for
`kSettleRenders` frames on a change because the figure lays out per render, and
has a floor that renders anyway every few ticks. The layout editor is excluded:
its buttons are placed from the last frame's rects, so a skipped frame there is
a stale widget rather than a stale picture. `skips=` in the probe dump is the
reading.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The last hand-built UI outside ui.cpp. The window, its header bar, its two pages and the
four buttons that are about the chooser rather than about a source now live in
src/ui/source-chooser.ui; the rows stay in code because a source row and a waveform row
are data, rebuilt whenever the known hosts or the recent files change.
The pages were a GtkNotebook and are an AdwViewStack under an AdwViewSwitcherBar, which
is what current libadwaita asks for and the same information without a tab strip. The
three actions moved from a button row at the foot into the header bar, the shape
source-form.ui already uses -- and deliberately without receives-default, since Enter
must keep activating the selected row.
Nothing carries a value: the tooltips and labels moved as they were, the handlers are
the same functions, and the switch is by page *name* rather than by index.
The probe reads the frame back -- `[sources] chooser ui: page='sources' title='Sources'
icon=yes page='waveforms' … switcher_wired=1 buttons=[prefs=1 new_host=1 open=1
cancel=1]` -- so a .ui that lost an id is named instead of opening a window with a hole
in it, and `check_ui` type-checks the file against the GIR (7 ok, 0 failed).
Two things the checks caught while doing it: the new frame dropped the call that fills
the waveforms page (the unused-function warning), and `AdwViewStackPage` is a GObject
that *wraps* a child rather than being one, so reading the pages needs
adw_view_stack_get_page().
GTK4 and libadwaita. It receives one UDP stream through the vendored
udpsclient, buffers it in a large ring, and draws it with the vendored
plotter: a trigger with a pre/post event window, a pause that freezes the
window, cursors, measurements, math channels in a node editor, an event cache,
named configurations, preferences, and an export of the visible window to
.npy/.csv. Waveform files can be opened and analysed in place.
The interface is two threads -- a producer that owns the socket and the GTK
main thread that owns every widget and the GL context -- sharing one model
under one mutex, with samples crossing through lock-free rings.
deps/plotter is a submodule pinned to the commit this tree builds against;
deps/udpsclient is vendored in place. docs/claude/ documents the rules that
cost a defect to learn, and SPECS.md is the requirement document.
Re-measured all four recorded cases and added a fifth for the re-plan path,
which needs 100 kS/s: at 10 kS/s the ring already covers 838 s, so no window
the control can reach moves time_stride and the window probe re-plans nothing.
Also notes that `stale` is 0 by construction while resequencing is on, and that
the reservation cap is counted in packets, so its sample cost scales with the
batch.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
--drop-every/--drop-burst withhold datagrams the counter and stream clock
have already advanced over, and --reorder-every inverts a pair on the
wire; both are deterministic for the reason --glitch-at is, since a probe
asserts the spacing of what it finds and a random count is not a figure.
A `noise` signal gives the dense-trace and intensity-floor paths a input
that is not piecewise smooth.
verification.md records the measured readings, including the one that is
a finding rather than a baseline: a burst past kGlitchPackets (256) is
read as a producer clock glitch, each bridge advances the anchor by one
packet span against a timeline 300 ahead, and four consecutive failures
then throw the stream onto the arrival clock.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- 08-trigger.txt: names all four condition shapes and the buffer search; fixes the first
paragraph and 'How to reach it' to match what the panel actually contains (fix 3).
- 10-measurements.txt: adds a Statistics bullet — one observation per recompute, what
resets the history, what does not (fix 3).
- probe-readings.md: re-quotes condition-probe lines with level2= and mode= fields;
corrects leg-2 window geometry (0.2 s / 10 % pre, not 1.0 s / 50 %); moves the
wide-window reason to legs 1 and 3; fixes the search-probe scheduling note
(search_probe calls condition_probe directly; do not also set CONDITION_MS) (fix 6).
- verification.md: corrects shape-2 window geometry to match the code (fix 7).
- invariants-session-and-export.md, README.md: document the measure_stats session key —
position, type, default false (fix 8).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two defects behind one report ("a trigger during a capture overrides the first,
and the hold-off does not apply"); the second is also what made a spike in one
channel look like the acquisition resetting.
The trigger engine keyed its invalidation on the trigger channel's *index*, which
every rebuild of the channel list renumbers -- enabling a signal that sorts before
it is enough. Any rebuild therefore abandoned an open capture and set no hold-off,
so the next edge inside the post window became t0. It keys on the channel's name
now, which changes only when the source genuinely does.
A packet whose stamps jump more than 256 packet spans ahead is now rejected as a
fault and *bridged* at the declared cadence, instead of poisoning the monotonic
mark (which pinned every later packet flat at the glitched value) and condemning
the next good packet. Only four consecutive failures abandon the declared time
signal, and that fallback no longer wipes the rings: the arrival clock is anchored
to continue from the monotonic mark, so the history is still on one timeline --
the wipe is what made one glitching packet look like the stream restarting from 0.
The declared unit is resolved before those checks, since they weigh the stamps
against a cadence in seconds.
Guards: UDPSCOPE_PROBE_TRIGRENUM_MS prints t0_held=1 through the real widgets,
and the mock's --time-glitch-at/--time-glitch-by injects the clock fault.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Clicking the coloured chip in the signal list opens a GtkColorDialog and the
colour it returns overrides that signal's palette entry. Held by name, like
every other identity here, and persisted as a session `[[color]]` table.
RowSet::signal_color() becomes the one resolver: install() builds the trace
through it and all three chip sites read it, because a chip exists to say which
trace a row is about and an override is exactly the state in which two routes to
a colour would disagree. A grey chip names no trace and so is not clickable, and
the way back is `Default colour` in the signal's menu -- a setting with no undo
is a trap.
The gesture sits on the existing label rather than in a GtkButton: padding on
this row is what made the signal list twice as tall once. It claims its sequence
so a click on the chip cannot become the start of a row drag.
UDPSCOPE_PROBE_COLOR_MS reads the result out of plot::Plot::trace() and out of
the chip's own markup, with an override in no palette slot, so neither leg can
pass by asking the resolver what it already said.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
A review of the whole node editor. The hit test measured the distance to the
24 curve samples rather than to the segments between them, so a wire spanning
several rows was unpickable between two samples -- the probe now places its
boxes 280 units apart and prints the nearest sample's own 8.4 against a 3.0
tolerance, which the point test would have missed. A refused connection said
nothing: the message is now carried in `MathDialog::refusal` and shown once by
the single writer of the label, and a self-wire is refused through the real
drag-end path instead of only through `math_connect()`.
Two defects were found only by the new assertions. `math_dialog_sync()`
re-entered itself -- `gtk_drop_down_set_selected()` emits `notify::selected`
for a programmatic set too, and the handler answers by syncing again, which
consumed the refusal before it could be drawn -- so the body is now guarded
one level deep. And a deleted node left `selected_wire` naming an index that
afterwards belongs to a different wire, so `Delete` cleared an input on the
wrong node.
A node is placed in the first free grid cell rather than at the end of the
list's own cell, so a node added after a delete no longer lands on top of a
survivor; the label ellipsizes with a tooltip rather than wrapping, because a
wrapped line makes the dialog's height depend on the text in it.
Each invariant records what went wrong rather than only the rule: a port hit
that lost its node, a hit test at the identity view that asserts nothing, a
`Delete` that must not fall through to the selected node, and a column pitch
too narrow for a wire's own tread. The warning baseline drops to four.
The file was 1967 lines and is auto-loaded into every context, so every
session paid for the whole of it to reach one invariant. It now carries the
project's goal, the three facts that are always relevant, and a table of
contents; the body is fourteen files under docs/claude/ reached by link
rather than by import, so nothing is loaded until it is asked for.
The content is moved verbatim -- the only edits are each file's own header
and the cross-references that became cross-file ("see the invariant below"
is now a link to the file that owns it).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>