docs: a user guide covering getting started, architecture, embedding, data, interaction, styling and testing
This commit is contained in:
1 parent
d03f9ce51a
commit
b7d0f901cc
7 files changed
+1082
No files matched your search
@@ -0,0 +1,171 @@
|
||||
# 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.
|
||||
Reference in new issue
Block a user