fix: a dense trace is drawn one pixel column at a time, so it cannot moiré
At a high rate, a high frequency or on noise the plots banded visibly and the bands crawled under a pan. Three things compound to produce it, none a bug alone: a pyramid bucket spans one to two pixel columns, Envelope emits two points per bucket so each contributes a near-vertical hairline and a near-vertical connector, and every one lands at a fractional x because a trace is deliberately not snapped -- so a column's brightness beats against the pixel grid, and a pan sweeps the beat. The cure is upstream (deps/plotter, pin moved): draw_traces reduces such a window onto the pixel grid, one snapped vertical segment per column from that column's lowest to its highest value. One sub-pixel phase for the whole trace, so there is nothing left to beat, and the geometry is bounded by axes.w rather than by the buckets. frame path: gains a columns= field, which is what separates "the segment count collapsed because the reduction ran" from "the window got narrower". On the fast mock at 3 kHz over 20 s: columns=1200, segments 2041/2237 over 1201 painted frames, against columns=0, segments 10960/216093 over 263 before -- the same run at 4.6x the frame rate, with trace ink: clean either way. Also corrects the documented mock invocation: --batch 20000 exceeds the datagram limit and the mock dies with Errno 90. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
62df049f53
commit
5f5b87ca60
5 files changed
+94
-8
No files matched your search
@@ -671,22 +671,34 @@ Two lines of the self-check output are assertions in disguise:
|
||||
blinking green. The `mode ...` lines carry the rest of the status line, so `rate` and
|
||||
`last event ... ago` are checked there.
|
||||
- `frame cost: tick A/B ms over N, draw C/D ms (max at frame F), paint E/G ms over M` and
|
||||
`frame path: raw=R lod=L frames, segments S/T (mean/max), visible max V samples` — the
|
||||
`frame path: raw=R lod=L columns=C frames, segments S/T (mean/max), visible max V samples`
|
||||
— the
|
||||
drawing cost, split into the frame tick, `PlotArea`'s own draw (timed from the frame
|
||||
clock's `before-paint` to the end of the host's `render` handler) and the whole GTK
|
||||
paint. **`segments` max must stay in the tens of thousands and `lod` must dominate `raw`
|
||||
paint. **`segments` max must stay in the low thousands and `lod` must dominate `raw`
|
||||
on any run holding a large capture**: a max in the *millions* with `lod=0` is the
|
||||
empty-pyramid defect (see the `MinMaxPyramid` invariant below) — the renderer drawing
|
||||
every raw sample of a held event on every frame, which measured 6,000,073 segments and
|
||||
100–230 ms a frame at 1 MS/s. `raw` frames are expected and cheap below 64 samples per
|
||||
pixel, which is where `level_for()`'s own gate puts the boundary. **`draw`'s max must sit
|
||||
pixel, which is where `level_for()`'s own gate puts the boundary.
|
||||
**`columns` must equal the frames that drew a trace at all on any run at a high rate**, and
|
||||
it is the reading that says *why* `segments` is small: a dense gathered window is reduced
|
||||
onto the pixel grid upstream (one snapped vertical segment per column — see the moiré
|
||||
invariant below), so the geometry is bounded by `axes.w`, and a `columns=0` beside a large
|
||||
`segments` is that reduction gone rather than a narrower window. Measured on
|
||||
`--rate 1300000 --batch 2000 --freq 3000` over 20 s: `raw=2 lod=1198 columns=1200 frames,
|
||||
segments 2041/2237` over 1201 painted frames, against `columns=0 ... segments
|
||||
10960/216093` over 263 before it existed — the same run at 4.6× the frame rate.
|
||||
**`draw`'s max must sit
|
||||
at a frame near 0** — that is the warm-up (glyph atlas, first vertex-buffer growth); a
|
||||
late `F` is a frame whose own geometry was expensive and worth tracing. `tick`'s max is
|
||||
the publish stall: `publish_event()` builds the snapshot's pyramids under `mutex()`, so a
|
||||
latch at over 1 MS/s costs one ~40 ms tick, which must not produce `gaps`. Both these
|
||||
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
|
||||
whole window fits in the raw path and the publish is too short to lose a race.
|
||||
`tools/mock_udpstreamer.py --rate 1300000 --batch 2000` — 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. (Do not
|
||||
raise `--batch` to chase the rate: a batch of 20000 exceeds the datagram limit and the mock
|
||||
dies with `OSError: [Errno 90] Message too long`.)
|
||||
- `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=..
|
||||
@@ -1111,6 +1123,34 @@ far the largest file.
|
||||
draw a bucket's extremes. It needs no index rebasing, unlike the ring: the snapshot's
|
||||
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.
|
||||
- **A trace denser than the axis is drawn one pixel column at a time, and that is what keeps
|
||||
the plots free of moiré.** The envelope staged as a polyline banded visibly and *crawled*
|
||||
under a pan on exactly the signals this app is pointed at — a high rate, a high frequency or
|
||||
noise — and three things compound to produce it, none a bug alone: `level_for()` sizes a
|
||||
bucket at one to two pixel columns, `Envelope` emits two points per bucket so each
|
||||
contributes a near-vertical min→max hairline *and* a near-vertical connector (≈3.6 per
|
||||
column per trace), and every one lands at a **fractional** x because a trace is deliberately
|
||||
not snapped. Under src-alpha compositing a column's brightness is then a non-linear function
|
||||
of the sub-pixel phases overlapping it, the pattern repeats at the beat between the bucket
|
||||
pitch and the pixel pitch, and a pan sweeps the beat. `deps/plotter`'s
|
||||
`Renderer::stage_trace_columns()` emits one snapped vertical segment per integer column
|
||||
instead, so the trace has **one** phase and there is nothing left to beat. Four things to
|
||||
know before touching it, all upstream in `docs/data-and-performance.md`: the gate is the
|
||||
gathered **density** (≥2 points per column) and `Envelope`, never the LOD level — the raw
|
||||
path at 1.5–64 spp beats the same way, and the density term is also what keeps the reduction
|
||||
off a *sparse* window whose trace is the two segments entering and leaving the frame; a hole
|
||||
stays a hole; disjoint neighbours are joined so a fast edge keeps its join; and it is the one
|
||||
carve-out in `u_snap`'s "never anything drawn at a data position" rule, because a column *is*
|
||||
a pixel column.
|
||||
**It needs a thin ribbon to be visible at all** — a window whose buckets span several periods
|
||||
paints a saturated block and hides the beat — so a screenshot of a slow sine at a whole-window
|
||||
view says nothing either way. The guards are upstream's
|
||||
`a_dense_trace_is_drawn_as_whole_pixel_columns`, which counts the **distinct coverage levels**
|
||||
in the ink (4 against the polyline's 126; a count of *partially covered* pixels separates
|
||||
nothing, 577 against 652, because a 1.5 px stroke's fringe always spills into the next
|
||||
column), and `frame path:`'s `columns=` field here. `trace ink:` deliberately cannot see this
|
||||
path: it recomputes the **gathered** points, which the reduction is downstream of, so its
|
||||
`drawn-vs-quoted` reads 0.0000 either way and remains the guard for the gather origin alone.
|
||||
- **The time ring is `f64` and the value rings are `f32`, and the asymmetry is the point.**
|
||||
A value's precision is a property of the signal (24 bits is plenty for a sample); a
|
||||
timestamp's is a property of *how long the scope has been running*, because the stamps are
|
||||
|
||||
Reference in new issue
Block a user