fb_sdl: move SDL event loop to dedicated thread

This patch is a preparatory step for relaxing the strictly periodic
operation of fb_sdl. With the new design, the SDL event loop can block
for events while Genode's main entrypoint stays receptive for I/O.
The main entrypoint can interact with the SDL thread by injecting
SDL user events.

The patch also replaces the full-screen clearing and update of the
SDL window by an update of the captured bounding box only. This reduces
the CPU load of fb_sdl when idle. When updating a small part of the
screen (e.g., when moving the mouse only), the load is still rather
heavy though.

Issue #5344
This commit is contained in:
Norman Feske 2024-09-16 15:46:17 +02:00 committed by Christian Helmuth
parent 3f1759a4d1
commit 2d3c2fc258

View File

@ -6,7 +6,7 @@
*/ */
/* /*
* Copyright (C) 2006-2017 Genode Labs GmbH * Copyright (C) 2006-2024 Genode Labs GmbH
* *
* This file is part of the Genode OS framework, which is distributed * This file is part of the Genode OS framework, which is distributed
* under the terms of the GNU Affero General Public License version 3. * under the terms of the GNU Affero General Public License version 3.
@ -31,63 +31,100 @@
namespace Fb_sdl { namespace Fb_sdl {
class Main; class Main;
class Sdl;
using namespace Genode; using namespace Genode;
}
/* fatal exceptions */
struct Sdl_init_failed : Genode::Exception { };
struct Sdl_videodriver_not_supported : Genode::Exception { };
struct Sdl_createwindow_failed : Genode::Exception { };
struct Sdl_createrenderer_failed : Genode::Exception { };
struct Sdl_creatergbsurface_failed : Genode::Exception { };
struct Sdl_createtexture_failed : Genode::Exception { };
struct Fb_sdl::Main
{
Env &_env;
Attached_rom_dataspace _config { _env, "config" };
Timer::Connection _timer { _env };
Event::Connection _event { _env };
using Area = Capture::Area; using Area = Capture::Area;
using Point = Capture::Area; using Rect = Capture::Rect;
using Pixel = Capture::Pixel; using Pixel = Capture::Pixel;
using Affected_rects = Capture::Session::Affected_rects; using Affected_rects = Capture::Session::Affected_rects;
using Event_batch = Event::Session_client::Batch; using Event_batch = Event::Session_client::Batch;
void _init_sdl() static constexpr int USER_EVENT_CAPTURE_WAKEUP = 99;
}
/**
* Interplay with libSDL
*/
struct Fb_sdl::Sdl : Noncopyable
{
Event::Connection &_event;
Capture::Connection &_capture;
Region_map &_rm;
struct Ticks { Uint32 ms; };
struct Attr
{ {
/* Area initial_size;
* Initialize libSDL window
*/ double fps; /* frames per second */
if (SDL_Init(SDL_INIT_VIDEO) < 0) { unsigned idle; /* disable capturing after 'idle' frames of no progress */
error("SDL_Init failed (", Genode::Cstring(SDL_GetError()), ")");
throw Sdl_init_failed(); static Attr from_xml(Xml_node const &node)
{
return {
.initial_size = { .w = node.attribute_value("width", 1024u),
.h = node.attribute_value("height", 768u) },
.fps = node.attribute_value("fps", 60.0),
.idle = node.attribute_value("idle", ~0U)
};
} }
SDL_ShowCursor(0); Ticks period() const
{
return { (fps > 0) ? unsigned(1000.0/fps) : 20u };
}
};
Attr const _attr;
/* fatal exceptions */
struct Init_failed : Exception { };
struct Createthread_failed : Exception { };
struct Videodriver_not_supported : Exception { };
struct Createwindow_failed : Exception { };
struct Createrenderer_failed : Exception { };
struct Creatergbsurface_failed : Exception { };
struct Createtexture_failed : Exception { };
void _thread();
static int _entry(void *data_ptr)
{
((Sdl *)data_ptr)->_thread();
return 0;
} }
bool const _sdl_initialized = ( _init_sdl(), true ); SDL_Thread &_init_thread()
{
SDL_Thread *ptr = SDL_CreateThread(_entry, "SDL", this);
if (ptr)
return *ptr;
struct Sdl_window throw Createthread_failed();
}
SDL_Thread &_sdl_thread = _init_thread();
struct Window
{ {
Area const _initial_size; Area const _initial_size;
SDL_Renderer &_sdl_renderer = _init_sdl_renderer(); SDL_Renderer &renderer = _init_renderer();
SDL_Renderer &_init_sdl_renderer() SDL_Renderer &_init_renderer()
{ {
unsigned const window_flags = 0; unsigned const window_flags = 0;
SDL_Window *window_ptr = SDL_CreateWindow("fb_sdl", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, _initial_size.w, _initial_size.h, window_flags); SDL_Window * const window_ptr =
SDL_CreateWindow("fb_sdl", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
_initial_size.w, _initial_size.h, window_flags);
if (!window_ptr) { if (!window_ptr) {
error("SDL_CreateWindow failed (", Genode::Cstring(SDL_GetError()), ")"); error("SDL_CreateWindow failed (", Cstring(SDL_GetError()), ")");
throw Sdl_createwindow_failed(); throw Createwindow_failed();
} }
SDL_SetWindowResizable(window_ptr, SDL_TRUE); SDL_SetWindowResizable(window_ptr, SDL_TRUE);
@ -96,35 +133,30 @@ struct Fb_sdl::Main
unsigned const renderer_flags = SDL_RENDERER_SOFTWARE; unsigned const renderer_flags = SDL_RENDERER_SOFTWARE;
SDL_Renderer *renderer_ptr = SDL_CreateRenderer(window_ptr, index, renderer_flags); SDL_Renderer *renderer_ptr = SDL_CreateRenderer(window_ptr, index, renderer_flags);
if (!renderer_ptr) { if (!renderer_ptr) {
error("SDL_CreateRenderer failed (", Genode::Cstring(SDL_GetError()), ")"); error("SDL_CreateRenderer failed (", Cstring(SDL_GetError()), ")");
throw Sdl_createrenderer_failed(); throw Createrenderer_failed();
} }
return *renderer_ptr; return *renderer_ptr;
} }
Sdl_window(Area size) : _initial_size(size) { } Window(Area size) : _initial_size(size) { }
~Sdl_window() ~Window()
{ {
SDL_DestroyRenderer(&_sdl_renderer); SDL_DestroyRenderer(&renderer);
}
SDL_Renderer &renderer()
{
return _sdl_renderer;
} }
}; };
struct Sdl_screen struct Screen
{ {
Area const size; Area const size;
SDL_Renderer &renderer; SDL_Renderer &renderer;
SDL_Surface &_sdl_surface = _init_sdl_surface(); SDL_Surface &_surface = _init_surface();
SDL_Texture &_sdl_texture = _init_sdl_texture(); SDL_Texture &_texture = _init_texture();
SDL_Surface &_init_sdl_surface() SDL_Surface &_init_surface()
{ {
unsigned const flags = 0; unsigned const flags = 0;
unsigned const bpp = 32; unsigned const bpp = 32;
@ -133,132 +165,215 @@ struct Fb_sdl::Main
unsigned const blue_mask = 0x000000FF; unsigned const blue_mask = 0x000000FF;
unsigned const alpha_mask = 0xFF000000; unsigned const alpha_mask = 0xFF000000;
SDL_Surface *surface_ptr = SDL_CreateRGBSurface(flags, size.w, size.h, bpp, red_mask, green_mask, blue_mask, alpha_mask); SDL_Surface * const surface_ptr =
SDL_CreateRGBSurface(flags, size.w, size.h, bpp,
red_mask, green_mask, blue_mask, alpha_mask);
if (!surface_ptr) { if (!surface_ptr) {
error("SDL_CreateRGBSurface failed (", Genode::Cstring(SDL_GetError()), ")"); error("SDL_CreateRGBSurface failed (", Cstring(SDL_GetError()), ")");
throw Sdl_creatergbsurface_failed(); throw Creatergbsurface_failed();
} }
return *surface_ptr; return *surface_ptr;
} }
SDL_Texture &_init_sdl_texture() SDL_Texture &_init_texture()
{ {
SDL_Texture *texture_ptr = SDL_CreateTexture(&renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, size.w, size.h); SDL_Texture * const texture_ptr =
SDL_CreateTexture(&renderer, SDL_PIXELFORMAT_ARGB8888,
SDL_TEXTUREACCESS_STREAMING, size.w, size.h);
if (!texture_ptr) { if (!texture_ptr) {
error("SDL_CreateTexture failed (", Genode::Cstring(SDL_GetError()), ")"); error("SDL_CreateTexture failed (", Cstring(SDL_GetError()), ")");
throw Sdl_createtexture_failed(); throw Createtexture_failed();
} }
return *texture_ptr; return *texture_ptr;
} }
Sdl_screen(Area size, SDL_Renderer &renderer) : size(size), renderer(renderer) { } Screen(Area size, SDL_Renderer &renderer) : size(size), renderer(renderer) { }
~Sdl_screen() ~Screen()
{ {
SDL_FreeSurface(&_sdl_surface); SDL_FreeSurface(&_surface);
SDL_DestroyTexture(&_sdl_texture); SDL_DestroyTexture(&_texture);
} }
template <typename FN> void with_surface(auto const &fn)
void with_surface(FN const &fn)
{ {
Surface<Pixel> surface { (Pixel *)_sdl_surface.pixels, size }; Surface<Pixel> surface { (Pixel *)_surface.pixels, size };
fn(surface); fn(surface);
} }
void flush() void flush(Capture::Rect const bounding_box)
{ {
SDL_UpdateTexture(&_sdl_texture, nullptr, _sdl_surface.pixels, _sdl_surface.pitch); SDL_Rect const rect { .x = bounding_box.at.x,
SDL_RenderClear(&renderer); .y = bounding_box.at.y,
SDL_RenderCopy(&renderer, &_sdl_texture, nullptr, nullptr); .w = int(bounding_box.area.w),
.h = int(bounding_box.area.h) };
SDL_UpdateTexture(&_texture, nullptr, _surface.pixels, _surface.pitch);
SDL_RenderCopy(&renderer, &_texture, &rect, &rect);
SDL_RenderPresent(&renderer); SDL_RenderPresent(&renderer);
} }
void flush_all() { flush(Rect { { 0, 0 }, size }); }
}; };
Constructible<Sdl_window> _sdl_window { }; Constructible<Window> _window { };
Constructible<Sdl_screen> _sdl_screen { }; Constructible<Screen> _screen { };
Capture::Connection _capture { _env };
Constructible<Capture::Connection::Screen> _captured_screen { }; Constructible<Capture::Connection::Screen> _captured_screen { };
Signal_handler<Main> _timer_handler {
_env.ep(), *this, &Main::_handle_timer };
int _mx = 0, _my = 0; int _mx = 0, _my = 0;
void _handle_sdl_event(Event_batch &, SDL_Event const &); unsigned _capture_woken_up = 0;
void _handle_sdl_events();
void _update_sdl_screen_from_capture() struct Previous_frame
{ {
Affected_rects const affected = _capture.capture_at(Capture::Point(0, 0)); Ticks timestamp;
Ticks remaining; /* remaining ticks to next frame */
unsigned idle; /* capture attempts without progress */
_sdl_screen->with_surface([&] (Surface<Pixel> &surface) { Ticks age() const { return { SDL_GetTicks() - timestamp.ms }; }
};
_captured_screen->with_texture([&] (Texture<Pixel> const &texture) { /* if constructed, the processing of a next frame is scheduled */
Constructible<Previous_frame> _previous_frame { };
affected.for_each_rect([&] (Capture::Rect const rect) { void _schedule_next_frame()
{
_previous_frame.construct(
Previous_frame { .timestamp = { SDL_GetTicks() },
.remaining = { _attr.period() },
.idle = { } });
}
surface.clip(rect); void _handle_event(Event_batch &, SDL_Event const &);
Blit_painter::paint(surface, texture, Capture::Point(0, 0)); bool _update_screen_from_capture()
}); {
}); bool progress = false;
_screen->with_surface([&] (Surface<Pixel> &surface) {
Rect const bounding_box = _captured_screen->apply_to_surface(surface);
progress = (bounding_box.area.count() > 0);
if (progress)
_screen->flush(bounding_box);
}); });
return progress;
/* flush pixels in SDL window */
_sdl_screen->flush();
} }
void _handle_timer() void _resize(Area const size)
{ {
_handle_sdl_events(); _screen.construct(size, _window->renderer);
_update_sdl_screen_from_capture();
}
void _resize(Area size)
{
_sdl_screen.construct(size, _sdl_window->renderer());
using Attr = Capture::Connection::Screen::Attr; using Attr = Capture::Connection::Screen::Attr;
_captured_screen.construct(_capture, _env.rm(), Attr { _captured_screen.construct(_capture, _rm, Attr {
.px = size, .px = size,
.mm = { } }); .mm = { } });
_update_sdl_screen_from_capture(); _update_screen_from_capture();
_schedule_next_frame();
} }
Main(Env &env) : _env(env) /*
{ * Construction executed by the main thread
Area size = Area(_config.xml().attribute_value("width", 1024U), */
_config.xml().attribute_value("height", 768U)); Sdl(Event::Connection &event, Capture::Connection &capture, Region_map &rm,
_sdl_window.construct(size); Attr const attr)
_resize(size); :
_event(event), _capture(capture), _rm(rm), _attr(attr)
_timer.sigh(_timer_handler); { }
_timer.trigger_periodic(100000000 / 5994); /* 59.94Hz */
}
}; };
void Fb_sdl::Main::_handle_sdl_event(Event_batch &batch, SDL_Event const &event) void Fb_sdl::Sdl::_thread()
{
if (SDL_Init(SDL_INIT_VIDEO) < 0) {
error("SDL_Init failed (", Cstring(SDL_GetError()), ")");
throw Init_failed();
}
SDL_ShowCursor(0);
_window.construct(_attr.initial_size);
_resize(_attr.initial_size);
/* mainloop */
for (;;) {
if (_previous_frame.constructed())
SDL_WaitEventTimeout(nullptr, _previous_frame->remaining.ms);
else
SDL_WaitEvent(nullptr);
unsigned const orig_capture_woken_up = _capture_woken_up;
_event.with_batch([&] (Event_batch &batch) {
SDL_Event event { };
while (SDL_PollEvent(&event))
_handle_event(batch, event); });
Ticks const period = _attr.period();
bool const woken_up = (_capture_woken_up != orig_capture_woken_up);
bool const frame_elapsed = _previous_frame.constructed()
&& _previous_frame->age().ms >= period.ms;
if (woken_up || frame_elapsed) {
bool const progress = _update_screen_from_capture();
bool const idle = !progress && !woken_up;
if (idle) {
if (_previous_frame.constructed()) {
_previous_frame->idle++;
if ((_attr.idle < ~0u) && (_previous_frame->idle > _attr.idle))
_previous_frame.destruct(); /* stop capturing */
}
} else {
_schedule_next_frame();
}
} else {
/*
* Events occurred in-between two frames.
* Update timeout for next call of 'SDL_WaitEventTimeout'.
*/
if (_previous_frame.constructed())
_previous_frame->remaining = {
min(period.ms, period.ms - _previous_frame->age().ms) };
}
}
}
void Fb_sdl::Sdl::_handle_event(Event_batch &batch, SDL_Event const &event)
{ {
using namespace Input; using namespace Input;
if (event.type == SDL_WINDOWEVENT && event.window.event == SDL_WINDOWEVENT_RESIZED) { if (event.type == SDL_WINDOWEVENT) {
int w = event.window.data1; if (event.window.event == SDL_WINDOWEVENT_RESIZED) {
int h = event.window.data2;
if (w < 0 || h < 0) { int const w = event.window.data1,
warning("attempt to resize to negative size"); h = event.window.data2;
return;
if (w <= 0 || h <= 0) {
warning("attempt to resize to invalid size");
return;
}
_resize({ unsigned(w), unsigned(h) });
} }
_resize(Area((unsigned)w, (unsigned)h)); _screen->flush_all();
return;
}
if (event.type == SDL_USEREVENT) {
if (event.user.code == USER_EVENT_CAPTURE_WAKEUP)
_capture_woken_up++;
return; return;
} }
@ -299,7 +414,6 @@ void Fb_sdl::Main::_handle_sdl_event(Event_batch &batch, SDL_Event const &event)
} }
} }
/* determine event type */
switch (event.type) { switch (event.type) {
case SDL_KEYUP: case SDL_KEYUP:
@ -328,16 +442,33 @@ void Fb_sdl::Main::_handle_sdl_event(Event_batch &batch, SDL_Event const &event)
} }
void Fb_sdl::Main::_handle_sdl_events() struct Fb_sdl::Main
{ {
SDL_Event event { }; Env &_env;
_event.with_batch([&] (Event_batch &batch) { Attached_rom_dataspace _config { _env, "config" };
while (SDL_PollEvent(&event)) Event::Connection _event { _env };
_handle_sdl_event(batch, event); Capture::Connection _capture { _env };
});
} void _handle_capture_wakeup()
{
SDL_Event ev { };
ev.user = SDL_UserEvent { .type = SDL_USEREVENT,
.timestamp = SDL_GetTicks(),
.windowID = { },
.code = USER_EVENT_CAPTURE_WAKEUP,
.data1 = { },
.data2 = { } };
if (SDL_PushEvent(&ev) == 0)
warning("SDL_PushEvent failed (", Cstring(SDL_GetError()), ")");
}
Sdl _sdl { _event, _capture, _env.rm(), Sdl::Attr::from_xml(_config.xml()) };
Main(Env &env) : _env(env) { }
};
void Component::construct(Genode::Env &env) { static Fb_sdl::Main inst(env); } void Component::construct(Genode::Env &env) { static Fb_sdl::Main inst(env); }