Files
plotter/src/render/shaders.cpp
T
Martino Ferrari 38ef459897
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
feat(plot)!: harden pipeline, XY series, dash/markers, touch, packaging
Breaking: Series::gather emits x relative to x0() and takes a GatherMode;
consumers reconstruct absolute x in f64 before mapping (series.hpp documents
the contract).

Bugs, each pinned by a fail-before regression test:
- render: f32 staging quantised epoch-time x into 128 px columns; every
  data-space draw now stages to_space(x) - to_space(origin) with the origin
  folded into the offset uniform in f64 (epoch/zero-origin twins rasterise
  identically)
- ticks: major test was ULP-fragile and blanked epoch axes; majors are an
  integer-index test and are never truncated by the minor cap
- gather: LOD envelope emitted buckets straddling [begin,end); ceil/floor
  bucket edges + raw lead/trail spans
- live follow: log x axes held a linear span and zoomed every frame; span and
  offset now live in axis space (plot + both figure link-group paths)
- input: a zero-notch wheel disarmed auto_scale on every axis; dy==0 pans x,
  factor == 1 / inf refused in wheel and drag zoom
- render: the single text flush painted legend names over the crosshair
  readout panel; two batches, under flushed before the panel
- data: NaN dropped from min/max unless it seeded the fold; non-finite
  samples are now excluded from every aggregate and from cached bounds

Robustness: viewport normalisation (no clamp UB), non-finite range refusal,
pyramid level cap at 57 (no shift/div UB through public gather), rounded ring
capacity documented + pre-sized, GL blend func and UNPACK_ALIGNMENT
save/restore, Renderer::release_gl() teardown, u64 buffer epochs, annotation
captions clamped, button-aware GLFW row capture, cached figure row rects (no
per-event layout), ruler-pair measurement refused, span-nonfinite guards in
axis_ends.

Features: XY series (sorted x, binary-search neighbours/snap, LOD over sample
index); StreamSeries::freeze(); ArraySeries::push(); per-trace dash and
markers (scatter at width 0) with auto or literal stride; GatherMode
Envelope/Nearest/Mean via bucket_sum/sumsq; Style::line_aa_px; per-glyph text
colour (readout values in trace colour); wheel-dx x pan; arrow-key pan;
Escape cancels a drag and restores captured ranges; modifier-masked box zoom
(Shift both, Ctrl x, Alt y); double-click reset via PointerEvent::time_s;
ImGui key repeat unified; pinch gestures on Qt and GTK; pixelDelta wheel path
and DevicePixelRatioChange handling; GNUInstallDirs, SOVERSION,
SameMinorVersion, generated plot/version.hpp, relocatable plot.pc, licence
install, FILES_MATCHING, GitHub Actions CI (debug/coverage/asan).

Suite 283 -> 350 cases, 0 failed; coverage 93.9% (3730/3971) over 24 files,
floor 80/90 held; ASAN preset suite green; all four demos run headless (the
GTK scripted-resize check needs a window manager and fails identically on the
parent commit); readme_main.cpp committed and compiles with the README
command; pkg-config and find_package consumption verified from a relocated
install prefix.
2026-09-30 17:32:06 +02:00

157 lines
7.0 KiB
C++

// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2026 Martino Ferrari
#include <plot/render/shaders.hpp>
// Both pipelines are `#version 330 core` and stay untouched by the upload
// strategy: the 4.3+ path changes how vertex data reaches the GPU, never the
// GLSL, so one program pair serves every context the library supports. The
// declarations here are the contract the render layer's attribute layouts and
// uniform setters mirror (see program.hpp, vertex_array.hpp, shaders.hpp).
namespace plot::render {
const char* const kLineVS = R"(#version 330 core
layout(location = 0) in vec2 a_p0; // per-instance segment start, data space
layout(location = 1) in vec2 a_p1; // per-instance segment end
uniform vec2 u_scale; // data -> pixels
uniform vec2 u_offset; // pixels
uniform vec2 u_viewport; // render target size, pixels
uniform float u_width; // stroke width, pixels
uniform float u_aa; // AA fringe half-width, pixels
out float v_dist; // signed distance to the segment axis, pixels
// Distance travelled from a_p0, pixels -- and the dash's phase anchor. Both of the
// crosshair's lines are staged from an *axes edge* (its vertical from `y_range.min`,
// the axes' bottom; its horizontal from `x_range.min`, the axes' left), so a dash measured
// from here is measured from a fixed edge of the rect: it does not crawl as the view pans
// or zooms, and a stationary crosshair renders identically frame to frame.
out float v_along;
void main() {
vec2 p0 = a_p0 * u_scale + u_offset;
vec2 p1 = a_p1 * u_scale + u_offset;
vec2 d = p1 - p0;
float len = length(d);
float half_w = u_width * 0.5 + u_aa; // + the AA fringe
// A degenerate segment has no direction: pin the normal, and give the quad
// area by stretching its two ends apart along x. Collapsing all four corners
// onto p0 would rasterise nothing, and a flat trace staged as p == p (the
// dense path's min == max buckets) would then silently vanish. When
// len > 1e-6 the two ends are p0 and p1 exactly as before.
vec2 n = len > 1e-6 ? vec2(-d.y, d.x) / len : vec2(0.0, 1.0);
vec2 e0 = len > 1e-6 ? p0 : p0 - vec2(half_w, 0.0);
vec2 e1 = len > 1e-6 ? p1 : p0 + vec2(half_w, 0.0);
// gl_VertexID picks the corner: 0,1 are the sides of the segment's start and
// 2,3 of its end, which is the order GL_TRIANGLE_STRIP expects.
float side = (gl_VertexID & 1) == 0 ? -1.0 : 1.0;
vec2 base = (gl_VertexID & 2) == 0 ? e0 : e1;
vec2 px = base + n * (side * half_w);
// Rect is top-left origin, GL is bottom-left: flip y once, here, and never
// again in the fragment shader or in a caller.
vec2 ndc = vec2(px.x / u_viewport.x * 2.0 - 1.0, 1.0 - px.y / u_viewport.y * 2.0);
gl_Position = vec4(ndc, 0.0, 1.0);
v_dist = side * half_w;
v_along = (gl_VertexID & 2) == 0 ? 0.0 : len;
}
)";
const char* const kLineFS = R"(#version 330 core
in float v_dist; // signed distance to the segment axis, pixels
in float v_along; // distance travelled from a_p0, pixels
uniform float u_width; // stroke width, pixels
uniform float u_aa; // AA fringe half-width, pixels
uniform vec4 u_color;
uniform vec2 u_dash; // dash pattern in pixels: x = ink, y = gap; y <= 0 is solid
out vec4 o_color;
void main() {
// The dash (spec §17): a fragment-shader discard, never new geometry -- the vertices
// stay in data space and the pattern is one per-draw uniform. `v_along` interpolates the
// distance from `a_p0` across the quad in pixels, so `mod` measures the pattern along the
// line rather than in screen space, which is what keeps it anchored to the segment while
// the view pans. `y <= 0` is solid, so every other stroke in the library -- grid, traces,
// axis lines, regions, cursors, the legend border -- draws unchanged by this uniform's
// default.
if (u_dash.y > 0.0) {
float period = u_dash.x + u_dash.y;
if (period > 0.0 && mod(v_along, period) >= u_dash.x) {
discard; // in the gap of the pattern
}
}
float half_w = u_width * 0.5;
float d = abs(v_dist);
if (d > half_w + u_aa) {
discard; // outside the fringe; nothing to blend
}
// Ramp over the fringe so a thin stroke keeps its weight instead of aliasing
// into a dashed line: the ramp is centred on `half_w` and spans `u_aa`, so
// `u_aa == 1.0` -- the style default -- reproduces the historical single-pixel
// ramp exactly. `u_aa` is the style's `line_aa_px` in device pixels, clamped
// at 0.25 by the renderer.
float alpha = 1.0 - smoothstep(half_w - 0.5 * u_aa, half_w + 0.5 * u_aa, d);
o_color = vec4(u_color.rgb, u_color.a * alpha);
}
)";
const char* const kTextVS = R"(#version 330 core
layout(location = 0) in vec2 a_pos; // per-instance glyph cell top-left, pixels
layout(location = 1) in vec2 a_uv; // per-instance glyph rect top-left, normalised
layout(location = 2) in vec2 a_axis; // per-instance cell +x axis, unit, pixels
layout(location = 3) in vec4 a_colour; // per-instance tint, straight alpha
uniform vec2 u_viewport; // render target size, pixels
uniform vec2 u_px_range; // glyph cell on screen, pixels (width, height)
uniform vec2 u_glyph_uv; // glyph cell in the atlas, normalised (width, height)
out vec2 v_uv; // normalised atlas coordinate of this fragment
out vec4 v_colour; // the instance's tint
void main() {
// The cell's two axes: the run direction the instance carries and its
// perpendicular, so the cell follows the basis instead of the screen axes. The
// perpendicular is the cell's own +v axis -- with an unrotated basis
// (1.0, 0.0) it is (0.0, 1.0), i.e. down the screen, which is the direction v
// grows in the atlas -- and building it this way keeps the quad's orientation:
// a mirrored perpendicular would draw every glyph flipped about its diagonal.
vec2 ux = a_axis;
vec2 uy = vec2(-ux.y, ux.x);
vec2 corner = vec2(float(gl_VertexID & 1), float((gl_VertexID >> 1) & 1));
vec2 px = a_pos + corner.x * u_px_range.x * ux + corner.y * u_px_range.y * uy;
// Rect is top-left origin, GL is bottom-left: the one and only y flip.
vec2 ndc = vec2(px.x / u_viewport.x * 2.0 - 1.0, 1.0 - px.y / u_viewport.y * 2.0);
gl_Position = vec4(ndc, 0.0, 1.0);
// The same handedness as the cell, so the glyph's rect lands on it the right
// way round: u runs along the basis, v down the glyph.
v_uv = a_uv + corner * u_glyph_uv;
v_colour = a_colour;
}
)";
const char* const kTextFS = R"(#version 330 core
in vec2 v_uv; // normalised atlas coordinate of this fragment
in vec4 v_colour; // the instance's tint
uniform sampler2D u_atlas; // GL_R8 glyph atlas, GL_NEAREST
uniform vec2 u_tex_size; // atlas size in texels
out vec4 o_color;
void main() {
// Sample the centre of the texel the fragment lands in. GL_NEAREST alone
// would tie at a texel boundary, where the driver may pick the neighbouring
// glyph cell; rounding to a centre removes the tie by construction.
vec2 texel = clamp(floor(v_uv * u_tex_size), vec2(0.0), u_tex_size - vec2(1.0));
float coverage = texture(u_atlas, (texel + vec2(0.5)) / u_tex_size).r;
o_color = vec4(v_colour.rgb, v_colour.a * coverage);
}
)";
} // namespace plot::render