Review: identify and resolve problem with sync logic

''Review supported by Claude Code''

Control flow logic was scrutinised, and a race identified
in the solution to pass on the display action to the GUI thread.

Reconsidering the design, cross-thread synchronisation is now
hinged rather on a single atomic buffer pointer, where the GUI
thread notifies back completion of display work by setting that
pointer to NULL. Overall this simplifies the control logic

----
As an aside, I setup an outline of topics for documentation in `DemoSpec.txt`
and then instructed Claude to fill in the paragraphs, based on insight gained
as part of the code validation / review. Furthermore, I requested to generate
some diagrams that could be useful later when preparting the presentation
This commit is contained in:
Fischlurch 2026-07-18 20:45:01 +02:00
parent 72e1838b36
commit 703e60c85a
8 changed files with 772 additions and 71 deletions

View file

@ -1984,7 +1984,10 @@
<edge COLOR="#808080" STYLE="bezier" WIDTH="thin"/> <edge COLOR="#808080" STYLE="bezier" WIDTH="thin"/>
<linktarget COLOR="#0c8e99" DESTINATION="ID_1043516833" ENDARROW="Default" ENDINCLINATION="556;-26;" ID="Arrow_ID_1416969408" SOURCE="ID_186490339" STARTARROW="None" STARTINCLINATION="1283;82;"/> <linktarget COLOR="#0c8e99" DESTINATION="ID_1043516833" ENDARROW="Default" ENDINCLINATION="556;-26;" ID="Arrow_ID_1416969408" SOURCE="ID_186490339" STARTARROW="None" STARTINCLINATION="1283;82;"/>
<icon BUILTIN="idea"/> <icon BUILTIN="idea"/>
<node BACKGROUND_COLOR="#fefc4e" COLOR="#351d75" CREATED="1784076979882" ID="ID_1843330900" MODIFIED="1784078228911" TEXT="Bemerkenswert: das hat sich durch die Konversation so ergeben"> <node BACKGROUND_COLOR="#fefc4e" COLOR="#351d75" CREATED="1784076979882" ID="ID_1129579308" MODIFIED="1784421369960" TEXT="Bemerkenswert">
<font BOLD="true" NAME="SansSerif" SIZE="12"/>
<icon BUILTIN="back"/>
<node BACKGROUND_COLOR="#eae5a4" COLOR="#351d75" CREATED="1784076979882" ID="ID_1843330900" MODIFIED="1784421405261" TEXT="das hat sich durch die Konversation so ergeben">
<richcontent TYPE="NOTE"><html> <richcontent TYPE="NOTE"><html>
<head/> <head/>
<body> <body>
@ -2007,8 +2010,12 @@
</p> </p>
</body> </body>
</html></richcontent> </html></richcontent>
<font BOLD="true" NAME="SansSerif" SIZE="12"/> <font NAME="SansSerif" SIZE="12"/>
<icon BUILTIN="back"/> </node>
<node BACKGROUND_COLOR="#f0d5c5" COLOR="#99005b" CREATED="1784421416065" ID="ID_428576666" MODIFIED="1784421838679" TEXT="und diese plausible Schlu&#xdf;folgerung ist fehlerhaft">
<arrowlink COLOR="#d8024e" DESTINATION="ID_419323733" ENDARROW="Default" ENDINCLINATION="580;-47;" ID="Arrow_ID_64123671" STARTARROW="None" STARTINCLINATION="1203;65;"/>
<icon BUILTIN="smiley-oh"/>
</node>
</node> </node>
<node COLOR="#435e98" CREATED="1784079033427" ID="ID_1573408699" MODIFIED="1784079050907" TEXT="Zustands&#xfc;berg&#xe4;nge"> <node COLOR="#435e98" CREATED="1784079033427" ID="ID_1573408699" MODIFIED="1784079050907" TEXT="Zustands&#xfc;berg&#xe4;nge">
<font BOLD="true" NAME="SansSerif" SIZE="12"/> <font BOLD="true" NAME="SansSerif" SIZE="12"/>
@ -2477,7 +2484,7 @@
</node> </node>
</node> </node>
<node CREATED="1784229335474" ID="ID_476891533" MODIFIED="1784229337398" TEXT="Fragen"> <node CREATED="1784229335474" ID="ID_476891533" MODIFIED="1784229337398" TEXT="Fragen">
<node BACKGROUND_COLOR="#f0d5c5" COLOR="#990033" CREATED="1784229339746" ID="ID_655619575" MODIFIED="1784229414327" TEXT="bisher habe ich in addNext... die mem_order_seq_cst"> <node COLOR="#5b280f" CREATED="1784229339746" ID="ID_655619575" MODIFIED="1784423380988" TEXT="bisher habe ich in addNext... die mem_order_seq_cst">
<richcontent TYPE="NOTE"><html> <richcontent TYPE="NOTE"><html>
<head/> <head/>
<body> <body>
@ -2488,8 +2495,10 @@
</html></richcontent> </html></richcontent>
<font NAME="SansSerif" SIZE="12"/> <font NAME="SansSerif" SIZE="12"/>
<icon BUILTIN="help"/> <icon BUILTIN="help"/>
<icon BUILTIN="button_cancel"/>
<node CREATED="1784229415567" ID="ID_572644187" MODIFIED="1784229431525" TEXT="g&#xe4;be es einen Unterschied zu mem_order_acquire?"/> <node CREATED="1784229415567" ID="ID_572644187" MODIFIED="1784229431525" TEXT="g&#xe4;be es einen Unterschied zu mem_order_acquire?"/>
<node CREATED="1784229436588" ID="ID_1851133925" MODIFIED="1784229459165" TEXT="oder wird beim Lesen keine Write-Barrier erzeugt?"/> <node CREATED="1784229436588" ID="ID_1851133925" MODIFIED="1784229459165" TEXT="oder wird beim Lesen keine Write-Barrier erzeugt?"/>
<node CREATED="1784423360704" ID="ID_104238528" LINK="#ID_524518128" MODIFIED="1784423391250" TEXT="hat sich durch die Umstellung auf den atomic-Buffer-Pointer erledigt"/>
</node> </node>
</node> </node>
</node> </node>
@ -2832,6 +2841,132 @@
</node> </node>
</node> </node>
</node> </node>
<node CREATED="1784421477265" ID="ID_1391027398" MODIFIED="1784421485484" TEXT="Code validieren und dokumentieren">
<node BACKGROUND_COLOR="#accdc3" COLOR="#3f4015" CREATED="1784421490377" ID="ID_839229493" MODIFIED="1784421553194" STYLE="fork" TEXT="lasse Claude-Code die Konsistenz &#xfc;berpr&#xfc;fen">
<edge COLOR="#808080" STYLE="bezier" WIDTH="thin"/>
<icon BUILTIN="wizard"/>
<node BACKGROUND_COLOR="#f3bba9" COLOR="#fa002a" CREATED="1784421589967" ID="ID_419323733" MODIFIED="1784421838679" TEXT="Race im Synchronisationsmechnismus">
<linktarget COLOR="#d8024e" DESTINATION="ID_419323733" ENDARROW="Default" ENDINCLINATION="580;-47;" ID="Arrow_ID_64123671" SOURCE="ID_428576666" STARTARROW="None" STARTINCLINATION="1203;65;"/>
<icon BUILTIN="broken-line"/>
<node CREATED="1784421650718" HGAP="39" ID="ID_682650657" MODIFIED="1784421708013" TEXT="Problem steckt auf der Lese-Seite" VSHIFT="9">
<icon BUILTIN="back"/>
</node>
<node CREATED="1784421659355" ID="ID_883501072" MODIFIED="1784421675151" TEXT="man braucht den Cursor bereits um auf den Slot zuzugreifen"/>
<node CREATED="1784421675731" ID="ID_22782051" MODIFIED="1784421691422" TEXT="aber erst der Zugriff auf den Slot konstituiert die Read-Barrier"/>
<node CREATED="1784421923608" ID="ID_1849827082" MODIFIED="1784422178172">
<richcontent TYPE="NODE"><html>
<head/>
<body>
<p>
<i>rein zuf&#228;llig</i>&#160;funktioniert es aber trotzdem
</p>
</body>
</html></richcontent>
<richcontent TYPE="NOTE"><html>
<head/>
<body>
<p>
und zwar, wie Claudes ausf&#252;hrt, weil unter Linux die Lese / Schreib-Kernel-Calls einen Syncrhonisationspunkt darstellen. Da der Glib::Dispatcher &#252;ber einen Pipe-Write/Read implementiert ist, f&#252;hrt bereits das Dispatch zu einer vollen Barrier, und daher ist es unter Linux ausgeschlossen, da&#223; der GUI-Thread noch einen alten, verschleppten Cursor-State sieht.
</p>
<p>
</p>
<p>
Das gilt aber nicht allgemein und f&#252;r andere Betriebssysteme und ist mithin nicht durch den C++-Standard gedeckt. Deshalb <i>beherrscht diese L&#246;sung</i>&#160;nicht das Problem, und die dargestellte Begr&#252;ndung ist nicht stichhaltig.
</p>
</body>
</html></richcontent>
</node>
</node>
<node BACKGROUND_COLOR="#e2e0bd" COLOR="#435e98" CREATED="1784421763371" ID="ID_1187975455" MODIFIED="1784422182182" TEXT="mein L&#xf6;sungsansatz beruht darauf da&#xdf;, nur ein Zustand synchronisiert werden mu&#xdf;">
<icon BUILTIN="idea"/>
</node>
<node BACKGROUND_COLOR="#c8c0b6" COLOR="#435e98" CREATED="1784422189692" ID="ID_524518128" MODIFIED="1784422226476" TEXT="Atomic von der State-Engine trennen &#x27f9; stattdessen ein std::atomic&lt;Buffer*&gt;">
<node CREATED="1784422263938" ID="ID_1098483559" MODIFIED="1784422275268" TEXT="das f&#xfc;hrt insgesamt zu einer deutlichen Vereinfachung"/>
<node CREATED="1784422275837" ID="ID_328441268" MODIFIED="1784422293595" TEXT="ich schlage daraufhin vor: die drei Cursor auf zwei zu reduzieren"/>
<node BACKGROUND_COLOR="#accdc3" COLOR="#3f4015" CREATED="1784422309123" ID="ID_975047310" MODIFIED="1784422325191" STYLE="fork" TEXT="beauftrage Claude eine entspr. L&#xf6;sung auszuarbeiten">
<edge COLOR="#808080" STYLE="bezier" WIDTH="thin"/>
<icon BUILTIN="wizard"/>
<node CREATED="1784422331646" ID="ID_691393919" MODIFIED="1784422358057" TEXT="Vorschlag: den nextShow-Cursor erst beim &#xbb;reclaim&#xab; des Buffers inkrementieren"/>
<node CREATED="1784422475931" ID="ID_566892395" MODIFIED="1784422543922" TEXT="d.h. die Backpressure wird hier realisiert, indem erst danach der n&#xe4;chste Slot &#xfc;bpr&#xfc;ft wird"/>
<node CREATED="1784422546982" ID="ID_230854798" MODIFIED="1784422774468" TEXT="diese L&#xf6;sung gef&#xe4;llt mir: Logik hat jetzt weitgehend die Form einer State-Engine"/>
<node CREATED="1784422779995" ID="ID_1873586884" MODIFIED="1784422802079" TEXT="Claude schl&#xe4;gt vor, ein Switch-Case zu verwenden &#x2014; auch das eine gute Idee"/>
</node>
</node>
<node BACKGROUND_COLOR="#e0ceaa" COLOR="#8c054b" CREATED="1784422824049" ID="ID_1421936373" MODIFIED="1784422874139">
<richcontent TYPE="NODE"><html>
<head/>
<body>
<p>
weiterer <i>theoretischer</i>&#160;Race: die playing-Steuervariable
</p>
</body>
</html></richcontent>
<icon BUILTIN="messagebox_warning"/>
<node CREATED="1784422887697" ID="ID_1880556582" MODIFIED="1784423000837" TEXT="extrem unwahrscheinlich zu triggern">
<richcontent TYPE="NOTE"><html>
<head/>
<body>
<p>
dazu m&#252;&#223;te das GUI sofort terminieren, w&#228;hrend der Proactor-Thread grade erst startet &#10233; dann w&#252;rde dort in der Loop ohne weiteren Check playing_ = true gesetzt, und damit ginge die Shutdown-Signalisierung verloren
</p>
</body>
</html></richcontent>
</node>
<node CREATED="1784423002330" ID="ID_191859221" MODIFIED="1784423029018" TEXT="Resultat w&#xe4;re, da&#xdf; der Proactor-Thread dann h&#xe4;ngt und irgenwann blockt"/>
<node CREATED="1784423036125" ID="ID_1995226372" MODIFIED="1784423050746" TEXT="und die Applikation w&#xfc;rde im Shutdown verklemmen (im thread.join)"/>
<node COLOR="#338800" CREATED="1784423055506" ID="ID_616531880" MODIFIED="1784423089086" TEXT="Mein Vorschlag: der Player ist sowiso &#xbb;RAII&#xab; &#x27f9; also gleich playing_ = true von Anfang an">
<icon BUILTIN="button_ok"/>
</node>
</node>
<node CREATED="1784423469985" ID="ID_1639097185" MODIFIED="1784423662654">
<richcontent TYPE="NODE"><html>
<head/>
<body>
<p>
<u>Hinweis</u>: in Prod-Code sollte man jedes <font color="#5b3131" face="Monospaced">io_uring_get_sqe() != nullptr</font>&#160;&#160;pr&#252;fen
</p>
</body>
</html></richcontent>
<richcontent TYPE="NOTE"><html>
<head/>
<body>
<p>
Begr&#252;ndung: im Moment kann das nicht passieren, da die Queue ausreichend dimensioniert ist auf RING_SIZ + 1.&#160;&#160;Sollte sich sowas aber unter Maintainnance &#228;ndern, dann k&#246;nnte man in einen Segfault laufen! (Ja das sehe ich genauso und h&#228;tte es in Prod-Code tats&#228;chlich in eine Hilfsfunktion gepackt; hier aber habe ich bewu&#223;t aus didaktischen Gr&#252;nden darauf verzichtet)
</p>
</body>
</html></richcontent>
<icon BUILTIN="clanbomber"/>
</node>
<node CREATED="1784423094670" ID="ID_891536459" MODIFIED="1784423144970">
<richcontent TYPE="NODE"><html>
<head/>
<body>
<p>
au&#223;erdem habe ich vergessen:&#160;<font color="#642880" face="DejaVu Sans Mono" size="9pt"><b>io_uring_queue_exit</b></font><font color="#11123a" face="DejaVu Sans Mono" size="9pt">(</font><font color="#65533c" face="DejaVu Sans Mono" size="9pt">&amp;</font><font color="#9c2303" face="DejaVu Sans Mono" size="9pt">uRing</font><font color="#11123a" face="DejaVu Sans Mono" size="9pt">)</font><font color="#65533c" face="DejaVu Sans Mono" size="9pt">;</font>
</p>
</body>
</html></richcontent>
</node>
</node>
<node CREATED="1784423169337" ID="ID_1181187272" MODIFIED="1784423187567" TEXT="Beschreibung in ProcSpec.txt">
<icon BUILTIN="edit"/>
<node CREATED="1784423189039" ID="ID_221455189" MODIFIED="1784423195187" TEXT="entwerfe eine Gliederung"/>
<node BACKGROUND_COLOR="#accdc3" COLOR="#3f4015" CREATED="1784423195777" ID="ID_497657354" MODIFIED="1784423304160" STYLE="fork" TEXT="lasse Claude die Abschnitte ausf&#xfc;llen">
<richcontent TYPE="NOTE"><html>
<head/>
<body>
<p>
...da der Konversations-Status ein komplettes Bild der Steuerlogik enth&#228;lt, bietet sich das gradezu an; stilistisch &#252;berarbeiten kann ich den Text sp&#228;ter immer noch
</p>
</body>
</html></richcontent>
<edge COLOR="#808080" STYLE="bezier" WIDTH="thin"/>
<icon BUILTIN="wizard"/>
</node>
<node CREATED="1784423283442" ID="ID_1109777699" MODIFIED="1784423292988" TEXT="lasse Claude auch gleich ein paar SVG-Diagramme generieren"/>
</node>
</node>
</node> </node>
</node> </node>
</node> </node>

