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

+178
View File
@@ -38,6 +38,23 @@ constexpr plot::u32 kH = 360;
/// 1M-wide window holds ~16 of them (the dense envelope spans the full height in
/// every column) while a 640-wide one is a nearly flat line (the sparse path's
/// polyline crosses every column exactly once).
/// A sine of **one period per pixel column** for the window
/// `a_dense_trace_can_be_graded_by_dwell` draws (60 000 x-units over a 572 px axis, so
/// 104.9 samples a column). That is the one density at which a column *has* a dwell
/// structure to grade: its envelope spans the full amplitude, so the band is tall, and
/// the sub-buckets inside it are a fraction of a period each, so the ones at the sine's
/// turning points are short and the ones crossing its middle are long. A period of many
/// columns leaves every band a thin ribbon with nothing to distinguish, and a period of a
/// fraction of a column fills every sub-bucket to the full amplitude -- also nothing.
std::vector<plot::f32> fast_sine_samples() {
std::vector<plot::f32> y(1'000'000);
const double w = 2.0 * 3.14159265358979323846 / 104.9;
for (plot::u64 i = 0; i < y.size(); ++i) {
y[i] = static_cast<plot::f32>(std::sin(static_cast<double>(i) * w));
}
return y;
}
std::vector<plot::f32> sine_samples() {
std::vector<plot::f32> y(1'000'000);
for (plot::u64 i = 0; i < y.size(); ++i) {
@@ -961,6 +978,167 @@ PLOT_TEST(a_dense_trace_is_drawn_as_whole_pixel_columns) {
ctx.destroy();
}
/// `Style::intensity_grading`: a reduced column is drawn **graded by dwell** -- bright
/// where the signal sits, faint where it only passes through -- which is the second half
/// of the moire answer. The columns alone removed the beat; this is what puts back the
/// information a flat band throws away, the way a phosphor display does.
///
/// The observable is the thing the feature claims and nothing weaker: within one column,
/// the rows at the **edges** of the band are brighter than the rows in its **middle**.
/// For a sine of one period per column that is exactly right -- it dwells at its turning
/// points and crosses its middle at full speed -- and it is a property of the *rendering*
/// rather than of the waveform, because the same frame drawn ungraded puts the band's
/// edge and its middle at the same coverage. Both frames are measured here, so what is
/// asserted is the difference between them.
///
/// It also pins what grading may *not* cost. The reduction still runs (`column_traces`),
/// so the beat is still gone; the geometry is still bounded by the axis' width times a
/// small factor, because a run is a stretch of equal tier and not a row; and the draw
/// calls are bounded by the steps, since the line pipeline's colour is a uniform.
PLOT_TEST(a_dense_trace_can_be_graded_by_dwell) {
plottest::HeadlessContext ctx(kW, kH);
plot::Status st;
PLOT_CHECK(ctx.create(&st));
PLOT_CHECK(plot::gl::load(nullptr));
plot::Renderer r;
PLOT_CHECK(r.init(&st));
plot::data::ArraySeries s(fast_sine_samples(), 0.0, 1.0);
plot::Plot p;
p.set_viewport({0, 0, static_cast<plot::f32>(kW), static_cast<plot::f32>(kH)});
const plot::u32 ax = p.add_x_axis({"t", {0.0, 60'000.0}, false, plot::AxisPlace::Bottom, false});
const plot::u32 ay = p.add_y_axis({"v", {-1.0, 1.0}, false, plot::AxisPlace::Left, false});
p.add_trace({&s, plot::Color::hex(0x40C0FF), 1.5f, ax, ay});
// The band's edges against its middle, averaged over columns across the axis. The
// shares are of the band's own height, so the reading does not depend on where the
// band happens to sit or on how tall it is: the outer fifth at each end against the
// middle fifth.
struct Profile {
plot::u32 edge = 0; ///< mean peak coverage over the band's two outer fifths
plot::u32 middle = 0; ///< mean peak coverage over its middle fifth
plot::u32 columns = 0; ///< columns that held a band at all
};
auto profile = [&](const plot::Rect& a) -> Profile {
const plot::u32 rows = static_cast<plot::u32>(a.h);
std::vector<int> cover(rows, 0);
plot::u64 edge_sum = 0;
plot::u64 middle_sum = 0;
Profile out;
for (plot::u32 col = 8; col + 8 < static_cast<plot::u32>(a.w); col += 16) {
plot::u32 lo = rows;
plot::u32 hi = 0;
for (plot::u32 row = 0; row < rows; ++row) {
plot::u8 px[4] = {};
const plot::u32 x = static_cast<plot::u32>(a.x) + col;
PLOT_CHECK(ctx.read_pixel(x, ctx.height - 1u - (static_cast<plot::u32>(a.y) + row), px));
cover[row] = static_cast<int>(px[2]) - static_cast<int>(px[0]);
if (cover[row] > 20) { // the faintest step is 0.3 of 191, so this is under it
if (row < lo) lo = row;
hi = row;
}
}
// A band of fewer than 15 rows has no middle fifth to speak of; at this density
// there is none, but the reading would be meaningless if there were.
if (lo >= hi || hi - lo < 15u) continue;
const plot::u32 h = hi - lo + 1u;
const plot::u32 fifth = h / 5u;
int edge = 0;
int middle = 0;
for (plot::u32 row = lo; row <= hi; ++row) {
const plot::u32 d = row - lo;
if (d < fifth || d >= h - fifth) {
if (cover[row] > edge) edge = cover[row];
} else if (d >= 2u * fifth && d < 3u * fifth) {
if (cover[row] > middle) middle = cover[row];
}
}
edge_sum += static_cast<plot::u64>(edge);
middle_sum += static_cast<plot::u64>(middle);
++out.columns;
}
if (out.columns != 0) {
out.edge = static_cast<plot::u32>(edge_sum / out.columns);
out.middle = static_cast<plot::u32>(middle_sum / out.columns);
}
return out;
};
// Ungraded first: the reference the grading is read against.
r.begin_frame(kW, kH, {0, 0, static_cast<plot::f32>(kW), static_cast<plot::f32>(kH)});
r.draw(p);
r.end_frame();
dump_if_requested(r, "ungraded.ppm");
const plot::Rect a = p.layout().axes;
const Profile flat = profile(a);
const plot::u32 flat_segments = r.stats().staged_segments;
const plot::u32 flat_draws = r.stats().draw_calls;
PLOT_CHECK(r.stats().column_traces == 1 && r.stats().graded_traces == 0);
PLOT_CHECK(!r.last_frame_had_gl_error());
r.style().intensity_grading = true;
r.begin_frame(kW, kH, {0, 0, static_cast<plot::f32>(kW), static_cast<plot::f32>(kH)});
r.draw(p);
r.end_frame();
dump_if_requested(r, "graded.ppm");
const Profile lit = profile(a);
std::printf("[graded] axes=[%.1f %.1f %.1f %.1f] flat edge=%u middle=%u cols=%u seg=%u "
"draws=%u | graded edge=%u middle=%u cols=%u seg=%u draws=%u columns=%u "
"graded_traces=%u\n",
a.x, a.y, a.w, a.h, flat.edge, flat.middle, flat.columns, flat_segments,
flat_draws, lit.edge, lit.middle, lit.columns, r.stats().staged_segments,
r.stats().draw_calls, r.stats().column_traces, r.stats().graded_traces);
// Both frames measured a band in most of the columns they sampled, or the two
// profiles are not comparable and everything below is vacuous.
PLOT_CHECK(flat.columns > 20u && lit.columns == flat.columns);
// The flat band is flat: its edge and its middle are one ink (191 and 191).
PLOT_CHECK(flat.middle > 0u && flat.edge * 100u < flat.middle * 115u);
// The graded one is not, and in the direction a sine dwells in: 191 against 131, a
// middle at 0.69 of the edge. The figure is a *tier* ratio and not the dwell ratio --
// the per-row dwell at the rails is several times the crossing's, and
// `intensity_gamma` (0.45) and `intensity_floor` (0.3) are there to compress exactly
// that into a range an eye reads as shading rather than as a gap. So the threshold is
// what the quantised ink does, with room for a step either way.
PLOT_CHECK(lit.middle > 0u && lit.middle * 100u < lit.edge * 80u);
// The reduction still ran, so the beat the columns removed is still gone.
PLOT_CHECK(r.stats().column_traces == 1 && r.stats().graded_traces == 1);
// A run is a stretch of equal tier, not a row: the geometry stays bounded by the
// axis' width times a small factor, nothing like `w * h`. Measured 8303 over a 572 px
// axis, 14.5 runs a column -- the dwell profile's own structure (four tiers from the
// dim middle out to each rail) plus the quantiser's dither at each boundary. The flat
// path's 627 is the other end of the scale, and 572 * 156 rows is what this is not.
PLOT_CHECK(r.stats().staged_segments < 24u * static_cast<plot::u32>(a.w));
// And the draw calls by the steps: one per tier that staged anything, over the flat
// frame's single trace draw.
PLOT_CHECK(r.stats().draw_calls <= flat_draws + r.style().intensity_steps);
// And the level floor, which the window above cannot see: at 105 samples a pixel the
// flat path is already on raw samples, so grading asks for nothing finer. A wider
// window puts the flat path on the pyramid, and there a sub-column is `spp / 8` -- which
// falls under one bucket long before the column does, so an unfloored ask answers level
// 0 and gathers the whole visible window. That is a real cost and it was measured: at
// 1.3 MS/s over a 2 s window udpscope's own frame report went from `raw=710 lod=453` to
// `raw=1154 lod=0` and 2.3 -> 6.4 ms a frame. The floor is one bucket level, so the
// reading is simply that a graded frame stays on the pyramid.
p.set_x_range(ax, {0.0, 200'000.0});
r.begin_frame(kW, kH, {0, 0, static_cast<plot::f32>(kW), static_cast<plot::f32>(kH)});
r.draw(p);
r.end_frame();
std::printf("[graded] wide: raw=%u lod=%u columns=%u graded=%u seg=%u visible=%llu\n",
r.stats().raw_traces, r.stats().lod_traces, r.stats().column_traces,
r.stats().graded_traces, r.stats().staged_segments,
static_cast<unsigned long long>(r.stats().visible_samples));
PLOT_CHECK(r.stats().visible_samples > 100'000u);
PLOT_CHECK(r.stats().lod_traces == 1 && r.stats().raw_traces == 0);
PLOT_CHECK(r.stats().column_traces == 1 && r.stats().graded_traces == 1);
PLOT_CHECK(r.stats().staged_segments < 24u * static_cast<plot::u32>(a.w));
PLOT_CHECK(!r.last_frame_had_gl_error());
ctx.destroy();
}
// Resolution #1 of the brief: the renderer draws into whatever framebuffer is
// bound and clears only its viewport, so a host that embeds the plot in a
// sub-rect (Qt/GTK/ImGui) keeps the rest of its surface. Coordinates are given