frame callback

Jonas Ådahl jadahl at redhat.com
Wed Nov 2 16:07:15 UTC 2022


On Tue, Nov 01, 2022 at 06:21:58PM +0300, Maxim Kartashev wrote:
> These are a few questions mainly directed to Wayland developers. I greatly
> appreciate any insight into how Wayland is supposed to work.
> 
> 1. The wl_surface.frame request gives you a hint as to when it is best to
> supply Wayland with new content for a surface. I wonder if it has any use
> in the GUI environment? I implemented redrawing such that this callback is
> used, but now re-implemented it without this callback as it seems to only
> complicate things without any benefit. On the other hand, Gnome apps seem
> to be using it (gedit is one example), so I wonder if I am missing
> something and the callback is actually there to drive the re-drawing cycle,
> for example?

The wl_surface.frame callback is there to allow GUI toolkits and
applications to draw in a way that they don't draw unnecessarily often.
It's especially useful if you ever do animations, as the frame callback
will roughly be synchronized to frame clock of the monitor the surface
is on.

This means that if you tie your paint loop to the frame callback, and
the surface is on a 144 Hz monitor, you'll draw your animation at 144
Hz, but if the surface is moved to a 60 Hz monitor, you'll automatically
start animating at 60 Hz.

Right now, wl_surface.frame is also how EGL implements
eglSwapInterval(1).

If you don't have any animations, and e.g. only redraw in response to
e.g. mouse movements or updates coming from the application, it's still
useful to postpone any drawing until a frame callback, as it means you
don't draw a bunch of frames that will never ever reach the users eyes.

Lets say for example you move around an icon under the mouse cursor; if
the mouse device sends you 120 motion events every second, but your
monitor only redraws 60 times a second, if you redraw for every pointer
motion event, half your frames will be wasted.

So in general, it's recommended to throttle your redrawing using
wl_surface.frame. There are also other similar Wayland protocol
extensions, such as the presentation time protocol[1], that can be
useful when dealing with frame timings.

> 
> 2. xdg_surface.ack_configure. What does this ack_configure actually mean
> for the server in these scenarios of a window resize operation: suppose
> there was toplevel.configure for size 100x100 and then
> 2.1 we ack_configure'd it, but then committed the surface with a new
> 200x200 buffer.
> 2.2 or we did not send ack_configure for this serial at all and committed
> the surface with the same buffer as previously (no change in size).
> Should we expect another toplevel.configure for the new size in each case
> following the "unexpected" surface commit? Does the protocol necessitate
> another toplevel.configure in either case?

There are in general three ways to resize a surface in Wayland. In two
of these, 'configure' and 'ack_configure' play a role, while in the
third, it does not. Let me go through them one by one, but first clarify
how 'xdg_surface.configure' and 'xdg_toplevel.configure' relate to each
other.

The xdg_toplevel interface is an "extension" to the xdg_surface
interface. The xdg-shell protocol extension defines a "configure
sequence" to be a sequence of events that the client should act on as a
whole, meaning each event builds up a pending configuration. A
configure sequence always concludes with 'xdg_surface.configure'.


1. Interactive resize

Interactive resize is when a user e.g. drags a corner or a side of the
window, and the window resizes in response to this. This is usually
triggered by the application or toolkit calling 'xdg_toplevel.resize'
with a serial coming from an input event such as pointer button down.

In response to this, the compositor will start to send
'xdg_toplevel.configure' with the size and 'xdg_surface.configure' with
a serial. What the application should respond with is a committed
surface state with 'xdg_surface.ack_configure' with the serial from the
configure event, containing a surface sized so that it doesn't exceed
the dimensions sent in the xdg_toplevel.configure event.

By looking at the serial event of the ack_configure request, the
compositor can match the newly committed state with the configure event
it sent, and for example know whether it should treat the resize as part
of the interactive resize, or if it was something else.

If you get a configure event with 100x100, ack it and create a 200x200
sized surface, the end result is that your client will behave badly
during the interactive resize.

If you at first do not ack the configure event, and commit a 200x200
surface, the compositor will treat it as a sporadic resize unrelated to
the interactive resize. This is a race condition that all compositors
must handle, no matter if the client did it on purpose or not, but in
the end, a client is required to ack_configure a configure event, and if
the client intends to behave correctly, it must follow the
specification, and in the interactive resize case, that means to clamp
the size to the configured dimension.

Regarding expecting another configure event after not ack:ing but still
committing - no, you cannot assume you'll get another configure event.
>From the compositors point of view, the resize that was committed before
an eventual ack_configure will be treated as a sporadic resize,
unrelated to the interactive resize.

2. Resize in response to state changes

State changes include maximizing, unmaximizing, fullscreening,
unfullscreening, switching between being maximized on one monitor being
moved to another.