View file

@ -56,8 +56,11 @@ namespace {
* and uses them cyclically, first adding new tasks to read a frame-block * and uses them cyclically, first adding new tasks to read a frame-block
* from the file, receiving completed tasks to hand-over the next frame * from the file, receiving completed tasks to hand-over the next frame
* driven by a frame-beat timer, that is also based on IO_URING. * driven by a frame-beat timer, that is also based on IO_URING.
* Each buffer »slot« is associated with a #BuffState flag, implemented * The lifecycle of each buffer »slot« is tracked by a #BuffState flag,
* as `std::atomic` to also serve for cross-thread visibility. * used solely within the »Proactor« thread. A single [atomic pointer](\ref toDisplay_)
* is used as baton to hand the current frame to the GUI and to signal back
* when the display was published, thereby also establishing cross-thread
* visibility barriers for the buffer contents.
* @note UringVideoPlayer must be constructed in the GTK UI thread, * @note UringVideoPlayer must be constructed in the GTK UI thread,
* yet will immediately launch the »Proactor« thread from the ctor. * yet will immediately launch the »Proactor« thread from the ctor.
* @remarks * @remarks
@ -75,10 +78,9 @@ class UringVideoPlayer
using Buffer = VideoDisplay::Buffer; using Buffer = VideoDisplay::Buffer;
enum BuffState { FREE, READ, FEED, SHOW, STOP }; enum BuffState { FREE, READ, FEED, SHOW, STOP };
using AtomicBuffState = std::atomic<BuffState>;
using BufferAlloc = unique_ptr<Buffer[]>; using BufferAlloc = unique_ptr<Buffer[]>;
using BuffStateAlloc = unique_ptr<AtomicBuffState[]>; using BuffStateAlloc = unique_ptr<BuffState[]>;
/** /**
* Connector to the VideoDisplay for dispatching video data. * Connector to the VideoDisplay for dispatching video data.
@ -98,18 +100,14 @@ class UringVideoPlayer
BufferDispatcher (UringVideoPlayer& player) BufferDispatcher (UringVideoPlayer& player)
: player_{player} : player_{player}
{ } { }
private:
Buffer& getCurrentFrame();
AtomicBuffState& getCurrentFrameState();
}; };
size_t nextRead_{0}; size_t nextRead_{0};
size_t nextShow_{0}; size_t nextShow_{0};
size_t currShow_{0};
BufferAlloc buffer_{}; BufferAlloc buffer_{};
BuffStateAlloc buffState_{}; BuffStateAlloc buffState_{};
std::atomic_bool playing_{false}; std::atomic<Buffer*> toDisplay_{nullptr};
std::atomic_bool playing_{true};
BufferDispatcher dispatchFrame_; BufferDispatcher dispatchFrame_;
VideoDisplay videoDisplayer_; VideoDisplay videoDisplayer_;
std::thread proactorThread_; std::thread proactorThread_;
@ -148,11 +146,11 @@ UringVideoPlayer::playVideo()
// Allocate several data buffers (used as ring) // Allocate several data buffers (used as ring)
buffer_ = make_unique<Buffer[]> (RING_SIZ); buffer_ = make_unique<Buffer[]> (RING_SIZ);
buffState_ = make_unique<AtomicBuffState[]> (RING_SIZ); buffState_ = make_unique<BuffState[]> (RING_SIZ);
// initialise uRing Kernel interface // initialise uRing Kernel interface
io_uring uRing; io_uring uRing;
io_uring_queue_init (RING_SIZ+1, &uRing, /*flags*/ 0 ); // Note: one additional »slot« for the timer event(s) io_uring_queue_init (RING_SIZ+1, &uRing, /*flags*/ 0 ); // Note: allocate one additional »slot« for the timer event(s)
// Setup periodic frame beat, firing infinitely // Setup periodic frame beat, firing infinitely
const auto TICK_TAG = UINT64_MAX; // special marker for timer events (differs from possible frame numbers) const auto TICK_TAG = UINT64_MAX; // special marker for timer events (differs from possible frame numbers)
@ -165,7 +163,6 @@ UringVideoPlayer::playVideo()
io_uring_submit (&uRing); io_uring_submit (&uRing);
/* =============== proactor-loop =============== */ /* =============== proactor-loop =============== */
playing_ = true;
while (playing_.load (std::memory_order_acquire)) while (playing_.load (std::memory_order_acquire))
{ {
io_uring_cqe* completion; io_uring_cqe* completion;
@ -188,6 +185,7 @@ UringVideoPlayer::playVideo()
io_uring_cqe_seen (&uRing, completion); io_uring_cqe_seen (&uRing, completion);
} }
close (inputFD); close (inputFD);
io_uring_queue_exit (&uRing);
} }
@ -212,7 +210,7 @@ UringVideoPlayer::addNextReadSubmissions (io_uring& uRing, int inputFD)
,BUFFSIZ * srcOffset ,BUFFSIZ * srcOffset
); );
io_uring_sqe_set_data64 (readSubmission, nextRead_); io_uring_sqe_set_data64 (readSubmission, nextRead_);
buffState_[nextRead_ % RING_SIZ].store (READ, std::memory_order_relaxed); buffState_[nextRead_ % RING_SIZ] = READ;
timings.markStart (nextRead_); timings.markStart (nextRead_);
++nextRead_; ++nextRead_;
} }
@ -232,84 +230,74 @@ UringVideoPlayer::handleCompletedRead (io_uring_cqe& completion)
timings.markStop(frameNr); timings.markStop(frameNr);
if (completion.res != sizeof(Buffer)) if (completion.res != sizeof(Buffer))
state.store(STOP, std::memory_order_relaxed); // Insufficient read, possibly end-of-file => signal stop of playback (to ourselves) state = STOP; // Insufficient read, possibly end-of-file => signal stop of playback (to ourselves)
else else
state.store(FEED, std::memory_order_relaxed); // Success; frame data is in the buffer => mark ready for use at the next tick event state = FEED; // Success; frame data is in the buffer => mark ready for use at the next tick event
} }
/** /**
* At each timer tick: see if the next frame can be displayed. * At each timer tick: see if the next frame can be displayed.
* @remark determine the buffer slot where the frame data was placed, * @remark the cursor #nextShow_ walks the ring of buffers. It rests on the slot
* based on the cursor for the next frame to display, wrapping * currently lent to the GUI-thread (marked #SHOW) until the latter did signal
* through the ring of buffers. Depending on the state marked * completion by clearing the atomic #toDisplay_. Only then is the slot reclaimed
* for this buffer, the frame can be displayed if the data * (#FREE) and the cursor advanced. The next slot reached thusly is displayed if
* was already loaded, or otherwise the frame will slip * its data was already loaded (#FEED); otherwise the frame slips forward to
* to the next regular time tick. * the next regular time tick.
* Thus #toDisplay_ also provides the back-pressure: a new frame is published
* only once the previous one was displayed and released.
*/ */
void void
UringVideoPlayer::maybeDisplayFrame() UringVideoPlayer::maybeDisplayFrame()
{ {
auto prevState = buffState_[currShow_ % RING_SIZ].load (std::memory_order_acquire); // check and possibly reclaim previously displayed buffer
auto nextState = buffState_[nextShow_ % RING_SIZ].load (std::memory_order_relaxed); if (SHOW == buffState_[nextShow_ % RING_SIZ] and
nullptr == toDisplay_.load (std::memory_order_acquire)) // read barrier
if (nextState == FEED and prevState != SHOW) { // GUI thread did mark completion
{ // can hand over this next frame to the VideoDisplay in the GUI-thread buffState_[nextShow_ % RING_SIZ] = FREE;
currShow_ = nextShow_;
++nextShow_; ++nextShow_;
// mark state to wait for display completion; this also creates a write barrier
buffState_[currShow_ % RING_SIZ].store (SHOW, std::memory_order_release);
// Notify GUI thread via Glib::Dispatcher
dispatchFrame_();
if (0 == currShow_ % EMA_PERIOD)
std::cout << "Frame #"<<currShow_<<" IO ∅ "<<timings.expMA()<<"µs"<< std::endl;
} }
else
if (nextState == STOP) switch (buffState_[nextShow_ % RING_SIZ])
{ {
case FEED:
{ // hand over this frame to the VideoDisplay in the GUI-thread...
buffState_[nextShow_ % RING_SIZ] = SHOW;
Buffer& frame{buffer_[nextShow_ % RING_SIZ]};
toDisplay_.store (& frame, std::memory_order_release); // write barrier
dispatchFrame_(); // Notify GUI thread via Glib::Dispatcher
if (0 == nextShow_ % EMA_PERIOD)
std::cout << "Frame #"<<nextShow_<<" IO ∅ "<<timings.expMA()<<"µs"<< std::endl;
}
break;
case STOP:
playing_ = false; playing_ = false;
std::cout << "Playback stopped due to short read" << std::endl; std::cout << "Playback stopped due to short read" << std::endl;
} break;
else
default: // READ (IO still pending) or SHOW (GUI not yet caught up)
std::cout << "WARNING: Frame slipped" << std::endl; std::cout << "WARNING: Frame slipped" << std::endl;
break;
}
} }
/** pick up the buffer published by the proactor-thread */
UringVideoPlayer::Buffer* UringVideoPlayer::Buffer*
UringVideoPlayer::BufferDispatcher::retrieveFrame() UringVideoPlayer::BufferDispatcher::retrieveFrame()
{ {
// retrieve buffer state from proactor-thread; this also creates a read barrier return player_.toDisplay_.load (std::memory_order_acquire);
auto frameState = getCurrentFrameState().load (std::memory_order_acquire); } // acquire-load also creates a read barrier
if (frameState == SHOW)
return & getCurrentFrame();
else
return nullptr;
// (can happen only in boundary cases)
}
/** signal completion of display activity to the proactor-thread */
void void
UringVideoPlayer::BufferDispatcher::completedFrame() UringVideoPlayer::BufferDispatcher::completedFrame()
{ {
// mark buffer as display complete for the proactor-thread; this also creates a write barrier player_.toDisplay_.store (nullptr, std::memory_order_release);
getCurrentFrameState().store (FREE, std::memory_order_release); } // release-store also creates a write barrier
}
UringVideoPlayer::Buffer&
UringVideoPlayer::BufferDispatcher::getCurrentFrame()
{
auto currFrame = player_.currShow_;
return player_.buffer_[currFrame % RING_SIZ];
}
UringVideoPlayer::AtomicBuffState&
UringVideoPlayer::BufferDispatcher::getCurrentFrameState()
{
auto currFrame = player_.currShow_;
return player_.buffState_[currFrame % RING_SIZ];
}
void void

