fix(render): a dense trace is drawn one column at a time, not as a polyline

An LOD envelope staged as a polyline moirés. level_for() sizes a bucket at one to
two pixel columns and Envelope emits two points per bucket, so a column carries
three or four near-vertical hairlines at as many sub-pixel phases; under src-alpha
compositing its brightness is then a non-linear function of those phases, the
pattern repeats at the beat between the bucket pitch and the pixel pitch, and a pan
sweeps the beat so the banding crawls.

draw_traces now reduces such a window onto the pixel grid: one exactly vertical
segment per integer column of the axes, from the lowest to the highest screen y any
gathered point put in it, flushed in framebuffer pixels with the snap on. One phase
for the whole trace, so there is nothing left to beat -- and the geometry becomes
O(axes.w) rather than O(buckets).

Gated on the gathered density rather than on the level (the raw path beats the same
way), and on Envelope alone. A hole stays a hole, disjoint neighbours are joined so
a fast edge keeps it, and a span under the stroke is drawn as the stroke since the
shader caps nothing. It is the one carve-out in u_snap's data/chrome rule: a column
is a pixel column, and the y bounds it reports are untouched.

Measured by the new test as the number of distinct coverage levels in the ink --
4 against the polyline's 126, with 627 segments against 60 054. A count of
partially covered pixels was tried first and separates nothing (577 against 652):
a 1.5 px stroke's fringe always spills into the next column.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Martino FerrariandClaude Opus 4.6 committed 2026-10-06 02:00:25 +02:00
1 parent bee318870f
commit e81e84645f
5 files changed
+301 -3

No files matched your search

+20 -1
View File
@@ -29,6 +29,11 @@ namespace plot {
struct FrameStats {
u32 raw_traces = 0; ///< traces drawn from raw samples (level 0)
u32 lod_traces = 0; ///< traces drawn from a min/max pyramid level
/// Traces whose geometry was reduced onto the pixel grid: one vertical segment
/// per column, lit from that column's own extremes. Counts a trace from either
/// path above -- the reduction is about the *density* of the gathered points,
/// not about which level produced them.
u32 column_traces = 0;
u64 visible_samples = 0; ///< samples the gathered windows covered, summed
u32 staged_segments = 0; ///< line segments staged, all colour groups
u32 staged_markers = 0; ///< marker shapes staged over the traces
@@ -500,6 +505,16 @@ class Renderer {
/// The data helpers take it too; they read the axes rect and the transforms it
/// pins.
void draw_traces(const Plot& plot, const PlotLayout& layout);
/// One gathered window reduced onto the pixel grid and staged in framebuffer
/// pixels: for each integer column of `layout.axes`, one exactly vertical
/// segment from the lowest to the highest screen y any gathered point put in
/// it. Returns how many columns were staged (0 stages nothing); the caller
/// flushes with `pixel_mapping()` and `snap = true`.
///
/// `gather_` is the input, read with the same origin `draw_traces` asked the
/// series for. See `draw_traces` for when this path is taken and why.
u32 stage_trace_columns(const Plot& plot, const PlotLayout& layout, const Trace& trace,
f64 x_origin, u32 points);
/// The cursors, both orientations: one stroke per cursor, placed at the coordinate its own
/// orientation owns (`Plot::cursor_screen_coordinate()`) and spanning the other axis' whole
/// range, in its own colour -- or in the accent at handle width while it is hovered or
@@ -611,7 +626,11 @@ class Renderer {
render::TextBatch glyphs_under_;
render::TextBatch glyphs_over_;
std::vector<f32> gather_; ///< scratch for one gathered window, re-used
std::vector<f32> staged_; ///< scratch for one staged window, re-used
/// One (lowest y, highest y) pair per pixel column of the axes, re-used:
/// `stage_trace_columns()`'s working set. Two floats per column, so an 8K-wide
/// axis is 64 kB -- bounded by the *screen*, where `gather_` is bounded by the
/// window.
std::vector<f32> columns_;
mutable std::vector<u8> readback_; ///< dump_frame_ppm's row buffer
/// The host's `GL_COLOR_CLEAR_VALUE`, queried on the first `begin_frame()` (GL