Four review findings on the Task 7 renderer: - InstanceStore::epoch() is derived from gl::Buffer::generation() -- advanced by every allocation, deletion and move -- plus the slot the instances were written to, so the orphan fallback of grow_persistent() can no longer leave the vertex array's attribute pointers bound to a deleted buffer: the epoch reports the new identity and upload() re-points them before the draw. - PlotLayout reserves y_title_area/x_title_area outside axes (sized from the text metrics) and draw_titles() draws in them, so a title can no longer land on the data, whatever the envelope does. - draw() computes plot.layout() once and hands it to the helpers, instead of six times per frame, each of which rebuilt the ticks and the grid. - A refused fence is no longer dropped: mark_drawn() falls back to the orphan path and reports the failure rather than rotating through an unsynchronised slot. New smoke cases force both refusals through the GL entry points (glMapBufferRange, glFenceSync) and assert the pixels plus the live transfer mode; the title case asserts the title colour is absent from the bands inside axes and present in both strips. All three fail against the pre-fix sources.
244 lines
12 KiB
C++
244 lines
12 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/gl/handle.hpp>
|
|
#include <plot/math/vec2.hpp>
|
|
#include <plot/plot/plot.hpp>
|
|
#include <plot/render/bitmap_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/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, builds the glyph 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;
|
|
|
|
/// 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.
|
|
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: `draw()` copies `style()` into the
|
|
/// plot, 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.
|
|
/// The plot also borrows this renderer's font (when it has no backend of its
|
|
/// own), so the renderer must outlive the plot's use of it.
|
|
///
|
|
/// `Plot::layout()` is computed exactly once, here, and handed to every helper:
|
|
/// it runs the auto scale, the strip fixed-point loop and the grid build, so
|
|
/// re-deriving it per helper would rebuild the ticks and the grid per helper.
|
|
void draw(Plot& plot);
|
|
|
|
/// 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()`).
|
|
///
|
|
/// 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 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, so
|
|
/// the layout it computes matches what is drawn. `line_width_px` is the width of
|
|
/// the strokes framing the data (axis lines, cursor markers) -- grid lines stay
|
|
/// hairlines and every trace carries its own width.
|
|
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 draw that should have been guarded is issued (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: `style().axis_font_px`
|
|
/// rounded to the nearest whole multiple of `render::BitmapFont::kGlyphPx`,
|
|
/// because a 1-bit glyph only scales cleanly by whole factors.
|
|
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.
|
|
bool dump_frame_ppm(const char* path, Status* st = nullptr) const noexcept;
|
|
|
|
private:
|
|
/// The bitmap font as the plot layer sees it, at the size the text pipeline
|
|
/// draws it: `measure`/`height`/`ascent` scale with the glyph cell, so a label
|
|
/// can never be laid out for one size and drawn at another. The 8x8 font scales
|
|
/// by whole multiples of its cell only -- anything else lands a 1-bit glyph's
|
|
/// strokes unevenly -- so `reset()` picks the multiple nearest the requested
|
|
/// size.
|
|
class AxisFont final : public render::TextBackend {
|
|
public:
|
|
void reset(const render::BitmapFont& font, f32 requested_px) noexcept;
|
|
f32 measure(const char* utf8) const override;
|
|
f32 height() const override;
|
|
f32 ascent() const override;
|
|
/// On-screen size of one glyph cell, in pixels.
|
|
f32 cell_px() const noexcept { return cell_px_; }
|
|
|
|
private:
|
|
const render::BitmapFont* font_ = nullptr;
|
|
f32 cell_px_ = 1.0f;
|
|
f32 scale_ = 1.0f;
|
|
};
|
|
|
|
/// 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;
|
|
|
|
/// 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);
|
|
|
|
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 belongs to `draw()` alone.
|
|
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);
|
|
void draw_measures(const Plot& plot, const PlotLayout& layout);
|
|
void draw_annotations(const Plot& plot);
|
|
|
|
render::BitmapFont font_;
|
|
AxisFont font_backend_;
|
|
render::Style style_;
|
|
|
|
gl::Texture 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
|
|
|
|
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
|