qemu-devel
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[Qemu-devel] experience with SDL2, event loop & main thread


From: Liviu Ionescu
Subject: [Qemu-devel] experience with SDL2, event loop & main thread
Date: Sat, 10 Dec 2016 18:16:20 +0200

given the reported problems with the SDL integration, here are some things I 
learned while updating GNU ARM Eclipse QEMU to use SDL2 (SDL1 is already 
issuing various warnings when running on macOS 10.12, and anyway it has several 
limitations).

---

initially I expected that the project requires just a simple build script 
update, but it was much more, I had to completely restructure the graphical 
code.

in short, the main problem is the competition/contradiction in using the main 
thread in QEMU.

by tradition, graphical systems and multi-theaded applications do not make good 
friends, because graphical systems expect to run only on the application's main 
thread.

SDL2 is not an exception; at least on macOS (Cocoa) and GNU/Linux (openGL), all 
my attempts to run the graphical event loop on another thread failed with 
horrible exceptions.


but running the event loop on the main thread is not enough, all graphical 
primitives must be called from the main thread. 

in QEMU this creates a problem, since normally graphic updates are triggered by 
the emulated peripherals, which run on the specific emulator thread (for 
example, in GNU ARM Eclipse QEMU case, for emulated LEDs, GPIO pin changes 
alternate a rectangle on the board picture).

there might be specific primitives/configurations/platforms where this rule can 
be relaxed, but the only portable, trouble free, solution is to run everything 
on the main thread (SDL window creation, draw primitives, event loop, and 
window destruction).


unfortunately this collides with QEMU use of the main thread to pump the I/O 
event loop. 

there are two possible solutions:

- move the qemu initialisations and I/O event loop on a new thread, and free 
the main thread exclusively for the SDL event loop (which can now block waiting 
for events)
- keep the qemu initialisations and the I/O event loop on the main thread and 
schedule a timer every few milliseconds to poll if new graphical events are in 
the queue.


the first solution is obviously the most efficient, since the graphical thread 
does not consume resources when the there is no graphical activity, and 
reaction speed to graphical updates is as good as possible. (unfortunately this 
solution currently does not work on windows)


the second solution, which uses a timer to schedule a poll routine every few 
milliseconds (for example every 10 ms), is much more inefficient, and adds a 
small delay to all graphical displays. the timer routine is scheduled to run on 
the main thread, so the condition to run the graphical event loop on the main 
thread is satisfied.


fyi, this inefficient second solution, with a timer, is currently used by the 
qemu graphical consoles.


to guarantee that all graphical primitives will be invoked only from the main 
thread, calls should not be performed from the actual location (usually the 
emulator tcg thread), but must be queued as user events and deferred to the 
main thread, possibly using user events in the graphical event loop; for 
example:

    case SDL_USEREVENT:

        // User events, enqueued with SDL_PushEvent().
        switch (event->user.code) {

        case GRAPHIC_EVENT_BOARD_INIT:
            board_graphic_context = (BoardGraphicContext *) event->user.data1;

            if (!cortexm_graphic_board_is_graphic_context_initialised(
                    board_graphic_context)) {
                cortexm_graphic_board_init_graphic_context(
                        board_graphic_context);
            }
            break;

        case GRAPHIC_EVENT_LED_INIT:
            state = (GPIOLEDState *) event->user.data1;

            if (!cortexm_graphic_led_is_graphic_context_initialised(
                    &(state->led_graphic_context))) {
                cortexm_graphic_led_init_graphic_context(
                        state->board_graphic_context,
                        &(state->led_graphic_context), state->colour.red,
                        state->colour.green, state->colour.blue);
            }
            break;

        case GRAPHIC_EVENT_LED_TURN:
            state = (GPIOLEDState *) event->user.data1;
            is_on = (bool) event->user.data2;

            cortexm_graphic_led_turn(state->board_graphic_context,
                    &(state->led_graphic_context), is_on);
            break;

        // ...

        default:
            fprintf(stderr, "Unimplemented user event %d\n", event->user.code);
        }
        break;



special care must be observed for window destruction at program termination.

the SDL manuals recommend adding an atexit(SDL_Quit), but this isn't as simple 
as it looks, because the SDL_Quit must be executed also on the main thread, 
while the registered atexit() functions are executed on the context of the 
thread that calls exit(). (trying to run SDL_Quit on another thread hangs on 
Windows).

in practical terms, this means that exit() can be called only from the main 
thread. calls from different threads should be replaced with code to push a 
user event on the graphical loop, and perform the quit on the event loop.


one problematic such case is the semihosting exit. the semihosting code is 
executed on the emulator thread, so it cannot directly call exit. enqueuing the 
event is simple, but this code is executed with the io mutex locked, so the I/O 
loop is not running, and the timer will not schedule graphical polls. my 
workaround was to call `qemu_mutex_unlock_iothread()` to unlock the mutex, and 
wait for the program to terminate. this might not be the right solution, but 
seems functional.

---

conclusions:

- the graphical event loop and all graphical primitives must be called from the 
main thread context
- in qemu this is not possible directly; an inefficient but functional solution 
uses a timer programmed to call a function every few milliseconds, to poll the 
event loop. (if the qemu I/O event loop is executed on the main thread, the 
timer function will be called from the main thread context)
- all graphical operations initiated by the all other thread (like the emulator 
thread), must be deferred (in SDL using user events) to the graphical event loop
- a function to run graphical destruction must be registered with atexit()
- only the main thread can call exit(), to ensure the destruction functions are 
not called from other threads
- exiting from other threads must be also deferred to the main thread, possibly 
with unlocking a mutex.


I hope that helps,

Liviu









reply via email to

[Prev in Thread] Current Thread [Next in Thread]