feat(imgui): embeddable plot widget and demo
This commit is contained in:
1 parent
fb2c2f5e4d
commit
15931a2405
9 files changed
+1072
-38
No files matched your search
@@ -53,6 +53,10 @@ if(PLOT_BUILD_GLFW)
|
|||||||
add_subdirectory(adapters/glfw)
|
add_subdirectory(adapters/glfw)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if(PLOT_BUILD_IMGUI)
|
||||||
|
add_subdirectory(adapters/imgui)
|
||||||
|
endif()
|
||||||
|
|
||||||
if(PLOT_BUILD_EXAMPLES)
|
if(PLOT_BUILD_EXAMPLES)
|
||||||
add_subdirectory(examples)
|
add_subdirectory(examples)
|
||||||
endif()
|
endif()
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
"cacheVariables": {
|
"cacheVariables": {
|
||||||
"CMAKE_BUILD_TYPE": "Debug",
|
"CMAKE_BUILD_TYPE": "Debug",
|
||||||
"PLOT_BUILD_GLFW": true,
|
"PLOT_BUILD_GLFW": true,
|
||||||
|
"PLOT_BUILD_IMGUI": true,
|
||||||
"PLOT_BUILD_EXAMPLES": true
|
"PLOT_BUILD_EXAMPLES": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -25,6 +26,7 @@
|
|||||||
"cacheVariables": {
|
"cacheVariables": {
|
||||||
"CMAKE_BUILD_TYPE": "Release",
|
"CMAKE_BUILD_TYPE": "Release",
|
||||||
"PLOT_BUILD_GLFW": true,
|
"PLOT_BUILD_GLFW": true,
|
||||||
|
"PLOT_BUILD_IMGUI": true,
|
||||||
"PLOT_BUILD_EXAMPLES": true
|
"PLOT_BUILD_EXAMPLES": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,44 +1,10 @@
|
|||||||
# The GLFW adapter: the reference host for the library. It owns the window, the
|
# The GLFW adapter: the reference host for the library. It owns the window, the
|
||||||
# context and the loop, and translates GLFW's events into plot::input events.
|
# context and the loop, and translates GLFW's events into plot::input events.
|
||||||
#
|
#
|
||||||
# Dependency discovery follows the design spec's order: find_package first,
|
# GLFW is resolved by the shared plot_find_glfw() (cmake/plot_options.cmake), so a
|
||||||
# pkg-config next, FetchContent only when neither can supply GLFW (the last
|
# build that also enables the Dear ImGui adapter -- which needs GLFW for
|
||||||
# resort, because it needs the network).
|
# imgui_impl_glfw -- discovers (and at worst fetches) a single copy.
|
||||||
set(plot_glfw_dep "")
|
plot_find_glfw(plot_glfw_dep)
|
||||||
|
|
||||||
find_package(glfw3 3.3 QUIET)
|
|
||||||
if(TARGET glfw)
|
|
||||||
set(plot_glfw_dep glfw)
|
|
||||||
elseif(TARGET glfw3::glfw3)
|
|
||||||
set(plot_glfw_dep glfw3::glfw3)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if(NOT plot_glfw_dep)
|
|
||||||
find_package(PkgConfig QUIET)
|
|
||||||
if(PkgConfig_FOUND)
|
|
||||||
pkg_check_modules(GLFW_PC QUIET IMPORTED_TARGET glfw3)
|
|
||||||
if(TARGET PkgConfig::GLFW_PC)
|
|
||||||
set(plot_glfw_dep PkgConfig::GLFW_PC)
|
|
||||||
endif()
|
|
||||||
endif()
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if(NOT plot_glfw_dep)
|
|
||||||
message(STATUS "plot: glfw3 not found via find_package or pkg-config; fetching it")
|
|
||||||
include(FetchContent)
|
|
||||||
set(GLFW_BUILD_DOCS OFF CACHE BOOL "" FORCE)
|
|
||||||
set(GLFW_BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
|
||||||
set(GLFW_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
|
|
||||||
set(GLFW_INSTALL OFF CACHE BOOL "" FORCE)
|
|
||||||
FetchContent_Declare(glfw
|
|
||||||
GIT_REPOSITORY https://github.com/glfw/glfw.git
|
|
||||||
GIT_TAG 3.4
|
|
||||||
GIT_SHALLOW TRUE)
|
|
||||||
FetchContent_MakeAvailable(glfw)
|
|
||||||
set(plot_glfw_dep glfw)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
message(STATUS "plot: GLFW adapter dependency: ${plot_glfw_dep}")
|
|
||||||
|
|
||||||
add_library(plot_glfw STATIC glfw_host.cpp)
|
add_library(plot_glfw STATIC glfw_host.cpp)
|
||||||
add_library(plot::glfw ALIAS plot_glfw)
|
add_library(plot::glfw ALIAS plot_glfw)
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# The Dear ImGui adapter: a widget that draws a plot into a child region of the
|
||||||
|
# framebuffer ImGui is rendering its own chrome into, and feeds that plot
|
||||||
|
# ImGui's mouse and keyboard state.
|
||||||
|
#
|
||||||
|
# ImGui is not a packaging dependency here: it is fetched pinned to a release
|
||||||
|
# tag (never a branch, so a build is reproducible) and only the sources this
|
||||||
|
# adapter needs are compiled -- the core plus the GLFW and OpenGL 3 backends.
|
||||||
|
# The fetched sources are third-party code: they are not held to this project's
|
||||||
|
# warning set, and the adapter and its example are.
|
||||||
|
include(FetchContent)
|
||||||
|
FetchContent_Declare(imgui
|
||||||
|
GIT_REPOSITORY https://github.com/ocornut/imgui.git
|
||||||
|
GIT_TAG v1.91.9b
|
||||||
|
GIT_SHALLOW TRUE)
|
||||||
|
# imgui has no top-level CMakeLists.txt, so MakeAvailable populates it and stops;
|
||||||
|
# the sources are added to the target below by name.
|
||||||
|
FetchContent_MakeAvailable(imgui)
|
||||||
|
|
||||||
|
if(NOT imgui_SOURCE_DIR OR NOT EXISTS "${imgui_SOURCE_DIR}/imgui.cpp")
|
||||||
|
message(FATAL_ERROR
|
||||||
|
"plot: the Dear ImGui sources for tag v1.91.9b are missing from "
|
||||||
|
"'${imgui_SOURCE_DIR}'. The FetchContent clone failed, so PLOT_BUILD_IMGUI "
|
||||||
|
"cannot link anything; either restore network access or pre-populate the "
|
||||||
|
"source with -DFETCHCONTENT_SOURCE_DIR_IMGUI=<path>.")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_library(imgui STATIC
|
||||||
|
"${imgui_SOURCE_DIR}/imgui.cpp"
|
||||||
|
"${imgui_SOURCE_DIR}/imgui_draw.cpp"
|
||||||
|
"${imgui_SOURCE_DIR}/imgui_tables.cpp"
|
||||||
|
"${imgui_SOURCE_DIR}/imgui_widgets.cpp"
|
||||||
|
"${imgui_SOURCE_DIR}/backends/imgui_impl_glfw.cpp"
|
||||||
|
"${imgui_SOURCE_DIR}/backends/imgui_impl_opengl3.cpp")
|
||||||
|
# Both include roots are public: a consumer of the backends -- the adapter and
|
||||||
|
# the example -- reaches them as <imgui.h> and <backends/imgui_impl_glfw.h>.
|
||||||
|
target_include_directories(imgui PUBLIC
|
||||||
|
"${imgui_SOURCE_DIR}"
|
||||||
|
"${imgui_SOURCE_DIR}/backends")
|
||||||
|
|
||||||
|
# imgui_impl_glfw.cpp needs GLFW; a build that also enables the GLFW adapter
|
||||||
|
# gets the same target, never a second copy.
|
||||||
|
plot_find_glfw(plot_imgui_glfw_dep)
|
||||||
|
target_link_libraries(imgui PUBLIC ${plot_imgui_glfw_dep})
|
||||||
|
target_compile_features(imgui PUBLIC cxx_std_20)
|
||||||
|
|
||||||
|
add_library(plot_imgui STATIC imgui_plot.cpp)
|
||||||
|
add_library(plot::imgui ALIAS plot_imgui)
|
||||||
|
# The public include root is `adapters/`, so the header is reached as
|
||||||
|
# <imgui/imgui_plot.hpp> -- the toolkit's name first, like every other adapter's.
|
||||||
|
target_include_directories(plot_imgui PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/..")
|
||||||
|
# ImGui stays PRIVATE: the adapter's header includes no ImGui header, so a
|
||||||
|
# consumer of plot::imgui does not inherit the toolkit's headers.
|
||||||
|
target_link_libraries(plot_imgui PUBLIC plot::core PRIVATE imgui ${plot_imgui_glfw_dep})
|
||||||
|
plot_apply_warnings(plot_imgui)
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
#include <imgui/imgui_plot.hpp>
|
||||||
|
|
||||||
|
#include <imgui.h>
|
||||||
|
|
||||||
|
namespace plot::imgui {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
/// ImGui's button indices are left, right, middle; the controller's enum is the
|
||||||
|
/// same three, spelled out.
|
||||||
|
input::Button button_from_index(int index) noexcept {
|
||||||
|
switch (index) {
|
||||||
|
case 1:
|
||||||
|
return input::Button::Right;
|
||||||
|
case 2:
|
||||||
|
return input::Button::Middle;
|
||||||
|
default:
|
||||||
|
return input::Button::Left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ImGui's modifier booleans as the controller's translatable mask. The bit
|
||||||
|
/// values are `input::Mod`'s by definition: it is documented as the mask a host
|
||||||
|
/// translates into, and the GLFW adapter's static_asserts pin the same layout.
|
||||||
|
u32 mods_from(const ImGuiIO& io) noexcept {
|
||||||
|
u32 mods = 0;
|
||||||
|
if (io.KeyShift) mods |= static_cast<u32>(input::Mod::Shift);
|
||||||
|
if (io.KeyCtrl) mods |= static_cast<u32>(input::Mod::Ctrl);
|
||||||
|
if (io.KeyAlt) mods |= static_cast<u32>(input::Mod::Alt);
|
||||||
|
if (io.KeySuper) mods |= static_cast<u32>(input::Mod::Super);
|
||||||
|
return mods;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void PlotWidget::draw(const char* id, Plot& plot, Vec2 size, Renderer& renderer) {
|
||||||
|
ImGuiIO& io = ImGui::GetIO();
|
||||||
|
|
||||||
|
if (!controller_ || plot_ != &plot) {
|
||||||
|
// The controller holds a Plot& (and the drag state that goes with it), so a
|
||||||
|
// different plot is a different controller; re-binding drops any drag rather
|
||||||
|
// than replaying its edges against the new plot.
|
||||||
|
controller_.emplace(plot);
|
||||||
|
plot_ = &plot;
|
||||||
|
reset_pointer();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The whole translation below rests on the backend having filled in a sane
|
||||||
|
// scale; a zero one would collapse the region and silently draw nothing.
|
||||||
|
IM_ASSERT(io.DisplayFramebufferScale.x > 0.0f && io.DisplayFramebufferScale.y > 0.0f);
|
||||||
|
|
||||||
|
// The child is a plot surface, not a scrollable panel: no scrollbar, whatever
|
||||||
|
// the item inside it measures. The wheel belongs to the plot while it is
|
||||||
|
// hovered, and is taken away from window scrolling by the key owner below.
|
||||||
|
const ImGuiWindowFlags child_flags = ImGuiWindowFlags_NoScrollbar;
|
||||||
|
|
||||||
|
// The child is the plot's surface: it must paint nothing of its own over it.
|
||||||
|
// ChildBg is transparent in every stock theme, but a host may have pushed a
|
||||||
|
// panel colour; forcing it here means the widget can never cover its own plot.
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.0f, 0.0f, 0.0f, 0.0f));
|
||||||
|
const bool shown =
|
||||||
|
ImGui::BeginChild(id, ImVec2(size.x, size.y), ImGuiChildFlags_None, child_flags);
|
||||||
|
if (!shown) {
|
||||||
|
// Clipped away entirely (or collapsed): there is no region to draw into, and
|
||||||
|
// no rect to aim a gesture at. EndChild is still owed.
|
||||||
|
ImGui::EndChild();
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A borderless child has no padding and no border, so the content region is
|
||||||
|
// the child's rectangle itself: the pixels the plot draws are exactly the ones
|
||||||
|
// the host laid out for it.
|
||||||
|
const ImVec2 origin = ImGui::GetCursorScreenPos();
|
||||||
|
const ImVec2 avail = ImGui::GetContentRegionAvail();
|
||||||
|
if (avail.x <= 0.0f || avail.y <= 0.0f) {
|
||||||
|
// A child with no room (a degenerate layout): there is no region to draw
|
||||||
|
// into and no rect to aim a gesture at, and InvisibleButton wants a size.
|
||||||
|
ImGui::EndChild();
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// An invisible item covering the region is what makes it *the* hovered thing:
|
||||||
|
// `IsItemHovered()` is false while a drag elsewhere in the host is active, so
|
||||||
|
// the plot does not pan when the user drags a slider next to it, and
|
||||||
|
// `SetItemKeyOwner()` -- ImGui's documented way for a widget to take the wheel
|
||||||
|
// away from window scrolling -- needs an item to own it. Both buttons are
|
||||||
|
// enabled: the controller's left and right drags are the plot's own, and ImGui
|
||||||
|
// only needs to know that the pointer is interacting with this region.
|
||||||
|
ImGui::InvisibleButton("surface", ImVec2(avail.x, avail.y),
|
||||||
|
ImGuiButtonFlags_MouseButtonLeft | ImGuiButtonFlags_MouseButtonRight |
|
||||||
|
ImGuiButtonFlags_MouseButtonMiddle);
|
||||||
|
const bool hovered = ImGui::IsItemHovered();
|
||||||
|
ImGui::SetItemKeyOwner(ImGuiKey_MouseWheelY);
|
||||||
|
|
||||||
|
const ImVec2 scale = io.DisplayFramebufferScale;
|
||||||
|
const f32 fb_w_f = io.DisplaySize.x * scale.x;
|
||||||
|
const f32 fb_h_f = io.DisplaySize.y * scale.y;
|
||||||
|
// Points -> framebuffer pixels: the assumption documented on the class. State
|
||||||
|
// it in the arithmetic so a reader sees where the mapping enters.
|
||||||
|
const Rect region{origin.x * scale.x, origin.y * scale.y, avail.x * scale.x, avail.y * scale.y};
|
||||||
|
|
||||||
|
if (region.w > 0.0f && region.h > 0.0f && fb_w_f >= 1.0f && fb_h_f >= 1.0f) {
|
||||||
|
// One viewport for the gesture geometry and the frame: the controller
|
||||||
|
// forwards it to the plot (and re-lays it out), and `begin_frame` gets the
|
||||||
|
// same rect and the whole framebuffer it is a sub-rectangle of.
|
||||||
|
controller_->set_viewport(region);
|
||||||
|
forward_input(hovered);
|
||||||
|
|
||||||
|
const u32 fb_w = static_cast<u32>(fb_w_f + 0.5f);
|
||||||
|
const u32 fb_h = static_cast<u32>(fb_h_f + 0.5f);
|
||||||
|
renderer.begin_frame(fb_w, fb_h, region);
|
||||||
|
renderer.draw(plot);
|
||||||
|
renderer.end_frame();
|
||||||
|
// `end_frame` leaves the GL state neutral and the viewport at the plot's
|
||||||
|
// rect; ImGui's backend sets its own viewport, blend function and so on
|
||||||
|
// before its first draw, so the chrome renders correctly after this.
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::EndChild();
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlotWidget::forward_input(bool hovered) {
|
||||||
|
ImGuiIO& io = ImGui::GetIO();
|
||||||
|
const ImVec2 scale = io.DisplayFramebufferScale;
|
||||||
|
|
||||||
|
const f32 px = io.MousePos.x * scale.x;
|
||||||
|
const f32 py = io.MousePos.y * scale.y;
|
||||||
|
const bool down[3] = {io.MouseDown[0], io.MouseDown[1], io.MouseDown[2]};
|
||||||
|
const bool any_down = down[0] || down[1] || down[2];
|
||||||
|
const u32 mods = mods_from(io);
|
||||||
|
|
||||||
|
if (!hovered && !capturing_) {
|
||||||
|
// Nowhere near the plot: remember the levels anyway, so re-entering the
|
||||||
|
// region does not synthesise an edge out of the state it left with.
|
||||||
|
prev_down_[0] = down[0];
|
||||||
|
prev_down_[1] = down[1];
|
||||||
|
prev_down_[2] = down[2];
|
||||||
|
prev_x_ = px;
|
||||||
|
prev_y_ = py;
|
||||||
|
have_prev_pos_ = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The movement since the previous frame is what a pan consumes; it rides the
|
||||||
|
// first event of this frame and nothing else, so one frame's motion can never
|
||||||
|
// be counted twice.
|
||||||
|
const f32 dx = have_prev_pos_ ? px - prev_x_ : 0.0f;
|
||||||
|
const f32 dy = have_prev_pos_ ? py - prev_y_ : 0.0f;
|
||||||
|
bool delta_pending = true;
|
||||||
|
|
||||||
|
const auto deliver = [&](input::Button button, bool pressed, bool released) {
|
||||||
|
input::PointerEvent e;
|
||||||
|
e.x = px;
|
||||||
|
e.y = py;
|
||||||
|
if (delta_pending) {
|
||||||
|
e.dx = dx;
|
||||||
|
e.dy = dy;
|
||||||
|
delta_pending = false;
|
||||||
|
}
|
||||||
|
e.button = button;
|
||||||
|
e.mods = mods;
|
||||||
|
e.down = any_down;
|
||||||
|
e.pressed = pressed;
|
||||||
|
e.released = released;
|
||||||
|
controller_->on_pointer(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ImGui reports levels; the controller wants edges. One event per button whose
|
||||||
|
// level changed, which is the press or the release the host's callback would
|
||||||
|
// have carried.
|
||||||
|
for (int i = 0; i < 3; ++i) {
|
||||||
|
if (down[i] == prev_down_[i]) continue;
|
||||||
|
deliver(button_from_index(i), down[i], !down[i]);
|
||||||
|
}
|
||||||
|
if (delta_pending) {
|
||||||
|
// No transition this frame: one plain move sample, so a pan in progress
|
||||||
|
// advances by exactly the movement since the last one and
|
||||||
|
// Controller::hovered_cursor() sees where the pointer is.
|
||||||
|
deliver(input::Button::Left, false, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hovered && io.MouseWheel != 0.0f) {
|
||||||
|
input::WheelEvent e;
|
||||||
|
e.x = px;
|
||||||
|
e.y = py;
|
||||||
|
e.dx = io.MouseWheelH;
|
||||||
|
e.dy = io.MouseWheel; // ImGui's positive y (scroll up) is the zoom-in sign
|
||||||
|
e.mods = mods;
|
||||||
|
controller_->on_wheel(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keyboard gestures, on the same hover gate as the pointer: a key pressed
|
||||||
|
// while the plot is under the pointer is aimed at the plot. `+` on most
|
||||||
|
// layouts is shift+`=`, and either spelling zooms in, as in the GLFW adapter.
|
||||||
|
if (hovered && !io.WantTextInput) {
|
||||||
|
const auto key = [&](int code) {
|
||||||
|
input::KeyEvent e;
|
||||||
|
e.key = code;
|
||||||
|
e.mods = mods;
|
||||||
|
e.down = true; // ImGui already edge-detected the press
|
||||||
|
controller_->on_key(e);
|
||||||
|
};
|
||||||
|
if (ImGui::IsKeyPressed(ImGuiKey_Equal, false) ||
|
||||||
|
ImGui::IsKeyPressed(ImGuiKey_KeypadAdd, false)) {
|
||||||
|
key(input::kKeyZoomIn);
|
||||||
|
}
|
||||||
|
if (ImGui::IsKeyPressed(ImGuiKey_Minus, false) ||
|
||||||
|
ImGui::IsKeyPressed(ImGuiKey_KeypadSubtract, false)) {
|
||||||
|
key(input::kKeyZoomOut);
|
||||||
|
}
|
||||||
|
if (ImGui::IsKeyPressed(ImGuiKey_F, false)) key(input::kKeyFit);
|
||||||
|
}
|
||||||
|
|
||||||
|
prev_down_[0] = down[0];
|
||||||
|
prev_down_[1] = down[1];
|
||||||
|
prev_down_[2] = down[2];
|
||||||
|
prev_x_ = px;
|
||||||
|
prev_y_ = py;
|
||||||
|
have_prev_pos_ = true;
|
||||||
|
// Capture: a press delivered while the pointer was over the child keeps the
|
||||||
|
// release flowing even once the pointer has left it.
|
||||||
|
capturing_ = any_down && (capturing_ || hovered);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlotWidget::reset_pointer() noexcept {
|
||||||
|
prev_down_[0] = false;
|
||||||
|
prev_down_[1] = false;
|
||||||
|
prev_down_[2] = false;
|
||||||
|
prev_x_ = 0.0f;
|
||||||
|
prev_y_ = 0.0f;
|
||||||
|
have_prev_pos_ = false;
|
||||||
|
capturing_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace plot::imgui
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
#include <plot/core/rect.hpp>
|
||||||
|
#include <plot/input/controller.hpp>
|
||||||
|
#include <plot/math/vec2.hpp>
|
||||||
|
#include <plot/plot/plot.hpp>
|
||||||
|
#include <plot/render/renderer.hpp>
|
||||||
|
|
||||||
|
namespace plot::imgui {
|
||||||
|
|
||||||
|
/// Embeds a `Plot` in a Dear ImGui window as a child region, and drives that plot
|
||||||
|
/// from ImGui's own input state.
|
||||||
|
///
|
||||||
|
/// `draw()` opens a child window of the requested size, translates its screen
|
||||||
|
/// rectangle into framebuffer pixels, hands that rectangle to
|
||||||
|
/// `Renderer::begin_frame()` and draws the plot into it: a scissor- and
|
||||||
|
/// viewport-bounded sub-rectangle of the very framebuffer ImGui is about to draw
|
||||||
|
/// its chrome into, not an offscreen texture. Nothing is rendered through a
|
||||||
|
/// texture or an FBO, and ImGui's frame is left undisturbed -- the renderer
|
||||||
|
/// clears only its own rect and restores the GL state it changed, and ImGui's
|
||||||
|
/// backend re-establishes its viewport and blend state before its first draw.
|
||||||
|
///
|
||||||
|
/// Two pixel spaces meet here, exactly as they do in the GLFW adapter. ImGui
|
||||||
|
/// lays out and reports in logical *points* -- `io.DisplaySize`, the cursor's
|
||||||
|
/// screen position, a child's content region -- while this library speaks
|
||||||
|
/// *framebuffer pixels*. The bridge is `io.DisplayFramebufferScale`, which the
|
||||||
|
/// platform backend fills in from the window and framebuffer sizes; on an
|
||||||
|
/// unscaled display it is (1, 1) and nothing changes.
|
||||||
|
///
|
||||||
|
/// ## The coordinate assumption
|
||||||
|
///
|
||||||
|
/// Points -> pixels through `io.DisplayFramebufferScale` is *the whole
|
||||||
|
/// framebuffer's* mapping only when both of these hold:
|
||||||
|
///
|
||||||
|
/// * the GL viewport the host is drawing its frame with is the whole
|
||||||
|
/// framebuffer -- origin (0, 0), size `DisplaySize * DisplayFramebufferScale`
|
||||||
|
/// -- and
|
||||||
|
/// * the bound draw target is the default framebuffer, not an offscreen one.
|
||||||
|
///
|
||||||
|
/// Under that assumption -- the one every ImGui platform backend documents, and
|
||||||
|
/// what a window that spans its framebuffer gives -- the child's point rectangle
|
||||||
|
/// is the pixel rectangle the plot is laid out and scissored into, and a gesture
|
||||||
|
/// lands on the pixel it was aimed at. A host that renders into an FBO, or with
|
||||||
|
/// a viewport that is not the whole framebuffer (a tiled compositor, a scaled
|
||||||
|
/// surface, a pass into a texture), breaks the assumption. This widget cannot
|
||||||
|
/// detect it: it has no view of the host's viewport and no way to ask. Such a
|
||||||
|
/// host must place the plot itself -- `Renderer::begin_frame()` takes the
|
||||||
|
/// framebuffer size and the plot's rect directly, so it needs no widget -- or
|
||||||
|
/// apply its own points -> pixels mapping on top of `plot.set_viewport()`.
|
||||||
|
///
|
||||||
|
/// ## What the widget draws over
|
||||||
|
///
|
||||||
|
/// The widget renders *immediately*, from inside the child, before ImGui renders
|
||||||
|
/// its draw data (see the frame order in the demo). It therefore writes the
|
||||||
|
/// child's pixels before every ImGui command of that frame, and ImGui has no way
|
||||||
|
/// to be told the child is opaque. The host must not paint over the child's
|
||||||
|
/// rectangle:
|
||||||
|
///
|
||||||
|
/// * the hosting window must not fill its background across the child -- use
|
||||||
|
/// `ImGui::SetNextWindowBgAlpha(0.0f)` or `ImGuiWindowFlags_NoBackground`
|
||||||
|
/// (`ImGuiCol_WindowBg`'s default alpha would otherwise dim the plot), and
|
||||||
|
/// * no other window may overlap the rectangle with opaque content.
|
||||||
|
///
|
||||||
|
/// The child's own background is forced transparent by the widget, whatever the
|
||||||
|
/// host's `ImGuiCol_ChildBg`, so the widget itself never paints over the plot.
|
||||||
|
///
|
||||||
|
/// ## Input
|
||||||
|
///
|
||||||
|
/// Pointer, wheel and key state are translated into `plot::input` events and
|
||||||
|
/// delivered to the plot's `Controller` -- the one the widget owns, reachable
|
||||||
|
/// through `controller()`. ImGui reports levels (`io.MouseDown`) where the
|
||||||
|
/// controller wants edges (`PointerEvent::pressed`/`released`), so the widget
|
||||||
|
/// remembers the previous frame's buttons and synthesises the transitions.
|
||||||
|
///
|
||||||
|
/// Events are forwarded while an invisible item covering the child's region is
|
||||||
|
/// hovered -- `IsItemHovered()` rather than `IsWindowHovered()`, because the
|
||||||
|
/// item is the child's whole rectangle (the child has no padding) and its hover
|
||||||
|
/// is false while the host is dragging some other widget, so dragging a slider
|
||||||
|
/// next to the plot cannot pan it. A press that started over the child keeps
|
||||||
|
/// being delivered until every button is up again, so a drag that leaves the
|
||||||
|
/// region still ends instead of leaving the plot mid-pan.
|
||||||
|
///
|
||||||
|
/// Wheel input goes to the plot while it is hovered, and the plot's zoom wins
|
||||||
|
/// over ImGui's window scrolling: `SetItemKeyOwner(ImGuiKey_MouseWheelY)` on that
|
||||||
|
/// invisible item makes ImGui withhold the wheel from the hosting window. The
|
||||||
|
/// claim takes effect from the frame after it is made, because ImGui decides
|
||||||
|
/// window scrolling in its own `NewFrame()` before any widget runs -- so a
|
||||||
|
/// hosting window with a scroll range can still scroll on the very first wheel of
|
||||||
|
/// a hover that has only just started. (The child's
|
||||||
|
/// `ImGuiWindowFlags_NoScrollWithMouse` would not achieve this on its own: ImGui
|
||||||
|
/// deliberately bubbles the wheel up to the parent when a child refuses it.)
|
||||||
|
/// ImGui consumes `io.MouseWheel` itself at the end of the frame, so a widget
|
||||||
|
/// cannot consume it any later than this.
|
||||||
|
///
|
||||||
|
/// Not thread-safe: like everything else in the render thread's frame.
|
||||||
|
class PlotWidget {
|
||||||
|
public:
|
||||||
|
PlotWidget() noexcept = default;
|
||||||
|
PlotWidget(const PlotWidget&) = delete;
|
||||||
|
PlotWidget& operator=(const PlotWidget&) = delete;
|
||||||
|
|
||||||
|
/// Allocates a child region of `size` (ImGui points) identified by `id`, and
|
||||||
|
/// renders `plot` into the framebuffer pixels it covers with `renderer`, which
|
||||||
|
/// must already be initialised on the current context.
|
||||||
|
///
|
||||||
|
/// Must be called between `ImGui::NewFrame()` and `ImGui::Render()`, from a
|
||||||
|
/// window; the plot is drawn (GL calls issued) during the call, inside the
|
||||||
|
/// child, so the host's later ImGui commands never have to restore anything.
|
||||||
|
///
|
||||||
|
/// The plot's viewport is the child's content rectangle, so it fills the child
|
||||||
|
/// exactly; the widget sets it every frame through the controller, and the plot
|
||||||
|
/// is re-laid out to match. A degenerate region (a collapsed or clipped child)
|
||||||
|
/// draws nothing and forwards nothing.
|
||||||
|
void draw(const char* id, Plot& plot, Vec2 size, Renderer& renderer);
|
||||||
|
|
||||||
|
/// The controller the widget feeds, or null before the first `draw()`. It is
|
||||||
|
/// bound to whichever plot the last `draw()` was given; a `draw()` for a
|
||||||
|
/// different plot re-binds it and drops any drag in progress.
|
||||||
|
input::Controller* controller() noexcept { return controller_ ? &*controller_ : nullptr; }
|
||||||
|
const input::Controller* controller() const noexcept {
|
||||||
|
return controller_ ? &*controller_ : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
/// Synthesises this frame's `plot::input` events from ImGui's levels.
|
||||||
|
void forward_input(bool hovered);
|
||||||
|
|
||||||
|
/// Forgets the previous frame's pointer state, so a re-bound plot cannot see an
|
||||||
|
/// edge that belongs to the old one.
|
||||||
|
void reset_pointer() noexcept;
|
||||||
|
|
||||||
|
Plot* plot_ = nullptr;
|
||||||
|
std::optional<input::Controller> controller_;
|
||||||
|
|
||||||
|
/// The previous frame's button levels, in ImGui's order (left, right, middle).
|
||||||
|
bool prev_down_[3] = {false, false, false};
|
||||||
|
f32 prev_x_ = 0.0f; ///< previous pointer position, in framebuffer pixels
|
||||||
|
f32 prev_y_ = 0.0f;
|
||||||
|
bool have_prev_pos_ = false;
|
||||||
|
/// True while a button pressed over the child is still down: keeps the release
|
||||||
|
/// flowing even after the pointer has left the region.
|
||||||
|
bool capturing_ = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace plot::imgui
|
||||||
@@ -31,3 +31,53 @@ function(plot_apply_warnings target)
|
|||||||
-Wshadow)
|
-Wshadow)
|
||||||
endif()
|
endif()
|
||||||
endfunction()
|
endfunction()
|
||||||
|
|
||||||
|
# Resolves GLFW once for every target that needs it -- the GLFW host adapter and
|
||||||
|
# the Dear ImGui backend both do. Discovery follows the design spec's order:
|
||||||
|
# find_package first, pkg-config next, FetchContent only when neither can supply
|
||||||
|
# GLFW (the last resort, because it needs the network). The result is cached in
|
||||||
|
# PLOT_GLFW_DEP, so a build with several adapters enabled discovers -- and at
|
||||||
|
# worst fetches -- exactly one copy. Sets <out_var> to the target to link.
|
||||||
|
function(plot_find_glfw out_var)
|
||||||
|
if(NOT DEFINED PLOT_GLFW_DEP)
|
||||||
|
set(plot_glfw_dep "")
|
||||||
|
|
||||||
|
find_package(glfw3 3.3 QUIET)
|
||||||
|
if(TARGET glfw)
|
||||||
|
set(plot_glfw_dep glfw)
|
||||||
|
elseif(TARGET glfw3::glfw3)
|
||||||
|
set(plot_glfw_dep glfw3::glfw3)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(NOT plot_glfw_dep)
|
||||||
|
find_package(PkgConfig QUIET)
|
||||||
|
if(PkgConfig_FOUND)
|
||||||
|
pkg_check_modules(GLFW_PC QUIET IMPORTED_TARGET glfw3)
|
||||||
|
if(TARGET PkgConfig::GLFW_PC)
|
||||||
|
set(plot_glfw_dep PkgConfig::GLFW_PC)
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(NOT plot_glfw_dep)
|
||||||
|
message(STATUS "plot: glfw3 not found via find_package or pkg-config; fetching it")
|
||||||
|
include(FetchContent)
|
||||||
|
set(GLFW_BUILD_DOCS OFF CACHE BOOL "" FORCE)
|
||||||
|
set(GLFW_BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
||||||
|
set(GLFW_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
|
||||||
|
set(GLFW_INSTALL OFF CACHE BOOL "" FORCE)
|
||||||
|
FetchContent_Declare(glfw
|
||||||
|
GIT_REPOSITORY https://github.com/glfw/glfw.git
|
||||||
|
GIT_TAG 3.4
|
||||||
|
GIT_SHALLOW TRUE)
|
||||||
|
FetchContent_MakeAvailable(glfw)
|
||||||
|
set(plot_glfw_dep glfw)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(PLOT_GLFW_DEP "${plot_glfw_dep}" CACHE INTERNAL
|
||||||
|
"GLFW target the plot adapters link against")
|
||||||
|
message(STATUS "plot: GLFW adapter dependency: ${plot_glfw_dep}")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(${out_var} "${PLOT_GLFW_DEP}" PARENT_SCOPE)
|
||||||
|
endfunction()
|
||||||
@@ -8,3 +8,16 @@ if(TARGET plot::glfw)
|
|||||||
target_link_libraries(plot_glfw_demo PRIVATE plot::glfw Threads::Threads)
|
target_link_libraries(plot_glfw_demo PRIVATE plot::glfw Threads::Threads)
|
||||||
plot_apply_warnings(plot_glfw_demo)
|
plot_apply_warnings(plot_glfw_demo)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if(TARGET plot::imgui)
|
||||||
|
# The demo owns the window, the ImGui context and the frame loop itself, so it
|
||||||
|
# links the toolkit and its backends directly; the adapter is only the widget.
|
||||||
|
plot_find_glfw(plot_imgui_demo_glfw_dep)
|
||||||
|
add_executable(plot_imgui_demo imgui_demo.cpp)
|
||||||
|
target_link_libraries(plot_imgui_demo PRIVATE
|
||||||
|
plot::imgui
|
||||||
|
imgui
|
||||||
|
${plot_imgui_demo_glfw_dep}
|
||||||
|
Threads::Threads)
|
||||||
|
plot_apply_warnings(plot_imgui_demo)
|
||||||
|
endif()
|
||||||
@@ -0,0 +1,561 @@
|
|||||||
|
// 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});
|
||||||
|
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<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;
|
||||||
|
}
|
||||||
Reference in new issue
Block a user