// The GTK4 application: the same 1 MS/s stream the other demos draw, as the // content of a GtkApplicationWindow whose header bar carries its own "Fit" // button, with GTK drawing the window chrome around a plot the area renders into // an FBO of its own. // // What it demonstrates: // // - `plot::gtk4::PlotArea`: a GtkGLArea (GTK's own framebuffer, no texture, no // offscreen pass of this project's) driven by GTK's gestures and event // controllers -- drag to pan, shift+drag to box zoom, ctrl+drag to move a // cursor, middle-drag on a label strip to zoom one axis, wheel to zoom about // the pointer, `+` / `-` / `f` on the keyboard; // - the device-pixel mapping: GTK allocates and reports in logical pixels, the // area's framebuffer is in device pixels, and the area converts both the // viewport and every input coordinate with the same factor (see the header); // - the demo-side controls the area knows nothing about -- the header bar's "Fit"; // - a scripted resize before the exit frame, so the headless run proves the area // re-lays the plot out for the new geometry rather than scaling a stale one; // - the headless smoke mode every demo here supports // (PLOT_DEMO_EXIT_AFTER_MS / PLOT_DEMO_SCREENSHOT). The screenshot is the GL // area's *own framebuffer*, read back by `dump_area_ppm` in the render // callback, so the PPM holds the plot surface: the header bar and the window // frame are GTK's and are not in that framebuffer. (GTK composites the area's // FBO into the window as a texture, so a whole-window shot would need GTK's // snapshot machinery; this one is honest about what it shows.) // // The producer is joined before the stream it writes into is destroyed, and // before the area's GL resources are released with the widget. // This library's GL surface first, then GTK -- the order every adapter here keeps. // GTK's headers pull in no ``, so unlike Qt there is no macro to dodge. #include #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 kDx = 1.0e-6; /// The retained window: 1 << 20 samples = 1.048576 s. 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; 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); /// Logical pixels. The area's framebuffer -- the area's GL viewport -- is in /// device pixels. constexpr int kWindowWidth = 1200; constexpr int kWindowHeight = 800; /// The size the scripted resize before the exit frame goes to: different on both /// axes, so a screenshot that kept the old layout is visibly the old size. constexpr int kExitWidth = 820; constexpr int kExitHeight = 620; /// y(t) for sample index `i`: sine + deterministic noise + a narrow spike, the /// signal the other demos use. 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. 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(); } 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; 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); } 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, refreshed a few times a second. struct Meter { f64 elapsed = 0.0; f64 last_report = 0.0; u64 last_produced = 0; f64 rate = 0.0; ///< samples/s over the last metered interval u64 frames = 0; ///< frame callbacks run (one per staged frame) }; f64 now_s() noexcept { return std::chrono::duration(std::chrono::steady_clock::now().time_since_epoch()).count(); } /// `PLOT_DEMO_EXIT_AFTER_MS` as seconds: 0.0 when unset, unparsable or not /// positive, which is "run until the window is closed". f64 exit_after_seconds() noexcept { const char* v = std::getenv("PLOT_DEMO_EXIT_AFTER_MS"); if (v == nullptr || v[0] == '\0') return 0.0; char* end = nullptr; const f64 ms = std::strtod(v, &end); if (end == v || !(ms > 0.0)) return 0.0; return ms * 1.0e-3; } /// Writes the GL area's own framebuffer out as a binary PPM (P6, 8 bits per /// channel, rows top-down). /// /// Demo-side, not library API -- hence the name: this is the smoke mode's picture /// of the *area's* framebuffer, which is what GTK composites into the window. /// `glReadPixels`'s origin is the framebuffer's bottom-left and a PPM's is its /// top-left, so the rows are written in reverse; the alpha channel is dropped, /// and the plot clears its whole viewport to an opaque background, so the RGB is /// final. An `GL_RGBA` row is `width * 4` bytes -- a multiple of four -- so the /// default `GL_PACK_ALIGNMENT` holds and no pixel-store state has to be touched. bool dump_area_ppm(const char* path, int w, int h) { if (path == nullptr || path[0] == '\0' || w <= 0 || h <= 0) return false; const plot::usize width = static_cast(w); const plot::usize height = static_cast(h); std::vector pixels(width * height * 4u); plot::gl::glReadPixels(0, 0, w, h, plot::gl::GL_RGBA, plot::gl::GL_UNSIGNED_BYTE, pixels.data()); std::FILE* file = std::fopen(path, "wb"); if (file == nullptr) return false; if (std::fprintf(file, "P6\n%d %d\n255\n", w, h) < 0) { std::fclose(file); return false; } const plot::usize row_bytes = width * 3u; std::vector row(row_bytes); bool ok = true; for (plot::usize y = height; y-- > 0u && ok;) { const plot::u8* src = pixels.data() + y * width * 4u; for (plot::usize x = 0; x < width; ++x) { row[x * 3u + 0u] = src[x * 4u + 0u]; row[x * 3u + 1u] = src[x * 4u + 1u]; row[x * 3u + 2u] = src[x * 4u + 2u]; } ok = std::fwrite(row.data(), 1u, row_bytes, file) == row_bytes; } if (!ok) { std::fclose(file); return false; } return std::fclose(file) == 0; } /// The run summary's numbers, taken while the widget -- and with it the area and /// its renderer -- is still alive. GTK frees the wrapper when the widget is /// finalized, so the summary prints this instead of reading through a pointer /// that may already be gone. struct Snapshot { bool have_renderer = false; plot::ErrorCode init_code = plot::ErrorCode::Ok; const char* init_message = ""; u64 frames = 0; f32 mean_ms = 0.0f; f32 last_ms = 0.0f; plot::FrameStats stats{}; plot::Rect axes{}; f32 dense_spp = 0.0f; bool persistent = false; bool gl_error = false; }; /// Everything the demo owns, reachable from the signal callbacks and from the /// exit path. struct Demo { plot::data::StreamSeries* stream = nullptr; Producer* producer = nullptr; plot::Plot* graph = nullptr; plot::gtk4::PlotArea* area = nullptr; ///< null once GTK has destroyed the widget GtkWindow* window = nullptr; GtkApplication* app = nullptr; u32 axis_x = 0; u32 axis_y = 0; u32 cursor_a = 0; u32 cursor_b = 0; u32 measure = 0; /// A weak pointer on the area's widget: GTK frees the wrapper with it, so this /// is how the callbacks that outlive the window (the pump, the summary) know /// whether the widget can still be touched. GtkWidget* widget = nullptr; bool automated = false; f64 exit_after_s = 0.0; f64 run_start = 0.0; const char* screenshot = nullptr; /// Set by the exit path, read by the after-render handler that takes the /// screenshot and ends the run. Non-null means "this render is the exit frame". const char* dump_path = nullptr; bool exit_pending = false; bool summary_printed = false; /// The two exit sequences, each guarded so it runs exactly once: the timer's /// block (which pumps the main context and can be re-entered by its own timeout) /// and the teardown (scheduled as an idle source). bool exit_started = false; bool torn_down = false; bool cursors_follow = true; Meter meter; Snapshot snapshot; }; void capture_snapshot(Demo& demo) { Snapshot& s = demo.snapshot; s.init_code = demo.area->init_status().code; s.init_message = demo.area->init_status().message; plot::Renderer* const renderer = demo.area->renderer(); s.have_renderer = renderer != nullptr; s.frames = demo.area->frames_painted(); s.mean_ms = demo.area->mean_frame_ms(); s.last_ms = demo.area->last_frame_ms(); s.axes = std::as_const(*demo.graph).layout().axes; if (renderer != nullptr) { s.stats = renderer->stats(); s.dense_spp = renderer->style().dense_threshold_spp; s.persistent = renderer->using_persistent_upload(); s.gl_error = renderer->last_frame_had_gl_error(); } } /// A window closed by hand -- or by the window manager -- takes the ordering the /// automated path takes: the producer is joined *before* GTK destroys the widget, /// and with it the renderer whose frames read the stream. GTK emits this before its /// own handler destroys the window, and returning FALSE lets that default happen. gboolean on_close_request(GtkWindow*, gpointer user_data) { Demo& demo = *static_cast(user_data); demo.producer->stop(); return FALSE; } /// Runs the window's destruction and the quit outside any signal emission: see /// `on_render_after`. gboolean on_teardown(gpointer user_data) { Demo& demo = *static_cast(user_data); // The teardown runs once, whatever scheduled it (the exit frame's idle source, or // a second one if two renders were pending): the window is destroyed and the // application quit here, and both of those must not happen twice. if (demo.torn_down) return G_SOURCE_REMOVE; demo.torn_down = true; // The producer is joined *first*, while the widget and the stream it feeds are // both still alive: the join is what makes "no thread pushes into the stream // after this point" true for the destruction below, and for the frames the // widget may still stage while it is being torn down. demo.producer->stop(); // Destroyed explicitly rather than left to the application's shutdown, so an // automated run exercises the widget's teardown -- the renderer's GL objects are // released in `::unrealize`, with the context still current -- and a fault in it // shows up in the run's exit status rather than after the output has been read. gtk_window_destroy(demo.window); g_application_quit(G_APPLICATION(demo.app)); return G_SOURCE_REMOVE; } /// The repaint pump. GTK only redraws the area when something asks it to, so this /// is the frame rate the demo runs at (a real application drives the same timer, /// or its toolkit's equivalent, from whatever it animates). Nothing here is /// library API: `gtk_gl_area_queue_render` is GTK's. gboolean on_repaint(gpointer user_data) { Demo& demo = *static_cast(user_data); if (demo.widget == nullptr) { // the window was closed: the wrapper is gone too demo.area = nullptr; return G_SOURCE_REMOVE; } capture_snapshot(demo); gtk_gl_area_queue_render(GTK_GL_AREA(demo.area->widget())); return G_SOURCE_CONTINUE; } /// Connected with `g_signal_connect_after`, so it runs once the adapter's own /// handler has staged the frame and the area's FBO holds it. It is also the one /// place that sees a render after a failed init -- the adapter's handler returns /// early then, but GTK still emits the signal -- which keeps the snapshot's /// status meaningful. gboolean on_render_after(GtkGLArea*, GdkGLContext*, gpointer user_data) { Demo& demo = *static_cast(user_data); if (demo.area != nullptr) capture_snapshot(demo); if (!demo.exit_pending) return FALSE; demo.exit_pending = false; if (demo.dump_path != nullptr && demo.dump_path[0] != '\0') { const plot::Rect viewport = demo.area->device_viewport(); const bool written = dump_area_ppm(demo.dump_path, static_cast(viewport.w), static_cast(viewport.h)); std::printf("[gtk_demo] screenshot %s (%dx%d GL px): %s\n", demo.dump_path, static_cast(viewport.w), static_cast(viewport.h), written ? "written" : "FAILED"); std::fflush(stdout); } // The teardown is *deferred*: closing the window destroys this very widget, and // the render signal is still being emitted, so doing it on this stack would free // the area GTK is in the middle of calling back into. An idle source runs on the // next main-loop iteration, after the emission has returned. g_idle_add(&on_teardown, &demo); return FALSE; } /// The header bar's "Fit": one-shot framing of the data. `auto_fit_*` re-arms /// auto scale, which the frame callback then follows, so this is also how a pan /// is undone. void on_fit_clicked(GtkButton*, gpointer user_data) { Demo& demo = *static_cast(user_data); demo.graph->auto_fit_x(); demo.graph->auto_fit_y(demo.axis_y); // A click moves focus to the button, which would take the controller's keys away // from the plot; hand it back, or `+`/`-`/`f` would stop reaching the plot. gtk_widget_grab_focus(demo.area->widget()); gtk_gl_area_queue_render(GTK_GL_AREA(demo.area->widget())); } /// The headless exit path: at the deadline, resize the window (scripted), check /// that the layout was recomputed for the new geometry, then queue the exit frame /// and let `on_render_after` take the screenshot and end the run. gboolean on_exit_timer(gpointer user_data) { Demo& demo = *static_cast(user_data); if (!demo.automated || now_s() - demo.run_start < demo.exit_after_s) return G_SOURCE_CONTINUE; // The pump below iterates the main context, so this very timeout can fire from // inside this call; the guard is what keeps the exit sequence -- the scripted // resize, its check, the exit frame -- to one run. if (demo.exit_started) return G_SOURCE_REMOVE; demo.exit_started = true; if (demo.widget == nullptr) return G_SOURCE_REMOVE; // the window is already gone const plot::PlotLayout before = std::as_const(*demo.graph).layout(); const plot::Rect viewport_before = demo.area->device_viewport(); const int width_before = gtk_widget_get_width(demo.area->widget()); const int height_before = gtk_widget_get_height(demo.area->widget()); // The resize goes through the window system, so pump the main context until the // area's allocation follows, with a deadline. The pump is what lets the resize // and the frames in between (which re-derive the viewport) actually happen. gtk_window_set_default_size(demo.window, kExitWidth, kExitHeight); const f64 deadline = now_s() + 1.0; while (gtk_widget_get_width(demo.area->widget()) == width_before && now_s() < deadline) { while (g_main_context_pending(nullptr)) g_main_context_iteration(nullptr, FALSE); g_usleep(1000); } const plot::PlotLayout after = std::as_const(*demo.graph).layout(); const plot::Rect viewport = demo.area->device_viewport(); // The evidence that the layout was *recomputed and not scaled*: the y label // strip is a number of *font pixels*, so it stays the same size while the axes takes // the rest of the new allocation. Two things are checked, and neither is "the // strip is the same number": // // * the axes and its strips tile the *current* viewport exactly -- a layout // left over from the old geometry would still tile the old one; // * the strip is nearer the previous strip than that strip scaled by the resize // ratio -- a layout whose strips were *scaled* would land on the second. // // The strip is not invariant, and the check must not pretend it is: it is the // widest major tick label plus padding, and the tick *step* follows the axes // length, which the resize changes. Measured at scale 2: 18 majors over 1440 // device px with "-800m" the widest label, 8 over 1080 with "-500m" -- 78 and // 76 px in this bitmap font, whose '8' is the wider glyph. That is 2 px of // recomputed strip, against 174 * 1620/2380 = 118 px of scaled strip. const f32 resize_ratio = viewport_before.w > 0.0f ? viewport.w / viewport_before.w : 1.0f; const f32 scaled_strip = before.axes.x * resize_ratio; const f32 drift = std::fabs(after.axes.x - before.axes.x); const bool tiles = std::fabs(after.axes.w - (viewport.w - after.axes.x)) <= 0.5f && std::fabs(after.axes.h - (viewport.h - after.axes.y - after.x_axis_height - after.x_title_height)) <= 0.5f; const bool metric_strip = drift < std::fabs(after.axes.x - scaled_strip); const bool recomputed = after.axes.w != before.axes.w && tiles && metric_strip; std::printf("\n[gtk_demo] ---- exit frame ----\n"); std::printf("[gtk_demo] resize logical %dx%d -> %dx%d, device viewport %ux%u, scale %g\n", width_before, height_before, gtk_widget_get_width(demo.area->widget()), gtk_widget_get_height(demo.area->widget()), static_cast(viewport.w), static_cast(viewport.h), static_cast(demo.area->device_scale())); std::printf("[gtk_demo] layout axes %.0fx%.0f at (%.0f,%.0f) -> %.0fx%.0f at (%.0f,%.0f): %s\n", static_cast(before.axes.w), static_cast(before.axes.h), static_cast(before.axes.x), static_cast(before.axes.y), static_cast(after.axes.w), static_cast(after.axes.h), static_cast(after.axes.x), static_cast(after.axes.y), recomputed ? "recomputed for the new viewport (strips metric, not scaled)" : "FAIL: stale or rescaled layout"); std::printf("[gtk_demo] layout y label strip %.0f -> %.0f px of a %.0f -> %.0f px wide axes," " %.0f px of drift against a scaled %.0f\n", static_cast(before.axes.x), static_cast(after.axes.x), static_cast(before.axes.w), static_cast(after.axes.w), static_cast(drift), static_cast(scaled_strip)); std::printf("[gtk_demo] init %s: %s\n", plot::to_string(demo.area->init_status().code), demo.area->init_status().message); std::fflush(stdout); demo.dump_path = demo.screenshot; demo.exit_pending = true; gtk_gl_area_queue_render(GTK_GL_AREA(demo.area->widget())); return G_SOURCE_REMOVE; } void on_activate(GtkApplication* app, gpointer user_data) { Demo& demo = *static_cast(user_data); demo.app = app; GtkWidget* window = gtk_application_window_new(app); demo.window = GTK_WINDOW(window); gtk_window_set_title(GTK_WINDOW(window), "plot gtk4 demo"); gtk_window_set_default_size(GTK_WINDOW(window), kWindowWidth, kWindowHeight); // The hand-close path's half of the shutdown ordering (see `on_close_request`); // the automated path stops the producer in `on_teardown` instead, because // `gtk_window_destroy()` does not emit this signal. g_signal_connect(window, "close-request", G_CALLBACK(on_close_request), &demo); // The plot area is the window's content. GTK owns the widget from here on: the // wrapper is freed with it, which is also what releases the plot's GL resources. demo.area = plot::gtk4::PlotArea::create(*demo.graph); gtk_window_set_child(GTK_WINDOW(window), demo.area->widget()); // The weak pointer has to be seeded with the widget it watches: GLib only // remembers the *address* and clears it at finalization, it does not read or // store the object (this is why the demo's copy is NULL until the two lines // below -- and why a missing assignment would leave every callback believing // the window was already gone). demo.widget = demo.area->widget(); g_object_add_weak_pointer(G_OBJECT(demo.widget), reinterpret_cast(&demo.widget)); GtkWidget* header = gtk_header_bar_new(); gtk_header_bar_set_title_widget(GTK_HEADER_BAR(header), gtk_label_new("plot gtk4 demo")); GtkWidget* fit = gtk_button_new_with_label("Fit"); g_signal_connect(fit, "clicked", G_CALLBACK(on_fit_clicked), &demo); gtk_header_bar_pack_start(GTK_HEADER_BAR(header), fit); gtk_window_set_titlebar(GTK_WINDOW(window), header); // The exit frame's screenshot is taken from the render signal, after the // adapter's handler has drawn the frame: `_after` is what guarantees the order. g_signal_connect_after(demo.area->widget(), "render", G_CALLBACK(on_render_after), &demo); // The frame callback: the same host-side work the other demos do, on the thread // that owns the GL context (the GTK main thread), never on the producer's. demo.area->set_frame_callback([&demo](plot::Plot& p, plot::f64 dt) { demo.stream->acquire(); // once per frame, on the render thread // Re-frame the live stream while auto scale is armed. `auto_fit_*` re-arms it, // so this may only run while it is still on: calling it after a pan would undo // the pan every frame. if (p.x_axis(demo.axis_x).auto_scale) p.auto_fit_x(); if (p.y_axis(demo.axis_y).auto_scale) p.auto_fit_y(demo.axis_y); if (demo.cursors_follow) { if (demo.area->controller().drag_mode() == plot::input::DragMode::MoveCursor || !p.x_axis(demo.axis_x).auto_scale) { demo.cursors_follow = false; } else { const plot::Range xr = p.x_range(demo.axis_x); p.set_cursor_x(demo.cursor_a, xr.min + 0.35 * xr.span()); p.set_cursor_x(demo.cursor_b, xr.min + 0.65 * xr.span()); } } demo.meter.frames += 1; demo.meter.elapsed += dt; if (demo.meter.elapsed - demo.meter.last_report >= 0.5) { const u64 produced = demo.producer->produced(); demo.meter.rate = static_cast(produced - demo.meter.last_produced) / (demo.meter.elapsed - demo.meter.last_report); demo.meter.last_produced = produced; demo.meter.last_report = demo.meter.elapsed; const plot::Rect viewport = demo.area->last_device_viewport(); const plot::Rect axes = std::as_const(p).layout().axes; const plot::Range xr = p.x_range(demo.axis_x); std::printf( "[gtk_demo] t=%.1fs %.3f MS/s %.2f ms/frame scale %g fbo %ux%u axes %.0fx%.0f" " x [%.6f, %.6f] span %.6f auto %d %llu frames\n", demo.meter.elapsed, demo.meter.rate * 1.0e-6, static_cast(demo.area->mean_frame_ms()), static_cast(demo.area->device_scale()), static_cast(viewport.w), static_cast(viewport.h), static_cast(axes.w), static_cast(axes.h), xr.min, xr.max, xr.span(), p.x_axis(demo.axis_x).auto_scale ? 1 : 0, static_cast(demo.meter.frames)); std::fflush(stdout); } }); gtk_window_present(GTK_WINDOW(window)); // So the controller's keys reach the plot: a key event goes to the focused // widget, and a `GtkGLArea` is focusable because the adapter says so. gtk_window_set_focus(GTK_WINDOW(window), demo.area->widget()); // The repaint pump (~60 Hz, the display rate this demo runs at) and -- in the // automated mode -- the deadline that runs the exit path. g_timeout_add(16, &on_repaint, &demo); if (demo.automated) g_timeout_add(20, &on_exit_timer, &demo); } /// The run summary, printed after the main loop: the stream, the plot and the /// producer are the demo's, and outlive the window, so only the renderer's /// numbers come from the snapshot. void print_summary(Demo& demo) { const Snapshot& s = demo.snapshot; const plot::Measurement m = demo.graph->measurement(demo.measure); const f64 spp = s.axes.w > 0.0f ? static_cast(s.stats.visible_samples) / static_cast(s.axes.w) : 0.0; std::printf("\n[gtk_demo] ---- run summary ----\n"); std::printf("[gtk_demo] producer %llu samples pushed, last metered %.3f MS/s\n", static_cast(demo.producer->produced()), demo.meter.rate * 1.0e-6); std::printf("[gtk_demo] frames %llu staged (%llu frame callbacks) in %.3f s, mean %.2f" " ms/frame (%.1f fps), last %.2f ms\n", static_cast(s.frames), static_cast(demo.meter.frames), demo.meter.elapsed, static_cast(s.mean_ms), s.mean_ms > 0.0f ? 1.0e3 / static_cast(s.mean_ms) : 0.0, static_cast(s.last_ms)); std::printf("[gtk_demo] retained %llu samples, x [%.6f, %.6f] s\n", static_cast(demo.stream->size()), demo.stream->x_bounds().min, demo.stream->x_bounds().max); std::printf("[gtk_demo] last frame %.1f samples/px (dense threshold %.1f) -> %s, visible %llu," " axes %.0fx%.0f, %u segments, %u draw calls\n", spp, static_cast(s.dense_spp), s.stats.lod_traces > 0 ? "LOD pyramid" : "raw samples", static_cast(s.stats.visible_samples), static_cast(s.axes.w), static_cast(s.axes.h), s.stats.staged_segments, s.stats.draw_calls); std::printf("[gtk_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("[gtk_demo] gl %s upload, gl error on the last frame %d\n", s.persistent ? "persistent-mapped ring" : "orphan", s.gl_error ? 1 : 0); std::printf("[gtk_demo] init %s: %s\n", plot::to_string(s.init_code), s.init_message); std::fflush(stdout); } } // namespace int main(int argc, char** argv) { const f64 exit_after_s = exit_after_seconds(); const bool automated = exit_after_s > 0.0; const char* const screenshot = std::getenv("PLOT_DEMO_SCREENSHOT"); // --- the stream and the plot the area draws ----------------------------- plot::data::StreamSeries stream(kCapacity, 0.0, kDx); Producer producer(stream); plot::Plot graph; // 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 = graph.add_x_axis( {"time", {-0.5, 0.5}, true, plot::AxisPlace::Bottom, false, plot::TickFormat::Time}); const u32 axis_y = graph.add_y_axis( {"signal", {-4.0, 4.0}, true, plot::AxisPlace::Left, false, plot::TickFormat::Si}); const u32 trace = graph.add_trace({&stream, kTraceColor, 1.5f, axis_x, axis_y}); // Snapped cursors sit exactly on a sample, so the readout measures samples and // not a rounded position between two of them. const u32 cursor_a = graph.add_cursor(0.0, kCursorAColor); const u32 cursor_b = graph.add_cursor(0.0, kCursorBColor); graph.set_cursor_snap(cursor_a, plot::SnapMode::NearestSample); graph.set_cursor_snap(cursor_b, plot::SnapMode::NearestSample); const u32 measure = graph.add_measure(cursor_a, cursor_b, trace); Demo demo; demo.stream = &stream; demo.producer = &producer; demo.graph = &graph; demo.axis_x = axis_x; demo.axis_y = axis_y; demo.cursor_a = cursor_a; demo.cursor_b = cursor_b; demo.measure = measure; demo.automated = automated; demo.exit_after_s = exit_after_s; demo.run_start = now_s(); demo.screenshot = screenshot; GtkApplication* app = gtk_application_new("org.plot.gtk_demo", G_APPLICATION_DEFAULT_FLAGS); g_signal_connect(app, "activate", G_CALLBACK(on_activate), &demo); const int app_result = g_application_run(G_APPLICATION(app), argc, argv); // The window is gone by now -- `on_render_after` destroyed it before quitting -- // so only what the demo owns is read below. producer.stop(); // already stopped by on_teardown; idempotent, and this is the // path a window closed by hand takes (no on_teardown then) print_summary(demo); std::printf("[gtk_demo] teardown the area widget is %s after the main loop\n", demo.widget != nullptr ? "still alive" : "destroyed (the wrapper and its GL" " resources went with it)"); g_object_unref(app); return app_result; }