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
+447
-2
@@ -18,6 +18,7 @@
|
||||
#include <plot/render/renderer.hpp>
|
||||
#include <plot/input/controller.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cmath>
|
||||
#include <csignal>
|
||||
@@ -309,7 +310,6 @@ InkBox draw_text_run(plottest::HeadlessContext& ctx, plot::render::TextAtlas& at
|
||||
program.set_glyph_uv(cell);
|
||||
program.set_px_range({cell.x * texels.x * px, cell.y * texels.y * px});
|
||||
program.set_texture_size(texels);
|
||||
program.set_color(text);
|
||||
atlas.bind();
|
||||
vao.bind();
|
||||
plot::gl::glDrawArraysInstanced(plot::gl::GL_TRIANGLE_STRIP, 0, 4,
|
||||
@@ -2315,7 +2315,7 @@ PLOT_TEST(legend_swatch_and_label_are_drawn) {
|
||||
const plot::PlotLayout layout = p.layout();
|
||||
const plot::LegendLayout legend = layout.legend;
|
||||
PLOT_CHECK(legend.rows.size() == 2u);
|
||||
const plot::Legend& cfg = p.legend();
|
||||
const plot::Legend cfg = p.legend(); // copy: set_legend() below swaps the member
|
||||
const plot::u32 swatch_area = static_cast<plot::u32>(cfg.swatch_w) *
|
||||
static_cast<plot::u32>(cfg.swatch_h);
|
||||
|
||||
@@ -5364,3 +5364,448 @@ PLOT_TEST(dash_phase_is_stable_across_frames) {
|
||||
|
||||
ctx.destroy();
|
||||
}
|
||||
|
||||
// ------------------------------------------------- B1: relative-x staging (epoch axes)
|
||||
|
||||
/// The B1 defect: gathering and staging an absolute epoch x as f32 quantised it to
|
||||
/// a grid coarser than a pixel (f32 carries 24 bits -- about 128 s at 1.7e9), so an
|
||||
/// epoch-window trace collapsed onto a few columns while its zero-origin twin drew
|
||||
/// correctly. The fix stages `to_space(x_abs) - to_space(origin)` -- O(visible
|
||||
/// pixels) -- and folds the origin into the offset uniform in f64, so the two
|
||||
/// windows rasterise to the same geometry.
|
||||
///
|
||||
/// Both plots below draw the *same samples* with the same width; the only
|
||||
/// difference is the series' `x0()` and the axis window's offset. The assertion is
|
||||
/// the per-column trace-ink pattern: identical column sets, not merely similar
|
||||
/// counts, which a quantised draw could not satisfy.
|
||||
namespace {
|
||||
|
||||
/// Columns (every x of the axes) that hold at least one trace-coloured pixel.
|
||||
std::vector<bool> trace_column_mask(plottest::HeadlessContext& ctx, const plot::Rect& a) {
|
||||
const plot::u32 first_row = static_cast<plot::u32>(a.y);
|
||||
const plot::u32 rows = static_cast<plot::u32>(a.h);
|
||||
std::vector<bool> mask(a.w > 0.0f ? static_cast<std::size_t>(a.w) : 1u, false);
|
||||
for (plot::u32 x = 0; x < static_cast<plot::u32>(a.w); ++x) {
|
||||
const plot::u32 px_x = static_cast<plot::u32>(a.x) + x;
|
||||
for (plot::u32 yy = 0; yy < rows; ++yy) {
|
||||
plot::u8 px[4] = {};
|
||||
if (ctx.read_pixel(px_x, ctx.height - 1u - (first_row + yy), px) && trace_pixel(px)) {
|
||||
mask[x] = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return mask;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
PLOT_TEST(epoch_window_and_zero_origin_twin_rasterise_identically) {
|
||||
plottest::HeadlessContext ctx(kW, kH);
|
||||
plot::Status st;
|
||||
PLOT_CHECK(ctx.create(&st));
|
||||
PLOT_CHECK(plot::gl::load(nullptr));
|
||||
plot::Renderer r;
|
||||
PLOT_CHECK(r.init(&st));
|
||||
|
||||
// A sparse sine: one raw sample per column, so every stage of the pipeline is
|
||||
// exercised at pixel resolution -- the f32 defect shows on the x coordinates.
|
||||
std::vector<plot::f32> ys(640u);
|
||||
for (plot::u32 i = 0; i < ys.size(); ++i) {
|
||||
ys[i] = 0.8f * std::sin(0.05f * static_cast<plot::f32>(i));
|
||||
}
|
||||
const f64 epoch = 1'700'000'000.0;
|
||||
|
||||
plot::data::ArraySeries zero(ys, 0.0, 1.0);
|
||||
plot::data::ArraySeries shifted(ys, epoch, 1.0);
|
||||
|
||||
plot::Plot p0;
|
||||
p0.set_viewport({0, 0, static_cast<plot::f32>(kW), static_cast<plot::f32>(kH)});
|
||||
const plot::u32 ax0 = p0.add_x_axis({"t", {0.0, 639.0}, true, plot::AxisPlace::Bottom, false});
|
||||
const plot::u32 ay0 = p0.add_y_axis({"v", {-1.0, 1.0}, true, plot::AxisPlace::Left, false});
|
||||
p0.add_trace({&zero, plot::Color::hex(0x40C0FF), 1.5f, ax0, ay0});
|
||||
|
||||
plot::Plot p1;
|
||||
p1.set_viewport({0, 0, static_cast<plot::f32>(kW), static_cast<plot::f32>(kH)});
|
||||
const plot::u32 ax1 =
|
||||
p1.add_x_axis({"t", {epoch, epoch + 639.0}, true, plot::AxisPlace::Bottom, false});
|
||||
const plot::u32 ay1 = p1.add_y_axis({"v", {-1.0, 1.0}, true, plot::AxisPlace::Left, false});
|
||||
p1.add_trace({&shifted, plot::Color::hex(0x40C0FF), 1.5f, ax1, ay1});
|
||||
|
||||
const plot::Rect vp{0, 0, static_cast<plot::f32>(kW), static_cast<plot::f32>(kH)};
|
||||
r.begin_frame(kW, kH, vp);
|
||||
r.draw(p0);
|
||||
r.end_frame();
|
||||
const std::vector<bool> columns_zero = trace_column_mask(ctx, p0.layout().axes);
|
||||
|
||||
r.begin_frame(kW, kH, vp);
|
||||
r.draw(p1);
|
||||
r.end_frame();
|
||||
const std::vector<bool> columns_epoch = trace_column_mask(ctx, p1.layout().axes);
|
||||
|
||||
PLOT_CHECK(columns_zero == columns_epoch);
|
||||
// The premise: the twin really drew a trace across the axes (the comparison is
|
||||
// not vacuously true on two empty frames).
|
||||
PLOT_CHECK(std::count(columns_zero.begin(), columns_zero.end(), true) >
|
||||
static_cast<std::size_t>(p0.layout().axes.w) * 9u / 10u);
|
||||
PLOT_CHECK(!r.last_frame_had_gl_error());
|
||||
ctx.destroy();
|
||||
}
|
||||
|
||||
// ------------------------------------------------- B6: two text batches
|
||||
|
||||
/// The B6 defect: one text batch, flushed after everything, put the legend's
|
||||
/// names *over* the crosshair's readout panel -- text above the backplate that
|
||||
/// exists to occlude it. The under batch (legend names included) is now flushed
|
||||
/// immediately before the panel, so a legend name the panel covers is occluded.
|
||||
///
|
||||
/// The scene: a legend in the bottom-left corner with three wide names, and a
|
||||
/// crosshair parked just inside that corner, whose readout panel flips up and
|
||||
/// right -- on top of the legend rows. The assertion reads the legend name boxes
|
||||
/// *minus* every rect the readout legitimately paints (its own label and value
|
||||
/// runs, and the crosshair's strokes): what is left of a covered name box must
|
||||
/// hold no text ink with the readout on, and held ink without it.
|
||||
namespace {
|
||||
|
||||
/// Text ink inside `area`, skipping every pixel inside any of `exclude`.
|
||||
plot::u32 text_ink_outside(const std::vector<plot::u8>& frame, const plot::Rect& area,
|
||||
const std::vector<plot::Rect>& exclude) {
|
||||
plot::u32 hit = 0;
|
||||
const plot::u32 x0 = static_cast<plot::u32>(std::max(0.0f, area.x));
|
||||
const plot::u32 y0 = static_cast<plot::u32>(std::max(0.0f, area.y));
|
||||
const plot::u32 x1 = static_cast<plot::u32>(std::min(area.right(), kFrameRect.right()));
|
||||
const plot::u32 y1 = static_cast<plot::u32>(std::min(area.bottom(), kFrameRect.bottom()));
|
||||
for (plot::u32 y = y0; y < y1; ++y) {
|
||||
for (plot::u32 x = x0; x < x1; ++x) {
|
||||
bool skipped = false;
|
||||
for (const plot::Rect& e : exclude) {
|
||||
if (e.contains(static_cast<plot::f32>(x) + 0.5f, static_cast<plot::f32>(y) + 0.5f)) {
|
||||
skipped = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (skipped) continue;
|
||||
if (text_pixel(&frame[(plot::usize(y) * kW + x) * 4u])) ++hit;
|
||||
}
|
||||
}
|
||||
return hit;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
PLOT_TEST(readout_panel_occludes_the_legend_names_it_covers) {
|
||||
plottest::HeadlessContext ctx(kW, kH);
|
||||
plot::Status st;
|
||||
PLOT_CHECK(ctx.create(&st));
|
||||
PLOT_CHECK(plot::gl::load(nullptr));
|
||||
plot::Renderer r;
|
||||
PLOT_CHECK(r.init(&st));
|
||||
|
||||
plot::Plot p;
|
||||
const plot::Rect vp{0, 0, static_cast<plot::f32>(kW), static_cast<plot::f32>(kH)};
|
||||
p.set_viewport(vp);
|
||||
const plot::u32 ax = p.add_x_axis({"t", {0.0, 639.0}, true, plot::AxisPlace::Bottom, false});
|
||||
const plot::u32 ay = p.add_y_axis({"v", {0.0, 1.0}, true, plot::AxisPlace::Left, false});
|
||||
plot::Legend legend;
|
||||
legend.place = plot::LegendPlace::BottomLeft;
|
||||
p.set_legend(legend);
|
||||
// Flat traces at distinct heights, wide names: three tall legend rows.
|
||||
plot::data::ArraySeries a(std::vector<plot::f32>(640u, 0.7f), 0.0, 1.0);
|
||||
plot::data::ArraySeries b(std::vector<plot::f32>(640u, 0.5f), 0.0, 1.0);
|
||||
plot::data::ArraySeries c(std::vector<plot::f32>(640u, 0.3f), 0.0, 1.0);
|
||||
p.add_trace({&a, plot::Color::hex(0x40C0FF), 1.5f, ax, ay, "channel one"});
|
||||
p.add_trace({&b, plot::Color::hex(0xFF5090), 1.5f, ax, ay, "channel two long name"});
|
||||
p.add_trace({&c, plot::Color::hex(0x50D050), 1.5f, ax, ay, "channel three"});
|
||||
|
||||
// Warm-up frame (installs the text backend), then the legend-only frame.
|
||||
r.begin_frame(kW, kH, vp);
|
||||
r.draw(p);
|
||||
r.end_frame();
|
||||
r.begin_frame(kW, kH, vp);
|
||||
r.draw(p);
|
||||
r.end_frame();
|
||||
const std::vector<plot::u8> legend_only = read_frame(ctx);
|
||||
const plot::PlotLayout layout = p.layout();
|
||||
PLOT_CHECK(layout.legend.rows.size() == 3u);
|
||||
PLOT_CHECK(layout.legend.panel.w > 0.0f);
|
||||
|
||||
// The crosshair just inside the bottom-left corner: its panel flips up and
|
||||
// right, over the legend rows.
|
||||
const plot::Transform tx = p.x_transform(ax);
|
||||
const plot::Transform ty = p.y_transform(ay);
|
||||
p.set_crosshair(tx.to_data_x(layout.axes.x + 30.0f),
|
||||
ty.to_data_y(layout.axes.bottom() - 30.0f), true);
|
||||
r.begin_frame(kW, kH, vp);
|
||||
r.draw(p);
|
||||
r.end_frame();
|
||||
const std::vector<plot::u8> with_readout = read_frame(ctx);
|
||||
|
||||
// Fresh layout: the crosshair's own geometry only exists now that it is placed
|
||||
// (the copy above was taken before `set_crosshair`).
|
||||
const plot::CrosshairLayout crosshair = p.layout().crosshair;
|
||||
PLOT_CHECK(crosshair.active);
|
||||
PLOT_CHECK(crosshair.panel.w > 0.0f);
|
||||
// The exclusions: the readout's own runs and the crosshair's strokes.
|
||||
std::vector<plot::Rect> exclude;
|
||||
for (const plot::CrosshairRow& row : crosshair.rows) {
|
||||
if (row.listed) {
|
||||
exclude.push_back(row.label_box);
|
||||
exclude.push_back(row.value_box);
|
||||
exclude.push_back(row.swatch); // the row's own swatch fill
|
||||
}
|
||||
}
|
||||
exclude.push_back(crosshair.vertical);
|
||||
exclude.push_back(crosshair.horizontal);
|
||||
|
||||
// The premise: the panel really does cover at least one legend name box.
|
||||
bool covers = false;
|
||||
for (const plot::LegendRow& row : layout.legend.rows) {
|
||||
if (row.ellipsis) continue;
|
||||
if (crosshair.panel.x < row.name.right() && row.name.x < crosshair.panel.right() &&
|
||||
crosshair.panel.y < row.name.bottom() && row.name.y < crosshair.panel.bottom()) {
|
||||
covers = true;
|
||||
}
|
||||
}
|
||||
PLOT_CHECK(covers);
|
||||
|
||||
// Each legend name box: ink without the readout; in the part the panel covers,
|
||||
// none of it left (outside the readout's own runs) once the panel is over it.
|
||||
// The uncovered remainder of a partially covered box keeps its ink -- that is
|
||||
// the panel doing its job and nothing more.
|
||||
bool checked = false;
|
||||
for (const plot::LegendRow& row : layout.legend.rows) {
|
||||
const bool overlaps = crosshair.panel.x < row.name.right() &&
|
||||
row.name.x < crosshair.panel.right() &&
|
||||
crosshair.panel.y < row.name.bottom() &&
|
||||
row.name.y < crosshair.panel.bottom();
|
||||
if (row.ellipsis || !overlaps) continue;
|
||||
// Inset past the panel's own border ring: the border is a hairline of the
|
||||
// bright crosshair colour (see draw_readout_panel), which the ink predicate
|
||||
// cannot tell from text -- it is the panel's chrome, not legend ink.
|
||||
const plot::Rect inner{crosshair.panel.x + 2.0f, crosshair.panel.y + 2.0f,
|
||||
crosshair.panel.w - 4.0f, crosshair.panel.h - 4.0f};
|
||||
const plot::Rect covered{
|
||||
std::max(inner.x, row.name.x), std::max(inner.y, row.name.y),
|
||||
std::min(inner.right(), row.name.right()) - std::max(inner.x, row.name.x),
|
||||
std::min(inner.bottom(), row.name.bottom()) - std::max(inner.y, row.name.y)};
|
||||
PLOT_CHECK(covered.w > 0.0f && covered.h > 0.0f);
|
||||
PLOT_CHECK(text_ink_outside(legend_only, covered, {}) > 0u);
|
||||
PLOT_CHECK(text_ink_outside(with_readout, covered, exclude) == 0u);
|
||||
checked = true;
|
||||
}
|
||||
PLOT_CHECK(checked);
|
||||
PLOT_CHECK(!r.last_frame_had_gl_error());
|
||||
ctx.destroy();
|
||||
}
|
||||
|
||||
// ------------------------------------------------- markers
|
||||
|
||||
/// Markers are staged as line segments through the trace's own batch: a
|
||||
/// marker'd trace lights pixels a plain line does not (the shapes extend past
|
||||
/// the stroke), and the stride is literal -- `marker_every = 3` stages exactly a
|
||||
/// third of the markers of `marker_every = 1` over the same window.
|
||||
PLOT_TEST(markers_light_more_than_the_line_and_stride_is_literal) {
|
||||
plottest::HeadlessContext ctx(kW, kH);
|
||||
plot::Status st;
|
||||
PLOT_CHECK(ctx.create(&st));
|
||||
PLOT_CHECK(plot::gl::load(nullptr));
|
||||
plot::Renderer r;
|
||||
PLOT_CHECK(r.init(&st));
|
||||
|
||||
// 32 sparse points: the auto stride would be ~1 marker per point anyway, so
|
||||
// `marker_every = 1` vs `3` differ by construction.
|
||||
std::vector<plot::f32> ys(32u);
|
||||
for (plot::u32 i = 0; i < ys.size(); ++i) {
|
||||
ys[i] = 0.5f + 0.4f * std::sin(0.9f * static_cast<plot::f32>(i));
|
||||
}
|
||||
plot::data::ArraySeries s(ys, 0.0, 20.0);
|
||||
|
||||
plot::Plot plain_p;
|
||||
plain_p.set_viewport({0, 0, static_cast<plot::f32>(kW), static_cast<plot::f32>(kH)});
|
||||
const plot::u32 ax = plain_p.add_x_axis({"t", {0.0, 620.0}, true, plot::AxisPlace::Bottom, false});
|
||||
const plot::u32 ay = plain_p.add_y_axis({"v", {0.0, 1.0}, true, plot::AxisPlace::Left, false});
|
||||
plot::Plot marked_p = plain_p;
|
||||
plot::Plot every3_p = plain_p;
|
||||
marked_p.add_trace({&s, plot::Color::hex(0xFF5090), 1.5f, ax, ay, "", plot::TraceMode::Envelope,
|
||||
0.0f, 0.0f, plot::Marker::Circle, 4.0f, 1u});
|
||||
every3_p.add_trace({&s, plot::Color::hex(0xFF5090), 1.5f, ax, ay, "", plot::TraceMode::Envelope,
|
||||
0.0f, 0.0f, plot::Marker::Circle, 4.0f, 3u});
|
||||
plain_p.add_trace({&s, plot::Color::hex(0xFF5090), 1.5f, ax, ay});
|
||||
|
||||
const plot::Rect vp{0, 0, static_cast<plot::f32>(kW), static_cast<plot::f32>(kH)};
|
||||
r.begin_frame(kW, kH, vp);
|
||||
r.draw(plain_p);
|
||||
r.end_frame();
|
||||
const plot::u32 plain_ink = predicate_pixels(read_frame(ctx), kW, plain_p.layout().axes,
|
||||
magenta_pixel);
|
||||
|
||||
r.begin_frame(kW, kH, vp);
|
||||
r.draw(marked_p);
|
||||
r.end_frame();
|
||||
const plot::u32 marked_ink = predicate_pixels(read_frame(ctx), kW, marked_p.layout().axes,
|
||||
magenta_pixel);
|
||||
const plot::u32 marked_count = r.stats().staged_markers;
|
||||
|
||||
r.begin_frame(kW, kH, vp);
|
||||
r.draw(every3_p);
|
||||
r.end_frame();
|
||||
const plot::u32 every3_count = r.stats().staged_markers;
|
||||
|
||||
// The premise: the line drew something; the markers drew strictly more.
|
||||
PLOT_CHECK(plain_ink > 0u);
|
||||
PLOT_CHECK(marked_ink > plain_ink);
|
||||
// The stride is literal: a third of the markers, ±1 for the window's edge
|
||||
// sample count.
|
||||
PLOT_CHECK(marked_count >= 30u);
|
||||
PLOT_CHECK(every3_count * 3u + 2u >= marked_count);
|
||||
PLOT_CHECK(every3_count * 3u <= marked_count + 2u);
|
||||
PLOT_CHECK(!r.last_frame_had_gl_error());
|
||||
ctx.destroy();
|
||||
}
|
||||
|
||||
// ------------------------------------------------- text colour
|
||||
|
||||
/// The readout's VALUE column draws in the row trace's own colour (resolved from
|
||||
/// the plot at draw time), the labels in the axis text colour: per-instance
|
||||
/// `a_colour` instead of one text tint per draw.
|
||||
PLOT_TEST(readout_value_draws_in_the_row_trace_colour) {
|
||||
plottest::HeadlessContext ctx(kW, kH);
|
||||
plot::Status st;
|
||||
PLOT_CHECK(ctx.create(&st));
|
||||
PLOT_CHECK(plot::gl::load(nullptr));
|
||||
plot::Renderer r;
|
||||
PLOT_CHECK(r.init(&st));
|
||||
|
||||
plot::Plot p;
|
||||
const plot::Rect vp{0, 0, static_cast<plot::f32>(kW), static_cast<plot::f32>(kH)};
|
||||
p.set_viewport(vp);
|
||||
const plot::u32 ax = p.add_x_axis({"t", {0.0, 639.0}, true, plot::AxisPlace::Bottom, false});
|
||||
const plot::u32 ay = p.add_y_axis({"v", {0.0, 1.0}, true, plot::AxisPlace::Left, false});
|
||||
plot::data::ArraySeries s(std::vector<plot::f32>(640u, 0.5f), 0.0, 1.0);
|
||||
const auto magenta = plot::Color::hex(0xFF5090);
|
||||
p.add_trace({&s, magenta, 1.5f, ax, ay, "row"});
|
||||
p.set_crosshair(300.0, 0.5, true);
|
||||
p.set_crosshair_readout(true);
|
||||
|
||||
r.begin_frame(kW, kH, vp);
|
||||
r.draw(p);
|
||||
r.end_frame();
|
||||
const std::vector<plot::u8> frame = read_frame(ctx);
|
||||
const plot::CrosshairLayout& crosshair = p.layout().crosshair;
|
||||
PLOT_CHECK(crosshair.rows.size() == 1u);
|
||||
PLOT_CHECK(crosshair.rows[0].listed);
|
||||
PLOT_CHECK(crosshair.rows[0].value_box.w > 0.0f);
|
||||
|
||||
// The value box holds pixels the row trace's colour explains (magenta
|
||||
// dominance at any blend over the dark panel) -- the old single-tint pipeline
|
||||
// drew every glyph in the axis text grey, so this is the defect's own pixel.
|
||||
plot::u32 coloured = 0;
|
||||
const plot::Rect box = crosshair.rows[0].value_box;
|
||||
for (plot::u32 y = static_cast<plot::u32>(box.y); y < static_cast<plot::u32>(box.bottom()); ++y) {
|
||||
for (plot::u32 x = static_cast<plot::u32>(box.x); x < static_cast<plot::u32>(box.right());
|
||||
++x) {
|
||||
plot::u8 px[4] = {};
|
||||
PLOT_CHECK(ctx.read_pixel(x, ctx.height - 1u - y, px));
|
||||
if (magenta_pixel(px)) ++coloured;
|
||||
}
|
||||
}
|
||||
PLOT_CHECK(coloured > 0u);
|
||||
// The label box, same frame, is the axis text colour, not the trace's.
|
||||
plot::u32 label_grey = 0;
|
||||
const plot::Rect labels = crosshair.rows[0].label_box;
|
||||
for (plot::u32 y = static_cast<plot::u32>(labels.y); y < static_cast<plot::u32>(labels.bottom());
|
||||
++y) {
|
||||
for (plot::u32 x = static_cast<plot::u32>(labels.x); x < static_cast<plot::u32>(labels.right());
|
||||
++x) {
|
||||
plot::u8 px[4] = {};
|
||||
PLOT_CHECK(ctx.read_pixel(x, ctx.height - 1u - y, px));
|
||||
if (text_pixel(px)) ++label_grey;
|
||||
}
|
||||
}
|
||||
PLOT_CHECK(label_grey > 0u);
|
||||
PLOT_CHECK(!r.last_frame_had_gl_error());
|
||||
ctx.destroy();
|
||||
}
|
||||
|
||||
// ------------------------------------------------- R6: GL state residue and shutdown
|
||||
|
||||
/// `begin_frame()` saves the host's blend function and enable bit; `end_frame()`
|
||||
/// hands both back -- a host that blends its own way before the frame must not
|
||||
/// inherit the frame's `SRC_ALPHA / ONE_MINUS_SRC_ALPHA`.
|
||||
PLOT_TEST(a_frame_hands_the_blend_state_back) {
|
||||
plottest::HeadlessContext ctx(kW, kH);
|
||||
plot::Status st;
|
||||
PLOT_CHECK(ctx.create(&st));
|
||||
PLOT_CHECK(plot::gl::load(nullptr));
|
||||
plot::Renderer r;
|
||||
PLOT_CHECK(r.init(&st));
|
||||
|
||||
plot::Plot p;
|
||||
p.set_viewport({0, 0, static_cast<plot::f32>(kW), static_cast<plot::f32>(kH)});
|
||||
const plot::u32 ax = p.add_x_axis({"t", {0.0, 1.0}, true, plot::AxisPlace::Bottom, false});
|
||||
const plot::u32 ay = p.add_y_axis({"v", {0.0, 1.0}, true, plot::AxisPlace::Left, false});
|
||||
plot::data::ArraySeries s(std::vector<plot::f32>(16u, 0.5f), 0.0, 1.0);
|
||||
p.add_trace({&s, plot::Color::hex(0x40C0FF), 1.5f, ax, ay});
|
||||
|
||||
// The host's own blend: function ONE/ZERO, enabled.
|
||||
plot::gl::glEnable(plot::gl::GL_BLEND);
|
||||
plot::gl::glBlendFunc(plot::gl::GL_ONE, plot::gl::GL_ZERO);
|
||||
const plot::Rect vp{0, 0, static_cast<plot::f32>(kW), static_cast<plot::f32>(kH)};
|
||||
r.begin_frame(kW, kH, vp);
|
||||
r.draw(p);
|
||||
r.end_frame();
|
||||
plot::gl::GLint src = -1;
|
||||
plot::gl::GLint dst = -1;
|
||||
plot::gl::glGetIntegerv(plot::gl::GL_BLEND_SRC_RGB, &src);
|
||||
plot::gl::glGetIntegerv(plot::gl::GL_BLEND_DST_RGB, &dst);
|
||||
PLOT_CHECK(src == static_cast<plot::gl::GLint>(plot::gl::GL_ONE));
|
||||
PLOT_CHECK(dst == static_cast<plot::gl::GLint>(plot::gl::GL_ZERO));
|
||||
PLOT_CHECK(plot::gl::glIsEnabled(plot::gl::GL_BLEND) != plot::gl::GL_FALSE);
|
||||
|
||||
// And with blending off, it comes back off.
|
||||
plot::gl::glDisable(plot::gl::GL_BLEND);
|
||||
r.begin_frame(kW, kH, vp);
|
||||
r.draw(p);
|
||||
r.end_frame();
|
||||
PLOT_CHECK(plot::gl::glIsEnabled(plot::gl::GL_BLEND) == plot::gl::GL_FALSE);
|
||||
PLOT_CHECK(!r.last_frame_had_gl_error());
|
||||
ctx.destroy();
|
||||
}
|
||||
|
||||
/// `release_gl()` destroys every GL handle while the context is current: after
|
||||
/// it the renderer is uninitialised, the driver holds no plot objects (a
|
||||
/// re-init and frame still draw, which is the observable half), and the same
|
||||
/// renderer object can be brought back.
|
||||
PLOT_TEST(release_gl_shuts_down_and_the_renderer_can_reinit) {
|
||||
plottest::HeadlessContext ctx(kW, kH);
|
||||
plot::Status st;
|
||||
PLOT_CHECK(ctx.create(&st));
|
||||
PLOT_CHECK(plot::gl::load(nullptr));
|
||||
plot::Renderer r;
|
||||
PLOT_CHECK(r.init(&st));
|
||||
|
||||
plot::Plot p;
|
||||
p.set_viewport({0, 0, static_cast<plot::f32>(kW), static_cast<plot::f32>(kH)});
|
||||
const plot::u32 ax = p.add_x_axis({"t", {0.0, 1.0}, true, plot::AxisPlace::Bottom, false});
|
||||
const plot::u32 ay = p.add_y_axis({"v", {0.0, 1.0}, true, plot::AxisPlace::Left, false});
|
||||
plot::data::ArraySeries s(std::vector<plot::f32>(16u, 0.5f), 0.0, 1.0);
|
||||
p.add_trace({&s, plot::Color::hex(0x40C0FF), 1.5f, ax, ay});
|
||||
const plot::Rect vp{0, 0, static_cast<plot::f32>(kW), static_cast<plot::f32>(kH)};
|
||||
r.begin_frame(kW, kH, vp);
|
||||
r.draw(p);
|
||||
r.end_frame();
|
||||
PLOT_CHECK(!r.last_frame_had_gl_error());
|
||||
|
||||
r.release_gl();
|
||||
PLOT_CHECK(!r.ready());
|
||||
PLOT_CHECK(plot::gl::glGetError() == plot::gl::GL_NO_ERROR);
|
||||
|
||||
// Re-init on the same object draws again -- the handles were released, not
|
||||
// half-destroyed.
|
||||
PLOT_CHECK(r.init(&st));
|
||||
r.begin_frame(kW, kH, vp);
|
||||
r.draw(p);
|
||||
r.end_frame();
|
||||
PLOT_CHECK(!r.last_frame_had_gl_error());
|
||||
ctx.destroy();
|
||||
}
|
||||
Reference in new issue
Block a user