Prototype: establish a top-level structure

leaning heavily on the similar structure used for last year's
FrOSCon talk regarding video output (by Benny Lyons and myself).

However, this time we'll use a process state object (the player)
and, furthermore, there is some cross-wiring of components involved...
This commit is contained in:
Fischlurch 2026-07-14 19:27:27 +02:00
parent e0d4de5737
commit b92de63d25
5 changed files with 729 additions and 127 deletions

View file

@ -852,16 +852,13 @@
<node CREATED="1783788564665" ID="ID_941883231" MODIFIED="1783788584317" TEXT="&#xfc;bernehme den Projektrahmen vom FrOSCon-Talk letztes Jahr (Video Output)"/>
<node CREATED="1783788586075" ID="ID_449956672" MODIFIED="1783788647212" TEXT="allerdings vereinfacht auf ein einziges Demo-Executable">
<richcontent TYPE="NOTE"><html>
<head>
</head>
<head/>
<body>
<p>
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
</p>
</body>
</html>
</richcontent>
</html></richcontent>
</node>
<node CREATED="1783788648324" ID="ID_1920636432" MODIFIED="1783788665326" TEXT="nehme sofort die Dependencies f&#xfc;r OpenGL / EGL mit dazu"/>
<node BACKGROUND_COLOR="#c8c0b6" COLOR="#435e98" CREATED="1783819908913" ID="ID_813164182" MODIFIED="1783820014700" TEXT="Liburing">
@ -916,16 +913,13 @@
<node CREATED="1783901091272" ID="ID_504921767" MODIFIED="1783901102000" TEXT="ich kl&#xe4;re die (mir ist die Entscheidung schon klar)">
<node CREATED="1783901105001" ID="ID_406126832" MODIFIED="1783901129762" TEXT="den Talk so einfach wie m&#xf6;glich halten">
<richcontent TYPE="NOTE"><html>
<head>
</head>
<head/>
<body>
<p>
die Zuh&#246;rer sind keine Systemprogrammierer
</p>
</body>
</html>
</richcontent>
</html></richcontent>
</node>
<node CREATED="1783901133887" ID="ID_1114781881" MODIFIED="1783901146536" TEXT="daher die einfachen userspace-Buffer und das normale read()"/>
<node CREATED="1783901151199" ID="ID_168398984" MODIFIED="1783901166527" TEXT="selbstverst&#xe4;ndlich will ich eine echte pipeline, d.h. mehrere Buffer">
@ -978,22 +972,17 @@
</node>
<node CREATED="1783902236744" ID="ID_1546055488" MODIFIED="1783902253577">
<richcontent TYPE="NODE"><html>
<head>
</head>
<head/>
<body>
<p>
allerdings: der Worker w&#228;re damit ein <b>Proactor</b>
</p>
</body>
</html>
</richcontent>
</html></richcontent>
</node>
<node CREATED="1783902258661" ID="ID_67058752" MODIFIED="1783902369689" TEXT="wollte ich zwar erst nicht, k&#xf6;nnte mich aber mit dem Gedanken anfreunden">
<richcontent TYPE="NOTE"><html>
<head>
</head>
<head/>
<body>
<p>
....es w&#228;re n&#228;mlich tats&#228;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&#228;tzlich etwas zu machen, was funktioniert wie in Lumiera geplant. Aber wenn ich mir das <i>wirklich vom didaktischen Gesichtspunkt her nochmal &#252;berlege,</i>&#160;ist es nicht so geschickt.
</p>
</body>
</html>
</richcontent>
</html></richcontent>
</node>
</node>
</node>
@ -1015,16 +1003,13 @@
<node CREATED="1783902573851" ID="ID_451286400" MODIFIED="1783902590704" TEXT="wenn ein Frame ready ist, sollte man lediglich dem GUI-Thread signalisieren"/>
<node CREATED="1783902591217" ID="ID_1763048000" MODIFIED="1783902681362" TEXT="er schl&#xe4;gt daf&#xfc;r g_idle_add() vor -- das sei low-level GLib und weithin etabliert">
<richcontent TYPE="NOTE"><html>
<head>
</head>
<head/>
<body>
<p>
...es w&#252;rde zur &quot;toolkit, nicht framework&quot;-Policy passen, und zudem w&#228;re das Signal explizit von jedem Thread aus triggerbar
</p>
</body>
</html>
</richcontent>
</html></richcontent>
</node>
<node CREATED="1783902821732" ID="ID_1465201307" MODIFIED="1783902838122" TEXT="getriggert w&#xfc;rde dann der Code, der die OpenGL-Pipeline mit neuen Daten versorgt"/>
<node CREATED="1783902838536" ID="ID_1259653595" MODIFIED="1783902845946" TEXT="dieser w&#xfc;rde mithin im GTK-Thread laufen"/>
@ -1076,7 +1061,33 @@
<node BACKGROUND_COLOR="#e0ceaa" COLOR="#520f69" CREATED="1783904751803" ID="ID_648202546" MODIFIED="1783904801052" TEXT="lasse mir von Claude zwei Code-Templates generieren">
<icon BUILTIN="edit"/>
<node CREATED="1783904765875" ID="ID_1730135150" MODIFIED="1783904774302" TEXT="die C-L&#xf6;sung mit g_idle_add()"/>
<node CREATED="1783904774961" ID="ID_795180608" MODIFIED="1783904783468" TEXT="die Gtkmm-L&#xf6;sung mit dem Glib::Dispatcher"/>
<node BACKGROUND_COLOR="#accdc3" COLOR="#3f4015" CREATED="1783904774961" ID="ID_795180608" MODIFIED="1783984701307" STYLE="fork" TEXT="die Gtkmm-L&#xf6;sung mit dem Glib::Dispatcher">
<richcontent TYPE="NOTE"><html>
<head/>
<body>
<p>
// --- GUI thread, once at setup ---
</p>
<p>
Glib::Dispatcher frameReady;
</p>
<p>
frameReady.connect([this]{ displayFrame(*processor_); });&#160;&#160;// upload + draw + eglSwapBuffers()
</p>
<p>
</p>
<p>
// --- worker thread, once a frame is ready ---
</p>
<p>
frameReady.emit();&#160;&#160;&#160;// thread-safe; wakes the GUI thread's main loop
</p>
</body>
</html></richcontent>
<edge COLOR="#808080" STYLE="bezier" WIDTH="thin"/>
<icon BUILTIN="wizard"/>
</node>
</node>
</node>
<node CREATED="1783952838505" ID="ID_1257919936" MODIFIED="1783952858029" TEXT="Worker Thread and State organisation">
@ -1112,22 +1123,17 @@
<node CREATED="1783953153918" ID="ID_1754327495" MODIFIED="1783953520369" TEXT="ich sage: f&#xfc;r async IO braucht man immer eine Art State-Engine">
<node CREATED="1783953183906" ID="ID_339096490" MODIFIED="1783953350863" TEXT="Claude widerspricht nicht">
<richcontent TYPE="NOTE"><html>
<head>
</head>
<head/>
<body>
<p>
Ich verwende das aktuelle &quot;Sonnet&quot; auf Stufe Medium. Das braucht erhebliche Rechenzeit f&#252;r jede Antwort auf diesem Diskussionslevel (und zeigt auch jeweils expandierende, schlu&#223;folgernde und kondensierende Phasen an)
</p>
</body>
</html>
</richcontent>
</html></richcontent>
</node>
<node CREATED="1783953200905" ID="ID_1912082579" MODIFIED="1783953252578" TEXT="aber er fa&#xdf;t die von mir genannten Optionen zusammen in zwei Klassen">
<richcontent TYPE="NOTE"><html>
<head>
</head>
<head/>
<body>
<p http-equiv="content-type" content="text/html; charset=utf-8" class="font-claude-response-body break-words whitespace-normal">
<strong>Proactor, confirmed.</strong>&#160;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 &#8212; as opposed to a reactor (epoll-style), where you're told &quot;this fd is ready,&quot; and <em>you</em>&#160;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 &quot;the real solution&quot; &#8212; 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 <em>audience cognitive load</em>&#160;&#8212; introducing C++20 coroutine machinery to explain a 3-state pipeline would be a bad trade for this talk).
</p>
</body>
</html>
</richcontent>
</html></richcontent>
</node>
<node CREATED="1783953439647" ID="ID_478135892" MODIFIED="1783953440909" TEXT="enum class SlotState { Empty, Submitted, Ready };"/>
<node CREATED="1783953460141" ID="ID_1859865790" MODIFIED="1783954625972">
<richcontent TYPE="NODE"><html>
<head>
</head>
<head/>
<body>
<p>
std::<b>atomic</b>&lt;SlotState&gt;
</p>
</body>
</html>
</richcontent>
</html></richcontent>
<node BACKGROUND_COLOR="#accdc3" COLOR="#3f4015" CREATED="1783953478348" ID="ID_186490339" MODIFIED="1783954023236" STYLE="fork" TEXT="with release-on-write / acquire-on-read">
<richcontent TYPE="NOTE"><html>
<head>
</head>
<head/>
<body>
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 @@
<code http-equiv="content-type" content="text/html; charset=utf-8" class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">std::atomic&lt;SlotState&gt;</code>&#160;with release-on-write / acquire-on-read is enough &#8212; 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 <em>buffer contents</em>&#160;the worker wrote, not just the state flag &#8212; a clean, concrete illustration of why acquire/release exists, tied directly to something the audience just watched you build.
</p>
</body>
</html>
</richcontent>
</html></richcontent>
<edge COLOR="#808080" STYLE="bezier" WIDTH="thin"/>
<icon BUILTIN="wizard"/>
</node>
@ -1234,16 +1233,13 @@
<node CREATED="1783954256289" ID="ID_582576620" MODIFIED="1783954281086" TEXT="&#xbb;Backpressure&#xab; falles out for free with this design"/>
<node CREATED="1783954286225" ID="ID_681083402" MODIFIED="1783954360400" TEXT="sollte nextFrame und Frame-Pos im Quellfile konzeptionell trennen">
<richcontent TYPE="NOTE"><html>
<head>
</head>
<head/>
<body>
<p>
...obschon man sie auf dem gleichen lokalen State aufbaut. Das Beispiel, das Claude nennt: man k&#246;nnte das Quellfile loopen
</p>
</body>
</html>
</richcontent>
</html></richcontent>
</node>
</node>
</node>
@ -1254,9 +1250,7 @@
<node CREATED="1783956516487" ID="ID_1971249448" MODIFIED="1783956541623" TEXT="Claude schreibt mir drei Code-Snippets">
<node CREATED="1783956542891" ID="ID_922011151" MODIFIED="1783956927430" TEXT="Setup von Uring und Timer + initiales Bef&#xfc;llen">
<richcontent TYPE="NOTE"><html>
<head>
</head>
<head/>
<body>
<p>
struct io_uring ring;
@ -1316,14 +1310,11 @@
io_uring_submit(&amp;ring);&#160;&#160;&#160;// one syscall flushes everything prepped above
</p>
</body>
</html>
</richcontent>
</html></richcontent>
</node>
<node CREATED="1783956564734" ID="ID_519769608" MODIFIED="1783956917195" TEXT="Skelett der Proactor-Loop">
<richcontent TYPE="NOTE"><html>
<head>
</head>
<head/>
<body>
<p>
for (;;) {
@ -1362,31 +1353,24 @@
}
</p>
</body>
</html>
</richcontent>
</html></richcontent>
</node>
<node CREATED="1783956594083" ID="ID_1956714359" MODIFIED="1783956898511" TEXT="onTick + follow-up">
<richcontent TYPE="NOTE"><html>
<head>
</head>
<head/>
<body>
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 &#8212; 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(&amp;ring); }
</body>
</html>
</richcontent>
</html></richcontent>
<node CREATED="1783956613435" ID="ID_121998302" MODIFIED="1783956875196" TEXT="Claude erl&#xe4;utert dazu....">
<richcontent TYPE="NOTE"><html>
<head>
</head>
<head/>
<body>
<p>
The reason top-up belongs in <code>onTick</code>&#160;rather than in <code>onReadComplete</code>:<br />a slot only becomes reusable once the <em>GUI thread</em>&#160;has finished drawing it and flips it back to <code>Empty</code>&#160;&#160;&#8212; and the worker, sitting blocked in <code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">io_uring_wait_cqe</code>, has no way to know that happened except by waking up for <em>something</em>. The tick is that guaranteed periodic wake-up, so it doubles as &quot;check if there's new capacity to fill,&quot; with a worst-case latency of one tick period (~40ms) before a freed slot gets reused &#8212; 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.
</p>
</body>
</html>
</richcontent>
</html></richcontent>
</node>
</node>
</node>
@ -1471,16 +1455,13 @@
</node>
<node COLOR="#338800" CREATED="1783964942767" ID="ID_993102807" MODIFIED="1783966105865" TEXT="lasse Claude die Diskrepanz herausarbeiten gegen&#xfc;ber dem Proactor-Pattern">
<richcontent TYPE="NOTE"><html>
<head>
</head>
<head/>
<body>
<p>
best&#228;tigt meine Analyse, ohne Diskrepanz
</p>
</body>
</html>
</richcontent>
</html></richcontent>
<icon BUILTIN="button_ok"/>
</node>
<node CREATED="1783966114659" ID="ID_325758280" MODIFIED="1783966124441" TEXT="Claude identifiziert Problemzonen">
@ -1500,37 +1481,29 @@
</node>
<node CREATED="1783966316107" ID="ID_1390907760" MODIFIED="1783966366559" TEXT="ich geme mein Reasoning bez&#xfc;glich Ring-Ownership">
<richcontent TYPE="NOTE"><html>
<head>
</head>
<head/>
<body>
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 &quot;mutual exclusive&quot; 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?
</body>
</html>
</richcontent>
</html></richcontent>
<node BACKGROUND_COLOR="#accdc3" COLOR="#3f4015" CREATED="1783966388626" ID="ID_857234213" MODIFIED="1783966398679" STYLE="fork" TEXT="yeour reasoning holds, with one caveat that matters a lot and a couple of smaller ones.">
<edge COLOR="#808080" STYLE="bezier" WIDTH="thin"/>
<icon BUILTIN="wizard"/>
</node>
<node BACKGROUND_COLOR="#e0ceaa" COLOR="#4a0f69" CREATED="1783966412590" ID="ID_1533910419" MODIFIED="1783966530095" TEXT="Achtung: acquire / release - Semantik is notwendig auf dem Grooming-Token">
<richcontent TYPE="NOTE"><html>
<head>
</head>
<head/>
<body>
<p>
Ja, wei&#223; ich. Deshalb hab ich's so programmiert. Freut mich da&#223; Claude da unabh&#228;ngig zum gleichen Schlu&#223; kommt (denn ich hab mich oft gefragt, ob ich mich da in Spitzfindigkeiten verrenne).<br />Also nochmal explizit: memory_order_relaxed reicht hier nicht<br />
</p>
</body>
</html>
</richcontent>
</html></richcontent>
<icon BUILTIN="messagebox_warning"/>
</node>
<node CREATED="1783966538813" ID="ID_340512988" MODIFIED="1783966733292" TEXT="das GroomingToken und die Barrieren in liburing dienen verschiedenen Zwecken">
<richcontent TYPE="NOTE"><html>
<head>
</head>
<head/>
<body>
<p>
Einmal geht es um shared-data zwischen Kernel und Userspace. Wohingegen das Grooming-Token die Sichtbarkeit &#252;ber die Worker hinweg steuert.
@ -1542,8 +1515,7 @@
Diese Unterscheidung ist konzeptionell (sehe ich so, Claude deutet es auch so). Beide Ans&#228;tze st&#246;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&#246;glich w&#228;re (wie hier der Fall, aber nur weil keine data-dependenciy im memory-ordering includiert ist)
</p>
</body>
</html>
</richcontent>
</html></richcontent>
<icon BUILTIN="idea"/>
</node>
<node BACKGROUND_COLOR="#f8f1cb" COLOR="#a50125" CREATED="1783966752311" ID="ID_1363514128" MODIFIED="1783966837345" TEXT="whatever you do inside the token-held section must never block! ">
@ -1649,16 +1621,13 @@
<icon BUILTIN="idea"/>
<node BACKGROUND_COLOR="#accdc3" COLOR="#3f4015" CREATED="1783967583375" ID="ID_1425666844" MODIFIED="1783967597761" STYLE="fork" TEXT="">
<richcontent TYPE="NOTE"><html>
<head>
</head>
<head/>
<body>
<p>
<strong http-equiv="content-type" content="text/html; charset=utf-8">Current 2 &#8212; thread-per-core, shared-nothing, and it's picking up real steam.</strong>&#160;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 &quot;work steering&quot; &#8212; 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 &#8212; each thread in Glommio gets its own private set of rings instead, which is the thread-per-core answer to the same &quot;who owns the ring&quot; question you and I just spent a while on. <a href="https://iggy.apache.org/blogs/2026/02/27/thread-per-core-io_uring/" target="_blank" class="group/tag relative h-[18px] rounded-full inline-flex items-center overflow-hidden -translate-y-px cursor-pointer">Apache + 2</a>
</p>
</body>
</html>
</richcontent>
</html></richcontent>
<edge COLOR="#808080" STYLE="bezier" WIDTH="thin"/>
<icon BUILTIN="wizard"/>
</node>
@ -1802,59 +1771,47 @@
</node>
<node CREATED="1783968264384" ID="ID_1390930082" MODIFIED="1783968317234" TEXT="das ist ausreichend um die Cross-Thread-Visibility sicherzustellen">
<richcontent TYPE="NOTE"><html>
<head>
</head>
<head/>
<body>
<p>
aber man sollte konzeptionell zwischen der visibility mit dem Kernel und der Visibility zwischen den Workern unterscheiden; f&#252;r ersteres gibt es die Barrieren in Liburing
</p>
</body>
</html>
</richcontent>
</html></richcontent>
</node>
<node BACKGROUND_COLOR="#e0ceaa" COLOR="#690f14" CREATED="1783968339226" ID="ID_52764530" MODIFIED="1783968361355" TEXT="Blocken in der Grooming-Phase verhindern &#x27f6; sonst Engine-Stall">
<icon BUILTIN="clanbomber"/>
</node>
<node CREATED="1783968381176" ID="ID_291465589" MODIFIED="1783968485845" TEXT="Flow/Capacity-Steuerung ist essentiell notwendig">
<richcontent TYPE="NOTE"><html>
<head>
</head>
<head/>
<body>
<p>
der Umstand da&#223; neue IO-Aktivit&#228;ten gescheduled werden und Completions entnommen werden m&#252;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&#228;uft.
</p>
</body>
</html>
</richcontent>
</html></richcontent>
</node>
<node CREATED="1783968535936" ID="ID_1390321079" MODIFIED="1783968546673" TEXT="Einordnung in gegenw&#xe4;rtige Trends">
<node CREATED="1783968547646" ID="ID_1203574703" MODIFIED="1783968635282" TEXT="Scheduler/Worker-Pattern wird standardisiert (C++26)">
<richcontent TYPE="NOTE"><html>
<head>
</head>
<head/>
<body>
<p>
Es ist eine etablierte und bew&#228;hrte Methode, und wird nicht weggehen. Was wir geschaffen haben konvergiert gut mit den allgemeinen Einsichten im Feld
</p>
</body>
</html>
</richcontent>
</html></richcontent>
</node>
<node CREATED="1783968562254" ID="ID_1154684301" MODIFIED="1783968600700" TEXT="High-Performance-Computing: thread-per-core no shared data">
<richcontent TYPE="NOTE"><html>
<head>
</head>
<head/>
<body>
<p>
das ist ein Gegentrend, der aber auf einer anderen Gr&#246;&#223;enordnung arbeitet (im Nanosekunden-Bereich)
</p>
</body>
</html>
</richcontent>
</html></richcontent>
</node>
<node CREATED="1783968636663" ID="ID_1048046164" MODIFIED="1783968651582" TEXT="sollte an sehr hohe Anzahl an Cores denken">
<icon BUILTIN="messagebox_warning"/>
@ -1863,6 +1820,93 @@
</node>
</node>
</node>
<node CREATED="1783984352027" ID="ID_1602724940" MODIFIED="1783984359484" TEXT="Aufbau der Application-Shell">
<node CREATED="1783984371031" ID="ID_1219649133" MODIFIED="1783984401222" TEXT="das Grundschema von letztem Jahr war n&#xfc;tzlich">
<node CREATED="1783984407459" ID="ID_321798557" MODIFIED="1783984427656" TEXT="&#x201e;mu&#xdf; den GTK-Code gar nicht anschauen&#x201c;"/>
<node CREATED="1783985102373" ID="ID_734300736" MODIFIED="1783985128117" TEXT="Aber: die Ablaufsteuerung liegt jetzt nicht mehr (komplett) in GTK">
<icon BUILTIN="messagebox_warning"/>
<node BACKGROUND_COLOR="#e0ceaa" COLOR="#690f14" CREATED="1783985519074" ID="ID_1873813285" MODIFIED="1783985537906" TEXT="&#x27f9; mu&#xdf; nun einen Frame-Callback (cross-Thread) realisieren">
<icon BUILTIN="messagebox_warning"/>
</node>
<node CREATED="1783985135020" ID="ID_1062164422" LINK="https://web.archive.org/web/20200806190801/https://developer.gnome.org/glibmm/stable/classGlib_1_1Dispatcher.html" MODIFIED="1783985145748" TEXT="Glib::Dispatcher">
<icon BUILTIN="list"/>
</node>
<node CREATED="1783985286044" ID="ID_1662752917" MODIFIED="1783985548196" TEXT="Regeln">
<icon BUILTIN="info"/>
<node CREATED="1783985294267" ID="ID_1295877512" MODIFIED="1783985308444" TEXT="ein receiver-Thread, aber Trigger von jedem anderen Thread"/>
<node CREATED="1783985309048" ID="ID_1248615877" MODIFIED="1783985320489" TEXT="im receiver-Thread mu&#xdf; eine Glib-Loop laufen"/>
<node CREATED="1783985321418" ID="ID_1994550502" MODIFIED="1783985333946" TEXT="ein Dispatcher-Objekt...">
<node CREATED="1783985334643" ID="ID_291484889" MODIFIED="1783985343649" TEXT="mu&#xdf; im Receiver-Thread erzeugt und zerst&#xf6;rt werden"/>
<node CREATED="1783985344196" ID="ID_1733483538" MODIFIED="1783985362637" TEXT="sollte vor allen anderen Threads erzeugt werden (&#x27f9; Sichtbarkeit)"/>
</node>
<node CREATED="1783985372148" ID="ID_1967828108" MODIFIED="1783985396343" TEXT="alle Dispatcher-Objekte eines receiver(Loop)-Threads verwenden eine einzige Pipe">
<node CREATED="1783985399747" ID="ID_720330070" MODIFIED="1783985405923" TEXT="Locks niemals auf der Senderseite"/>
</node>
</node>
<node CREATED="1783985980271" ID="ID_1205516934" MODIFIED="1783985994016" TEXT="Konstruktions-Problem: Verdrahtung">
<node CREATED="1783985998296" ID="ID_602134117" MODIFIED="1783986005435" TEXT="die Verdrahtung ist jetzt kreuzweise">
<node CREATED="1783986006656" ID="ID_1853634036" MODIFIED="1783986020234" TEXT="GtkApp &#x27f6; Setup"/>
<node CREATED="1783986025918" ID="ID_905927115" MODIFIED="1783986037848" TEXT="Setup &#x27f9; erzeugt Player"/>
<node CREATED="1783986070733" ID="ID_1828650606" MODIFIED="1783986085969" TEXT="GtkApp &#x27f9; erzeugt Dispatcher f&#xfc;r Player"/>
<node CREATED="1783986088261" ID="ID_863448551" MODIFIED="1783986143522" TEXT="Player &#x27f6; OpenGL-Display"/>
<node CREATED="1783986156014" ID="ID_1075245760" MODIFIED="1783986166367" TEXT="Player &#x27f6; Frame-Signal"/>
<node CREATED="1783986171122" ID="ID_931723726" MODIFIED="1783986184677" TEXT="GtkApp &#x27f6; Player-Stop"/>
</node>
<node BACKGROUND_COLOR="#f0d5c5" COLOR="#990033" CREATED="1783986532114" ID="ID_1245562739" MODIFIED="1783986543992" TEXT="Knackpunkt: wo sitzt der Dispatcher?">
<icon BUILTIN="help"/>
<node COLOR="#5b280f" CREATED="1783986546063" ID="ID_1573759034" MODIFIED="1783986688651" TEXT="L&#xf6;sung-1 : in der GtkApp">
<icon BUILTIN="button_cancel"/>
<node CREATED="1783986575996" ID="ID_236050628" MODIFIED="1783986584350" TEXT="Vorteil: nat&#xfc;rlicherweise die GTK-Verbindung"/>
<node CREATED="1783986584944" ID="ID_478484131" MODIFIED="1783986600350" TEXT="bleibt damit Interna der GTK-App"/>
<node CREATED="1783986603534" ID="ID_1364662813" MODIFIED="1783986612304" TEXT="Nachteil: Verdrahtung nicht offensichtlich"/>
</node>
<node CREATED="1783986560783" ID="ID_1335290152" MODIFIED="1783986686480" TEXT="L&#xf6;sung-2 : im Player">
<icon BUILTIN="back"/>
<node CREATED="1783986627477" ID="ID_281071387" MODIFIED="1783986637023" TEXT="Vorteil: Verdrahtung offensichtlich und einfach"/>
<node CREATED="1783986637869" ID="ID_1603304" MODIFIED="1783986655939" TEXT="Nachteil : GTK-spezifisches Detail leaked"/>
<node CREATED="1783986737099" ID="ID_1043412300" MODIFIED="1783986749522" TEXT="Nachteil: Thread-Bezug ist implizit"/>
</node>
<node CREATED="1783986666480" ID="ID_1770044442" MODIFIED="1783986681514" TEXT="Entscheidung: L&#xf6;sung-2">
<icon BUILTIN="yes"/>
<node CREATED="1783986693369" ID="ID_1838622028" MODIFIED="1783986710414" TEXT="GTK-Bez&#xfc;ge sind auch f&#xfc;r das EglDisplay wichtig"/>
<node CREATED="1783986712145" ID="ID_625334732" MODIFIED="1783986726835" TEXT="Dispatcher ist zentraler Bestandteil meiner Demo"/>
<node CREATED="1783986758056" ID="ID_223369723" MODIFIED="1783986770513" TEXT="auch das Thema &#xbb;Threading&#xab; geh&#xf6;rt in den Scope der Demo"/>
</node>
</node>
<node CREATED="1784045996386" ID="ID_1209517516" MODIFIED="1784046001812" TEXT="immer noch schwierig....">
<node CREATED="1784046009218" ID="ID_20536040" MODIFIED="1784046024950" TEXT="jetzt habe ich ein Objekt &#x2014; das sollte non-copyable sein"/>
<node CREATED="1784046116554" ID="ID_234421285" MODIFIED="1784046238573" TEXT="die Callback-Hook m&#xf6;chte ich aber trotzdem &#x2014; aus didaktischen Gr&#xfc;nden">
<richcontent TYPE="NOTE"><html>
<head/>
<body>
<p>
Hier m&#246;chte ich sowohl den GTK-Code selber verbergen, alsauch dem Umstand da&#223; 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
</p>
</body>
</html></richcontent>
</node>
<node CREATED="1784047131109" ID="ID_34162762" MODIFIED="1784047205771" TEXT="also geht es vor allem darum, die Form aufrecht zu erhalten">
<richcontent TYPE="NOTE"><html>
<head/>
<body>
<p>
dazu geh&#246;rt auch, keine &#187;Tricks&#171; anzuwenden und keine schlechten Praktiken vorzuf&#252;hren. Der Zustands-Tr&#228;ge sollte unique sein, ohne Wenn und Aber.
</p>
</body>
</html></richcontent>
</node>
</node>
</node>
</node>
<node CREATED="1783986793999" ID="ID_1360252421" MODIFIED="1783986816919" TEXT="L&#xf6;sung: baue ein Player-Objekt im Start-Hook">
<node CREATED="1783986820215" ID="ID_360250150" MODIFIED="1783986836070" TEXT="das tritt an die Stelle wo bisher der EglContext stand"/>
<node CREATED="1783986838784" ID="ID_1847977458" MODIFIED="1783986861705" TEXT="L&#xf6;sung: das EglDisplay wird nun ein weiteres Objekt">
<icon BUILTIN="idea"/>
</node>
<node CREATED="1783986866318" ID="ID_623423104" MODIFIED="1783986874503" TEXT="alles per Konstruktor-Verdrahtung"/>
</node>
</node>
</node>
<node CREATED="1783644425567" ID="ID_238189215" MODIFIED="1783644429274" TEXT="Flow-Steuerung">
<node CREATED="1783644430659" ID="ID_269686877" MODIFIED="1783644452555" TEXT="Aufrabe: Vorlauf steuern">
<node CREATED="1783644529049" ID="ID_486151338" MODIFIED="1783644875737" TEXT="zwei Phasen (Chase/Lock)">
@ -1898,22 +1942,17 @@
<icon BUILTIN="button_cancel"/>
<node CREATED="1783786773754" ID="ID_12321607" MODIFIED="1783786935315" TEXT="es w&#xe4;re dieses Mal leicht m&#xf6;glich">
<richcontent TYPE="NOTE"><html>
<head>
</head>
<head/>
<body>
<p>
...anders als bei dem Video-Output-Thema, bei dem wir uns die zentralen Element &#252;berhaupt erst aufschlie&#223;en mu&#223;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&#223; ich nirgends Code abschreiben mu&#223;, den ich nicht auch genauso selber schreiben k&#246;nnte.
</p>
</body>
</html>
</richcontent>
</html></richcontent>
</node>
<node CREATED="1783786937038" ID="ID_782656761" MODIFIED="1783787030571" TEXT="ist das &#xfc;berhaupt noch relevant??">
<richcontent TYPE="NOTE"><html>
<head>
</head>
<head/>
<body>
<ul>
<li>
@ -1924,8 +1963,7 @@
</li>
</ul>
</body>
</html>
</richcontent>
</html></richcontent>
<icon BUILTIN="smily_bad"/>
</node>
</node>