View file

@ -8,11 +8,23 @@ Specification for the Demo
This specification is the result of our research and provides a short summary of the relevant This specification is the result of our research and provides a short summary of the relevant
aspects of io_uring. Furthermore, the specific design of the demo code is explained and motivated. aspects of io_uring. Furthermore, the specific design of the demo code is explained and motivated.
[red yellow-background]#TBW#
IO_URING kernel interface IO_URING kernel interface
------------------------- -------------------------
`io_uring` is a Linux kernel interface for asynchronous I/O (available since kernel 5.1). It is
built around two ring buffers, shared and memory-mapped between user space and kernel: a
_Submission Queue_ (SQ) and a _Completion Queue_ (CQ). The application fills a _Submission Queue
Entry_ (SQE) describing an operation — read, write, timeout, accept, … — and notifies the kernel;
once the operation has run to completion, the kernel deposits a _Completion Queue Entry_ (CQE)
carrying the result. Submission and completion are thus decoupled: many operations can be in
flight concurrently, and — unlike the classic `read(2)`/`write(2)` model — no thread has to block
while an operation is pending, and no syscall is required per individual operation (a single
`io_uring_submit` hands over a whole batch, and with a polled queue even that can be elided).
For a media application this is attractive: playback and editing require moving large volumes of
data with predictable latency, ideally without dedicating a blocked thread to each stream. The
uniform, batchable async model of io_uring fits this need directly.
Liburing Liburing
~~~~~~~~ ~~~~~~~~
@ -29,3 +41,162 @@ To avoid a performance penalty due to this indirection and encapsulation, Liburi
to a large extent in the form of _static inline functions_. A variant called `liburing-ffi` is provided to a large extent in the form of _static inline functions_. A variant called `liburing-ffi` is provided
as alternative for cross-language bindings and for usage with older compilers, providing an definition as alternative for cross-language bindings and for usage with older compilers, providing an definition
and binary symbol for each function; obviously using these explicit calls adds a noticeable penalty. and binary symbol for each function; obviously using these explicit calls adds a noticeable penalty.
Reading of raw video as example of using IO_URING
-------------------------------------------------
Playing a raw video file is a minimal yet realistic streaming workload — a steady, latency-bound
sequence of large block reads that has to keep pace with a fixed frame beat — and therefore serves
well to demonstrate the decoupling of submission and completion.
The »Proactor« pattern
~~~~~~~~~~~~~~~~~~~~~~
It is instructive to contrast two patterns for event-driven I/O. In the *Reactor* pattern the
program waits for a resource to become _ready_ and then performs the operation itself, synchronously,
from within the event handler. In the *Proactor* pattern the operation is _initiated_ up front, the
operating system carries it out, and a handler is invoked on _completion_ — with the result already
available. io_uring is inherently proactor-shaped: we submit a read, the kernel performs it, and we
later receive a completion that carries the outcome.
The demo realises this with a single dedicated *»Proactor« thread* (`UringVideoPlayer::playVideo()`).
Its whole life is one loop around `io_uring_wait_cqe()`: block until the next completion arrives,
dispatch it to a handler based on a tag carried in the CQE `user_data`, then re-arm further work and
submit. Two kinds of completion are multiplexed onto the same ring and told apart by that tag:
* a periodic *frame-beat timer* (a multishot timeout, tagged with the sentinel `TICK_TAG`)
* *read completions* for individual video frames (tagged with the running frame number)
Overall design of the demo code
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The code is deliberately split so that the io_uring logic stands apart from the display machinery:
`UringVideoPlayer` (demo-uring.cpp)::
The subject of this write-up. Owns the ring, the buffers and their state, and the Proactor thread.
`EglDisplay` (egl-display.hpp)::
A self-contained OpenGL/EGL display component, best read as a _black box_. It is reached only
through the small `PixelFeed` interface — `retrieveFrame()` to obtain the current frame's pixel
data and `completedFrame()` to signal that the data is no longer needed.
`GtkUringApp` (gtk-uring-app.hpp)::
A minimal GTK-3 host application providing the window, the start button and the shutdown hook.
Two threads are involved: the *GTK/GUI main thread* and the *Proactor thread*. The player is
constructed on the GUI thread and — following a RAII spirit — immediately launches the Proactor
thread from its constructor. Note that `proactorThread_` is declared _last_ among the members, so
by C++'s declaration-order construction rule all buffers, state and the display connection are fully
initialised before the thread can observe them. Data flows in one direction: the Proactor reads file
blocks into buffers and publishes a ready frame; the GUI thread uploads that frame into a GPU texture
and draws it.
.Overall data flow across the two threads, with the atomic baton `toDisplay_` between them
image::diagrams/thread-handoff.svg[Cross-thread data flow and visibility handoff,align="center"]
The problem of data visibility synchronisation
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The two threads share the buffer array, which raises two questions of _cross-thread visibility_:
. once the Proactor announces a frame, the GUI thread must observe the buffer's contents _completely_
written, not a partially filled buffer;
. the Proactor must know when the GUI is _done_ with a buffer, so the slot may be reused safely.
Rather than making the entire state array atomic, or sharing cursor indices across the boundary, the
design funnels the whole cross-thread contract through a *single* atomic — `std::atomic<Buffer*>
toDisplay_` — which _is_ the payload rather than a key to look it up:
[cols="1,3"]
|===
| `toDisplay_ != nullptr` | a frame is currently lent to the GUI thread
| `toDisplay_ == nullptr` | the Proactor owns the slot again and may reuse it
|===
The Proactor publishes with a *release*-store; the GUI picks the pointer up with an *acquire*-load
and thereby is guaranteed to see the buffer contents written before the store. When finished, the
GUI writes `nullptr` with a *release*-store, which the Proactor observes with an *acquire*-load
before reclaiming the slot. This is the classic single-slot, single-producer/single-consumer
_mailbox_ handoff: the two release/acquire pairs establish the _happens-before_ relations that carry
buffer-content visibility in both directions. Everything else — the `BuffState` array and the frame
cursors — is confined to the Proactor thread alone and therefore needs no synchronisation at all.
Buffer data structure
~~~~~~~~~~~~~~~~~~~~~
A fixed set of `RING_SIZ` frame-sized buffers is held in a `unique_ptr<Buffer[]>` and used as a
_ring_: the slot for a frame is simply `frameNr % RING_SIZ`. Frame numbers themselves are monotonic,
unbounded counters — `nextRead_` for the production side and `nextShow_` for the consumption side —
so the modulo maps an ever-growing frame index onto the small cyclic set of buffers. The same running
frame number does triple duty: as the SQE `user_data` tag that later identifies the completed read, as
the key of the per-slot IO timer, and (via modulo) as the slot index. The io_uring queues are sized
`RING_SIZ+1`, the one extra entry accommodating the always-present timer event.
.Snapshot of the ring: the two cursors chase each other, the gap holds the in-flight frames
image::diagrams/ring-buffer.svg[Ring-buffer snapshot with production and consumption cursors,align="center"]
Tracking state of a buffer slot
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Each slot carries a `BuffState` describing where in its life-cycle it currently is:
FREE -> READ -> FEED -> SHOW -> FREE (with STOP as terminal state)
[cols="1,4"]
|===
| `FREE` | available; `addNextReadSubmissions()` may submit a read into it
| `READ` | a read has been submitted and is in flight
| `FEED` | the read completed successfully; frame data is ready to be shown
| `SHOW` | the frame has been handed to the GUI thread (the single slot currently lent out)
| `STOP` | the read fell short (end-of-file); signals playback to stop
|===
Because this array is written and read _only_ from the Proactor thread, it is a plain, non-atomic
array. The `SHOW` marker records _which_ slot is presently on loan to the GUI, complementing the
`toDisplay_` baton which records _whether_ it has come back yet.
.The life-cycle of a single buffer slot (all transitions driven by the Proactor thread)
image::diagrams/state-engine.svg[Buffer-slot state engine,align="center"]
Control flow and backpressure
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Production and consumption advance independently, each with its own cursor. Production
(`addNextReadSubmissions()`) greedily fills _every_ `FREE` slot ahead with read submissions, tagging
each with its frame number. Consumption happens once per timer tick in `maybeDisplayFrame()`: first
a completed frame is _reclaimed_, then the next frame is _published_ if its data has arrived.
The essential back-pressure is that the single show-cursor does not advance while a frame is still out
with the GUI (`toDisplay_` non-null). No new frame is published until the previous one has been
displayed and released, and — since reads only ever refill `FREE` slots — a lagging consumer naturally
throttles production. A frame whose read has not completed in time, or whose predecessor the GUI has
not yet released, simply _slips_ to the next tick.
[source,cpp]
--------------------------------------------------------------------------
void
UringVideoPlayer::maybeDisplayFrame()
{
// reclaim previously displayed buffer
if (SHOW == buffState_[nextShow_ % RING_SIZ] and
nullptr == toDisplay_.load (memory_order_acquire)) // <1>
{
buffState_[nextShow_ % RING_SIZ] = FREE;
++nextShow_; // <2>
}
switch (buffState_[nextShow_ % RING_SIZ])
{
case FEED: // <3>
buffState_[nextShow_ % RING_SIZ] = SHOW;
toDisplay_.store (& buffer_[nextShow_ % RING_SIZ]
, memory_order_release); // <4>
dispatchFrame_();
break;
case STOP:
playing_ = false;
break;
default: // <5>
std::cout << "WARNING: Frame slipped" << std::endl;
}
}
--------------------------------------------------------------------------
<1> reclaim only the slot lent to the GUI, and only once it cleared the baton
<2> advance the single show-cursor — this is where the back-pressure lives
<3> the frame data has arrived (`FEED`): hand it over
<4> the release-store publishes the buffer and makes its contents visible to the GUI
<5> `READ` (IO still pending) or `SHOW` (GUI not yet caught up) ⇒ the frame slips to the next tick

