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.
This commit is contained in:
1 parent
f132a5ec92
commit
38ef459897
77 files changed
+5905
-688
No files matched your search
@@ -35,10 +35,11 @@ struct LineInstance {
|
||||
/// rect onto the two the same way round, so a cell is never mirrored. `(1, 0)`
|
||||
/// is unrotated text, and `(0, -1)` is a run advancing up the screen, i.e. a
|
||||
/// 90 degree counter-clockwise label such as a y-axis title.
|
||||
/// - `(r, g, b, a)` is the run's tint, straight alpha. Per instance rather than
|
||||
/// a uniform, so one draw can carry a readout whose values are coloured per
|
||||
/// row (each row's own trace) while its labels stay the axis text colour.
|
||||
///
|
||||
/// Six floats, so the whole text pipeline still fits one instanced draw call per
|
||||
/// frame with rotation free: a matrix per glyph would be three times the bytes
|
||||
/// for the same pixels.
|
||||
/// Ten floats, still one instanced draw call per frame.
|
||||
struct TextInstance {
|
||||
f32 x;
|
||||
f32 y;
|
||||
@@ -46,10 +47,14 @@ struct TextInstance {
|
||||
f32 v;
|
||||
f32 ax;
|
||||
f32 ay;
|
||||
f32 r;
|
||||
f32 g;
|
||||
f32 b;
|
||||
f32 a;
|
||||
};
|
||||
|
||||
static_assert(sizeof(LineInstance) == 4 * sizeof(f32), "line instance must stay 4 floats");
|
||||
static_assert(sizeof(TextInstance) == 6 * sizeof(f32), "text instance must stay 6 floats");
|
||||
static_assert(sizeof(TextInstance) == 10 * sizeof(f32), "text instance must stay 10 floats");
|
||||
|
||||
/// How a store's staged instances reach the GPU. Semantics are identical either
|
||||
/// way -- only the transfer differs, and `Renderer::init()` picks one from
|
||||
@@ -137,7 +142,9 @@ class InstanceStore {
|
||||
/// buffer cannot leave the vertex array bound to a deleted one), and `slot_epoch_`
|
||||
/// advances on every upload that lands in a new slot. Both counters only ever
|
||||
/// increase, so their sum changes whenever either does, and never repeats.
|
||||
u32 epoch() const noexcept { return buffer_.generation() + slot_epoch_; }
|
||||
/// 64-bit: the sum of two counters that advance every upload of a streaming
|
||||
/// frame loop must not be able to wrap into a stale value.
|
||||
u64 epoch() const noexcept { return buffer_.generation() + slot_epoch_; }
|
||||
|
||||
void bind() const noexcept { buffer_.bind(); }
|
||||
bool persistent() const noexcept { return mode_ == UploadMode::PersistentMapped; }
|
||||
@@ -165,7 +172,7 @@ class InstanceStore {
|
||||
u32 frame_uploads_ = 0;
|
||||
/// Times an upload landed in a new slot. Kept apart from the buffer's own
|
||||
/// identity (see `epoch()`) so neither has to know about the other.
|
||||
u32 slot_epoch_ = 0;
|
||||
u64 slot_epoch_ = 0;
|
||||
u8* mapped_ = nullptr; ///< base of the mapped ring, null in `Orphan` mode
|
||||
/// Guards every slot the frame just ended wrote. Recorded once, after its last
|
||||
/// draw; waited on once, at the start of the next frame.
|
||||
@@ -186,7 +193,7 @@ class LineBuffer {
|
||||
}
|
||||
bool mark_drawn() noexcept { return store_.mark_drawn(); }
|
||||
usize slot_offset() const noexcept { return store_.slot_offset(); }
|
||||
u32 epoch() const noexcept { return store_.epoch(); }
|
||||
u64 epoch() const noexcept { return store_.epoch(); }
|
||||
bool persistent() const noexcept { return store_.persistent(); }
|
||||
void bind() const noexcept { store_.bind(); }
|
||||
|
||||
@@ -206,7 +213,7 @@ class TextBuffer {
|
||||
}
|
||||
bool mark_drawn() noexcept { return store_.mark_drawn(); }
|
||||
usize slot_offset() const noexcept { return store_.slot_offset(); }
|
||||
u32 epoch() const noexcept { return store_.epoch(); }
|
||||
u64 epoch() const noexcept { return store_.epoch(); }
|
||||
bool persistent() const noexcept { return store_.persistent(); }
|
||||
void bind() const noexcept { store_.bind(); }
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-FileCopyrightText: 2026 Martino Ferrari
|
||||
|
||||
#pragma once
|
||||
#include <plot/core/color.hpp>
|
||||
#include <plot/core/types.hpp>
|
||||
#include <plot/math/vec2.hpp>
|
||||
#include <plot/render/text.hpp>
|
||||
@@ -60,7 +61,7 @@ class LineBatch {
|
||||
/// Like `LineBatch` it keeps its capacity across frames. Each staged glyph is
|
||||
/// one instance: the glyph cell's top-left corner in pixels, the glyph rect's
|
||||
/// normalised top-left in the atlas and the cell's rotation basis, interleaved as
|
||||
/// `x,y,u,v,ax,ay` — the layout the text shader declares (see `TextInstance` in
|
||||
/// `x,y,u,v,ax,ay,r,g,b,a` — the layout the text shader declares (see `TextInstance` in
|
||||
/// buffer.hpp).
|
||||
///
|
||||
/// Every glyph a batch stages is read through one atlas rect of one size — its
|
||||
@@ -92,8 +93,9 @@ class TextBatch {
|
||||
/// rasterised them. `axis` is the direction the run advances in, in pixel
|
||||
/// space; it rotates the cell (see `TextInstance`), and is normalised here, so
|
||||
/// any non-zero direction works and a zero or non-finite one means "unrotated".
|
||||
void add_glyph(Vec2 pen_px, const Glyph& glyph, f32 px = 1.0f,
|
||||
Vec2 axis = {1.0f, 0.0f});
|
||||
/// `color` tints this run's glyphs (per-vertex `a_colour`, see `TextInstance`).
|
||||
void add_glyph(Vec2 pen_px, const Glyph& glyph, f32 px = 1.0f, Vec2 axis = {1.0f, 0.0f},
|
||||
Color color = {1.0f, 1.0f, 1.0f, 1.0f});
|
||||
|
||||
/// Stages every codepoint of `utf8` from `pen_px` and returns the pen past the
|
||||
/// last glyph, so the run is exactly `font.measure(utf8) * px` long measured
|
||||
@@ -104,14 +106,14 @@ class TextBatch {
|
||||
/// `next_codepoint()` and `glyph()`, so a run stages exactly what `measure()`
|
||||
/// summed. A codepoint the backend has no glyph for stages nothing and moves
|
||||
/// the pen nowhere — a run is never wider than the glyphs it can draw. A null
|
||||
/// or empty string stages nothing and returns `pen_px`.
|
||||
/// or empty string stages nothing and returns `pen_px`. `color` tints the run.
|
||||
Vec2 add_text(const char* utf8, Vec2 pen_px, f32 px, const TextBackend& font,
|
||||
Vec2 axis = {1.0f, 0.0f});
|
||||
Vec2 axis = {1.0f, 0.0f}, Color color = {1.0f, 1.0f, 1.0f, 1.0f});
|
||||
|
||||
/// Glyphs staged so far, one instance each.
|
||||
u32 glyph_count() const noexcept { return static_cast<u32>(v_.size() / 6u); }
|
||||
u32 glyph_count() const noexcept { return static_cast<u32>(v_.size() / 10u); }
|
||||
|
||||
/// Interleaved `x,y,u,v,ax,ay`, six floats per staged glyph.
|
||||
/// Interleaved `x,y,u,v,ax,ay,r,g,b,a`, ten floats per staged glyph.
|
||||
const std::vector<f32>& data() const noexcept { return v_; }
|
||||
|
||||
/// The cell the staged glyphs are drawn in: what `set_cell()` declared, or --
|
||||
@@ -124,7 +126,7 @@ class TextBatch {
|
||||
private:
|
||||
/// Stages one cell whose basis the caller has already normalised, so a run and
|
||||
/// the pen it advances share one rounding of the axis.
|
||||
void add_cell(Vec2 pen_px, const Glyph& glyph, f32 px, Vec2 unit_ux);
|
||||
void add_cell(Vec2 pen_px, const Glyph& glyph, f32 px, Vec2 unit_ux, Color color);
|
||||
|
||||
std::vector<f32> v_;
|
||||
Vec2 cell_uv_{};
|
||||
|
||||
@@ -35,6 +35,9 @@ class LineProgram {
|
||||
/// 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;
|
||||
|
||||
@@ -66,8 +69,8 @@ class TextProgram {
|
||||
void set_glyph_uv(Vec2 uv) const noexcept;
|
||||
/// `u_tex_size`: atlas size in texels.
|
||||
void set_texture_size(Vec2 texels) const noexcept;
|
||||
/// `u_color`: tint colour, alpha included.
|
||||
void set_color(Color color) 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_;
|
||||
|
||||
@@ -31,6 +31,7 @@ struct FrameStats {
|
||||
u32 lod_traces = 0; ///< traces drawn from a min/max pyramid level
|
||||
u64 visible_samples = 0; ///< samples the gathered windows covered, summed
|
||||
u32 staged_segments = 0; ///< line segments staged, all colour groups
|
||||
u32 staged_markers = 0; ///< marker shapes staged over the traces
|
||||
u32 draw_calls = 0; ///< draw calls issued, line and text
|
||||
};
|
||||
|
||||
@@ -70,10 +71,26 @@ struct FrameStats {
|
||||
class Renderer {
|
||||
public:
|
||||
Renderer() noexcept = default;
|
||||
~Renderer() = default;
|
||||
/// Releases every GL handle first (`release_gl()`), so the destructor is safe
|
||||
/// whenever the context that owns them is still current -- the normal case for
|
||||
/// a member renderer destroyed by its host's widget teardown.
|
||||
~Renderer();
|
||||
Renderer(const Renderer&) = delete;
|
||||
Renderer& operator=(const Renderer&) = delete;
|
||||
|
||||
/// Destroys every GL object this renderer created: both programs, both vertex
|
||||
/// arrays and their instance buffers, and the glyph atlas texture. The
|
||||
/// renderer is left uninitialised (`ready() == false`) and `init()` can be
|
||||
/// called again on the same object.
|
||||
///
|
||||
/// **Teardown contract:** call this while the context that owns the names is
|
||||
/// still current, before the host destroys that context -- a GL name deleted
|
||||
/// after its context is gone is leaked, not deleted. The destructor calls this
|
||||
/// first for the common case of a member renderer whose context outlives it;
|
||||
/// a host that tears the context down before its renderer must call
|
||||
/// `release_gl()` explicitly.
|
||||
void release_gl() noexcept;
|
||||
|
||||
/// Compiles both programs, uploads the font's atlas and allocates the vertex
|
||||
/// buffers. The calling thread must have the context current and
|
||||
/// `plot::gl::load()` must have succeeded, otherwise the returned `Status` is
|
||||
@@ -215,7 +232,7 @@ class Renderer {
|
||||
///
|
||||
/// The GL state it leaves behind, so a host can embed the plot without guessing:
|
||||
/// vertex array 0, program 0, `GL_ARRAY_BUFFER` 0, texture unit 0 active and
|
||||
/// nothing bound to `GL_TEXTURE_2D` on it, blending off, scissoring off, and the
|
||||
/// nothing bound to `GL_TEXTURE_2D` on it, scissoring off, and the
|
||||
/// viewport left at the frame's rect (the caller sets its own for the next pass).
|
||||
///
|
||||
/// The scissor is the other piece of state a frame moves, and it ends the frame *off*:
|
||||
@@ -346,12 +363,19 @@ class Renderer {
|
||||
/// One axis pair's data -> pixel mapping, already shifted so the viewport's
|
||||
/// top-left corner is the origin (the shaders emit NDC relative to the GL
|
||||
/// viewport, not the framebuffer).
|
||||
///
|
||||
/// `origin_x` is the data-space reference the caller staged its geometry
|
||||
/// relative to: every data-space x reaches this layer as the f32
|
||||
/// `to_space(x) - to_space(origin_x)` (B1), so the mapping folds the origin
|
||||
/// back into the offset uniform -- `offset_x = sx*to_space(origin_x) + tx -
|
||||
/// viewport.x`, computed in f64 and narrowed once -- and the staged values
|
||||
/// stay O(visible pixels) however large the absolute x is (an epoch axis).
|
||||
struct Mapping {
|
||||
Vec2 scale{1.0f, 1.0f};
|
||||
Vec2 offset{0.0f, 0.0f};
|
||||
};
|
||||
|
||||
Mapping mapping(const Plot& plot, u32 axis_x, u32 axis_y) const noexcept;
|
||||
Mapping mapping(const Plot& plot, u32 axis_x, u32 axis_y, f64 origin_x) const noexcept;
|
||||
/// The identity mapping for geometry already staged in viewport pixels.
|
||||
Mapping pixel_mapping() const noexcept;
|
||||
/// Shifts one framebuffer-space pixel position into viewport space.
|
||||
@@ -370,12 +394,14 @@ class Renderer {
|
||||
///
|
||||
/// `dash` is the pattern in pixels for the fragment shader's discard: `x` the ink
|
||||
/// length, `y` the gap, and a non-positive `y` -- the default every caller but the
|
||||
/// crosshair passes -- means **solid**. It is set on every flush, so one draw's dash
|
||||
/// can never leak into the next.
|
||||
/// crosshair passes and a dashed trace -- means **solid**. It is set on every flush,
|
||||
/// so one draw's dash can never leak into the next. The AA fringe (`u_aa`) is
|
||||
/// likewise set on every flush, from `style().line_aa_px` in device pixels.
|
||||
void flush_lines(const Mapping& map, Color color, f32 width, Vec2 dash = Vec2{}) noexcept;
|
||||
/// Draws every staged glyph in `style().axis_text` and empties the batch. Text
|
||||
/// is texture-bound, so this runs once per frame, after all line geometry.
|
||||
void flush_text() noexcept;
|
||||
/// Draws every staged glyph in `batch` -- each instance carries its own tint
|
||||
/// (`a_colour`) -- and empties the batch. Text is texture-bound, so this runs
|
||||
/// once per text batch, after the line geometry it belongs over.
|
||||
void flush_text(render::TextBatch& batch) noexcept;
|
||||
|
||||
/// Narrows the frame's clip to `rect` -- framebuffer pixels, top-left origin, like every
|
||||
/// `Rect` here -- and *intersects* it with the frame's viewport, so the clip can only
|
||||
@@ -408,13 +434,20 @@ class Renderer {
|
||||
/// measurement band fills, is the wrong primitive here). A rect with no width or
|
||||
/// no height stages nothing.
|
||||
void fill_pixels(const Rect& rect, Color color);
|
||||
/// Stages one data-space segment, taking the two axes' log flags into account.
|
||||
void stage_segment(f64 x0, f64 y0, f64 x1, f64 y1, bool log_x, bool log_y);
|
||||
/// Stages `n` interleaved `x,y` data-space points (a gathered window).
|
||||
void stage_xy(const f32* xy, u32 n, bool log_x, bool log_y);
|
||||
/// Lays one run of text out at a framebuffer position and stages it, shifting
|
||||
/// it into viewport space. Returns the pen past the run.
|
||||
Vec2 stage_text(Vec2 pos_px, const char* utf8);
|
||||
/// Stages one data-space segment as f32 values **relative to `origin_x`** in
|
||||
/// the axis' pre-affine space (`log10` of it on a log axis): the mapping folds
|
||||
/// `origin_x` back in, in f64, before the single f32 narrowing (B1), so a
|
||||
/// large-magnitude x -- an epoch axis -- never loses sub-pixel precision.
|
||||
void stage_segment(f64 origin_x, f64 x0, f64 y0, f64 x1, f64 y1, bool log_x, bool log_y);
|
||||
/// Stages `n` interleaved `x,y` points of a gathered window (see
|
||||
/// `Series::gather`): x **relative to the series' `x0()`**, y absolute. The
|
||||
/// absolute data x is reconstructed in f64 (`series_x0 + rel`) and staged as
|
||||
/// `to_space(x_abs) - to_space(origin_x)` -- O(visible pixels), not O(epoch).
|
||||
void stage_xy(const f32* xy, u32 n, f64 series_x0, f64 origin_x, bool log_x, bool log_y);
|
||||
/// Lays one run of text out at a framebuffer position and stages it into
|
||||
/// `batch`, shifting it into viewport space and tinting it `color` (the
|
||||
/// per-instance `a_colour`). Returns the pen past the run.
|
||||
Vec2 stage_text(render::TextBatch& batch, Vec2 pos_px, const char* utf8, Color color);
|
||||
/// Width of the longest prefix of `utf8` that fits `budget_px` along `axis`, at
|
||||
/// the size the text pipeline draws with: exactly what `stage_text_clipped()`
|
||||
/// stages.
|
||||
@@ -430,7 +463,8 @@ class Renderer {
|
||||
/// horizontal x title, which spends the strip's width, and (0,-1) for the y
|
||||
/// title, which reads bottom-to-top and spends the strip's height. `pos_px` is
|
||||
/// the pen -- for the rotated run, the bottom-left corner of its first cell.
|
||||
f32 stage_text_clipped(Vec2 pos_px, const char* utf8, f32 budget_px, Vec2 axis);
|
||||
f32 stage_text_clipped(render::TextBatch& batch, Vec2 pos_px, const char* utf8, f32 budget_px,
|
||||
Vec2 axis, Color color);
|
||||
|
||||
/// Pins the text pipeline for a frame: the plot layer's backend at the size the
|
||||
/// style asks for, the cell every staged glyph is drawn in, and the atlas the
|
||||
@@ -531,6 +565,12 @@ class Renderer {
|
||||
/// pointer is on: a readout the legend covered would be the feature failing at its one job.
|
||||
void draw_crosshair_readout(const Plot& plot, const PlotLayout& layout);
|
||||
|
||||
/// One marker shape (`Marker`) at a viewport-pixel centre: `Cross` two
|
||||
/// segments, `Circle` an 8-gon, `Square` four. Staged into the line batch in
|
||||
/// pixel space, at `size_px` half-extent, so a marker's size is device pixels
|
||||
/// whatever the data scale is.
|
||||
void stage_marker(plot::Marker marker, Vec2 centre_px, f32 size_px);
|
||||
|
||||
/// The built-in backend the renderer owns, and the face the frames draw with:
|
||||
/// `set_text_backend()` writes `font_`, which starts here and comes back here
|
||||
/// when a host passes null. Every use of the face goes through the
|
||||
@@ -549,8 +589,16 @@ class Renderer {
|
||||
render::TextVertexArray text_vao_;
|
||||
|
||||
render::LineBatch lines_;
|
||||
render::TextBatch glyphs_;
|
||||
/// The frame's two text batches (B6): `glyphs_under_` collects the text the
|
||||
/// crosshair readout's panel may legitimately occlude (legend names, cursor
|
||||
/// labels, measure labels, annotation captions) and is flushed immediately
|
||||
/// before `draw_crosshair_readout()`; `glyphs_over_` collects what must sit
|
||||
/// over everything (readout text, tick labels, titles) and is flushed at
|
||||
/// `end_frame()`. One batch per z-order, one draw call each.
|
||||
render::TextBatch glyphs_under_;
|
||||
render::TextBatch glyphs_over_;
|
||||
std::vector<f32> gather_; ///< scratch for one gathered window, re-used
|
||||
std::vector<f32> staged_; ///< scratch for one staged window, re-used
|
||||
mutable std::vector<u8> readback_; ///< dump_frame_ppm's row buffer
|
||||
|
||||
/// The host's `GL_COLOR_CLEAR_VALUE`, queried on the first `begin_frame()` (GL
|
||||
@@ -559,6 +607,16 @@ class Renderer {
|
||||
Color host_clear_{};
|
||||
bool host_clear_saved_ = false;
|
||||
|
||||
/// The host's blend state, saved by `begin_frame()` and handed back by
|
||||
/// `end_frame()` (R6): a frame sets blending for its straight-alpha pipelines
|
||||
/// and must not leave its function -- or the enable bit -- behind for the
|
||||
/// host's own draws to inherit.
|
||||
gl::GLint blend_src_rgb_ = gl::GL_ONE;
|
||||
gl::GLint blend_src_a_ = gl::GL_ONE;
|
||||
gl::GLint blend_dst_rgb_ = gl::GL_ZERO;
|
||||
gl::GLint blend_dst_a_ = gl::GL_ZERO;
|
||||
bool blend_was_on_ = false;
|
||||
|
||||
Rect viewport_; ///< the frame's region, top-left origin, framebuffer px
|
||||
Rect gl_viewport_; ///< the same rect flipped for GL, and for readback
|
||||
u32 fb_w_ = 0;
|
||||
|
||||
@@ -16,7 +16,9 @@ namespace plot::render {
|
||||
/// `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_color` (vec4).
|
||||
/// `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.
|
||||
///
|
||||
@@ -44,13 +46,14 @@ extern const char* const kLineFS;
|
||||
/// handedness, so a glyph's top row stays its top row however the cell is
|
||||
/// turned. The fragment shader converts back to texels and samples the
|
||||
/// single-channel `GL_R8` atlas at the nearest texel centre, so `GL_NEAREST`
|
||||
/// never lands on a cell boundary. `u_color` tints the straight alpha.
|
||||
/// never lands on a cell boundary. 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), `u_tex_size` (vec2, atlas size in
|
||||
/// texels), `u_atlas` (sampler2D, unit 0), `u_color` (vec4).
|
||||
/// texels), `u_atlas` (sampler2D, unit 0).
|
||||
extern const char* const kTextVS;
|
||||
extern const char* const kTextFS;
|
||||
|
||||
|
||||
@@ -102,6 +102,12 @@ struct Style {
|
||||
bool grid_minor_visible = true; ///< stage the minor grid at all.
|
||||
f32 line_width_px = 1.5f; ///< logical width of the strokes framing the data
|
||||
///< (axis lines, cursor markers).
|
||||
/// Logical half-width of the anti-aliased fringe the line shader blends at
|
||||
/// every stroke edge (the smoothstep ramp runs over `|dist| in [w - aa,
|
||||
/// w + aa]`). Clamped to at least 0.25 device px at use: below that the
|
||||
/// ramp is thinner than a fragment and edges alias again. 1.0 is the value
|
||||
/// every default was chosen on.
|
||||
f32 line_aa_px = 1.0f;
|
||||
/// Logical width of a *draggable handle* drawn highlighted: the tab a hovered or
|
||||
/// grabbed cursor marker is widened to, and the tab a region's hot bound is drawn
|
||||
/// as -- the latter sized by `Plot::layout()` with this same metric, so the two
|
||||
|
||||
@@ -54,20 +54,21 @@ class LineVertexArray {
|
||||
gl::VertexArray vao_;
|
||||
u32 instances_ = 0;
|
||||
/// Instance-buffer generation the attribute pointers currently point into.
|
||||
u32 epoch_ = 0;
|
||||
u64 epoch_ = 0;
|
||||
};
|
||||
|
||||
/// Vertex array plus instance buffer of the text pipeline: `a_pos` at location 0
|
||||
/// (the cell's top-left corner in pixels), the glyph's atlas rect `a_uv` at
|
||||
/// location 1 and its rotation basis `a_axis` at location 2, two floats each,
|
||||
/// every one advancing once per instance. The layout is the one `TextInstance`
|
||||
/// location 1, its rotation basis `a_axis` at location 2 and the run's tint
|
||||
/// `a_colour` at location 3 (`vec2`, `vec2`, `vec2`, `vec4` floats), each
|
||||
/// advancing once per instance. The layout is the one `TextInstance`
|
||||
/// declares and shaders.hpp consumes; a caller never writes a location, stride or
|
||||
/// offset, it stages a `TextBatch` and uploads its `data()`.
|
||||
class TextVertexArray {
|
||||
public:
|
||||
bool create(Status* st = nullptr, UploadMode mode = UploadMode::Orphan) noexcept;
|
||||
|
||||
/// Uploads `glyphs` instances from interleaved `x,y,u,v,ax,ay` floats.
|
||||
/// Uploads `glyphs` instances from interleaved `x,y,u,v,ax,ay,r,g,b,a` floats.
|
||||
void upload(const f32* interleaved, u32 glyphs) noexcept;
|
||||
|
||||
void bind() const noexcept { vao_.bind(); }
|
||||
@@ -84,7 +85,7 @@ class TextVertexArray {
|
||||
TextBuffer buffer_;
|
||||
gl::VertexArray vao_;
|
||||
u32 instances_ = 0;
|
||||
u32 epoch_ = 0;
|
||||
u64 epoch_ = 0;
|
||||
};
|
||||
|
||||
} // namespace plot::render
|
||||
Reference in new issue
Block a user