diff --git a/FrOSCon26/FrOSCon.26.mm b/FrOSCon26/FrOSCon.26.mm index 18948e4..efdcc99 100644 --- a/FrOSCon26/FrOSCon.26.mm +++ b/FrOSCon26/FrOSCon.26.mm @@ -852,16 +852,13 @@ - - - +

letztes Jahr haben wir vier verschiedene Varianten gezeigt, die jeweils unterschiedliche Lib-Dependencies hatten. Deshalb war es ein Multimodule-Projekt. Das kann ich diesmal vereinfachen in ein einziges Projekt

- -
+
@@ -916,16 +913,13 @@ - - - +

die Zuhörer sind keine Systemprogrammierer

- -
+
@@ -978,22 +972,17 @@ - - - +

allerdings: der Worker wäre damit ein Proactor

- -
+
- - - +

....es wäre nämlich tatsächlich dann ganz ideomatisch (so hab ich die Doku verstanden, und Claude sagt das von sich aus auch). @@ -1002,8 +991,7 @@ Zwar hatte ich die Idee, im Talk schon grundsätzlich etwas zu machen, was funktioniert wie in Lumiera geplant. Aber wenn ich mir das wirklich vom didaktischen Gesichtspunkt her nochmal überlege, ist es nicht so geschickt.

- -
+
@@ -1015,16 +1003,13 @@ - - - +

...es würde zur "toolkit, nicht framework"-Policy passen, und zudem wäre das Signal explizit von jedem Thread aus triggerbar

- -
+
@@ -1076,7 +1061,33 @@ - + + + + +

+ // --- GUI thread, once at setup --- +

+

+ Glib::Dispatcher frameReady; +

+

+ frameReady.connect([this]{ displayFrame(*processor_); });  // upload + draw + eglSwapBuffers() +

+

+ +

+

+ // --- worker thread, once a frame is ready --- +

+

+ frameReady.emit();   // thread-safe; wakes the GUI thread's main loop +

+ +
+ + +
@@ -1112,22 +1123,17 @@ - - - +

Ich verwende das aktuelle "Sonnet" auf Stufe Medium. Das braucht erhebliche Rechenzeit für jede Antwort auf diesem Diskussionslevel (und zeigt auch jeweils expandierende, schlußfolgernde und kondensierende Phasen an)

- -
+
- - - +

Proactor, confirmed. io_uring is the textbook proactor: you submit a description of the completed operation you want, the kernel does the work, and your thread's job is purely to consume results and act on them — as opposed to a reactor (epoll-style), where you're told "this fd is ready," and you still perform the (potentially blocking, now guaranteed-non-blocking) I/O yourself. Good instinct to name this explicitly in the talk; it's the single sentence that makes the rest of the design legible. @@ -1147,27 +1153,21 @@ Given that framing: you're right that a flag-per-buffer is not a lesser or simplified version of "the real solution" — it's the same solution as coroutines, just written manually because your problem is small and linear enough that the compiler-generated version would be pure overhead (both in runtime terms and, more importantly here, in audience cognitive load — introducing C++20 coroutine machinery to explain a 3-state pipeline would be a bad trade for this talk).

- -
+
- - - +

std::atomic<SlotState>

- -
+ - - - + Since each slot has exactly one producer (the worker) and one consumer (the GUI thread, via the dispatcher callback) and they never touch a slot at the same time by construction, @@ -1175,8 +1175,7 @@ std::atomic<SlotState> with release-on-write / acquire-on-read is enough — no mutex needed. That pairing is worth a beat in the talk on its own: the atomic isn't there to prevent concurrent writes (there aren't any), it's there to guarantee the GUI thread sees the buffer contents the worker wrote, not just the state flag — a clean, concrete illustration of why acquire/release exists, tied directly to something the audience just watched you build.

- -
+
@@ -1234,16 +1233,13 @@ - - - +

...obschon man sie auf dem gleichen lokalen State aufbaut. Das Beispiel, das Claude nennt: man könnte das Quellfile loopen