View file

@ -0,0 +1,111 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 620 560" font-family="sans-serif">
<!--
Ring-buffer snapshot for the io_uring video demo.
A fixed array of RING_SIZ buffers is used cyclically: slot = frameNr % RING_SIZ.
Two independent, monotonic cursors chase each other around the ring:
nextRead_ (production) submits reads into every FREE slot ahead
nextShow_ (consumption) rests on the slot currently shown, advancing on release
This is an illustrative snapshot (START_FRAME = 40, RING_SIZ = 5).
-->
<defs>
<marker id="arrow" markerWidth="9" markerHeight="9" refX="7" refY="4.5"
orient="auto" markerUnits="userSpaceOnUse">
<path d="M0,0 L9,4.5 L0,9 Z" fill="#333"/>
</marker>
<marker id="arrowRot" markerWidth="10" markerHeight="10" refX="6" refY="5"
orient="auto" markerUnits="userSpaceOnUse">
<path d="M0,0 L10,5 L0,10 Z" fill="#888"/>
</marker>
</defs>
<text x="310" y="34" text-anchor="middle" font-size="18" font-weight="bold">
Buffers used as a ring: slot = frameNr % RING_SIZ
</text>
<!-- rotation hint (clockwise) -->
<path d="M 395 138 A 130 130 0 0 1 452 300" fill="none" stroke="#bbb"
stroke-width="2" stroke-dasharray="4 5" marker-end="url(#arrowRot)"/>
<text x="470" y="320" font-size="12" fill="#999">advances</text>
<!-- ===================== slots ===================== -->
<!-- slot 0 : SHOW (frame 40, currently lent to GUI) -->
<g>
<circle cx="310" cy="120" r="46" fill="#e7d8ff" stroke="#5a2ca0" stroke-width="2.2"/>
<text x="310" y="112" text-anchor="middle" font-size="14" font-weight="bold">SHOW</text>
<text x="310" y="132" text-anchor="middle" font-size="12" fill="#444">frame 40</text>
<text x="372" y="104" text-anchor="middle" font-size="11" fill="#888">slot 0</text>
</g>
<!-- slot 1 : FEED (frame 41, ready to show) -->
<g>
<circle cx="452" cy="203" r="46" fill="#d8f0d8" stroke="#3a7a3a" stroke-width="2.2"/>
<text x="452" y="199" text-anchor="middle" font-size="14" font-weight="bold">FEED</text>
<text x="452" y="219" text-anchor="middle" font-size="12" fill="#444">frame 41</text>
<text x="507" y="200" text-anchor="middle" font-size="11" fill="#888">slot 1</text>
</g>
<!-- slot 2 : READ (frame 42, IO in flight) -->
<g>
<circle cx="398" cy="371" r="46" fill="#ffedcc" stroke="#b8860b" stroke-width="2.2"/>
<text x="398" y="367" text-anchor="middle" font-size="14" font-weight="bold">READ</text>
<text x="398" y="387" text-anchor="middle" font-size="12" fill="#444">frame 42</text>
<text x="398" y="432" text-anchor="middle" font-size="11" fill="#888">slot 2</text>
</g>
<!-- slot 3 : READ (frame 43, IO in flight) -->
<g>
<circle cx="222" cy="371" r="46" fill="#ffedcc" stroke="#b8860b" stroke-width="2.2"/>
<text x="222" y="367" text-anchor="middle" font-size="14" font-weight="bold">READ</text>
<text x="222" y="387" text-anchor="middle" font-size="12" fill="#444">frame 43</text>
<text x="222" y="432" text-anchor="middle" font-size="11" fill="#888">slot 3</text>
</g>
<!-- slot 4 : FREE (next to be submitted) -->
<g>
<circle cx="168" cy="203" r="46" fill="#dbe9ff" stroke="#3a5a8a" stroke-width="2.2"/>
<text x="168" y="199" text-anchor="middle" font-size="14" font-weight="bold">FREE</text>
<text x="168" y="219" text-anchor="middle" font-size="12" fill="#444">—</text>
<text x="113" y="200" text-anchor="middle" font-size="11" fill="#888">slot 4</text>
</g>
<!-- center caption -->
<text x="310" y="258" text-anchor="middle" font-size="13" fill="#666">buffer_[ ]</text>
<text x="310" y="277" text-anchor="middle" font-size="11" fill="#999">RING_SIZ = 5</text>
<!-- ===================== cursors ===================== -->
<!-- nextShow_ -> slot 0 -->
<g>
<rect x="150" y="52" width="150" height="26" rx="5" fill="#f3ecff" stroke="#5a2ca0"/>
<text x="225" y="70" text-anchor="middle" font-size="13" fill="#5a2ca0">
nextShow_ = 40
</text>
<path d="M 278 72 C 300 84, 300 74, 300 96" fill="none" stroke="#5a2ca0"
stroke-width="2" marker-end="url(#arrow)"/>
</g>
<!-- nextRead_ -> slot 4 -->
<g>
<rect x="18" y="120" width="150" height="26" rx="5" fill="#eaf1fb" stroke="#3a5a8a"/>
<text x="93" y="138" text-anchor="middle" font-size="13" fill="#3a5a8a">
nextRead_ = 44
</text>
<path d="M 110 146 C 120 168, 138 168, 150 182" fill="none" stroke="#3a5a8a"
stroke-width="2" marker-end="url(#arrow)"/>
</g>
<!-- ===================== legend ===================== -->
<g font-size="12" transform="translate(40,478)">
<text x="0" y="0" font-weight="bold" font-size="13">States:</text>
<rect x="66" y="-11" width="15" height="15" rx="3" fill="#dbe9ff" stroke="#3a5a8a"/>
<text x="86" y="1">FREE</text>
<rect x="140" y="-11" width="15" height="15" rx="3" fill="#ffedcc" stroke="#b8860b"/>
<text x="160" y="1">READ</text>
<rect x="216" y="-11" width="15" height="15" rx="3" fill="#d8f0d8" stroke="#3a7a3a"/>
<text x="236" y="1">FEED</text>
<rect x="292" y="-11" width="15" height="15" rx="3" fill="#e7d8ff" stroke="#5a2ca0"/>
<text x="312" y="1">SHOW</text>
<rect x="372" y="-11" width="15" height="15" rx="3" fill="#ffd6d6" stroke="#a00000"/>
<text x="392" y="1">STOP</text>
</g>
<text x="40" y="516" font-size="12" fill="#666">
Between the two cursors lie the frames currently in flight (READ) or ready (FEED);
</text>
<text x="40" y="534" font-size="12" fill="#666">
the gap can never exceed RING_SIZ — that is where the back-pressure lives.
</text>
</svg>

