457 lines
25 KiB
C++
457 lines
25 KiB
C++
#pragma once
|
|
#include <span>
|
|
#include <vector>
|
|
|
|
#include <plot/core/color.hpp>
|
|
#include <plot/core/rect.hpp>
|
|
#include <plot/core/status.hpp>
|
|
#include <plot/core/types.hpp>
|
|
#include <plot/math/vec2.hpp>
|
|
#include <plot/plot/figure.hpp>
|
|
#include <plot/plot/plot.hpp>
|
|
#include <plot/render/embedded_mono_font.hpp>
|
|
#include <plot/render/buffer.hpp>
|
|
#include <plot/render/primitives.hpp>
|
|
#include <plot/render/program.hpp>
|
|
#include <plot/render/style.hpp>
|
|
#include <plot/render/text_atlas.hpp>
|
|
#include <plot/render/vertex_array.hpp>
|
|
|
|
namespace plot {
|
|
|
|
/// What one frame staged and drew. It exists because pixels alone cannot say
|
|
/// which of the two rasterisation paths a trace took: a 1M-sample series drawn
|
|
/// from the LOD pyramid and one drawn from raw samples can cover the same
|
|
/// columns, so the smoke test asserts the path here and the pixels there.
|
|
struct FrameStats {
|
|
u32 raw_traces = 0; ///< traces drawn from raw samples (level 0)
|
|
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 draw_calls = 0; ///< draw calls issued, line and text
|
|
};
|
|
|
|
/// Draws a `Plot` into whatever framebuffer is bound.
|
|
///
|
|
/// The renderer never creates, binds or resizes a framebuffer: `begin_frame()`
|
|
/// takes the framebuffer size for the projection and a `Rect` for the region the
|
|
/// plot occupies, clears exactly that region (`GL_SCISSOR_TEST` around the clear,
|
|
/// so a host that has already painted the rest of its surface keeps it) and sets
|
|
/// `glViewport` to the flipped rect. That is the whole embedding contract of the
|
|
/// adapters (Qt/GTK/ImGui widgets).
|
|
///
|
|
/// Two properties drive the rest of the design:
|
|
///
|
|
/// - **Only the visible window is gathered.** A trace's x range is mapped onto
|
|
/// sample indices and `Series::gather` is asked for that window -- never the
|
|
/// whole series -- at `pyramid().level_for(n, samples_per_pixel)` whenever a
|
|
/// pixel covers `dense_threshold_spp` samples or more. A 100M-sample series
|
|
/// therefore stages O(pixels) vertices, and `FrameStats` records which way each
|
|
/// trace went.
|
|
/// - **Vertex data stays in data space.** Segments are staged in data
|
|
/// coordinates and mapped to pixels by the line program's `u_scale`/`u_offset`
|
|
/// uniforms, so nothing is ever baked into screen space per primitive; a log
|
|
/// axis is staged in the transform's pre-affine coordinate (`log10`), which is
|
|
/// what the affine part expects. Pan and zoom are uniform updates.
|
|
///
|
|
/// The vertex transfer itself is one of two strategies, chosen once in `init()`
|
|
/// from `plot::gl::caps()` and invisible to every caller afterwards: a
|
|
/// persistent-mapped, fence-guarded ring on 4.3+/ARB_buffer_storage, and the
|
|
/// orphan-and-refill baseline on 3.3. Setting `PLOT_FORCE_GL33_UPLOAD=1` in the
|
|
/// environment forces the baseline path, which is how both are exercised on one
|
|
/// machine. A driver that refuses the storage, a map or a fence drops the store
|
|
/// that hit it to the baseline path -- the ring is never rotated through without
|
|
/// a guard -- and the frame reports it (see `last_frame_had_gl_error()`).
|
|
///
|
|
/// Not thread-safe, and confined to the thread that owns the current context.
|
|
class Renderer {
|
|
public:
|
|
Renderer() noexcept = default;
|
|
~Renderer() = default;
|
|
Renderer(const Renderer&) = delete;
|
|
Renderer& operator=(const Renderer&) = delete;
|
|
|
|
/// 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
|
|
/// `Unsupported`; anything the driver refuses is `GLFailure` plus its log.
|
|
bool init(Status* st = nullptr) noexcept;
|
|
|
|
/// The face every frame this renderer draws uses: the embedded one until a host
|
|
/// installs another here.
|
|
|
|
/// `init()` binds `render::EmbeddedMonoFont`, and `draw()` installs the binding
|
|
/// on the plot it is given (replacing whatever a host set with
|
|
/// `Plot::set_text_backend()`, which is how a plot with no renderer keeps its
|
|
/// own), so this is the way to draw with a face of your own -- a
|
|
/// `plot::text::TrueTypeFont` from the optional target, say, or any other
|
|
/// `TextBackend`. Passing null restores the embedded face. The swap takes
|
|
/// effect on the next frame, which re-reads the backend's metrics at
|
|
/// `style().device(axis_font_px)` and re-uploads its atlas -- so nothing here
|
|
/// has to know whether the replacement is a different face, the same face
|
|
/// re-baked at another size, or a backend whose pixels changed: the revision
|
|
/// comparison in `render::TextAtlas` decides, and a replacement whose atlas is
|
|
/// already on the GPU under the same revision is uploaded anyway (see the
|
|
/// implementation for why that is the safe direction).
|
|
///
|
|
/// The backend is borrowed, not owned: it must outlive the frames drawn with it,
|
|
/// which is the same lifetime rule `Plot::set_text_backend()` states.
|
|
void set_text_backend(const render::TextBackend* font) noexcept;
|
|
|
|
/// The backend this renderer *borrows*: whatever `set_text_backend()` was given,
|
|
/// or the embedded face when nothing was installed. It is deliberately **not**
|
|
/// the `AxisFont` wrapper the renderer builds around it -- after a `draw()`,
|
|
/// `Plot::text_backend()` reads back that wrapper, because the wrapper is what
|
|
/// the plot measured with -- so a caller checking or re-pointing the face itself
|
|
/// asks here.
|
|
const render::TextBackend* text_backend() const noexcept { return font_; }
|
|
|
|
/// Starts a frame: records `fb_w` x `fb_h` for the projection, flips
|
|
/// `viewport_px` (top-left origin, like every `Rect` in this library) into GL's
|
|
/// bottom-left origin once, clears that region to `style().background` and
|
|
/// empties the staging batches.
|
|
///
|
|
/// It is also where the instance stores wait -- once each -- for the previous
|
|
/// frame's draws to retire the ring they read, before this frame writes any
|
|
/// slot (see `InstanceStore::begin_frame`): a frame never synchronises with the
|
|
/// work it issued itself. A store that cannot retire that frame drops to the
|
|
/// baseline path, which this frame reports (see `last_frame_had_gl_error()`).
|
|
void begin_frame(u32 fb_w, u32 fb_h, Rect viewport_px) noexcept;
|
|
|
|
/// Draws `plot`, refreshing its layout, ticks and grid first.
|
|
///
|
|
/// The renderer owns the presentation, style and text both: `draw()` copies
|
|
/// `style()` into the plot, hands it the style's `ui_scale` *before* it lays out
|
|
/// (`Plot::set_ui_scale()`) and installs its own font backend on it, so the strips
|
|
/// and tick densities the plot lays out with are the values the renderer draws
|
|
/// with -- set colours and metrics here, not on the plot, and expect a backend
|
|
/// set with `Plot::set_text_backend()` to be replaced on the first frame (a
|
|
/// debug build names the one it dropped). The renderer must therefore outlive the
|
|
/// plot's use of it.
|
|
///
|
|
/// Every metric in `style()` is a logical unit, so this is also where the scale
|
|
/// becomes pixels: the font is reset at `style().device(axis_font_px)` and each
|
|
/// stroke and padding is converted through the same call. A host therefore states
|
|
/// its display once, in `style().ui_scale`, and the frame it gets back is the same
|
|
/// physical size on a 1x panel and a 2x one.
|
|
///
|
|
/// `Plot::layout()` is computed exactly once, here, and handed to every helper:
|
|
/// it runs the auto scale, the strip fixed-point loop, the grid build and the
|
|
/// region geometry, so re-deriving it per helper would rebuild all of that per
|
|
/// helper.
|
|
///
|
|
/// The handles a pointer is on are drawn highlighted -- a cursor marker is widened
|
|
/// into a tab, a region bound is drawn in the style's cursor colour at the width
|
|
/// `PlotLayout` sized its handle at -- and what is hot comes from
|
|
/// `Plot::hot_handles()`, which the input layer publishes. The renderer therefore
|
|
/// never sees a pointer, a button or a gesture: it draws what the plot says is
|
|
/// being manipulated, and only when a host drives the plot with a controller is
|
|
/// anything ever hot.
|
|
///
|
|
/// A legend the plot has is part of the frame: the panel and its swatches are
|
|
/// line geometry staged with everything else, the names are glyphs on the text
|
|
/// path, and both read the boxes `PlotLayout::legend` measured. It is the last
|
|
/// line batch, so the legend cannot be drawn under a trace, and it leaves no GL
|
|
/// state of its own -- the frame's blend and its neighbours' scissor already say
|
|
/// everything a fill and a hairline border need (see end_frame's residue list).
|
|
///
|
|
/// Regions are part of that same evidence: the fill is one instance of the line
|
|
/// pipeline (`fill_pixels()`, the primitive the legend panel uses), the bounds are
|
|
/// vertical strokes and the label is a staged glyph run, so a frame with regions
|
|
/// leaves behind exactly what a frame without them does.
|
|
void draw(Plot& plot);
|
|
/// Draws a whole `Figure`: every row in its own rect, then the figure's one shared
|
|
/// x axis.
|
|
|
|
/// Each row is drawn by the *same* path as `draw(Plot&)` -- the viewport and the
|
|
/// x-label policy come from `Figure::layout()`, which the figure hands out and
|
|
/// which lays every row out against its rect -- so a row is chrome and data drawn
|
|
/// exactly as a stand-alone plot is, and `draw(Plot&)` itself is unchanged for
|
|
/// single-plot hosts.
|
|
|
|
/// The figure's one shared x axis is the **bottom row's own**, so it is drawn by
|
|
/// that row's normal path: when the figure says labels belong on the bottom only
|
|
/// (the default), the rows above reserve no x chrome and the bottom row stages its
|
|
/// own labels and title once, inside its own strip -- the rect
|
|
/// `FigureLayout::x_axis_strip` reads back. With that switched off, every row keeps
|
|
/// its own chrome and each draws its own labels; there is then no shared axis.
|
|
|
|
/// Styling, the text backend and `ui_scale` are installed on **every** row before
|
|
/// the figure computes its layout, so the strips the rows measure and the strokes
|
|
/// this draws are one set of numbers, exactly as they are for one plot.
|
|
void draw(Figure& figure);
|
|
|
|
/// Ends the frame, draining `glGetError()` in a loop so a stale error cannot be
|
|
/// attributed to a later frame (see `last_frame_had_gl_error()`).
|
|
///
|
|
/// It also records each instance store's single fence for the frame, behind
|
|
/// every draw that read the ring, which is what the next `begin_frame()` waits
|
|
/// on (see `InstanceStore::mark_drawn`).
|
|
///
|
|
/// 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
|
|
/// viewport left at the frame's rect (the caller sets its own for the next pass).
|
|
/// The one piece of global state the frame *changed* is handed back rather than
|
|
/// left where the frame wanted it: the clear colour, which is
|
|
/// `style().background` only for the duration of the frame. GL keeps no history
|
|
/// for it, so the host's value is queried on the first `begin_frame()`; that
|
|
/// first value is what comes back here, so a colour the host changes after the
|
|
/// first frame is restored over by it.
|
|
///
|
|
/// `GL_UNPACK_ALIGNMENT` is deliberately not in that list any more: the atlas
|
|
/// upload needs it at 1 for single-channel rows, and `render::TextAtlas` saves
|
|
/// and restores it around every upload it performs, so no frame -- initial or
|
|
/// mid-run -- leaves it moved for the host's own uploads to inherit.
|
|
/// The framebuffer binding is never touched -- it is the caller's, and so is the
|
|
/// pixel-pack alignment if `dump_frame_ppm()` ran.
|
|
void end_frame() noexcept;
|
|
|
|
/// The frame's colours and metrics; `draw()` hands them to the plot as well --
|
|
/// style and `ui_scale` both -- so the layout it computes matches what is drawn.
|
|
/// Every metric here is a logical unit (`Style`'s unit rule): the host writes
|
|
/// `ui_scale` from the display it is on and this converts each metric with
|
|
/// `device()`. `line_width_px` is the width of the strokes framing the data (axis
|
|
/// lines, cursor markers); the grid carries a width *per class*
|
|
/// (`grid_major_width` / `grid_minor_width`) and every trace its own, and a grid
|
|
/// class the style switches off stages no geometry at all.
|
|
render::Style& style() noexcept { return style_; }
|
|
const render::Style& style() const noexcept { return style_; }
|
|
|
|
/// True when the last frame reported a GL failure: an error drained from
|
|
/// `glGetError()` in `end_frame()`, or a fence the driver refused, which the
|
|
/// store reports when the frame's fence is recorded or waited on (such a
|
|
/// frame has already fallen back to the baseline path). Errors are drained in a
|
|
/// loop, so a stale one cannot be attributed to a later frame.
|
|
bool last_frame_had_gl_error() const noexcept { return gl_error_; }
|
|
|
|
/// Statistics of the most recently drawn frame.
|
|
const FrameStats& stats() const noexcept { return stats_; }
|
|
|
|
bool ready() const noexcept { return ready_; }
|
|
/// True while both pipelines are uploading through the persistent-mapped ring:
|
|
/// `init()` selects it from the caps and the entry points (4.3+ with
|
|
/// ARB_buffer_storage, and `PLOT_FORCE_GL33_UPLOAD` unset), and a driver failure
|
|
/// later -- a refused map or fence -- leaves the store that hit it on the
|
|
/// baseline path, which this then says.
|
|
bool using_persistent_upload() const noexcept {
|
|
return line_vao_.persistent() && text_vao_.persistent();
|
|
}
|
|
/// The axis text size the renderer actually draws with, in **device** pixels: the
|
|
/// on-screen cell height of the backend it draws with, which is
|
|
/// `style().device(Style::axis_font_px)`'s own cell rather than the logical request
|
|
/// itself. `Style::axis_font_px` is an em size, so with an ascent-to-descent cell
|
|
/// it is this that a host reads back as the line height it got -- and it is the
|
|
/// backend's own `height()` scaled, so it always agrees with the metrics the plot
|
|
/// laid the labels out with.
|
|
f32 axis_font_px() const noexcept { return font_backend_.cell_px(); }
|
|
|
|
/// Writes the framebuffer's viewport region as a binary PPM (P6, 8 bits per
|
|
/// channel, rows top-down) so a headless render can be looked at without a
|
|
/// window. Reads the framebuffer that is bound now, which is the one the frame
|
|
/// was drawn into. Returns false, with `st` filled in, on a bad argument or an
|
|
/// unreadable pixel.
|
|
///
|
|
/// The one entry point here that is not `noexcept`: it sizes its readback hole
|
|
/// and its output row, so an allocation failure propagates `std::bad_alloc`
|
|
/// rather than terminating a process that could have carried on without a
|
|
/// screenshot. A dump is a diagnostic, and no `Status` code could say more than
|
|
/// the exception already does.
|
|
bool dump_frame_ppm(const char* path, Status* st = nullptr) const;
|
|
|
|
private:
|
|
/// The backend the plot layer sees, at the size the text pipeline draws it:
|
|
/// `measure`/`height`/`ascent` scale with the run, so a label can never be laid
|
|
/// out for one size and drawn at another.
|
|
///
|
|
/// It wraps a `TextBackend` and nothing more: the scale is
|
|
/// `axis_font_px / em_px()`, a plain ratio, so an anti-aliased face is drawn at
|
|
/// the size the style asked for rather than at a whole multiple of its cell,
|
|
/// and any backend -- the embedded font, a host's TrueType face -- is drawn the
|
|
/// same way. It forwards the whole interface: the metrics and the glyph
|
|
/// bearings and advances are scaled, while the artwork itself -- the atlas
|
|
/// bytes, its size and its revision -- is the backend's own and passes through
|
|
/// untouched.
|
|
class AxisFont final : public render::TextBackend {
|
|
public:
|
|
void reset(const render::TextBackend& font, f32 requested_px) noexcept;
|
|
f32 measure(const char* utf8) const override;
|
|
f32 height() const override;
|
|
f32 ascent() const override;
|
|
f32 em_px() const override;
|
|
/// The font's glyph at this wrapper's scale: the atlas rect is unchanged (it
|
|
/// is scale-free) and the bearing and advance are multiplied by it, so one
|
|
/// `Glyph` describes the glyph this backend draws. The result lives in the
|
|
/// wrapper until the next call, which is enough for a caller measuring or
|
|
/// laying out one glyph at a time.
|
|
const render::Glyph* glyph(u32 codepoint) const override;
|
|
/// The font's cell: the atlas is the font's own, and a cell's size is a
|
|
/// property of the atlas, not of the size text is drawn at, so this forwards
|
|
/// unchanged -- which is what makes it the authoritative value the staging
|
|
/// path declares and the draw is told.
|
|
Vec2 cell_uv() const override;
|
|
const u8* atlas_pixels() const override;
|
|
u32 atlas_width() const override;
|
|
u32 atlas_height() const override;
|
|
u32 revision() const override;
|
|
/// On-screen size of one glyph cell, in pixels.
|
|
f32 cell_px() const noexcept { return cell_px_; }
|
|
/// On-screen pixels per font pixel: the factor `measure()` and `glyph()` are
|
|
/// scaled by, and what `TextBatch::add_text` is handed as its size.
|
|
f32 scale() const noexcept { return scale_; }
|
|
|
|
private:
|
|
const render::TextBackend* font_ = nullptr;
|
|
f32 cell_px_ = 1.0f;
|
|
f32 scale_ = 1.0f;
|
|
/// `glyph()`'s scaled result, returned by pointer because the interface is
|
|
/// `const Glyph*` and the scale is the one thing this wrapper adds.
|
|
mutable render::Glyph scaled_{};
|
|
};
|
|
|
|
/// 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).
|
|
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;
|
|
/// The identity mapping for geometry already staged in viewport pixels.
|
|
Mapping pixel_mapping() const noexcept;
|
|
/// Shifts one framebuffer-space pixel position into viewport space.
|
|
Vec2 to_viewport(Vec2 px) const noexcept;
|
|
|
|
/// The single transfer point of the line pipeline: the staged `LineBatch`
|
|
/// payload (four floats per instance) goes to whichever strategy `init()`
|
|
/// selected. Callers of this helper never see which one is live.
|
|
void upload_vertices(std::span<const f32> interleaved) noexcept;
|
|
/// Same, for the text pipeline's four-float glyph instances.
|
|
void upload_text_vertices(std::span<const f32> interleaved) noexcept;
|
|
|
|
/// Uploads the staged line batch, maps it with (`scale`, `offset`), rasterises
|
|
/// it in one colour and empties the batch. A no-op for an empty batch, so the
|
|
/// caller can flush unconditionally.
|
|
void flush_lines(const Mapping& map, Color color, f32 width) 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;
|
|
|
|
/// Fills `rect` -- in framebuffer pixels, like `Rect` -- with `color`, in one
|
|
/// instance: a horizontal stroke whose width is the rect's height *is* the rect,
|
|
/// because a stroke's quad is its perpendicular width and the fragment shader
|
|
/// ramps on that perpendicular alone -- the ends are not capped (see
|
|
/// `fill_pixels()` in the implementation for why a per-column fill, the way the
|
|
/// 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);
|
|
/// 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.
|
|
f32 clipped_text_width(const char* utf8, f32 budget_px, Vec2 axis) const;
|
|
/// Stages as much of `utf8` as fits `budget_px` along `axis` at `pos_px` and
|
|
/// returns the length it staged. Titles are the runs that need this: a title
|
|
/// longer than the strip the layout reserved for it is cut there, rather than
|
|
/// being allowed to grow the strip (which would shrink the plot) or to spill
|
|
/// over the tick labels and the data (which is the defect the strips exist to
|
|
/// prevent).
|
|
///
|
|
/// `axis` is the run's own direction and a unit vector: (1, 0) for the
|
|
/// 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);
|
|
|
|
/// 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
|
|
/// backend publishes -- re-uploaded when its revision moved. False when a driver
|
|
/// refused the re-upload, which the frame reports (see `last_frame_had_gl_error()`)
|
|
/// and carries on from, drawing with the atlas it already had.
|
|
bool begin_frame_text() noexcept;
|
|
/// Hands `plot` everything the frame draws with: the renderer's text backend (a
|
|
/// host's is replaced here, named in a debug build, as it always was), the style,
|
|
/// and the `ui_scale` the plot converts that style's logical metrics by. Called
|
|
/// before a plot is laid out, which is why it is separate from the drawing.
|
|
void prepare_plot(Plot& plot);
|
|
/// Draws one plot's frame with the layout the caller already computed:
|
|
/// `draw(Plot&)` passes `plot.layout()`, and `draw(Figure&)` the layout
|
|
/// `Figure::layout()` computed for that row -- so a figure's rows are drawn by
|
|
/// exactly this path and neither entry point lays a plot out twice.
|
|
void draw_plot(Plot& plot, const PlotLayout& layout);
|
|
|
|
void draw_grid(const Plot& plot);
|
|
void draw_axis_lines(const Plot& plot);
|
|
/// The chrome helpers take the frame's layout: `Plot::layout()` recomputes the
|
|
/// ticks, the strips and the grid, so it is computed once, in `draw()`, and handed
|
|
/// down -- never re-derived per helper.
|
|
void draw_tick_labels(const Plot& plot, const PlotLayout& layout);
|
|
void draw_titles(const Plot& plot, const PlotLayout& layout);
|
|
/// The data helpers take it too; they read the axes rect and the transforms it
|
|
/// pins.
|
|
void draw_traces(const Plot& plot, const PlotLayout& layout);
|
|
void draw_cursors(const Plot& plot, const PlotLayout& layout);
|
|
/// The regions: the fill, one bound per edge, the optional label. Every rect comes
|
|
/// from `PlotLayout::regions` -- the same geometry the controller hit-tests -- so
|
|
/// a region is drawn where it can be grabbed, and neither side derives the other's
|
|
/// rects.
|
|
void draw_regions(const Plot& plot, const PlotLayout& layout);
|
|
/// One region bound: a vertical stroke of `width` down the tab's own column, in
|
|
/// `color`. The column is `handle_column()` of the tab -- the bound the layout
|
|
/// placed -- and a bound outside the axes' columns is skipped, exactly as a cursor
|
|
/// marker out of the visible window is.
|
|
void draw_region_edge(const Rect& tab, const Rect& axes, Color color, f32 width);
|
|
void draw_measures(const Plot& plot, const PlotLayout& layout);
|
|
void draw_annotations(const Plot& plot);
|
|
/// The legend: the panel and its swatches as line geometry, the names on the text
|
|
/// path. It reads the boxes `Plot::layout()` measured rather than deriving any of
|
|
/// them again, so the panel a test asserts and the panel this draws are the same
|
|
/// numbers.
|
|
void draw_legend(const Plot& plot, const PlotLayout& layout);
|
|
|
|
/// 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
|
|
/// `TextBackend` interface -- `self`-typed nowhere -- so a host's backend and
|
|
/// this one are interchangeable, and the only thing that has to happen on a swap
|
|
/// is the atlas re-upload `set_text_backend()` arranges.
|
|
render::EmbeddedMonoFont default_font_;
|
|
const render::TextBackend* font_ = &default_font_;
|
|
AxisFont font_backend_;
|
|
render::Style style_;
|
|
|
|
render::TextAtlas atlas_;
|
|
render::LineProgram line_program_;
|
|
render::TextProgram text_program_;
|
|
render::LineVertexArray line_vao_;
|
|
render::TextVertexArray text_vao_;
|
|
|
|
render::LineBatch lines_;
|
|
render::TextBatch glyphs_;
|
|
std::vector<f32> gather_; ///< scratch for one gathered 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
|
|
/// has no other way to recover it once the frame has set it) and handed back by
|
|
/// `end_frame()`.
|
|
Color host_clear_{};
|
|
bool host_clear_saved_ = 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;
|
|
u32 fb_h_ = 0;
|
|
bool ready_ = false;
|
|
bool gl_error_ = false;
|
|
FrameStats stats_{};
|
|
};
|
|
|
|
} // namespace plot
|