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.
157 lines
7.0 KiB
C++
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
|