/*
 * wpe_subclasses.c — GObject subclasses for buffr-webkit's WPE platform path.
 *
 * Defines four final GObject subclasses (BuffrDisplay/View/Toplevel/Screen)
 * using upstream WebKit's WPE platform classes as parents. The C side is the
 * boilerplate registration that bindgen can't faithfully reproduce (the
 * *Class struct layouts are needed for setting vmethod function pointers).
 *
 * Also defines:
 *   BuffrDisplayWayland (#152): WPEDisplay subclass that reuses the host
 *     Wayland connection instead of opening its own.
 *   BuffrViewWayland (#153): WPEView subclass that composites WebKit frames
 *     directly into a wl_subsurface via DMA-BUF / zwp_linux_dmabuf_v1.
 *     Eliminates the CPU readback path on Wayland sessions.
 *
 * Per-frame work for OSR path: `buffr_view_render_buffer` forwards each
 * WPEBuffer* to the Rust callback `buffr_rust_render_buffer`.
 * Per-frame work for Wayland path: `buffr_view_wayland_render_buffer_vfunc`
 * wraps the DMA-BUF fds in a wl_buffer and attaches it to the wl_subsurface.
 *
 * Build: compiled by build.rs via the cc crate, linked into buffr-webkit
 * alongside the bindgen-generated FFI bindings.
 */

#include <wpe/webkit.h>
#include <wpe/wpe-platform.h>
#include <EGL/egl.h>
#include <EGL/eglext.h>
#include <wayland-client.h>
/* Generated by wayland-scanner from linux-dmabuf-v1.xml (placed in OUT_DIR). */
#include "linux-dmabuf-client.h"

/* Forward declaration of the Rust-side render callback. Implemented in
 * src/platform/wpe_subclass.rs with #[no_mangle] pub extern "C". */
extern void buffr_rust_render_buffer(WPEView *view, WPEBuffer *buffer);

/* ── BuffrScreen ────────────────────────────────────────────────────── */

#define BUFFR_TYPE_SCREEN (buffr_screen_get_type())
G_DECLARE_FINAL_TYPE(BuffrScreen, buffr_screen, BUFFR, SCREEN, WPEScreen)

struct _BuffrScreen {
    WPEScreen parent_instance;
};

G_DEFINE_FINAL_TYPE(BuffrScreen, buffr_screen, WPE_TYPE_SCREEN)

static void buffr_screen_init(BuffrScreen *self) { (void)self; }
static void buffr_screen_class_init(BuffrScreenClass *klass) { (void)klass; }

/* ── BuffrToplevel ──────────────────────────────────────────────────── */

#define BUFFR_TYPE_TOPLEVEL (buffr_toplevel_get_type())
G_DECLARE_FINAL_TYPE(BuffrToplevel, buffr_toplevel, BUFFR, TOPLEVEL, WPEToplevel)

struct _BuffrToplevel {
    WPEToplevel parent_instance;
};

G_DEFINE_FINAL_TYPE(BuffrToplevel, buffr_toplevel, WPE_TYPE_TOPLEVEL)

static void buffr_toplevel_constructed(GObject *object) {
    G_OBJECT_CLASS(buffr_toplevel_parent_class)->constructed(object);
    /* Active right away so WebKit doesn't wait on a configure event. */
    wpe_toplevel_state_changed(WPE_TOPLEVEL(object), WPE_TOPLEVEL_STATE_ACTIVE);
}

static gboolean buffr_toplevel_propagate_size(WPEToplevel *tl, WPEView *v, gpointer user_data) {
    (void)user_data;
    int w, h;
    wpe_toplevel_get_size(tl, &w, &h);
    wpe_view_resized(v, w, h);
    return FALSE;
}

static gboolean buffr_toplevel_resize_vfunc(WPEToplevel *toplevel, int width, int height) {
    wpe_toplevel_resized(toplevel, width, height);
    /* Propagate to every view bound to this toplevel. */
    wpe_toplevel_foreach_view(toplevel, buffr_toplevel_propagate_size, NULL);
    return TRUE;
}

static gboolean buffr_toplevel_set_fullscreen_vfunc(WPEToplevel *toplevel, gboolean fullscreen) {
    WPEToplevelState state = wpe_toplevel_get_state(toplevel);
    state = fullscreen
        ? (WPEToplevelState)(state | WPE_TOPLEVEL_STATE_FULLSCREEN)
        : (WPEToplevelState)(state & ~WPE_TOPLEVEL_STATE_FULLSCREEN);
    wpe_toplevel_state_changed(toplevel, state);
    return TRUE;
}

static void buffr_toplevel_init(BuffrToplevel *self) { (void)self; }
static void buffr_toplevel_class_init(BuffrToplevelClass *klass) {
    GObjectClass *object_class = G_OBJECT_CLASS(klass);
    object_class->constructed = buffr_toplevel_constructed;

    WPEToplevelClass *toplevel_class = WPE_TOPLEVEL_CLASS(klass);
    toplevel_class->resize = buffr_toplevel_resize_vfunc;
    toplevel_class->set_fullscreen = buffr_toplevel_set_fullscreen_vfunc;
}

/* ── BuffrView ──────────────────────────────────────────────────────── */

#define BUFFR_TYPE_VIEW (buffr_view_get_type())
G_DECLARE_FINAL_TYPE(BuffrView, buffr_view, BUFFR, VIEW, WPEView)

struct _BuffrView {
    WPEView parent_instance;
};

G_DEFINE_FINAL_TYPE(BuffrView, buffr_view, WPE_TYPE_VIEW)

static gboolean buffr_view_render_buffer_vfunc(WPEView *view,
                                                WPEBuffer *buffer,
                                                const WPERectangle *damage,
                                                guint n_damage,
                                                GError **error) {
    (void)damage;
    (void)n_damage;
    (void)error;
    g_debug("buffr: render_buffer view=%p buffer=%p", view, buffer);
    /* Forward to Rust for pixel ingestion. Rust calls
     * wpe_view_buffer_rendered itself once it's done. */
    buffr_rust_render_buffer(view, buffer);
    return TRUE;
}

static void buffr_view_on_notify_toplevel(WPEView *view, GParamSpec *pspec, gpointer user_data) {
    (void)pspec;
    (void)user_data;
    WPEToplevel *toplevel = wpe_view_get_toplevel(view);
    g_debug("buffr: notify::toplevel view=%p toplevel=%p", view, toplevel);
    if (!toplevel) {
        wpe_view_unmap(view);
        return;
    }
    int w, h;
    wpe_toplevel_get_size(toplevel, &w, &h);
    g_debug("buffr: toplevel size=%dx%d", w, h);
    if (w > 0 && h > 0)
        wpe_view_resized(view, w, h);
    wpe_view_map(view);
    g_debug("buffr: view mapped=%d", wpe_view_get_mapped(view));
}

