Unveiling the Hidden Magic of GTK FrameClock

This article will delve deeper into the intricacies of the GTK FrameClock, its interaction with the compositor, and how it ensures smooth and synchronized animations. Specifically, we will explore the GTK FrameClockIdle implementation and understand how it manages timing cycles and aligns them with VSync signals in the Wayland platform to optimize performance and enhance the user experience.

Over the last few days, I have been immersed in understanding the inner workings of the GTK FrameClock. This exploration holds significant importance to better understanding of the integration of animated applications. My focus in this post lies in comprehending two key aspects:

  • How the clock utilizes the system time to implement its ticks.
  • The synchronization mechanism the clock employs with the display refresh rate (VSync).

Notice: The article uses this code for the examples.

First steps. The overall view …

The gdk.FrameClock can be likened to a timing coordinator for a window within an application. It plays a vital role in informing the application when to update and repaint the window. By optionally syncing with the monitor’s refresh rate, it ensures smooth animations. Even without synchronization, the gdk.FrameClock aids in synchronizing painting operations, reducing unnecessary frames and optimizing performance. Additionally, the frame clock can pause painting when frames will not be visible, or adjust animation rates as needed.

When an application requests a frame, the frame clock processes it and emits signals for different phases. These signals help update animations. The phases of a FrameClock can be the following:

  1. Before Paint: This phase occurs before the painting process of a frame. It is a preparatory phase where the application can perform any necessary setup or calculations before the actual rendering.
  2. Update: The FrameClock updates the state of animations and other time-based elements. It signals the application to update the content of the frame.
  3. Layout: This phase involves the layout calculation, where the application organizes and positions the elements to be displayed in the frame.
  4. Draw: The application performs the actual rendering of the frame. It involves painting the content on the screen based on the updated layout.
  5. Paint: After the rendering is completed, this phase marks the end of the painting process. The frame is ready to be presented to the screen.
  6. After Paint: This phase follows the completion of painting and may involve additional clean-up or bookkeeping tasks related to the frame presentation.

These phases represent the sequence of events that a FrameClock typically goes through during the generation and presentation of a frame. The phase it can be adjusted manually with the gdk_frame_clock_request_phase() method.

GTK internally manages the concept of the frame drawn signal. This signal informs the gdk.FrameClock about the successful rendering and presentation of a frame on the screen by the compositor or windowing system. This signal is crucial as it allows the FrameClock to stay aligned with the monitor’s vertical refresh rate (VSync) whenever the signal is received. In the absence of the frame drawn signal, the frame clock cycles continue to occur at a constant cadence, providing regular updates to the application.

Understanding the cycle of the time …

The frame time given by Frame.clockGetFrameTime() is reported in microseconds and is similar to g_get_monotonic_time() but not the same. It doesn’t change while a frame is being painted and stays the same for similar calls outside of a frame. This makes sure that different animations timed using the frame time stay synchronized. Overall, gdk.FrameClock helps keep animations smooth and coordinated. The next output of the gtk-frame-clock-example application illustrates a complete frame generation cycle:

(s): Cycle start
               clock:on_before_paint
               clock:on_update
               get timings:
               |  - now: 1803864692852
               |  - frame time: 1803864709176 (counter: 75) (frame time - now: 16324)
               |  - predicted presentation time: 1803864726132 (predicted - now: 33280)
               \
1803864692852:  widget:on_tick_callback (rate: 16454)
               clock:on_layout
               get timings:
               |  - now: 1803864694022
               |  - frame time: 1803864709176 (counter: 75) (frame time - now: 15154)
               |  - predicted presentation time: 1803864726132 (predicted - now: 32110)
               \
1803864694022:  widget:on_draw (tick-draw latency: 1170)
               clock:on_paint
               clock:on_after_paint
               get timings:
               |  - now: 1803864709326
               |  - frame time: 1803864709176 (counter: 75) (frame time - now: -150)
               |  - predicted presentation time: 1803864726132 (predicted - now: 16806)
               \
1803864709326:  wl_surface:on_commit
(e): End of cycle

