// SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: 2026 Martino Ferrari #include // 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