Files
plotter/include/plot/render/shaders.hpp
T
Martino Ferrari f79a97be9b fix: a density grid is bounded in cells, not by the window's pixels
A grid was the axes rect in device pixels, so dragging a window wide made the
image wider with it -- and the per-frame pass over it is O(cells): the fade
multiplies every cell, a host's ink()/peak() read every cell, and a frame the
fade moved converts and uploads every cell as one byte. At 2x device scale a
1520x850 window's axes rect is 2877 device pixels wide, so four rows of one
trace are 4.5 M cells and 4.9 MB uploaded a frame; measured headless on
llvmpipe with a 2 s window, 12.9 ms of tick and 12.8 ms of draw a frame, every
frame under a time fade -- a cost that follows the window and has no ceiling.

The grid is now the axes rect up to kDensityCellCap (2^18 cells, a mebibyte of
counts), scaled down uniformly past it so cells stay square and the picture
keeps the axes' aspect, and magnified back over the rect with a linear filter:
a cell is a count, so a fragment between two of them is between their two ink
levels, which a point sample cannot give once a cell is several pixels wide.
The same four rows at the cap are 1.05 M cells, 3.9 ms of tick and 5.6 ms of
draw and 1.0 MB uploaded, while a grid under the cap is untouched -- a 640x360
viewport's 573x313 axes is still 179 349 cells, one a device pixel.

The image's footprint is the axes rect rounded up to whole device pixels on
both sides of the pipe, so a grid under the cap keeps the exact
one-texel-a-pixel mapping it has always had (sx/sy come out exactly 1) and only
a capped grid reads its cell index through a scale. Moving the decay itself to
the GPU was considered and left out: the CPU grid is what ink()/peak() read,
what the deposit adds to and what the per-sweep dedupe keys on, so a GPU image
would have to be reconciled with it per frame -- a larger change than this
bound, and not one to half-do.

Tests: a_density_grid_is_capped_whatever_the_window reads the bound off a
3840x2160 viewport (261 504 cells of a 3784x2138 axes, aspect kept),
a_capped_density_is_magnified_over_the_axes_rect reads the screen side of it
(569x460 cells covering all 832 of the rect's columns), and the density cases
now assert their capped figures. 399 cases, 0 failed.
2026-10-16 21:09:07 +02:00

94 lines
5.2 KiB
C++

// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2026 Martino Ferrari
#pragma once
namespace plot::render {
/// GLSL source of the instanced antialiased line pipeline.
///
/// The vertex shader takes one segment per instance (`a_p0` at location 0,
/// `a_p1` at location 1, both with a vertex attribute divisor of 1), expands it
/// in pixel space along its normal by `u_width * 0.5 + 1.0` pixels of AA fringe,
/// and emits a four-vertex `GL_TRIANGLE_STRIP` quad whose corners come from
/// `gl_VertexID` (0 and 1 are the two sides of `a_p0`, 2 and 3 of `a_p1`). It
/// outputs `v_dist`, the signed distance from the segment axis in pixels, and
/// `v_along`, the distance travelled from `a_p0` in pixels.
///
/// Uniforms: `u_scale` (vec2, data space to pixels), `u_offset` (vec2, pixels),
/// `u_viewport` (vec2, pixels), `u_width` (float, pixels), `u_aa` (float, the AA
/// fringe half-width in pixels -- the style's `line_aa_px` in device pixels,
/// clamped at 0.25), `u_color` (vec4).
/// `plot::Rect` is top-left origin while GL is bottom-left, so the y axis is
/// flipped exactly once, here.
///
/// A degenerate segment (`a_p0 == a_p1`) keeps a finite normal and is stretched
/// into a square mark one stroke width and fringe across, rather than
/// collapsing to zero area and rasterising nothing — the dense path stages
/// `min == max` buckets as exactly such segments.
///
/// The fragment shader discards outside the fringe and ramps alpha over the last
/// pixel of the half width with `smoothstep`, which is what keeps a 1px trace
/// from aliasing into dashes.
extern const char* const kLineVS;
extern const char* const kLineFS;
/// GLSL source of the instanced text pipeline.
///
/// One glyph cell per instance, every attribute with a vertex attribute divisor
/// of 1: `a_pos` at location 0 is the cell's top-left corner in Rect pixels,
/// `a_uv` at location 1 is the glyph rect's normalised top-left in the atlas and
/// `a_axis` at location 2 is the cell's local +x axis, a unit vector in pixels.
///
/// The vertex shader builds the cell from that basis and its perpendicular --
/// the cell's own +v direction -- so `a_axis` of `(0, -1)` draws a run that
/// advances up the screen, and it maps `a_uv` over the cell with the same
/// handedness, so a glyph's top row stays its top row however the cell is
/// turned. It also passes the cell's origin through `flat`, which is the bound
/// the fragment shader filters against.
///
/// The fragment shader converts back to texels and blends the four texels the
/// fragment sits between, **clamped to the instance's own cell**. Filtering is
/// the shader's, not the sampler's: every fetch is an explicit texel centre, so
/// `GL_NEAREST` stays correct, and the clamp keeps a footprint reaching past a
/// cell edge from blending in the neighbouring glyph of what is a tight,
/// unpadded strip of cells. A point sample would have been enough only at an
/// integer scale: at the library's own default of a 12 px style on the 13 px
/// embedded bake it *deleted* one of every 8 columns and one of every 17 rows of
/// every glyph. At scale 1 with a whole pen the two taps coincide and the result
/// is exactly as crisp as a point sample. The instance's own `a_colour` tints the
/// straight alpha -- per vertex, so one draw can carry runs of several colours.
///
/// Uniforms: `u_viewport` (vec2, pixels), `u_px_range` (vec2, the glyph cell's
/// size on screen in pixels), `u_glyph_uv` (vec2, the glyph cell's size in
/// normalised atlas coordinates -- one size for the whole draw, since every
/// glyph of a frame comes from one atlas; read in *both* stages, the fragment
/// one needing it for the clamp), `u_tex_size` (vec2, atlas size in texels),
/// `u_atlas` (sampler2D, unit 0).
extern const char* const kTextVS;
extern const char* const kTextFS;
/// GLSL source of the phosphor-density pipeline: one textured quad over the axes rect.
///
/// The vertex shader builds the quad from `gl_VertexID` -- a four-vertex `GL_TRIANGLE_STRIP`
/// like the other two pipelines, and the only one with **no vertex attributes at all**: the
/// image is one rectangle whose corners are uniform state, so there is no geometry to stage and
/// nothing to upload (the density's own bytes go to a texture, not to a buffer).
///
/// Uniforms: `u_viewport` (vec2, pixels), `u_origin` (vec2, the quad's top-left corner in `Rect`
/// pixels), `u_size` (vec2, its size in pixels -- the axes rect the image is drawn over, which the
/// texture's cells are spread across: one texel a cell while the rect is under `kDensityCellCap`,
/// and a filtered magnification past it), `u_color` (vec4), `u_density` (sampler2D, unit 0).
///
/// The fragment shader multiplies the sampled single-channel image into `u_color`'s alpha, which
/// is the whole of the brightness mapping: a cell's count was turned into a luminance byte on the
/// CPU (`Plot`), where the count, the decay and the gamma live, so this stage has no scale of its
/// own to get out of step with the numbers a host reads back.
///
/// `Rect` is top-left origin while GL is bottom-left, so the y axis is flipped exactly once,
/// here, as in the line pipeline.
extern const char* const kDensityVS;
extern const char* const kDensityFS;
} // namespace plot::render