Files
plotter/examples/imgui_demo.cpp
T
Martino Ferrari 942f88c568 feat(examples): a three-row synced Figure demo, on a GLFW host that draws one
The multi-plot example the phase exists for: three plots stacked in one
Figure, sharing one x link with following armed, each row carrying its own
named signals and its own legend, driven by six StreamSeries at 500 kS/s.
Two cursors and a region with an exact statistics readout reach the direct
manipulation of spec §5 in the same frame, and PLOT_DEMO_SYNC=1 is a
self-checking mode that exits non-zero unless all three rows hold one range
and the shared right edge keeps tracking the newest sample through a zoom, a
pan on another row and more arriving data.

The GLFW host gains the figure mode the demo needs: set_figure() makes the
run loop draw the Figure instead of its own plot and route each pointer,
wheel and key event to the row it lands in, through one Controller per row
(row_controller()). All of it is opt-in; the single-plot path is untouched.
2026-09-28 15:24:22 +02:00

563 lines
24 KiB
C++

// The Dear ImGui application: the same stream the GLFW demo draws, embedded as a
// widget inside an ImGui window that also owns a few controls, with ImGui drawing
// its own chrome into the same framebuffer.
//
// What it demonstrates:
//
// - `plot::imgui::PlotWidget`: a child region whose framebuffer rectangle the
// plot renders into directly (no texture, no FBO), while ImGui's chrome is
// drawn around it in the same pass;
// - the demo-side controls the widget deliberately knows nothing about: a
// slider for the producer's ingest rate, a button that injects a spike into
// the `StreamSeries`, and a checkbox toggling the LOD statistics overlay;
// - the frame order the embedding needs -- NewFrame, build UI (the widget draws
// the plot here), ImGui::Render, then the backend's draw data -- and that
// ImGui still renders correctly after the plot renderer has come and gone;
// - the headless smoke mode every demo here supports
// (PLOT_DEMO_EXIT_AFTER_MS / PLOT_DEMO_SCREENSHOT), whose screenshot this
// demo writes as the *whole* framebuffer (dump_full_framebuffer_ppm below), so
// the PPM holds the ImGui chrome as well as the plot.
//
// The producer is joined before the stream it writes into is destroyed, and
// before the GL context goes away.
// Before GLFW's header, so it does not include <GL/gl.h>: that header's macros
// would rewrite the enumerators of plot/gl/api.hpp, which this file includes.
#define GLFW_INCLUDE_NONE
#include <GLFW/glfw3.h>
#include <backends/imgui_impl_glfw.h>
#include <backends/imgui_impl_opengl3.h>
#include <imgui.h>
#include <imgui/imgui_plot.hpp>
#include <plot/data/stream_series.hpp>
#include <plot/gl/loader.hpp>
#include <algorithm>
#include <atomic>
#include <chrono>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <numbers>
#include <optional>
#include <thread>
#include <utility>
#include <vector>
namespace {
using plot::f32;
using plot::f64;
using plot::u32;
using plot::u64;
/// The sample *spacing* of the series: 1 microsecond, as in the GLFW demo. The
/// rate slider below changes how fast samples arrive, not this spacing (a
/// `StreamSeries`' dx is fixed at construction), so a slower ingest simply
/// advances the window more slowly.
constexpr f64 kDx = 1.0e-6;
/// The retained window: 1 << 20 samples = 1.048576 s of samples.
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 stopped and resumed catches up over
/// several iterations instead of allocating a batch for the whole gap.
constexpr u64 kMaxBatch = 1u << 16;
/// Longest the producer sleeps in one go, so a slider move is picked up promptly.
constexpr f64 kMaxSleepS = 0.05;
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 manual spike the button injects: tall enough to be the maximum of every
/// LOD bucket it falls in, so it survives decimation.
constexpr f64 kInjectedSpikeAmp = 5.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);
/// The host's own background, behind everything ImGui and the plot do: kept
/// distinct from the renderer's background so the PPM shows that the plot cleared
/// only its own rectangle.
constexpr plot::Color kHostClearColor = plot::Color::hex(0x263145);
constexpr f32 kWindowWidth = 960.0f;
constexpr f32 kWindowHeight = 680.0f;
/// y(t) for sample index `i`: sine + deterministic noise + a periodic narrow
/// spike. Same signal as the GLFW demo, so the two screenshots show the same
/// trace from the same source.
f32 sample_at(u64 i) noexcept {
const f64 t = static_cast<f64>(i) * kDx;
f64 y = std::sin(2.0 * std::numbers::pi * kSineHz * t);
u32 h = static_cast<u32>(i) * 2654435761u;
h ^= h >> 15;
h *= 2246822519u;
h ^= h >> 13;
h *= 3266489917u;
h ^= h >> 16;
y += kNoiseAmp * (static_cast<f64>(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<f32>(y);
}
/// Feeds a `StreamSeries` from its own thread at a settable rate, 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. The rate is settable from the render thread, and a requested
/// spike rides the middle sample of the batch in flight.
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 per second the producer aims for. Written by the render thread.
void set_rate(f64 samples_per_second) noexcept {
rate_.store(samples_per_second > 0.0 ? samples_per_second : 1.0, std::memory_order_relaxed);
}
/// Asks the next full batch to carry one tall sample. Accumulates, so a rapid
/// double click injects two.
void request_spike() noexcept { spikes_.fetch_add(1, std::memory_order_relaxed); }
/// 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<f64>(clock::now() - start).count();
const f64 rate = rate_.load(std::memory_order_relaxed);
const u64 due = static_cast<u64>(elapsed * rate) + 1; // samples whose time has come
if (due > next) {
u64 n = due - next;
if (n > kMaxBatch) n = kMaxBatch;
batch_.resize(static_cast<plot::usize>(n));
for (u64 k = 0; k < n; ++k) {
batch_[static_cast<plot::usize>(k)] = sample_at(next + k);
}
if (n > 0 && spikes_.load(std::memory_order_relaxed) > 0) {
batch_[static_cast<plot::usize>(n / 2)] += static_cast<f32>(kInjectedSpikeAmp);
spikes_.fetch_sub(1, std::memory_order_relaxed);
}
series_->push(batch_.data(), n);
next += n;
produced_.store(next, std::memory_order_relaxed);
}
// Sleep until a batch past the last produced sample, capped so a rate
// change is noticed quickly instead of at the old rate's next deadline.
const f64 wake_s =
(static_cast<f64>(next) + static_cast<f64>(kBatch)) / rate;
const f64 sleep_s = wake_s < kMaxSleepS ? wake_s : kMaxSleepS;
std::this_thread::sleep_until(
start + std::chrono::duration_cast<clock::duration>(std::chrono::duration<f64>(sleep_s)));
}
}
plot::data::StreamSeries* series_;
std::vector<f32> batch_;
std::atomic<f64> rate_{1.0e6};
std::atomic<u64> produced_{0};
std::atomic<int> spikes_{0};
std::atomic<bool> 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 frame_ms = 0.0; ///< exponential moving average of the whole frame
f64 elapsed = 0.0; ///< seconds of loop time
f64 last_report = 0.0;
u64 last_produced = 0;
u64 frames = 0;
f64 rate = 0.0; ///< samples/s over the last metered interval
};
/// `glfwGetProcAddress` returns GLFW's `void (*)(void)`, `plot::gl::ProcLoader`
/// returns `void*`: a function pointer respelled as an object pointer. POSIX
/// guarantees the two have the same representation, and the bit copy states that
/// where a `reinterpret_cast` would be merely conditionally supported.
void* resolve_gl(const char* name) noexcept {
GLFWglproc p = glfwGetProcAddress(name);
void* out = nullptr;
static_assert(sizeof(out) == sizeof(p), "function and object pointers must match");
std::memcpy(&out, &p, sizeof(out));
return out;
}
void report_failure(const plot::Status& st, const char* what) noexcept {
std::fprintf(stderr, "plot_imgui_demo: %s: %s: %s\n", what, plot::to_string(st.code), st.message);
}
/// Writes the *whole* bound framebuffer as a PPM (P6).
///
/// Demo-side, not library API: `Renderer::dump_frame_ppm()` covers the plot's
/// viewport only, and this smoke mode has to show the ImGui chrome as well. Reads
/// RGBA (4-byte pixels have no row padding whatever the pack alignment) and
/// writes the rows top-down, since glReadPixels hands them back bottom-up.
bool dump_full_framebuffer_ppm(const char* path, int fb_w, int fb_h) {
if (path == nullptr || path[0] == '\0' || fb_w <= 0 || fb_h <= 0) return false;
const plot::usize w = static_cast<plot::usize>(fb_w);
const plot::usize h = static_cast<plot::usize>(fb_h);
std::vector<unsigned char> pixels(w * h * 4u, 0u);
plot::gl::glPixelStorei(plot::gl::GL_PACK_ALIGNMENT, 4);
plot::gl::glReadPixels(0, 0, fb_w, fb_h, plot::gl::GL_RGBA, plot::gl::GL_UNSIGNED_BYTE,
pixels.data());
std::FILE* file = std::fopen(path, "wb");
if (file == nullptr) return false;
std::fprintf(file, "P6\n%d %d\n255\n", fb_w, fb_h);
std::vector<unsigned char> row(w * 3u);
for (plot::usize y = 0; y < h; ++y) {
const unsigned char* src = pixels.data() + (h - 1u - y) * w * 4u;
for (plot::usize x = 0; x < w; ++x) {
row[x * 3u] = src[x * 4u];
row[x * 3u + 1u] = src[x * 4u + 1u];
row[x * 3u + 2u] = src[x * 4u + 2u];
}
if (std::fwrite(row.data(), 1u, row.size(), file) != row.size()) {
std::fclose(file);
return false;
}
}
return std::fclose(file) == 0;
}
/// `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;
}
} // namespace
int main() {
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");
// --- window and 3.3 core context ------------------------------------
if (glfwInit() != GLFW_TRUE) {
std::fprintf(stderr, "plot_imgui_demo: glfwInit() failed\n");
return 1;
}
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GLFW_TRUE);
// A run with a deadline is the automated path: it stays off-screen, so a CI run
// never flashes a window on a real display.
if (automated) glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
GLFWwindow* const window =
glfwCreateWindow(1280, 720, "plot imgui demo", nullptr, nullptr);
if (window == nullptr) {
const char* desc = nullptr;
(void)glfwGetError(&desc);
std::fprintf(stderr, "plot_imgui_demo: glfwCreateWindow() failed: %s\n",
desc != nullptr ? desc : "no error description");
glfwTerminate();
return 1;
}
glfwMakeContextCurrent(window);
// The bounded run is not throttled by a display refresh, so its frame times are
// the CPU's and not a vblank's.
glfwSwapInterval(automated ? 0 : 1);
plot::Status st;
if (!plot::gl::load(&resolve_gl)) {
st = plot::Status{plot::ErrorCode::Unsupported,
"plot::gl::load() failed: missing OpenGL 3.3 entry point"};
report_failure(st, "gl::load()");
glfwDestroyWindow(window);
glfwTerminate();
return 1;
}
std::optional<plot::Renderer> renderer;
renderer.emplace();
if (!renderer->init(&st)) {
if (st.code == plot::ErrorCode::Ok) {
st = plot::Status{plot::ErrorCode::GLFailure, "Renderer::init() failed"};
}
report_failure(st, "Renderer::init()");
renderer.reset();
glfwDestroyWindow(window);
glfwTerminate();
return 1;
}
// --- Dear ImGui -----------------------------------------------------
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGui::GetIO().IniFilename = nullptr; // no imgui.ini written next to the binary
ImGui::StyleColorsDark();
const bool platform_ok = ImGui_ImplGlfw_InitForOpenGL(window, true);
const bool backend_ok = platform_ok && ImGui_ImplOpenGL3_Init("#version 330 core");
if (!backend_ok) {
st = plot::Status{plot::ErrorCode::Unsupported,
"Dear ImGui backend init failed (ImGui_ImplGlfw_InitForOpenGL / "
"ImGui_ImplOpenGL3_Init): no GL loader for the current context"};
report_failure(st, "imgui backend");
if (platform_ok) ImGui_ImplGlfw_Shutdown();
ImGui::DestroyContext();
renderer.reset();
glfwDestroyWindow(window);
glfwTerminate();
return 1;
}
// --- the plot and its stream ----------------------------------------
plot::data::StreamSeries stream(kCapacity, 0.0, kDx);
Producer producer(stream);
producer.set_rate(1.0e6);
plot::Plot graph;
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});
// Named, so the plot's legend -- on by default -- lists the signal.
graph.add_trace({&stream, kTraceColor, 1.5f, axis_x, axis_y, "signal"});
plot::imgui::PlotWidget widget;
// Demo state: the widget knows about none of it.
f32 rate_msamples = 1.0f;
bool show_lod_stats = true;
Meter meter;
const f64 start = glfwGetTime();
f64 previous = start;
while (glfwWindowShouldClose(window) == GLFW_FALSE) {
glfwPollEvents();
int fb_w = 0;
int fb_h = 0;
glfwGetFramebufferSize(window, &fb_w, &fb_h);
if (fb_w <= 0 || fb_h <= 0) {
// Minimised or otherwise undrawable: nothing to render and no screenshot to
// take. Still honour the deadline, so a bounded run ends rather than idling
// past the one it was given.
if (automated && glfwGetTime() - start >= exit_after_s) {
std::fprintf(stderr,
"[imgui_demo] exit deadline reached with no drawable (framebuffer 0x0);"
" nothing to dump\n");
break;
}
glfwWaitEventsTimeout(0.05);
continue;
}
stream.acquire(); // once per frame, on the render thread
const f64 now = glfwGetTime();
const f64 dt = now - previous;
previous = now;
// The host clears its own frame first: the renderer clears only the plot's
// rectangle, so everything outside it is ours to paint.
plot::gl::glViewport(0, 0, fb_w, fb_h);
plot::gl::glClearColor(kHostClearColor.r, kHostClearColor.g, kHostClearColor.b,
kHostClearColor.a);
plot::gl::glClear(plot::gl::GL_COLOR_BUFFER_BIT);
ImGui_ImplGlfw_NewFrame();
ImGui_ImplOpenGL3_NewFrame(); // builds the font atlas/texture on its first call
ImGui::NewFrame();
// Re-frame the live stream while the user has not overridden the ranges:
// auto_fit_* re-arms auto scale, so it may only run while that is still on.
if (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);
const std::chrono::steady_clock::time_point frame_begin =
std::chrono::steady_clock::now();
ImGui::SetNextWindowPos(ImVec2(24.0f, 24.0f), ImGuiCond_Once);
ImGui::SetNextWindowSize(ImVec2(kWindowWidth, kWindowHeight), ImGuiCond_Once);
// NoBackground: the window's own fill would otherwise be drawn over the
// child's rectangle after the widget has already rendered the plot there.
// This is the host's side of the widget's documented precondition.
ImGui::Begin("plot imgui demo", nullptr, ImGuiWindowFlags_NoBackground);
ImGui::Text("%.3f Msamples/s | %.2f ms/frame | %llu frames | %dx%d fb",
meter.rate * 1.0e-6, meter.frame_ms,
static_cast<unsigned long long>(meter.frames), fb_w, fb_h);
ImGui::SliderFloat("ingest rate", &rate_msamples, 0.05f, 2.0f, "%.2f Msamples/s");
ImGui::SameLine();
if (ImGui::Button("inject spike")) producer.request_spike();
ImGui::Checkbox("LOD statistics overlay", &show_lod_stats);
ImGui::Separator();
producer.set_rate(static_cast<f64>(rate_msamples) * 1.0e6);
// The widget takes the rest of the window, and draws the plot into exactly
// that rectangle of the framebuffer, now, before ImGui renders its chrome.
const ImVec2 plot_origin = ImGui::GetCursorScreenPos();
const ImVec2 plot_size = ImGui::GetContentRegionAvail();
widget.draw("plot", graph, plot::Vec2{plot_size.x, plot_size.y}, *renderer);
if (show_lod_stats) {
// Demo-side overlay: the renderer's own statistics for the frame it just
// drew, painted on top of the plot through ImGui's draw list. Nothing here
// is library API, and the widget is unaware of it.
const plot::FrameStats stats = renderer->stats();
const plot::Rect axes = std::as_const(graph).layout().axes;
const f64 spp = axes.w > 0.0f ? static_cast<f64>(stats.visible_samples) /
static_cast<f64>(axes.w)
: 0.0;
char line_a[160];
char line_b[160];
std::snprintf(line_a, sizeof line_a,
"LOD %s | %llu samples in window | %.0f samples/px | axes %.0fx%.0f",
stats.lod_traces > 0 ? "pyramid" : (stats.raw_traces > 0 ? "raw" : "--"),
static_cast<unsigned long long>(stats.visible_samples), spp,
static_cast<double>(axes.w), static_cast<double>(axes.h));
std::snprintf(line_b, sizeof line_b,
"%u segments | %u draw calls | lod %u raw %u | %.2f ms/frame",
stats.staged_segments, stats.draw_calls, stats.lod_traces, stats.raw_traces,
meter.frame_ms);
const ImVec2 size_a = ImGui::CalcTextSize(line_a);
const ImVec2 size_b = ImGui::CalcTextSize(line_b);
const ImVec2 pad{6.0f, 4.0f};
const ImVec2 box{std::max(size_a.x, size_b.x), size_a.y + size_b.y};
const ImVec2 at{plot_origin.x + 10.0f, plot_origin.y + 10.0f};
ImDrawList* const dl = ImGui::GetWindowDrawList();
dl->AddRectFilled(ImVec2(at.x - pad.x, at.y - pad.y),
ImVec2(at.x + box.x + pad.x, at.y + box.y + pad.y),
IM_COL32(8, 12, 18, 190), 3.0f);
dl->AddText(at, IM_COL32(235, 240, 248, 255), line_a);
dl->AddText(ImVec2(at.x, at.y + size_a.y), IM_COL32(150, 200, 240, 255), line_b);
}
ImGui::End();
ImGui::Render();
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
meter.frames += 1;
const f64 frame_ms =
std::chrono::duration<f64>(std::chrono::steady_clock::now() - frame_begin).count() * 1.0e3;
meter.frame_ms += 0.1 * (frame_ms - meter.frame_ms); // EMA
meter.elapsed += dt;
if (meter.elapsed - meter.last_report >= 0.5) {
const u64 produced = producer.produced();
meter.rate = static_cast<f64>(produced - meter.last_produced) /
(meter.elapsed - meter.last_report);
meter.last_produced = produced;
meter.last_report = meter.elapsed;
const plot::Range xr = graph.x_range(axis_x);
const bool auto_x = graph.x_axis(axis_x).auto_scale;
const bool auto_y = graph.y_axis(axis_y).auto_scale;
std::printf("[imgui_demo] t=%.1fs %.3f Msamples/s %.2f ms/frame %llu frames"
" x=[%.4f, %.4f] s (span %.4f) auto x=%d y=%d\n",
meter.elapsed, meter.rate * 1.0e-6, meter.frame_ms,
static_cast<unsigned long long>(meter.frames), xr.min, xr.max, xr.span(),
auto_x ? 1 : 0, auto_y ? 1 : 0);
std::fflush(stdout);
}
const bool exiting = automated && (glfwGetTime() - start) >= exit_after_s;
if (exiting && screenshot != nullptr && screenshot[0] != '\0') {
const bool dumped = dump_full_framebuffer_ppm(screenshot, fb_w, fb_h);
std::printf("[imgui_demo] screenshot %s: %s\n", screenshot,
dumped ? "written" : "FAILED");
std::fflush(stdout);
}
glfwSwapBuffers(window);
if (exiting) break;
}
// --- teardown, in the order the resources depend on each other --------
int last_fb_w = 0;
int last_fb_h = 0;
int last_window_w = 0;
int last_window_h = 0;
float scale_x = 1.0f;
float scale_y = 1.0f;
glfwGetFramebufferSize(window, &last_fb_w, &last_fb_h);
glfwGetWindowSize(window, &last_window_w, &last_window_h);
glfwGetWindowContentScale(window, &scale_x, &scale_y);
const plot::FrameStats last_stats = renderer->stats();
const f64 dense_spp = renderer->style().dense_threshold_spp;
const bool persistent_upload = renderer->using_persistent_upload();
const bool gl_error = renderer->last_frame_had_gl_error();
producer.stop(); // no thread pushes into the stream after this point
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplGlfw_Shutdown();
ImGui::DestroyContext();
// The renderer's RAII handles call glDelete*, so the context must be current --
// and alive -- when they run.
renderer.reset();
glfwDestroyWindow(window);
glfwTerminate();
const f64 mean_ms =
meter.frames > 0 ? meter.elapsed / static_cast<f64>(meter.frames) * 1.0e3 : 0.0;
std::printf("\n[imgui_demo] ---- run summary ----\n");
std::printf("[imgui_demo] producer %llu samples pushed, last metered %.3f Msamples/s\n",
static_cast<unsigned long long>(producer.produced()), meter.rate * 1.0e-6);
std::printf("[imgui_demo] frames %llu in %.3f s, mean %.2f ms/frame (%.1f fps),"
" last metered %.2f ms/frame\n",
static_cast<unsigned long long>(meter.frames), meter.elapsed, mean_ms,
mean_ms > 0.0 ? 1.0e3 / mean_ms : 0.0, meter.frame_ms);
const plot::Rect last_axes = std::as_const(graph).layout().axes;
const f64 last_spp = last_axes.w > 0.0f ? static_cast<f64>(last_stats.visible_samples) /
static_cast<f64>(last_axes.w)
: 0.0;
std::printf("[imgui_demo] retained %llu samples, x [%.6f, %.6f] s\n",
static_cast<unsigned long long>(stream.size()), stream.x_bounds().min,
stream.x_bounds().max);
std::printf("[imgui_demo] last frame %.1f samples/px (dense threshold %.1f) -> %s\n", last_spp,
dense_spp, last_stats.lod_traces > 0 ? "LOD pyramid" : "raw samples");
std::printf("[imgui_demo] last frame visible_samples %llu, axes %.0fx%.0f, staged_segments %u,"
" draw_calls %u, lod %u raw %u\n",
static_cast<unsigned long long>(last_stats.visible_samples),
static_cast<double>(last_axes.w), static_cast<double>(last_axes.h),
last_stats.staged_segments, last_stats.draw_calls, last_stats.lod_traces,
last_stats.raw_traces);
std::printf("[imgui_demo] pixel spaces framebuffer %dx%d, window %dx%d, content scale %.2fx%.2f\n",
last_fb_w, last_fb_h, last_window_w, last_window_h, static_cast<double>(scale_x),
static_cast<double>(scale_y));
std::printf("[imgui_demo] upload path %s, gl error on the last frame %d\n",
persistent_upload ? "persistent-mapped ring" : "orphan", gl_error ? 1 : 0);
return 0;
}