feat(glfw): adapter and 1 MS/s streaming demo

This commit is contained in:
Martino Ferrari committed 2026-09-27 23:44:30 +02:00
1 parent 1f5aa11de2
commit 4b649a2783
7 files changed
+884 -2

No files matched your search

+346
View File
@@ -0,0 +1,346 @@
#include <glfw/glfw_host.hpp>
// Before GLFW's header, so it does not include <GL/gl.h>: that header's macros
// (GL_COLOR_BUFFER_BIT and friends) would rewrite `plot::gl`'s enumerators of the
// same name. The adapter needs GLFW for the window, the context and the events
// only -- every GL entry point comes from plot::gl::load().
#define GLFW_INCLUDE_NONE
#include <GLFW/glfw3.h>
#include <plot/gl/loader.hpp>
#include <cstdio>
#include <cstdlib>
#include <cstring>
namespace plot::glfw {
namespace {
/// GLFW_MOD_* and `plot::input::Mod` are the same bits, by design: the
/// controller documents `Mod` as the host-translatable mask. A key event's mask
/// therefore passes through, and only a pointer or wheel event, whose callbacks
/// carry no mask, has to ask the window for the current key state.
static_assert(static_cast<u32>(GLFW_MOD_SHIFT) == static_cast<u32>(input::Mod::Shift));
static_assert(static_cast<u32>(GLFW_MOD_CONTROL) == static_cast<u32>(input::Mod::Ctrl));
static_assert(static_cast<u32>(GLFW_MOD_ALT) == static_cast<u32>(input::Mod::Alt));
static_assert(static_cast<u32>(GLFW_MOD_SUPER) == static_cast<u32>(input::Mod::Super));
u32 mods_now(GLFWwindow* w) noexcept {
u32 m = 0;
if (glfwGetKey(w, GLFW_KEY_LEFT_SHIFT) == GLFW_PRESS ||
glfwGetKey(w, GLFW_KEY_RIGHT_SHIFT) == GLFW_PRESS) {
m |= GLFW_MOD_SHIFT;
}
if (glfwGetKey(w, GLFW_KEY_LEFT_CONTROL) == GLFW_PRESS ||
glfwGetKey(w, GLFW_KEY_RIGHT_CONTROL) == GLFW_PRESS) {
m |= GLFW_MOD_CONTROL;
}
if (glfwGetKey(w, GLFW_KEY_LEFT_ALT) == GLFW_PRESS ||
glfwGetKey(w, GLFW_KEY_RIGHT_ALT) == GLFW_PRESS) {
m |= GLFW_MOD_ALT;
}
if (glfwGetKey(w, GLFW_KEY_LEFT_SUPER) == GLFW_PRESS ||
glfwGetKey(w, GLFW_KEY_RIGHT_SUPER) == GLFW_PRESS) {
m |= GLFW_MOD_SUPER;
}
return m;
}
bool any_button_down(GLFWwindow* w) noexcept {
return glfwGetMouseButton(w, GLFW_MOUSE_BUTTON_LEFT) == GLFW_PRESS ||
glfwGetMouseButton(w, GLFW_MOUSE_BUTTON_RIGHT) == GLFW_PRESS ||
glfwGetMouseButton(w, GLFW_MOUSE_BUTTON_MIDDLE) == GLFW_PRESS;
}
/// `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;
}
bool fail(Status* st, ErrorCode code, const char* message) noexcept {
if (st != nullptr) {
st->code = code;
st->message = message;
}
return false;
}
/// `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
Host::Host() noexcept : controller_(plot_) {}
Host::~Host() { close(); }
void Host::close() noexcept {
// The renderer's RAII handles call glDelete* from their destructors, so the
// context has to be current -- and alive -- when they run.
if (renderer_) {
if (window_ != nullptr) glfwMakeContextCurrent(window_);
renderer_.reset();
}
if (window_ != nullptr) {
glfwDestroyWindow(window_);
window_ = nullptr;
}
glfwMakeContextCurrent(nullptr);
glfwTerminate();
have_pointer_ = false;
}
bool Host::run(const RunConfig& cfg, const FrameFn& frame, Status* st) {
if (st != nullptr) *st = Status{};
close(); // a second run() starts from a clean window
if (glfwInit() != GLFW_TRUE) {
return fail(st, ErrorCode::Unsupported, "glfwInit() failed");
}
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
// Required on macOS 10.7+, ignored elsewhere.
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GLFW_TRUE);
// A run with a deadline is the automated path, so it stays off-screen: a
// bounded CI run must never flash a window on a real display.
const f64 exit_after_s = exit_after_seconds();
if (exit_after_s > 0.0) glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
GLFWwindow* w = glfwCreateWindow(cfg.width > 0 ? cfg.width : 1,
cfg.height > 0 ? cfg.height : 1,
cfg.title != nullptr ? cfg.title : "plot", nullptr, nullptr);
if (w == nullptr) {
const char* desc = nullptr;
(void)glfwGetError(&desc);
std::fprintf(stderr, "plot::glfw: glfwCreateWindow() failed: %s\n",
desc != nullptr ? desc : "no error description");
close();
return fail(st, ErrorCode::Unsupported,
"glfwCreateWindow() failed: no OpenGL 3.3 core context (see stderr)");
}
window_ = w;
glfwSetWindowUserPointer(w, this);
glfwMakeContextCurrent(w);
glfwSwapInterval(cfg.vsync ? 1 : 0);
glfwSetCursorPosCallback(w, &Host::cursor_pos_cb);
glfwSetMouseButtonCallback(w, &Host::mouse_button_cb);
glfwSetScrollCallback(w, &Host::scroll_cb);
glfwSetKeyCallback(w, &Host::key_cb);
glfwSetFramebufferSizeCallback(w, &Host::framebuffer_size_cb);
refresh_sizes();
if (!plot::gl::load(&resolve_gl)) {
close();
return fail(st, ErrorCode::Unsupported,
"plot::gl::load() failed: the context is missing a required OpenGL 3.3 entry point");
}
renderer_.emplace();
if (!renderer_->init(st)) {
close();
if (st != nullptr && st->code == ErrorCode::Ok) {
*st = Status{ErrorCode::GLFailure, "Renderer::init() failed"};
}
return false;
}
const char* const screenshot = std::getenv("PLOT_DEMO_SCREENSHOT");
const f64 start = glfwGetTime();
f64 previous = start;
while (glfwWindowShouldClose(window_) == GLFW_FALSE) {
glfwPollEvents();
refresh_sizes();
if (fb_width_ == 0 || fb_height_ == 0) {
// Minimised, or a size the compositor reports as zero: nothing to draw
// into. Idle briefly rather than spin, so an exit deadline still lands.
glfwWaitEventsTimeout(0.05);
continue;
}
// The plot's region is the whole window here, which is what the controller
// and `begin_frame` must agree on: one rect, in framebuffer pixels, set
// before the frame callback runs so it sees the layout it draws into.
const Rect viewport{0.0f, 0.0f, static_cast<f32>(fb_width_), static_cast<f32>(fb_height_)};
controller_.set_viewport(viewport); // also Plot::set_viewport, then layout()
const f64 now = glfwGetTime();
const f64 dt = now - previous;
previous = now;
if (frame) frame(plot_, dt);
const bool exiting = exit_after_s > 0.0 && (now - start) >= exit_after_s;
renderer_->begin_frame(fb_width_, fb_height_, viewport);
renderer_->draw(plot_);
if (exiting && screenshot != nullptr && screenshot[0] != '\0') {
Status shot;
const bool dumped = renderer_->dump_frame_ppm(screenshot, &shot);
std::printf("[glfw] screenshot %s: %s\n", screenshot,
dumped ? "written" : shot.message);
std::fflush(stdout);
}
renderer_->end_frame();
glfwSwapBuffers(window_);
if (exiting) break;
}
return true;
}
void Host::set_title(const char* title) noexcept {
if (window_ != nullptr) glfwSetWindowTitle(window_, title != nullptr ? title : "");
}
void Host::refresh_sizes() noexcept {
if (window_ == nullptr) return;
int fw = 0;
int fh = 0;
int ww = 0;
int wh = 0;
glfwGetFramebufferSize(window_, &fw, &fh);
glfwGetWindowSize(window_, &ww, &wh);
float sx = 1.0f;
float sy = 1.0f;
glfwGetWindowContentScale(window_, &sx, &sy);
content_scale_x_ = sx;
content_scale_y_ = sy;
if (fw > 0) fb_width_ = static_cast<u32>(fw);
if (fh > 0) fb_height_ = static_cast<u32>(fh);
if (ww > 0 && fw > 0) fb_scale_x_ = static_cast<f32>(fw) / static_cast<f32>(ww);
if (wh > 0 && fh > 0) fb_scale_y_ = static_cast<f32>(fh) / static_cast<f32>(wh);
}
// ------------------------------------------------------------------ events
Host* Host::from(GLFWwindow* w) noexcept {
return w != nullptr ? static_cast<Host*>(glfwGetWindowUserPointer(w)) : nullptr;
}
void Host::cursor_pos_cb(GLFWwindow* w, double x, double y) {
if (Host* h = from(w)) h->on_cursor_pos(x, y);
}
void Host::mouse_button_cb(GLFWwindow* w, int button, int action, int mods) {
if (Host* h = from(w)) h->on_mouse_button(button, action, mods);
}
void Host::scroll_cb(GLFWwindow* w, double dx, double dy) {
if (Host* h = from(w)) h->on_scroll(dx, dy);
}
void Host::key_cb(GLFWwindow* w, int key, int scancode, int action, int mods) {
(void)scancode;
if (Host* h = from(w)) h->on_key(key, action, mods);
}
void Host::framebuffer_size_cb(GLFWwindow* w, int width, int height) {
(void)width;
(void)height;
if (Host* h = from(w)) h->refresh_sizes();
}
void Host::on_cursor_pos(f64 wx, f64 wy) noexcept {
// Window coordinates -> framebuffer pixels: the same translation the viewport
// and every other Rect here use, so a gesture lands where it was aimed.
const f32 x = static_cast<f32>(wx) * fb_scale_x_;
const f32 y = static_cast<f32>(wy) * fb_scale_y_;
input::PointerEvent e;
e.x = x;
e.y = y;
if (have_pointer_) {
e.dx = x - last_pointer_x_;
e.dy = y - last_pointer_y_;
}
last_pointer_x_ = x;
last_pointer_y_ = y;
have_pointer_ = true;
e.mods = mods_now(window_);
e.down = any_button_down(window_);
controller_.on_pointer(e);
}
void Host::on_mouse_button(int button, int action, int mods) noexcept {
input::Button b;
switch (button) {
case GLFW_MOUSE_BUTTON_LEFT: b = input::Button::Left; break;
case GLFW_MOUSE_BUTTON_MIDDLE: b = input::Button::Middle; break;
case GLFW_MOUSE_BUTTON_RIGHT: b = input::Button::Right; break;
default: return; // the controller has no gesture for the auxiliary buttons
}
double wx = 0.0;
double wy = 0.0;
glfwGetCursorPos(window_, &wx, &wy);
input::PointerEvent e;
e.x = static_cast<f32>(wx) * fb_scale_x_;
e.y = static_cast<f32>(wy) * fb_scale_y_;
// The press/release is also a pointer sample: the next move's delta starts
// here, or a drag's first motion would include the jump to the click.
last_pointer_x_ = e.x;
last_pointer_y_ = e.y;
have_pointer_ = true;
e.button = b;
e.mods = static_cast<u32>(mods);
e.pressed = action == GLFW_PRESS;
e.released = action == GLFW_RELEASE;
e.down = any_button_down(window_);
controller_.on_pointer(e);
}
void Host::on_scroll(f64 dx, f64 dy) noexcept {
double wx = 0.0;
double wy = 0.0;
glfwGetCursorPos(window_, &wx, &wy);
input::WheelEvent e;
e.x = static_cast<f32>(wx) * fb_scale_x_;
e.y = static_cast<f32>(wy) * fb_scale_y_;
e.dx = static_cast<f32>(dx);
e.dy = static_cast<f32>(dy); // GLFW's positive y (scroll up) is the zoom-in sign
e.mods = mods_now(window_);
controller_.on_wheel(e);
}
void Host::on_key(int key, int action, int mods) noexcept {
int mapped = 0;
switch (key) {
// The controller zooms on '+', which on most layouts is shift+'='; the '='
// key is mapped to it as well, so both spellings zoom in.
case GLFW_KEY_EQUAL:
case GLFW_KEY_KP_ADD: mapped = input::kKeyZoomIn; break;
case GLFW_KEY_MINUS:
case GLFW_KEY_KP_SUBTRACT: mapped = input::kKeyZoomOut; break;
case GLFW_KEY_F: mapped = input::kKeyFit; break;
case GLFW_KEY_ESCAPE:
if (action == GLFW_PRESS) glfwSetWindowShouldClose(window_, GLFW_TRUE);
return;
default: return;
}
input::KeyEvent e;
e.key = mapped;
e.mods = static_cast<u32>(mods);
// GLFW reports auto-repeat as its own action; a held key keeps acting, exactly
// as holding the key on a keyboard does.
e.down = action != GLFW_RELEASE;
controller_.on_key(e);
}
} // namespace plot::glfw