// SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: 2026 Martino Ferrari #pragma once #include #include #include #include namespace plot::render { /// Linked line program and its uniform surface. /// /// It owns the `plot::gl::Program` and knows the GLSL pair (shaders.hpp), so no /// caller spells out a uniform name that could drift from the shader. Creating /// it propagates what the GL layer reports: `ErrorCode::Unsupported` while /// `plot::gl::available()` is false, `GLFailure` plus the driver's log when the /// GLSL does not compile or link. class LineProgram { public: bool create(Status* st = nullptr) noexcept; bool valid() const noexcept { return program_.id() != 0; } void bind() const noexcept { program_.use(); } /// `u_scale`: data space to pixels. void set_scale(Vec2 data_to_px) const noexcept; /// `u_offset`: pixel offset added after `set_scale`. void set_offset(Vec2 px) const noexcept; /// `u_viewport`: render target size in pixels. void set_viewport(Vec2 px) const noexcept; /// `u_width`: stroke width in pixels. void set_width(f32 px) const noexcept; /// `u_dash`: the dash pattern in pixels -- `x` the ink length, `y` the gap, and a /// non-positive `y` (the default a caller passes) means **solid**. It is a fragment-shader /// discard on the interpolated `v_along`, so it costs no geometry, and it is the one uniform /// that distinguishes the crosshair's stroke from every other line in the library. void set_dash(Vec2 on_off) const noexcept; /// `u_aa`: the AA fringe half-width in pixels. The style's `line_aa_px` in /// device pixels, clamped at 0.25; 1.0 reproduces the historical fringe. void set_aa(f32 px) const noexcept; /// `u_color`: stroke colour, alpha included. void set_color(Color color) const noexcept; /// `u_snap`: with it on, an **axis-aligned** segment's ink is rounded onto whole /// pixels along the axis it is constant on, so a hairline lights one column (or /// row) fully instead of two at half coverage. It is for the chrome that frames a /// plot -- grid lines, axis lines, panel and legend borders. Anything that reports /// a data position -- a trace, a cursor, a region edge, the crosshair -- leaves it /// off, because the rounding moves a line by up to half a pixel. A diagonal segment /// is untouched whatever this says. void set_snap(bool on) const noexcept; private: gl::Program program_; }; /// Linked text program and its uniform surface. It fails exactly the way /// `LineProgram::create()` does, propagating `Unsupported` and `GLFailure`. /// /// `create()` also points the `u_atlas` sampler at texture unit 0, which is the /// unit `plot::gl::Texture::bind()` defaults to, and leaves this program bound. /// The atlas itself stays with the caller, who owns its lifetime. class TextProgram { public: bool create(Status* st = nullptr) noexcept; bool valid() const noexcept { return program_.id() != 0; } void bind() const noexcept { program_.use(); } /// `u_viewport`: render target size in pixels. void set_viewport(Vec2 px) const noexcept; /// `u_px_range`: the glyph cell's size on screen, in pixels (width and /// height -- a cell need not be square). void set_px_range(Vec2 px) const noexcept; /// `u_glyph_uv`: the glyph cell's size in the atlas, normalised. It is the /// rect every staged glyph is read through, so it is one size per draw -- and /// it is the *backend's* (`TextBackend::cell_uv`), never a glyph's, because a /// draw may stage glyphs whose origins differ even when their size cannot. void set_glyph_uv(Vec2 uv) const noexcept; /// `u_tex_size`: atlas size in texels. void set_texture_size(Vec2 texels) const noexcept; /// The tint is per instance (`a_colour`), not a uniform, so a draw can carry /// runs of several colours -- there is deliberately no colour setter here. private: gl::Program program_; }; /// Linked phosphor-density program and its uniform surface: one textured quad over a plot's /// axes rect. It fails exactly the way the two above do, propagating `Unsupported` and /// `GLFailure`. /// /// It is the one pipeline with **no vertex data**: the quad's four corners come from /// `gl_VertexID`, so a draw is one four-vertex strip whatever the grid's size and there is no /// buffer to stage or upload. The density itself reaches the GPU as a texture -- one texel per /// cell -- which is the one thing this pipeline has that the others do not. /// /// `create()` also points the `u_density` sampler at texture unit 0, which is the unit /// `plot::gl::Texture::bind()` defaults to, and leaves this program bound. The texture stays /// with the caller, who owns its lifetime. class DensityProgram { public: bool create(Status* st = nullptr) noexcept; bool valid() const noexcept { return program_.id() != 0; } void bind() const noexcept { program_.use(); } /// `u_viewport`: render target size in pixels. void set_viewport(Vec2 px) const noexcept; /// `u_origin` and `u_size`: the quad in `Rect` pixels -- the axes rect's top-left corner and /// the grid's own size, which is the axes rect in whole device pixels. They are two uniforms /// rather than a rect because a `vec4` uniform would need a fourth setter shape for one /// caller; a corner and a size are what the vertex shader builds the quad from anyway. void set_rect(Vec2 origin, Vec2 size) const noexcept; /// `u_color`: the tint the cells' luminances are multiplied into. void set_color(Color color) const noexcept; private: gl::Program program_; }; } // namespace plot::render