static void buffr_view_constructed(GObject *object) {
    G_OBJECT_CLASS(buffr_view_parent_class)->constructed(object);

    /* WPE WebKit expects the view to be "mapped" before it'll begin
     * delivering frames. Attach a notify::toplevel handler that maps the
     * view as soon as a toplevel is assigned. */
    g_signal_connect(object, "notify::toplevel",
                     G_CALLBACK(buffr_view_on_notify_toplevel), NULL);
}

static void buffr_view_init(BuffrView *self) { (void)self; }
static void buffr_view_class_init(BuffrViewClass *klass) {
    GObjectClass *object_class = G_OBJECT_CLASS(klass);
    object_class->constructed = buffr_view_constructed;

    WPEViewClass *view_class = WPE_VIEW_CLASS(klass);
    view_class->render_buffer = buffr_view_render_buffer_vfunc;
}

/* ── BuffrDisplay ───────────────────────────────────────────────────── */

#define BUFFR_TYPE_DISPLAY (buffr_display_get_type())
G_DECLARE_FINAL_TYPE(BuffrDisplay, buffr_display, BUFFR, DISPLAY, WPEDisplay)

struct _BuffrDisplay {
    WPEDisplay parent_instance;

    /* Configured by buffr_display_new(): EGLDisplay we hand WebKit, plus
     * the initial viewport size that drives BuffrScreen. */
    gpointer egl_display;
    int viewport_w;
    int viewport_h;
    double scale;
    int refresh_hz;

    /* Lazily-created single screen handed out by get_screen. */
    BuffrScreen *screen;

    /* Lazily-created DRM device handed out by get_drm_device. */
    WPEDRMDevice *drm_device;
};

G_DEFINE_FINAL_TYPE(BuffrDisplay, buffr_display, WPE_TYPE_DISPLAY)

/* Stash for the most recently created WPEView so Rust can retrieve it
 * after webkit_web_view_new returns (the WebView path swallows the view).
 * Guarded by a static GMutex since GLib may dispatch create_view from any
 * thread it considers thread-default. */
static GMutex buffr_last_view_mutex;
static WPEView *buffr_last_created_view = NULL;

WPEView *buffr_display_take_last_view(void) {
    g_mutex_lock(&buffr_last_view_mutex);
    WPEView *v = buffr_last_created_view;
    buffr_last_created_view = NULL;
    g_mutex_unlock(&buffr_last_view_mutex);
    return v;
}

static gboolean buffr_display_connect_vfunc(WPEDisplay *display, GError **error) {
    (void)display;
    (void)error;
    return TRUE;
}

static WPEView *buffr_display_create_view_vfunc(WPEDisplay *display) {
    WPEView *v = WPE_VIEW(g_object_new(BUFFR_TYPE_VIEW, "display", display, NULL));
    g_mutex_lock(&buffr_last_view_mutex);
    /* Hold a borrowed reference for Rust; we don't ref so that ownership
     * stays with WebKit. Rust must not unref this pointer. */
    buffr_last_created_view = v;
    g_mutex_unlock(&buffr_last_view_mutex);
    return v;
}

static WPEToplevel *buffr_display_create_toplevel_vfunc(WPEDisplay *display, guint max_views) {
    BuffrDisplay *self = BUFFR_DISPLAY(display);
    WPEToplevel *tl = WPE_TOPLEVEL(g_object_new(BUFFR_TYPE_TOPLEVEL,
                                                 "display", display,
                                                 "max-views", max_views,
                                                 NULL));
    /* WPE's default toplevel is 1024x768; mark it sized to the host
     * viewport so WebKit paints at the dims buffr-app expects. */
    if (self->viewport_w > 0 && self->viewport_h > 0)
        wpe_toplevel_resize(tl, self->viewport_w, self->viewport_h);
    return tl;
}

static gpointer buffr_display_get_egl_vfunc(WPEDisplay *display, GError **error) {
    (void)error;
    return BUFFR_DISPLAY(display)->egl_display;
}

static guint buffr_display_get_n_screens_vfunc(WPEDisplay *display) {
    (void)display;
    return 1;
}

static WPEDRMDevice *buffr_display_get_drm_device_vfunc(WPEDisplay *display) {
    BuffrDisplay *self = BUFFR_DISPLAY(display);
    if (!self->drm_device) {
        /* Use the primary node + render node from /dev/dri. Mesa picks
         * sensible defaults; we hand WebKit a device descriptor so its
         * AcceleratedBackingStore can produce DMA-BUF / EGLImage buffers. */
        self->drm_device = wpe_drm_device_new("/dev/dri/card0", "/dev/dri/renderD128");
    }
    return self->drm_device;
}

static WPEScreen *buffr_display_get_screen_vfunc(WPEDisplay *display, guint index) {
    if (index != 0)
        return NULL;
    BuffrDisplay *self = BUFFR_DISPLAY(display);
    if (!self->screen) {
        /* `id` is a G_PARAM_CONSTRUCT_ONLY property on WPEScreen and reads
         * back via wpe_screen_get_id. WebKit's ScreenManager keys a
         * HashMap<uint32, ScreenData> off this id; HashTable<uint32, …>
         * uses 0 as the empty-slot sentinel, so a screen reporting id=0
         * trips a WTFCrash in HashTable.h on insert. Use a non-zero id. */
        self->screen = g_object_new(BUFFR_TYPE_SCREEN, "id", (guint32)1, NULL);
        wpe_screen_set_size(WPE_SCREEN(self->screen), self->viewport_w, self->viewport_h);
        wpe_screen_set_scale(WPE_SCREEN(self->screen), self->scale);
        wpe_screen_set_refresh_rate(WPE_SCREEN(self->screen), self->refresh_hz * 1000);
        /* WebKit's ScreenManager::collectScreenProperties divides by the
         * screen's physical diagonal-in-mm to compute DPI; if both physical
         * dimensions are zero the result is +inf and WTFCrash fires from
         * inside WebPageProxy::launchProcess. Pick a 96-DPI-equivalent size
         * derived from the pixel viewport so the DPI math lands at 96. */
        int phys_w_mm = (int)(self->viewport_w * 25.4 / 96.0);
        int phys_h_mm = (int)(self->viewport_h * 25.4 / 96.0);
        if (phys_w_mm <= 0) phys_w_mm = 300;
        if (phys_h_mm <= 0) phys_h_mm = 200;
        wpe_screen_set_physical_size(WPE_SCREEN(self->screen), phys_w_mm, phys_h_mm);
    }
    return WPE_SCREEN(self->screen);
}