- -
+
@@ -1254,9 +1250,7 @@ - - - +

struct io_uring ring; @@ -1316,14 +1310,11 @@ io_uring_submit(&ring);   // one syscall flushes everything prepped above

- -
+
- - - +

for (;;) { @@ -1362,31 +1353,24 @@ }

- -
+
- - - + void onTick() { if (slots[nextExpected % N].state == Ready) { frameReady.emit(); // GUI thread will draw slots[nextExpected % N] ++nextExpected; // GUI marks the slot Empty once it's done drawing } // else: do nothing — this frame slips one tick later. Not dropped, just delayed. while (slots[nextToSubmit % N].state == Empty) { submitRead(nextToSubmit); // io_uring_prep_read + set_data64(nextToSubmit) ++nextToSubmit; } io_uring_submit(&ring); } - - + - - - +

The reason top-up belongs in onTick rather than in onReadComplete:
a slot only becomes reusable once the GUI thread has finished drawing it and flips it back to Empty  — and the worker, sitting blocked in io_uring_wait_cqe, has no way to know that happened except by waking up for something. The tick is that guaranteed periodic wake-up, so it doubles as "check if there's new capacity to fill," with a worst-case latency of one tick period (~40ms) before a freed slot gets reused — fine at this cadence, and a nice illustration that the timer isn't just pacing display, it's also the thing that keeps the whole state machine alive even when nothing else is happening.

- -
+
@@ -1471,16 +1455,13 @@
- - - +

bestätigt meine Analyse, ohne Diskrepanz

- -
+
@@ -1500,37 +1481,29 @@ - - - + As far as I could see from looking at the liburing sources, there is a fundamental read barrier before retrieving/updating the rings, and a write barrier at the other side (when done with a completion or submitting). Now you also pointed out the conclusion that submitting the next IO task could itself be a scheduled activity. Combining both, if we ensure that interaction with io_uring happens only in the "mutual exclusive" phase (i.e. the worker holds the GroomingToken) -- then that would ensure that, at any given time, only one thread at max interacts with the urings. And Liburing inserts the necessary barriers, so that the next task to enter that zone would see the changes. Does that reasoning hold? What difficulties do you see? - - + - - - +

Ja, weiß ich. Deshalb hab ich's so programmiert. Freut mich daß Claude da unabhängig zum gleichen Schluß kommt (denn ich hab mich oft gefragt, ob ich mich da in Spitzfindigkeiten verrenne).
Also nochmal explizit: memory_order_relaxed reicht hier nicht

- -
+
- - - +

Einmal geht es um shared-data zwischen Kernel und Userspace. Wohingegen das Grooming-Token die Sichtbarkeit über die Worker hinweg steuert. @@ -1542,8 +1515,7 @@ Diese Unterscheidung ist konzeptionell (sehe ich so, Claude deutet es auch so). Beide Ansätze stören sich nicht, man sollte sie aber auseinanderhalten. Ich denke, das ist ein generischer guter Rat beim Design solcher Systeme, man sollte nicht danach streben, solche verschiedenen Belange zusammenzufassen, selbst wenn es im konkreten Fall möglich wäre (wie hier der Fall, aber nur weil keine data-dependenciy im memory-ordering includiert ist)

- -
+
@@ -1649,16 +1621,13 @@ - - - +

Current 2 — thread-per-core, shared-nothing, and it's picking up real steam. This is the more consequential trend to be aware of, because it's a genuinely different answer to the same problem you're solving, and it's currently the fashionable one in high-performance I/O-heavy systems: pin one thread per core, partition data and work by that core (typically via hashing), eliminate cross-thread shared state entirely, and communicate exclusively via message passing rather than shared queues or locks. This is the architecture behind ScyllaDB and Redpanda via the Seastar framework, and it's spreading into the Rust ecosystem too (Glommio, compio, and others). A recent writeup from the Apache Iggy project frames the shift precisely as moving from work stealing to "work steering" — i.e., instead of idle workers reaching into a shared structure (what you have), work is routed to the specific core that already owns the relevant data, and nobody has to contend for anything. Interestingly, the standard advice against a single shared io_uring instance per application is exactly the contention concern we discussed for your ring — each thread in Glommio gets its own private set of rings instead, which is the thread-per-core answer to the same "who owns the ring" question you and I just spent a while on. Apache + 2

- -
+
@@ -1802,59 +1771,47 @@
- - - +

aber man sollte konzeptionell zwischen der visibility mit dem Kernel und der Visibility zwischen den Workern unterscheiden; für ersteres gibt es die Barrieren in Liburing

- -
+
- - - +

der Umstand daß neue IO-Aktivitäten gescheduled werden und Completions entnommen werden müssen, garantiert nur eine passive Backpressure (nicht entnommene Completions gehen nicht verloren, und der Platz in den URings ist begrenzt). Sehr wahrscheinlich ist mehr notwendig, damit die Engine rund läuft.

- -
+
- - - +

Es ist eine etablierte und bewährte Methode, und wird nicht weggehen. Was wir geschaffen haben konvergiert gut mit den allgemeinen Einsichten im Feld

- -
+
- - - +

das ist ein Gegentrend, der aber auf einer anderen Größenordnung arbeitet (im Nanosekunden-Bereich)

- -
+
@@ -1863,6 +1820,93 @@
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ Hier möchte ich sowohl den GTK-Code selber verbergen, alsauch dem Umstand daß die Start-Lokik in der Applikation nicht ganz trivial ist, und mit dem Start des Players verwoben. Lesbarkeit ist eine weitere Motivation: man kann dadurch Dinge benennen +

+ +
+
+ + + + +

+ dazu gehört auch, keine »Tricks« anzuwenden und keine schlechten Praktiken vorzuführen. Der Zustands-Träge sollte unique sein, ohne Wenn und Aber. +

+ +
+
+
+
+
+ + + + + + + + + @@ -1898,22 +1942,17 @@ - - - +

...anders als bei dem Video-Output-Thema, bei dem wir uns die zentralen Element überhaupt erst aufschließen mußten, und dies nur mithilfe bestehender GPL-Codebasen geschafft haben. Diesmal ist die Situation ganz anders, das Thema ist ziemlich gut in Tutorials und Blogs aufbereitet. Viele haben zwar kein brauchbares Lizenz-Statement, aber andererseits ist die Aufgabe so einfach, daß ich nirgends Code abschreiben muß, den ich nicht auch genauso selber schreiben könnte.

- -
+
- - - +
  • @@ -1924,8 +1963,7 @@
- -
+
diff --git a/FrOSCon26/examples/commons.hpp b/FrOSCon26/examples/commons.hpp new file mode 100644 index 0000000..be15fc1 --- /dev/null +++ b/FrOSCon26/examples/commons.hpp @@ -0,0 +1,33 @@ +/* + commons.hpp - common definitions and utils for the io_uring video output demo code + + Copyright (C) + 2026 Hermann Vosseler + + This program is free software; you can redistribute it and/or modify it +  under the terms of the GNU GPL version 2+ See the LICENSE file for details. + +* ************************************************************************/ + +#ifndef COMMONS_H +#define COMMONS_H + + +#include +#include +#include + +using std::string; +using uint = unsigned int; +using FrameRate = uint; + + +inline void +__FAIL (string msg) +{ + std::cerr << "FAIL: " << msg << std::endl; + std::abort(); +} + + +#endif /*COMMONS_H*/ diff --git a/FrOSCon26/examples/demo-uring.cpp b/FrOSCon26/examples/demo-uring.cpp index 4dbca5f..f91f248 100644 --- a/FrOSCon26/examples/demo-uring.cpp +++ b/FrOSCon26/examples/demo-uring.cpp @@ -9,11 +9,65 @@ * ************************************************************************/ + +#include "commons.hpp" +#include "egl-display.hpp" +#include "gtk-uring-app.hpp" + #include +#include +#include + +using std::unique_ptr; + +//-----------------hard-coded-config-- +//-----------------hard-coded-config-- + + +class UringVideoPlayer + { + Glib::Dispatcher dispatchFrame_; + EglDisplay videoDisplayer_; + std::thread proactorThread_; + + public: + UringVideoPlayer (Gtk::Window& appWindow) + : videoDisplayer_{appWindow, dispatchFrame_} + , proactorThread_{[this]{ playVideo(); }} + { } + + private: + void + playVideo() + { + + } + }; + + +unique_ptr +launchPlayer (Gtk::Window& appWindow) +{ + auto player = std::make_unique (appWindow); + + return player; +} + + + +void +cleanUp (UringVideoPlayer& player) +{ + +} + int main (int, const char*[]) { - std::cout << "Hello Uring" << std::endl; + return GtkUringApp{"io-uring-demo"} + .onClick (launchPlayer) + .onClose (cleanUp) + .run(); } diff --git a/FrOSCon26/examples/egl-display.hpp b/FrOSCon26/examples/egl-display.hpp new file mode 100644 index 0000000..a4b45e0 --- /dev/null +++ b/FrOSCon26/examples/egl-display.hpp @@ -0,0 +1,355 @@ +/* + egl-display.hpp - output video from a GTK application, using EGL - OpenGL binding + + Copyright (C) + 2025, Benny Lyons + 2025, Hermann Vosseler + + This program is free software; you can redistribute it and/or modify it +  under the terms of the GNU GPL version 2+ See the LICENSE file for details. + +* ************************************************************************/ + + +#include "commons.hpp" +#include "gtk-egl-app.hpp" +#include "image-generator.hpp" + +#include +#include +#include +#include +#include + +// for low-level access -> X-Window +#include + +// X11, EGL and GLEW for OpenGL +#include +#include +#include // defines plattform-specific config and extensions +#include // provides system-specific bindings for OpenGL extensions + +using std::string; + + +/** + * Connection and drawing context used for EGL based video display. + */ +class EglDisplay + { + /** EGL / X11 connection. */ + EGLDisplay display{nullptr}; + EGLSurface surface{nullptr}; + + EGLContext egl{nullptr}; + + uint texID{0}; + uint vboID{0}; + uint vaoID{0}; + + float scaleX{1}; + float scaleY{1}; + + public: + // hard wired here (should be dynamic in real-world usage) + constexpr static uint VIDEO_WIDTH {320}; + constexpr static uint VIDEO_HEIGHT{240}; + + EglDisplay(FrameRate fps) + { } + }; + + + + +EglDisplay +openDisplay (Gtk::Window& appWindow, FrameRate fps) +{ + std::cout << "Open display-connection through EGL..." << std::endl; + EglDisplay ctx{fps}; + + // use the X-Window as anchor to build an OpenGL context via EGL + Glib::RefPtr gdkWindow = appWindow.get_window(); + Window xWindow = GDK_WINDOW_XID (gdkWindow->gobj()); + Display* xDisplay = GDK_WINDOW_XDISPLAY (gdkWindow->gobj()); + + long xScreen; + XWindowAttributes xWinAttrs; + if (XGetWindowAttributes (xDisplay, xWindow, &xWinAttrs)) + xScreen = XScreenNumberOfScreen (xWinAttrs.screen); + else + __FAIL ("unable to retrieve screen number from the X11 window attributes."); + + long SCREEN_SPEC[] + {EGL_PLATFORM_X11_SCREEN_EXT, xScreen + ,EGL_NONE}; + + ctx.display = eglGetPlatformDisplay (EGL_PLATFORM_X11_EXT, xDisplay, SCREEN_SPEC); + if (EGL_NO_DISPLAY == ctx.display + or not eglInitialize(ctx.display, NULL,NULL)) + __FAIL ("could not establish EGL Display connection."); + + EGLint DESIRED_ATTRIBS[] + {EGL_COLOR_BUFFER_TYPE, EGL_RGB_BUFFER + ,EGL_RED_SIZE, 4 + ,EGL_GREEN_SIZE, 4 + ,EGL_BLUE_SIZE, 4 + ,EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT // config must support creating an OpenGL context + ,EGL_SURFACE_TYPE, EGL_WINDOW_BIT // want to create a window surface + ,EGL_DEPTH_SIZE, 0 // prefer config without depth buffer (occlusion testing not needed) + ,EGL_STENCIL_SIZE, 0 // prefer config without stencil buffer (no advanced visual effects) + ,EGL_NONE}; + + EGLint _cnt; + EGLConfig config; + if (not eglChooseConfig (ctx.display + ,DESIRED_ATTRIBS + ,& config + ,1,&_cnt) + or not config) + __FAIL ("unable to select a EGL display config with the required attributes"); + + + ctx.surface = eglCreateWindowSurface (ctx.display,config,xWindow,nullptr); + if (not ctx.surface) + switch (eglGetError()) + { + case EGL_BAD_CONFIG: + __FAIL ("selected display configuration is not valid"); + break; + case EGL_BAD_ALLOC: + __FAIL ("unable to allocate resources or collision with existing resources"); + break; + case EGL_BAD_MATCH: + __FAIL ("mismatch between X11 window visuals and the capabilities of the selected config"); + break; + default: + __FAIL ("unable to create EGL window surface, for unknown reasons"); + break; + } + + if (not eglBindAPI( EGL_OPENGL_API)) + __FAIL ("unable to configure EGL for usage with the OpenGL API."); + + EGLint CTX_ATTRIBS[] // Note: require modern OpenGL + {EGL_CONTEXT_OPENGL_PROFILE_MASK, EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT +// ,EGL_CONTEXT_MAJOR_VERSION, 3 +// ,EGL_CONTEXT_MINOR_VERSION, 3 + ,EGL_NONE}; + + ctx.egl = eglCreateContext (ctx.display, config + ,EGL_NO_CONTEXT // do not share definitions and data with another context + ,CTX_ATTRIBS); + if (not ctx.egl) + __FAIL ("failed to create OpenGL context for this display with desired visuals"); + + // create a binding for the current thread to use this context on this window + if (not eglMakeCurrent (ctx.display, ctx.surface,ctx.surface, ctx.egl)) + __FAIL ("failed to attach an OpenGL context to the application X-Window"); + + + // use lib-GLEW to manage OpenGL extensions (notably GL Shader Language) + GLenum glewErr = glewInit(); + if (GLEW_OK != glewErr) + __FAIL ("could not bind OpenGL extensions through lib-GLEW:\n" + +string{reinterpret_cast (glewGetErrorString(glewErr))}); + + + auto compileShader = [](GLenum kind, string code) + { + uint shaderID = glCreateShader (kind); + const GLchar* codeSegment = code.c_str(); + glShaderSource (shaderID, 1, &codeSegment, NULL); + glCompileShader(shaderID); + // diagnostics... + int success; + glGetShaderiv (shaderID, GL_COMPILE_STATUS, &success); + if (not success) + { + int logSiz{0}; + glGetShaderiv (shaderID, GL_INFO_LOG_LENGTH, &logSiz); + string report(logSiz,'.'); + glGetShaderInfoLog (shaderID, logSiz, NULL, & report[0]); + std::cout << "\n■□■□■□■□■----Shader-Code-------------------------\n" + << code + << "\n■□■□■□■□■----Failure-Report----------------------\n" + << report.c_str() + << std::endl; + __FAIL ("shader compilation failure"); + } + return shaderID; + }; + + auto checkLinkError = [](uint programID) + { + int success; + glGetProgramiv (programID, GL_LINK_STATUS, &success); + if (not success) + { + int logSiz{0}; + glGetProgramiv (programID, GL_INFO_LOG_LENGTH, &logSiz); + string report(logSiz,'.'); + glGetProgramInfoLog (programID, logSiz, NULL, & report[0]); + std::cout << "\n■□■□■□■□■----Failure-Report----------------------\n" + << report.c_str() + << std::endl; + __FAIL ("unable to link shader code"); + } + }; + + + uint vertexShader + = compileShader (GL_VERTEX_SHADER, + R"-(#version 330 core + + layout(location = 0) in vec2 position; + layout(location = 1) in vec2 texCoord; + + out vec2 fragTexCoord; + + void main() { + gl_Position = vec4 (position, 0.0, 1.0); + fragTexCoord = texCoord; + } + )-"); + + uint fragmentShader + = compileShader (GL_FRAGMENT_SHADER, + R"-(#version 330 core + + in vec2 fragTexCoord; + out vec4 colour; + + uniform sampler2D textureSampler; + + void main() { + colour = texture (textureSampler, fragTexCoord); + } + )-"); + + uint gpuProgram = glCreateProgram(); + glAttachShader (gpuProgram, vertexShader); + glAttachShader (gpuProgram, fragmentShader); + glLinkProgram (gpuProgram); + checkLinkError (gpuProgram); + + glUseProgram (gpuProgram); + glDeleteShader(vertexShader); + glDeleteShader(fragmentShader); + + + /* === setup a 2D texture, to be mapped into the viewport === */ + glActiveTexture(GL_TEXTURE0); // activate texture-unit #0 on the GPU + glGenTextures (1, &ctx.texID); // allocate 1 new texture ID + glBindTexture (GL_TEXTURE_2D, ctx.texID); // use this textureID as "the" TEXTURE_2D + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); // configure image scaling filter + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); // clamp, don't wrap at the texture edges + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + + // configure the `textureSampler` parameter (»uniform« is a global parameter visible in all shaders) + GLint samplerParam = glGetUniformLocation(gpuProgram, "textureSampler"); + glUniform1i(samplerParam, 0); // configure the shader to use texture-unit #0 of the GPU + + + /* === setup a fixed geometry to hold that texture === */ + glDisable (GL_DEPTH_TEST); // we do not need 3D layering / positioning + glViewport (0, 0, ctx.VIDEO_WIDTH, ctx.VIDEO_HEIGHT); // Origin in the middle of the window (note Y points upwards) + + // Note: this demo uses a fixed-size window and hard-coded video size; + // a real-world implementation would have to place the video frame + // dynamically into the available screen space, possibly scaling up/down + GLfloat sX{ctx.scaleX}; + GLfloat sY{ctx.scaleY}; // Note: positions use »normalised device coordinates« (NDC) + GLfloat w{1}; // TEXTURE_2D points cover the range (0,0) ... (1,1) + GLfloat h{1}; + + GLfloat vertexData[] // Geometry and mapping data to be sent to the GPU... + //-point-+--texture // We define the 4 edges of a rectangular »projection screen« + {-sX,-sY , 0,h // For each edge, we define the position in NDC [-1 ... +1] + , sX,-sY , w,h // and we specify which point in the texture to map there + , sX, sY , w,0 // Note: the rows of the video image run top-down, and thus we + ,-sX, sY , 0,0 // have to map the texture flipped, since OpenGL orients Y upwards. + }; + + glGenBuffers (1, &ctx.vboID); // allocate 1 new vertex-buffer-object + glGenVertexArrays(1, &ctx.vaoID); // allocate 1 new vertex-array-object (to attach all definitions) + + glBindVertexArray(ctx.vaoID); // activate the VAO to pick up the following definitions... + glBindBuffer (GL_ARRAY_BUFFER, ctx.vboID); + // copy the vertexData into the vertex-buffer-object currently bound to GL_ARRAY_BUFFER + glBufferData (GL_ARRAY_BUFFER, sizeof(vertexData), vertexData, GL_STATIC_DRAW); + + auto SIZ = sizeof(float); // parameters are transferred to the GPU as float data + auto attrib = 2; // we use 2 input attributes for the shader pipeline + auto coords = 2; // our data vectors have 2 coordinates (2D) + auto stride = attrib * coords * SIZ; // stride is the offset in bytes between full data points + auto offset = [&](uint elm){ return (void*) (elm * SIZ); }; // offset of one attribute relative to the start of a data point (packaged into a void*) + auto normalise = GL_FALSE; // do not auto-normalise, positions are already [-1 ... +1] + + // define how two two attributes are packed into the data sequence we send to the GPU + glVertexAttribPointer (0, coords, GL_FLOAT, normalise, stride, offset(0) ); + glVertexAttribPointer (1, coords, GL_FLOAT, normalise, stride, offset(2) ); + glEnableVertexAttribArray (0); // activate attribute #1 ≙ input argument `position` in vertexShader + glEnableVertexAttribArray (1); // activate attribute #2 ≙ input argument `texCoord` in vertexShader + + + // hand-over the activated connection context + // to be managed by the GTK application... + std::cout << "Started playback at "<{"demo.egl"} + .onStart (openDisplay) + .onFrame (displayFrame) + .onClose (cleanUp) + .run(30); +} diff --git a/FrOSCon26/examples/gtk-uring-app.hpp b/FrOSCon26/examples/gtk-uring-app.hpp new file mode 100644 index 0000000..0124642 --- /dev/null +++ b/FrOSCon26/examples/gtk-uring-app.hpp @@ -0,0 +1,122 @@ +/* + gtk-uring-app.hpp - simple GTK Host Application + + Copyright (C) + 2025, Benny Lyons + 2025,2026 Hermann Vosseler + + This program is free software; you can redistribute it and/or modify it +  under the terms of the GNU GPL version 2+ See the LICENSE file for details. + +* ************************************************************************/ + +#ifndef GTK_URING_APP_H +#define GTK_URING_APP_H + +#include "commons.hpp" + +#include +#include +#include +#include + +#include +#include + +using std::unique_ptr; +using Glib::ustring; +using std::move; + + + +/** + * Minimalist GTK application, used as framework for a demo of reading and displaying video. + * @tparam CTX custom data context to allocate while periodic processing is active + * @remark The actual actions can be installed as functors / callbacks + * - onClick() installs a function to be invoked when the button is first clicked; + * this function must return a _context object,_ which represents "the process" + * and will be copied into heap storage; the further functors will receive context. + * - onClose(CTX&) will be invoked when the application is shut down + */ +template +class GtkUringApp + : public Gtk::Application + { + class DemoWindow + : public Gtk::Window + { + public: + DemoWindow() + { +// set_default_size (CTX::VIDEO_WIDTH, CTX::VIDEO_HEIGHT); + set_resizable (false); + add (button_); + button_.show(); + } + + Gtk::Button button_{"click to start video..."}; + }; + + DemoWindow demoWindow_; + + + public: + GtkUringApp (ustring appID) + : Gtk::Application{appID} + { } + + + using StartTask = std::function(Gtk::Window&)>; + using CloseTask = std::function; + + GtkUringApp& + onClick (StartTask task) + { + startTask_ = move(task); + return *this; + } + GtkUringApp& + onClose (CloseTask task) + { + closeTask_ = move(task); + return *this; + } + + int + run() + { + demoWindow_.button_.signal_clicked().connect( + sigc::mem_fun (*this, GtkUringApp::triggerProcessing)); + + return Gtk::Application::run (demoWindow_); + } // blocks while application is active + + + private: + std::unique_ptr processor_; + StartTask startTask_; + CloseTask closeTask_; + + void + triggerProcessing() + { + if (processor_) return; + + if (startTask_) + { + processor_ = startTask_(demoWindow_); + + demoWindow_.button_.set_sensitive(false); // disable the button + demoWindow_.button_.set_label("active"); + + auto closeHook = [this]{ + if (closeTask_) + closeTask_(*processor_); + }; + + this->signal_shutdown().connect (closeHook); + } + } + }; + +#endif /*GTK_URING_APP_H*/