172 lines
7.1 KiB
Markdown
172 lines
7.1 KiB
Markdown
# Getting started
|
|
|
|
`plot` is a C++20 library for real-time, high-sample-rate time-series plots drawn with
|
|
OpenGL 3.3 core. The core library — the CMake target `plot::core` — has no third-party
|
|
dependency at all; the host adapters for GLFW, Dear ImGui, Qt6 and GTK4 are separate,
|
|
opt-in targets on top of it (see [embedding.md](embedding.md)).
|
|
|
|
## Prerequisites
|
|
|
|
- A C++20 compiler.
|
|
- CMake **3.24 or newer**. `cmake/plot_options.cmake` fails the configure with a
|
|
`FATAL_ERROR` below that, and the presets in `CMakePresets.json` declare
|
|
`cmakeMinimumRequired` 3.24.
|
|
- A driver exposing an **OpenGL 3.3 core** profile. `plot::gl::load()` resolves only the
|
|
3.3 entry points the library actually calls; anything newer (persistent buffer mapping,
|
|
sync objects) is optional, probed through `plot::gl::caps()` and only used when the
|
|
driver offers it.
|
|
- To build the **test suite** you also need EGL: `tests/CMakeLists.txt` fails the configure
|
|
without it, because the GL tests create a real headless EGL context. The library itself
|
|
does not need EGL.
|
|
- Each adapter needs its own toolkit (GLFW, Dear ImGui downloaded at configure time, Qt6 or
|
|
GTK4) — see [building-and-testing.md](building-and-testing.md) for the options.
|
|
|
|
## Build and install
|
|
|
|
The presets live in `CMakePresets.json`:
|
|
|
|
```sh
|
|
cmake --preset linux-release # or linux-debug, linux-asan, linux-coverage
|
|
cmake --build build/release
|
|
cmake --install build/release --prefix /where/you/want
|
|
```
|
|
|
|
Install is on by default (`PLOT_INSTALL`, `ON`). It installs the `plot_core` archive, the
|
|
public headers under `include/plot/`, and a CMake package config under
|
|
`lib/cmake/plot/`. Adapters join the install/export set only when their toolkit can be
|
|
re-found on a consumer's machine; the configure log records which ones were exported and
|
|
why.
|
|
|
|
## Consuming it
|
|
|
|
```cmake
|
|
find_package(plot 0.1 REQUIRED)
|
|
target_link_libraries(my_app PRIVATE plot::core)
|
|
```
|
|
|
|
`plot::core` is the alias of the `plot_core` target (`EXPORT_NAME core`, so the installed
|
|
name matches the build-tree name). It carries the C++20 requirement and the include
|
|
directory as its public interface and nothing else. The exported adapters are named
|
|
`plot::glfw`, `plot::qt6`, `plot::gtk4` and (when built) `plot::font_ttf`;
|
|
`plot::imgui` is a build-tree target only, because ImGui is supplied by `FetchContent` and
|
|
there is no ImGui package for an installed config file to point at.
|
|
|
|
Every public component is reachable through the umbrella header:
|
|
|
|
```cpp
|
|
#include <plot/plot.hpp> // core, math, gl, data, render, plot, input — in layer order
|
|
```
|
|
|
|
Adapters are deliberately **not** in the umbrella: their headers are reached as
|
|
`<glfw/glfw_host.hpp>`, `<imgui/imgui_plot.hpp>`, `<qt6/qt_plot_widget.hpp>` and
|
|
`<gtk4/gtk_plot_area.hpp>`, so including the umbrella never drags a toolkit in.
|
|
|
|
## A minimal GLFW program
|
|
|
|
The program below opens a window, creates a 3.3 core context, loads the GL entry points,
|
|
and draws one trace. It uses the GLFW toolkit for the window and `plot::core` directly —
|
|
it does not use the shipped `plot::glfw::Host` adapter, so it also shows the pieces the
|
|
adapter hides (window, context, loader, renderer, controller, frame loop).
|
|
|
|
It was compiled and linked as part of writing this page:
|
|
|
|
```sh
|
|
g++ -std=c++20 -I include main.cpp build/debug/libplot_core.a \
|
|
$(pkg-config --libs glfw3) -ldl -lpthread -o plot_quickstart
|
|
```
|
|
|
|
```cpp
|
|
// A minimal plot: one window, one axis pair, one trace.
|
|
#include <plot/plot.hpp> // the whole public surface, in layer order
|
|
#include <plot/gl/loader.hpp>
|
|
|
|
#define GLFW_INCLUDE_NONE // keep GLFW from including <GL/gl.h> and its macros
|
|
#include <GLFW/glfw3.h>
|
|
|
|
#include <cmath>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <vector>
|
|
|
|
namespace {
|
|
// GLFW hands back `void (*)(void)`; plot::gl::ProcLoader wants `void*`.
|
|
void* resolve_gl(const char* name) {
|
|
GLFWglproc p = glfwGetProcAddress(name);
|
|
void* out = nullptr;
|
|
static_assert(sizeof(out) == sizeof(p));
|
|
std::memcpy(&out, &p, sizeof(out));
|
|
return out;
|
|
}
|
|
} // namespace
|
|
|
|
int main() {
|
|
if (glfwInit() != GLFW_TRUE) 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);
|
|
GLFWwindow* window = glfwCreateWindow(800, 480, "plot quick-start", nullptr, nullptr);
|
|
if (window == nullptr) { glfwTerminate(); return 1; }
|
|
glfwMakeContextCurrent(window);
|
|
glfwSwapInterval(1);
|
|
|
|
plot::Status st;
|
|
if (!plot::gl::load(&resolve_gl)) { std::fprintf(stderr, "gl::load failed\n"); return 1; }
|
|
plot::Renderer renderer;
|
|
if (!renderer.init(&st)) { std::fprintf(stderr, "init failed: %s\n", st.message); return 1; }
|
|
|
|
// One trace: 512 samples of a sine, x = sample index.
|
|
std::vector<plot::f32> y(512);
|
|
for (plot::usize i = 0; i < y.size(); ++i) {
|
|
y[i] = static_cast<plot::f32>(std::sin(0.05 * static_cast<double>(i)));
|
|
}
|
|
plot::data::ArraySeries series(std::move(y), 0.0, 1.0);
|
|
|
|
plot::Plot plot;
|
|
const plot::u32 x_axis = plot.add_x_axis({"x", {0.0, 511.0}, true});
|
|
const plot::u32 y_axis =
|
|
plot.add_y_axis({"signal", {-1.0, 1.0}, true, plot::AxisPlace::Left});
|
|
plot.add_trace({&series, plot::Color::hex(0x4FA3FF), 1.5f, x_axis, y_axis, "sine"});
|
|
|
|
plot::input::Controller controller(plot);
|
|
|
|
while (glfwWindowShouldClose(window) == GLFW_FALSE) {
|
|
glfwPollEvents();
|
|
int fb_w = 0, fb_h = 0;
|
|
glfwGetFramebufferSize(window, &fb_w, &fb_h);
|
|
if (fb_w == 0 || fb_h == 0) continue;
|
|
const plot::Rect viewport{0.0f, 0.0f, static_cast<plot::f32>(fb_w),
|
|
static_cast<plot::f32>(fb_h)};
|
|
controller.set_viewport(viewport);
|
|
renderer.style().ui_scale = 1.0f; // device pixels per logical unit
|
|
renderer.begin_frame(static_cast<plot::u32>(fb_w), static_cast<plot::u32>(fb_h), viewport);
|
|
renderer.draw(plot);
|
|
renderer.end_frame();
|
|
glfwSwapBuffers(window);
|
|
}
|
|
|
|
glfwDestroyWindow(window);
|
|
glfwTerminate();
|
|
return 0;
|
|
}
|
|
```
|
|
|
|
Things worth noticing, because they are the embedding contract in miniature:
|
|
|
|
- `plot::gl::load()` must run on the thread that owns the current context, and
|
|
`Renderer::init()` returns `false` with a `Status` when the load failed or the driver
|
|
refused a shader/buffer. The renderer never creates, binds or resizes a framebuffer.
|
|
- `begin_frame(fb_w, fb_h, viewport_px)` takes the framebuffer size (for the projection)
|
|
and the top-left-origin rectangle the plot occupies; `draw()` fills exactly that region;
|
|
`end_frame()` restores the GL state it changed.
|
|
- The `Plot` is pure CPU state. When you draw it through a `Renderer`, set colours and
|
|
metrics on `renderer.style()`, not on the plot — `draw()` copies the style into the plot
|
|
and hands it the `ui_scale` before laying out.
|
|
- `ui_scale` is device pixels per logical unit. Here it is `1.0` because the program uses
|
|
the framebuffer 1:1; a HiDPI host passes the framebuffer/window ratio instead (see
|
|
[embedding.md](embedding.md)).
|
|
|
|
Next: [architecture.md](architecture.md) for how the pieces fit, [embedding.md](embedding.md)
|
|
to put a plot into a real host, and [data-and-performance.md](data-and-performance.md) for
|
|
feeding it a live stream.
|