static void buffr_display_init(BuffrDisplay *self) {
    self->egl_display = NULL;
    self->viewport_w = 1280;
    self->viewport_h = 720;
    self->scale = 1.0;
    self->refresh_hz = 60;
    self->screen = NULL;
    self->drm_device = NULL;
}

static void buffr_display_dispose(GObject *object) {
    BuffrDisplay *self = BUFFR_DISPLAY(object);
    g_clear_object(&self->screen);
    g_clear_pointer(&self->drm_device, wpe_drm_device_unref);
    G_OBJECT_CLASS(buffr_display_parent_class)->dispose(object);
}

static void buffr_display_class_init(BuffrDisplayClass *klass) {
    GObjectClass *object_class = G_OBJECT_CLASS(klass);
    object_class->dispose = buffr_display_dispose;

    WPEDisplayClass *display_class = WPE_DISPLAY_CLASS(klass);
    display_class->connect = buffr_display_connect_vfunc;
    display_class->create_view = buffr_display_create_view_vfunc;
    display_class->create_toplevel = buffr_display_create_toplevel_vfunc;
    display_class->get_egl_display = buffr_display_get_egl_vfunc;
    display_class->get_drm_device = buffr_display_get_drm_device_vfunc;
    /* Expose one screen w/ non-zero physical size so AcceleratedBackingStore
     * can allocate frame buffers against it. The DPI-from-physical-mm WTFCrash
     * in ScreenManager::collectScreenProperties is dodged by computing physical
     * mm from the pixel viewport at 96 DPI in buffr_display_get_screen_vfunc. */
    display_class->get_n_screens = buffr_display_get_n_screens_vfunc;
    display_class->get_screen = buffr_display_get_screen_vfunc;
}

/* Public constructor exposed to Rust. */
WPEDisplay *buffr_display_new(gpointer egl_display,
                              int viewport_w,
                              int viewport_h,
                              double scale,
                              int refresh_hz) {
    BuffrDisplay *self = g_object_new(BUFFR_TYPE_DISPLAY, NULL);
    self->egl_display = egl_display;
    self->viewport_w = viewport_w;
    self->viewport_h = viewport_h;
    self->scale = scale > 0 ? scale : 1.0;
    self->refresh_hz = refresh_hz > 0 ? refresh_hz : 60;
    return WPE_DISPLAY(self);
}

/* Re-export the toplevel/view GType getters so Rust can pass them to
 * g_object_new directly if it ever needs to (the default flow goes through
 * the display's create_toplevel/create_view vmethods). */
GType buffr_display_get_view_type(void) { return BUFFR_TYPE_VIEW; }
GType buffr_display_get_toplevel_type(void) { return BUFFR_TYPE_TOPLEVEL; }

/* ── BuffrDisplayWayland (#152) ─────────────────────────────────────────── */
/*
 * WPEDisplay subclass that reuses an existing Wayland connection supplied by
 * buffr-app instead of opening its own.  This eliminates the cross-client
 * wl_subsurface problem that blocked Phase 3 with stock WPEDisplayWayland.
 *
 * The display borrows all Wayland object pointers — they are owned by winit
 * for the lifetime of the host process.  EGL is initialised here via
 * eglGetPlatformDisplay(EGL_PLATFORM_WAYLAND_KHR, wl_display, NULL).
 *
 * create_view stubs out to a BuffrView (the existing OSR view) WITHOUT
 * attaching a ViewCtx so WebKit gets a valid WPEView object but the OSR
 * render-buffer ingest path never fires.  #153 will replace this stub with
 * a proper BuffrViewWayland that composites into a wl_subsurface.
 */

#define BUFFR_TYPE_DISPLAY_WAYLAND (buffr_display_wayland_get_type())
G_DECLARE_FINAL_TYPE(BuffrDisplayWayland, buffr_display_wayland, BUFFR, DISPLAY_WAYLAND, WPEDisplay)

struct _BuffrDisplayWayland {
    WPEDisplay parent_instance;

    /* Wayland objects — borrowed from buffr-app / winit. Valid for the
     * lifetime of the host process; we never own or free these. */
    struct wl_display      *wl_display;
    struct wl_compositor   *wl_compositor;
    struct wl_subcompositor *wl_subcompositor;
    struct wl_surface      *parent_surface;

    /* zwp_linux_dmabuf_v1 global bound during registry enumeration in the
     * constructor.  NULL when the compositor doesn't advertise this protocol
     * (e.g. nested Weston without dmabuf support).  Shared with every
     * BuffrViewWayland created by this display; not owned here (protocol
     * objects are owned by the connection, freed with wl_display_disconnect
     * which we never call — the connection is owned by the host process). */
    struct zwp_linux_dmabuf_v1 *dmabuf;

    /* EGL display created here from the host wl_display.  Valid as long as
     * eglInitialize succeeded; NULL on failure (falls back to OSR in Rust). */
    EGLDisplay egl_display;

    /* Lazily-created DRM device handed out by get_drm_device. Without
     * this, WebKit picks the SHM software path even when the
     * compositor advertises zwp_linux_dmabuf_v1 — surfacing the
     * "non-DMA-BUF buffer type" render error in BuffrViewWayland.
     * Mirrors the OSR-flavoured BuffrDisplay's behaviour. */
    WPEDRMDevice *drm_device;

    /* Viewport / screen configuration. */
    int    viewport_w;
    int    viewport_h;
    double scale;
    int    refresh_hz;

    /* Lazily-created single screen. */
    BuffrScreen *screen;

    /* Stash for the most-recently-created WPEView so Rust can retrieve it
     * after webkit_web_view_new (mirrors BuffrDisplay). */
    GMutex  last_view_mutex;
    WPEView *last_created_view;
};

G_DEFINE_FINAL_TYPE(BuffrDisplayWayland, buffr_display_wayland, WPE_TYPE_DISPLAY)

/* Forward declaration — BuffrViewWayland is defined later in this file. */
static WPEView *buffr_view_wayland_new(WPEDisplay *display,
                                        BuffrDisplayWayland *parent_display);

/* ── Stash accessor ─────────────────────────────────────────────────────── */

