'How do I use SDL_PeepEvents properly in multiple event loops?

I would like to do a simple thing — in the unit with the logic of handling the window, I want to update the window (the first event loop), and then in another unit with the logic of e.g. keyboard handling, update its state (second event loop). Window events must be put back in the SDL queue to be visible when the keyboard is updated (to be able to check if the window is in focus or not).

In short, there is one event queue (internal to SDL), but there are many loops that read events. If an event needs to be visible in several loops, it must return to the queue after handling it. To make this possible, each loop should iterate over the contents of the queue in such a way as not to process the events that was pushed back to the queue after handling.

For this, I am using SDL_PeepEvents which returns the number of queued events and iterates through the for loop this many times. The test program code looks like this:

uses
  SDL2;

  procedure UpdateWindow();
  var
    Event: TSDL_Event;
    Index: Integer;
  begin
    for Index := 0 to SDL_PeepEvents(nil, -1, SDL_PEEKEVENT, SDL_FIRSTEVENT, SDL_LASTEVENT) - 1 do
    begin
      SDL_PollEvent(@Event);

      if Event.Type_ = SDL_WINDOWEVENT then
        case Event.Window.Event of
          SDL_WINDOWEVENT_FOCUS_GAINED: WriteLn('focus gain - window');
          SDL_WINDOWEVENT_FOCUS_LOST:   WriteLn('focus lost - window');
        end;

      // put all events back in the queue to be seen in other event loops
      SDL_PushEvent(@Event);
    end;
  end;

  procedure UpdateKeyboard();
  var
    Event: TSDL_Event;
    Index: Integer;
  begin
    for Index := 0 to SDL_PeepEvents(nil, -1, SDL_PEEKEVENT, SDL_FIRSTEVENT, SDL_LASTEVENT) - 1 do
    begin
      SDL_PollEvent(@Event);

      // reading window events a second time
      if Event.Type_ = SDL_WINDOWEVENT then
        case Event.Window.Event of
          SDL_WINDOWEVENT_FOCUS_GAINED: WriteLn('focus gain - keyboard');
          SDL_WINDOWEVENT_FOCUS_LOST:   WriteLn('focus lost - keyboard');
        end;

      SDL_PushEvent(@Event);
    end;
  end;

var
  Window: PSDL_Window;
  Done: Boolean = False;
begin
  SDL_Init(SDL_INIT_EVERYTHING);
  Window := SDL_CreateWindow('Events test', SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 640, 480, SDL_WINDOW_SHOWN);

  while not Done do
  begin
    SDL_PumpEvents();

    UpdateWindow();
    UpdateKeyboard();

    // clear events queue
    SDL_FlushEvents(SDL_FIRSTEVENT, SDL_LASTEVENT);
    SDL_Delay(20);
  end;

  SDL_DestroyWindow(Window);
  SDL_Quit();
end.

The above code works, but only sometimes. When the program is started and the window appears, sometimes it says that the window and keyboard are in focus, and sometimes it's just that the window has focus only. This means that the SDL_WINDOWEVENT_FOCUS_GAINED event was handled in the first loop but not seen in the second loop (even though it was added back to the queue).

With the console window and the SDL window visible, I click alternately to both, activating and deactivating the SDL window. Each time the SDL window is activated, two lines should be added to the console (one for the event loop) and the same when deactivating the window. Unfortunately, the content of the console when clicking looks like this:

focus gain - window
focus gain - keyboard
focus lost - window
focus lost - keyboard
focus gain - window
focus lost - window
focus lost - keyboard
focus gain - window
focus lost - window
focus lost - keyboard
focus gain - window
focus lost - window
focus lost - keyboard
focus gain - window
focus gain - keyboard
focus lost - window
focus lost - keyboard
focus gain - window
focus lost - window
focus lost - keyboard
focus gain - window
focus lost - window
focus lost - keyboard
focus gain - window
focus lost - window
focus lost - keyboard

{...}

As you can see, sometimes when activating a window, the second loop doesn't see the SDL_WINDOWEVENT_FOCUS_GAINED event, so it doesn't add the focus gained - keyboard line to the console. This event is lost somewhere even though every event in the first loop is queued back using SDL_PushEvent.


Question: how to properly handle SDL events in multiple loops and how to put handled events back in the queue so that they are visible to multiple loops?

Lazarus 2.2.0, FPC 3.2.2, SDL2-for-Pascal headers and SDL 2.0.22.0 — all 64-bit.



Solution 1:[1]

If possible, don't do that at all. Best solution is to fetch events only once. If you want to process events multiple times, nothing prevents you from fetching all events you're interested in into single array and iterating this array as many times as you want. You can fetch all events into big array with single SDL_PeepEvents(SDL_GETEVENT) call.

The problem you have is based on two factors: SDL_PollEvent's return value is 0 when queue is drained; but nowhere in documentation it is stated that SDL_PushEvent puts event into the same processing frame - i.e. PollEvent may indicate queue is drained and return your newly pushed events on next frame. If that happens, in your second loop you process one event incorrectly (as PollEvent said 0 and no data is filled into Event structure), and skip one last event.

But, leaving higher level logic as it is, you have two options:

  1. Use SDL_PeepEvent to fetch event, not SDL_PollEvent. I.e. replace SDL_PollEvent(@Event) with SDL_PeepEvent(@Event, 1, SDL_GETEVENT, SDL_FIRSTEVENT, SDL_LASTEVENT).

  2. Check return value of SDL_PollEvent and if it returns 0 - you'll have to call it again, as it means no event is popped from queue. This solution is prone to race conditions and you can't guarantee it wouldn't call SDL_PumpEvents under the hood, and your last SDL_FlushEvents may kill some unprocessed events.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 keltar