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:
1 parent
bee318870f
commit
e81e84645f
5 files changed
+301
-3
No files matched your search
@@ -166,6 +166,56 @@ series therefore stages O(visible pixels) vertices, not O(samples), and `FrameSt
|
||||
which path each trace took. Below the threshold the raw samples are drawn instead; both
|
||||
sides go through the same instanced anti-aliased quad pipeline.
|
||||
|
||||
## Dense traces are reduced onto the pixel grid
|
||||
|
||||
A gathered window denser than the axis is **not** staged as a polyline. For each integer
|
||||
column of `layout.axes`, `Renderer::draw_traces` emits one exactly vertical segment from the
|
||||
lowest to the highest screen y any gathered point put in that column, flushed in framebuffer
|
||||
pixels with `u_snap` on. `FrameStats::column_traces` counts the traces that took this path.
|
||||
|
||||
It exists because a polyline over such a window **moirés**, and three things compound to make
|
||||
it. `MinMaxPyramid::level_for()` picks the level whose buckets hold about
|
||||
`samples_per_pixel` samples, so a bucket spans **one to two pixel columns**;
|
||||
`GatherMode::Envelope` emits two points per bucket, so each bucket contributes a near-vertical
|
||||
min→max hairline *and* a near-vertical connector to the next — about 3.6 near-vertical
|
||||
hairlines per column; and each of those lands at a **fractional** x, so with ordinary
|
||||
src-alpha compositing a column's brightness is a non-linear function of the sub-pixel phases
|
||||
overlapping it. The phase pattern repeats at the beat between the bucket pitch and the pixel
|
||||
pitch, which is a visible banding, and panning sweeps the beat so it crawls. Reducing to one
|
||||
segment per column leaves the trace **one** sub-pixel phase, so there is nothing left to beat,
|
||||
and the snap then puts that one phase on the grid.
|
||||
|
||||
Four properties of the reduction are load-bearing:
|
||||
|
||||
- **It is gated on the gathered *density*, not on the level.** It runs when the window is
|
||||
`GatherMode::Envelope` and carries at least two gathered points per column of the axes —
|
||||
the raw path at 1.5–64 samples per pixel has the same beat and the same cure. The density
|
||||
term is also what keeps it away from a *sparse* window, whose trace is drawn by the two
|
||||
segments that enter and leave the frame (`a_sparse_window_between_samples_still_inks_both_margins`);
|
||||
`Nearest` and `Mean` are excluded because they are one-point-per-bucket modes a band defeats.
|
||||
- **A hole stays a hole.** A column no gathered point landed in emits nothing. Bridging it
|
||||
would draw a vertical line over x the series says nothing about.
|
||||
- **Disjoint columns are joined.** Two neighbouring columns' extremes need not overlap — a
|
||||
fast edge rises through a whole column — and two disjoint vertical segments read as a broken
|
||||
trace, which is what the polyline's connector prevented. The later column is extended to
|
||||
meet the earlier one's nearest bound, so the join is made inside the column that moved.
|
||||
- **A span shorter than the stroke is drawn *as* the stroke.** The line fragment shader ramps
|
||||
on the distance to the segment's axis and does not cap its ends, so a vertical segment
|
||||
shorter than its own width rasterises a sliver of itself, and a flat trace's column, where
|
||||
the two bounds are equal, nothing at all.
|
||||
|
||||
This is the **one carve-out** in `u_snap`'s rule that chrome may be snapped and data may not
|
||||
(see `LineProgram::set_snap()`): a column *is* a pixel column and its x is already a screen
|
||||
coordinate, so there is no data position to move it off, and the y bounds the segment reports
|
||||
are untouched by the snap.
|
||||
|
||||
`a_dense_trace_is_drawn_as_whole_pixel_columns` is the guard, and what it measures is the
|
||||
**number of distinct coverage levels** in the ink: 4 with the reduction against 126 with the
|
||||
polyline, over one sine period across 60 000 samples. A count of *partially covered pixels* is
|
||||
**not** the reading and was tried first — a 1.5 px stroke with a 1 px anti-aliasing fringe
|
||||
always spills into its neighbour column, so the two renderings measure 577 and 652, which
|
||||
separates nothing. The frame's whole geometry collapses with it: 627 segments against 60 054.
|
||||
|
||||
## One sample or a statistic of many
|
||||
|
||||
Every number the library reports is one the samples actually hold. What differs between
|
||||
|
||||
Reference in new issue
Block a user