2026-07-11 19:03:31 +02:00
|
|
|
|
/*
|
|
|
|
|
|
demo-uring.cpp - async file IO with io_uring
|
|
|
|
|
|
|
|
|
|
|
|
Copyright (C)
|
|
|
|
|
|
2026, Hermann Vosseler <Ichthyostega@web.de>
|
|
|
|
|
|
|
|
|
|
|
|
This program is free software; you can redistribute it and/or modify it
|
|
|
|
|
|
under the terms of the GNU GPL version 2+ See the LICENSE file for details.
|
|
|
|
|
|
|
|
|
|
|
|
* ************************************************************************/
|
|
|
|
|
|
|
2026-07-14 19:27:27 +02:00
|
|
|
|
|
|
|
|
|
|
#include "commons.hpp"
|
|
|
|
|
|
#include "egl-display.hpp"
|
|
|
|
|
|
#include "gtk-uring-app.hpp"
|
|
|
|
|
|
|
2026-07-11 19:03:31 +02:00
|
|
|
|
#include <iostream>
|
2026-07-14 19:27:27 +02:00
|
|
|
|
#include <memory>
|
|
|
|
|
|
#include <thread>
|
2026-07-15 20:40:59 +02:00
|
|
|
|
#include <atomic>
|
2026-07-14 19:27:27 +02:00
|
|
|
|
|
2026-07-15 23:53:23 +02:00
|
|
|
|
#include <fcntl.h>
|
|
|
|
|
|
#include <liburing.h>
|
|
|
|
|
|
|
2026-07-14 19:27:27 +02:00
|
|
|
|
using std::unique_ptr;
|
2026-07-15 20:40:59 +02:00
|
|
|
|
using std::make_unique;
|
2026-07-15 23:53:23 +02:00
|
|
|
|
using std::chrono::operator ""ms;
|
|
|
|
|
|
|
2026-07-14 19:27:27 +02:00
|
|
|
|
|
|
|
|
|
|
//-----------------hard-coded-config--
|
2026-07-15 18:56:37 +02:00
|
|
|
|
const size_t RING_SIZ = 5;
|
2026-07-15 23:53:23 +02:00
|
|
|
|
const string RAW_VIDEO_INPUT_FILE = "demovideo.raw";
|
2026-07-17 18:59:57 +02:00
|
|
|
|
const uint VIDEO_WIDTH = 1200;
|
|
|
|
|
|
const uint VIDEO_HEIGHT = 675;
|
|
|
|
|
|
const size_t START_FRAME = 40;
|
|
|
|
|
|
const size_t MAX_LOOP_FRAME = 960;
|
2026-07-15 23:53:23 +02:00
|
|
|
|
|
|
|
|
|
|
auto calcSrcFrame = [](size_t runningNr){ return START_FRAME + runningNr % MAX_LOOP_FRAME; };
|
|
|
|
|
|
|
2026-07-17 23:22:33 +02:00
|
|
|
|
const uint EMA_PERIOD = 20;
|
2026-07-14 19:27:27 +02:00
|
|
|
|
//-----------------hard-coded-config--
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-17 23:22:33 +02:00
|
|
|
|
namespace {
|
|
|
|
|
|
/** a set of timers, one for each buffer »slot«
|
|
|
|
|
|
* to observe average IO times */
|
|
|
|
|
|
OverlappingTimers<RING_SIZ, EMA_PERIOD> timings;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-16 19:31:34 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* Read video frames from a raw video file and pass them to OpenGL display.
|
|
|
|
|
|
* Using a »Proactor« thread to coordinate reading via IO_URING.
|
|
|
|
|
|
* Allocates a set of frame-sized buffers (using #RING_SIZ buffers)
|
|
|
|
|
|
* 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.
|
2026-07-18 20:45:01 +02:00
|
|
|
|
* 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.
|
2026-07-16 19:31:34 +02:00
|
|
|
|
* @note UringVideoPlayer must be constructed in the GTK UI thread,
|
|
|
|
|
|
* yet will immediately launch the »Proactor« thread from the ctor.
|
|
|
|
|
|
* @remarks
|
|
|
|
|
|
* - the source video and layout is hard wired
|
|
|
|
|
|
* - playback will either be looped (if the loop fits into the source video)
|
|
|
|
|
|
* - otherwise, when encountering end-of-file, or when the GTK window is closed,
|
|
|
|
|
|
* the »Proactor« thread will terminate.
|
|
|
|
|
|
* - frames will be submitted to display at the regular timer beats,
|
|
|
|
|
|
* whenever they are ready; frames delayed by IO will slip to the next beat.
|
|
|
|
|
|
*/
|
2026-07-14 19:27:27 +02:00
|
|
|
|
class UringVideoPlayer
|
2026-07-15 20:40:59 +02:00
|
|
|
|
: NonCopyable
|
2026-07-14 19:27:27 +02:00
|
|
|
|
{
|
2026-07-15 18:56:37 +02:00
|
|
|
|
using VideoDisplay = EglDisplay<uint8_t, VIDEO_WIDTH,VIDEO_HEIGHT>;
|
|
|
|
|
|
using Buffer = VideoDisplay::Buffer;
|
|
|
|
|
|
|
2026-07-16 19:31:34 +02:00
|
|
|
|
enum BuffState { FREE, READ, FEED, SHOW, STOP };
|
2026-07-15 20:40:59 +02:00
|
|
|
|
|
|
|
|
|
|
using BufferAlloc = unique_ptr<Buffer[]>;
|
2026-07-18 20:45:01 +02:00
|
|
|
|
using BuffStateAlloc = unique_ptr<BuffState[]>;
|
2026-07-15 20:40:59 +02:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Connector to the VideoDisplay for dispatching video data.
|
|
|
|
|
|
* @note inherits from Glib::Dispatcher -> signal frame readiness
|
|
|
|
|
|
* @remark the callback functions allow the VideoDisplay to interact
|
|
|
|
|
|
* with the buffer data and state in a threadsafe manner
|
|
|
|
|
|
*/
|
|
|
|
|
|
class BufferDispatcher
|
|
|
|
|
|
: public VideoDisplay::PixelFeed
|
|
|
|
|
|
{
|
|
|
|
|
|
Buffer* retrieveFrame() override;
|
|
|
|
|
|
void completedFrame() override;
|
|
|
|
|
|
|
|
|
|
|
|
UringVideoPlayer& player_;
|
2026-07-16 19:31:34 +02:00
|
|
|
|
|
2026-07-15 20:40:59 +02:00
|
|
|
|
public:
|
|
|
|
|
|
BufferDispatcher (UringVideoPlayer& player)
|
|
|
|
|
|
: player_{player}
|
|
|
|
|
|
{ }
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
size_t nextRead_{0};
|
|
|
|
|
|
size_t nextShow_{0};
|
|
|
|
|
|
BufferAlloc buffer_{};
|
|
|
|
|
|
BuffStateAlloc buffState_{};
|
2026-07-18 20:45:01 +02:00
|
|
|
|
std::atomic<Buffer*> toDisplay_{nullptr};
|
|
|
|
|
|
std::atomic_bool playing_{true};
|
2026-07-15 20:40:59 +02:00
|
|
|
|
BufferDispatcher dispatchFrame_;
|
2026-07-15 18:56:37 +02:00
|
|
|
|
VideoDisplay videoDisplayer_;
|
2026-07-14 19:27:27 +02:00
|
|
|
|
std::thread proactorThread_;
|
2026-07-14 20:16:18 +02:00
|
|
|
|
|
2026-07-14 19:27:27 +02:00
|
|
|
|
public:
|
|
|
|
|
|
UringVideoPlayer (Gtk::Window& appWindow)
|
2026-07-15 20:40:59 +02:00
|
|
|
|
: dispatchFrame_{*this}
|
|
|
|
|
|
, videoDisplayer_{appWindow, dispatchFrame_}
|
2026-07-14 19:27:27 +02:00
|
|
|
|
, proactorThread_{[this]{ playVideo(); }}
|
|
|
|
|
|
{ }
|
2026-07-14 20:16:18 +02:00
|
|
|
|
|
|
|
|
|
|
void terminate();
|
2026-07-15 18:56:37 +02:00
|
|
|
|
|
2026-07-14 19:27:27 +02:00
|
|
|
|
private:
|
2026-07-15 20:40:59 +02:00
|
|
|
|
void playVideo();
|
2026-07-15 23:53:23 +02:00
|
|
|
|
void addNextReadSubmissions (io_uring&, int inputFD);
|
2026-07-16 03:28:48 +02:00
|
|
|
|
void handleCompletedRead (io_uring_cqe&);
|
|
|
|
|
|
void maybeDisplayFrame();
|
2026-07-14 19:27:27 +02:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-15 20:40:59 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* Logic of the »Proactor Thread«:
|
|
|
|
|
|
* - maintain the working buffers
|
|
|
|
|
|
* - open the raw video file to retrieve data
|
|
|
|
|
|
* - establish an IO_URING connection for reading
|
|
|
|
|
|
* - periodic processing to fill and hand-off buffers
|
|
|
|
|
|
*/
|
|
|
|
|
|
void
|
|
|
|
|
|
UringVideoPlayer::playVideo()
|
2026-07-14 19:27:27 +02:00
|
|
|
|
{
|
2026-07-16 19:31:34 +02:00
|
|
|
|
/* =============== initial-setup =============== */
|
2026-07-15 23:53:23 +02:00
|
|
|
|
int inputFD = open (RAW_VIDEO_INPUT_FILE.c_str(), O_RDONLY);
|
|
|
|
|
|
if (inputFD < 0)
|
|
|
|
|
|
__FAIL ("open file \""+RAW_VIDEO_INPUT_FILE+"\"");
|
|
|
|
|
|
|
|
|
|
|
|
// Allocate several data buffers (used as ring)
|
2026-07-18 20:45:01 +02:00
|
|
|
|
buffer_ = make_unique<Buffer[]> (RING_SIZ);
|
|
|
|
|
|
buffState_ = make_unique<BuffState[]> (RING_SIZ);
|
2026-07-15 23:53:23 +02:00
|
|
|
|
|
|
|
|
|
|
// initialise uRing Kernel interface
|
|
|
|
|
|
io_uring uRing;
|
2026-07-18 20:45:01 +02:00
|
|
|
|
io_uring_queue_init (RING_SIZ+1, &uRing, /*flags*/ 0 ); // Note: allocate one additional »slot« for the timer event(s)
|
2026-07-15 23:53:23 +02:00
|
|
|
|
|
|
|
|
|
|
// Setup periodic frame beat, firing infinitely
|
2026-07-16 19:31:34 +02:00
|
|
|
|
const auto TICK_TAG = UINT64_MAX; // special marker for timer events (differs from possible frame numbers)
|
2026-07-17 18:59:57 +02:00
|
|
|
|
auto timespec = asKernelTimespec (20ms); // 50fps ≙ 20ms per frame
|
2026-07-16 19:31:34 +02:00
|
|
|
|
io_uring_sqe* timerSubmission{io_uring_get_sqe (&uRing)};
|
2026-07-15 23:53:23 +02:00
|
|
|
|
io_uring_prep_timeout (timerSubmission, ×pec, /*no count limit*/ 0, IORING_TIMEOUT_MULTISHOT);
|
|
|
|
|
|
io_uring_sqe_set_data64(timerSubmission, TICK_TAG);
|
|
|
|
|
|
|
|
|
|
|
|
addNextReadSubmissions (uRing, inputFD);
|
|
|
|
|
|
io_uring_submit (&uRing);
|
|
|
|
|
|
|
2026-07-16 19:31:34 +02:00
|
|
|
|
/* =============== proactor-loop =============== */
|
2026-07-16 03:28:48 +02:00
|
|
|
|
while (playing_.load (std::memory_order_acquire))
|
|
|
|
|
|
{
|
|
|
|
|
|
io_uring_cqe* completion;
|
|
|
|
|
|
// blocking wait for the next completion entry
|
|
|
|
|
|
int res = io_uring_wait_cqe (&uRing, &completion);
|
2026-07-16 19:31:34 +02:00
|
|
|
|
if (res < 0)
|
2026-07-16 03:28:48 +02:00
|
|
|
|
__FAIL ("retrieval of io_uring completion failed");
|
|
|
|
|
|
|
|
|
|
|
|
uint64_t tag = completion->user_data;
|
|
|
|
|
|
if (tag == TICK_TAG)
|
|
|
|
|
|
{
|
|
|
|
|
|
maybeDisplayFrame();
|
|
|
|
|
|
addNextReadSubmissions (uRing, inputFD);
|
2026-07-15 23:33:13 +02:00
|
|
|
|
io_uring_submit (&uRing);
|
2026-07-16 03:28:48 +02:00
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
handleCompletedRead (*completion);
|
|
|
|
|
|
|
|
|
|
|
|
// mark completion as treated (Barrier)
|
|
|
|
|
|
io_uring_cqe_seen (&uRing, completion);
|
|
|
|
|
|
}
|
2026-07-15 23:53:23 +02:00
|
|
|
|
close (inputFD);
|
2026-07-18 20:45:01 +02:00
|
|
|
|
io_uring_queue_exit (&uRing);
|
2026-07-15 23:53:23 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Instruct the next read operations,
|
|
|
|
|
|
* using all free buffer slots ahead.
|
|
|
|
|
|
*/
|
|
|
|
|
|
void
|
|
|
|
|
|
UringVideoPlayer::addNextReadSubmissions (io_uring& uRing, int inputFD)
|
|
|
|
|
|
{
|
|
|
|
|
|
assert (0 < inputFD);
|
|
|
|
|
|
const size_t BUFFSIZ = sizeof(Buffer);
|
|
|
|
|
|
|
2026-07-16 19:31:34 +02:00
|
|
|
|
while (FREE == buffState_[nextRead_ % RING_SIZ])
|
2026-07-15 23:53:23 +02:00
|
|
|
|
{
|
|
|
|
|
|
size_t srcOffset = calcSrcFrame (nextRead_);
|
|
|
|
|
|
io_uring_sqe* readSubmission = io_uring_get_sqe (&uRing);
|
2026-08-09 20:11:26 +02:00
|
|
|
|
if (nullptr == readSubmission)
|
|
|
|
|
|
__FAIL ("URing slot allocation failed");
|
|
|
|
|
|
|
2026-07-15 23:53:23 +02:00
|
|
|
|
io_uring_prep_read (readSubmission
|
|
|
|
|
|
,inputFD
|
|
|
|
|
|
,& buffer_[nextRead_ % RING_SIZ]
|
|
|
|
|
|
,BUFFSIZ
|
|
|
|
|
|
,BUFFSIZ * srcOffset
|
|
|
|
|
|
);
|
|
|
|
|
|
io_uring_sqe_set_data64 (readSubmission, nextRead_);
|
2026-07-18 20:45:01 +02:00
|
|
|
|
buffState_[nextRead_ % RING_SIZ] = READ;
|
2026-07-17 23:22:33 +02:00
|
|
|
|
timings.markStart (nextRead_);
|
2026-07-15 23:53:23 +02:00
|
|
|
|
++nextRead_;
|
|
|
|
|
|
}
|
2026-07-15 20:40:59 +02:00
|
|
|
|
}
|
2026-07-14 20:16:18 +02:00
|
|
|
|
|
|
|
|
|
|
|
2026-07-16 03:28:48 +02:00
|
|
|
|
/** transition state of the buffer slot due to reported result */
|
|
|
|
|
|
void
|
|
|
|
|
|
UringVideoPlayer::handleCompletedRead (io_uring_cqe& completion)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (0 > completion.res)
|
|
|
|
|
|
__FAIL ("async read of frame from video file failed");
|
|
|
|
|
|
|
|
|
|
|
|
size_t frameNr = completion.user_data; // Note: completion was marked with the frameNr as »user data«
|
2026-07-16 19:31:34 +02:00
|
|
|
|
auto& state = buffState_[frameNr % RING_SIZ]; // Can thus pick buffer slot based on frame number, cycling through the ring
|
2026-07-16 03:28:48 +02:00
|
|
|
|
|
2026-07-17 23:22:33 +02:00
|
|
|
|
timings.markStop(frameNr);
|
|
|
|
|
|
|
2026-07-16 03:28:48 +02:00
|
|
|
|
if (completion.res != sizeof(Buffer))
|
2026-07-18 20:45:01 +02:00
|
|
|
|
state = STOP; // Insufficient read, possibly end-of-file => signal stop of playback (to ourselves)
|
2026-07-16 03:28:48 +02:00
|
|
|
|
else
|
2026-07-18 20:45:01 +02:00
|
|
|
|
state = FEED; // Success; frame data is in the buffer => mark ready for use at the next tick event
|
2026-07-16 03:28:48 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* At each timer tick: see if the next frame can be displayed.
|
2026-07-18 20:45:01 +02:00
|
|
|
|
* @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.
|
2026-07-16 03:28:48 +02:00
|
|
|
|
*/
|
|
|
|
|
|
void
|
|
|
|
|
|
UringVideoPlayer::maybeDisplayFrame()
|
|
|
|
|
|
{
|
2026-07-18 20:45:01 +02:00
|
|
|
|
// 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;
|
2026-07-16 03:28:48 +02:00
|
|
|
|
++nextShow_;
|
|
|
|
|
|
}
|
2026-07-18 20:45:01 +02:00
|
|
|
|
|
|
|
|
|
|
switch (buffState_[nextShow_ % RING_SIZ])
|
2026-07-16 03:28:48 +02:00
|
|
|
|
{
|
2026-07-18 20:45:01 +02:00
|
|
|
|
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:
|
2026-07-16 03:28:48 +02:00
|
|
|
|
playing_ = false;
|
|
|
|
|
|
std::cout << "Playback stopped due to short read" << std::endl;
|
2026-07-18 20:45:01 +02:00
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
default: // READ (IO still pending) or SHOW (GUI not yet caught up)
|
2026-07-16 03:28:48 +02:00
|
|
|
|
std::cout << "WARNING: Frame slipped" << std::endl;
|
2026-07-18 20:45:01 +02:00
|
|
|
|
break;
|
|
|
|
|
|
}
|
2026-07-16 03:28:48 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-18 20:45:01 +02:00
|
|
|
|
/** pick up the buffer published by the proactor-thread */
|
2026-07-15 20:40:59 +02:00
|
|
|
|
UringVideoPlayer::Buffer*
|
|
|
|
|
|
UringVideoPlayer::BufferDispatcher::retrieveFrame()
|
|
|
|
|
|
{
|
2026-07-18 20:45:01 +02:00
|
|
|
|
return player_.toDisplay_.load (std::memory_order_acquire);
|
|
|
|
|
|
} // acquire-load also creates a read barrier
|
2026-07-14 19:27:27 +02:00
|
|
|
|
|
|
|
|
|
|
|
2026-07-18 20:45:01 +02:00
|
|
|
|
/** signal completion of display activity to the proactor-thread */
|
2026-07-15 20:40:59 +02:00
|
|
|
|
void
|
|
|
|
|
|
UringVideoPlayer::BufferDispatcher::completedFrame()
|
|
|
|
|
|
{
|
2026-07-18 20:45:01 +02:00
|
|
|
|
player_.toDisplay_.store (nullptr, std::memory_order_release);
|
|
|
|
|
|
} // release-store also creates a write barrier
|
2026-07-15 20:40:59 +02:00
|
|
|
|
|
2026-07-14 19:27:27 +02:00
|
|
|
|
|
|
|
|
|
|
void
|
2026-07-14 20:16:18 +02:00
|
|
|
|
UringVideoPlayer::terminate()
|
2026-07-14 19:27:27 +02:00
|
|
|
|
{
|
2026-07-15 20:40:59 +02:00
|
|
|
|
playing_.store (false, std::memory_order_release);
|
|
|
|
|
|
proactorThread_.join();
|
2026-07-14 20:16:18 +02:00
|
|
|
|
videoDisplayer_.cleanUp();
|
2026-07-14 19:27:27 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-11 19:03:31 +02:00
|
|
|
|
|
|
|
|
|
|
|
2026-07-15 20:40:59 +02:00
|
|
|
|
unique_ptr<UringVideoPlayer>
|
|
|
|
|
|
launchPlayer (Gtk::Window& appWindow)
|
2026-07-15 23:33:13 +02:00
|
|
|
|
{ // force minimum size to fit the player...
|
|
|
|
|
|
appWindow.set_size_request (VIDEO_WIDTH, VIDEO_HEIGHT);
|
2026-07-17 18:59:57 +02:00
|
|
|
|
appWindow.set_title ("powered by io_uring");
|
2026-07-15 23:33:13 +02:00
|
|
|
|
appWindow.set_resizable (false);
|
|
|
|
|
|
|
2026-07-15 20:40:59 +02:00
|
|
|
|
return std::make_unique<UringVideoPlayer> (appWindow);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-11 19:03:31 +02:00
|
|
|
|
int
|
|
|
|
|
|
main (int, const char*[])
|
|
|
|
|
|
{
|
2026-07-14 19:27:27 +02:00
|
|
|
|
return GtkUringApp<UringVideoPlayer>{"io-uring-demo"}
|
|
|
|
|
|
.onClick (launchPlayer)
|
2026-07-14 20:16:18 +02:00
|
|
|
|
.onClose ([](auto& player){ player.terminate();})
|
2026-07-14 19:27:27 +02:00
|
|
|
|
.run();
|
2026-07-11 19:03:31 +02:00
|
|
|
|
}
|