feat(glfw): adapter and 1 MS/s streaming demo
This commit is contained in:
1 parent
1f5aa11de2
commit
4b649a2783
7 files changed
+884
-2
No files matched your search
@@ -0,0 +1,51 @@
|
||||
# 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.
|
||||
#
|
||||
# Dependency 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).
|
||||
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()
|
||||
|
||||
message(STATUS "plot: GLFW adapter dependency: ${plot_glfw_dep}")
|
||||
|
||||
add_library(plot_glfw STATIC glfw_host.cpp)
|
||||
add_library(plot::glfw ALIAS plot_glfw)
|
||||
# The public include root is `adapters/`, so the header is reached as
|
||||
# <glfw/glfw_host.hpp> -- the toolkit's name first, like every other adapter's.
|
||||
target_include_directories(plot_glfw PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/..")
|
||||
# GLFW stays PRIVATE: the header forward-declares GLFWwindow and includes no GLFW
|
||||
# header, so a consumer of plot::glfw does not inherit the toolkit.
|
||||
target_link_libraries(plot_glfw PUBLIC plot::core PRIVATE ${plot_glfw_dep})
|
||||
plot_apply_warnings(plot_glfw)
|
||||
@@ -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
|
||||
@@ -0,0 +1,154 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
|
||||
#include <plot/core/rect.hpp>
|
||||
#include <plot/core/status.hpp>
|
||||
#include <plot/core/types.hpp>
|
||||
#include <plot/input/controller.hpp>
|
||||
#include <plot/plot/plot.hpp>
|
||||
#include <plot/render/renderer.hpp>
|
||||
|
||||
/// GLFW's window type. Forward-declared on purpose: this header is
|
||||
/// self-contained and needs no GLFW include directory, so a consumer of the
|
||||
/// adapter does not inherit the toolkit's headers (and their `<GL/gl.h>`, whose
|
||||
/// macros would rewrite this library's enumerators).
|
||||
struct GLFWwindow;
|
||||
|
||||
namespace plot::glfw {
|
||||
|
||||
/// What `Host::run()` opens the window with.
|
||||
struct RunConfig {
|
||||
int width = 1280;
|
||||
int height = 720;
|
||||
const char* title = "plot";
|
||||
bool vsync = true;
|
||||
};
|
||||
|
||||
/// One frame of application code, called by `Host::run()` after the viewport is
|
||||
/// set and before the frame is staged, so it is where data is refreshed.
|
||||
/// `dt` is the wall time since the previous call, in seconds.
|
||||
using FrameFn = std::function<void(plot::Plot&, f64 dt)>;
|
||||
|
||||
/// The reference host: owns the GLFW window and its OpenGL 3.3 core context, the
|
||||
/// GL entry points, the `Renderer`, the `Plot`, the `input::Controller` and the
|
||||
/// frame loop, so nothing above it names GLFW or GL.
|
||||
///
|
||||
/// Per-frame sequence, in this order:
|
||||
///
|
||||
/// poll events -> controller.set_viewport(framebuffer-pixel rect) ->
|
||||
/// frame(plot, dt) -> begin_frame -> draw -> [screenshot] -> end_frame ->
|
||||
/// swap buffers
|
||||
///
|
||||
/// Two pixel spaces meet here and must not be confused. GLFW reports the cursor
|
||||
/// and the window in *window* coordinates, while the framebuffer -- what
|
||||
/// `begin_frame` takes and what every `Rect` in this library speaks -- can be
|
||||
/// larger under a fractional or HiDPI scale, so `glfwGetFramebufferSize()` and
|
||||
/// `glfwGetWindowSize()` disagree. The host calls `begin_frame` with the
|
||||
/// framebuffer size and scales every pointer coordinate by the ratio
|
||||
/// `framebuffer / window`, so a gesture lands on the pixels the plot was laid out
|
||||
/// in. On an unscaled display the ratio is 1.0 and nothing changes.
|
||||
///
|
||||
/// Headless smoke mode, read once per `run()`:
|
||||
///
|
||||
/// `PLOT_DEMO_EXIT_AFTER_MS=<n>` stop the loop after n ms and return
|
||||
/// (default 0: run until the window is closed)
|
||||
/// `PLOT_DEMO_SCREENSHOT=<path>` on that final frame, write it with
|
||||
/// `Renderer::dump_frame_ppm()` before the swap
|
||||
///
|
||||
/// That is what lets a CI run a GUI library under `xvfb-run`: no display access
|
||||
/// and no screen capture, just a PPM on disk.
|
||||
///
|
||||
/// A run with a deadline is that automated path, so its window is created hidden
|
||||
/// (`GLFW_VISIBLE` false): an automated run must not flash a window on whatever
|
||||
/// display it lands on. It also means the loop is not throttled by a display
|
||||
/// refresh, so the frame times it reports are the CPU's, not a vblank's. A run
|
||||
/// without a deadline shows its window, as an interactive application must.
|
||||
///
|
||||
/// Not thread-safe, and confined to the thread that runs `run()`.
|
||||
class Host {
|
||||
public:
|
||||
Host() noexcept;
|
||||
~Host();
|
||||
Host(const Host&) = delete;
|
||||
Host& operator=(const Host&) = delete;
|
||||
|
||||
/// Opens the window, makes a 3.3 core context current, resolves the GL entry
|
||||
/// points and initialises the renderer, then runs the loop until the window is
|
||||
/// closed or the exit deadline passes. Returns false with `st` filled in when
|
||||
/// GLFW, the context or `Renderer::init()` fails; a failed run leaves nothing
|
||||
/// open.
|
||||
///
|
||||
/// On success the window *stays open*, so a caller can stop and join whatever
|
||||
/// feeds the plot before the GL resources go away: `close()` (or the
|
||||
/// destructor) releases the renderer, the context and the window, in that
|
||||
/// order, on this thread. Calling `run()` again reopens the window and keeps
|
||||
/// the plot.
|
||||
bool run(const RunConfig& cfg, const FrameFn& frame, Status* st = nullptr);
|
||||
|
||||
/// Releases the renderer and the window. Idempotent, and safe on a host that
|
||||
/// never ran.
|
||||
void close() noexcept;
|
||||
|
||||
/// The plot the frame callback populates. Valid for the host's whole life.
|
||||
Plot& plot() noexcept { return plot_; }
|
||||
/// The gesture controller the host feeds. Valid for the host's whole life.
|
||||
input::Controller& controller() noexcept { return controller_; }
|
||||
|
||||
/// The renderer, or null while no window is open. Valid from inside the frame
|
||||
/// callback and after a successful `run()`; `close()` releases it.
|
||||
Renderer* renderer() noexcept { return renderer_ ? &*renderer_ : nullptr; }
|
||||
|
||||
/// Retitles the open window -- from the frame callback, for a live
|
||||
/// samples/second and frame-time readout. A no-op while no window is open.
|
||||
void set_title(const char* title) noexcept;
|
||||
|
||||
/// Framebuffer pixels per window pixel, one ratio per axis, as of the last
|
||||
/// event or frame. 1.0 on an unscaled display.
|
||||
f32 framebuffer_scale_x() const noexcept { return fb_scale_x_; }
|
||||
f32 framebuffer_scale_y() const noexcept { return fb_scale_y_; }
|
||||
/// The window's content scale, one per axis, as of the last frame.
|
||||
f32 content_scale_x() const noexcept { return content_scale_x_; }
|
||||
f32 content_scale_y() const noexcept { return content_scale_y_; }
|
||||
|
||||
private:
|
||||
/// Reads the framebuffer and window sizes and the content scale, refreshing
|
||||
/// the window -> framebuffer ratios the pointer translation uses.
|
||||
void refresh_sizes() noexcept;
|
||||
|
||||
/// Callbacks find their host through GLFW's per-window user pointer.
|
||||
static Host* from(GLFWwindow* w) noexcept;
|
||||
static void cursor_pos_cb(GLFWwindow* w, double x, double y);
|
||||
static void mouse_button_cb(GLFWwindow* w, int button, int action, int mods);
|
||||
static void scroll_cb(GLFWwindow* w, double dx, double dy);
|
||||
static void key_cb(GLFWwindow* w, int key, int scancode, int action, int mods);
|
||||
static void framebuffer_size_cb(GLFWwindow* w, int width, int height);
|
||||
|
||||
void on_cursor_pos(f64 wx, f64 wy) noexcept;
|
||||
void on_mouse_button(int button, int action, int mods) noexcept;
|
||||
void on_scroll(f64 dx, f64 dy) noexcept;
|
||||
void on_key(int key, int action, int mods) noexcept;
|
||||
|
||||
Plot plot_;
|
||||
input::Controller controller_; ///< forwards its viewport to `plot_`
|
||||
std::optional<Renderer> renderer_;
|
||||
GLFWwindow* window_ = nullptr;
|
||||
|
||||
u32 fb_width_ = 0; ///< framebuffer size of the last refresh_sizes()
|
||||
u32 fb_height_ = 0;
|
||||
/// Framebuffer pixels per window pixel. Taken from the two sizes GLFW reports
|
||||
/// rather than from the content scale, which under a fractional scale does not
|
||||
/// round back to the framebuffer size.
|
||||
f32 fb_scale_x_ = 1.0f;
|
||||
f32 fb_scale_y_ = 1.0f;
|
||||
f32 content_scale_x_ = 1.0f;
|
||||
f32 content_scale_y_ = 1.0f;
|
||||
/// Last pointer position in framebuffer pixels: the deltas the controller pans
|
||||
/// with are the movements since the previous event, which GLFW does not report.
|
||||
f32 last_pointer_x_ = 0.0f;
|
||||
f32 last_pointer_y_ = 0.0f;
|
||||
bool have_pointer_ = false;
|
||||
};
|
||||
|
||||
} // namespace plot::glfw
|
||||
Reference in new issue
Block a user