After

Width:  |  Height:  |  Size: 5.6 KiB

View file

@ -0,0 +1,34 @@
// Buffer-slot state engine of the io_uring video demo.
// Render: dot -Tsvg state-engine.dot -o state-engine.svg
//
// The BuffState of each ring slot is written and read ONLY by the Proactor
// thread; the single SHOW->FREE transition is triggered by the GUI thread
// clearing the atomic baton (toDisplay_), but observed and enacted here.
digraph BuffState {
rankdir=LR;
bgcolor="transparent";
labelloc="t";
fontname="sans-serif";
fontsize=14;
label="Life-cycle of one buffer »slot«";
node [shape=circle, fixedsize=true, width=0.9,
fontname="sans-serif", fontsize=12, style=filled, penwidth=1.4];
edge [fontname="sans-serif", fontsize=10];
FREE [fillcolor="#dbe9ff"];
READ [fillcolor="#ffedcc"];
FEED [fillcolor="#d8f0d8"];
SHOW [fillcolor="#e7d8ff"];
STOP [shape=doublecircle, width=0.85, fillcolor="#ffd6d6"];
FREE -> READ [label=" submit read\l addNextReadSubmissions()\l"];
READ -> FEED [label=" read completed OK\l handleCompletedRead()\l"];
READ -> STOP [label=" short read / EOF ", fontcolor="#a00000", color="#a00000"];
FEED -> SHOW [label=" publish frame\l maybeDisplayFrame()\l"];
SHOW -> FREE [label=" GUI released frame\l (toDisplay_ == nullptr)\l",
fontcolor="#5a2ca0", color="#5a2ca0", constraint=false];
{ rank=same; FREE; }
}