View file

@ -0,0 +1,33 @@
/*
commons.hpp - common definitions and utils for the io_uring video output demo code
Copyright (C)
2026 Hermann Vosseler <Ichthyostega@web.de>
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 <cstdint>
#include <iostream>
#include <string>
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*/

View file

@ -9,11 +9,65 @@
* ************************************************************************/
#include "commons.hpp"
#include "egl-display.hpp"
#include "gtk-uring-app.hpp"
#include <iostream>
#include <memory>
#include <thread>
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<UringVideoPlayer>
launchPlayer (Gtk::Window& appWindow)
{
auto player = std::make_unique<UringVideoPlayer> (appWindow);
return player;
}
void
cleanUp (UringVideoPlayer& player)
{
}
int
main (int, const char*[])
{
std::cout << "Hello Uring" << std::endl;
return GtkUringApp<UringVideoPlayer>{"io-uring-demo"}
.onClick (launchPlayer)
.onClose (cleanUp)
.run();
}

View file

@ -0,0 +1,355 @@
/*
egl-display.hpp - output video from a GTK application, using EGL - OpenGL binding
Copyright (C)
2025, Benny Lyons <benny.lyons@gmx.net>
2025, Hermann Vosseler <Ichthyostega@web.de>
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 <iostream>
#include <algorithm>
#include <cassert>
#include <string>
#include <set>
// for low-level access -> X-Window
#include <gdk/gdkx.h>
// X11, EGL and GLEW for OpenGL
#include <X11/Xlib.h>
#include <EGL/egl.h>
#include <EGL/eglext.h> // defines plattform-specific config and extensions
#include <GL/glew.h> // 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<Gdk::Window> 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<const char*> (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 "<<fps<<" frames/sec." << std::endl;
return ctx;
}
void
displayFrame (EglDisplay& ctx)
{
uint frameNr = ctx.imgGen_.getFrameNr();
uint fps = ctx.imgGen_.getFps();
if (0 == frameNr % fps)
std::cout << "tick ... " << ctx.imgGen_.getFrameNr() << std::endl;
// compute a buffer with RGB data and bind it into the prepared texture...
const void* buffer = ctx.imgGen_.buildNext().data();
glTexImage2D (GL_TEXTURE_2D // the target texture store to work on, here "the" TEXTURE_2D
,0 // detail level (when using mipmap reduction, which we don't)
,GL_RGB // internal format or features to use for this texture
,ctx.VIDEO_WIDTH,ctx.VIDEO_HEIGHT, /*border*/ 0
,GL_RGB // data layout of the provided pixels
,GL_UNSIGNED_BYTE // data format / size of the pixels
,buffer
);
// Draw the quadrilateral
glDrawArrays(GL_QUADS, 0, 4);
// double-buffer flip, automatically invokes glFlush()
eglSwapBuffers (ctx.display, ctx.surface);
}
void
cleanUp (EglDisplay& ctx)
{
std::cout << "STOP " << ctx.imgGen_.getFrameNr() << " frames displayed." << std::endl;
// detach binding with OpenGL context
eglMakeCurrent (ctx.display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
eglDestroySurface (ctx.display, ctx.surface);
eglDestroyContext (ctx.display, ctx.egl);
eglTerminate(ctx.display); // detach EGL from display
}
int
main (int, const char*[])
{
return GtkEglApp<EglDisplay>{"demo.egl"}
.onStart (openDisplay)
.onFrame (displayFrame)
.onClose (cleanUp)
.run(30);
}

View file

@ -0,0 +1,122 @@
/*
gtk-uring-app.hpp - simple GTK Host Application
Copyright (C)
2025, Benny Lyons <benny.lyons@gmx.net>
2025,2026 Hermann Vosseler <Ichthyostega@web.de>
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 <gtkmm/window.h>
#include <gtkmm/button.h>
#include <gtkmm/application.h>
#include <glibmm/dispatcher.h>
#include <functional>
#include <memory>
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 CTX>
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<unique_ptr<CTX>(Gtk::Window&)>;
using CloseTask = std::function<void(CTX&)>;
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<CTX> 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*/