WPEView *buffr_display_wayland_take_last_view(BuffrDisplayWayland *self) {
    g_mutex_lock(&self->last_view_mutex);
    WPEView *v = self->last_created_view;
    self->last_created_view = NULL;
    g_mutex_unlock(&self->last_view_mutex);
    return v;
}

/* ── vmethods ───────────────────────────────────────────────────────────── */

static gboolean buffr_display_wayland_connect_vfunc(WPEDisplay *display, GError **error) {
    /* We don't own the wl_display — it is already connected by the host.
     * Nothing to do; always succeed. */
    (void)display;
    (void)error;
    return TRUE;
}

static gpointer buffr_display_wayland_get_egl_display_vfunc(WPEDisplay *display, GError **error) {
    (void)error;
    BuffrDisplayWayland *self = BUFFR_DISPLAY_WAYLAND(display);
    return self->egl_display;
}

static WPEView *buffr_display_wayland_create_view_vfunc(WPEDisplay *display) {
    /* #153: Create a BuffrViewWayland that composites into a wl_subsurface. */
    g_debug("buffr_display_wayland: create_view → BuffrViewWayland (#153)");
    BuffrDisplayWayland *self = BUFFR_DISPLAY_WAYLAND(display);
    WPEView *v = buffr_view_wayland_new(display, self);

    g_mutex_lock(&self->last_view_mutex);
    self->last_created_view = v;
    g_mutex_unlock(&self->last_view_mutex);

    return v;
}

static WPEToplevel *buffr_display_wayland_create_toplevel_vfunc(WPEDisplay *display, guint max_views) {
    BuffrDisplayWayland *self = BUFFR_DISPLAY_WAYLAND(display);
    WPEToplevel *tl = WPE_TOPLEVEL(g_object_new(BUFFR_TYPE_TOPLEVEL,
                                                 "display", display,
                                                 "max-views", max_views,
                                                 NULL));
    if (self->viewport_w > 0 && self->viewport_h > 0)
        wpe_toplevel_resize(tl, self->viewport_w, self->viewport_h);
    return tl;
}

static guint buffr_display_wayland_get_n_screens_vfunc(WPEDisplay *display) {
    (void)display;
    return 1;
}

static WPEScreen *buffr_display_wayland_get_screen_vfunc(WPEDisplay *display, guint index) {
    if (index != 0)
        return NULL;
    BuffrDisplayWayland *self = BUFFR_DISPLAY_WAYLAND(display);
    if (!self->screen) {
        /* Mirror the screen setup from BuffrDisplay: non-zero id to avoid the
         * WTFCrash in WebKit's ScreenManager (id=0 is the HashMap empty
         * sentinel), and non-zero physical size so the DPI math doesn't hit
         * +inf / WTFCrash in ScreenManager::collectScreenProperties. */
        self->screen = g_object_new(BUFFR_TYPE_SCREEN, "id", (guint32)1, NULL);
        wpe_screen_set_size(WPE_SCREEN(self->screen), self->viewport_w, self->viewport_h);
        wpe_screen_set_scale(WPE_SCREEN(self->screen), self->scale);
        wpe_screen_set_refresh_rate(WPE_SCREEN(self->screen), self->refresh_hz * 1000);
        int phys_w_mm = (int)(self->viewport_w * 25.4 / 96.0);
        int phys_h_mm = (int)(self->viewport_h * 25.4 / 96.0);
        if (phys_w_mm <= 0) phys_w_mm = 300;
        if (phys_h_mm <= 0) phys_h_mm = 200;
        wpe_screen_set_physical_size(WPE_SCREEN(self->screen), phys_w_mm, phys_h_mm);
    }
    return WPE_SCREEN(self->screen);
}

/* ── GObject lifecycle ──────────────────────────────────────────────────── */

static void buffr_display_wayland_init(BuffrDisplayWayland *self) {
    self->wl_display        = NULL;
    self->wl_compositor     = NULL;
    self->wl_subcompositor  = NULL;
    self->parent_surface    = NULL;
    self->dmabuf            = NULL;
    self->egl_display       = EGL_NO_DISPLAY;
    self->drm_device        = NULL;
    self->viewport_w        = 1280;
    self->viewport_h        = 720;
    self->scale             = 1.0;
    self->refresh_hz        = 60;
    self->screen            = NULL;
    self->last_created_view = NULL;
    g_mutex_init(&self->last_view_mutex);
}

static WPEDRMDevice *buffr_display_wayland_get_drm_device_vfunc(WPEDisplay *display) {
    BuffrDisplayWayland *self = BUFFR_DISPLAY_WAYLAND(display);
    if (!self->drm_device) {
        /* Same defaults as the OSR-flavoured BuffrDisplay. WebKit
         * uses the DRM device descriptor to drive its
         * AcceleratedBackingStore down the DMA-BUF path; without a
         * device, BackingStore falls back to SHM and our render
         * vfunc rejects the frames. */
        self->drm_device = wpe_drm_device_new("/dev/dri/card0", "/dev/dri/renderD128");
    }
    return self->drm_device;
}

static void buffr_display_wayland_dispose(GObject *object) {
    BuffrDisplayWayland *self = BUFFR_DISPLAY_WAYLAND(object);
    g_clear_object(&self->screen);
    g_clear_pointer(&self->drm_device, wpe_drm_device_unref);
    g_mutex_clear(&self->last_view_mutex);
    /* Note: we do NOT call eglTerminate here — the EGLDisplay belongs to the
     * host wl_display connection that outlives us. */
    G_OBJECT_CLASS(buffr_display_wayland_parent_class)->dispose(object);
}

static void buffr_display_wayland_class_init(BuffrDisplayWaylandClass *klass) {
    GObjectClass *object_class = G_OBJECT_CLASS(klass);
    object_class->dispose = buffr_display_wayland_dispose;

    WPEDisplayClass *display_class = WPE_DISPLAY_CLASS(klass);
    display_class->connect          = buffr_display_wayland_connect_vfunc;
    display_class->create_view      = buffr_display_wayland_create_view_vfunc;
    display_class->create_toplevel  = buffr_display_wayland_create_toplevel_vfunc;
    display_class->get_egl_display  = buffr_display_wayland_get_egl_display_vfunc;
    display_class->get_n_screens    = buffr_display_wayland_get_n_screens_vfunc;
    display_class->get_screen       = buffr_display_wayland_get_screen_vfunc;
    display_class->get_drm_device   = buffr_display_wayland_get_drm_device_vfunc;
    /* get_keymap: NULL → WebKit synthesises a keymap on demand. */
}

/* ── Public constructor ─────────────────────────────────────────────────── */

