Files
plotter/docs/getting-started.md
T

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.