View file

@ -0,0 +1,83 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 2.42.4 (0)
-->
<!-- Title: BuffState Pages: 1 -->
<svg width="762pt" height="210pt"
viewBox="0.00 0.00 762.00 209.94" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 205.94)">
<title>BuffState</title>
<text text-anchor="middle" x="377" y="-186.74" font-family="sans-serif" font-size="14.00">Life&#45;cycle of one buffer »slot«</text>
<!-- FREE -->
<g id="node1" class="node">
<title>FREE</title>
<ellipse fill="#dbe9ff" stroke="black" stroke-width="1.4" cx="32.5" cy="-94.44" rx="32.5" ry="32.5"/>
<text text-anchor="middle" x="32.5" y="-91.34" font-family="sans-serif" font-size="12.00">FREE</text>
</g>
<!-- READ -->
<g id="node2" class="node">
<title>READ</title>
<ellipse fill="#ffedcc" stroke="black" stroke-width="1.4" cx="275.5" cy="-94.44" rx="32.5" ry="32.5"/>
<text text-anchor="middle" x="275.5" y="-91.34" font-family="sans-serif" font-size="12.00">READ</text>
</g>
<!-- FREE&#45;&gt;READ -->
<g id="edge1" class="edge">
<title>FREE&#45;&gt;READ</title>
<path fill="none" stroke="black" d="M65.01,-94.44C108.04,-94.44 185.27,-94.44 232.99,-94.44"/>
<polygon fill="black" stroke="black" points="232.99,-97.94 242.99,-94.44 232.99,-90.94 232.99,-97.94"/>
<text text-anchor="start" x="83" y="-108.44" font-family="sans-serif" font-size="10.00"> &#160;submit read</text>
<text text-anchor="start" x="83" y="-97.44" font-family="sans-serif" font-size="10.00"> &#160;addNextReadSubmissions()</text>
</g>
<!-- FEED -->
<g id="node3" class="node">
<title>FEED</title>
<ellipse fill="#d8f0d8" stroke="black" stroke-width="1.4" cx="503.5" cy="-146.44" rx="32.5" ry="32.5"/>
<text text-anchor="middle" x="503.5" y="-143.34" font-family="sans-serif" font-size="12.00">FEED</text>
</g>
<!-- READ&#45;&gt;FEED -->
<g id="edge2" class="edge">
<title>READ&#45;&gt;FEED</title>
<path fill="none" stroke="black" d="M307.45,-101.56C347.49,-110.77 417.35,-126.84 461.69,-137.05"/>
<polygon fill="black" stroke="black" points="461.03,-140.49 471.56,-139.32 462.6,-133.67 461.03,-140.49"/>
<text text-anchor="start" x="326" y="-147.44" font-family="sans-serif" font-size="10.00"> &#160;read completed OK</text>
<text text-anchor="start" x="326" y="-136.44" font-family="sans-serif" font-size="10.00"> &#160;handleCompletedRead()</text>
</g>
<!-- STOP -->
<g id="node5" class="node">
<title>STOP</title>
<ellipse fill="#ffd6d6" stroke="black" stroke-width="1.4" cx="503.5" cy="-61.44" rx="30.5" ry="30.5"/>
<ellipse fill="none" stroke="black" stroke-width="1.4" cx="503.5" cy="-61.44" rx="34.5" ry="34.5"/>
<text text-anchor="middle" x="503.5" y="-58.34" font-family="sans-serif" font-size="12.00">STOP</text>
</g>
<!-- READ&#45;&gt;STOP -->
<g id="edge3" class="edge">
<title>READ&#45;&gt;STOP</title>
<path fill="none" stroke="#a00000" d="M307.92,-89.85C347.22,-84.11 414.71,-74.26 458.97,-67.79"/>
<polygon fill="#a00000" stroke="#a00000" points="459.75,-71.22 469.14,-66.31 458.74,-64.29 459.75,-71.22"/>
<text text-anchor="middle" x="388.5" y="-89.44" font-family="sans-serif" font-size="10.00" fill="#a00000"> &#160;short read / EOF &#160;</text>
</g>
<!-- SHOW -->
<g id="node4" class="node">
<title>SHOW</title>
<ellipse fill="#e7d8ff" stroke="black" stroke-width="1.4" cx="721.5" cy="-146.44" rx="32.5" ry="32.5"/>
<text text-anchor="middle" x="721.5" y="-143.34" font-family="sans-serif" font-size="12.00">SHOW</text>
</g>
<!-- FEED&#45;&gt;SHOW -->
<g id="edge4" class="edge">
<title>FEED&#45;&gt;SHOW</title>
<path fill="none" stroke="black" d="M536.37,-146.44C574.13,-146.44 637.14,-146.44 678.76,-146.44"/>
<polygon fill="black" stroke="black" points="678.85,-149.94 688.85,-146.44 678.85,-142.94 678.85,-149.94"/>
<text text-anchor="start" x="556" y="-160.44" font-family="sans-serif" font-size="10.00"> &#160;publish frame</text>
<text text-anchor="start" x="556" y="-149.44" font-family="sans-serif" font-size="10.00"> &#160;maybeDisplayFrame()</text>
</g>
<!-- SHOW&#45;&gt;FREE -->
<g id="edge5" class="edge">
<title>SHOW&#45;&gt;FREE</title>
<path fill="none" stroke="#5a2ca0" d="M698.02,-123.55C665.91,-92.66 603.51,-39.12 538,-18.44 369.44,34.78 157.11,-41.15 71.68,-77.22"/>
<polygon fill="#5a2ca0" stroke="#5a2ca0" points="70.19,-74.05 62.37,-81.21 72.94,-80.49 70.19,-74.05"/>
<text text-anchor="start" x="328.5" y="-19.44" font-family="sans-serif" font-size="10.00" fill="#5a2ca0"> &#160;GUI released frame</text>
<text text-anchor="start" x="328.5" y="-8.44" font-family="sans-serif" font-size="10.00" fill="#5a2ca0"> &#160;(toDisplay_ == nullptr)</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