/* ── BuffrViewWayland (#153) ─────────────────────────────────────────────────── */
/*
 * WPEView subclass that composites WebKit frames directly into a wl_subsurface
 * owned by this view. Frames arrive via the render_buffer vmethod as
 * WPEBufferDMABuf objects; we wrap the DMA-BUF fds using zwp_linux_dmabuf_v1
 * and attach the resulting wl_buffer to our wl_surface. No CPU readback.
 *
 * Lifecycle:
 *   - Created by BuffrDisplayWayland::create_view_vfunc.
 *   - Borrows Wayland handles (wl_compositor, wl_subcompositor, parent_surface,
 *     wl_display, dmabuf) from the parent BuffrDisplayWayland; these remain
 *     valid for the host process lifetime.
 *   - Creates its own wl_surface + wl_subsurface in buffr_view_wayland_init
 *     after the struct fields are populated by the constructor helper.
 *   - dispose() destroys the subsurface + surface.
 *
 * Render-buffer flow:
 *   1. render_buffer vmethod receives WPEBufferDMABuf from WebKit.
 *   2. We allocate a buffer_release_ctx (view + wpe_buffer) on the heap.
 *   3. Build zwp_linux_buffer_params_v1, add each plane fd/offset/stride/mod.
 *   4. create_immed to get a wl_buffer immediately.
 *   5. Attach → damage_buffer → commit on our wl_surface.
 *   6. On wl_buffer.release: call wpe_view_buffer_rendered (acks WebKit pool
 *      slot) + wl_buffer_destroy + g_object_unref on the WPEBuffer.
 *
 * Buffer-release data packing: both (view, wpe_buffer) are packed into a
 * heap-allocated `struct buffer_release_ctx`, passed as wl_buffer listener
 * user_data. The on_wl_buffer_release callback frees it after acking.
 */

#define BUFFR_TYPE_VIEW_WAYLAND (buffr_view_wayland_get_type())
G_DECLARE_FINAL_TYPE(BuffrViewWayland, buffr_view_wayland, BUFFR, VIEW_WAYLAND, WPEView)

struct _BuffrViewWayland {
    WPEView parent_instance;

    /* Child wl_surface + subsurface owned by this view. */
    struct wl_surface    *wl_surface;
    struct wl_subsurface *wl_subsurface;

    /* Borrowed from BuffrDisplayWayland — valid for process lifetime. */
    struct wl_display       *wl_display;
    struct wl_compositor    *wl_compositor;
    struct wl_subcompositor *wl_subcompositor;
    struct wl_surface       *parent_surface;
    /* NULL when compositor doesn't advertise zwp_linux_dmabuf_v1. */
    struct zwp_linux_dmabuf_v1 *dmabuf;
};

G_DEFINE_FINAL_TYPE(BuffrViewWayland, buffr_view_wayland, WPE_TYPE_VIEW)

/* Context passed as listener user_data on each wl_buffer so the release
 * callback can ack WebKit and free both the wl_buffer and this struct. */
struct buffer_release_ctx {
    WPEView   *view;
    WPEBuffer *wpe_buffer;
};

static void on_wl_buffer_release(void *data, struct wl_buffer *wl_buf) {
    struct buffer_release_ctx *ctx = (struct buffer_release_ctx *)data;
    if (ctx) {
        /* Ack to WebKit: recycles the pool slot so the next frame can be
         * allocated. Must happen before wl_buffer_destroy. */
        wpe_view_buffer_rendered(ctx->view, ctx->wpe_buffer);
        g_object_unref(ctx->wpe_buffer);
        g_free(ctx);
    }
    wl_buffer_destroy(wl_buf);
}

static const struct wl_buffer_listener buffr_wl_buffer_listener = {
    .release = on_wl_buffer_release,
};

static gboolean buffr_view_wayland_render_buffer_vfunc(
    WPEView            *view,
    WPEBuffer          *buffer,
    const WPERectangle *damage_rects,
    guint               n_damage_rects,
    GError            **error)
{
    (void)damage_rects;
    (void)n_damage_rects;

    BuffrViewWayland *self = BUFFR_VIEW_WAYLAND(view);

    g_debug("buffr_view_wayland: render_buffer view=%p buffer=%p dmabuf_global=%p",
            view, buffer, self->dmabuf);

    /* Surface must be ready. */
    if (!self->wl_surface) {
        g_set_error(error, WPE_VIEW_ERROR, WPE_VIEW_ERROR_RENDER_FAILED,
                    "BuffrViewWayland: wl_surface not initialised");
        return FALSE;
    }

    /* Only DMA-BUF buffers are supported on the Wayland native path.
     * SHM buffers could be supported as a fallback but are deliberately
     * omitted for now — WebKit always produces DMA-BUF when the display
     * advertises a DRM device. */
    if (!WPE_IS_BUFFER_DMA_BUF(buffer)) {
        g_set_error(error, WPE_VIEW_ERROR, WPE_VIEW_ERROR_RENDER_FAILED,
                    "BuffrViewWayland: non-DMA-BUF buffer type — only "
                    "WPEBufferDMABuf is supported on the Wayland path");
        return FALSE;
    }

    if (!self->dmabuf) {
        /* Compositor doesn't expose zwp_linux_dmabuf_v1. Ack + drop; the
         * caller sees a blank subsurface but no crash. */
        g_debug("buffr_view_wayland: dmabuf protocol not available — "
                "acking buffer without attach (blank surface)");
        wpe_view_buffer_rendered(view, buffer);
        return TRUE;
    }

    WPEBufferDMABuf *dma = WPE_BUFFER_DMA_BUF(buffer);
    guint32 n_planes  = wpe_buffer_dma_buf_get_n_planes(dma);
    guint64 modifier  = wpe_buffer_dma_buf_get_modifier(dma);
    guint32 format    = wpe_buffer_dma_buf_get_format(dma);
    int     buf_w     = wpe_buffer_get_width(buffer);
    int     buf_h     = wpe_buffer_get_height(buffer);

    struct zwp_linux_buffer_params_v1 *params =
        zwp_linux_dmabuf_v1_create_params(self->dmabuf);
    if (!params) {
        g_set_error(error, WPE_VIEW_ERROR, WPE_VIEW_ERROR_RENDER_FAILED,
                    "BuffrViewWayland: zwp_linux_dmabuf_v1_create_params returned NULL");
        return FALSE;
    }

    for (guint32 i = 0; i < n_planes; i++) {
        int     fd     = wpe_buffer_dma_buf_get_fd(dma, i);
        guint32 offset = wpe_buffer_dma_buf_get_offset(dma, i);
        guint32 stride = wpe_buffer_dma_buf_get_stride(dma, i);
        zwp_linux_buffer_params_v1_add(
            params, fd, i, offset, stride,
            (uint32_t)(modifier >> 32),
            (uint32_t)(modifier & 0xFFFFFFFF));
    }

    struct wl_buffer *wl_buf = zwp_linux_buffer_params_v1_create_immed(
        params, buf_w, buf_h, format, 0);
    zwp_linux_buffer_params_v1_destroy(params);

    if (!wl_buf) {
        g_set_error(error, WPE_VIEW_ERROR, WPE_VIEW_ERROR_RENDER_FAILED,
                    "BuffrViewWayland: zwp_linux_buffer_params_v1_create_immed "
                    "returned NULL — compositor rejected DMA-BUF import");
        return FALSE;
    }

    /* Pack (view, wpe_buffer) into a release-callback context.
     * Ownership: freed by on_wl_buffer_release. */
    struct buffer_release_ctx *ctx =
        g_new0(struct buffer_release_ctx, 1);
    ctx->view       = view;
    ctx->wpe_buffer = buffer;
    /* Keep WPE buffer alive until the compositor releases the wl_buffer. */
    g_object_ref(buffer);

    wl_buffer_add_listener(wl_buf, &buffr_wl_buffer_listener, ctx);

    wl_surface_attach(self->wl_surface, wl_buf, 0, 0);
    wl_surface_damage_buffer(self->wl_surface, 0, 0, buf_w, buf_h);
    wl_surface_commit(self->wl_surface);

    /* Flush outgoing Wayland messages so the compositor sees the commit. */
    wl_display_flush(self->wl_display);

    g_debug("buffr_view_wayland: attached DMA-BUF wl_buffer %p (%dx%d fmt=0x%x)",
            wl_buf, buf_w, buf_h, format);
    return TRUE;
}

