BlinkGTK Live Render Path Switching and Handoff Meta (BlinkShift)

Version: 1.2.1-build3
Last updated: 2026-09-01
Language: | English

日本語


About this page

This page documents the API that switches the render path without restarting
the application
, together with Handoff Meta, which carries state across a
switch.

The switch itself (blink_web_view_switch_render_path() and the
render-path-changed signal) is described in
signals-api-en.md. This page focuses on the handoff
metadata and how the two are used together.


1. What this API is for

Render paths differ in what they are good at. CPU compositing is broadly
compatible; handing dmabuf directly to the GPU is better for bandwidth and CPU
time. Which one is preferable depends on the reader's machine and the moment.

Changing paths used to mean starting the process again. To a reader that is a
pause of several seconds. BlinkShift makes it a switch inside the running
process
.

Handoff Meta expresses what is carried across a switch, in a form that does not
depend on any particular path.


2. Units

Geometry is carried as logical pixels plus an explicit scale.

physical px = logical x device_scale_factor

Physical pixels are not passed on their own, so that the receiving side never
has to guess the scale. When the two sides disagree about scale, it shows up as
content drawn at half size.


3. Structure

#define BLINK_HANDOFF_META_SCHEMA_VERSION 1

typedef struct _BlinkHandoffMeta {
  int   schema_version;       /* == BLINK_HANDOFF_META_SCHEMA_VERSION */
  int   logical_width;        /* geometry: logical px */
  int   logical_height;
  int   device_scale_factor;  /* physical px = logical x this */
  char* url;                  /* content: current URL (owned) */
  int   scroll_x;             /* logical px */
  int   scroll_y;
  char* path_id;              /* output path: "software" / "egl" ... (owned) */
} BlinkHandoffMeta;

schema_version exists for forward compatibility. A reader of the structure
should check it against the version it knows before using the contents.


4. Functions

Function Purpose
blink_web_view_export_handoff_meta() Write the current state into a meta. The caller frees the result
blink_web_view_import_handoff_meta() Restore state from a meta
blink_handoff_meta_serialize() Turn a meta into text. The caller frees the result
blink_handoff_meta_deserialize() Build a meta from text. The caller frees the result
blink_handoff_meta_free() Free a meta
BlinkHandoffMeta* blink_web_view_export_handoff_meta(BlinkWebView* web_view);
void  blink_web_view_import_handoff_meta(BlinkWebView* web_view,
                                         const BlinkHandoffMeta* meta);
char* blink_handoff_meta_serialize(const BlinkHandoffMeta* meta);
BlinkHandoffMeta* blink_handoff_meta_deserialize(const char* text);
void  blink_handoff_meta_free(BlinkHandoffMeta* meta);

export returns NULL on failure. Free the result of serialize with
free(), and the results of deserialize and export with
blink_handoff_meta_free().

The serialized form is INI-compatible text and can be read the same way as the
session file.


5. What is carried, and what is not

Only path-independent semantic state is carried.

Carried Not carried
Geometry (logical px + scale) EGL context
URL Wayland surface
Scroll position (logical px) dmabuf file descriptors
Output path id Surface identifiers

Everything in the right column is recreated on each switch. Because no raw
resource is carried across, a failed switch can return to the previous path.

Not in the meta today

The following state is not something the engine can judge the correctness
of
, so it is not carried:

Attaching a type to a value the engine cannot verify produces a field that
looks safe but whose contents nothing guarantees. Save and restore these in
the application.

Page geometry such as column pitch is available from the vertical writing
metrics API. Keeping a copy in the meta would mean two places holding the same
value, where one can go stale unnoticed.


6. Using it

Take a meta before and after the switch and compare them. The switch is
asynchronous, so completion arrives as a signal.

#include <blink_gtk/blink_gtk.h>
#include <gtk/gtk.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static int   g_seq = 0;
static char* g_before = NULL;

static void on_render_path_changed(BlinkWebView* view,
                                   const char* to,
                                   const char* result,
                                   int gate_ms,
                                   int seq,
                                   gpointer user_data) {
  (void)user_data;
  if (seq != g_seq) {
    return;  /* a different switch; ignore when the key does not match */
  }
  if (strcmp(result, "ok") != 0) {
    printf("switch did not take (%s); the previous path is still in use\n", result);
    return;
  }
  BlinkHandoffMeta* after = blink_web_view_export_handoff_meta(view);
  if (after != NULL) {
    char* text = blink_handoff_meta_serialize(after);
    printf("path=%s  %d ms\n", to, gate_ms);
    printf("  before: %s", g_before != NULL ? g_before : "(unavailable)\n");
    printf("  after:  %s", text != NULL ? text : "(unavailable)\n");
    free(text);
    blink_handoff_meta_free(after);
  }
}

static void request_switch(BlinkWebView* view, const char* path_id) {
  BlinkHandoffMeta* before = blink_web_view_export_handoff_meta(view);
  if (before != NULL) {
    free(g_before);
    g_before = blink_handoff_meta_serialize(before);
    blink_handoff_meta_free(before);
  }
  g_seq = blink_web_view_switch_render_path(view, path_id);
  if (g_seq == 0) {
    printf("the switch request was not accepted\n");
  }
}

int main(int argc, char** argv) {
  blink_gtk_init(&argc, &argv);
  GtkWidget* window = gtk_window_new();
  GtkWidget* widget = blink_web_view_new();
  BlinkWebView* view = BLINK_WEB_VIEW(widget);

  g_signal_connect(view, "render-path-changed",
                   G_CALLBACK(on_render_path_changed), NULL);
  gtk_window_set_child(GTK_WINDOW(window), widget);
  gtk_window_set_default_size(GTK_WINDOW(window), 1024, 768);
  blink_web_view_load_uri(view, "https://example.com/");
  gtk_widget_set_visible(window, TRUE);

  request_switch(view, "P2");

  blink_gtk_run_main_loop();
  free(g_before);
  blink_gtk_shutdown();
  return 0;
}

Tying a notification to its switch

blink_web_view_switch_render_path() returns the sequence number of the
accepted switch (1 or greater; 0 means the request was not accepted). The
completion signal carries the same number in its seq argument.

When switches follow one another, matching before and after by how close the
timestamps are will pair records from different switches
. Worse, the pairing
looks like agreement, so the mistake goes unnoticed. Use the number.

The three results

result Meaning
ok The new path is drawing stably
rollback The new path failed its check and the previous path was restored. The display is intact
fail The previous path could not be restored. Treat as an error

rollback is a failure, but the reader's screen is not broken by it.


7. Path ids

id Meaning
P1 CPU compositing, direct delivery (default)
P2 CPU compositing, shared-memory delivery
P3 GPU dmabuf delivery. Only the output layer is exchanged, so a failure can return to CPU compositing

blink_web_view_get_render_path() returns the current path id. The value
changes only when a switch completes with ok.


See also