feat(render): a reduced column can be graded by dwell, not drawn as one flat band
CI / debug (tests + all adapters) (push) Canceled after 0s
CI / coverage (floor + measured table) (push) Canceled after 0s
CI / asan (AddressSanitizer) (push) Canceled after 0s

A pixel column reduced from a dense envelope is honest about where the signal
went and silent about how long it was there: its min and max are identical
whether its samples fill the span or pile at its two ends, so a ramp and a pulse
are one band. Style::intensity_grading grades it instead -- bright where the
signal sits, faint where it only passes through -- by asking the gather for a
level finer than the pixel grid and reading the distribution from those
sub-buckets. Off by default: a change of look, not a correction of one.

The measure is dwell per row, each segment depositing one unit spread over the
rows it spans; counting +1 per row instead grades a sine's fast middle as
brightly as its rails, which is the pattern inverted. The accumulator is one
column wide (a full w*h density image is 33 MB at 4K) with each span written as
two delta writes, and normalisation is against the column's own peak -- a
frame-wide scale fades the trace out exactly where it moves fastest. Alpha is
quantised into tiers, one LineBatch and one draw call each, because the line
pipeline's colour is a uniform and a per-segment alpha is inexpressible.

The finer ask is floored at level 1 whenever the flat path would have used the
pyramid: level_for() answers 0 -- raw samples -- as soon as a sub-column falls
under one bucket, so an unfloored ask turns a moderately dense frame into an
O(visible) gather (measured at 172 samples a pixel: raw=1154 lod=0 and 6.4 ms a
frame against raw=710 lod=453 and 2.3 ms). Grading is a way of drawing a
reduction and may not undo it.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Martino FerrariandClaude Opus 4.6 committed 2026-10-06 03:02:36 +02:00
1 parent e81e84645f
commit cc908efa36
6 files changed
+629 -13

No files matched your search

+76
View File
@@ -177,6 +177,82 @@ struct Style {
///< sides draw through the one instanced
///< anti-aliased quad path.
// --- intensity grading: counts and ratios, never sizes -------------------
/// Draw a dense trace's pixel columns **graded by how long the signal dwells at
/// each height**, the way a digital-phosphor scope does, instead of as one flat
/// band per column: bright where samples pile up (a flat top, a peak), faint
/// where they only pass through (an edge, a noise excursion).
///
/// Off by default, because it is a change of *look* and not a correction: the
/// flat band is the honest answer to "what did this column reach", and a host
/// that wants that -- or that draws few enough samples never to take the column
/// path at all -- should not have its traces restyled by an upgrade. A scope-like
/// host turns it on.
///
/// It is not derivable from the flat band. A column's min and max are the same
/// whether its samples fill the span or sit piled at its two ends, which is
/// exactly the difference between a ramp and a pulse -- so grading needs the
/// gather asked at a *finer* level than the pixel grid (`intensity_sub_columns`)
/// and reads the distribution from those sub-buckets.
///
/// The measure is *dwell per row*, and the weighting is the whole of it: each
/// sub-column segment deposits **one unit spread over the rows it spans**, so a
/// tall crossing leaves a thin smear and a short one at a turning point leaves a
/// bright mark. Counting a row at +1 per segment that touches it instead -- the
/// obvious thing -- grades a sine's fast middle as brightly as its rails, which
/// is the pattern inverted.
bool intensity_grading = false;
/// Sub-buckets per pixel column the gather is asked for when grading: the level
/// is chosen for `samples_per_pixel / intensity_sub_columns`, so a column's
/// density is read from this many envelopes instead of from one.
///
/// It buys resolution in *dwell*, linearly, for the same factor in gathered
/// points -- and the points stay bounded by the screen, which is the property
/// that matters (8 of them over a 1000 px axis is 16 000 points whatever the
/// window holds). Below about 4 a pulse's flat levels and its edge are too few
/// samples apart to separate.
///
/// It is a *want*, not a promise: the renderer floors the level it asks for at the
/// pyramid's coarsest bucket level, so a window too sparse to hold this many buckets
/// a column is graded from however many it does hold. Without that floor `level_for`
/// answers 0 -- raw samples -- the moment a sub-column falls under one bucket, which
/// turns a frame the flat path draws off the pyramid into an O(visible) gather. So
/// raising this past about 8 costs resolution nothing and buys it nothing either: the
/// bucket size is the limit, not this number.
u32 intensity_sub_columns = 8;
/// Quantisation of the grading, in alpha steps. Each step is one draw call (the
/// line pipeline's colour is a uniform, so a per-segment alpha would mean a
/// per-instance one), and the segments are bounded by the columns, so the cost
/// of a step is a draw call over a few hundred instances.
///
/// Four is enough to read a waveform's structure and few enough that a step is a
/// visible tier rather than a gradient -- the point is to separate "dwelt here"
/// from "passed through", not to measure.
u32 intensity_steps = 4;
/// Alpha the faintest graded step is drawn at, as a fraction of the trace's own.
///
/// Not 0: a column the signal merely passed through is still a column the signal
/// *reached*, and a riser dimmed out of existence is a square wave drawn as two
/// disconnected lines. This is the floor that keeps the trace continuous.
f32 intensity_floor = 0.3f;
/// Exponent the normalised dwell is raised to before it is quantised. Below 1 it
/// lifts the faint end, which is what makes a fast edge visible beside a flat top
/// whose dwell is three orders of magnitude greater.
///
/// The dwell is normalised **against the column's own peak**, not against a scale
/// for the whole frame, so the steps spend their range on the structure *inside*
/// each column. One scale for the frame needs the whole `w * h` density image
/// (33 MB at 4K, which is why the accumulator is one column wide) and reads
/// worse: a column the signal flew straight through holds little dwell at any
/// height, so it would grade uniformly faint -- a trace that fades out exactly
/// where it moves fastest.
f32 intensity_gamma = 0.45f;
/// `logical` in device pixels: the single conversion point for every logical
/// metric above, so the arithmetic appears once per consumer instead of a `*
/// ui_scale` scattered over the renderer and the layout.