View file

@ -0,0 +1,56 @@
// Cross-thread architecture and data-visibility handoff.
// Render: dot -Tsvg thread-handoff.dot -o thread-handoff.svg
//
// One direction of data flow: the Proactor thread reads file blocks into the
// ring of buffers and publishes a ready frame through the single atomic baton
// `toDisplay_`; the GUI thread picks it up, uploads it to the GPU, and hands
// the slot back. The release/acquire pairs on the baton carry buffer-content
// visibility in both directions.
digraph handoff {
rankdir=LR;
bgcolor="transparent";
compound=true;
fontname="sans-serif";
node [fontname="sans-serif", fontsize=11];
edge [fontname="sans-serif", fontsize=10];
file [shape=cylinder, fillcolor="#eeeeee", style=filled, label="demovideo.raw"];
subgraph cluster_proactor {
label=" »Proactor« thread — playVideo() loop ";
labeljust="l";
style="filled,rounded"; fillcolor="#eef4ff"; color="#7092c0"; fontsize=12;
wait [shape=box, style="rounded,filled", fillcolor="#ffffff",
label="io_uring_wait_cqe()\n(blocking)"];
timer [shape=box, style="rounded,filled", fillcolor="#ffedcc",
label="frame-beat timer\n20 ms · multishot"];
ring [shape=record, style=filled, fillcolor="#ffffff",
label="ring buffers\ (RING_SIZ)|<s0>slot|<s1>slot|<s2>slot|<s3>…"];
}
subgraph cluster_gui {
label=" GTK / GUI thread ";
labeljust="l";
style="filled,rounded"; fillcolor="#eefaee"; color="#6bab6b"; fontsize=12;
disp [shape=box, style="rounded,filled", fillcolor="#ffffff",
label="EglDisplay::displayFrame()\nretrieveFrame() / completedFrame()"];
gpu [shape=box, style="rounded,filled", fillcolor="#ffffff",
label="GPU texture\n(glTexImage2D → draw)"];
}
baton [shape=box, style="filled", fillcolor="#e7d8ff", penwidth=1.6,
label="toDisplay_\nstd::atomic<Buffer*>"];
file -> ring [label="io_uring read", lhead=cluster_proactor];
timer -> wait [style=dotted, label="CQE (TICK_TAG)"];
ring -> baton [label="publish\nstore(release)", color="#5a2ca0", fontcolor="#5a2ca0"];
baton -> disp [label="retrieveFrame\nload(acquire)", color="#5a2ca0", fontcolor="#5a2ca0"];
disp -> gpu [label="upload + draw"];
disp -> baton [label="completedFrame\nstore(nullptr, release)",
style=dashed, color="#3a7a3a", fontcolor="#3a7a3a"];
{ rank=same; baton; }
}

View file

