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:
parent
72e1838b36
commit
703e60c85a
8 changed files with 772 additions and 71 deletions
|
|
@ -1984,7 +1984,10 @@
|
|||
<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;"/>
|
||||
<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>
|
||||
<head/>
|
||||
<body>
|
||||
|
|
@ -2007,8 +2010,12 @@
|
|||
</p>
|
||||
</body>
|
||||
</html></richcontent>
|
||||
<font BOLD="true" NAME="SansSerif" SIZE="12"/>
|
||||
<icon BUILTIN="back"/>
|
||||
<font NAME="SansSerif" SIZE="12"/>
|
||||
</node>
|
||||
<node BACKGROUND_COLOR="#f0d5c5" COLOR="#99005b" CREATED="1784421416065" ID="ID_428576666" MODIFIED="1784421838679" TEXT="und diese plausible Schluß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 COLOR="#435e98" CREATED="1784079033427" ID="ID_1573408699" MODIFIED="1784079050907" TEXT="Zustandsübergänge">
|
||||
<font BOLD="true" NAME="SansSerif" SIZE="12"/>
|
||||
|
|
@ -2477,7 +2484,7 @@
|
|||
</node>
|
||||
</node>
|
||||
<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>
|
||||
<head/>
|
||||
<body>
|
||||
|
|
@ -2488,8 +2495,10 @@
|
|||
</html></richcontent>
|
||||
<font NAME="SansSerif" SIZE="12"/>
|
||||
<icon BUILTIN="help"/>
|
||||
<icon BUILTIN="button_cancel"/>
|
||||
<node CREATED="1784229415567" ID="ID_572644187" MODIFIED="1784229431525" TEXT="gä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="1784423360704" ID="ID_104238528" LINK="#ID_524518128" MODIFIED="1784423391250" TEXT="hat sich durch die Umstellung auf den atomic-Buffer-Pointer erledigt"/>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
|
|
@ -2832,6 +2841,132 @@
|
|||
</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 überprü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ällig</i> funktioniert es aber trotzdem
|
||||
</p>
|
||||
</body>
|
||||
</html></richcontent>
|
||||
<richcontent TYPE="NOTE"><html>
|
||||
<head/>
|
||||
<body>
|
||||
<p>
|
||||
und zwar, wie Claudes ausführt, weil unter Linux die Lese / Schreib-Kernel-Calls einen Syncrhonisationspunkt darstellen. Da der Glib::Dispatcher über einen Pipe-Write/Read implementiert ist, führt bereits das Dispatch zu einer vollen Barrier, und daher ist es unter Linux ausgeschlossen, daß der GUI-Thread noch einen alten, verschleppten Cursor-State sieht.
|
||||
</p>
|
||||
<p>
|
||||
|
||||
</p>
|
||||
<p>
|
||||
Das gilt aber nicht allgemein und für andere Betriebssysteme und ist mithin nicht durch den C++-Standard gedeckt. Deshalb <i>beherrscht diese Lösung</i> nicht das Problem, und die dargestellte Begrü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ösungsansatz beruht darauf daß, nur ein Zustand synchronisiert werden muß">
|
||||
<icon BUILTIN="idea"/>
|
||||
</node>
|
||||
<node BACKGROUND_COLOR="#c8c0b6" COLOR="#435e98" CREATED="1784422189692" ID="ID_524518128" MODIFIED="1784422226476" TEXT="Atomic von der State-Engine trennen ⟹ stattdessen ein std::atomic<Buffer*>">
|
||||
<node CREATED="1784422263938" ID="ID_1098483559" MODIFIED="1784422275268" TEXT="das fü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ö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 »reclaim« des Buffers inkrementieren"/>
|
||||
<node CREATED="1784422475931" ID="ID_566892395" MODIFIED="1784422543922" TEXT="d.h. die Backpressure wird hier realisiert, indem erst danach der nächste Slot übprüft wird"/>
|
||||
<node CREATED="1784422546982" ID="ID_230854798" MODIFIED="1784422774468" TEXT="diese Lösung gefällt mir: Logik hat jetzt weitgehend die Form einer State-Engine"/>
|
||||
<node CREATED="1784422779995" ID="ID_1873586884" MODIFIED="1784422802079" TEXT="Claude schlägt vor, ein Switch-Case zu verwenden — 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> 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üßte das GUI sofort terminieren, während der Proactor-Thread grade erst startet ⟹ dann wü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äre, daß der Proactor-Thread dann hängt und irgenwann blockt"/>
|
||||
<node CREATED="1784423036125" ID="ID_1995226372" MODIFIED="1784423050746" TEXT="und die Applikation würde im Shutdown verklemmen (im thread.join)"/>
|
||||
<node COLOR="#338800" CREATED="1784423055506" ID="ID_616531880" MODIFIED="1784423089086" TEXT="Mein Vorschlag: der Player ist sowiso »RAII« ⟹ 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>  prüfen
|
||||
</p>
|
||||
</body>
|
||||
</html></richcontent>
|
||||
<richcontent TYPE="NOTE"><html>
|
||||
<head/>
|
||||
<body>
|
||||
<p>
|
||||
Begründung: im Moment kann das nicht passieren, da die Queue ausreichend dimensioniert ist auf RING_SIZ + 1.  Sollte sich sowas aber unter Maintainnance ändern, dann könnte man in einen Segfault laufen! (Ja das sehe ich genauso und hätte es in Prod-Code tatsächlich in eine Hilfsfunktion gepackt; hier aber habe ich bewußt aus didaktischen Grü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ßerdem habe ich vergessen: <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">&</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üllen">
|
||||
<richcontent TYPE="NOTE"><html>
|
||||
<head/>
|
||||
<body>
|
||||
<p>
|
||||
...da der Konversations-Status ein komplettes Bild der Steuerlogik enthält, bietet sich das gradezu an; stilistisch überarbeiten kann ich den Text spä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>
|
||||
|
|
|
|||
|
|
@ -56,8 +56,11 @@ namespace {
|
|||
* 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
|
||||
* driven by a frame-beat timer, that is also based on IO_URING.
|
||||
* Each buffer »slot« is associated with a #BuffState flag, implemented
|
||||
* as `std::atomic` to also serve for cross-thread visibility.
|
||||
* The lifecycle of each buffer »slot« is tracked by a #BuffState flag,
|
||||
* 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,
|
||||
* yet will immediately launch the »Proactor« thread from the ctor.
|
||||
* @remarks
|
||||
|
|
@ -75,10 +78,9 @@ class UringVideoPlayer
|
|||
using Buffer = VideoDisplay::Buffer;
|
||||
|
||||
enum BuffState { FREE, READ, FEED, SHOW, STOP };
|
||||
using AtomicBuffState = std::atomic<BuffState>;
|
||||
|
||||
using BufferAlloc = unique_ptr<Buffer[]>;
|
||||
using BuffStateAlloc = unique_ptr<AtomicBuffState[]>;
|
||||
using BuffStateAlloc = unique_ptr<BuffState[]>;
|
||||
|
||||
/**
|
||||
* Connector to the VideoDisplay for dispatching video data.
|
||||
|
|
@ -98,18 +100,14 @@ class UringVideoPlayer
|
|||
BufferDispatcher (UringVideoPlayer& player)
|
||||
: player_{player}
|
||||
{ }
|
||||
|
||||
private:
|
||||
Buffer& getCurrentFrame();
|
||||
AtomicBuffState& getCurrentFrameState();
|
||||
};
|
||||
|
||||
size_t nextRead_{0};
|
||||
size_t nextShow_{0};
|
||||
size_t currShow_{0};
|
||||
BufferAlloc buffer_{};
|
||||
BuffStateAlloc buffState_{};
|
||||
std::atomic_bool playing_{false};
|
||||
std::atomic<Buffer*> toDisplay_{nullptr};
|
||||
std::atomic_bool playing_{true};
|
||||
BufferDispatcher dispatchFrame_;
|
||||
VideoDisplay videoDisplayer_;
|
||||
std::thread proactorThread_;
|
||||
|
|
@ -147,12 +145,12 @@ UringVideoPlayer::playVideo()
|
|||
__FAIL ("open file \""+RAW_VIDEO_INPUT_FILE+"\"");
|
||||
|
||||
// Allocate several data buffers (used as ring)
|
||||
buffer_ = make_unique<Buffer[]> (RING_SIZ);
|
||||
buffState_ = make_unique<AtomicBuffState[]> (RING_SIZ);
|
||||
buffer_ = make_unique<Buffer[]> (RING_SIZ);
|
||||
buffState_ = make_unique<BuffState[]> (RING_SIZ);
|
||||
|
||||
// initialise uRing Kernel interface
|
||||
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
|
||||
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);
|
||||
|
||||
/* =============== proactor-loop =============== */
|
||||
playing_ = true;
|
||||
while (playing_.load (std::memory_order_acquire))
|
||||
{
|
||||
io_uring_cqe* completion;
|
||||
|
|
@ -188,6 +185,7 @@ UringVideoPlayer::playVideo()
|
|||
io_uring_cqe_seen (&uRing, completion);
|
||||
}
|
||||
close (inputFD);
|
||||
io_uring_queue_exit (&uRing);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -212,7 +210,7 @@ UringVideoPlayer::addNextReadSubmissions (io_uring& uRing, int inputFD)
|
|||
,BUFFSIZ * srcOffset
|
||||
);
|
||||
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_);
|
||||
++nextRead_;
|
||||
}
|
||||
|
|
@ -232,84 +230,74 @@ UringVideoPlayer::handleCompletedRead (io_uring_cqe& completion)
|
|||
timings.markStop(frameNr);
|
||||
|
||||
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
|
||||
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.
|
||||
* @remark determine the buffer slot where the frame data was placed,
|
||||
* based on the cursor for the next frame to display, wrapping
|
||||
* through the ring of buffers. Depending on the state marked
|
||||
* for this buffer, the frame can be displayed if the data
|
||||
* was already loaded, or otherwise the frame will slip
|
||||
* to the next regular time tick.
|
||||
* @remark the cursor #nextShow_ walks the ring of buffers. It rests on the slot
|
||||
* currently lent to the GUI-thread (marked #SHOW) until the latter did signal
|
||||
* completion by clearing the atomic #toDisplay_. Only then is the slot reclaimed
|
||||
* (#FREE) and the cursor advanced. The next slot reached thusly is displayed if
|
||||
* its data was already loaded (#FEED); otherwise the frame slips forward to
|
||||
* 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
|
||||
UringVideoPlayer::maybeDisplayFrame()
|
||||
{
|
||||
auto prevState = buffState_[currShow_ % RING_SIZ].load (std::memory_order_acquire);
|
||||
auto nextState = buffState_[nextShow_ % RING_SIZ].load (std::memory_order_relaxed);
|
||||
|
||||
if (nextState == FEED and prevState != SHOW)
|
||||
{ // can hand over this next frame to the VideoDisplay in the GUI-thread
|
||||
currShow_ = nextShow_;
|
||||
// check and possibly reclaim previously displayed buffer
|
||||
if (SHOW == buffState_[nextShow_ % RING_SIZ] and
|
||||
nullptr == toDisplay_.load (std::memory_order_acquire)) // read barrier
|
||||
{ // GUI thread did mark completion
|
||||
buffState_[nextShow_ % RING_SIZ] = FREE;
|
||||
++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;
|
||||
std::cout << "Playback stopped due to short read" << std::endl;
|
||||
}
|
||||
else
|
||||
break;
|
||||
|
||||
default: // READ (IO still pending) or SHOW (GUI not yet caught up)
|
||||
std::cout << "WARNING: Frame slipped" << std::endl;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** pick up the buffer published by the proactor-thread */
|
||||
UringVideoPlayer::Buffer*
|
||||
UringVideoPlayer::BufferDispatcher::retrieveFrame()
|
||||
{
|
||||
// retrieve buffer state from proactor-thread; this also creates a read barrier
|
||||
auto frameState = getCurrentFrameState().load (std::memory_order_acquire);
|
||||
if (frameState == SHOW)
|
||||
return & getCurrentFrame();
|
||||
else
|
||||
return nullptr;
|
||||
// (can happen only in boundary cases)
|
||||
}
|
||||
return player_.toDisplay_.load (std::memory_order_acquire);
|
||||
} // acquire-load also creates a read barrier
|
||||
|
||||
|
||||
/** signal completion of display activity to the proactor-thread */
|
||||
void
|
||||
UringVideoPlayer::BufferDispatcher::completedFrame()
|
||||
{
|
||||
// mark buffer as display complete for the proactor-thread; this also creates a write barrier
|
||||
getCurrentFrameState().store (FREE, std::memory_order_release);
|
||||
}
|
||||
|
||||
|
||||
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];
|
||||
}
|
||||
player_.toDisplay_.store (nullptr, std::memory_order_release);
|
||||
} // release-store also creates a write barrier
|
||||
|
||||
|
||||
void
|
||||
|
|
|
|||
|
|
@ -8,11 +8,23 @@ Specification for the Demo
|
|||
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.
|
||||
|
||||
[red yellow-background]#TBW#
|
||||
|
||||
|
||||
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
|
||||
~~~~~~~~
|
||||
|
|
@ -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
|
||||
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.
|
||||
|
||||
|
||||
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
|
||||
|
|
|
|||
111
FrOSCon26/research/diagrams/ring-buffer.svg
Normal file
111
FrOSCon26/research/diagrams/ring-buffer.svg
Normal 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 |
34
FrOSCon26/research/diagrams/state-engine.dot
Normal file
34
FrOSCon26/research/diagrams/state-engine.dot
Normal 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; }
|
||||
}
|
||||
83
FrOSCon26/research/diagrams/state-engine.svg
Normal file
83
FrOSCon26/research/diagrams/state-engine.svg
Normal 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-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->READ -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>FREE->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">  submit read</text>
|
||||
<text text-anchor="start" x="83" y="-97.44" font-family="sans-serif" font-size="10.00">  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->FEED -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>READ->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">  read completed OK</text>
|
||||
<text text-anchor="start" x="326" y="-136.44" font-family="sans-serif" font-size="10.00">  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->STOP -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>READ->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">  short read / EOF  </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->SHOW -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>FEED->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">  publish frame</text>
|
||||
<text text-anchor="start" x="556" y="-149.44" font-family="sans-serif" font-size="10.00">  maybeDisplayFrame()</text>
|
||||
</g>
|
||||
<!-- SHOW->FREE -->
|
||||
<g id="edge5" class="edge">
|
||||
<title>SHOW->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">  GUI released frame</text>
|
||||
<text text-anchor="start" x="328.5" y="-8.44" font-family="sans-serif" font-size="10.00" fill="#5a2ca0">  (toDisplay_ == nullptr)</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.7 KiB |
56
FrOSCon26/research/diagrams/thread-handoff.dot
Normal file
56
FrOSCon26/research/diagrams/thread-handoff.dot
Normal 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; }
|
||||
}
|
||||
123
FrOSCon26/research/diagrams/thread-handoff.svg
Normal file
123
FrOSCon26/research/diagrams/thread-handoff.svg
Normal 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">  »Proactor« thread  —  playVideo() loop  </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">  GTK / GUI thread  </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->ring -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>file->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-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->wait -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>timer->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<Buffer*></text>
|
||||
</g>
|
||||
<!-- ring->baton -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>ring->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->gpu -->
|
||||
<g id="edge5" class="edge">
|
||||
<title>disp->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->baton -->
|
||||
<g id="edge6" class="edge">
|
||||
<title>disp->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->disp -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>baton->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 |
Loading…
Reference in a new issue