feat(render): a reduced column can be graded by dwell, not drawn as one flat band
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:
1 parent
e81e84645f
commit
cc908efa36
6 files changed
+629
-13
No files matched your search
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user