// 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 : that header's macros // would rewrite the enumerators of plot/gl/api.hpp, which this file includes. #define GLFW_INCLUDE_NONE #include #include #include #include #include #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; /// 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(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 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(clock::now() - start).count(); const f64 rate = rate_.load(std::memory_order_relaxed); const u64 due = static_cast(elapsed * rate) + 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); } if (n > 0 && spikes_.load(std::memory_order_relaxed) > 0) { batch_[static_cast(n / 2)] += static_cast(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(next) + static_cast(kBatch)) / rate; const f64 sleep_s = wake_s < kMaxSleepS ? wake_s : kMaxSleepS; std::this_thread::sleep_until( start + std::chrono::duration_cast(std::chrono::duration(sleep_s))); } } plot::data::StreamSeries* series_; std::vector batch_; std::atomic rate_{1.0e6}; std::atomic produced_{0}; std::atomic spikes_{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 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(fb_w); const plot::usize h = static_cast(fb_h); std::vector 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 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 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}); graph.add_trace({&stream, kTraceColor, 1.5f, axis_x, axis_y}); 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(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(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(stats.visible_samples) / static_cast(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(stats.visible_samples), spp, static_cast(axes.w), static_cast(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(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(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(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(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(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(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(last_stats.visible_samples) / static_cast(last_axes.w) : 0.0; std::printf("[imgui_demo] retained %llu samples, x [%.6f, %.6f] s\n", static_cast(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(last_stats.visible_samples), static_cast(last_axes.w), static_cast(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(scale_x), static_cast(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; }