/* Propagate resize to subsurface (no-op for now; toplevel handles sizes). */
static void buffr_view_wayland_on_notify_toplevel(WPEView *view,
                                                   GParamSpec *pspec,
                                                   gpointer user_data)
{
    (void)pspec;
    (void)user_data;
    WPEToplevel *toplevel = wpe_view_get_toplevel(view);
    g_debug("buffr_view_wayland: notify::toplevel view=%p toplevel=%p", view, toplevel);
    if (!toplevel) {
        wpe_view_unmap(view);
        return;
    }
    int w, h;
    wpe_toplevel_get_size(toplevel, &w, &h);
    if (w > 0 && h > 0)
        wpe_view_resized(view, w, h);
    wpe_view_map(view);
}

static void buffr_view_wayland_constructed(GObject *object) {
    G_OBJECT_CLASS(buffr_view_wayland_parent_class)->constructed(object);
    g_signal_connect(object, "notify::toplevel",
                     G_CALLBACK(buffr_view_wayland_on_notify_toplevel), NULL);
}

static void buffr_view_wayland_dispose(GObject *object) {
    BuffrViewWayland *self = BUFFR_VIEW_WAYLAND(object);
    if (self->wl_subsurface) {
        wl_subsurface_destroy(self->wl_subsurface);
        self->wl_subsurface = NULL;
    }
    if (self->wl_surface) {
        wl_surface_destroy(self->wl_surface);
        self->wl_surface = NULL;
    }
    /* Note: wl_display, wl_compositor, wl_subcompositor, parent_surface,
     * and dmabuf are all borrowed from BuffrDisplayWayland — not freed here. */
    G_OBJECT_CLASS(buffr_view_wayland_parent_class)->dispose(object);
}

static void buffr_view_wayland_init(BuffrViewWayland *self) {
    self->wl_surface       = NULL;
    self->wl_subsurface    = NULL;
    self->wl_display       = NULL;
    self->wl_compositor    = NULL;
    self->wl_subcompositor = NULL;
    self->parent_surface   = NULL;
    self->dmabuf           = NULL;
}

static void buffr_view_wayland_class_init(BuffrViewWaylandClass *klass) {
    GObjectClass *object_class = G_OBJECT_CLASS(klass);
    object_class->constructed = buffr_view_wayland_constructed;
    object_class->dispose     = buffr_view_wayland_dispose;

    WPEViewClass *view_class = WPE_VIEW_CLASS(klass);
    view_class->render_buffer = buffr_view_wayland_render_buffer_vfunc;
}

/*
 * buffr_view_wayland_new — constructor called from BuffrDisplayWayland's
 * create_view vmethod.
 *
 * Creates the child wl_surface + wl_subsurface from the parent display's
 * Wayland handles, then returns the new WPEView.
 */
static WPEView *buffr_view_wayland_new(WPEDisplay *display,
                                        BuffrDisplayWayland *parent_display)
{
    BuffrViewWayland *self =
        BUFFR_VIEW_WAYLAND(g_object_new(BUFFR_TYPE_VIEW_WAYLAND,
                                         "display", display, NULL));

    /* Borrow handles from the parent display. */
    self->wl_display       = parent_display->wl_display;
    self->wl_compositor    = parent_display->wl_compositor;
    self->wl_subcompositor = parent_display->wl_subcompositor;
    self->parent_surface   = parent_display->parent_surface;
    self->dmabuf           = parent_display->dmabuf;

    /* Create child surface + subsurface. */
    if (self->wl_compositor) {
        self->wl_surface = wl_compositor_create_surface(self->wl_compositor);
        g_debug("buffr_view_wayland_new: wl_surface=%p", self->wl_surface);
    }
    if (self->wl_surface && self->wl_subcompositor && self->parent_surface) {
        self->wl_subsurface = wl_subcompositor_get_subsurface(
            self->wl_subcompositor, self->wl_surface, self->parent_surface);
        g_debug("buffr_view_wayland_new: wl_subsurface=%p", self->wl_subsurface);
        if (self->wl_subsurface) {
            /* Desync mode: WebKit commits frames independently of the host
             * app's parent-surface commit cadence. */
            wl_subsurface_set_desync(self->wl_subsurface);
            wl_subsurface_set_position(self->wl_subsurface, 0, 0);
        }
    }

    return WPE_VIEW(self);
}