When this happens, the compositor will send a configure sequence
(xdg_toplevel.configure + xdg_surface.configure) with relevant state,
and the client should respond by ack:ing the configure event while
providing a new surface state according to the new configuration.


3. Sporadic resizes

An application can resize itself sporadically. An example for this is
clicking an expandable that causes the window size to grow. In this
case, there is no configure event nor any ack_configure request
involved, but only a matter of the application committing a state
containing a larger buffer, and a larger window geometry.

Since sporadic resizes in theory can happen any time, compositors must
handle race conditions where they happen e.g. immediately before an
interactive resize.

> 
> 3. Initial xdg_toplevel.configure.
> Does the protocol require the server to send the initial toplevel.configure
> with the size 0x0 after the creation of that xdg_toplevel? It doesn't seem
> to say so in the protocol's xml files, but *not* waiting for that configure
> event and attaching a buffer to a surface prior to sending any
> xdg_surface.ack_configure seem to contradict the protocol ("the client must
> make an ack_configure request sometime before the commit").

The compositor must send an initial configure after the client commits
an initial "empty" state, with empty meaning that it haven't yet
attached a buffer. You can read about it here[2]. After a client
received the initial configure, it should ack_configure it, attach a
buffer according to configured state and size, and lastly commit the
surface.

The reason why a client must always wait for the initial configure event
before committing its initial state is so that the compositor can
tell the client how it should draw the first frame - for example if it
should be maximized, tiled, fullscreen, focused, etc.

Thus, it will not necessarily send the size 0x0; if the first frame the
client should draw is maximized, the configured size will be one that
makes the surface fit the whole work area.

The mentioned contradiction is not a contradiction. Most wl_surface and
related role state is double buffered, meaning it is not applied until
wl_surface.commit. What "make an ack_configure request sometime before
the commit" means in this case is that the ack_configure request is sent
before wl_surface.commit, so that it will be part of the atomically
applied state.

Let me draw up how it will appear to the compositor and client:

  /-  wl_compositor.create_surface(new wl_surface at 1)
  |   xdg_wm_base.get_xdg_surface(new xdg_surface at 2, wl_surface at 1)
  |   xdg_surface.get_toplevel(new xdg_toplevel at 3, xdg_surface at 2)
  |   xdg_toplevel at 3.set_maximized() // configure toplevel
  \-> wl_surface at 1.commit() // apply initial empty state atomically

This whole sequence to the compositor is a single atomic operation where
the client creates a new toplevel where the client whishes to be mapped
as maximized. In response to this the compositor may respond with

  /-  xdg_toplevel at 3.configure(1920, 1030, maximized|activated)
  \-> xdg_surface at 2.configure(123)

The client should treat these two events atomically, concluded by the
xdg_surface.configure event, as an intent, where the client is told to
paint itself as maximized and focused (activated). In response to this,
the client responds to the configuration with

  /-  xdg_surface at 2.ack_configure(123)
  |   wl_surface at 1.attach(buffer_with_size_1920_1030)
  |   wl_surface at 1.set_input_region(..)
  |   wl_surface at 1.damage(1920, 1030)
  |   xdg_surface at 2.set_window_geometry(0, 0, 1920, 1030)
  \-> wl_surface at 1.commit()

The compositor will interpret this as an atomically committed state that
contains the following changes to be applied together:

 * This was a commit that was done in response to a configure event with
   serial 123
 * A new buffer sized 1920x1030 was attached
 * The whole surface content changed (damage was 1920x1030)
 * The window geometry was set to 1920x1030 at 0,0

Some things to keep in mind:

 * When attaching a buffer which changes the size of the window, it is
   important to call xdg_surface.set_window_geometry() as part of the
   same commit sequence, as otherwise the compositor will clamp the
   effective window geometry.
 * Some xdg_toplevel states mandate that the client always respects the
   configured size. These include 'maximized' and 'tiled'. For
   fullscreen, what instead is mandated is that the committed size must
   match the size, or be smaller.
 * It is not allowed to commit a non-empty (attach buffer) state before
   the initial configure event from the compositor.
 * Always think of configure sequences and state committed to a surface
   as atomically applied transactions, and not a sequence of individual
   commands.


I hope this helps. Let me know if I should expand on any other area, or
explain more why things are the way they are.


Jonas



[1] https://gitlab.freedesktop.org/wayland/wayland-protocols/-/blob/main/stable/presentation-time/presentation-time.xml
[2] https://gitlab.freedesktop.org/wayland/wayland-protocols/-/blob/8d79352851199eeb4fe1ad7644c06502c1cb518f/stable/xdg-shell/xdg-shell.xml#L431-434



More information about the wakefield-dev mailing list