The sequence of events and timings associated with this example frame generation cycle are described below:

  1. (s) Cycle Start: The cycle begins, representing the start of a new frame generation cycle.
  2. clock:on_before_paint: The FrameClock emits the “on_before_paint” signal, indicating the preparation phase before painting the current frame.
  3. clock:on_update: The FrameClock emits the “on_update” signal, triggering an update for the current frame. The
  4. get timings: Show various timings related to the frame generation cycle. These timings include:
  • now: The current monotonic system time in microseconds.
  • frame time: The time allocated for rendering and painting this frame, along with a counter indicating the frame number.
  • predicted presentation time: The expected time when this frame will be presented on the screen.
  1. widget:on_tick_callback (rate: 16454): The application’s widget is receiving a tick callback, at an average interval of 16454 microsecons. This callback notifies the application about the right time to initiate the generation of the next frame. Usually the application will to decide if it has to update the animations and it will put in the queue (gtk_widget_queue_draw())
  2. clock:on_layout: The FrameClock emits the “on_layout” signal, indicating the layout phase, where the application prepares the layout before painting the frame.
  3. widget:on_draw (tick-draw latency: 1170): The widget receives a draw callback, which indicates the right moment to paint the frame. The “tick-draw latency” measures the time delay between the tick callback and the actual drawing of the frame.
  4. clock:on_paint: The FrameClock emits the “on_paint” signal, marking the actual painting phase of the frame.
  5. clock:on_after_paint: The FrameClock emits the “on_after_paint” signal, indicating that the frame painting is completed.
  6. wl_surface:on_commit: This event indicates that the Wayland surface has been committed, meaning that the frame has been drawn and is ready for presentation.
  7. (e) End of Cycle: The cycle ends, representing the completion of the frame generation cycle.

How does the FrameClock calculate the frame times …

When an animation begins, its first cycle might start at a random time due to external triggers like input events or timers. This phase shift, called the phase of the clock cycle start time, impacts the smoothness of animations.

During the first cycle, the smooth frame time is set at the cycle’s start time. Subsequent cycles may not align with vsync signals. However, once a frame drawn signal is received from the compositor, the clock cycles will synchronize with vsync signals, maintaining a regular cadence. This may cause the first vsync-related cycle to occur close to the previous non-vsync-related one, altering the phase of cycle start times.

To ensure consistent reported frame times, adjustments are made to the frame time. The phase of the first clock cycle start time is computed, considering skipped frames due to compositor stalls. The goal is to have the first vsync-related smooth time separated by exactly 1 frame interval from the previous one. This adjustment maintains regularity even if “frame drawn” signals are missed in subsequent frames.

In the next diagram from gdk/gdkframeclockidle.c#L468, the relationship between vsync signals, clock cycle starts, adjusted frame times, and “frame drawn” events is illustrated. The changing cadence of the clock cycles after the first vsync-related cycle is highlighted, while the regularity of the cycle cadence is maintained even if “frame drawn” events are absent in certain frames.

In the following diagram, '|' mark a vsync, '*' mark the start of a clock cycle, '+' is the adjusted
frame time, '!' marks the reception of *frame drawn* events from the compositor. Note that the clock
cycle cadence changed after the first vsync-related cycle. This cadence is kept even if we don't
receive a 'frame drawn' signal in a subsequent frame, since then we schedule the clock at intervals of
refresh_interval.

vsync             |           |           |           |           |           |... 
frame drawn       |           |           |!          |!          |           |...
cycle start       |       *   |       *   |*          |*          |*          |...
adjusted times    |       *   |       *   |       +   |       +   |       +   |...
phase                                      ^------^

You can get more information from the comment om gdk/gdkframeclockidle.c for more in-deph information about how the FrameClock handles the adjustment of reported frame times. Here is where the concept of frame drawn is introduced and explained in detail. As it was mentioned before, the frame drawn signal refers to whatever method to allows the FrameClock to know when a frame has been successfully drawn and presented on the screen by the compositor or windowing system.