/* ── BuffrInputMethodContext ─────────────────────────────────────────────── */
/*
 * Subclass of WebKitInputMethodContext that lets Rust push preedit strings
 * and commit text from winit IME events.
 *
 * The subclass stores the current preedit text and cursor byte range.
 * Rust calls:
 *   buffr_input_method_context_set_preedit(ctx, text, cursor)
 *   buffr_input_method_context_commit(ctx, text)
 *   buffr_input_method_context_cancel(ctx)
 *
 * The get_preedit vmethod returns the stored preedit and converts the byte
 * cursor offset to a character offset using g_utf8_pointer_to_offset, as
 * required by WebKitInputMethodContextClass.
 */

#define BUFFR_TYPE_INPUT_METHOD_CONTEXT (buffr_input_method_context_get_type())
G_DECLARE_FINAL_TYPE(BuffrInputMethodContext, buffr_input_method_context,
                     BUFFR, INPUT_METHOD_CONTEXT, WebKitInputMethodContext)

struct _BuffrInputMethodContext {
    WebKitInputMethodContext parent_instance;

    /* Current preedit string. NULL or "" when no composition is active. */
    gchar *preedit_text;
    /* Byte offset of the cursor within preedit_text. */
    gint cursor_end;
};

G_DEFINE_FINAL_TYPE(BuffrInputMethodContext, buffr_input_method_context,
                    WEBKIT_TYPE_INPUT_METHOD_CONTEXT)

/* get_preedit vmethod: returns the stored preedit and a character-offset
 * cursor derived from the byte-offset cursor_end stored in the instance.
 * WebKit g_free()s *text and g_list_free_full()s *underlines on its side. */
static void buffr_input_method_context_get_preedit_vfunc(
    WebKitInputMethodContext *context,
    gchar                   **text,
    GList                   **underlines,
    guint                    *cursor_offset)
{
    BuffrInputMethodContext *self = BUFFR_INPUT_METHOD_CONTEXT(context);

    if (text) {
        *text = self->preedit_text ? g_strdup(self->preedit_text) : g_strdup("");
    }
    if (underlines) {
        *underlines = NULL; /* No per-character underline styling from winit. */
    }
    if (cursor_offset) {
        if (self->preedit_text && self->cursor_end > 0) {
            /* Convert byte offset to character offset.
             * cursor_end is the exclusive end of the selection in bytes.
             * Clamp to string length to guard against stale/bad values. */
            gsize byte_len = strlen(self->preedit_text);
            gint clamped = (self->cursor_end > (gint)byte_len)
                ? (gint)byte_len : self->cursor_end;
            /* g_utf8_pointer_to_offset returns the character count from
             * the start of the string to the pointer position. */
            *cursor_offset = (guint)g_utf8_pointer_to_offset(
                self->preedit_text,
                self->preedit_text + clamped);
        } else {
            *cursor_offset = 0;
        }
    }
}

static void buffr_input_method_context_init(BuffrInputMethodContext *self)
{
    self->preedit_text = NULL;
    self->cursor_end   = 0;
}

static void buffr_input_method_context_finalize(GObject *object)
{
    BuffrInputMethodContext *self = BUFFR_INPUT_METHOD_CONTEXT(object);
    g_free(self->preedit_text);
    self->preedit_text = NULL;
    G_OBJECT_CLASS(buffr_input_method_context_parent_class)->finalize(object);
}

static void buffr_input_method_context_class_init(
    BuffrInputMethodContextClass *klass)
{
    GObjectClass *object_class = G_OBJECT_CLASS(klass);
    object_class->finalize = buffr_input_method_context_finalize;

    WebKitInputMethodContextClass *imc_class =
        WEBKIT_INPUT_METHOD_CONTEXT_CLASS(klass);
    imc_class->get_preedit = buffr_input_method_context_get_preedit_vfunc;
}

/* ── Public API ─────────────────────────────────────────────────────────── */

BuffrInputMethodContext *buffr_input_method_context_new(void)
{
    return g_object_new(BUFFR_TYPE_INPUT_METHOD_CONTEXT, NULL);
}

void buffr_input_method_context_set_preedit(
    BuffrInputMethodContext *ctx,
    const char              *text,
    int                      cursor)
{
    if (!ctx)
        return;

    gboolean was_empty = (!ctx->preedit_text || ctx->preedit_text[0] == '\0');
    gboolean new_empty = (!text || text[0] == '\0');

    g_free(ctx->preedit_text);
    ctx->preedit_text = g_strdup(text ? text : "");
    ctx->cursor_end   = cursor;

    if (new_empty) {
        /* Preedit cleared: always emit preedit-finished. */
        g_signal_emit_by_name(ctx, "preedit-finished");
    } else {
        if (was_empty) {
            /* First preedit character: emit preedit-started before changed. */
            g_signal_emit_by_name(ctx, "preedit-started");
        }
        g_signal_emit_by_name(ctx, "preedit-changed");
    }
}

void buffr_input_method_context_commit(
    BuffrInputMethodContext *ctx,
    const char              *text)
{
    if (!ctx)
        return;

    gboolean had_preedit = (ctx->preedit_text && ctx->preedit_text[0] != '\0');

    /* Clear preedit state before emitting so get_preedit returns "" if
     * WebKit queries it from within the committed signal handler OR the
     * preedit-finished handler that follows. */
    g_free(ctx->preedit_text);
    ctx->preedit_text = g_strdup("");
    ctx->cursor_end   = 0;

    /* Emit committed with the final text. */
    g_signal_emit_by_name(ctx, "committed", text ? text : "");

    /* If there was an active preedit, close it. */
    if (had_preedit) {
        g_signal_emit_by_name(ctx, "preedit-finished");
    }
}

void buffr_input_method_context_cancel(BuffrInputMethodContext *ctx)
{
    if (!ctx)
        return;

    gboolean had_preedit = (ctx->preedit_text && ctx->preedit_text[0] != '\0');

    g_free(ctx->preedit_text);
    ctx->preedit_text = g_strdup("");
    ctx->cursor_end   = 0;

    if (had_preedit) {
        g_signal_emit_by_name(ctx, "preedit-finished");
    }
}

/* ── BuffrViewWayland: buffr_view_wayland_set_rect ─────────────────────────── */
/*
 * buffr_view_wayland_set_rect — update subsurface position and view size.
 *
 * Called from Rust when the apps-layer browser_rect changes, typically driven
 * by chrome layout (address bar height, sidebar width, etc.).  Must be called
 * on the GLib worker thread (same thread that commits Wayland protocol).
 */
