// The reference application: the whole library running as a real program, fed by // a 1 MS/s producer thread through the GLFW adapter. // // What it demonstrates, and what the other three adapters copy: // // - a `StreamSeries` at the retention limit (1 << 20 samples, 1.048576 s at this // rate) pushed from a background thread and acquired exactly once per frame on // the render thread; // - a signal whose decimation correctness is visible: a 437 Hz sine plus a small // noise term plus a narrow spike every 100 ms. A bucket that kept only its // first sample would drop the spike; a bucket that keeps its extrema cannot; // - a plot the user can drive: grid, both axes titled and labelled (the time // axis with `format_time` clock labels, the value axis with SI ones), one // trace, two cursors and the measurement readout the plot layer draws itself // (shaded band plus a `dx/dy/rms/mean` line) -- no drawing code here; // - the headless smoke mode the adapter implements once // (PLOT_DEMO_EXIT_AFTER_MS / PLOT_DEMO_SCREENSHOT), so CI can run this under // `xvfb-run` and check the frame it wrote; // - `PLOT_DEMO_FOLLOW=1`: the same run with the time axis armed for live following // (`XFollow::Live`), driving three wheel notches and a pan through the host's own // controller and printing, at each step, the range's width, its distance behind // the newest sample and the follow state -- the interaction spec §12 is about, // measured on a real stream rather than in a unit test. It is a *check*, not a // readout: the run exits non-zero if a measured property fails to hold, or if the // sequence did not get far enough to measure all four. // // The producer is joined before the stream it writes into is destroyed, and // before the host closes the GL context (main's destruction order does that). #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { using plot::f32; using plot::f64; using plot::u32; using plot::u64; /// One sample per microsecond: 1 MS/s. constexpr f64 kSampleRate = 1.0e6; constexpr f64 kDx = 1.0 / kSampleRate; /// The retained window: 1 << 20 samples = 1.048576 s. A power of two, so the /// ring never has to round it up. constexpr u64 kCapacity = 1u << 20; /// Producer wakeup: about a millisecond of samples per push batch. constexpr u64 kBatch = 1u << 10; /// Cap on one push batch, so a process that was stopped and resumed catches up /// over several iterations instead of allocating a batch for the whole gap. constexpr u64 kMaxBatch = 1u << 16; /// The signal: a few hundred Hz sine, a small noise term, a narrow spike. constexpr f64 kSineHz = 437.0; constexpr f64 kNoiseAmp = 0.02; constexpr f64 kSpikePeriodS = 0.1; constexpr f64 kSpikeWidthS = 2.0e-6; constexpr f64 kSpikeAmp = 3.0; // The trace colour the render smoke test's blue-dominance predicate expects, so // the same predicate reads this demo's screenshot. constexpr plot::Color kTraceColor = plot::Color::hex(0x40C0FF); constexpr plot::Color kCursorAColor = plot::Color::hex(0xFFB020); constexpr plot::Color kCursorBColor = plot::Color::hex(0x35D0A8); /// The region the direct-manipulation script drags: a fill and an edge no other element /// of the frame produces -- the trace is blue, the chrome grey, cursor A the accent and /// cursor B a teal -- so a pixel count of one of them can only be about the region. The /// two are the render smoke test's own colours, so its predicates read this screenshot /// too. constexpr plot::Color kRegionFill = plot::Color::hex(0x33FF00, 0.35f); constexpr plot::Color kRegionEdge = plot::Color::hex(0xFF00FF); /// The pan the region script takes, in framebuffer pixels, and the window it freezes the /// view on, in seconds: 20 ms of this 1 MS/s stream is ~20 000 samples, wider than the /// axes, so the trace is drawn from the pyramid and the region's interval is hundreds of /// pixels across. constexpr plot::u32 kRegionPanPx = 100u; constexpr plot::f64 kRegionWindowS = 0.020; /// y(t) for sample index `i`: sine + deterministic noise + a two-sample spike. /// /// The noise hash takes the low 32 bits of the index, so its pattern repeats /// every ~4295 s at 1 MS/s -- far beyond any run of this demo, and it keeps the /// sample path allocation-free and branch-light, unlike a PRNG. f32 sample_at(u64 i) noexcept { const f64 t = static_cast(i) * kDx; f64 y = std::sin(2.0 * std::numbers::pi * kSineHz * t); u32 h = static_cast(i) * 2654435761u; h ^= h >> 15; h *= 2246822519u; h ^= h >> 13; h *= 3266489917u; h ^= h >> 16; y += kNoiseAmp * (static_cast(h & 0xFFFFFFu) / 8388608.0 - 1.0); const f64 phase = std::fmod(t, kSpikePeriodS); if (phase < kSpikeWidthS) { y += kSpikeAmp * (1.0 - phase / kSpikeWidthS); } return static_cast(y); } /// Feeds a `StreamSeries` from its own thread at 1 MS/s on average, paced by /// absolute deadlines off a steady clock: every wakeup produces every sample /// whose time has already come, so a late wakeup is caught up instead of pushing /// the phase back, and the achieved rate cannot drift the way a fixed /// `sleep_for()` per batch does. class Producer { public: explicit Producer(plot::data::StreamSeries& series) : series_(&series), thread_([this] { run(); }) {} ~Producer() { stop(); } Producer(const Producer&) = delete; Producer& operator=(const Producer&) = delete; /// Sets the stop flag and joins the thread. Idempotent; the destructor calls /// it, so the thread is never detached and never outlives the stream. void stop() noexcept { stop_.store(true, std::memory_order_relaxed); if (thread_.joinable()) thread_.join(); } /// Samples pushed so far. Written by the producer, read by the render thread. u64 produced() const noexcept { return produced_.load(std::memory_order_relaxed); } private: void run() { using clock = std::chrono::steady_clock; const auto start = clock::now(); u64 next = 0; while (!stop_.load(std::memory_order_relaxed)) { const f64 elapsed = std::chrono::duration(clock::now() - start).count(); const u64 due = static_cast(elapsed / kDx) + 1; // samples whose time has come if (due > next) { u64 n = due - next; if (n > kMaxBatch) n = kMaxBatch; batch_.resize(static_cast(n)); for (u64 k = 0; k < n; ++k) { batch_[static_cast(k)] = sample_at(next + k); } series_->push(batch_.data(), n); next += n; produced_.store(next, std::memory_order_relaxed); } // Sleep until one batch past the last produced sample: an absolute // deadline, so the phase is fixed at `start` and never accumulates. const f64 wake_s = static_cast(next + kBatch) * kDx; std::this_thread::sleep_until( start + std::chrono::duration_cast(std::chrono::duration(wake_s))); } } plot::data::StreamSeries* series_; std::vector batch_; std::atomic produced_{0}; std::atomic stop_{false}; std::thread thread_; ///< declared last: the lambda runs once everything is up }; /// The live readout: the achieved push rate, the frame time and the frame count. struct Meter { f64 rate = 0.0; ///< samples/s, refreshed a few times a second f64 frame_ms = 0.0; ///< exponential moving average of the frame time f64 elapsed = 0.0; ///< seconds of frame callback time f64 last_report = 0.0; u64 last_produced = 0; u64 frames = 0; }; /// The scripted live-follow sequence `PLOT_DEMO_FOLLOW=1` runs. /// /// The follow arithmetic is pinned by the library's tests; "the plot keeps showing /// the newest sample while the user zooms and pans it" is an *interaction*, and this /// is the program that has one -- a real 1 MS/s producer, the real host, the real /// controller. So the sequence arms `XFollow::Live` once the stream holds data and /// then takes four measurements, one per frame: /// /// (a) steady state, with the retained window framed; /// (b) after three wheel notches, rolled over the x label strip -- outside `axes`, /// where the wheel's anchor is the live edge, so the zoom changes the width and /// nothing else; /// (c) after a 100 px left-drag, the real pan gesture, which moves the window back /// from the live edge; /// (d) after a further 50 ms of data has arrived. /// /// Each measurement prints the range's width, how far its right edge sits behind the /// newest sample, and whether following is still armed. Only the zoom may change the /// width, only the pan may change the distance behind, and following must be armed /// throughout. The verdict is carried out of the run in `failed`, so `main()` exits /// non-zero on a failed check -- a check whose result only stdout knows is not a check /// -- and also when the sequence never completed, since a run that measured nothing /// has not passed. struct FollowScript { bool enabled = false; /// Set by report(); read by main(). False until the checks have all held. bool failed = false; /// Set by report(): the sequence reached a verdict at all. A run that ended before /// the fourth measurement has not passed, it just has not finished. bool completed = false; plot::u32 step = 0; plot::f64 width[4]{}; plot::f64 behind[4]{}; bool live[4]{}; plot::f64 wait_from = 0.0; static FollowScript from_env() { FollowScript s; const char* v = std::getenv("PLOT_DEMO_FOLLOW"); s.enabled = v != nullptr && v[0] != '0' && v[0] != '\0'; return s; } /// One frame per step: the gesture, then the measurement of what it did. The last /// step waits for data to actually arrive, because the last measurement is about /// what more samples do to a window the user has moved. void advance(plot::glfw::Host& host, plot::data::StreamSeries& stream, plot::Plot& p, plot::u32 axis) { switch (step) { case 0: // Enough retained data for the numbers to be worth printing; the producer // starts before the window opens, so this usually fires on the first frame. if (stream.size() < 262144u) return; p.set_x_follow(axis, plot::XFollow::Live); p.auto_fit_x(); // the retained window, ending at the live edge record(0, p, stream, axis); break; case 1: notches(host, p); record(1, p, stream, axis); break; case 2: pan(host, p); record(2, p, stream, axis); wait_from = stream.x_bounds().max; break; case 3: // 50 ms at this demo's 1 MS/s, i.e. ~50000 samples of new data before the // fourth measurement. if (stream.x_bounds().max - wait_from < 0.05) return; record(3, p, stream, axis); report(); break; default: return; } ++step; } private: /// Three notches of the wheel over the x label strip centre. static void notches(plot::glfw::Host& host, plot::Plot& p) { const plot::Rect strip = p.layout().x_label_area; const plot::input::WheelEvent w{strip.x + 0.5f * strip.w, strip.y + 0.5f * strip.h, 0.0f, 1.0f, 0}; for (int i = 0; i < 3; ++i) host.controller().on_wheel(w); } /// A 100 px drag to the right across the axes, as press/move/release. static void pan(plot::glfw::Host& host, plot::Plot& p) { const plot::Rect axes = p.layout().axes; const f32 x = axes.x + 0.5f * axes.w; const f32 y = axes.y + 0.5f * axes.h; host.controller().on_pointer( {x, y, 0.0f, 0.0f, plot::input::Button::Left, 0, true, true, false}); host.controller().on_pointer( {x + 100.0f, y, 100.0f, 0.0f, plot::input::Button::Left, 0, true, false, false}); host.controller().on_pointer( {x + 100.0f, y, 0.0f, 0.0f, plot::input::Button::Left, 0, false, false, true}); } void record(int i, plot::Plot& p, plot::data::StreamSeries& stream, plot::u32 axis) { p.layout(); const plot::Range r = p.x_range(axis); width[i] = r.span(); behind[i] = stream.x_bounds().max - r.max; live[i] = p.x_follow(axis) == plot::XFollow::Live; std::printf("[glfw_demo] follow %c: width %9.6f s behind %9.6f s follow %s newest %.6f s\n", "abcd"[i], width[i], behind[i], live[i] ? "armed" : "OFF", stream.x_bounds().max); std::fflush(stdout); } void report() { // Tolerances rather than `==`: the window is recomputed from a newest x that has // moved between the frames the two numbers were taken on, so the last bits of // the width and the distance behind may differ. const auto same = [](plot::f64 a, plot::f64 b) { return std::fabs(a - b) <= 1e-9 * std::fabs(a) + 1e-12; }; const auto at_edge = [](plot::f64 distance) { return std::fabs(distance) < 1e-6; }; const bool zoom_only_width = width[0] > width[1] * 1.01 && same(width[1], width[2]) && same(width[2], width[3]); const bool pan_only_behind = at_edge(behind[0]) && at_edge(behind[1]) && behind[2] > 1e-6 && same(behind[2], behind[3]); const bool following = live[0] && live[1] && live[2] && live[3]; failed = !(zoom_only_width && pan_only_behind && following); completed = true; std::printf( "[glfw_demo] follow checks: width changed only by the zoom: %s; distance behind only" " by the pan: %s; following armed throughout: %s\n", zoom_only_width ? "yes" : "NO", pan_only_behind ? "yes" : "NO", following ? "yes" : "NO"); std::fflush(stdout); } }; /// The region's geometry as `Plot::layout()` measured it -- the rects the renderer paints /// and the controller hit-tests, so what this reports is what the frame drew -- or null /// when the layout holds no such region. const plot::RegionLayout* region_layout(const plot::Plot& p, plot::u32 region) { for (const plot::RegionLayout& r : std::as_const(p).layout().regions) { if (r.region == region) return &r; } return nullptr; } /// The shaded interval a region occupies right now, or an empty rect when the layout /// holds no such region. plot::Rect region_fill(const plot::Plot& p, plot::u32 region) { const plot::RegionLayout* r = region_layout(p, region); return r != nullptr ? r->fill : plot::Rect{}; } /// The scripted direct-manipulation sequence `PLOT_DEMO_REGIONS` runs: the region half of /// spec §5, on a real host, in the shape the follow script above established. /// /// A region is geometry in *data* coordinates while what a pointer grabs and a frame /// paints is its *pixel* rect, so the claims worth making on a live stream are that the /// rect the layout measured is the rect the data moved, and that it tracks the view /// rather than the screen. Three frames measure exactly that: /// /// (a) freeze the x view on a fixed window behind the live edge -- a user gesture does /// the same by clearing auto scale, and it is what stops the live fit from sliding /// the window under the region on every frame -- and place the region on a fraction /// of what that window shows; /// (b) pan 100 px with the press *outside* the region's interval, which is the press /// that means "move the view", and check that the region's fill rect moved by those /// 100 px and by nothing else, with its width untouched: the region is data-anchored, /// so the pan moved the data *under* it and the region came along; /// (c) leave the pointer on the region's left bound (`PLOT_DEMO_REGIONS=hot`) or off the /// data area (any other value), which is the A/B the screenshot's emphasized handle /// is counted from. /// /// The verdict is carried in `failed`, so `main()` exits non-zero when a check said no, /// and the run's screenshot is what the pixel counts are read from. struct RegionScript { bool enabled = false; /// Leave a bound hovered as the final pointer state: `PLOT_DEMO_REGIONS=hot`. Any other /// value leaves the pointer where nothing is hot, so running twice -- with and without /// `hot` -- is a pixel-level A/B of the emphasis. bool hover = false; bool failed = false; /// Set once the third measurement has been taken: a run that ended before it has not /// passed, it just has not finished. bool completed = false; plot::u32 step = 0; plot::Rect before{}; plot::Rect after{}; plot::RegionGrab grab = plot::RegionGrab::None; static RegionScript from_env() { RegionScript s; const char* v = std::getenv("PLOT_DEMO_REGIONS"); s.enabled = v != nullptr && v[0] != '\0' && v[0] != '0'; s.hover = v != nullptr && std::strcmp(v, "hot") == 0; return s; } void advance(plot::glfw::Host& host, plot::data::StreamSeries& stream, plot::Plot& p, plot::u32 axis, plot::u32 region) { switch (step) { case 0: { // The producer starts while the window is being built, so the first frame can // already hold every sample this needs -- and the renderer installs the font the // strips and the labels are measured with on its *first* draw. Measuring the // region's rect against a layout computed before that would measure the default // strips, so the script waits for a frame the renderer has drawn. if (stream.size() < 262144u || p.text_backend() == nullptr) return; const plot::Range live = stream.x_bounds(); p.set_x_range(axis, {live.max - kRegionWindowS, live.max}); const plot::Range w = p.x_range(axis); p.set_region(region, w.min + 0.30 * w.span(), w.min + 0.55 * w.span()); p.layout(); const plot::RegionLayout* rl = region_layout(p, region); if (rl == nullptr) { failed = true; break; } before = rl->fill; std::printf("[glfw_demo] region %u: frozen x [%.6f, %.6f] s, fill x %.1f w %.1f h" " %.1f, bounds at %.1f and %.1f\n", region, w.min, w.max, before.x, before.w, before.h, static_cast(plot::handle_column(rl->left_handle)), static_cast(plot::handle_column(rl->right_handle))); break; } case 1: { // The rect the pan starts from, from the settled layout of this frame rather than // from the frame the region was placed on: the strip the chrome reserves settles // with the tick labels the frozen window produces. p.layout(); before = region_fill(p, region); const plot::RegionLayout* rl = region_layout(p, region); if (rl == nullptr) { failed = true; break; } const plot::Rect axes = std::as_const(p).layout().axes; // Left of the region and inside the axes: a press on no handle, so this is the // pan and not a region grab. const plot::f32 x = rl->fill.x - 40.0f; const plot::f32 y = axes.y + 0.5f * axes.h; const plot::f64 held = p.x_transform(axis).to_data_x(x); host.controller().on_pointer( {x, y, 0.0f, 0.0f, plot::input::Button::Left, 0u, true, true, false}); const bool panned = host.controller().drag_mode() == plot::input::DragMode::Pan; host.controller().on_pointer({x + static_cast(kRegionPanPx), y, static_cast(kRegionPanPx), 0.0f, plot::input::Button::Left, 0u, true, false, false}); host.controller().on_pointer({x + static_cast(kRegionPanPx), y, 0.0f, 0.0f, plot::input::Button::Left, 0u, false, false, true}); p.layout(); after = region_fill(p, region); // The invariant the pan has always held, which is what makes a data-anchored // region track the view: the data under the press is under the pointer afterwards. const plot::f64 under = p.x_transform(axis).to_data_x(x + static_cast(kRegionPanPx)); const plot::f64 delta = static_cast(after.x) - static_cast(before.x); const bool tracked = std::fabs(delta - static_cast(kRegionPanPx)) <= 1.0; const bool width_kept = std::fabs(static_cast(after.w) - static_cast(before.w)) <= 0.5; const bool data_kept = std::fabs(under - held) <= 1e-9; failed = failed || !panned || !tracked || !width_kept || !data_kept; std::printf("[glfw_demo] region pan %u px: drag mode %s, fill x %.1f -> %.1f (delta" " %.1f, wanted %u), width %.1f -> %.1f, data under the press %.9f -> %.9f\n", kRegionPanPx, panned ? "Pan" : "NOT PAN", before.x, after.x, delta, kRegionPanPx, before.w, after.w, held, under); break; } case 2: { const plot::Rect axes = std::as_const(p).layout().axes; if (hover) { const plot::RegionLayout* rl = region_layout(p, region); if (rl == nullptr) { failed = true; break; } const plot::f32 col = plot::handle_column(rl->left_handle); host.controller().on_pointer({col, axes.y + 0.5f * axes.h, 0.0f, 0.0f, plot::input::Button::Left, 0u, false, false, false}); grab = host.controller().region_grab(region); failed = failed || grab != plot::RegionGrab::Left; } else { // Inside the window, outside the data area: no handle is hot, so a frame drawn // from here is the baseline the emphasized one is compared against. const plot::Rect strip = std::as_const(p).layout().x_label_area; host.controller().on_pointer({strip.x + 0.5f * strip.w, strip.y + 0.5f * strip.h, 0.0f, 0.0f, plot::input::Button::Left, 0u, false, false, false}); grab = host.controller().region_grab(region); failed = failed || grab != plot::RegionGrab::None; } const plot::HotHandles hot = p.hot_handles(); std::printf("[glfw_demo] region final pointer: grab zone %u, hot region %llu, hot" " zone %u, hovered cursor %llu\n", static_cast(grab), static_cast(hot.region), static_cast(hot.zone), static_cast(hot.cursor)); completed = true; break; } default: return; } ++step; } }; } // namespace int main() { plot::glfw::Host host; plot::data::StreamSeries stream(kCapacity, 0.0, kDx); Producer producer(stream); plot::Plot& p = host.plot(); // Time axis: clock labels, which `TickFormat::Time` routes through // `format_time`. The value axis keeps SI labels, which is `format_si`. const u32 axis_x = p.add_x_axis( {"time", {-0.5, 0.5}, true, plot::AxisPlace::Bottom, false, plot::TickFormat::Time}); const u32 axis_y = p.add_y_axis( {"signal", {-4.0, 4.0}, true, plot::AxisPlace::Left, false, plot::TickFormat::Si}); const u32 trace = p.add_trace({&stream, kTraceColor, 1.5f, axis_x, axis_y}); // Snapped cursors sit exactly on a sample, so the readout measures samples // rather than a rounded position between two of them. const u32 cursor_a = p.add_cursor(0.0, kCursorAColor); const u32 cursor_b = p.add_cursor(0.0, kCursorBColor); p.set_cursor_snap(cursor_a, plot::SnapMode::NearestSample); p.set_cursor_snap(cursor_b, plot::SnapMode::NearestSample); const u32 measure = p.add_measure(cursor_a, cursor_b, trace); // The region the direct-manipulation script drags and the pixels are counted from. Its // bounds are placed by the script, once the view it is measured against has been frozen. plot::Region region_desc; region_desc.snap = plot::RegionSnap::Free; region_desc.fill = kRegionFill; region_desc.edge = kRegionEdge; region_desc.label = "roi"; const u32 region = p.add_region(region_desc); Meter meter; /// The scripted live-follow verification, off unless `PLOT_DEMO_FOLLOW` asks for /// it. It is the same shape as the headless smoke mode above: a driven run whose /// result is a few numbers on stdout. FollowScript script = FollowScript::from_env(); /// The scripted region sequence, off unless `PLOT_DEMO_REGIONS` asks for it. Same shape /// as the follow script: a driven run whose verdict is carried out in its `failed`. RegionScript region_script = RegionScript::from_env(); // The cursors ride the live window until the user takes over: the first // ctrl+drag on a cursor, or a pan/zoom that clears the x axis' auto scale, // stops them where the user put them. Under `Live` the follow path owns the x // range whatever auto scale says, so a zoom is not the user taking the x view // over -- only a cursor drag, or following being switched off, is. bool cursors_follow = true; const plot::glfw::FrameFn frame = [&](plot::Plot& graph, plot::f64 dt) { stream.acquire(); // once per frame, on the render thread if (script.enabled) script.advance(host, stream, graph, axis_x); if (region_script.enabled) region_script.advance(host, stream, graph, axis_x, region); // Re-frame the live stream while the user has not overridden the ranges, and // while the axis is not following. `auto_fit_*` re-arms auto scale, so it may // only run while auto scale is still on: calling it after a pan would undo the // pan every frame -- and under `Live` a fit means "back to the live edge", so a // per-frame one would undo the offset the user just panned to as well. if (graph.x_follow(axis_x) == plot::XFollow::Off && graph.x_axis(axis_x).auto_scale) { graph.auto_fit_x(); } if (graph.y_axis(axis_y).auto_scale) graph.auto_fit_y(axis_y); if (cursors_follow) { const bool x_taken_over = graph.x_follow(axis_x) == plot::XFollow::Live ? false : !graph.x_axis(axis_x).auto_scale; if (host.controller().drag_mode() == plot::input::DragMode::MoveCursor || x_taken_over) { cursors_follow = false; } else { const plot::Range xr = graph.x_range(axis_x); graph.set_cursor_x(cursor_a, xr.min + 0.35 * xr.span()); graph.set_cursor_x(cursor_b, xr.min + 0.65 * xr.span()); } } meter.elapsed += dt; meter.frames += 1; meter.frame_ms += 0.1 * (dt * 1.0e3 - meter.frame_ms); // EMA if (meter.elapsed - meter.last_report >= 0.25) { const u64 produced = producer.produced(); meter.rate = static_cast(produced - meter.last_produced) / (meter.elapsed - meter.last_report); meter.last_produced = produced; meter.last_report = meter.elapsed; // The title carries the live readout: samples/second, frame time, and the // samples per pixel that say which rasterisation path the trace took. The // stats are the previous frame's, since this runs before draw(). const plot::Renderer* r = host.renderer(); const plot::FrameStats prev = r != nullptr ? r->stats() : plot::FrameStats{}; const plot::Rect axes = std::as_const(graph).layout().axes; const f64 spp = axes.w > 0.0f ? static_cast(prev.visible_samples) / static_cast(axes.w) : 0.0; char title[192]; std::snprintf(title, sizeof title, "plot glfw demo | %.3f MS/s | %.2f ms/frame | %.0f samples/px | %s | %llu frames", meter.rate * 1.0e-6, meter.frame_ms, spp, prev.lod_traces > 0 ? "LOD" : (prev.raw_traces > 0 ? "raw" : "-"), static_cast(meter.frames)); host.set_title(title); std::printf("[glfw_demo] t=%.1fs %.3f MS/s %.2f ms/frame %.0f samples/px %s\n", meter.elapsed, meter.rate * 1.0e-6, meter.frame_ms, spp, prev.lod_traces > 0 ? "LOD" : (prev.raw_traces > 0 ? "raw" : "-")); std::fflush(stdout); } }; // vsync on: this is a real application, and the headless mode below is what // makes it verifiable without a display. plot::glfw::RunConfig cfg; cfg.title = "plot glfw demo"; cfg.vsync = true; plot::Status st; const bool ok = host.run(cfg, frame, &st); producer.stop(); // no thread pushes into the stream after this point if (!ok) { std::fprintf(stderr, "plot_glfw_demo: %s: %s\n", plot::to_string(st.code), st.message); return 1; } // The live-follow script's verdict, before the summary: a run that asked for the // verification and did not get a passing one fails, whether a check said NO or the // sequence never got far enough to say anything (a deadline too short to reach the // fourth measurement -- a run that measured nothing has not passed). if (region_script.enabled) { if (region_script.failed) { std::fprintf(stderr, "plot_glfw_demo: the region checks failed (see the 'region' lines above)\n"); return 1; } if (!region_script.completed) { std::fprintf(stderr, "plot_glfw_demo: the region script did not complete (raise" " PLOT_DEMO_EXIT_AFTER_MS)\n"); return 1; } } if (script.enabled) { if (script.failed) { std::fprintf(stderr, "plot_glfw_demo: the live-follow checks failed (see the 'follow checks'" " line above)\n"); return 1; } if (!script.completed) { std::fprintf(stderr, "plot_glfw_demo: the live-follow script did not complete (raise" " PLOT_DEMO_EXIT_AFTER_MS)\n"); return 1; } } const plot::Renderer* r = host.renderer(); const plot::FrameStats stats = r != nullptr ? r->stats() : plot::FrameStats{}; const plot::Rect axes = std::as_const(p).layout().axes; const f64 spp = axes.w > 0.0f ? static_cast(stats.visible_samples) / static_cast(axes.w) : 0.0; const f64 dense = r != nullptr ? static_cast(r->style().dense_threshold_spp) : 0.0; const plot::Measurement m = p.measurement(measure); const f64 mean_ms = meter.frames > 0 ? meter.elapsed / static_cast(meter.frames) * 1.0e3 : 0.0; const plot::Range yr = stream.y_bounds(); std::printf("\n[glfw_demo] ---- run summary ----\n"); // The rate is the last metered interval, not produced/elapsed: this thread's // clock starts when the first frame runs, and the producer starts ~0.4 s // earlier, still building the window, so a cumulative division would report a // rate the producer never sustained. std::printf("[glfw_demo] producer %llu samples pushed, last metered %.3f MS/s\n", static_cast(producer.produced()), meter.rate * 1.0e-6); std::printf("[glfw_demo] frames %llu in %.3f s, mean %.2f ms/frame (%.1f fps)," " last metered %.2f ms/frame\n", static_cast(meter.frames), meter.elapsed, mean_ms, mean_ms > 0.0 ? 1.0e3 / mean_ms : 0.0, meter.frame_ms); std::printf("[glfw_demo] retained %llu samples, x [%.6f, %.6f] s, y [%.3f, %.3f]\n", static_cast(stream.size()), stream.x_bounds().min, stream.x_bounds().max, yr.min, yr.max); std::printf("[glfw_demo] pixel spaces viewport (framebuffer px) %.0fx%.0f, window->framebuffer" " scale %.2fx%.2f, content scale %.2fx%.2f\n", static_cast(p.viewport().w), static_cast(p.viewport().h), static_cast(host.framebuffer_scale_x()), static_cast(host.framebuffer_scale_y()), static_cast(host.content_scale_x()), static_cast(host.content_scale_y())); std::printf("[glfw_demo] last frame %.1f samples/px (dense threshold %.1f) -> %s\n", spp, dense, stats.lod_traces > 0 ? "LOD pyramid" : "raw samples"); std::printf("[glfw_demo] last frame visible_samples %llu, axes %.0fx%.0f, staged_segments %u" ", draw_calls %u, lod %u raw %u\n", static_cast(stats.visible_samples), static_cast(axes.w), static_cast(axes.h), stats.staged_segments, stats.draw_calls, stats.lod_traces, stats.raw_traces); std::printf("[glfw_demo] measurement %s dx %g s dy %g rms %g mean %g\n", m.valid ? "valid:" : "INVALID", m.dx, m.dy, m.rms, m.mean); std::printf("[glfw_demo] region x [%g, %g] s, snap %s, %s; checks %s\n", p.region(region).x0, p.region(region).x1, p.region(region).snap == plot::RegionSnap::Free ? "free" : "nearest sample", region_script.enabled ? (region_script.hover ? "bound hovered at exit" : "pointer off the data area at exit") : "not driven", region_script.enabled ? (region_script.failed ? "FAILED" : "passed") : "-"); if (const plot::RegionLayout* rl = region_layout(p, region); rl != nullptr) { // The last frame's geometry, as the layout measured it: the rects the fills, bounds // and pixel counts all have to agree with. std::printf("[glfw_demo] region fill x %.1f y %.1f w %.1f h %.1f, bounds at %.1f and" " %.1f, tab w %.1f, label w %.1f\n", rl->fill.x, rl->fill.y, rl->fill.w, rl->fill.h, static_cast(plot::handle_column(rl->left_handle)), static_cast(plot::handle_column(rl->right_handle)), rl->left_handle.w, rl->label.w); } std::printf("[glfw_demo] upload path %s, gl error on the last frame %d\n", r != nullptr && r->using_persistent_upload() ? "persistent-mapped ring" : "orphan", r != nullptr && r->last_frame_had_gl_error() ? 1 : 0); return 0; // ~producer (no-op after stop), ~stream, then ~host closes GL }