@ -0,0 +1,123 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 2.42.4 (0)
-->
<!-- Title: handoff Pages: 1 -->
<svg width="728pt" height="352pt"
viewBox="0.00 0.00 728.00 352.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 348)">
<title>handoff</title>
<g id="clust1" class="cluster">
<title>cluster_proactor</title>
<path fill="#eef4ff" stroke="#7092c0" d="M241.5,-143C241.5,-143 688,-143 688,-143 694,-143 700,-149 700,-155 700,-155 700,-324 700,-324 700,-330 694,-336 688,-336 688,-336 241.5,-336 241.5,-336 235.5,-336 229.5,-330 229.5,-324 229.5,-324 229.5,-155 229.5,-155 229.5,-149 235.5,-143 241.5,-143"/>
<text text-anchor="middle" x="361" y="-322.4" font-family="sans-serif" font-size="12.00"> &#160;»Proactor« thread &#160;— &#160;playVideo() loop &#160;</text>
</g>
<g id="clust2" class="cluster">
<title>cluster_gui</title>
<path fill="#eefaee" stroke="#6bab6b" d="M205,-8C205,-8 700,-8 700,-8 706,-8 712,-14 712,-20 712,-20 712,-69 712,-69 712,-75 706,-81 700,-81 700,-81 205,-81 205,-81 199,-81 193,-75 193,-69 193,-69 193,-20 193,-20 193,-14 199,-8 205,-8"/>
<text text-anchor="middle" x="258.5" y="-67.4" font-family="sans-serif" font-size="12.00"> &#160;GTK / GUI thread &#160;</text>
</g>
<!-- file -->
<g id="node1" class="node">
<title>file</title>
<path fill="#eeeeee" stroke="black" d="M101,-216.73C101,-218.53 78.37,-220 50.5,-220 22.63,-220 0,-218.53 0,-216.73 0,-216.73 0,-187.27 0,-187.27 0,-185.47 22.63,-184 50.5,-184 78.37,-184 101,-185.47 101,-187.27 101,-187.27 101,-216.73 101,-216.73"/>
<path fill="none" stroke="black" d="M101,-216.73C101,-214.92 78.37,-213.45 50.5,-213.45 22.63,-213.45 0,-214.92 0,-216.73"/>
<text text-anchor="middle" x="50.5" y="-199.2" font-family="sans-serif" font-size="11.00">demovideo.raw</text>
</g>
<!-- ring -->
<g id="node4" class="node">
<title>ring</title>
<polygon fill="#ffffff" stroke="black" points="237.5,-152 237.5,-252 379.5,-252 379.5,-152 237.5,-152"/>
<text text-anchor="middle" x="308.5" y="-239.2" font-family="sans-serif" font-size="11.00">ring buffers (RING_SIZ)</text>
<polyline fill="none" stroke="black" points="237.5,-232 379.5,-232 "/>
<text text-anchor="middle" x="308.5" y="-219.2" font-family="sans-serif" font-size="11.00">slot</text>
<polyline fill="none" stroke="black" points="237.5,-212 379.5,-212 "/>
<text text-anchor="middle" x="308.5" y="-199.2" font-family="sans-serif" font-size="11.00">slot</text>
<polyline fill="none" stroke="black" points="237.5,-192 379.5,-192 "/>
<text text-anchor="middle" x="308.5" y="-179.2" font-family="sans-serif" font-size="11.00">slot</text>
<polyline fill="none" stroke="black" points="237.5,-172 379.5,-172 "/>
<text text-anchor="middle" x="308.5" y="-159.2" font-family="sans-serif" font-size="11.00">…</text>
</g>
<!-- file&#45;&gt;ring -->
<g id="edge1" class="edge">
<title>file&#45;&gt;ring</title>
<path fill="none" stroke="black" d="M101.16,-202C136.89,-202 185.86,-202 227.12,-202"/>
<polygon fill="black" stroke="black" points="219.5,-205.5 229.5,-202 219.5,-198.5 219.5,-205.5"/>
<text text-anchor="middle" x="151" y="-205" font-family="sans-serif" font-size="10.00">io_uring read</text>
</g>
<!-- wait -->
<g id="node2" class="node">
<title>wait</title>
<path fill="#ffffff" stroke="black" d="M680,-307C680,-307 582,-307 582,-307 576,-307 570,-301 570,-295 570,-295 570,-283 570,-283 570,-277 576,-271 582,-271 582,-271 680,-271 680,-271 686,-271 692,-277 692,-283 692,-283 692,-295 692,-295 692,-301 686,-307 680,-307"/>
<text text-anchor="middle" x="631" y="-292.2" font-family="sans-serif" font-size="11.00">io_uring_wait_cqe()</text>
<text text-anchor="middle" x="631" y="-280.2" font-family="sans-serif" font-size="11.00">(blocking)</text>
</g>
<!-- timer -->
<g id="node3" class="node">
<title>timer</title>
<path fill="#ffedcc" stroke="black" d="M353,-307C353,-307 264,-307 264,-307 258,-307 252,-301 252,-295 252,-295 252,-283 252,-283 252,-277 258,-271 264,-271 264,-271 353,-271 353,-271 359,-271 365,-277 365,-283 365,-283 365,-295 365,-295 365,-301 359,-307 353,-307"/>
<text text-anchor="middle" x="308.5" y="-292.2" font-family="sans-serif" font-size="11.00">frame&#45;beat timer</text>
<text text-anchor="middle" x="308.5" y="-280.2" font-family="sans-serif" font-size="11.00">20 ms · multishot</text>
</g>
<!-- timer&#45;&gt;wait -->
<g id="edge2" class="edge">
<title>timer&#45;&gt;wait</title>
<path fill="none" stroke="black" stroke-dasharray="1,5" d="M365.33,-289C419.3,-289 501.26,-289 559.66,-289"/>
<polygon fill="black" stroke="black" points="559.92,-292.5 569.92,-289 559.92,-285.5 559.92,-292.5"/>
<text text-anchor="middle" x="487" y="-292" font-family="sans-serif" font-size="10.00">CQE (TICK_TAG)</text>
</g>
<!-- baton -->
<g id="node7" class="node">
<title>baton</title>
<polygon fill="#e7d8ff" stroke="black" stroke-width="1.6" points="698.5,-131 563.5,-131 563.5,-95 698.5,-95 698.5,-131"/>
<text text-anchor="middle" x="631" y="-116.2" font-family="sans-serif" font-size="11.00">toDisplay_</text>
<text text-anchor="middle" x="631" y="-104.2" font-family="sans-serif" font-size="11.00">std::atomic&lt;Buffer*&gt;</text>
</g>
<!-- ring&#45;&gt;baton -->
<g id="edge3" class="edge">
<title>ring&#45;&gt;baton</title>
<path fill="none" stroke="#5a2ca0" d="M379.84,-182.47C431.79,-168.04 502.39,-148.44 555.34,-133.73"/>
<polygon fill="#5a2ca0" stroke="#5a2ca0" points="556.31,-137.09 565.01,-131.05 554.44,-130.35 556.31,-137.09"/>
<text text-anchor="middle" x="487" y="-179" font-family="sans-serif" font-size="10.00" fill="#5a2ca0">publish</text>
<text text-anchor="middle" x="487" y="-168" font-family="sans-serif" font-size="10.00" fill="#5a2ca0">store(release)</text>
</g>
<!-- disp -->
<g id="node5" class="node">
<title>disp</title>
<path fill="#ffffff" stroke="black" d="M404,-52C404,-52 213,-52 213,-52 207,-52 201,-46 201,-40 201,-40 201,-28 201,-28 201,-22 207,-16 213,-16 213,-16 404,-16 404,-16 410,-16 416,-22 416,-28 416,-28 416,-40 416,-40 416,-46 410,-52 404,-52"/>
<text text-anchor="middle" x="308.5" y="-37.2" font-family="sans-serif" font-size="11.00">EglDisplay::displayFrame()</text>
<text text-anchor="middle" x="308.5" y="-25.2" font-family="sans-serif" font-size="11.00">retrieveFrame() / completedFrame()</text>
</g>
<!-- gpu -->
<g id="node6" class="node">
<title>gpu</title>
<path fill="#ffffff" stroke="black" d="M692,-52C692,-52 570,-52 570,-52 564,-52 558,-46 558,-40 558,-40 558,-28 558,-28 558,-22 564,-16 570,-16 570,-16 692,-16 692,-16 698,-16 704,-22 704,-28 704,-28 704,-40 704,-40 704,-46 698,-52 692,-52"/>
<text text-anchor="middle" x="631" y="-37.2" font-family="sans-serif" font-size="11.00">GPU texture</text>
<text text-anchor="middle" x="631" y="-25.2" font-family="sans-serif" font-size="11.00">(glTexImage2D → draw)</text>
</g>
<!-- disp&#45;&gt;gpu -->
<g id="edge5" class="edge">
<title>disp&#45;&gt;gpu</title>
<path fill="none" stroke="black" d="M416.22,-34C458.94,-34 507.39,-34 547.35,-34"/>
<polygon fill="black" stroke="black" points="547.58,-37.5 557.58,-34 547.58,-30.5 547.58,-37.5"/>
<text text-anchor="middle" x="487" y="-37" font-family="sans-serif" font-size="10.00">upload + draw</text>
</g>
<!-- disp&#45;&gt;baton -->
<g id="edge6" class="edge">
<title>disp&#45;&gt;baton</title>
<path fill="none" stroke="#3a7a3a" stroke-dasharray="5,2" d="M341.45,-52.07C365.99,-65.08 401.13,-81.88 434,-91 472.45,-101.67 516.52,-107.18 553.11,-110.02"/>
<polygon fill="#3a7a3a" stroke="#3a7a3a" points="553,-113.52 563.22,-110.76 553.5,-106.54 553,-113.52"/>
<text text-anchor="middle" x="487" y="-122" font-family="sans-serif" font-size="10.00" fill="#3a7a3a">completedFrame</text>
<text text-anchor="middle" x="487" y="-111" font-family="sans-serif" font-size="10.00" fill="#3a7a3a">store(nullptr, release)</text>
</g>
<!-- baton&#45;&gt;disp -->
<g id="edge4" class="edge">
<title>baton&#45;&gt;disp</title>
<path fill="none" stroke="#5a2ca0" d="M577.04,-94.93C570.45,-91.95 563.95,-88.64 558,-85 548.87,-79.42 549.69,-73.55 540,-69 519.39,-59.33 472.38,-51.4 426.16,-45.55"/>
<polygon fill="#5a2ca0" stroke="#5a2ca0" points="426.36,-42.05 416.01,-44.29 425.5,-48.99 426.36,-42.05"/>
<text text-anchor="middle" x="487" y="-83" font-family="sans-serif" font-size="10.00" fill="#5a2ca0">retrieveFrame</text>
<text text-anchor="middle" x="487" y="-72" font-family="sans-serif" font-size="10.00" fill="#5a2ca0">load(acquire)</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.4 KiB