Initially, the frame clock cycles occur at a regular interval, approximately matching the desired frame rate, but these cycles are not directly tied to the monitor’s vertical refresh rate (VSync) but it will be eventually smoothly aligned as far a frame drawn signal is received.

In the absence of the frame drawn signal, the frame clock cycles will continue to occur at a constant cadence. However, when the frame drawn signal is received from the compositor, it marks the successful completion of frame rendering and indicates that the frame clock cycles should align with the monitor’s VSync signals.

The frame draw signal for GTK in the Wayland platform it is the “frame.done” signal. This represents the Vsync for GTK in a Wayland environment. The FrameClock becomes freeze/unfreeze as long as the (gdk_frame_clock_idle_is_frozen function in gdkframeclockidle.c#L279) ticks are being accumulated and there is not a “frame.done” callback invokation from the compositor. This is how it works for the particular case of Wayland but similar approach are used for X11 and other supported platforms on GTK.

How can I get a FrameClock for my widget?

Unfortunatelly, there are not public methods in the GTK API for the manual creation of FrameClock instances. The common way to get a frame clock for a GTK widget is by adding it to a GTK window and then request for the clock with
gtk_widget_get_frame_clock() once the widget were realized:

static void on_realize(GtkWidget* widget, gpointer user_data) {
    frame_clock = gtk_widget_get_frame_clock(widget);
}

GtkWidget *drawing_area = gtk_drawing_area_new();
gtk_container_add(GTK_CONTAINER(window), drawing_area);

g_signal_connect(drawing_area, "realize", G_CALLBACK(on_realize), NULL);

The obtained FrameClock will be the one created during the instantiation of a new GTK window:

#0  gdk_frame_clock_idle_init (frame_clock_idle=0x5555555e8140) at ../../../../gdk/gdkframeclockidle.c:137
#1  0x00007ffff7e67fba in g_type_create_instance (type=<optimized out>) at ../../../gobject/gtype.c:1929
#2  0x00007ffff7e4f0ed in g_object_new_internal (class=class@entry=0x5555555efa80, params=params@entry=0x0, n_params=n_params@entry=0) at ../../../gobject/gobject.c:2023
#3  0x00007ffff7e5034d in g_object_new_with_propertiesPython Exception <class 'TypeError'>: can only concatenate str (not "NoneType") to str
 (object_type=, n_properties=0, names=names@entry=0x0, values=values@entry=0x0) at ../../../gobject/gobject.c:2193
#4  0x00007ffff7e50e51 in g_object_new (object_type=<optimized out>, first_property_name=first_property_name@entry=0x0) at ../../../gobject/gobject.c:1833
#5  0x00007ffff7ed8ba9 in gdk_window_new (parent=0x555555581110, attributes=0x7fffffffd770, attributes_mask=44) at ../../../../gdk/gdkwindow.c:1488
#6  0x00007ffff7f22c42 in create_foreign_dnd_window (display=0x55555557c0e0) at wayland/../../../../../gdk/wayland/gdkdevice-wayland.c:4803
#7  _gdk_wayland_device_manager_add_seat (wl_seat=<optimized out>, id=<optimized out>, device_manager=0x555555572e60) at wayland/../../../../../gdk/wayland/gdkdevice-wayland.c:5177
#8  _gdk_wayland_display_add_seat (version=<optimized out>, id=<optimized out>, display_wayland=0x55555557c0e0) at wayland/../../../../../gdk/wayland/gdkdisplay-wayland.c:238
#9  seat_added_closure_run (display_wayland=0x55555557c0e0, closure=<optimized out>) at wayland/../../../../../gdk/wayland/gdkdisplay-wayland.c:249
#10 0x00007ffff7f241d1 in process_on_globals_closures (display_wayland=0x55555557c0e0) at wayland/../../../../../gdk/wayland/gdkdisplay-wayland.c:209
#11 _gdk_wayland_display_open (display_name=<optimized out>) at wayland/../../../../../gdk/wayland/gdkdisplay-wayland.c:621
#12 0x00007ffff7ec268f in gdk_display_manager_open_display (manager=<optimized out>, name=0x0) at ../../../../gdk/gdkdisplaymanager.c:462
#13 0x00007ffff784ed4b in gtk_init_check (argc=<optimized out>, argv=<optimized out>) at ../../../../gtk/gtkmain.c:1110
#14 gtk_init_check (argc=<optimized out>, argv=<optimized out>) at ../../../../gtk/gtkmain.c:1102
#15 0x00007ffff784ed7d in gtk_init (argc=<optimized out>, argv=<optimized out>) at ../../../../gtk/gtkmain.c:1167
#16 0x0000555555557144 in main (argc=1, argv=0x7fffffffda88) at /home/user/local/git/examples/example_gdk_frame_clock/src/main.c:176

When is the right time to paint my widget?

Overall, the following code demonstrates how to set up a basic drawing area in a GTK application, connect pre-frame and drawing callbacks, and handle custom graphics rendering using the Cairo library. The on_tick_callback ensures that the widget is scheduled for redraw, and the on_draw function is responsible for actually rendering the graphics within the widget:

/*
 * This signal is emitted when a widget to be redrawn in the PAINT PHASE of the current or the next frame.
 */
static int on_tick_callback(GtkWidget *widget, GdkFrameClock *frame_clock, gpointer user_data) {
    // Schedules this widget to be redrawn in the paint phase of the current or the next frame.
    gtk_widget_queue_draw(GTK_WIDGET(user_data));
    // ...
    return G_SOURCE_CONTINUE;
}

/*
 * This signal is emitted when a widget is supposed to render itself in the PAINT PHASE.
 */
static gboolean on_draw(GtkWidget *widget, cairo_t *cr, gpointer user_data) {
    // Your drawing operations here. E.g: cairo_paint(cr);
    // ...
    return FALSE;
}

// ...

GtkWidget *drawing_area = gtk_drawing_area_new();
gtk_container_add(GTK_CONTAINER(window), drawing_area);

gtk_widget_add_tick_callback(GTK_WIDGET(drawing_area), on_tick_callback, drawing_area, NULL);
g_signal_connect(drawing_area, "draw", G_CALLBACK(on_draw), NULL);

// ...

The FrameClock will notify the widget when it is the rigth time to schedule the generation of a new frame. This happens in the update phase:

#0  gtk_widget_on_frame_clock_update (frame_clock=0x5555555e74c0, widget=0x5555555a8530) at ../../../../gtk/gtkwidget.c:5273
#4  0x00007ffff7e5c863 in <emit signal ??? on instance ???> (instance=instance@entry=0x5555555e74c0, signal_id=<optimized out>, detail=detail@entry=0) at ../../../gobject/gsignal.c:3587
    #1  0x00007ffff7e3ed2f in g_closure_invoke (closure=0x5555559fd320, return_value=0x0, n_param_values=1, param_values=0x7fffffffd540, invocation_hint=0x7fffffffd4c0) at ../../../gobject/gclosure.c:830
    #2  0x00007ffff7e5ac36 in signal_emit_unlocked_R
    (node=node@entry=0x5555555aa000, detail=detail@entry=0, instance=instance@entry=0x5555555e74c0, emission_return=emission_return@entry=0x0, instance_and_params=instance_and_params@entry=0x7fffffffd540)
    at ../../../gobject/gsignal.c:3777
    #3  0x00007ffff7e5c614 in g_signal_emit_valist (instance=<optimized out>, signal_id=<optimized out>, detail=<optimized out>, var_args=var_args@entry=0x7fffffffd6f0) at ../../../gobject/gsignal.c:3530
#5  0x00007ffff7ed0b57 in _gdk_frame_clock_emit_update (frame_clock=0x5555555e74c0) at ../../../../gdk/gdkframeclock.c:645
#6  gdk_frame_clock_paint_idle (data=0x5555555e74c0) at ../../../../gdk/gdkframeclockidle.c:547
#7  0x00007ffff7ebd2ad in gdk_threads_dispatch (data=0x55555578b140, data@entry=<error reading variable: value has been optimized out>) at ../../../../gdk/gdk.c:769
#8  0x00007ffff6b032c8 in g_timeout_dispatch (source=0x555555677120, callback=<optimized out>, user_data=<optimized out>) at ../../../glib/gmain.c:4973
#9  0x00007ffff6b02c44 in g_main_dispatch (context=0x55555558e800) at ../../../glib/gmain.c:3419
#10 g_main_context_dispatch (context=0x55555558e800) at ../../../glib/gmain.c:4137
#11 0x00007ffff6b58258 in g_main_context_iterate.constprop.0 (context=0x55555558e800, block=block@entry=1, dispatch=dispatch@entry=1, self=<optimized out>) at ../../../glib/gmain.c:4213
#12 0x00007ffff6b022b3 in g_main_loop_run (loop=0x5555558d6d30) at ../../../glib/gmain.c:4413
#13 0x00007ffff7848d2d in gtk_main () at ../../../../gtk/gtkmain.c:1329
#14 0x0000555555557276 in main (argc=1, argv=0x7fffffffda88) at /home/user/local/git/examples/example_gdk_frame_clock/src/main.c:194

The gtk_widget_add_tick_callback() actually is attaching the callback to the update signal from the FrameClock. Therefore, it basically queues the animation updates and attaches a callback executed before each frame (check the implementation of gtk_widget_add_tick_callback() in gtk/gtkwidget.c):

guint
gtk_widget_add_tick_callback (GtkWidget       *widget,
                              GtkTickCallback  callback,
                              gpointer         user_data,
                              GDestroyNotify   notify)
{
  // ...
      frame_clock = gtk_widget_get_frame_clock (widget);

      if (frame_clock)
        {
          priv->clock_tick_id = g_signal_connect (frame_clock, "update",
                                                  G_CALLBACK (gtk_widget_on_frame_clock_update),
                                                  widget);
          gdk_frame_clock_begin_updating (frame_clock);
        }
  // ...
  info = g_new0 (GtkTickCallbackInfo, 1);
  // info->...
  info->callback = callback;
  // info->...
  priv->tick_callbacks = g_list_prepend (priv->tick_callbacks,
                                         info);
  // ...

The callback runs frequently, matching the output device’s frame rate or the app’s repaint speed, whichever is slower. Inside the on_tick_callback() function, the gtk_widget_queue_draw() is called to schedule the specified widget for redrawing during the current or next frame’s paint phase. This means the widget will be marked for update, and its draw signal will be emitted.

Lastly, the on_draw() callback will be responsible for rendering and drawing on a widget during the paint phase. This callback takes three parameters: a GtkWidget pointer (widget), a cairo_t context pointer (cr) for drawing operations, and user data pointer (user_data). This callback is the right place for performing draw operations using the Cairo drawing library, for example.

Keeping the ticks aligned with the VSync signals

As I already mentioned, the GTK framework internally handles the concept of the frame drawn signal. This signal lets the gdk.FrameClock know when a frame has been successfully rendered and presented on the screen. This is vital for keeping the FrameClock in sync with the monitor’s refresh rate (VSync) upon receiving the signal. With the frame drawn signal, the frame clock maintains a consistent cycle, delivering regular updates to the application.

In the context of a GTK application running in a Wayland environment this sync is implemented by adding an listener to the .done event for the wl_surface_commit() action. This is the frame_callback callback added from the on_frame_clock_after_paint() in the gdk/wayland/gdkwindow-wayland.c:

static void
on_frame_clock_after_paint (GdkFrameClock *clock,
                            GdkWindow     *window)
{
  // ...
  if (impl->surface_callback == NULL)
    {
      callback = wl_surface_frame (impl->display_server.wl_surface);
      wl_callback_add_listener (callback, &frame_listener, window);  // <-- Here
      impl->surface_callback = callback;
    }
  // ...
static void
frame_callback (void               *data,
                struct wl_callback *callback,
                uint32_t            time)
{
  // ...
  _gdk_frame_clock_thaw (clock);  
  // ...
}

The frame_callback will be called when the server has finished processing the surface commit and has made the changes visible on the screen. This function will inmediately thaw the Frameclock.

The term thaw refers to the process of unfreezing the FrameClock after it has been frozen. When a FrameClock is frozen, it means that the clock is temporarily paused or halted, preventing the generation of new frame ticks and updates.

When the FrameClock thaws, it resumes its normal operation of generating frame ticks and updates. This is typically done when the application determines that it needs to resume animations or updates that were previously paused.

GTK uses this freezing mechanism to optimize performance and reduce unnecessary updates during periods when animations or updates are not needed, or to align the generation of next frame with the presentation time of the current frame when limited by the monitor’s vertical refresh rate (VSync).

The following is a GDB backtrace from the example code with a breakpoint added in the frame_callback() function:

(gdb) b frame_callback
Breakpoint 3 at 0x7ffff7f2e100: file wayland/../../../../../gdk/wayland/gdkwindow-wayland.c, line 570.
(gdb) c
Continuing.
(s): Cycle start
               clock:on_before_paint
               clock:on_update
               get timings:
               |  - now: 1903492480109
               |  - frame time: 1903492494736 (counter: 11271) (frame time - now: 14627)
               |  - predicted presentation time: 1903492505049 (predicted - now: 24940)
               \
1903492480109:  widget:on_tick_callback (rate: 337853788)
               get timings:
               |  - now: 1903492480316
               |  - frame time: 1903492494736 (counter: 11271) (frame time - now: 14420)
               |  - predicted presentation time: 1903492505049 (predicted - now: 24733)
               \
1903492480316:  widget:on_draw (tick-draw latency: 207)
               clock:on_paint
               clock:on_after_paint
               get timings:
               |  - now: 1903492499890
               |  - frame time: 1903492494736 (counter: 11271) (frame time - now: -5154)
               |  - predicted presentation time: 1903492505049 (predicted - now: 5159)
               \
1903492499890:  wl_surface:on_commit
(e): End of cycle
Thread 1 "gtk-frame-clock" hit Breakpoint 3, frame_callback (data=0x555555581ad0, callback=0x555555bd2420, time=1903492499) at wayland/../../../../../gdk/wayland/gdkwindow-wayland.c:570
570    {
(gdb) bt
#0  frame_callback (data=0x555555581ad0, callback=0x555555bd2420, time=1903492499) at wayland/../../../../../gdk/wayland/gdkwindow-wayland.c:570
#1  0x00007ffff66e9e2e in ffi_call_unix64 () at ../src/x86/unix64.S:105
#2  0x00007ffff66e6493 in ffi_call_int (cif=<optimized out>, fn=<optimized out>, rvalue=<optimized out>, avalue=<optimized out>, closure=<optimized out>) at ../src/x86/ffi64.c:672
#3  0x00007ffff74cdad0 in wl_closure_invoke (closure=closure@entry=0x5555555e1ac0, target=<optimized out>, target@entry=0x555555bd2420, opcode=opcode@entry=0, data=<optimized out>, flags=<optimized out>) at ../src/connection.c:1025
#4  0x00007ffff74ce243 in dispatch_event (display=display@entry=0x555555575220, queue=0x5555555752f0, queue=<optimized out>) at ../src/wayland-client.c:1583
#5  0x00007ffff74ce43c in dispatch_queue (queue=0x5555555752f0, display=0x555555575220) at ../src/wayland-client.c:1729
#6  wl_display_dispatch_queue_pending (display=0x555555575220, queue=0x5555555752f0) at ../src/wayland-client.c:1971
#7  0x00007ffff74ce490 in wl_display_dispatch_pending (display=<optimized out>) at ../src/wayland-client.c:2034
#8  0x00007ffff7f25548 in _gdk_wayland_display_queue_events (display=<optimized out>) at wayland/../../../../../gdk/wayland/gdkeventsource.c:201
#9  0x00007ffff7ec0a99 in gdk_display_get_event (display=0x55555557c0e0) at ../../../../gdk/gdkdisplay.c:442
#10 0x00007ffff7f2a996 in gdk_event_source_dispatch (base=<optimized out>, callback=<optimized out>, data=<optimized out>) at wayland/../../../../../gdk/wayland/gdkeventsource.c:120
#11 0x00007ffff6b02d3b in g_main_dispatch (context=0x55555558e800) at ../../../glib/gmain.c:3419
#12 g_main_context_dispatch (context=0x55555558e800) at ../../../glib/gmain.c:4137
#13 0x00007ffff6b58258 in g_main_context_iterate.constprop.0 (context=0x55555558e800, block=block@entry=1, dispatch=dispatch@entry=1, self=<optimized out>) at ../../../glib/gmain.c:4213
#14 0x00007ffff6b022b3 in g_main_loop_run (loop=0x555555825be0) at ../../../glib/gmain.c:4413
#15 0x00007ffff7848d2d in gtk_main () at ../../../../gtk/gtkmain.c:1329
#16 0x00005555555570a2 in main (argc=1, argv=0x7fffffffda88) at /home/user/local/git/examples/example_gdk_frame_clock/src/main.c:198
(gdb) c
Continuing.
(s): Cycle start
               clock:on_before_paint
               clock:on_update
               get timings:
               |  - now: 1903498816343
               |  - frame time: 1903498827058 (counter: 11272) (frame time - now: 10715)
               |  - predicted presentation time: 1903498841278 (predicted - now: 24935)
               \
1903498816343:  widget:on_tick_callback (rate: 6336234)
               get timings:
               |  - now: 1903498816550
               |  - frame time: 1903498827058 (counter: 11272) (frame time - now: 10508)
               |  - predicted presentation time: 1903498841278 (predicted - now: 24728)
               \
1903498816550:  widget:on_draw (tick-draw latency: 207)
               clock:on_paint
               clock:on_after_paint
               get timings:
               |  - now: 1903498830894
               |  - frame time: 1903498827058 (counter: 11272) (frame time - now: -3836)
               |  - predicted presentation time: 1903498841278 (predicted - now: 10384)
               \
1903498830894:  wl_surface:on_commit
(e): End of cycle

Here’s what’s happening:

  1. A breakpoint at line 570 of the gdkwindow-wayland.c file was set inside the frame_callback function.
  2. The program continues (c) and enters a FrameClock cycle (from “Cycle start”).
  3. The FrameClock goes through different phases like on_before_paint, on_update, and so on, collecting timings including current time, frame time, and predicted presentation time.
  4. The program hits the breakpoint at line 570 (frame_callback) as part of the cycle. The backtrace (bt) shows the function call stack, indicating that the frame_callback was triggered due to a Wayland event.
  5. The program again continues (c) and progresses through the FrameClock phases.

This output represents the cycle of the FrameClock in the example code program, with debug information showing the specific phases, timings, and the function calls being executed.

So… in conclusion

The FrameClock in GTK acts as a coordinator for frame updates and animations. It triggers a series of phases for each frame, including Before Paint, Update, Layout, Draw, Paint, and After Paint. These phases ensure that animations are smoothly coordinated and displayed, optimizing performance.

The concept of the frame drawn signal is crucial for achieving synchronization with the display’s VSync. This signal is emitted when a frame has been successfully presented on the screen by the compositor or windowing system. It allows the FrameClock to adjust its cycle and stay in harmony with the monitor’s refresh rate. This synchronization is achieved in the Wayland platform by utilizing the wl_surface commit done callback, which corresponds to the VSync signal.

There is no way to create your own FrameClock instance for your app. GTK applications that want to work with FrameClock should first add widgets to a GTK window and then obtain the FrameClock instance using gtk_widget_get_frame_clock().

And basically this is all what I have. My motivation for digging into this was to get a better understanding of the GTK FrameClock and how it works internally to produce smooth, synchronized animations in graphical applications. I hope this analysis is useful for others who are also curious about how this works.

Leave a comment