void buffr_view_wayland_set_rect(WPEView *view, int x, int y, int w, int h)
{
    if (!view || !BUFFR_IS_VIEW_WAYLAND(view))
        return;
    BuffrViewWayland *self = BUFFR_VIEW_WAYLAND(view);
    g_debug("buffr_view_wayland_set_rect: (%d,%d) %dx%d", x, y, w, h);
    if (self->wl_subsurface) {
        wl_subsurface_set_position(self->wl_subsurface, x, y);
    }
    /* Tell WebKit the new render size so it repaints at the right dimensions. */
    if (w > 0 && h > 0) {
        wpe_view_resized(view, w, h);
    }
}

/* ── BuffrDisplayWayland (#152) ─────────────────────────────────────────────── */

/* ── wl_registry listener for dmabuf global binding ──────────────────────── */

struct dmabuf_registry_ctx {
    struct zwp_linux_dmabuf_v1 *dmabuf;
};

static void on_registry_global(void *data, struct wl_registry *registry,
                                uint32_t name, const char *interface,
                                uint32_t version)
{
    struct dmabuf_registry_ctx *ctx = (struct dmabuf_registry_ctx *)data;
    if (strcmp(interface, zwp_linux_dmabuf_v1_interface.name) == 0) {
        uint32_t bind_ver = (version >= 3) ? 3 : version;
        ctx->dmabuf = (struct zwp_linux_dmabuf_v1 *)
            wl_registry_bind(registry, name,
                             &zwp_linux_dmabuf_v1_interface, bind_ver);
        g_debug("buffr_display_wayland: bound zwp_linux_dmabuf_v1 v%u (server v%u)",
                bind_ver, version);
    }
}

static void on_registry_global_remove(void *data, struct wl_registry *registry,
                                       uint32_t name)
{
    (void)data; (void)registry; (void)name;
}

static const struct wl_registry_listener buffr_registry_listener = {
    .global        = on_registry_global,
    .global_remove = on_registry_global_remove,
};

/* ── Public constructor ──────────────────────────────────────────────────── */

/*
 * buffr_display_wayland_new — construct a BuffrDisplayWayland.
 *
 * Takes borrowed Wayland object pointers from the host winit window and
 * creates an EGL platform display from the host wl_display.  Returns NULL
 * when eglInitialize fails so the Rust caller can fall back to OSR.
 *
 * Also binds zwp_linux_dmabuf_v1 from the compositor registry so
 * BuffrViewWayland can wrap DMA-BUF frames as wl_buffers.
 *
 * All pointer arguments are borrowed; the caller (winit / buffr-app) owns
 * them for the lifetime of the host process.
 */
WPEDisplay *buffr_display_wayland_new(
    void *wl_display_ptr,
    void *wl_compositor_ptr,
    void *wl_subcompositor_ptr,
    void *parent_surface_ptr,
    int viewport_w,
    int viewport_h,
    double scale,
    int refresh_hz)
{
    if (!wl_display_ptr) {
        g_warning("buffr_display_wayland_new: wl_display is NULL — refusing to construct");
        return NULL;
    }

    /* Initialise EGL using the host wl_display.  We use
     * eglGetPlatformDisplay (EGL 1.5 / EGL_EXT_platform_base) so Mesa
     * picks the Wayland platform rather than guessing from the pointer type.
     * If the implementation only supports eglGetDisplay, fall back to that. */
    EGLDisplay egl_dpy = EGL_NO_DISPLAY;
    PFNEGLGETPLATFORMDISPLAYEXTPROC eglGetPlatformDisplayEXT =
        (PFNEGLGETPLATFORMDISPLAYEXTPROC)eglGetProcAddress("eglGetPlatformDisplayEXT");
    if (eglGetPlatformDisplayEXT) {
        egl_dpy = eglGetPlatformDisplayEXT(EGL_PLATFORM_WAYLAND_KHR,
                                           wl_display_ptr, NULL);
    }
    if (egl_dpy == EGL_NO_DISPLAY) {
        /* Fallback: standard eglGetDisplay with the wl_display pointer cast
         * to EGLNativeDisplayType.  Works on most Mesa Wayland drivers. */
        egl_dpy = eglGetDisplay((EGLNativeDisplayType)wl_display_ptr);
    }
    if (egl_dpy == EGL_NO_DISPLAY) {
        g_warning("buffr_display_wayland_new: eglGetDisplay returned EGL_NO_DISPLAY");
        return NULL;
    }

    EGLint major = 0, minor = 0;
    if (!eglInitialize(egl_dpy, &major, &minor)) {
        g_warning("buffr_display_wayland_new: eglInitialize failed (error 0x%x)",
                  (unsigned)eglGetError());
        return NULL;
    }
    g_debug("buffr_display_wayland: EGL %d.%d initialised on wl_display=%p",
            major, minor, wl_display_ptr);

    BuffrDisplayWayland *self = g_object_new(BUFFR_TYPE_DISPLAY_WAYLAND, NULL);
    self->wl_display        = (struct wl_display *)wl_display_ptr;
    self->wl_compositor     = (struct wl_compositor *)wl_compositor_ptr;
    self->wl_subcompositor  = (struct wl_subcompositor *)wl_subcompositor_ptr;
    self->parent_surface    = (struct wl_surface *)parent_surface_ptr;
    self->egl_display       = egl_dpy;
    self->viewport_w        = viewport_w > 0  ? viewport_w  : 1280;
    self->viewport_h        = viewport_h > 0  ? viewport_h  : 720;
    self->scale             = scale > 0.0     ? scale        : 1.0;
    self->refresh_hz        = refresh_hz > 0  ? refresh_hz   : 60;

    /* Enumerate the compositor registry to bind zwp_linux_dmabuf_v1.
     * We do a synchronous roundtrip so the binding is available before
     * the first create_view call. */
    {
        struct dmabuf_registry_ctx reg_ctx = { .dmabuf = NULL };
        struct wl_registry *registry =
            wl_display_get_registry((struct wl_display *)wl_display_ptr);
        if (registry) {
            wl_registry_add_listener(registry, &buffr_registry_listener, &reg_ctx);
            /* Roundtrip: process all pending events so the global callbacks fire. */
            wl_display_roundtrip((struct wl_display *)wl_display_ptr);
            self->dmabuf = reg_ctx.dmabuf;
            /* Keep the registry proxy alive for the connection's lifetime. */
            /* (We don't destroy it — the Wayland connection owns it.) */
        }
        if (self->dmabuf) {
            g_debug("buffr_display_wayland_new: dmabuf protocol bound");
        } else {
            g_warning("buffr_display_wayland_new: zwp_linux_dmabuf_v1 not "
                      "available — BuffrViewWayland will ack-and-drop frames");
        }
    }

    return WPE_DISPLAY(self);
}
