On Wed, 8 May 2024 13:49:51 +0200
Philip Bizimis <bizimisphi...@gmail.com> wrote:

> I am currently working on a small Wayland Snake game to
> learn about Wayland and game development. I am using SHM
> double buffers for rendering.
> 
> My main game loop has two parts that run at different speeds so
> that I can have a constant game speed independent of variable
> FPS (based on https://dewitters.com/dewitters-gameloop):
> ```
> game_loop() {
>     while(1) {
>         // wl_display_dispatch
>         while(at least X times a sec) {
>             update();
>         }
>         render();
>     }
> ```
> In my case, there could be several game updates before a frame
> is rendered. But there could also be hundreds of frames between
> game updates if the hardware is good enough.
> 
> Currently, the game works fine if I use wl_display_dispatch.
> However, I feel like for optimal performance a blocking call does not
> make sense. Also, I would think that I would only want to process
> events that I need for the updating in the update loop, and not only
> once in the main loop.

Hi Philip,

do I understand correctly that your game event loop architecture is not
tied to any ready-made library, and it is all yours to write any way
you like? That is sweet.

I should probably start with a disclaimer that there are many opinions
on how a game main loop should be architected, and my opinion is
probably one of the less popular among game developers. However, it
does suit both single-threaded and multi-threaded programs.

I would suggest a fully event driven main loop. You would advance your
game simulation by a timer. You would record input events as they come,
and then handle them when you advance the simulation. You would render
only when the display server is ready for a new frame.

The event loop would have these event sources:
- Wayland display fd, for dispatching incoming events.
  - input events
  - wl_surface.frame callbacks
  - wl_buffer.release events
- A timer, to advance the simulation at a steady rate.
- A trigger to render a new frame.

When you dispatch input events, you record what happened. Maybe you
just want to keep track of what was the last key pressed, so that the
next simulation update knows to change the snake direction. Or maybe
you want to record the timestamp of when that press happened, so your
simulation can handle keys more precisely than just at a simulation
cycle granularity.

In order to render a new frame, several conditions must be fulfilled:

- You have something new to show, that is, the simulation has advanced
  since the last time.

- The display server is ready to use a new frame; wl_surface.frame
  callback from the previous frame has returned, *if* there was a
  previous frame. (1)

- You have a free wl_buffer to render into. If you wait for
  wl_surface.frame, you practically always do if you have enough
  wl_buffers to choose from, but this is how to wait for one to be
  freed by the display server if necessary.

When all these conditions are fulfilled, you trigger rendering a new
frame. If you use an event loop library, they may offer facilities to
run code before the event loop goes to sleep again. That's the point
where I would render.

I would recommend finding an event loop library you like, rather than
writing it yourself. It will be much easier to make things more
fancy later. They also have readily solved problems like how to watch
for an fd and a timer at the same time. OTOH, it's not too hard to
write something that works on Linux with poll() or epoll and timeouts
or timerfd.

Integrating the Wayland fd with an event loop does take some care, so
that everything runs smoothly. The documentation of
wl_display_prepare_read_queue() attempts to explain how to do it:
https://wayland.freedesktop.org/docs/html/apb.html#Client-classwl__display_1a40039c1169b153269a3dc0796a54ddb0

The main parts are:

- When preparing to sleep/poll, first make sure all incoming events
  have been handled, and all outgoing requests are actually flushed out.

- Sleep in poll.

- When anything causes a wake-up, you have to either
  wl_display_read_events() for the Wayland fd being readable, or
  wl_display_cancel_read() for any other wake-up reason.

- Dispatch incoming events.

It may seem complicated, but it also makes the Wayland display handling
thread-safe. Your program might not create threads, but libraries
might, so it's good to be safe just in case.

(1) When you create the window, you naturally have no wl_surface.frame
callback to wait for. It's ok to render a new frame any time you like
after the xdg_toplevel setup sequence. The wait for the frame callback
is just to throttle the rendering so that no rendered frame gets
discarded without displaying. Discarded frames are wasted work.

I think that should get you a nice foundation. It should be relatively
straightforward to extend later with fancier features, like more precise
timing of rendered frames vs. real-time simulation, network support, or
threads. The main idea is to decouple the rendering loop and the
simulation loop as much as possible. When you don't need a GPU for the
simulation, I think this is a good design. The game won't freeze even
if the game window is moved off-screen for a while and frame callbacks
stop coming temporarily.


I hope that helps,
pq

Attachment: pgprhbiJvVBmv.pgp
Description: OpenPGP digital signature

Reply via email to