Files
DroidDeck/app/src/main/cpp/waylandcomp/src/compositor.c
T
The412Banner e8aaae1343 Touch: a lost finger-up no longer blocks every later touch
On the pointer path (a client that takes no wl_touch - gamescope), touch_down counted the
fingers still down before clearing a sequence whose up had been lost, so the stale point left
any_active set and every later tap or swipe was dropped: in Direct touch mode Big Picture
ignored the screen while the pad worked. The count is taken after the clean-up, and the app
ends whatever the compositor still holds when a new gesture's first finger lands (Android has
ended every earlier gesture by then). The compositor logs once per gesture whether the window
got touch or the pointer.
2026-09-25 15:55:26 -04:00

3687 lines
177 KiB
C

/*
* bannerlator-wayland - the embedded compositor of the Wayland display path.
*
* Clients are the Wine processes of a container (winewayland.drv) and, through them,
* Mesa's Vulkan WSI. Each process is its own client, so a Windows virtual desktop is
* described with banner_desktop_v1: the desktop owner marks one surface as the desktop,
* and every process reports its top-level windows' desktop positions and the Windows
* stacking order.
*
* The scene is the desktop surface at the bottom, then every mapped xdg_toplevel at its
* reported position in stacking order, each with its subsurface tree (a game's Vulkan
* swapchain is a subsurface of its window) and wp_viewport crop/scale applied. It is
* redrawn once per event-loop pass after any commit or layout change, stretched to the
* Android surface.
*
* Input goes to the desktop surface in desktop coordinates: the Wine server routes it to
* the window under the pointer (or the focus window) in whichever process owns it, the
* same as winex11's virtual desktop. Without a desktop (a client that doesn't speak
* banner_desktop_v1) the topmost window under the pointer gets it.
*/
#define _GNU_SOURCE 1
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdint.h>
#include <fcntl.h>
#include <time.h>
#include <stdarg.h>
#include <errno.h>
#include <sys/timerfd.h>
#include <sys/syscall.h>
#include <sys/sysmacros.h>
#include <android/log.h>
#include <wayland-server.h>
#include "xdg-shell-server-protocol.h"
#include "linux-dmabuf-v1-server-protocol.h"
#include "sc_layer.h"
#include "ahb_swapchain.h"
#include "viewporter-server-protocol.h"
#include "banner-desktop-v1-server-protocol.h"
#include "presentation-time-server-protocol.h"
#include "pointer-constraints-unstable-v1-server-protocol.h"
#include "relative-pointer-unstable-v1-server-protocol.h"
#include "vk_present.h"
#include "framegen_bridge.h"
#include <pthread.h>
#include "effects_chain.h"
#include "banner_ext.h"
#include "banner_color.h"
#define WLOGI(...) __android_log_print(ANDROID_LOG_INFO, "BannerWayland", __VA_ARGS__)
#define WLOGE(...) __android_log_print(ANDROID_LOG_ERROR, "BannerWayland", __VA_ARGS__)
/* ------------------------------------------------------------------ session log
* One readable, time-stamped file per Wayland session in Download/Wayland-logs, also
* mirrored to logcat: which programs connect, the desktop, windows opening and closing,
* Vulkan frames arriving, a frame-rate summary every 10 seconds, and errors. */
#define SESSION_LOG_DIR "/storage/emulated/0/Download/Wayland-logs"
static FILE *g_log;
/* Lines logged before the session file exists are kept here and written into it, in order, the
* moment it opens - the compositor runs on its own thread, so anything the app reports as that
* thread starts (the display's HDR capability, for one) would otherwise only reach logcat. The
* lock also serialises the file writes, which now come from the app's threads too. */
#define PRELOG_MAX 32
static pthread_mutex_t g_log_lock = PTHREAD_MUTEX_INITIALIZER;
static char *g_prelog[PRELOG_MAX];
static int g_prelog_n;
void banner_log(const char *tag, const char *fmt, ...) {
char msg[512];
va_list ap;
va_start(ap, fmt);
vsnprintf(msg, sizeof(msg), fmt, ap);
va_end(ap);
WLOGI("[%s] %s", tag, msg);
struct timespec ts;
struct tm tm;
clock_gettime(CLOCK_REALTIME, &ts);
localtime_r(&ts.tv_sec, &tm);
char line[640];
snprintf(line, sizeof(line), "%02d:%02d:%02d.%03ld %-9s %s\n", tm.tm_hour, tm.tm_min, tm.tm_sec,
ts.tv_nsec / 1000000, tag, msg);
pthread_mutex_lock(&g_log_lock);
if (g_log) fputs(line, g_log);
else if (g_prelog_n < PRELOG_MAX) g_prelog[g_prelog_n++] = strdup(line);
pthread_mutex_unlock(&g_log_lock);
}
static void open_session_log(void) {
char path[256], stamp[32];
time_t now = time(NULL);
struct tm tm;
FILE *f;
localtime_r(&now, &tm);
strftime(stamp, sizeof(stamp), "%Y-%m-%d_%H-%M-%S", &tm);
/* The app names the file when it has a session folder to put it in, so one session's logs sit
* together; without that we keep our own dated file. The folder is made on the Java side. */
const char *given = getenv("BL_WAYLAND_LOG");
if (given && *given) {
snprintf(path, sizeof(path), "%s", given);
} else {
mkdir("/storage/emulated/0/Download", 0775);
if (mkdir(SESSION_LOG_DIR, 0775) != 0 && errno != EEXIST)
WLOGE("can't create %s: %s", SESSION_LOG_DIR, strerror(errno));
snprintf(path, sizeof(path), "%s/wayland-%s.log", SESSION_LOG_DIR, stamp);
}
if (!(f = fopen(path, "w"))) {
WLOGE("can't open session log %s: %s", path, strerror(errno));
return;
}
setvbuf(f, NULL, _IOLBF, 0);
strftime(stamp, sizeof(stamp), "%Y-%m-%d %H:%M:%S", &tm);
fprintf(f,
"Bannerlator Wayland session\n"
"===========================\n"
"Started %s\n"
"Display Wayland: Windows programs draw through the Bannerlator compositor.\n"
" No X server is used for this session.\n"
"Log %s\n\n"
"Time Area Event\n"
"------------ -------- -----------------------------------------------------\n",
stamp, path);
/* Publish the file and flush anything logged before it existed, in one critical section, so a
* line from another thread can never land ahead of the header or be dropped between the two. */
pthread_mutex_lock(&g_log_lock);
for (int i = 0; i < g_prelog_n; i++) { fputs(g_prelog[i], f); free(g_prelog[i]); g_prelog[i] = NULL; }
g_prelog_n = 0;
g_log = f;
pthread_mutex_unlock(&g_log_lock);
WLOGI("session log: %s", path);
}
/* At most ~10 lines a second for chatty events (window moves), so a drag can't flood logcat. */
static int log_budget(void) {
static struct timespec window;
static int used;
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
if (now.tv_sec != window.tv_sec) { window = now; used = 0; }
return used++ < 10;
}
#ifndef BTN_LEFT
#define BTN_LEFT 0x110 /* linux/input-event-codes.h */
#endif
#ifndef BTN_RIGHT
#define BTN_RIGHT 0x111
#endif
/* Java sends pointer coordinates in this space, covering the whole output surface. */
#define INPUT_SPACE_W 1920
#define INPUT_SPACE_H 1080
static struct wl_display *g_display;
/* Which program each Wayland client is (from /proc/<pid>/cmdline), for the session log. */
struct client_info {
struct wl_client *client;
struct wl_listener destroy;
pid_t pid;
char name[64];
/* An OpenGL program whose EGL gave up on the GPU: it asked for dma-buf feedback (EGL's
* Wayland GPU path always does), never made a dma-buf buffer, and draws wl_shm frames. */
unsigned asked_feedback : 1, shm_gl_said : 1;
unsigned dmabuf_buffers, shm_frames;
struct client_info *next;
};
static struct client_info *g_clients;
static struct client_info *client_info_of(struct wl_client *client) {
for (struct client_info *ci = g_clients; ci; ci = ci->next)
if (ci->client == client) return ci;
return NULL;
}
static const char *client_name(struct wl_client *client) {
struct client_info *ci = client_info_of(client);
return ci ? ci->name : "a program";
}
static void on_client_destroyed(struct wl_listener *l, void *data) {
struct client_info *ci = wl_container_of(l, ci, destroy), **pp;
banner_log("program", "disconnected: %s (pid %d)", ci->name, (int)ci->pid);
banner_color_client_gone(ci->client); /* HDR summary for a program that presented HDR (gate open only) */
for (pp = &g_clients; *pp; pp = &(*pp)->next)
if (*pp == ci) { *pp = ci->next; break; }
free(ci);
}
static void on_client_created(struct wl_listener *l, void *data) {
struct wl_client *client = data;
struct client_info *ci = calloc(1, sizeof(*ci));
char path[64], buf[256];
uid_t uid; gid_t gid;
int fd;
ssize_t n;
if (!ci) return;
ci->client = client;
wl_client_get_credentials(client, &ci->pid, &uid, &gid);
snprintf(ci->name, sizeof(ci->name), "pid %d", (int)ci->pid);
snprintf(path, sizeof(path), "/proc/%d/cmdline", (int)ci->pid);
if ((fd = open(path, O_RDONLY | O_CLOEXEC)) >= 0) {
if ((n = read(fd, buf, sizeof(buf) - 1)) > 0) {
char *base = buf, *p;
buf[n] = 0; /* argv[0] only; Wine puts the Windows program path there */
for (p = buf; *p; p++) if (*p == '\\' || *p == '/') base = p + 1;
if (*base) snprintf(ci->name, sizeof(ci->name), "%s", base);
}
close(fd);
}
ci->destroy.notify = on_client_destroyed;
wl_client_add_destroy_listener(client, &ci->destroy);
ci->next = g_clients;
g_clients = ci;
banner_log("program", "connected over Wayland: %s (pid %d)", ci->name, (int)ci->pid);
}
/* ------------------------------------------------------------------ surfaces */
enum surface_role { ROLE_NONE, ROLE_TOPLEVEL, ROLE_SUBSURFACE, ROLE_DESKTOP, ROLE_CURSOR };
struct surface {
struct wl_resource *resource;
struct wl_list link; /* g_surfaces */
/* Pending state, applied on commit. */
struct wl_resource *pending_buffer;
struct wl_listener pending_buffer_destroy;
int pending_attach;
int pending_src_set, pending_dst_set;
float pending_src[4]; /* x, y, w, h; w < 0 = unset */
int pending_dst[2]; /* w, h; w < 0 = unset */
struct wl_list pending_frames;
struct wl_list pending_feedback; /* wp_presentation feedback asked for before commit */
/* Current content. */
struct vkp_image *shm_img; /* our copy of the last wl_shm buffer */
struct wl_resource *dmabuf; /* current dmabuf wl_buffer (NULL once the client destroyed it) */
struct dmabuf_buffer *dmabuf_buf; /* its imported image, kept until replaced or the surface goes */
struct wl_listener dmabuf_destroy;
int buf_w, buf_h, has_content;
int src_set, dst_set;
float src[4];
int dst[2];
struct wl_list frames; /* frame callbacks for the next redraw */
struct wl_list feedback; /* presentation feedback for the current content */
int drawn; /* part of the last rendered scene */
int64_t next_release_ns; /* FPS limiter: when this surface's last buffer goes back */
int releases_pending; /* FPS limiter: buffers of this surface still to be released */
enum surface_role role;
struct wl_resource *xdg_surface, *xdg_toplevel;
struct wl_resource *viewport;
/* Toplevel placement on the scene. */
int mapped; /* in g_toplevels */
struct wl_list toplevel_link;
int x, y, placed;
uint32_t hwnd;
/* Subsurface tree. */
struct surface *parent;
struct wl_resource *subsurface;
struct wl_list children; /* bottom to top */
struct wl_list child_link;
int sub_x, sub_y, sub_pending_x, sub_pending_y, sub_pending;
int below_parent;
int fullscreen; /* xdg_toplevel.set_fullscreen: no client decorations */
int toplevel_committed; /* the toplevel's initial commit was answered */
char *title; /* xdg_toplevel title, for the session log */
int announced_vulkan; /* logged its first dmabuf frame */
uint32_t hdr_fmt_logged; /* HDR session: the buffer format last named in the log */
};
static struct wl_list g_surfaces; /* every surface */
static struct wl_list g_toplevels; /* mapped toplevels, bottom to top */
static struct surface *g_desktop;
static uint32_t *g_zorder; /* last reported Windows stacking order, top first */
static size_t g_zorder_count;
static struct wl_event_source *g_render_idle, *g_frame_timer;
/* Rendering is driven by the screen's vsync (Java's Choreographer ticks, nativeVsync): one scene
* per refresh showing the newest buffers, so the event loop is never parked in the swapchain and
* clients get their buffers back as soon as they're replaced. Without ticks (an older app, no
* window) a fallback timer renders instead. */
static int g_dirty; /* something changed since the last render */
static int64_t g_last_vsync_ns; /* last tick, 0 = none yet */
static int64_t g_refresh_ns = 16666667; /* measured tick interval (pacing only, never reported) */
static struct wl_event_source *g_fallback_timer;
static int g_fallback_armed;
/* FPS limiter (the in-game drawer's): frames per second, 0 = unlimited. Set from the app thread,
* read here. Like the X11 path, which delays the "your buffer is free" notice, the limit paces
* when a replaced buffer is released to its client, so the game itself slows to the cap. */
volatile int g_fps_limit;
/* Shortcut launches: explorer's windows (the desktop, taskbar, Start menu) aren't drawn, matching
* the X11 renderer's unviewable "explorer.exe". They still exist for input routing. */
volatile int g_hide_shell;
/* Experimental layer mode (BANNER_WAYLAND_ZERO_COPY=1 in the container's environment): a single
* fullscreen window is shown on its own Android layer (sc_layer.c) instead of being blitted into the
* screen swapchain. Set from the app before the compositor starts; read per frame. */
volatile int g_zero_copy;
static int g_zero_copy_paused; /* effects hold the layer path off (render_scene) */
static int g_zero_copy_fx_skip_said;
/* Compressed (UBWC) game buffers: zwp_linux_dmabuf_v1 advertises DRM_FORMAT_MOD_QCOM_COMPRESSED next
* to LINEAR for every format the renderer's own driver can import that way, so Turnip's Wayland WSI
* in the game allocates UBWC swapchain images instead of resolving each frame to a linear copy.
* BANNER_WAYLAND_UBWC=0 in the container's environment turns the advertisement off (A/B switch). Set
* from the app before the compositor starts; read at every zwp_linux_dmabuf_v1 bind. */
volatile int g_ubwc = 1;
/* Debug (BANNER_WAYLAND_NO_RENDER_NODE=1): name no DRM device in the dma-buf feedback, as a phone
* that exposes no /dev/dri node to apps does, to reproduce its OpenGL path here. Set from the app
* before the compositor starts. */
volatile int g_no_render_node;
/* The panel's refresh rate in mHz, from the app; wl_output advertises it so Wine's display modes
* carry the real rate (games pick their saved 144 Hz mode, as on X11). 0 = 60 Hz. */
volatile int g_output_refresh_mhz;
/* The advertised output size: the container's screen (what the app's X server reports on X11),
* so Wine's display-mode list stops at the desktop size. 0 = 1920x1080. */
volatile int g_output_w, g_output_h;
struct pending_release {
struct wl_resource *buffer;
struct wl_listener destroy;
struct surface *surface; /* NULL once the surface is gone */
int64_t at_ns;
int64_t since_ns; /* when the compositor let go of it (the perf line) */
struct wl_list link;
};
static struct wl_list g_pending_releases;
static int g_release_timer_fd = -1;
static struct wl_event_source *g_release_source;
static int g_scene_w, g_scene_h; /* size of the last drawn scene */
static int g_desktop_w, g_desktop_h; /* last size the desktop had content at */
static unsigned g_stat_frames, g_stat_dmabuf, g_stat_shm; /* since the last 10 s summary */
/* The 10 s `perf` line (on_stats_timer): where the compositor thread's time goes and how long games
* wait for their buffers back. The driver-side times (acquire, present, fence waits) are counted in
* vk_present.c (vkp_perf_take). Compositor thread only. */
static struct {
unsigned ticks; /* screen refreshes (Choreographer ticks) */
unsigned scenes; /* render_scene() calls ... */
int64_t scene_ns, scene_max_ns; /* ... and their wall time */
unsigned copy_scenes; /* scenes drawn into the screen swapchain (the copy path) */
unsigned releases, releases_held; /* wl_buffer.release after a replacement; held = 1 ms or more */
int64_t release_ns, release_max_ns; /* replaced (or taken off the layer) -> released to the game */
} g_perf;
static void schedule_render(void);
static int64_t now_ns(void) {
struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts);
return (int64_t)ts.tv_sec * 1000000000LL + ts.tv_nsec;
}
/* A game got a replaced buffer back; since_ns = when the compositor let go of it (0 = not counted). */
static void perf_note_release(int64_t since_ns) {
if (since_ns <= 0) return;
int64_t d = now_ns() - since_ns;
if (d < 0) d = 0;
g_perf.releases++;
g_perf.release_ns += d;
if (d > g_perf.release_max_ns) g_perf.release_max_ns = d;
if (d >= 1000000LL) g_perf.releases_held++;
}
/* ------------------------------------------------------------------ buffer release pacing */
static void pending_release_free(struct pending_release *pr) {
if (pr->surface && pr->surface->releases_pending > 0) pr->surface->releases_pending--;
wl_list_remove(&pr->destroy.link);
wl_list_remove(&pr->link);
free(pr);
}
/* A surface is going away: its queued releases still go out on time, they just stop counting. */
static void pending_releases_forget_surface(struct surface *s) {
struct pending_release *pr;
wl_list_for_each(pr, &g_pending_releases, link)
if (pr->surface == s) pr->surface = NULL;
}
static void on_pending_release_buffer_destroyed(struct wl_listener *l, void *data) {
struct pending_release *pr = wl_container_of(l, pr, destroy);
pending_release_free(pr);
}
static void arm_release_timer(void) {
struct pending_release *pr;
int64_t earliest = 0;
if (g_release_timer_fd < 0) return;
wl_list_for_each(pr, &g_pending_releases, link)
if (!earliest || pr->at_ns < earliest) earliest = pr->at_ns;
struct itimerspec its = {0};
if (earliest) { its.it_value.tv_sec = earliest / 1000000000LL; its.it_value.tv_nsec = earliest % 1000000000LL; }
timerfd_settime(g_release_timer_fd, TFD_TIMER_ABSTIME, &its, NULL);
}
static int on_release_timer(int fd, uint32_t mask, void *data) {
struct pending_release *pr, *tmp;
uint64_t expirations;
int64_t now = now_ns() + 300000; /* anything due within 0.3 ms goes now */
if (read(fd, &expirations, sizeof(expirations)) < 0 && errno != EAGAIN) return 0;
wl_list_for_each_safe(pr, tmp, &g_pending_releases, link) {
if (pr->at_ns <= now) {
wl_buffer_send_release(pr->buffer);
perf_note_release(pr->since_ns);
pending_release_free(pr);
}
}
wl_display_flush_clients(g_display);
arm_release_timer();
return 0;
}
/* Give a replaced buffer back to its client: now, or on the limiter's cadence.
*
* Each release takes the next slot of a per-surface cadence (one slot per interval), so a game
* gets one buffer back per interval however fast it commits. The schedule is kept honest at both
* ends: a slot in the past is brought to now (the game was slower than the cap, no catch-up burst
* is owed), and it never runs further ahead than the releases actually queued - with p releases
* still pending the new one lands at most (p + 1) intervals out, nothing pending means at most
* one interval. Without that bound the schedule kept slots that were consumed but never
* delivered (a swapchain rebuild destroys buffers with queued releases) or that a coarser earlier
* limit had spaced out, and the game's first frames after that waited on empty slots. */
static void release_buffer(struct surface *s, struct wl_resource *buffer, int64_t since_ns) {
int limit = g_fps_limit;
if (limit <= 0 || g_release_timer_fd < 0) { wl_buffer_send_release(buffer); perf_note_release(since_ns); return; }
int64_t interval = 1000000000LL / limit, now = now_ns();
int64_t at = s->next_release_ns + interval;
int64_t latest = now + interval * (int64_t)(s->releases_pending + 1);
if (at < now) at = now;
if (at > latest) at = latest;
s->next_release_ns = at;
struct pending_release *pr = calloc(1, sizeof(*pr));
if (!pr) { wl_buffer_send_release(buffer); perf_note_release(since_ns); return; }
pr->buffer = buffer;
pr->surface = s;
pr->at_ns = at;
pr->since_ns = since_ns;
pr->destroy.notify = on_pending_release_buffer_destroyed;
wl_resource_add_destroy_listener(buffer, &pr->destroy);
wl_list_insert(g_pending_releases.prev, &pr->link);
s->releases_pending++;
arm_release_timer();
}
/* ------------------------------------------------------------------ seat state */
/* Each Wine process is a separate client with its own wl_pointer / wl_keyboard. */
struct seat_pointer { struct wl_resource *ptr; struct wl_resource *focus; };
struct seat_keyboard { struct wl_resource *kb; struct wl_resource *focus; };
struct seat_touch { struct wl_resource *touch; struct wl_resource *focus; };
#define MAX_PTRS 32
#define MAX_TOUCH_POINTS 32
static struct seat_pointer g_ptrs[MAX_PTRS];
static int g_nptrs;
static struct seat_keyboard g_kbs[MAX_PTRS];
static int g_nkbs;
static struct seat_touch g_touches[MAX_PTRS];
static int g_ntouches;
/* ---- the client's cursor (wl_pointer.set_cursor) -------------------------------------------
* Not composited: under zero-copy the program's buffer goes straight to an Android layer and there
* is nothing to composite into. The cursor surface is snapshotted here instead and the app draws it
* as its overlay, above every present path (Bannerlator's design). What it buys: the real SHAPE -
* labwc's resize arrows at a window's edges and corners, a text field's I-beam - and an
* authoritative HIDE (set_cursor with a NULL surface, what a mouse-look game does).
* wl_shm cursors (Wine, pixman) are copied directly; a GPU cursor (labwc on Vulkan/GLES, which
* wlroots hands over as a dma-buf, usually UBWC) is read back through vkp_image_readback. */
#define CURSOR_MAX_PX (256 * 256)
static pthread_mutex_t g_cursor_lock = PTHREAD_MUTEX_INITIALIZER;
static struct surface *g_cursor_surface; /* compositor thread only */
static int g_cursor_hx, g_cursor_hy; /* hotspot, surface-local */
static uint32_t g_cursor_px[CURSOR_MAX_PX]; /* guarded by g_cursor_lock: ARGB8888 snapshot */
static uint32_t g_cursor_rb[CURSOR_MAX_PX]; /* compositor thread: GPU readback staging */
static int g_cursor_w, g_cursor_h; /* 0 = nothing to draw */
static int g_cursor_hidden = 1; /* the client asked for no pointer */
static int g_cursor_serial; /* bumped on every change; the app polls it */
static void cursor_publish_hidden(void) {
pthread_mutex_lock(&g_cursor_lock);
if (!g_cursor_hidden) { g_cursor_hidden = 1; g_cursor_serial++; }
pthread_mutex_unlock(&g_cursor_lock);
}
static void cursor_publish_pixels(const uint8_t *src, int w, int h, size_t stride, int hx, int hy) {
if (w <= 0 || h <= 0 || w * h > CURSOR_MAX_PX) return;
pthread_mutex_lock(&g_cursor_lock);
for (int y = 0; y < h; y++)
memcpy(&g_cursor_px[y * w], src + (size_t)y * stride, (size_t)w * 4);
g_cursor_w = w; g_cursor_h = h;
g_cursor_hx = hx; g_cursor_hy = hy;
g_cursor_hidden = 0;
g_cursor_serial++;
pthread_mutex_unlock(&g_cursor_lock);
}
static void cursor_publish_shm(struct wl_shm_buffer *shm, int hx, int hy) {
int32_t w = wl_shm_buffer_get_width(shm), h = wl_shm_buffer_get_height(shm);
if (w <= 0 || h <= 0 || w * h > CURSOR_MAX_PX) return;
wl_shm_buffer_begin_access(shm);
cursor_publish_pixels((const uint8_t *)wl_shm_buffer_get_data(shm), w, h,
(size_t)wl_shm_buffer_get_stride(shm), hx, hy);
wl_shm_buffer_end_access(shm);
}
/* Read by the app (UI thread). out = [serial, hidden, w, h, hotspotX, hotspotY, pixels...].
* Returns the number of ints written, or 0 if out is too small. */
int banner_cursor_snapshot(int *out, int cap) {
int n = 0;
pthread_mutex_lock(&g_cursor_lock);
int need = 6 + (g_cursor_hidden ? 0 : g_cursor_w * g_cursor_h);
if (cap >= need) {
out[0] = g_cursor_serial; out[1] = g_cursor_hidden;
out[2] = g_cursor_w; out[3] = g_cursor_h;
out[4] = g_cursor_hx; out[5] = g_cursor_hy;
if (!g_cursor_hidden)
memcpy(out + 6, g_cursor_px, (size_t)g_cursor_w * g_cursor_h * 4);
n = need;
}
pthread_mutex_unlock(&g_cursor_lock);
return n;
}
struct active_touch { struct surface *surface; int pointer_fallback; };
static struct active_touch g_touch_points[MAX_TOUCH_POINTS];
static void touch_cancel_surface(struct surface *surface);
static struct surface *g_grab; /* no-desktop fallback: surface holding the button */
static struct surface *g_key_target; /* no-desktop fallback: last clicked surface */
static struct surface *g_ime_click; /* last clicked program window: where text input goes */
static int g_input_pipe[2] = {-1, -1};
struct input_msg { int type; int p1; int p2; int p3; int p4; };
/* Pointer constraints (zwp_pointer_constraints_v1) and relative pointers
* (zwp_relative_pointer_manager_v1); see the "pointer constraints" section. */
struct surface;
static void constraints_surface_gone(struct surface *s);
static void constraints_surface_commit(struct surface *s);
static void constraints_pointer_gone(struct wl_resource *pointer);
static void relative_pointers_pointer_gone(struct wl_resource *pointer);
static void constraints_focus_entered(struct wl_resource *target, struct wl_client *client);
/* ------------------------------------------------------------------ dmabuf buffers */
#define FOURCC(a, b, c, d) \
((uint32_t)(a) | ((uint32_t)(b) << 8) | ((uint32_t)(c) << 16) | ((uint32_t)(d) << 24))
#define DRM_ARGB8888 FOURCC('A', 'R', '2', '4')
#define DRM_XRGB8888 FOURCC('X', 'R', '2', '4')
#define DRM_ABGR8888 FOURCC('A', 'B', '2', '4')
#define DRM_XBGR8888 FOURCC('X', 'B', '2', '4')
#define MOD_LINEAR VKP_MOD_LINEAR
#define MOD_INVALID VKP_MOD_INVALID
#define MAX_PLANES 4
struct dmabuf_params {
int fd[MAX_PLANES];
uint32_t offset[MAX_PLANES], stride[MAX_PLANES];
uint64_t modifier[MAX_PLANES];
int n_planes;
};
struct dmabuf_buffer {
int fd[MAX_PLANES];
uint32_t offset[MAX_PLANES], stride[MAX_PLANES];
int n_planes;
int32_t width, height;
uint32_t format;
uint64_t modifier;
struct vkp_image *img; /* imported once, reused for every frame */
int import_failed;
/* One reference for the wl_buffer resource, one per surface showing the buffer. Mesa destroys
* a swapchain's wl_buffers the moment the game rebuilds its swapchain, i.e. while the last
* committed one is still what is on screen: the import (and the dma-buf memory it pins) stays
* until the surface commits something newer, so the picture never blinks to black. */
int refs;
void *ahb_state; /* zero-copy: ahb_swapchain.c's record (the game's AHardwareBuffer) */
};
static void dmabuf_buffer_unref(struct dmabuf_buffer *b) {
if (!b || --b->refs > 0) return;
vkp_image_destroy(b->img);
for (int i = 0; i < b->n_planes; i++)
if (b->fd[i] >= 0) close(b->fd[i]);
free(b);
}
static void dbuf_buffer_destroy_req(struct wl_client *c, struct wl_resource *r) {
wl_resource_destroy(r);
}
static const struct wl_buffer_interface dbuf_buffer_impl = {
.destroy = dbuf_buffer_destroy_req,
};
static struct dmabuf_buffer *get_dmabuf(struct wl_resource *buffer) {
if (buffer && wl_resource_instance_of(buffer, &wl_buffer_interface, &dbuf_buffer_impl))
return wl_resource_get_user_data(buffer);
return NULL;
}
/* ------------------------------------------------------------------ surface state */
static void surface_size(const struct surface *s, int *w, int *h) {
if (s->dst_set) { *w = s->dst[0]; *h = s->dst[1]; }
else if (s->src_set) { *w = (int)(s->src[2] + 0.5f); *h = (int)(s->src[3] + 0.5f); }
else { *w = s->buf_w; *h = s->buf_h; }
}
/* "Title" (program) for the log; subsurfaces are named after the window they belong to. */
static void describe(const struct surface *s, char *out, size_t size) {
const struct surface *w = s;
while (w->parent) w = w->parent;
const char *prog = client_name(wl_resource_get_client(w->resource));
if (w->title && *w->title) snprintf(out, size, "\"%s\" (%s)", w->title, prog);
else if (w->role == ROLE_DESKTOP) snprintf(out, size, "the desktop (%s)", prog);
else snprintf(out, size, "window %#x (%s)", w->hwnd, prog);
}
static struct surface *toplevel_by_hwnd(uint32_t hwnd) {
struct surface *s;
wl_list_for_each(s, &g_toplevels, toplevel_link)
if (s->hwnd == hwnd) return s;
return NULL;
}
/* Reorder the mapped toplevels to match the last reported Windows z-order. Windows the
* report doesn't mention keep their relative order below the ones it does. */
static void apply_zorder(void) {
for (size_t i = g_zorder_count; i-- > 0;) {
struct surface *s = toplevel_by_hwnd(g_zorder[i]);
if (!s) continue;
wl_list_remove(&s->toplevel_link);
wl_list_insert(g_toplevels.prev, &s->toplevel_link);
}
}
static void map_toplevel(struct surface *s) {
int w, h;
if (s->mapped) return;
s->mapped = 1;
surface_size(s, &w, &h);
{
char name[160];
describe(s, name, sizeof(name));
banner_log("window", "opened %s %dx%d at %d,%d%s", name, w, h, s->x, s->y,
s->placed ? "" : " (no desktop position yet)");
}
wl_list_insert(g_toplevels.prev, &s->toplevel_link); /* new windows start on top */
/* Without a Wine desktop the clients are whole programs (the Linux desktop, a gamescope a
* program on it started over it): a window that opens on top takes the keys too, as it would
* on any desktop, rather than the one clicked last that is now underneath it. */
if (!g_desktop) g_key_target = s;
apply_zorder();
}
static void unmap_toplevel(struct surface *s) {
if (!s->mapped) return;
s->mapped = 0;
{
char name[160];
describe(s, name, sizeof(name));
banner_log("window", "closed %s", name);
}
wl_list_remove(&s->toplevel_link);
wl_list_init(&s->toplevel_link);
if (g_ime_click == s) g_ime_click = NULL;
banner_text_input_refocus();
}
/* Let go of the surface's dmabuf content. paced = 1: the buffer was replaced, give it back on the
* limiter's cadence; paced = 0: the surface is going away, give it back at once (a buffer of a
* destroyed surface is not shown again and must not sit unreleased with the client). */
static void drop_dmabuf(struct surface *s, int paced) {
if (s->dmabuf) {
wl_list_remove(&s->dmabuf_destroy.link);
/* A buffer on the zero-copy layer is the display's until SurfaceFlinger says otherwise:
* ahb_swapchain.c releases it then. */
if (!ahb_swapchain_defer_release(s->dmabuf_buf, s->dmabuf, s, paced)) {
if (paced) release_buffer(s, s->dmabuf, now_ns());
else wl_buffer_send_release(s->dmabuf);
}
s->dmabuf = NULL;
}
dmabuf_buffer_unref(s->dmabuf_buf);
s->dmabuf_buf = NULL;
}
/* The client destroyed the wl_buffer we are showing (a swapchain rebuild): keep showing its
* image (s->dmabuf_buf holds it) until the next commit replaces it; only the resource is gone. */
static void on_dmabuf_destroyed(struct wl_listener *l, void *data) {
struct surface *s = wl_container_of(l, s, dmabuf_destroy);
wl_list_remove(&s->dmabuf_destroy.link);
s->dmabuf = NULL;
}
static void on_pending_buffer_destroyed(struct wl_listener *l, void *data) {
struct surface *s = wl_container_of(l, s, pending_buffer_destroy);
wl_list_remove(&s->pending_buffer_destroy.link);
wl_list_init(&s->pending_buffer_destroy.link);
s->pending_buffer = NULL;
}
static void fire_frames(struct wl_list *frames) {
struct wl_resource *cb, *tmp;
uint32_t t;
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
t = (uint32_t)(ts.tv_sec * 1000 + ts.tv_nsec / 1000000);
wl_resource_for_each_safe(cb, tmp, frames) {
wl_callback_send_done(cb, t);
wl_resource_destroy(cb);
}
}
/* Copy a committed wl_shm buffer into the surface's image and release the buffer. */
static void take_shm(struct surface *s, struct wl_shm_buffer *shm, struct wl_resource *buffer) {
int32_t w = wl_shm_buffer_get_width(shm), h = wl_shm_buffer_get_height(shm);
int32_t stride = wl_shm_buffer_get_stride(shm);
if (s->shm_img && (vkp_image_width(s->shm_img) != w || vkp_image_height(s->shm_img) != h)) {
vkp_image_destroy(s->shm_img);
s->shm_img = NULL;
}
if (!s->shm_img) s->shm_img = vkp_image_create_shm(w, h);
if (s->shm_img) {
wl_shm_buffer_begin_access(shm);
vkp_image_upload_shm(s->shm_img, wl_shm_buffer_get_data(shm), stride);
wl_shm_buffer_end_access(shm);
}
wl_buffer_send_release(buffer);
s->buf_w = w;
s->buf_h = h;
s->has_content = s->shm_img != NULL;
g_stat_shm++;
struct client_info *ci = client_info_of(wl_resource_get_client(s->resource));
if (ci && ci->asked_feedback) ci->shm_frames++; /* since it asked for GPU buffers */
}
/* The window the app's performance HUD follows: the latest one at least as big as the last to
* start presenting GPU frames (take_dmabuf) (X11 binds the HUD to the _MESA_DRV window and counts X presents instead). JNI upcalls. */
static struct surface *g_hud_surface;
/* When the HUD's window last presented a frame (now_ns), to let a replacement take over. */
static int64_t g_hud_last_ns;
extern void banner_on_game_surface(const char *window, const char *gpu); /* window NULL = gone */
extern void banner_on_game_frame(void);
/* The program behind that window: its Linux pid (the Wayland client's credentials) and executable name
* ("" when /proc gave none) - the app arms its CPU affinity on it (X11 does that from window events). */
extern void banner_on_game_program(int pid, const char *program);
static void take_dmabuf(struct surface *s, struct dmabuf_buffer *b, struct wl_resource *buffer) {
if (s->dmabuf != buffer || s->dmabuf_buf != b) {
drop_dmabuf(s, 1);
s->dmabuf = buffer;
s->dmabuf_buf = b;
b->refs++;
s->dmabuf_destroy.notify = on_dmabuf_destroyed;
wl_resource_add_destroy_listener(buffer, &s->dmabuf_destroy);
}
if (!b->img && !b->import_failed && b->n_planes >= 1) {
b->img = vkp_image_from_dmabuf(b->fd[0], b->format, b->modifier, b->width, b->height,
b->stride[0], b->offset[0]);
if (b->img && g_zero_copy) sc_layer_probe_dmabuf_fd(b->fd[0]);
if (!b->img) {
b->import_failed = 1;
WLOGE("dmabuf import failed (%dx%d fmt=0x%08x mod=0x%llx)", b->width, b->height,
b->format, (unsigned long long)b->modifier);
}
}
if (s->shm_img) { vkp_image_destroy(s->shm_img); s->shm_img = NULL; }
/* The size of the frames a window commits, when it changes after the first announcement: the
* program rebuilt its swapchain. It earns a line of its own, because a game whose swapchain
* came back smaller than it was is exactly what a picture suddenly filling one corner of the
* screen looks like from here - and the two sizes are the whole of the evidence. Rare by
* nature, so there is nothing to rate-limit. */
if (s->announced_vulkan && s->buf_w && s->buf_h &&
(s->buf_w != b->width || s->buf_h != b->height)) {
char name[160];
describe(s, name, sizeof(name));
banner_log("vulkan", "%s changed its frame size: %dx%d -> %dx%d", name,
s->buf_w, s->buf_h, b->width, b->height);
}
s->buf_w = b->width;
s->buf_h = b->height;
s->has_content = b->img != NULL;
g_stat_dmabuf++;
const int announced_now = !s->announced_vulkan;
if (!s->announced_vulkan) {
char name[160];
s->announced_vulkan = 1;
describe(s, name, sizeof(name));
if (b->img)
/* Imported for the copy path. Whether its frames ALSO go on the display layer without a copy
* is decided per frame (fullscreen, zero-copy on) and counted in the 10 s lines. */
banner_log("vulkan", "%s is presenting GPU frames through Wayland: %dx%d, format %c%c%c%c, %s (%s)",
name, b->width, b->height, b->format & 0xff, (b->format >> 8) & 0xff,
(b->format >> 16) & 0xff, (b->format >> 24) & 0xff,
vkp_modifier_name(b->modifier),
ahb_swapchain_has_ahb(b) ? "gralloc buffers: can go on the display layer without a copy"
: "dma-buf: copied into the screen swapchain");
else if (ahb_swapchain_has_ahb(b))
banner_log("vulkan", "%s is presenting GPU frames through Wayland: %dx%d on its own display layer only "
"(gralloc buffers the compositor cannot import for the copy path)",
name, b->width, b->height);
else
banner_log("error", "could not import GPU frames from %s (%dx%d, modifier %#llx)",
name, b->width, b->height, (unsigned long long)b->modifier);
}
/* The HUD follows the window whether its frames are copied or go straight to the layer: a
* zero-copy frame the compositor never imported is still a presented game frame. A
* program's small surfaces - gamescope's 1x1 cursor, Qt's menus and tooltips (melonDS) -
* came after the game's and drew too rarely to count, which left the HUD on 0 fps while the
* game ran at 60. So a new window takes the HUD only when it is at least as big (a
* full-screen gamescope over the desktop still does), and a window gone quiet for a second
* gives way to whatever draws next. */
if (s != g_hud_surface && (b->img || ahb_swapchain_has_ahb(b)) &&
(!g_hud_surface || now_ns() - g_hud_last_ns > 1000000000LL ||
(announced_now && (int64_t)b->width * b->height >= (int64_t)g_hud_surface->buf_w * g_hud_surface->buf_h))) {
char name[160];
describe(s, name, sizeof(name));
g_hud_surface = s;
banner_on_game_surface(name, vkp_gpu_name());
struct client_info *ci = client_info_of(wl_resource_get_client(s->resource));
if (ci) banner_on_game_program((int)ci->pid, strncmp(ci->name, "pid ", 4) ? ci->name : "");
}
/* HDR session (gate open): a DXVK game switches to HDR by REBUILDING its swapchain on the same
* surface, so the one-shot line above never sees the 10-bit buffers - name every format change. */
if (b->format != s->hdr_fmt_logged && banner_color_hdr_open()) {
char name[160];
uint32_t af = ahb_swapchain_ahb_format(b);
int ten = b->format == FOURCC('A', 'B', '3', '0') || b->format == FOURCC('X', 'B', '3', '0');
describe(s, name, sizeof(name));
banner_log("color", "%s presents %dx%d buffers in %c%c%c%c (%s), %s%s%s; %s", name, b->width, b->height,
b->format & 0xff, (b->format >> 8) & 0xff, (b->format >> 16) & 0xff, (b->format >> 24) & 0xff,
ten ? "10-bit A2B10G10R10" : "8-bit", vkp_modifier_name(b->modifier),
af ? ", gralloc " : ", no gralloc buffer (copy path only)", af ? banner_ahb_format_name(af) : "",
b->img ? "the compositor imported it"
: af ? "the compositor's driver could NOT import it (display layer only, fullscreen)"
: "the compositor's driver could NOT import it (nothing can show it)");
s->hdr_fmt_logged = b->format;
}
/* Every buffer that reaches take_dmabuf is a presented GPU frame, so the HUD counts it.
* Asking whether the compositor could import it, as this used to, is a question about the
* copy path and not about whether a frame happened: a game whose buffers go straight to
* the display layer draws on screen while the counter sat at 0.0 fps and 1000.0 ms. */
if (s == g_hud_surface) { g_hud_last_ns = now_ns(); banner_on_game_frame(); }
}
/* ---- hooks for ahb_swapchain.c (zero-copy layers) */
struct dmabuf_buffer *banner_dmabuf_from_resource(struct wl_resource *buffer) { return get_dmabuf(buffer); }
int banner_dmabuf_fd(const struct dmabuf_buffer *b) { return b && b->n_planes > 0 ? b->fd[0] : -1; }
void banner_dmabuf_size(const struct dmabuf_buffer *b, int *w, int *h) { *w = b->width; *h = b->height; }
void **banner_dmabuf_ahb_slot(struct dmabuf_buffer *b) { return &b->ahb_state; }
void banner_dmabuf_ref(struct dmabuf_buffer *b) { b->refs++; }
void banner_dmabuf_unref(struct dmabuf_buffer *b) { dmabuf_buffer_unref(b); }
void banner_release_buffer(struct surface *s, struct wl_resource *buffer, int paced, int64_t since_ns) {
if (paced && s) release_buffer(s, buffer, since_ns);
else { wl_buffer_send_release(buffer); perf_note_release(since_ns); }
}
void banner_surface_describe(const struct surface *s, char *out, size_t size) { describe(s, out, size); }
const struct banner_color *banner_surface_color(const struct surface *s) {
return s ? banner_color_of(s->resource) : NULL;
}
/* ------------------------------------------------------------------ wl_surface */
static void surface_destroy(struct wl_client *c, struct wl_resource *r) {
wl_resource_destroy(r);
}
static void surface_attach(struct wl_client *c, struct wl_resource *r,
struct wl_resource *buffer, int32_t x, int32_t y) {
struct surface *s = wl_resource_get_user_data(r);
if (s->pending_buffer) wl_list_remove(&s->pending_buffer_destroy.link);
wl_list_init(&s->pending_buffer_destroy.link);
s->pending_buffer = buffer;
s->pending_attach = 1;
if (buffer) {
s->pending_buffer_destroy.notify = on_pending_buffer_destroyed;
wl_resource_add_destroy_listener(buffer, &s->pending_buffer_destroy);
}
}
static void surface_damage(struct wl_client *c, struct wl_resource *r,
int32_t x, int32_t y, int32_t w, int32_t h) {}
static void frame_callback_destroy(struct wl_resource *r) {
wl_list_remove(wl_resource_get_link(r));
}
/* wp_presentation: tells Mesa's Vulkan driver when a frame reached the screen or was replaced
* before it did. Without it the driver waits for a frame callback (one per refresh) for every
* frame, which paced games to the screen rate even in mailbox mode. */
static void feedback_resource_destroy(struct wl_resource *r) {
wl_list_remove(wl_resource_get_link(r));
}
static void feedback_discard_all(struct wl_list *list) {
struct wl_resource *fb, *tmp;
wl_resource_for_each_safe(fb, tmp, list) {
wp_presentation_feedback_send_discarded(fb);
wl_resource_destroy(fb);
}
}
/* The current mode's refresh interval in ns, from the rate the app read off the display. */
static uint32_t nominal_refresh_ns(void) {
int mhz = g_output_refresh_mhz;
if (mhz <= 0) mhz = 60000;
return (uint32_t)(1000000000000LL / (int64_t)mhz);
}
static void feedback_present_all(struct wl_list *list, int64_t t) {
struct wl_resource *fb, *tmp;
uint64_t sec = (uint64_t)(t / 1000000000LL);
uint32_t nsec = (uint32_t)(t % 1000000000LL);
wl_resource_for_each_safe(fb, tmp, list) {
/* The protocol's "refresh" is the NOMINAL interval of the current mode, a constant - not
* the interval this frame happened to take. Reporting the measured average instead makes a
* variable-refresh panel look like a mode change on every frame: gamescope re-derives its
* pacing each time ("Changed refresh to: 119.2 ... 121.4hz", 826 times in a minute on a
* Galaxy Z Fold, none on a fixed-refresh device). Report the panel's rate, which the app
* gives us and wl_output already advertises. */
wp_presentation_feedback_send_presented(fb, (uint32_t)(sec >> 32), (uint32_t)sec, nsec,
nominal_refresh_ns(), 0, 0,
WP_PRESENTATION_FEEDBACK_KIND_VSYNC);
wl_resource_destroy(fb);
}
}
static void presentation_destroy(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); }
static void presentation_feedback(struct wl_client *c, struct wl_resource *r,
struct wl_resource *surface, uint32_t id) {
struct surface *s = wl_resource_get_user_data(surface);
struct wl_resource *fb = wl_resource_create(c, &wp_presentation_feedback_interface,
wl_resource_get_version(r), id);
if (!fb) { wl_client_post_no_memory(c); return; }
wl_resource_set_implementation(fb, NULL, NULL, feedback_resource_destroy);
if (!s) { wp_presentation_feedback_send_discarded(fb); wl_resource_destroy(fb); return; }
wl_list_insert(s->pending_feedback.prev, wl_resource_get_link(fb));
}
static const struct wp_presentation_interface presentation_impl = {
.destroy = presentation_destroy,
.feedback = presentation_feedback,
};
static void bind_presentation(struct wl_client *c, void *data, uint32_t ver, uint32_t id) {
struct wl_resource *r = wl_resource_create(c, &wp_presentation_interface, ver, id);
if (!r) { wl_client_post_no_memory(c); return; }
wl_resource_set_implementation(r, &presentation_impl, NULL, NULL);
wp_presentation_send_clock_id(r, CLOCK_MONOTONIC);
}
static void surface_frame(struct wl_client *c, struct wl_resource *r, uint32_t cb) {
struct surface *s = wl_resource_get_user_data(r);
struct wl_resource *callback = wl_resource_create(c, &wl_callback_interface, 1, cb);
if (!callback) { wl_client_post_no_memory(c); return; }
wl_resource_set_implementation(callback, NULL, NULL, frame_callback_destroy);
wl_list_insert(s->pending_frames.prev, wl_resource_get_link(callback));
}
static void surface_set_opaque(struct wl_client *c, struct wl_resource *r,
struct wl_resource *region) {}
static void surface_set_input(struct wl_client *c, struct wl_resource *r,
struct wl_resource *region) {}
static void send_toplevel_configure(struct surface *s);
static void surface_commit(struct wl_client *c, struct wl_resource *r) {
struct surface *s = wl_resource_get_user_data(r);
struct surface *child;
/* xdg-shell: the initial commit of a toplevel is answered with a configure. One is also sent
* at get_toplevel, which Wine's driver has always had; a client that waits for the reply to its
* commit on a queue of its own (wlroots' Wayland backend - the desktop's labwc) can read that
* early one onto the wrong queue and would wait for ever. */
if (s->xdg_toplevel && !s->toplevel_committed) {
s->toplevel_committed = 1;
send_toplevel_configure(s);
}
if (s->pending_src_set) {
s->src_set = s->pending_src[2] > 0;
memcpy(s->src, s->pending_src, sizeof(s->src));
s->pending_src_set = 0;
}
if (s->pending_dst_set) {
s->dst_set = s->pending_dst[0] > 0;
memcpy(s->dst, s->pending_dst, sizeof(s->dst));
s->pending_dst_set = 0;
}
if (s->pending_attach) {
struct wl_resource *buffer = s->pending_buffer;
struct dmabuf_buffer *db = get_dmabuf(buffer);
struct wl_shm_buffer *shm = buffer && !db ? wl_shm_buffer_get(buffer) : NULL;
/* The previous content is replaced before reaching the screen. */
feedback_discard_all(&s->feedback);
if (buffer) {
wl_list_remove(&s->pending_buffer_destroy.link);
wl_list_init(&s->pending_buffer_destroy.link);
}
s->pending_buffer = NULL;
s->pending_attach = 0;
if (s->role == ROLE_CURSOR) {
/* The client's pointer image, copied out for the app's overlay (see cursor_publish_*)
* rather than composited, so it survives the zero-copy and HDR layer paths. */
if (!buffer) {
cursor_publish_hidden(); /* wlroots clears its cursor surface to hide the pointer */
} else if (shm) {
cursor_publish_shm(shm, g_cursor_hx, g_cursor_hy);
} else if (db && db->n_planes >= 1 && db->width * db->height <= CURSOR_MAX_PX) {
if (!db->img && !db->import_failed) {
db->img = vkp_image_from_dmabuf(db->fd[0], db->format, db->modifier, db->width, db->height,
db->stride[0], db->offset[0]);
if (!db->img) db->import_failed = 1;
}
if (db->img && vkp_image_readback(db->img, g_cursor_rb, CURSOR_MAX_PX) == 0)
cursor_publish_pixels((const uint8_t *)g_cursor_rb, db->width, db->height,
(size_t)db->width * 4, g_cursor_hx, g_cursor_hy);
}
if (buffer) wl_buffer_send_release(buffer);
} else if (s->role == ROLE_NONE) {
/* Role-less surface: never drawn. */
if (buffer) wl_buffer_send_release(buffer);
} else if (db) {
take_dmabuf(s, db, buffer);
} else if (shm) {
drop_dmabuf(s, 1);
take_shm(s, shm, buffer);
} else {
drop_dmabuf(s, 1);
s->has_content = 0;
if (buffer) wl_buffer_send_release(buffer);
}
}
/* Subsurface positions are applied when the parent commits. */
wl_list_for_each(child, &s->children, child_link) {
if (child->sub_pending) {
child->sub_x = child->sub_pending_x;
child->sub_y = child->sub_pending_y;
child->sub_pending = 0;
}
}
wl_list_insert_list(s->frames.prev, &s->pending_frames);
wl_list_init(&s->pending_frames);
wl_list_insert_list(s->feedback.prev, &s->pending_feedback);
wl_list_init(&s->pending_feedback);
constraints_surface_commit(s);
banner_color_commit(s->resource); /* wp_color_management_surface_v1 state (returns at once when HDR is off) */
if (s->role == ROLE_TOPLEVEL && s->xdg_toplevel) {
if (s->has_content) map_toplevel(s);
else unmap_toplevel(s);
}
schedule_render();
}
static void surface_set_buffer_transform(struct wl_client *c, struct wl_resource *r, int32_t t) {}
static void surface_set_buffer_scale(struct wl_client *c, struct wl_resource *r, int32_t s) {}
static void surface_damage_buffer(struct wl_client *c, struct wl_resource *r,
int32_t x, int32_t y, int32_t w, int32_t h) {}
static void surface_offset(struct wl_client *c, struct wl_resource *r, int32_t x, int32_t y) {}
static const struct wl_surface_interface surface_impl = {
.destroy = surface_destroy,
.attach = surface_attach,
.damage = surface_damage,
.frame = surface_frame,
.set_opaque_region = surface_set_opaque,
.set_input_region = surface_set_input,
.commit = surface_commit,
.set_buffer_transform = surface_set_buffer_transform,
.set_buffer_scale = surface_set_buffer_scale,
.damage_buffer = surface_damage_buffer,
.offset = surface_offset,
};
static void detach_from_parent(struct surface *s) {
if (!s->parent) return;
wl_list_remove(&s->child_link);
wl_list_init(&s->child_link);
s->parent = NULL;
}
static void surface_resource_destroy(struct wl_resource *r) {
struct surface *s = wl_resource_get_user_data(r);
struct surface *child, *tmp;
struct wl_resource *cb, *cbtmp;
if (!s) return;
for (int i = 0; i < g_nptrs; i++) if (g_ptrs[i].focus == r) g_ptrs[i].focus = NULL;
for (int i = 0; i < g_nkbs; i++) if (g_kbs[i].focus == r) g_kbs[i].focus = NULL;
touch_cancel_surface(s);
if (g_cursor_surface == s) { g_cursor_surface = NULL; cursor_publish_hidden(); }
if (g_grab == s) g_grab = NULL;
if (g_key_target == s) g_key_target = NULL;
if (g_ime_click == s) g_ime_click = NULL;
banner_text_input_surface_gone(r);
if (g_desktop == s) { g_desktop = NULL; banner_log("desktop", "the desktop closed"); }
if (g_hud_surface == s) { g_hud_surface = NULL; banner_on_game_surface(NULL, NULL); }
constraints_surface_gone(s);
unmap_toplevel(s);
detach_from_parent(s);
wl_list_for_each_safe(child, tmp, &s->children, child_link) {
wl_list_remove(&child->child_link);
wl_list_init(&child->child_link);
child->parent = NULL;
}
if (s->pending_buffer) wl_list_remove(&s->pending_buffer_destroy.link);
pending_releases_forget_surface(s);
drop_dmabuf(s, 0);
ahb_swapchain_surface_gone(s);
vkp_image_destroy(s->shm_img);
wl_resource_for_each_safe(cb, cbtmp, &s->pending_frames) wl_resource_destroy(cb);
wl_resource_for_each_safe(cb, cbtmp, &s->frames) wl_resource_destroy(cb);
feedback_discard_all(&s->pending_feedback);
feedback_discard_all(&s->feedback);
if (s->viewport) wl_resource_set_user_data(s->viewport, NULL);
if (s->subsurface) wl_resource_set_user_data(s->subsurface, NULL);
if (s->xdg_surface) wl_resource_set_user_data(s->xdg_surface, NULL);
if (s->xdg_toplevel) wl_resource_set_user_data(s->xdg_toplevel, NULL);
wl_list_remove(&s->link);
free(s->title);
free(s);
schedule_render();
}
/* ------------------------------------------------------------------ wl_region */
/* A region is kept as the bounding box of its rectangles: enough for pointer confinement,
* where winewayland sends one rectangle (the ClipCursor area). Subtractions are ignored. */
struct region { int set; int x, y, w, h; };
static void region_destroy(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); }
static void region_add(struct wl_client *c, struct wl_resource *r,
int32_t x, int32_t y, int32_t w, int32_t h) {
struct region *rg = wl_resource_get_user_data(r);
if (!rg || w <= 0 || h <= 0) return;
if (!rg->set) { rg->x = x; rg->y = y; rg->w = w; rg->h = h; rg->set = 1; return; }
int x2 = rg->x + rg->w > x + w ? rg->x + rg->w : x + w;
int y2 = rg->y + rg->h > y + h ? rg->y + rg->h : y + h;
if (x < rg->x) rg->x = x;
if (y < rg->y) rg->y = y;
rg->w = x2 - rg->x; rg->h = y2 - rg->y;
}
static void region_subtract(struct wl_client *c, struct wl_resource *r,
int32_t x, int32_t y, int32_t w, int32_t h) {}
static void region_resource_destroy(struct wl_resource *r) { free(wl_resource_get_user_data(r)); }
static const struct wl_region_interface region_impl = {
.destroy = region_destroy,
.add = region_add,
.subtract = region_subtract,
};
/* ------------------------------------------------------------------ wl_compositor */
static void compositor_create_surface(struct wl_client *c, struct wl_resource *r, uint32_t id) {
struct surface *s = calloc(1, sizeof(*s));
if (!s) { wl_client_post_no_memory(c); return; }
s->resource = wl_resource_create(c, &wl_surface_interface, wl_resource_get_version(r), id);
if (!s->resource) { free(s); wl_client_post_no_memory(c); return; }
wl_list_init(&s->pending_frames);
wl_list_init(&s->frames);
wl_list_init(&s->pending_feedback);
wl_list_init(&s->feedback);
wl_list_init(&s->children);
wl_list_init(&s->child_link);
wl_list_init(&s->toplevel_link);
wl_list_init(&s->pending_buffer_destroy.link);
wl_list_insert(&g_surfaces, &s->link);
wl_resource_set_implementation(s->resource, &surface_impl, s, surface_resource_destroy);
}
static void compositor_create_region(struct wl_client *c, struct wl_resource *r, uint32_t id) {
struct wl_resource *reg = wl_resource_create(c, &wl_region_interface, 1, id);
if (!reg) { wl_client_post_no_memory(c); return; }
wl_resource_set_implementation(reg, &region_impl, calloc(1, sizeof(struct region)),
region_resource_destroy);
}
static const struct wl_compositor_interface compositor_impl = {
.create_surface = compositor_create_surface,
.create_region = compositor_create_region,
};
static void bind_compositor(struct wl_client *c, void *data, uint32_t ver, uint32_t id) {
struct wl_resource *r = wl_resource_create(c, &wl_compositor_interface, ver, id);
wl_resource_set_implementation(r, &compositor_impl, NULL, NULL);
}
/* --------------------------------------------------------------- wl_subcompositor
* winewayland puts a window's Vulkan/GL swapchain in a subsurface over the window. */
static void subsurface_destroy(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); }
static void subsurface_set_position(struct wl_client *c, struct wl_resource *r, int32_t x, int32_t y) {
struct surface *s = wl_resource_get_user_data(r);
if (!s) return;
s->sub_pending_x = x;
s->sub_pending_y = y;
s->sub_pending = 1;
}
static void restack_child(struct surface *s, struct surface *sibling, int above) {
if (!s || !s->parent || !sibling) return;
if (sibling == s->parent) {
/* Relative to the parent: directly above it or directly below it. */
s->below_parent = !above;
wl_list_remove(&s->child_link);
if (above) wl_list_insert(&s->parent->children, &s->child_link);
else wl_list_insert(s->parent->children.prev, &s->child_link);
} else if (sibling->parent == s->parent) {
s->below_parent = sibling->below_parent;
wl_list_remove(&s->child_link);
if (above) wl_list_insert(&sibling->child_link, &s->child_link);
else wl_list_insert(sibling->child_link.prev, &s->child_link);
}
schedule_render();
}
static void subsurface_place_above(struct wl_client *c, struct wl_resource *r,
struct wl_resource *sibling) {
restack_child(wl_resource_get_user_data(r), wl_resource_get_user_data(sibling), 1);
}
static void subsurface_place_below(struct wl_client *c, struct wl_resource *r,
struct wl_resource *sibling) {
restack_child(wl_resource_get_user_data(r), wl_resource_get_user_data(sibling), 0);
}
static void subsurface_set_sync(struct wl_client *c, struct wl_resource *r) {}
static void subsurface_set_desync(struct wl_client *c, struct wl_resource *r) {}
static const struct wl_subsurface_interface subsurface_impl = {
.destroy = subsurface_destroy,
.set_position = subsurface_set_position,
.place_above = subsurface_place_above,
.place_below = subsurface_place_below,
.set_sync = subsurface_set_sync,
.set_desync = subsurface_set_desync,
};
static void subsurface_resource_destroy(struct wl_resource *r) {
struct surface *s = wl_resource_get_user_data(r);
if (!s) return;
detach_from_parent(s);
s->subsurface = NULL;
s->role = ROLE_NONE;
s->has_content = 0;
drop_dmabuf(s, 1);
schedule_render();
}
static void subcompositor_destroy(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); }
static void subcompositor_get_subsurface(struct wl_client *c, struct wl_resource *r, uint32_t id,
struct wl_resource *surface, struct wl_resource *parent) {
struct surface *s = wl_resource_get_user_data(surface);
struct surface *p = wl_resource_get_user_data(parent);
struct wl_resource *sub = wl_resource_create(c, &wl_subsurface_interface, wl_resource_get_version(r), id);
if (!sub) { wl_client_post_no_memory(c); return; }
wl_resource_set_implementation(sub, &subsurface_impl, s, subsurface_resource_destroy);
if (!s || !p || s == p) return;
detach_from_parent(s);
s->role = ROLE_SUBSURFACE;
s->subsurface = sub;
s->parent = p;
s->below_parent = 0;
s->sub_x = s->sub_y = 0;
wl_list_insert(p->children.prev, &s->child_link); /* new subsurfaces go on top */
}
static const struct wl_subcompositor_interface subcompositor_impl = {
.destroy = subcompositor_destroy,
.get_subsurface = subcompositor_get_subsurface,
};
static void bind_subcompositor(struct wl_client *c, void *data, uint32_t ver, uint32_t id) {
struct wl_resource *r = wl_resource_create(c, &wl_subcompositor_interface, ver, id);
wl_resource_set_implementation(r, &subcompositor_impl, NULL, NULL);
}
/* ---------------------------------------------------------------- wp_viewporter
* winewayland crops window buffers to the window size and scales swapchains to the
* client area with viewports, so they must be honoured for things to line up. */
static void viewport_destroy(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); }
static void viewport_set_source(struct wl_client *c, struct wl_resource *r,
wl_fixed_t x, wl_fixed_t y, wl_fixed_t w, wl_fixed_t h) {
struct surface *s = wl_resource_get_user_data(r);
if (!s) return;
s->pending_src[0] = (float)wl_fixed_to_double(x);
s->pending_src[1] = (float)wl_fixed_to_double(y);
s->pending_src[2] = w == wl_fixed_from_int(-1) ? -1.0f : (float)wl_fixed_to_double(w);
s->pending_src[3] = h == wl_fixed_from_int(-1) ? -1.0f : (float)wl_fixed_to_double(h);
s->pending_src_set = 1;
}
static void viewport_set_destination(struct wl_client *c, struct wl_resource *r, int32_t w, int32_t h) {
struct surface *s = wl_resource_get_user_data(r);
if (!s) return;
s->pending_dst[0] = w;
s->pending_dst[1] = h;
s->pending_dst_set = 1;
}
static const struct wp_viewport_interface viewport_impl = {
.destroy = viewport_destroy,
.set_source = viewport_set_source,
.set_destination = viewport_set_destination,
};
static void viewport_resource_destroy(struct wl_resource *r) {
struct surface *s = wl_resource_get_user_data(r);
if (!s) return;
s->viewport = NULL;
s->pending_src[2] = -1.0f; s->pending_src_set = 1;
s->pending_dst[0] = -1; s->pending_dst_set = 1;
}
static void viewporter_destroy(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); }
static void viewporter_get_viewport(struct wl_client *c, struct wl_resource *r, uint32_t id,
struct wl_resource *surface) {
struct surface *s = wl_resource_get_user_data(surface);
struct wl_resource *vp = wl_resource_create(c, &wp_viewport_interface, wl_resource_get_version(r), id);
if (!vp) { wl_client_post_no_memory(c); return; }
wl_resource_set_implementation(vp, &viewport_impl, s, viewport_resource_destroy);
if (s) s->viewport = vp;
}
static const struct wp_viewporter_interface viewporter_impl = {
.destroy = viewporter_destroy,
.get_viewport = viewporter_get_viewport,
};
static void bind_viewporter(struct wl_client *c, void *data, uint32_t ver, uint32_t id) {
struct wl_resource *r = wl_resource_create(c, &wp_viewporter_interface, ver, id);
wl_resource_set_implementation(r, &viewporter_impl, NULL, NULL);
}
/* ------------------------------------------------------------------ xdg_shell */
static void xdg_toplevel_destroy_req(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); }
static void xdg_toplevel_noop_parent(struct wl_client *c, struct wl_resource *r, struct wl_resource *p) {}
static void xdg_toplevel_set_title(struct wl_client *c, struct wl_resource *r, const char *title) {
struct surface *s = wl_resource_get_user_data(r);
if (!s) return;
/* A window already on screen being renamed is something else taking it over: gamescope keeps
* one toplevel and retitles it as the game takes the screen. Unlogged, that left the log
* showing a window "closed" under a name it had never been logged as opening - which reads
* like a missing event and is really a rename. A title set before the window is mapped needs
* no line: the "opened" line carries it. */
int announce = s->mapped && title && *title && (!s->title || strcmp(s->title, title) != 0);
char before[160];
if (announce) describe(s, before, sizeof(before));
free(s->title);
s->title = title ? strdup(title) : NULL;
if (announce) {
char after[160];
describe(s, after, sizeof(after));
banner_log("window", "renamed %s -> %s", before, after);
}
}
static void xdg_toplevel_set_app_id(struct wl_client *c, struct wl_resource *r, const char *id) {}
static void xdg_toplevel_show_menu(struct wl_client *c, struct wl_resource *r, struct wl_resource *seat,
uint32_t serial, int32_t x, int32_t y) {}
static void xdg_toplevel_move(struct wl_client *c, struct wl_resource *r, struct wl_resource *seat,
uint32_t serial) {}
static void xdg_toplevel_resize(struct wl_client *c, struct wl_resource *r, struct wl_resource *seat,
uint32_t serial, uint32_t edges) {}
static void xdg_toplevel_set_i32(struct wl_client *c, struct wl_resource *r, int32_t w, int32_t h) {}
static void xdg_toplevel_noop(struct wl_client *c, struct wl_resource *r) {}
/* The output's size; active, and fullscreen when asked. gamescope waits for the fullscreen
* state before it drops its libdecor frame, and never draws until it arrives; it was told the
* same size on its command line. A nested wlroots compositor (labwc) has no other way to learn
* it: given 0x0 it opens its default 1280x720 and the whole desktop is upscaled from that. */
static void send_toplevel_configure(struct surface *s) {
struct wl_array states;
wl_array_init(&states);
uint32_t *st = wl_array_add(&states, sizeof(uint32_t));
*st = XDG_TOPLEVEL_STATE_ACTIVATED;
if (s->fullscreen) {
st = wl_array_add(&states, sizeof(uint32_t));
*st = XDG_TOPLEVEL_STATE_FULLSCREEN;
}
xdg_toplevel_send_configure(s->xdg_toplevel, g_output_w > 0 ? g_output_w : 0,
g_output_h > 0 ? g_output_h : 0, &states);
wl_array_release(&states);
xdg_surface_send_configure(s->xdg_surface, wl_display_next_serial(g_display));
}
static void xdg_toplevel_set_fullscreen(struct wl_client *c, struct wl_resource *r,
struct wl_resource *output) {
struct surface *s = wl_resource_get_user_data(r);
if (!s || !s->xdg_surface || !s->xdg_toplevel || s->fullscreen) return;
s->fullscreen = 1;
send_toplevel_configure(s);
}
static void xdg_toplevel_unset_fullscreen(struct wl_client *c, struct wl_resource *r) {
struct surface *s = wl_resource_get_user_data(r);
if (!s || !s->xdg_surface || !s->xdg_toplevel || !s->fullscreen) return;
s->fullscreen = 0;
send_toplevel_configure(s);
}
static const struct xdg_toplevel_interface xdg_toplevel_impl = {
.destroy = xdg_toplevel_destroy_req,
.set_parent = xdg_toplevel_noop_parent,
.set_title = xdg_toplevel_set_title,
.set_app_id = xdg_toplevel_set_app_id,
.show_window_menu = xdg_toplevel_show_menu,
.move = xdg_toplevel_move,
.resize = xdg_toplevel_resize,
.set_max_size = xdg_toplevel_set_i32,
.set_min_size = xdg_toplevel_set_i32,
.set_maximized = xdg_toplevel_noop,
.unset_maximized = xdg_toplevel_noop,
.set_fullscreen = xdg_toplevel_set_fullscreen,
.unset_fullscreen = xdg_toplevel_unset_fullscreen,
.set_minimized = xdg_toplevel_noop,
};
static void xdg_toplevel_resource_destroy(struct wl_resource *r) {
struct surface *s = wl_resource_get_user_data(r);
if (!s) return;
s->xdg_toplevel = NULL;
s->toplevel_committed = 0;
unmap_toplevel(s);
s->placed = 0;
s->hwnd = 0;
schedule_render();
}
static void xdg_surface_destroy_req(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); }
static void xdg_surface_get_toplevel(struct wl_client *c, struct wl_resource *r, uint32_t id) {
struct surface *s = wl_resource_get_user_data(r);
struct wl_resource *tl = wl_resource_create(c, &xdg_toplevel_interface, wl_resource_get_version(r), id);
if (!tl) { wl_client_post_no_memory(c); return; }
wl_resource_set_implementation(tl, &xdg_toplevel_impl, s, xdg_toplevel_resource_destroy);
if (!s) return;
s->role = ROLE_TOPLEVEL;
s->xdg_toplevel = tl;
send_toplevel_configure(s);
}
/* A popup is dismissed as soon as it is made (see the positioner below). */
static void popup_destroy(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); }
static void popup_grab(struct wl_client *c, struct wl_resource *r, struct wl_resource *seat, uint32_t serial) {}
static void popup_reposition(struct wl_client *c, struct wl_resource *r, struct wl_resource *positioner,
uint32_t token) {}
static const struct xdg_popup_interface popup_impl = {
.destroy = popup_destroy,
.grab = popup_grab,
.reposition = popup_reposition,
};
static void xdg_surface_get_popup(struct wl_client *c, struct wl_resource *r, uint32_t id,
struct wl_resource *parent, struct wl_resource *positioner) {
struct wl_resource *p = wl_resource_create(c, &xdg_popup_interface, wl_resource_get_version(r), id);
if (!p) { wl_client_post_no_memory(c); return; }
wl_resource_set_implementation(p, &popup_impl, NULL, NULL);
xdg_popup_send_popup_done(p);
}
static void xdg_surface_set_geometry(struct wl_client *c, struct wl_resource *r,
int32_t x, int32_t y, int32_t w, int32_t h) {}
static void xdg_surface_ack_configure(struct wl_client *c, struct wl_resource *r, uint32_t serial) {}
static const struct xdg_surface_interface xdg_surface_impl = {
.destroy = xdg_surface_destroy_req,
.get_toplevel = xdg_surface_get_toplevel,
.get_popup = xdg_surface_get_popup,
.set_window_geometry = xdg_surface_set_geometry,
.ack_configure = xdg_surface_ack_configure,
};
static void xdg_surface_resource_destroy(struct wl_resource *r) {
struct surface *s = wl_resource_get_user_data(r);
if (s) s->xdg_surface = NULL;
}
static void xdg_wm_base_destroy(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); }
/* Popups are not drawn here, but their requests must still land somewhere: a resource without an
* implementation makes libwayland abort on the first request to it - the whole compositor, and the
* app with it. A positioner takes every request and keeps nothing; a popup is dismissed at once
* (popup_done), which is what a client does with a popup it cannot show. Seen with a gamescope
* started from the Linux desktop beside labwc. */
static void positioner_destroy(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); }
static void positioner_set_size(struct wl_client *c, struct wl_resource *r, int32_t w, int32_t h) {}
static void positioner_set_anchor_rect(struct wl_client *c, struct wl_resource *r, int32_t x, int32_t y,
int32_t w, int32_t h) {}
static void positioner_set_anchor(struct wl_client *c, struct wl_resource *r, uint32_t a) {}
static void positioner_set_gravity(struct wl_client *c, struct wl_resource *r, uint32_t g) {}
static void positioner_set_constraint_adjustment(struct wl_client *c, struct wl_resource *r, uint32_t a) {}
static void positioner_set_offset(struct wl_client *c, struct wl_resource *r, int32_t x, int32_t y) {}
static void positioner_set_reactive(struct wl_client *c, struct wl_resource *r) {}
static void positioner_set_parent_size(struct wl_client *c, struct wl_resource *r, int32_t w, int32_t h) {}
static void positioner_set_parent_configure(struct wl_client *c, struct wl_resource *r, uint32_t serial) {}
static const struct xdg_positioner_interface positioner_impl = {
.destroy = positioner_destroy,
.set_size = positioner_set_size,
.set_anchor_rect = positioner_set_anchor_rect,
.set_anchor = positioner_set_anchor,
.set_gravity = positioner_set_gravity,
.set_constraint_adjustment = positioner_set_constraint_adjustment,
.set_offset = positioner_set_offset,
.set_reactive = positioner_set_reactive,
.set_parent_size = positioner_set_parent_size,
.set_parent_configure = positioner_set_parent_configure,
};
static void xdg_wm_base_create_positioner(struct wl_client *c, struct wl_resource *r, uint32_t id) {
struct wl_resource *p = wl_resource_create(c, &xdg_positioner_interface, wl_resource_get_version(r), id);
if (!p) { wl_client_post_no_memory(c); return; }
wl_resource_set_implementation(p, &positioner_impl, NULL, NULL);
}
static void xdg_wm_base_get_xdg_surface(struct wl_client *c, struct wl_resource *r, uint32_t id,
struct wl_resource *surf) {
struct surface *s = wl_resource_get_user_data(surf);
struct wl_resource *xs = wl_resource_create(c, &xdg_surface_interface, wl_resource_get_version(r), id);
if (!xs) { wl_client_post_no_memory(c); return; }
wl_resource_set_implementation(xs, &xdg_surface_impl, s, xdg_surface_resource_destroy);
if (s) s->xdg_surface = xs;
}
static void xdg_wm_base_pong(struct wl_client *c, struct wl_resource *r, uint32_t serial) {}
static const struct xdg_wm_base_interface xdg_wm_base_impl = {
.destroy = xdg_wm_base_destroy,
.create_positioner = xdg_wm_base_create_positioner,
.get_xdg_surface = xdg_wm_base_get_xdg_surface,
.pong = xdg_wm_base_pong,
};
static void bind_xdg_wm_base(struct wl_client *c, void *data, uint32_t ver, uint32_t id) {
struct wl_resource *r = wl_resource_create(c, &xdg_wm_base_interface, ver, id);
wl_resource_set_implementation(r, &xdg_wm_base_impl, NULL, NULL);
}
/* ------------------------------------------------------------------ banner_desktop_v1 */
static void desktop_destroy_req(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); }
static void desktop_set_desktop(struct wl_client *c, struct wl_resource *r, struct wl_resource *surface) {
struct surface *s = wl_resource_get_user_data(surface);
if (!s || (s->role != ROLE_NONE && s->role != ROLE_DESKTOP)) return;
s->role = ROLE_DESKTOP;
g_desktop = s;
banner_log("desktop", "Windows virtual desktop created by %s", client_name(c));
schedule_render();
}
static void desktop_set_window(struct wl_client *c, struct wl_resource *r, struct wl_resource *surface,
uint32_t hwnd, int32_t x, int32_t y) {
struct surface *s = wl_resource_get_user_data(surface);
if (!s) return;
if (s->placed && (s->x != x || s->y != y) && s->mapped && log_budget()) {
char name[160];
describe(s, name, sizeof(name));
banner_log("window", "moved %s to %d,%d", name, x, y);
}
s->hwnd = hwnd;
s->x = x;
s->y = y;
s->placed = 1;
if (s->mapped) apply_zorder();
schedule_render();
}
static void desktop_set_zorder(struct wl_client *c, struct wl_resource *r, struct wl_array *hwnds) {
size_t count = hwnds->size / sizeof(uint32_t);
uint32_t *copy = count ? malloc(count * sizeof(uint32_t)) : NULL;
if (count && !copy) return;
if (count) memcpy(copy, hwnds->data, count * sizeof(uint32_t));
free(g_zorder);
g_zorder = copy;
g_zorder_count = count;
apply_zorder();
schedule_render();
}
static const struct banner_desktop_v1_interface desktop_impl = {
.destroy = desktop_destroy_req,
.set_desktop = desktop_set_desktop,
.set_window = desktop_set_window,
.set_zorder = desktop_set_zorder,
};
static void bind_desktop(struct wl_client *c, void *data, uint32_t ver, uint32_t id) {
struct wl_resource *r = wl_resource_create(c, &banner_desktop_v1_interface, ver, id);
wl_resource_set_implementation(r, &desktop_impl, NULL, NULL);
}
/* ------------------------------------------------------------ zwp_linux_dmabuf_v1 */
static void dbuf_buffer_resource_destroy(struct wl_resource *r) {
dmabuf_buffer_unref(wl_resource_get_user_data(r));
}
static void params_destroy(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); }
static void params_add(struct wl_client *c, struct wl_resource *r, int32_t fd, uint32_t plane,
uint32_t offset, uint32_t stride, uint32_t mod_hi, uint32_t mod_lo) {
struct dmabuf_params *p = wl_resource_get_user_data(r);
if (plane >= MAX_PLANES) { close(fd); return; }
if (p->fd[plane] >= 0) close(p->fd[plane]);
p->fd[plane] = fd;
p->offset[plane] = offset;
p->stride[plane] = stride;
p->modifier[plane] = ((uint64_t)mod_hi << 32) | mod_lo;
if ((int)plane + 1 > p->n_planes) p->n_planes = plane + 1;
}
static struct wl_resource *params_do_create(struct wl_client *c, struct wl_resource *r, uint32_t id,
int32_t w, int32_t h, uint32_t format, uint32_t flags) {
struct dmabuf_params *p = wl_resource_get_user_data(r);
struct dmabuf_buffer *b = calloc(1, sizeof(*b));
if (!b) return NULL;
b->n_planes = p->n_planes;
b->refs = 1; /* the wl_buffer resource's */
b->width = w; b->height = h; b->format = format;
b->modifier = p->modifier[0];
for (int i = 0; i < MAX_PLANES; i++) b->fd[i] = -1;
for (int i = 0; i < p->n_planes; i++) {
b->fd[i] = p->fd[i];
b->offset[i] = p->offset[i];
b->stride[i] = p->stride[i];
p->fd[i] = -1; /* ownership moves to the buffer */
}
struct wl_resource *buf = wl_resource_create(c, &wl_buffer_interface, 1, id);
if (!buf) {
for (int i = 0; i < b->n_planes; i++) if (b->fd[i] >= 0) close(b->fd[i]);
free(b);
wl_client_post_no_memory(c);
return NULL;
}
wl_resource_set_implementation(buf, &dbuf_buffer_impl, b, dbuf_buffer_resource_destroy);
struct client_info *ci = client_info_of(c);
if (ci) ci->dmabuf_buffers++;
return buf;
}
static void params_create(struct wl_client *c, struct wl_resource *r, int32_t w, int32_t h,
uint32_t format, uint32_t flags) {
struct wl_resource *buf = params_do_create(c, r, 0, w, h, format, flags);
if (buf) zwp_linux_buffer_params_v1_send_created(r, buf);
else zwp_linux_buffer_params_v1_send_failed(r);
}
static void params_create_immed(struct wl_client *c, struct wl_resource *r, uint32_t buffer_id,
int32_t w, int32_t h, uint32_t format, uint32_t flags) {
params_do_create(c, r, buffer_id, w, h, format, flags);
}
static const struct zwp_linux_buffer_params_v1_interface params_impl = {
.destroy = params_destroy,
.add = params_add,
.create = params_create,
.create_immed = params_create_immed,
};
static void params_resource_destroy(struct wl_resource *r) {
struct dmabuf_params *p = wl_resource_get_user_data(r);
if (!p) return;
for (int i = 0; i < MAX_PLANES; i++)
if (p->fd[i] >= 0) close(p->fd[i]);
free(p);
}
static void dmabuf_destroy(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); }
static void dmabuf_create_params(struct wl_client *c, struct wl_resource *r, uint32_t id) {
struct dmabuf_params *p = calloc(1, sizeof(*p));
if (!p) { wl_client_post_no_memory(c); return; }
for (int i = 0; i < MAX_PLANES; i++) p->fd[i] = -1;
struct wl_resource *pr = wl_resource_create(c, &zwp_linux_buffer_params_v1_interface,
wl_resource_get_version(r), id);
if (!pr) { free(p); wl_client_post_no_memory(c); return; }
wl_resource_set_implementation(pr, &params_impl, p, params_resource_destroy);
}
static void dmabuf_get_default_feedback(struct wl_client *c, struct wl_resource *r, uint32_t id);
static void dmabuf_get_surface_feedback(struct wl_client *c, struct wl_resource *r, uint32_t id,
struct wl_resource *surface);
static const struct zwp_linux_dmabuf_v1_interface dmabuf_impl = {
.destroy = dmabuf_destroy,
.create_params = dmabuf_create_params,
.get_default_feedback = dmabuf_get_default_feedback,
.get_surface_feedback = dmabuf_get_surface_feedback,
};
/* The advertised format/modifier table, built at the first bind from what the renderer's driver
* can import (vkp_dmabuf_modifiers). INVALID is always offered too (Mesa drops it; other clients
* may use it for the implicit path). Without a renderer the list is LINEAR + INVALID, as before. */
/* The last two rows are HDR10's (banner_color.h): A2B10G10R10 as AB30 (alpha) + XB30 (opaque) - Mesa
* lists a VkFormat only when both are advertised, and it is the one 10-bit layout our zero-copy WSI
* can put in a gralloc buffer (AHARDWAREBUFFER_FORMAT_R10G10B10A2_UNORM). They are advertised ONLY
* while the HDR gate is open, and after the 8-bit rows, so every other session - and every client that
* takes the first format it sees - gets exactly the table it always had. */
#define DMABUF_NFMT_MAX 6
#define DMABUF_NFMT_SDR 4
#define DMABUF_NMOD 4
#define DRM_ABGR2101010 FOURCC('A', 'B', '3', '0')
#define DRM_XBGR2101010 FOURCC('X', 'B', '3', '0')
static struct { uint32_t fmt; uint64_t mods[DMABUF_NMOD]; int n; } g_dmabuf_fmts[DMABUF_NFMT_MAX] = {
{DRM_ARGB8888}, {DRM_XRGB8888}, {DRM_ABGR8888}, {DRM_XBGR8888}, {DRM_ABGR2101010}, {DRM_XBGR2101010}};
static int g_dmabuf_nfmt = DMABUF_NFMT_SDR; /* rows advertised: the SDR four, + 2 with the HDR gate open */
static int g_dmabuf_fmts_ready;
static void dmabuf_build_feedback(void);
static void dmabuf_build_formats(void) {
char line[320];
int pos = 0, compressed = 0;
g_dmabuf_fmts_ready = 1;
g_dmabuf_nfmt = banner_color_hdr_open() ? DMABUF_NFMT_MAX : DMABUF_NFMT_SDR;
for (int f = 0; f < g_dmabuf_nfmt; f++) {
uint64_t got[DMABUF_NMOD];
int n = vkp_dmabuf_modifiers(g_dmabuf_fmts[f].fmt, got, DMABUF_NMOD), k = 0;
/* LINEAR first: it is the layout every client and the shm fallback agree on, and the one
* we advertised before this table existed. */
g_dmabuf_fmts[f].mods[k++] = MOD_LINEAR;
for (int i = 0; i < n && k < DMABUF_NMOD - 1; i++) {
if (got[i] == MOD_LINEAR) continue;
if (got[i] == VKP_MOD_QCOM_COMPRESSED && !g_ubwc) continue;
g_dmabuf_fmts[f].mods[k++] = got[i];
if (got[i] == VKP_MOD_QCOM_COMPRESSED) compressed++;
}
g_dmabuf_fmts[f].mods[k++] = MOD_INVALID;
g_dmabuf_fmts[f].n = k;
uint32_t fmt = g_dmabuf_fmts[f].fmt;
pos += snprintf(line + pos, sizeof(line) - (size_t)pos, "%s%c%c%c%c", f ? ", " : "",
fmt & 0xff, (fmt >> 8) & 0xff, (fmt >> 16) & 0xff, (fmt >> 24) & 0xff);
for (int i = 0; i < k - 1 && pos < (int)sizeof(line); i++)
pos += snprintf(line + pos, sizeof(line) - (size_t)pos, "%s%s", i ? "+" : " ",
vkp_modifier_name(g_dmabuf_fmts[f].mods[i]));
if (pos >= (int)sizeof(line)) pos = (int)sizeof(line) - 1;
}
banner_log("dmabuf", "formats: %s%s", line,
!g_ubwc ? " (BANNER_WAYLAND_UBWC=0: qcom_compressed not advertised)" : "");
if (g_ubwc && !compressed)
banner_log("dmabuf", "the compositor's driver (%s) reports no importable qcom_compressed layout: "
"game swapchains stay linear", vkp_gpu_name());
if (g_dmabuf_nfmt > DMABUF_NFMT_SDR) {
/* The rows above always carry LINEAR; say what the compositor's own driver can really import
* for 10-bit, since a gralloc buffer it cannot import is shown on the display layer only. */
uint64_t got[DMABUF_NMOD];
int n = vkp_dmabuf_modifiers(DRM_XBGR2101010, got, DMABUF_NMOD), ubwc10 = 0;
for (int i = 0; i < n; i++) if (got[i] == VKP_MOD_QCOM_COMPRESSED) ubwc10 = 1;
banner_log("color", "10-bit dma-buf formats AB30/XB30 advertised for HDR10; the compositor's driver (%s) "
"imports XB30 %s", vkp_gpu_name(),
n == 0 ? "with no layout it reports (copy path unlikely; display layer only)"
: ubwc10 ? "linear and UBWC" : "linear only (UBWC 10-bit frames: display layer only)");
}
dmabuf_build_feedback();
}
/* ---- dmabuf feedback (zwp_linux_dmabuf_v1 version 4)
*
* Turnip's Vulkan WSI is happy with the version 3 format/modifier events, so every Vulkan game
* worked while we only advertised 3. Mesa's EGL is not: its Wayland platform only takes the GPU
* (kopper/Zink) path when it can bind this interface with FEEDBACK, and with 3 it silently drops
* to its wl_shm software path. On this Proton layer that path has no software rasteriser to fall
* back to (the gallium build is zink+kopper+swrast, no llvmpipe), so the shm buffer it commits is
* never written: a native OpenGL window came out solid black, ~30 shm commits/s and no GPU frame
* at all (Wizardry: The Labyrinth of Lost Souls). Feedback is what makes that path work.
*
* What a client needs from us is one tranche describing "everything the compositor can import":
* a format table it mmaps read-only, the device to allocate on, and the indices it may use.
* Clients binding versions 1-3 keep getting the old format/modifier events instead. */
#ifndef MFD_CLOEXEC
#define MFD_CLOEXEC 0x0001U
#endif
#ifndef MFD_ALLOW_SEALING
#define MFD_ALLOW_SEALING 0x0002U
#endif
#ifndef F_ADD_SEALS
#define F_ADD_SEALS 1033
#define F_SEAL_SEAL 0x0001
#define F_SEAL_SHRINK 0x0002
#define F_SEAL_GROW 0x0004
#define F_SEAL_WRITE 0x0008
#endif
struct dmabuf_fmt_entry { uint32_t format; uint32_t pad; uint64_t modifier; }; /* the wire layout */
static int g_fmt_table_fd = -1; /* sealed read-only memfd of dmabuf_fmt_entry[] */
static size_t g_fmt_table_size;
static uint16_t g_fmt_table_n; /* entries, == the indices a tranche may name */
static dev_t g_main_device; /* the render node clients should allocate on */
/* The GPU we import through is reached with KGSL, not DRM, so there is no render node of our own
* to name. Clients only use main_device to match "the same device as the compositor" and to pick
* a driver; the one DRM render node this platform has is the right answer, and Zink ignores it
* anyway (it renders on the Vulkan device it already has). 0 if the platform has none. */
static dev_t dmabuf_render_node(void) {
static const char *nodes[] = {"/dev/dri/renderD128", "/dev/dri/renderD129", "/dev/dri/card0"};
struct stat st;
if (g_no_render_node) return 0;
for (size_t i = 0; i < sizeof(nodes) / sizeof(nodes[0]); i++)
if (!stat(nodes[i], &st) && S_ISCHR(st.st_mode)) return st.st_rdev;
return 0;
}
static void dmabuf_build_feedback(void) {
struct dmabuf_fmt_entry entries[DMABUF_NFMT_MAX * DMABUF_NMOD];
int n = 0;
g_main_device = dmabuf_render_node();
for (int f = 0; f < g_dmabuf_nfmt; f++)
for (int m = 0; m < g_dmabuf_fmts[f].n; m++) {
if (g_dmabuf_fmts[f].mods[m] == MOD_INVALID) continue; /* never offer INVALID here */
entries[n].format = g_dmabuf_fmts[f].fmt;
entries[n].pad = 0;
entries[n].modifier = g_dmabuf_fmts[f].mods[m];
n++;
}
if (!n) return;
int fd = (int)syscall(__NR_memfd_create, "banner-dmabuf-formats",
MFD_CLOEXEC | MFD_ALLOW_SEALING);
if (fd < 0) { WLOGE("dmabuf feedback: memfd_create failed (%s)", strerror(errno)); return; }
size_t size = (size_t)n * sizeof(entries[0]);
if (write(fd, entries, size) != (ssize_t)size) {
WLOGE("dmabuf feedback: could not write the format table (%s)", strerror(errno));
close(fd);
return;
}
/* The client mmaps this read-only and trusts it not to change under it. */
fcntl(fd, F_ADD_SEALS, F_SEAL_SEAL | F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE);
g_fmt_table_fd = fd;
g_fmt_table_size = size;
g_fmt_table_n = (uint16_t)n;
banner_log("dmabuf", "feedback ready: %d format/modifier pairs, main device %u:%u",
n, (unsigned)major(g_main_device), (unsigned)minor(g_main_device));
/* Vulkan games never need the node. Mesa's EGL did until Wayland layer versionCode 9: without
* one it fell back to a software path that draws nothing here (black window, sound plays). */
if (!g_main_device)
banner_log("dmabuf", "%s: OpenGL games need Wayland layer versionCode 9 or newer, "
"which runs OpenGL on the GPU without a DRM node; older layers show a black window",
g_no_render_node ? "no DRM device named (forced by BANNER_WAYLAND_NO_RENDER_NODE=1)"
: "this device gives apps no display (DRM) device (/dev/dri)");
}
/* One tranche: our device, every pair in the table, no scanout flag. */
static void dmabuf_feedback_send(struct wl_resource *fb) {
struct wl_array dev, idx;
uint16_t *ind;
if (g_fmt_table_fd < 0) { zwp_linux_dmabuf_feedback_v1_send_done(fb); return; }
zwp_linux_dmabuf_feedback_v1_send_format_table(fb, g_fmt_table_fd, (uint32_t)g_fmt_table_size);
wl_array_init(&dev);
memcpy(wl_array_add(&dev, sizeof(dev_t)), &g_main_device, sizeof(dev_t));
zwp_linux_dmabuf_feedback_v1_send_main_device(fb, &dev);
zwp_linux_dmabuf_feedback_v1_send_tranche_target_device(fb, &dev);
wl_array_release(&dev);
wl_array_init(&idx);
ind = wl_array_add(&idx, (size_t)g_fmt_table_n * sizeof(uint16_t));
if (ind) for (uint16_t i = 0; i < g_fmt_table_n; i++) ind[i] = i;
zwp_linux_dmabuf_feedback_v1_send_tranche_formats(fb, &idx);
wl_array_release(&idx);
zwp_linux_dmabuf_feedback_v1_send_tranche_flags(fb, 0);
zwp_linux_dmabuf_feedback_v1_send_tranche_done(fb);
zwp_linux_dmabuf_feedback_v1_send_done(fb);
}
static void dmabuf_feedback_destroy(struct wl_client *c, struct wl_resource *r) {
wl_resource_destroy(r);
}
static const struct zwp_linux_dmabuf_feedback_v1_interface dmabuf_feedback_impl = {
.destroy = dmabuf_feedback_destroy,
};
static void dmabuf_new_feedback(struct wl_client *c, struct wl_resource *parent, uint32_t id) {
struct wl_resource *fb = wl_resource_create(c, &zwp_linux_dmabuf_feedback_v1_interface,
wl_resource_get_version(parent), id);
if (!fb) { wl_client_post_no_memory(c); return; }
wl_resource_set_implementation(fb, &dmabuf_feedback_impl, NULL, NULL);
if (!g_dmabuf_fmts_ready) dmabuf_build_formats();
dmabuf_feedback_send(fb);
}
static void dmabuf_get_default_feedback(struct wl_client *c, struct wl_resource *r, uint32_t id) {
struct client_info *ci = client_info_of(c);
if (ci) ci->asked_feedback = 1;
dmabuf_new_feedback(c, r, id);
}
/* Per-surface feedback would let us hint a different tranche for a window on its own display
* layer; we have nothing better to say per surface, so it is the default one. */
static void dmabuf_get_surface_feedback(struct wl_client *c, struct wl_resource *r, uint32_t id,
struct wl_resource *surface) {
dmabuf_new_feedback(c, r, id);
}
static void bind_dmabuf(struct wl_client *c, void *data, uint32_t ver, uint32_t id) {
struct wl_resource *r = wl_resource_create(c, &zwp_linux_dmabuf_v1_interface, ver, id);
wl_resource_set_implementation(r, &dmabuf_impl, NULL, NULL);
if (!g_dmabuf_fmts_ready) dmabuf_build_formats();
/* From version 4 the format and modifier events are gone: the client asks for feedback
* instead, and sending both would only confuse it about which list is authoritative. */
if (ver >= 4) return;
for (int f = 0; f < g_dmabuf_nfmt; f++) {
zwp_linux_dmabuf_v1_send_format(r, g_dmabuf_fmts[f].fmt);
if (ver >= 3)
for (int m = 0; m < g_dmabuf_fmts[f].n; m++)
zwp_linux_dmabuf_v1_send_modifier(r, g_dmabuf_fmts[f].fmt,
(uint32_t)(g_dmabuf_fmts[f].mods[m] >> 32),
(uint32_t)(g_dmabuf_fmts[f].mods[m] & 0xffffffff));
}
}
/* ------------------------------------------------------------------ wl_output */
/* wl_output v3+ has a request (release); without an implementation the first client to send it
* aborted the compositor. */
static void output_release(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); }
static const struct wl_output_interface output_impl = { .release = output_release };
static void bind_output(struct wl_client *c, void *data, uint32_t ver, uint32_t id) {
struct wl_resource *r = wl_resource_create(c, &wl_output_interface, ver, id);
if (!r) { wl_client_post_no_memory(c); return; }
wl_resource_set_implementation(r, &output_impl, NULL, NULL);
wl_output_send_geometry(r, 0, 0, 340, 190, WL_OUTPUT_SUBPIXEL_UNKNOWN,
"Bannerlator", "Wayland", WL_OUTPUT_TRANSFORM_NORMAL);
wl_output_send_mode(r, WL_OUTPUT_MODE_CURRENT | WL_OUTPUT_MODE_PREFERRED,
g_output_w > 0 ? g_output_w : 1920, g_output_h > 0 ? g_output_h : 1080,
g_output_refresh_mhz > 0 ? g_output_refresh_mhz : 60000);
if (ver >= WL_OUTPUT_NAME_SINCE_VERSION) {
wl_output_send_name(r, "Bannerlator-1");
wl_output_send_description(r, "Bannerlator display");
}
if (ver >= 2) {
wl_output_send_scale(r, 1);
wl_output_send_done(r);
}
}
/* ------------------------------------------------------------------ rendering */
struct draw_list { struct vkp_draw *d; int n, cap; };
static struct vkp_image *surface_image(struct surface *s) {
if (!s->has_content) return NULL;
if (s->shm_img) return s->shm_img;
return s->dmabuf_buf ? s->dmabuf_buf->img : NULL;
}
/* HDR (gate open) only: the scene's HDR surface whose current frame is one of the game's gralloc buffers
* that the compositor's own driver could NOT import (a 10-bit UBWC layout it does not take, say). Such a
* frame has no draw, so neither layer_candidate() nor ahb_layer_only_candidate() (toplevels only) can
* find it - and a Vulkan game's swapchain is a SUBSURFACE of its window. Remembered here while the scene
* is built so it can still go on the display layer, which never needed the import. Reset per scene;
* never set while the HDR gate is closed. */
static struct surface *g_hdr_unimported;
static int g_hdr_unimported_rect[4], g_hdr_unimported_below; /* scene x,y,w,h; draws under it */
static void note_hdr_unimported(const struct draw_list *dl, struct surface *s, int ox, int oy) {
if (!s->dmabuf_buf || s->dmabuf_buf->img || !ahb_swapchain_has_ahb(s->dmabuf_buf)) return;
const struct banner_color *c = banner_color_of(s->resource);
if (!c || !c->dataspace) return;
if (s->src_set && (s->src[0] != 0 || s->src[1] != 0 || (int)(s->src[2] + 0.5f) != s->buf_w ||
(int)(s->src[3] + 0.5f) != s->buf_h)) return; /* cropped: the layer shows whole buffers */
int dw, dh;
surface_size(s, &dw, &dh);
g_hdr_unimported = s;
g_hdr_unimported_rect[0] = ox; g_hdr_unimported_rect[1] = oy;
g_hdr_unimported_rect[2] = dw; g_hdr_unimported_rect[3] = dh;
g_hdr_unimported_below = dl->n;
}
static void add_surface(struct draw_list *dl, struct surface *s, int ox, int oy) {
struct vkp_image *img = surface_image(s);
float sx = 0, sy = 0, sw = (float)s->buf_w, sh = (float)s->buf_h;
int dw, dh;
if (!img) {
if (g_zero_copy && banner_color_hdr_open()) note_hdr_unimported(dl, s, ox, oy);
return;
}
if (s->src_set) { sx = s->src[0]; sy = s->src[1]; sw = s->src[2]; sh = s->src[3]; }
surface_size(s, &dw, &dh);
if (dw <= 0 || dh <= 0 || sw <= 0 || sh <= 0) return;
if (dl->n == dl->cap) {
int cap = dl->cap ? dl->cap * 2 : 32;
struct vkp_draw *d = realloc(dl->d, (size_t)cap * sizeof(*d));
if (!d) return;
dl->d = d;
dl->cap = cap;
}
dl->d[dl->n++] = (struct vkp_draw){img, sx, sy, sw, sh, ox, oy, dw, dh};
s->drawn = 1;
}
static void add_tree(struct draw_list *dl, struct surface *s, int ox, int oy, int depth) {
struct surface *c;
if (depth > 8) return;
wl_list_for_each(c, &s->children, child_link)
if (c->below_parent) add_tree(dl, c, ox + c->sub_x, oy + c->sub_y, depth + 1);
add_surface(dl, s, ox, oy);
wl_list_for_each(c, &s->children, child_link)
if (!c->below_parent) add_tree(dl, c, ox + c->sub_x, oy + c->sub_y, depth + 1);
}
/* Scene size: the desktop's, or without one the largest window's (a single
* fullscreen game then fills the screen exactly as before). */
static void scene_size(int *w, int *h) {
struct surface *s;
long long best = 0;
if (g_desktop && g_desktop->has_content) {
surface_size(g_desktop, w, h);
g_desktop_w = *w;
g_desktop_h = *h;
return;
}
if (g_desktop && g_desktop_w > 0) { *w = g_desktop_w; *h = g_desktop_h; return; }
*w = *h = 0;
wl_list_for_each(s, &g_toplevels, toplevel_link) {
int sw, sh;
surface_size(s, &sw, &sh);
if ((long long)sw * sh > best) { best = (long long)sw * sh; *w = sw; *h = sh; }
}
if (*w <= 0 || *h <= 0) { *w = INPUT_SPACE_W; *h = INPUT_SPACE_H; }
}
static void fire_all_frames(void) {
struct surface *s;
wl_list_for_each(s, &g_surfaces, link) fire_frames(&s->frames);
}
/* With no output window (the app is in the background, or the SurfaceView is being rebuilt)
* nothing gets drawn, but the clients must not be left waiting on us. A FIFO swapchain blocks
* in its present until its wp_presentation feedback and frame callback arrive, and a client
* blocked there stops pumping messages: winhandler.exe then wedges behind it and the app
* concludes the program has exited and closes the whole session. (DXVK's mailbox presents
* never wait, which is why only OpenGL/Zink clients died on a background/resume.) Answer every
* pending frame and feedback as discarded, and keep doing so until a window is back. */
static void pace_without_output(void) {
struct surface *s;
wl_list_for_each(s, &g_surfaces, link) {
fire_frames(&s->frames);
feedback_discard_all(&s->feedback);
}
wl_display_flush_clients(g_display);
}
static int on_frame_timer(void *data) {
pace_without_output();
if (vkp_has_window()) schedule_render(); /* it is back: draw the current scene */
else if (g_frame_timer) wl_event_source_timer_update(g_frame_timer, 16);
return 0;
}
/* Layer mode: the index of a draw that shows a whole client GPU frame over the whole scene (one
* fullscreen game), or -1. Only the top two positions are looked at: the game must be the topmost
* draw, or have exactly ONE draw above it, which then goes on the overlay layer (sc_layer.h).
* Anything deeper would need more display layers than HWC will compose. */
static int layer_candidate(const struct draw_list *dl, int scene_w, int scene_h) {
for (int i = dl->n - 1; i >= 0 && i >= dl->n - 2; i--) {
const struct vkp_draw *d = &dl->d[i];
if (!vkp_image_is_dmabuf(d->img)) continue;
if (d->dx != 0 || d->dy != 0 || d->dw != scene_w || d->dh != scene_h) continue;
if (d->sx != 0 || d->sy != 0 || (int)d->sw != vkp_image_width(d->img) ||
(int)d->sh != vkp_image_height(d->img)) continue;
return i;
}
return -1;
}
/* Zero-copy: the surface whose current GPU frame `img` is (layer_candidate found the draw). */
static struct surface *surface_for_image(const struct vkp_image *img) {
struct surface *s;
wl_list_for_each(s, &g_surfaces, link)
if (s->dmabuf_buf && s->dmabuf_buf->img == img) return s;
return NULL;
}
/* Zero-copy: the topmost window whose frame is one of the game's AHardwareBuffers that this
* renderer could NOT import (so it has no draw), covering the whole scene at 0,0 with nothing of
* its own above it: it can only be shown on the layer. NULL otherwise. */
static struct surface *ahb_layer_only_candidate(int scene_w, int scene_h) {
struct surface *s;
int w, h;
if (wl_list_empty(&g_toplevels)) return NULL;
s = wl_container_of(g_toplevels.prev, s, toplevel_link);
if (!s->dmabuf_buf || s->dmabuf_buf->img || !ahb_swapchain_has_ahb(s->dmabuf_buf)) return NULL;
if ((g_desktop && !s->placed) || s->x != 0 || s->y != 0 || s->src_set || s->dst_set) return NULL;
if (!wl_list_empty(&s->children)) return NULL;
if (g_hide_shell && !strcmp(client_name(wl_resource_get_client(s->resource)), "explorer.exe")) return NULL;
surface_size(s, &w, &h);
if (w != scene_w || h != scene_h || s->buf_w != scene_w || s->buf_h != scene_h) return NULL;
return s;
}
/* ---- HDR (banner_color.h) in the scene
* An HDR frame is only right on the game's own display layer: everything the compositor draws itself -
* the scene blit, the effects chain, frame generation, its swapchain - is 8-bit sRGB, and a PQ-encoded
* frame pushed through it as it stands comes out washed out. So a scene with HDR in it takes one of
* three routes (hdr_plan):
* 0 the HDR game ALONE on its display layer (zero-copy, or one copy) - with at most one SDR window
* above it on the overlay layer where this display can compose that. Best: nothing is converted.
* 1 the HDR PICTURE: everything else without frame generation - screen effects on, a window above
* the game on a display that cannot take a second layer, a windowed game, zero-copy off. The whole
* scene is composed into ONE 10-bit PQ BT.2020 picture (SDR content placed at the SDR white; the
* effects run on it in 10-bit) and put on the game layer tagged BT2020_PQ (hdr_compose.h).
* 2 frame generation: its extra frames need presents of their own, so the scene is composed into PQ
* and presented through an HDR10 swapchain - or, where the screen surface offers none,
* tone-mapped to SDR (correct colours instead of washed out).
* 3 a frame the compositor could not import: only its display layer can show it, so effects and
* frame generation are skipped for it (said in the log).
* The drawer's HDR output switch (banner_color_output) OFF: the same routes, tone-mapped - route 0
* becomes route 1 (the whole scene composed, tone-mapped to sRGB, onto the game layer UNtagged), route 2
* tone-maps into an SDR swapchain, route 3 cannot (nothing can read those frames) and stays HDR, said
* in the log. The game is told nothing; it keeps rendering HDR.
* Nothing here runs while the HDR gate is closed. */
struct hdr_scene {
struct vkp_hdr_frame hf;
unsigned char *is_hdr; /* per draw (calloc'd; free after the frame) */
int n_hdr;
const struct banner_color *color; /* the topmost HDR draw's description: what the picture is tagged */
struct surface *who; /* its surface, for the log */
const char *why; /* route 1: why the game cannot be alone on its layer */
};
/* The HDR surface note_hdr_unimported() saw in THIS scene, when it covers the whole scene at 0,0 and
* nothing is drawn above it: the display layer is the only place its frame can be shown. */
static struct surface *hdr_unimported_fullscreen(const struct draw_list *dl, int w, int h) {
if (!g_hdr_unimported || g_hdr_unimported_below != dl->n) return NULL;
if (g_hdr_unimported_rect[0] != 0 || g_hdr_unimported_rect[1] != 0 ||
g_hdr_unimported_rect[2] != w || g_hdr_unimported_rect[3] != h) return NULL;
return g_hdr_unimported;
}
static int is_hdr_surface(struct surface *s) {
const struct banner_color *c = s ? banner_color_of(s->resource) : NULL;
return c && c->dataspace;
}
/* Which route this scene takes (see above); fills hs. 0 whenever the gate is closed or no HDR is drawn. */
static int hdr_plan(const struct draw_list *dl, int w, int h, int fx_on, int framegen, struct hdr_scene *hs) {
memset(hs, 0, sizeof(*hs));
if (!banner_color_hdr_open()) return 0;
/* A fullscreen HDR frame this compositor could not import has no draw: layer only (route 3). */
struct surface *un = hdr_unimported_fullscreen(dl, w, h);
if (!un) {
struct surface *lo = ahb_layer_only_candidate(w, h);
if (lo && is_hdr_surface(lo)) un = lo;
}
if (un && g_zero_copy) { hs->color = banner_color_of(un->resource); hs->who = un; return 3; }
hs->hf.tonemap = !banner_color_output();
if (dl->n <= 0 || !(hs->is_hdr = calloc((size_t)dl->n, 1))) return 0;
for (int i = 0; i < dl->n; i++) {
struct surface *s = surface_for_image(dl->d[i].img);
if (!is_hdr_surface(s)) continue;
hs->is_hdr[i] = 1;
hs->n_hdr++;
hs->color = banner_color_of(s->resource); /* later draws are above: the topmost wins */
hs->who = s;
}
if (!hs->n_hdr) { free(hs->is_hdr); hs->is_hdr = NULL; return 0; }
hs->hf.is_hdr = hs->is_hdr;
hs->hf.color = hs->color;
hs->hf.peak_nits = hs->color->max_cll > 0.0f ? hs->color->max_cll
: hs->color->has_st2086 ? hs->color->max_lum : 1000.0f;
if (framegen) return 2;
/* Route 0 when the game can be alone on its layer: no effects, zero-copy on, the fullscreen draw
* is the HDR game, and above it nothing - or one window the overlay layer can carry. Not while the
* HDR output switch is off: its frame as it stands is PQ, and only the composition can tone-map it. */
int li = g_zero_copy ? layer_candidate(dl, w, h) : -1;
int over = li >= 0 ? dl->n - 1 - li : 0;
if (!hs->hf.tonemap && !fx_on && li >= 0 && hs->is_hdr[li] &&
(over == 0 || (over == 1 && sc_layer_overlay_affordable())))
return 0;
hs->why = hs->hf.tonemap ? "HDR output is switched off in the drawer"
: fx_on ? "screen effects are on"
: !g_zero_copy ? "zero-copy presentation is off"
: (li >= 0 && hs->is_hdr[li]) ? "a window is above the game and this display cannot compose a second layer"
: "the HDR game is not one fullscreen window";
return 1;
}
/* One line per change of route (and of its reason, and of the HDR output switch). */
static void hdr_note_route(int route, const struct hdr_scene *hs) {
static int said = -1, said_tm = -1;
static const char *said_why;
const int tm = route != 0 && !banner_color_output(); /* route 0 never runs with the switch off */
if (route == said && tm == said_tm && (route != 1 || hs->why == said_why)) return;
int was = said;
said = route; said_why = hs->why; said_tm = tm;
char name[160] = "the HDR game";
if (hs->who) describe(hs->who, name, sizeof(name));
switch (route) {
case 1:
if (tm)
banner_log("color", "tone-mapped picture for %s: %s - the whole scene is composed and tone-mapped to SDR "
"(HDR peak %.0f nits rolled off to SDR white, screen effects applied after it) on the game's "
"display layer, untagged", name, hs->why ? hs->why : "?", hs->hf.peak_nits);
else
banner_log("color", "HDR picture for %s: %s - the whole scene is composed into one 10-bit PQ BT.2020 picture "
"(SDR content at %.0f nits, screen effects applied in 10-bit) on the game's display layer, tagged "
"BT2020_PQ", name, hs->why ? hs->why : "?", banner_color_sdr_white());
break;
case 2:
if (tm)
banner_log("color", "HDR with frame generation for %s, HDR output switched off: the scene is tone-mapped to "
"SDR and presented through an SDR screen swapchain", name);
else
banner_log("color", "HDR with frame generation for %s: the scene is composed into PQ and presented through "
"the screen swapchain (HDR10 where the surface offers it, else tone-mapped to SDR)", name);
break;
case 3:
if (tm)
banner_log("color", "HDR output is switched off, but %s's HDR frames cannot be imported by the compositor, "
"so nothing can tone-map them: they stay HDR on its own display layer", name);
break; /* otherwise hdr_note_precedence says it */
default:
if (was == 1 || was == 2)
banner_log("color", "%s is back on its own display layer (no composition needed)", name);
break;
}
}
/* Say when effects / frame generation start or stop being skipped for an HDR game (once per change). */
static void hdr_note_precedence(struct surface *hs, int fx_on, int framegen) {
static int fx_said = -1, fg_said = -1;
int fx_skip = hs && fx_on, fg_skip = hs && framegen;
char name[160] = "the game";
if (hs) describe(hs, name, sizeof(name));
if (fx_skip != fx_said) {
if (fx_skip)
banner_log("color", "screen effects are NOT applied to %s: its HDR frames cannot be imported by the "
"compositor, so only its own display layer can show them (as they are)", name);
else if (fx_said == 1)
banner_log("color", "screen effects apply to the scene again (no HDR game on its layer, or effects off)");
fx_said = fx_skip;
}
if (fg_skip != fg_said) {
if (fg_skip)
banner_log("color", "frame generation is NOT applied to %s: its HDR frames cannot be imported by the "
"compositor, so only its own display layer can show them (as they are)", name);
else if (fg_said == 1)
banner_log("color", "frame generation applies again (no HDR game on its layer, or frame generation off)");
fg_said = fg_skip;
}
}
/* This scene went through the copy path: count every HDR-described surface in it, with the reason. */
static void hdr_count_copied(const struct draw_list *dl, const char *reason) {
for (int i = 0; i < dl->n; i++) {
struct surface *s = surface_for_image(dl->d[i].img);
const struct banner_color *c = s ? banner_color_of(s->resource) : NULL;
if (!c || !c->dataspace) continue;
char name[160];
describe(s, name, sizeof(name));
banner_color_frame_copied(name, reason);
}
}
static void render_scene(void) {
struct draw_list dl = {0};
struct surface *s;
int w, h;
const int64_t t_scene = now_ns();
int copy = 0; /* this scene went through the screen swapchain (the perf line's "copy") */
g_dirty = 0;
g_hdr_unimported = NULL; /* found again while the scene is built (HDR gate open only) */
wl_list_for_each(s, &g_surfaces, link) s->drawn = 0;
scene_size(&w, &h);
if (w != g_scene_w || h != g_scene_h) {
if (g_desktop) banner_log("desktop", "size %dx%d", w, h);
else banner_log("desktop", "no desktop: showing %dx%d (largest window)", w, h);
g_scene_w = w;
g_scene_h = h;
}
if (g_desktop && !g_hide_shell) add_tree(&dl, g_desktop, 0, 0, 0);
wl_list_for_each(s, &g_toplevels, toplevel_link) {
if (g_desktop && !s->placed) continue; /* wait for its position */
if (g_hide_shell && !strcmp(client_name(wl_resource_get_client(s->resource)), "explorer.exe")) continue;
add_tree(&dl, s, s->placed ? s->x : 0, s->placed ? s->y : 0, 0);
}
int rendered = 0;
int fx_on = vkp_effects_active();
int framegen = vkp_framegen_active();
/* HDR (see hdr_plan): route 0 = the old paths below; 1 / 2 = composed; 3 = layer only, effects and
* frame generation skipped. Always 0 while the HDR gate is closed: nothing changes for that session. */
struct hdr_scene hs;
int hdr_route = hdr_plan(&dl, w, h, fx_on, framegen, &hs);
const char *hdr_copy = NULL; /* why this scene's HDR frames (if any) took the copy path */
if (banner_color_hdr_open()) {
hdr_note_route(hdr_route, &hs);
hdr_note_precedence(hdr_route == 3 ? hs.who : NULL, fx_on, framegen);
}
if (hdr_route == 3) { fx_on = 0; framegen = 0; }
if (hdr_route == 1) {
static int fell_back;
vkp_apply_window_request();
sc_layer_hide_overlay();
if (sc_layer_present_hdr_scene(dl.d, dl.n, &hs.hf, w, h, hs.color) == 0) {
rendered = vkp_base_black(w, h) == 0; /* black under the composed picture, presented only when it is not already */
if (fell_back) { fell_back = 0; banner_log("color", "the composed picture is on the game's display layer again"); }
} else {
hdr_route = 2; /* no layer this frame: the picture goes through the swapchain instead */
if (!fell_back) {
fell_back = 1;
banner_log("color", "the composed picture could not go on the game's display layer (no layer, or the "
"10-bit pass failed): it goes through the screen swapchain instead until that changes");
}
}
}
if (hdr_route == 2) {
int how = 0;
sc_layer_hide();
rendered = vkp_render_hdr(w, h, dl.d, dl.n, &hs.hf, &how) == 0;
copy = 1;
if (rendered && how == 1) banner_color_frame_shown(hs.color, BANNER_HDR_SWAPCHAIN, 0);
else if (rendered && how == 2) banner_color_frame_shown(hs.color, BANNER_HDR_TONEMAPPED, 0);
else if (rendered) hdr_copy = "the HDR composition pass is unavailable on this driver";
}
if (hdr_route == 0 || hdr_route == 3) {
/* What still forces the whole scene back through the compositor pass (and so through the app's
* own swapchain), and what no longer does:
* - frame generation ALWAYS does: its extra frames each need a present of their own on
* consecutive vblanks, and a display layer latches one buffer per refresh;
* - screen effects do NOT any more when the game is alone on screen - the chain runs and its
* result is copied into the game layer's own gralloc buffer (sc_layer_present_pass), so the
* game keeps its layer and the display does the scene -> output mapping;
* - screen effects DO when a window is drawn above the game: the chain has to see the whole
* scene to look the way it does on the copy path, and here the game is only part of it. */
const int pass_on = fx_on || framegen;
int li = (g_zero_copy && !framegen) ? layer_candidate(&dl, w, h) : -1;
int over = li >= 0 ? dl.n - 1 - li : 0; /* draws above the fullscreen game (0 or 1) */
const int fx_blocks = (li >= 0 && fx_on && over > 0);
if (fx_blocks) { li = -1; over = 0; }
/* A window above the game needs a SECOND display layer, and on some displays that costs the
* hardware composition the first layer was worth having (sc_layer_overlay_affordable). Where it
* does, the whole scene goes down the copy path exactly as it did before the layer set existed.
* Decided HERE, before the game is committed to a layer: deciding it after the present would
* show the game layer and hide it again on every frame. */
const int ov_blocks = (li >= 0 && over > 0 && !fx_blocks && !sc_layer_overlay_affordable());
if (ov_blocks) { li = -1; over = 0; }
const int blocked = framegen ? 1 : (fx_blocks ? 2 : (ov_blocks ? 3 : 0));
if (g_zero_copy && blocked != g_zero_copy_paused) {
g_zero_copy_paused = blocked;
if (blocked == 1)
banner_log("framegen", "zero-copy paused: frame generation needs the compositor pass");
else if (blocked == 2)
banner_log("effects", "zero-copy paused: a window above the game needs the compositor pass for the whole scene");
else if (blocked == 3)
banner_log("layer", "zero-copy paused: a window above the game would need a second display layer, "
"which this display cannot compose in hardware - whole scene on the copy path");
else
banner_log("effects", "zero-copy resumed: the game is back on its own display layer");
}
struct surface *ls = li >= 0 ? surface_for_image(dl.d[li].img) : NULL;
/* A frame this renderer could not import can only be shown on the layer, effects or not. */
if (li < 0) ls = ahb_layer_only_candidate(w, h);
/* HDR: the same, for a game's SUBSURFACE swapchain whose 10-bit frame the compositor could not
* import (NULL whenever the HDR gate is closed - nothing is ever noted then). */
if (li < 0 && !ls) ls = hdr_unimported_fullscreen(&dl, w, h);
if (ls && pass_on && li < 0 && !g_zero_copy_fx_skip_said) {
g_zero_copy_fx_skip_said = 1;
banner_log(framegen ? "framegen" : "effects",
"the game's frames cannot be imported by the compositor: shown zero-copy, %s skipped",
framegen ? "frame generation" : "effects");
}
if (li >= 0 || ls) {
/* Layer mode: the fullscreen window goes on the game layer and (at most) one window above
* it on the overlay layer; whatever is under the game is hidden by it, so the screen
* surface only needs to be black. The game's frame goes on its layer as is when it is one
* of the game's own AHardwareBuffers (zero-copy, ahb_swapchain.c), through the effects
* chain's result when a Look is on, else through one blit into the pool. If a layer can't
* take its frame, the whole scene is drawn the usual way. */
/* The layers go up FIRST and the base surface's black frame is presented after them. The
* order matters: a present holds an acquired swapchain image, and the acquire semaphore is
* a vblank gate - putting the effects chain or the layer transaction behind it costs a
* whole refresh (measured on the Pocket FIT: 72 fps that way, 111 fps this way, with Retro
* CRT at 1920x1080). The window request the app may have left is applied here instead,
* since vkp_render no longer runs before the layer path. */
vkp_apply_window_request();
int r = -1;
/* The game's own buffer can only go on the layer AS IS - with a Look on, the frame has to
* go through the chain first, so the raw buffer is skipped in favour of the pass (a frame
* this renderer could not import has no draw, li < 0, and stays zero-copy with the effects
* skipped, said once above). */
if (!(fx_on && li >= 0) && ls && ls->dmabuf_buf && ahb_swapchain_has_ahb(ls->dmabuf_buf))
r = ahb_swapchain_present(ls->dmabuf_buf, ls, w, h);
if (r != 0 && li >= 0)
r = fx_on ? sc_layer_present_pass(&dl.d[li], 1, w, h)
: sc_layer_present(dl.d[li].img, w, h, ls ? banner_surface_color(ls) : NULL);
if (r == 0) {
if (ls) ls->drawn = 1;
/* The one window above the game keeps the game off the copy path entirely: it goes on
* its own layer, cropped and placed by the display. */
int go[8], ov = 0;
if (over == 1 && li >= 0 && vkp_map_draw(&dl.d[li + 1], go))
ov = sc_layer_present_overlay(dl.d[li + 1].img, go) == 0 ? 1 : -1;
if (ov <= 0) sc_layer_hide_overlay();
if (ov < 0) { /* the overlay layer refused it: draw the whole scene the old way */
sc_layer_hide();
rendered = vkp_render(w, h, dl.d, dl.n) == 0;
copy = 1;
hdr_copy = "the overlay layer refused the window above it";
} else {
/* Black under the opaque layers. It is a plain black frame (no effects, no frame
* generation on it: they would only run on black), and it is presented only when the
* base surface does not already show one - entering layer mode, after a copy-path or
* frame-generation frame, after a swapchain rebuild. Every other layer frame draws and
* presents nothing here: the display layers are all that changes on screen. This loop
* is paced by the Choreographer's ticks (on_vsync), not by this present. */
rendered = vkp_base_black(w, h) == 0;
}
} else {
rendered = vkp_render(w, h, dl.d, dl.n) == 0;
copy = 1;
hdr_copy = "its display layer was unavailable this frame";
}
} else {
sc_layer_hide();
rendered = vkp_render(w, h, dl.d, dl.n) == 0;
copy = 1;
hdr_copy = ov_blocks ? "a window is above it and this display cannot compose a second layer in hardware"
: fx_blocks ? "a window is above it and screen effects need the whole scene"
: !g_zero_copy ? "zero-copy presentation is off"
: framegen ? "frame generation needs the compositor pass"
: "it is not the one fullscreen window (only that one gets its own display layer)";
}
} /* routes 0 and 3 */
free(hs.is_hdr);
if (hdr_copy && rendered && banner_color_hdr_open()) hdr_count_copied(&dl, hdr_copy);
if (rendered) {
int64_t t = now_ns();
g_stat_frames++;
/* A surface outside the scene (role-less, not placed yet, a hidden helper window such
* as wined3d's device window) was not shown, so its feedback is discarded rather than
* left pending: a FIFO present waits on it, and a client blocked there never commits
* again. */
wl_list_for_each(s, &g_surfaces, link) {
if (s->drawn) feedback_present_all(&s->feedback, t);
else feedback_discard_all(&s->feedback);
}
fire_all_frames();
} else {
/* No output surface yet (or it went away): keep clients paced without it, now and on a
* timer that re-arms itself until a window is back (see pace_without_output). */
pace_without_output();
if (!g_frame_timer)
g_frame_timer = wl_event_loop_add_timer(wl_display_get_event_loop(g_display),
on_frame_timer, NULL);
if (g_frame_timer) wl_event_source_timer_update(g_frame_timer, 16);
}
free(dl.d);
wl_display_flush_clients(g_display);
if (rendered && copy) g_perf.copy_scenes++;
g_perf.scenes++;
const int64_t scene_ns = now_ns() - t_scene;
g_perf.scene_ns += scene_ns;
if (scene_ns > g_perf.scene_max_ns) g_perf.scene_max_ns = scene_ns;
}
static int on_fallback_timer(void *data) {
g_fallback_armed = 0;
if (g_dirty) render_scene();
return 0;
}
static void schedule_render(void) {
g_dirty = 1;
if (!g_display) return;
/* With vsync ticks flowing the next tick renders; otherwise render on a timer. */
if (g_last_vsync_ns && now_ns() - g_last_vsync_ns < 100000000LL) return;
if (g_fallback_armed) return;
if (!g_fallback_timer)
g_fallback_timer = wl_event_loop_add_timer(wl_display_get_event_loop(g_display),
on_fallback_timer, NULL);
if (!g_fallback_timer) return;
g_fallback_armed = 1;
wl_event_source_timer_update(g_fallback_timer, 8);
}
/* A screen refresh (Choreographer tick): draw the newest state once. */
static void on_vsync(int64_t frame_time_ns) {
int64_t now = now_ns();
g_perf.ticks++;
if (g_last_vsync_ns) {
int64_t d = now - g_last_vsync_ns;
if (d > 3000000LL && d < 40000000LL) g_refresh_ns = (g_refresh_ns * 7 + d) / 8;
}
g_last_vsync_ns = now;
(void)frame_time_ns;
/* The app's surface may have been replaced or taken away since the last frame: apply that now
* (the app never waits for us), and redraw the scene onto a new one. */
if (vkp_apply_window_request()) g_dirty = 1;
/* A screen-effect setting changed (JNI, any thread): redraw so it shows on a static scene too. */
if (vkp_effects_sync()) g_dirty = 1;
if (g_dirty) render_scene();
}
/* ------------------------------------------------------------------ wl_seat */
static uint32_t now_ms(void) {
struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts);
return (uint32_t)(ts.tv_sec * 1000 + ts.tv_nsec / 1000000);
}
/* wl_pointer.set_cursor: the client tells us its pointer image, or asks for none. A NULL surface
* means "no cursor" and is authoritative. The surface itself is snapshotted on its next commit. */
static void pointer_set_cursor(struct wl_client *c, struct wl_resource *r, uint32_t serial,
struct wl_resource *surface, int32_t hx, int32_t hy) {
if (!surface) {
if (g_cursor_surface) g_cursor_surface->role = ROLE_NONE;
g_cursor_surface = NULL;
cursor_publish_hidden();
return;
}
struct surface *s = wl_resource_get_user_data(surface);
if (!s) return;
if (s->role != ROLE_NONE && s->role != ROLE_CURSOR) return; /* already has another role */
s->role = ROLE_CURSOR;
g_cursor_surface = s;
g_cursor_hx = hx;
g_cursor_hy = hy;
}
static void pointer_release(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); }
static const struct wl_pointer_interface pointer_impl = {
.set_cursor = pointer_set_cursor, .release = pointer_release,
};
static void pointer_res_destroy(struct wl_resource *r) {
constraints_pointer_gone(r);
relative_pointers_pointer_gone(r);
for (int i = 0; i < g_nptrs; i++)
if (g_ptrs[i].ptr == r) { g_ptrs[i] = g_ptrs[--g_nptrs]; break; }
}
static void keyboard_release(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); }
static const struct wl_keyboard_interface keyboard_impl = { .release = keyboard_release };
static void keyboard_res_destroy(struct wl_resource *r) {
for (int i = 0; i < g_nkbs; i++)
if (g_kbs[i].kb == r) { g_kbs[i] = g_kbs[--g_nkbs]; break; }
}
static void touch_release(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); }
static const struct wl_touch_interface touch_impl = { .release = touch_release };
static void touch_res_destroy(struct wl_resource *r) {
for (int i = 0; i < g_ntouches; i++)
if (g_touches[i].touch == r) { g_touches[i] = g_touches[--g_ntouches]; break; }
}
static void seat_get_pointer(struct wl_client *c, struct wl_resource *r, uint32_t id) {
struct wl_resource *p = wl_resource_create(c, &wl_pointer_interface, wl_resource_get_version(r), id);
if (!p) { wl_client_post_no_memory(c); return; }
wl_resource_set_implementation(p, &pointer_impl, NULL, pointer_res_destroy);
if (g_nptrs < MAX_PTRS) { g_ptrs[g_nptrs].ptr = p; g_ptrs[g_nptrs].focus = NULL; g_nptrs++; }
}
static void seat_get_keyboard(struct wl_client *c, struct wl_resource *r, uint32_t id) {
struct wl_resource *k = wl_resource_create(c, &wl_keyboard_interface, wl_resource_get_version(r), id);
if (!k) { wl_client_post_no_memory(c); return; }
wl_resource_set_implementation(k, &keyboard_impl, NULL, keyboard_res_destroy);
if (g_nkbs < MAX_PTRS) { g_kbs[g_nkbs].kb = k; g_kbs[g_nkbs].focus = NULL; g_nkbs++; }
/* Send the xkb keymap the app extracted to $XDG_RUNTIME_DIR/keymap.xkb. Without a keymap the
* client can't interpret our evdev key codes. */
const char *rt = getenv("XDG_RUNTIME_DIR");
char path[512];
snprintf(path, sizeof(path), "%s/keymap.xkb", rt ? rt : ".");
int fd = open(path, O_RDONLY | O_CLOEXEC);
if (fd >= 0) {
struct stat st;
if (fstat(fd, &st) == 0 && st.st_size > 0)
wl_keyboard_send_keymap(k, WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1, fd, (uint32_t)st.st_size);
close(fd);
} else {
WLOGE("keymap open failed: %s", path);
}
if (wl_resource_get_version(k) >= WL_KEYBOARD_REPEAT_INFO_SINCE_VERSION)
wl_keyboard_send_repeat_info(k, 25, 500);
}
static void seat_get_touch(struct wl_client *c, struct wl_resource *r, uint32_t id) {
struct wl_resource *t = wl_resource_create(c, &wl_touch_interface, wl_resource_get_version(r), id);
if (!t) { wl_client_post_no_memory(c); return; }
wl_resource_set_implementation(t, &touch_impl, NULL, touch_res_destroy);
if (g_ntouches < MAX_PTRS) {
g_touches[g_ntouches].touch = t;
g_touches[g_ntouches].focus = NULL;
g_ntouches++;
}
}
static void seat_release(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); }
static const struct wl_seat_interface seat_impl = {
.get_pointer = seat_get_pointer, .get_keyboard = seat_get_keyboard,
.get_touch = seat_get_touch, .release = seat_release,
};
static void bind_seat(struct wl_client *c, void *data, uint32_t ver, uint32_t id) {
struct wl_resource *r = wl_resource_create(c, &wl_seat_interface, ver, id);
wl_resource_set_implementation(r, &seat_impl, NULL, NULL);
wl_seat_send_capabilities(r, WL_SEAT_CAPABILITY_POINTER | WL_SEAT_CAPABILITY_KEYBOARD | WL_SEAT_CAPABILITY_TOUCH);
if (ver >= 2) wl_seat_send_name(r, "bannerlator-seat");
}
static struct seat_pointer *pointer_for(struct wl_client *client) {
for (int i = 0; i < g_nptrs; i++)
if (wl_resource_get_client(g_ptrs[i].ptr) == client) return &g_ptrs[i];
return NULL;
}
static struct seat_keyboard *keyboard_for(struct wl_client *client) {
for (int i = 0; i < g_nkbs; i++)
if (wl_resource_get_client(g_kbs[i].kb) == client) return &g_kbs[i];
return NULL;
}
/* A client may hold more than one wl_pointer/wl_keyboard: gamescope creates a second pair and
* reads input on its own thread, so an event sent only to the first object is never seen.
* Every delivery below walks all of the client's objects. */
#define for_each_pointer_of(client, sp) \
for (int _i = 0; _i < g_nptrs; _i++) \
if (((sp) = &g_ptrs[_i]), wl_resource_get_client((sp)->ptr) == (client))
#define for_each_keyboard_of(client, sk) \
for (int _i = 0; _i < g_nkbs; _i++) \
if (((sk) = &g_kbs[_i]), wl_resource_get_client((sk)->kb) == (client))
#define for_each_touch_of(client, st) \
for (int _i = 0; _i < g_ntouches; _i++) \
if (((st) = &g_touches[_i]), wl_resource_get_client((st)->touch) == (client))
/* Topmost window whose scene rectangle contains the point (no-desktop fallback). */
static struct surface *toplevel_at(double x, double y) {
struct surface *s;
wl_list_for_each_reverse(s, &g_toplevels, toplevel_link) {
int w, h, sx = s->placed ? s->x : 0, sy = s->placed ? s->y : 0;
surface_size(s, &w, &h);
if (x >= sx && y >= sy && x < sx + w && y < sy + h) return s;
}
return NULL;
}
static void pointer_focus(struct wl_resource *target, wl_fixed_t fx, wl_fixed_t fy) {
struct wl_client *client = wl_resource_get_client(target);
for (int i = 0; i < g_nptrs; i++) {
if (!g_ptrs[i].focus || g_ptrs[i].focus == target) continue;
wl_pointer_send_leave(g_ptrs[i].ptr, wl_display_next_serial(g_display), g_ptrs[i].focus);
if (wl_resource_get_version(g_ptrs[i].ptr) >= WL_POINTER_FRAME_SINCE_VERSION)
wl_pointer_send_frame(g_ptrs[i].ptr);
g_ptrs[i].focus = NULL;
}
struct seat_pointer *sp;
int entered = 0;
for_each_pointer_of(client, sp) {
if (sp->focus == target) continue;
sp->focus = target;
wl_pointer_send_enter(sp->ptr, wl_display_next_serial(g_display), target, fx, fy);
entered = 1;
}
if (entered) constraints_focus_entered(target, client);
}
static void keyboard_focus(struct wl_resource *target) {
struct wl_client *client = wl_resource_get_client(target);
struct seat_keyboard *sk;
int need = 0;
for_each_keyboard_of(client, sk) if (sk->focus != target) need = 1;
if (!need) return;
for (int i = 0; i < g_nkbs; i++) {
if (!g_kbs[i].focus || g_kbs[i].focus == target) continue;
wl_keyboard_send_leave(g_kbs[i].kb, wl_display_next_serial(g_display), g_kbs[i].focus);
g_kbs[i].focus = NULL;
}
struct wl_array keys;
wl_array_init(&keys);
for_each_keyboard_of(client, sk) {
if (sk->focus == target) continue;
sk->focus = target;
wl_keyboard_send_enter(sk->kb, wl_display_next_serial(g_display), target, &keys);
/* Baseline modifiers = none; Shift/Ctrl arrive as their own key events. */
wl_keyboard_send_modifiers(sk->kb, wl_display_next_serial(g_display), 0, 0, 0, 0);
}
wl_array_release(&keys);
banner_clipboard_keyboard_focus(client); /* wl_data_device selection follows focus */
}
/* ------------------------------------------------------------------ pointer constraints
* zwp_pointer_constraints_v1 and zwp_relative_pointer_manager_v1: what winewayland uses for
* SetCursorPos, ClipCursor and hidden-cursor (mouse-look) games.
*
* A lock freezes the pointer where it is: no wl_pointer.motion is sent while it holds, and
* every input delta (a relative event from the app, or the difference between absolute
* positions) reaches the locking program as zwp_relative_pointer_v1.relative_motion. A confine
* clamps the pointer to the surface (and the program's region inside it). Relative motion is
* also sent while unlocked, to any program holding a relative pointer.
*
* Input normally goes to the desktop surface; while a constraint holds, pointer focus moves
* to the constrained surface, so the constraining program receives the events itself
* (winewayland only enables relative motion for a window its pointer has entered). One
* constraint holds at a time; a newer request ends the older one. A lock's cursor position
* hint is applied on the surface's commit and is where the pointer ends up when the lock
* ends (winewayland's SetCursorPos: lock, hint, commit, unlock). */
struct constraint {
struct wl_list link; /* g_constraints */
struct wl_resource *resource; /* zwp_locked_pointer_v1 or zwp_confined_pointer_v1 */
struct wl_resource *pointer; /* the program's wl_pointer; NULL once released */
struct surface *surface; /* NULL once the surface is gone */
int is_lock;
uint32_t lifetime;
int active;
int defunct; /* a oneshot that ended, or surface/pointer gone */
struct region region, pending_region; /* confine area, surface-local; unset = whole surface */
int pending_region_set;
int hint_set, pending_hint_set; /* lock: cursor position hint, surface-local */
double hint_x, hint_y, pending_hint_x, pending_hint_y;
};
static struct wl_list g_constraints;
static struct constraint *g_active_constraint;
struct relative_pointer {
struct wl_list link; /* g_relative_pointers */
struct wl_resource *resource;
struct wl_resource *pointer; /* the program's wl_pointer; NULL once released */
};
static struct wl_list g_relative_pointers;
/* Last pointer position in scene coordinates (buttons and scrolls from the app's X-server
* input path arrive without one). */
static double g_ptr_x, g_ptr_y;
/* Last absolute input position: absolute input becomes deltas while the pointer is locked. */
static double g_raw_x, g_raw_y;
static int g_raw_valid;
/* JNI: the app switches its touch/mouse path to deltas while a lock holds and re-syncs its
* pointer to x,y when it ends. */
extern void banner_on_pointer_lock(int locked, int x, int y);
static void sync_lock_notify(void) {
static int last;
int locked = g_active_constraint && g_active_constraint->is_lock;
if (locked == last) return;
last = locked;
banner_on_pointer_lock(locked, (int)g_ptr_x, (int)g_ptr_y);
}
static void constraint_describe(const struct constraint *k, char *out, size_t size) {
if (k->surface) describe(k->surface, out, size);
else snprintf(out, size, "a closed window");
}
/* Scene position of the constrained surface. */
static void constraint_origin(const struct constraint *k, int *x, int *y) {
*x = *y = 0;
if (k->surface && k->surface != g_desktop && k->surface->placed) { *x = k->surface->x; *y = k->surface->y; }
}
/* Keep x,y (scene coordinates) inside the confine area. */
static void confine_clamp(const struct constraint *k, double *x, double *y) {
int ox, oy, w, h, rx = 0, ry = 0, rw, rh;
if (!k->surface) return;
constraint_origin(k, &ox, &oy);
surface_size(k->surface, &w, &h);
rw = w; rh = h;
if (k->region.set) {
int x2 = rx + rw < k->region.x + k->region.w ? rx + rw : k->region.x + k->region.w;
int y2 = ry + rh < k->region.y + k->region.h ? ry + rh : k->region.y + k->region.h;
if (k->region.x > rx) rx = k->region.x;
if (k->region.y > ry) ry = k->region.y;
rw = x2 - rx; rh = y2 - ry;
}
if (rw <= 0 || rh <= 0) return;
if (*x < ox + rx) *x = ox + rx;
if (*x > ox + rx + rw - 1) *x = ox + rx + rw - 1;
if (*y < oy + ry) *y = oy + ry;
if (*y > oy + ry + rh - 1) *y = oy + ry + rh - 1;
}
static void constraint_send_state(struct constraint *k, int on) {
if (k->is_lock) {
if (on) zwp_locked_pointer_v1_send_locked(k->resource);
else zwp_locked_pointer_v1_send_unlocked(k->resource);
} else {
if (on) zwp_confined_pointer_v1_send_confined(k->resource);
else zwp_confined_pointer_v1_send_unconfined(k->resource);
}
}
/* A constraint stops holding. tell_client = 0 when its resource is being destroyed. */
static void constraint_end(struct constraint *k, int tell_client, const char *why) {
char name[160];
if (!k->active) return;
k->active = 0;
if (g_active_constraint == k) g_active_constraint = NULL;
if (k->lifetime == ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_ONESHOT) k->defunct = 1;
if (k->is_lock && k->hint_set && k->surface) {
int ox, oy;
constraint_origin(k, &ox, &oy);
g_ptr_x = ox + k->hint_x;
g_ptr_y = oy + k->hint_y;
}
g_raw_valid = 0;
if (tell_client) constraint_send_state(k, 0);
constraint_describe(k, name, sizeof(name));
banner_log("pointer", "%s: %s (%s), pointer at %d,%d", k->is_lock ? "unlocked" : "unconfined",
name, why, (int)g_ptr_x, (int)g_ptr_y);
sync_lock_notify();
}
static void constraint_activate(struct constraint *k) {
char name[160];
int ox, oy;
if (k->active || k->defunct || !k->surface || !k->pointer) return;
if (g_active_constraint && g_active_constraint != k)
constraint_end(g_active_constraint, 1, "replaced by a newer request");
k->active = 1;
g_active_constraint = k;
if (!k->is_lock) confine_clamp(k, &g_ptr_x, &g_ptr_y);
/* Pointer focus follows the constraint: the program gets the events itself. */
constraint_origin(k, &ox, &oy);
pointer_focus(k->surface->resource, wl_fixed_from_double(g_ptr_x - ox), wl_fixed_from_double(g_ptr_y - oy));
constraint_send_state(k, 1);
constraint_describe(k, name, sizeof(name));
if (k->is_lock)
banner_log("pointer", "locked: %s (%s), pointer frozen at %d,%d", name,
k->lifetime == ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_ONESHOT ? "oneshot" : "persistent",
(int)g_ptr_x, (int)g_ptr_y);
else if (k->region.set)
banner_log("pointer", "confined: %s (%s) to %dx%d at %d,%d of the window", name,
k->lifetime == ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_ONESHOT ? "oneshot" : "persistent",
k->region.w, k->region.h, k->region.x, k->region.y);
else
banner_log("pointer", "confined: %s (%s) to the whole window", name,
k->lifetime == ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_ONESHOT ? "oneshot" : "persistent");
sync_lock_notify();
wl_display_flush_clients(g_display);
}
/* Pointer focus entered target: a persistent constraint waiting on it takes hold again. */
static void constraints_focus_entered(struct wl_resource *target, struct wl_client *client) {
struct constraint *k;
if (g_active_constraint) return;
wl_list_for_each(k, &g_constraints, link) {
if (k->active || k->defunct || !k->surface || !k->pointer) continue;
if (k->surface->resource != target || wl_resource_get_client(k->pointer) != client) continue;
constraint_activate(k);
return;
}
}
static void constraints_surface_gone(struct surface *s) {
struct constraint *k;
wl_list_for_each(k, &g_constraints, link) {
if (k->surface != s) continue;
constraint_end(k, 1, "window closed");
k->surface = NULL;
k->defunct = 1;
}
}
static void constraints_pointer_gone(struct wl_resource *pointer) {
struct constraint *k;
wl_list_for_each(k, &g_constraints, link) {
if (k->pointer != pointer) continue;
constraint_end(k, 1, "pointer released");
k->pointer = NULL;
k->defunct = 1;
}
}
/* Double-buffered state (position hint, confine region) applies on the surface's commit. */
static void constraints_surface_commit(struct surface *s) {
struct constraint *k;
wl_list_for_each(k, &g_constraints, link) {
if (k->surface != s) continue;
if (k->pending_hint_set) {
k->hint_set = 1;
k->hint_x = k->pending_hint_x;
k->hint_y = k->pending_hint_y;
k->pending_hint_set = 0;
if (k->active && k->is_lock) {
int ox, oy;
constraint_origin(k, &ox, &oy);
g_ptr_x = ox + k->hint_x;
g_ptr_y = oy + k->hint_y;
if (log_budget())
banner_log("pointer", "position hint: pointer moved to %d,%d", (int)g_ptr_x, (int)g_ptr_y);
}
}
if (k->pending_region_set) {
k->region = k->pending_region;
k->pending_region_set = 0;
if (k->active && !k->is_lock) confine_clamp(k, &g_ptr_x, &g_ptr_y);
}
}
}
static void constraint_resource_destroy(struct wl_resource *r) {
struct constraint *k = wl_resource_get_user_data(r);
if (!k) return;
constraint_end(k, 0, "released by the program");
wl_list_remove(&k->link);
free(k);
}
static void constraint_destroy_req(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); }
static void locked_pointer_set_cursor_position_hint(struct wl_client *c, struct wl_resource *r,
wl_fixed_t x, wl_fixed_t y) {
struct constraint *k = wl_resource_get_user_data(r);
if (!k) return;
k->pending_hint_set = 1;
k->pending_hint_x = wl_fixed_to_double(x);
k->pending_hint_y = wl_fixed_to_double(y);
}
static void locked_pointer_set_region(struct wl_client *c, struct wl_resource *r, struct wl_resource *region) {
/* A lock's region only says where it may take hold; a lock here takes hold anywhere. */
}
static const struct zwp_locked_pointer_v1_interface locked_pointer_impl = {
.destroy = constraint_destroy_req,
.set_cursor_position_hint = locked_pointer_set_cursor_position_hint,
.set_region = locked_pointer_set_region,
};
static void confined_pointer_set_region(struct wl_client *c, struct wl_resource *r, struct wl_resource *region) {
struct constraint *k = wl_resource_get_user_data(r);
struct region *rg = region ? wl_resource_get_user_data(region) : NULL;
if (!k) return;
k->pending_region_set = 1;
if (rg) k->pending_region = *rg;
else memset(&k->pending_region, 0, sizeof(k->pending_region));
}
static const struct zwp_confined_pointer_v1_interface confined_pointer_impl = {
.destroy = constraint_destroy_req,
.set_region = confined_pointer_set_region,
};
static void constraint_create(struct wl_client *c, struct wl_resource *r, uint32_t id,
struct wl_resource *surface_res, struct wl_resource *pointer_res,
struct wl_resource *region_res, uint32_t lifetime, int is_lock) {
struct surface *s = wl_resource_get_user_data(surface_res);
struct constraint *k;
struct wl_resource *res;
char name[160];
wl_list_for_each(k, &g_constraints, link) {
if (k->defunct || k->surface != s || k->pointer != pointer_res) continue;
wl_resource_post_error(r, ZWP_POINTER_CONSTRAINTS_V1_ERROR_ALREADY_CONSTRAINED,
"the surface already has a pointer constraint for this pointer");
return;
}
res = wl_resource_create(c, is_lock ? &zwp_locked_pointer_v1_interface : &zwp_confined_pointer_v1_interface,
wl_resource_get_version(r), id);
if (!res || !(k = calloc(1, sizeof(*k)))) { wl_client_post_no_memory(c); return; }
k->resource = res;
k->pointer = pointer_res;
k->surface = s;
k->is_lock = is_lock;
k->lifetime = lifetime == ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_PERSISTENT
? ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_PERSISTENT : ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_ONESHOT;
if (!is_lock && region_res) {
struct region *rg = wl_resource_get_user_data(region_res);
if (rg) k->region = *rg;
}
wl_resource_set_implementation(res, is_lock ? (const void *)&locked_pointer_impl : (const void *)&confined_pointer_impl,
k, constraint_resource_destroy);
wl_list_insert(g_constraints.prev, &k->link);
if (!s) { k->defunct = 1; return; }
constraint_describe(k, name, sizeof(name));
banner_log("pointer", "%s requested by %s for %s", is_lock ? "lock" : "confine", client_name(c), name);
constraint_activate(k);
}
static void pointer_constraints_destroy(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); }
static void pointer_constraints_lock_pointer(struct wl_client *c, struct wl_resource *r, uint32_t id,
struct wl_resource *surface, struct wl_resource *pointer,
struct wl_resource *region, uint32_t lifetime) {
constraint_create(c, r, id, surface, pointer, region, lifetime, 1);
}
static void pointer_constraints_confine_pointer(struct wl_client *c, struct wl_resource *r, uint32_t id,
struct wl_resource *surface, struct wl_resource *pointer,
struct wl_resource *region, uint32_t lifetime) {
constraint_create(c, r, id, surface, pointer, region, lifetime, 0);
}
static const struct zwp_pointer_constraints_v1_interface pointer_constraints_impl = {
.destroy = pointer_constraints_destroy,
.lock_pointer = pointer_constraints_lock_pointer,
.confine_pointer = pointer_constraints_confine_pointer,
};
static void bind_pointer_constraints(struct wl_client *c, void *data, uint32_t ver, uint32_t id) {
struct wl_resource *r = wl_resource_create(c, &zwp_pointer_constraints_v1_interface, ver, id);
if (!r) { wl_client_post_no_memory(c); return; }
wl_resource_set_implementation(r, &pointer_constraints_impl, NULL, NULL);
}
/* --- relative pointers --- */
static void relative_pointer_resource_destroy(struct wl_resource *r) {
struct relative_pointer *rp = wl_resource_get_user_data(r);
if (!rp) return;
banner_log("pointer", "relative pointer released by %s", client_name(wl_resource_get_client(r)));
wl_list_remove(&rp->link);
free(rp);
}
static void relative_pointer_destroy_req(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); }
static const struct zwp_relative_pointer_v1_interface relative_pointer_impl = {
.destroy = relative_pointer_destroy_req,
};
static void relative_pointer_manager_destroy(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); }
static void relative_pointer_manager_get(struct wl_client *c, struct wl_resource *r, uint32_t id,
struct wl_resource *pointer) {
struct wl_resource *res = wl_resource_create(c, &zwp_relative_pointer_v1_interface, wl_resource_get_version(r), id);
struct relative_pointer *rp;
if (!res || !(rp = calloc(1, sizeof(*rp)))) { wl_client_post_no_memory(c); return; }
rp->resource = res;
rp->pointer = pointer;
wl_resource_set_implementation(res, &relative_pointer_impl, rp, relative_pointer_resource_destroy);
wl_list_insert(g_relative_pointers.prev, &rp->link);
banner_log("pointer", "relative pointer created for %s (motion arrives as deltas)", client_name(c));
}
static const struct zwp_relative_pointer_manager_v1_interface relative_pointer_manager_impl = {
.destroy = relative_pointer_manager_destroy,
.get_relative_pointer = relative_pointer_manager_get,
};
static void bind_relative_pointer_manager(struct wl_client *c, void *data, uint32_t ver, uint32_t id) {
struct wl_resource *r = wl_resource_create(c, &zwp_relative_pointer_manager_v1_interface, ver, id);
if (!r) { wl_client_post_no_memory(c); return; }
wl_resource_set_implementation(r, &relative_pointer_manager_impl, NULL, NULL);
}
static void relative_pointers_pointer_gone(struct wl_resource *pointer) {
struct relative_pointer *rp;
wl_list_for_each(rp, &g_relative_pointers, link)
if (rp->pointer == pointer) rp->pointer = NULL;
}
/* relative_motion to every relative pointer of the program owning the focus (winewayland wants
* it whenever it holds one, locked or not). Returns whether any was sent. */
static int send_relative_motion(struct wl_client *client, double dx, double dy) {
struct relative_pointer *rp;
struct timespec ts;
uint64_t ut;
int sent = 0;
if (dx == 0 && dy == 0) return 0;
clock_gettime(CLOCK_MONOTONIC, &ts);
ut = (uint64_t)ts.tv_sec * 1000000ULL + (uint64_t)ts.tv_nsec / 1000;
wl_list_for_each(rp, &g_relative_pointers, link) {
if (!rp->pointer || wl_resource_get_client(rp->pointer) != client) continue;
zwp_relative_pointer_v1_send_relative_motion(rp->resource, (uint32_t)(ut >> 32), (uint32_t)ut,
wl_fixed_from_double(dx), wl_fixed_from_double(dy),
wl_fixed_from_double(dx), wl_fixed_from_double(dy));
sent = 1;
}
return sent;
}
/* ------------------------------------------------------------------ pointer events */
/* One pointer event: a motion, then an optional button change (button 0 = none). Absolute
* x,y are scene coordinates; with `relative` they are a delta. While a lock holds the pointer
* stays put and the delta (or the change between absolute positions) becomes relative motion
* for the locking program; a confine keeps the pointer inside its area. */
static void pointer_input(double x, double y, int relative, uint32_t button, int pressed) {
struct surface *target;
struct constraint *k = g_active_constraint;
double dx, dy;
if (relative) {
dx = x; dy = y;
} else {
dx = g_raw_valid ? x - g_raw_x : 0;
dy = g_raw_valid ? y - g_raw_y : 0;
g_raw_x = x; g_raw_y = y; g_raw_valid = 1;
}
if (k && k->is_lock) {
target = k->surface;
} else {
if (relative) {
int w, h;
scene_size(&w, &h);
x = g_ptr_x + dx; y = g_ptr_y + dy;
if (x < 0) x = 0;
if (y < 0) y = 0;
if (x > w - 1) x = w - 1;
if (y > h - 1) y = h - 1;
}
if (k) confine_clamp(k, &x, &y);
g_ptr_x = x; g_ptr_y = y;
if (k) {
target = k->surface;
} else if (g_desktop) {
target = g_desktop;
} else {
target = g_grab ? g_grab : toplevel_at(x, y);
if (button && pressed) { g_grab = target; g_key_target = target; }
if (button && !pressed) g_grab = NULL;
}
}
if (!target) return;
struct wl_client *client = wl_resource_get_client(target->resource);
struct seat_pointer *sp;
if (!pointer_for(client)) return;
int tx = 0, ty = 0;
if (target != g_desktop && target->placed) { tx = target->x; ty = target->y; }
wl_fixed_t fx = wl_fixed_from_double(g_ptr_x - tx), fy = wl_fixed_from_double(g_ptr_y - ty);
uint32_t t = now_ms();
pointer_focus(target->resource, fx, fy);
if (!(k && k->is_lock))
for_each_pointer_of(client, sp) wl_pointer_send_motion(sp->ptr, t, fx, fy);
send_relative_motion(client, dx, dy);
for_each_pointer_of(client, sp) {
if (button)
wl_pointer_send_button(sp->ptr, wl_display_next_serial(g_display), t, button,
pressed ? WL_POINTER_BUTTON_STATE_PRESSED : WL_POINTER_BUTTON_STATE_RELEASED);
if (wl_resource_get_version(sp->ptr) >= WL_POINTER_FRAME_SINCE_VERSION)
wl_pointer_send_frame(sp->ptr);
}
/* A click lands text input (the soft keyboard's IME text) on the window under the pointer:
* that's where Wine's focus goes, and winewayland routes IME updates per process. */
if (button && pressed && !(k && k->is_lock)) {
struct surface *clicked = toplevel_at(g_ptr_x, g_ptr_y);
if (clicked) g_ime_click = clicked;
banner_text_input_refocus();
}
wl_display_flush_clients(g_display);
}
/* Absolute motion to scene x,y, then an optional button change. */
static void pointer_event(double x, double y, uint32_t button, int pressed) {
pointer_input(x, y, 0, button, pressed);
}
/* A delta from the app's relative input path (Relative Mouse, captured mouse, stick-as-mouse). */
static void pointer_delta(double dx, double dy) {
pointer_input(dx, dy, 1, 0, 0);
}
/* A button change at the current pointer position. */
static void pointer_button(uint32_t button, int pressed) {
pointer_input(0, 0, 1, button, pressed);
}
/* Java touch events arrive in INPUT_SPACE over the whole output: action 0=press, 1=move, 2=release.
* Output pixels go through the inverse of the scale mode's mapping (letterbox bars, FILL crop, a
* TOP/BOTTOM half), so the touch lands on the scene pixel that is drawn under the finger. */
static void deliver_pointer(const struct input_msg *m) {
int w, h, ow, oh;
double x, y;
scene_size(&w, &h);
vkp_output_size(&ow, &oh);
if (ow <= 0 || oh <= 0 ||
!vkp_output_to_scene((double)m->p2 * ow / INPUT_SPACE_W, (double)m->p3 * oh / INPUT_SPACE_H, &x, &y)) {
x = (double)m->p2 * w / INPUT_SPACE_W; /* nothing drawn yet: plain stretch */
y = (double)m->p3 * h / INPUT_SPACE_H;
}
if (x < 0) x = 0;
if (y < 0) y = 0;
if (x > w - 1) x = w - 1;
if (y > h - 1) y = h - 1;
pointer_event(x, y, m->p1 == 1 ? 0 : BTN_LEFT, m->p1 == 0);
}
static void input_output_to_scene(int px, int py, double *x, double *y) {
int w, h, ow, oh;
scene_size(&w, &h);
vkp_output_size(&ow, &oh);
if (ow <= 0 || oh <= 0 ||
!vkp_output_to_scene((double)px * ow / INPUT_SPACE_W, (double)py * oh / INPUT_SPACE_H, x, y)) {
*x = (double)px * w / INPUT_SPACE_W;
*y = (double)py * h / INPUT_SPACE_H;
}
if (*x < 0) *x = 0;
if (*y < 0) *y = 0;
if (*x > w - 1) *x = w - 1;
if (*y > h - 1) *y = h - 1;
}
static int touch_points_for_client(struct wl_client *client) {
for (int i = 0; i < MAX_TOUCH_POINTS; i++) {
struct surface *surface = g_touch_points[i].surface;
if (surface && wl_resource_get_client(surface->resource) == client) return 1;
}
return 0;
}
static void touch_frame(struct wl_client *client) {
struct seat_touch *st;
for_each_touch_of(client, st)
if (wl_resource_get_version(st->touch) >= WL_TOUCH_FRAME_SINCE_VERSION)
wl_touch_send_frame(st->touch);
}
static void touch_cancel_client(struct wl_client *client) {
struct seat_touch *st;
int release_pointer = 0;
for_each_touch_of(client, st) {
if (st->focus) wl_touch_send_cancel(st->touch);
st->focus = NULL;
}
for (int i = 0; i < MAX_TOUCH_POINTS; i++) {
struct surface *surface = g_touch_points[i].surface;
if (surface && wl_resource_get_client(surface->resource) == client) {
release_pointer |= g_touch_points[i].pointer_fallback;
memset(&g_touch_points[i], 0, sizeof(g_touch_points[i]));
}
}
if (release_pointer) pointer_button(BTN_LEFT, 0);
wl_display_flush_clients(g_display);
}
static void touch_cancel_all(void) {
int release_pointer = 0;
for (int i = 0; i < g_ntouches; i++) {
if (g_touches[i].focus) wl_touch_send_cancel(g_touches[i].touch);
g_touches[i].focus = NULL;
}
for (int i = 0; i < MAX_TOUCH_POINTS; i++)
release_pointer |= g_touch_points[i].pointer_fallback;
memset(g_touch_points, 0, sizeof(g_touch_points));
if (release_pointer) pointer_button(BTN_LEFT, 0);
wl_display_flush_clients(g_display);
}
static void touch_cancel_surface(struct surface *surface) {
struct wl_client *client = wl_resource_get_client(surface->resource);
for (int i = 0; i < MAX_TOUCH_POINTS; i++)
if (g_touch_points[i].surface == surface) {
touch_cancel_client(client);
return;
}
}
static struct surface *touch_target_at(double x, double y) {
struct surface *target = toplevel_at(x, y);
if (target) return target;
if (g_desktop) return g_desktop;
return g_grab ? g_grab : g_key_target;
}
static void touch_down(int id, double x, double y) {
if (id < 0 || id >= MAX_TOUCH_POINTS) return;
struct surface *target = touch_target_at(x, y);
if (!target) return;
struct wl_client *client = wl_resource_get_client(target->resource);
/* A down on an id that is still held means that sequence's up was lost: end it first. The
* fingers still down are counted only after that - counted before, a lost up left any_active
* set, and on the pointer path every later touch was dropped (a tap or swipe did nothing). */
if (g_touch_points[id].surface) touch_cancel_client(client);
int any_active = 0;
for (int i = 0; i < MAX_TOUCH_POINTS; i++) {
struct surface *active = g_touch_points[i].surface;
if (!active) continue;
if (active != target) {
touch_cancel_all();
any_active = 0;
break;
}
any_active = 1;
}
struct seat_touch *st;
int has_touch = 0;
for_each_touch_of(client, st) has_touch = 1;
/* One line per gesture (first finger), so a test can see where a touch went. */
if (!any_active) {
char name[160];
describe(target, name, sizeof(name));
banner_log("touch", "%s gets %s", name, has_touch ? "touch" : "the pointer (it takes no touch)");
}
if (!has_touch) {
if (any_active) return; /* one finger drives the pointer; a second one is ignored */
g_touch_points[id].surface = target;
g_touch_points[id].pointer_fallback = 1;
pointer_event(x, y, BTN_LEFT, 1);
wl_display_flush_clients(g_display);
return;
}
int tx = 0, ty = 0;
if (target != g_desktop && target->placed) { tx = target->x; ty = target->y; }
wl_fixed_t fx = wl_fixed_from_double(x - tx), fy = wl_fixed_from_double(y - ty);
int sent = 0;
for_each_touch_of(client, st) {
st->focus = target->resource;
wl_touch_send_down(st->touch, wl_display_next_serial(g_display), now_ms(),
target->resource, id, fx, fy);
sent = 1;
}
if (!sent) return;
g_touch_points[id].surface = target;
touch_frame(client);
wl_display_flush_clients(g_display);
}
static void touch_motion(int id, double x, double y) {
if (id < 0 || id >= MAX_TOUCH_POINTS) return;
struct surface *target = g_touch_points[id].surface;
if (!target) return;
if (g_touch_points[id].pointer_fallback) {
pointer_event(x, y, 0, 0);
wl_display_flush_clients(g_display);
return;
}
struct wl_client *client = wl_resource_get_client(target->resource);
int tx = 0, ty = 0;
if (target != g_desktop && target->placed) { tx = target->x; ty = target->y; }
wl_fixed_t fx = wl_fixed_from_double(x - tx), fy = wl_fixed_from_double(y - ty);
struct seat_touch *st;
for_each_touch_of(client, st)
if (st->focus == target->resource)
wl_touch_send_motion(st->touch, now_ms(), id, fx, fy);
touch_frame(client);
wl_display_flush_clients(g_display);
}
static void touch_up(int id, double x, double y) {
if (id < 0 || id >= MAX_TOUCH_POINTS) return;
struct surface *target = g_touch_points[id].surface;
if (!target) return;
if (g_touch_points[id].pointer_fallback) {
memset(&g_touch_points[id], 0, sizeof(g_touch_points[id]));
pointer_event(x, y, BTN_LEFT, 0);
wl_display_flush_clients(g_display);
return;
}
struct wl_client *client = wl_resource_get_client(target->resource);
struct seat_touch *st;
for_each_touch_of(client, st)
if (st->focus == target->resource)
wl_touch_send_up(st->touch, wl_display_next_serial(g_display), now_ms(), id);
memset(&g_touch_points[id], 0, sizeof(g_touch_points[id]));
touch_frame(client);
if (!touch_points_for_client(client))
for_each_touch_of(client, st) st->focus = NULL;
wl_display_flush_clients(g_display);
}
static void deliver_touch(const struct input_msg *m) {
double x, y;
input_output_to_scene(m->p3, m->p4, &x, &y);
if (m->p1 == 0) touch_down(m->p2, x, y);
else if (m->p1 == 1) touch_motion(m->p2, x, y);
else if (m->p1 == 2) touch_up(m->p2, x, y);
else if (m->p1 == 3) touch_cancel_all();
}
static void key_event(uint32_t evdev, int pressed);
struct wl_resource *banner_ime_target(void);
static void deliver_key(const struct input_msg *m) {
key_event((uint32_t)m->p1, m->p2);
}
/* Vertical wheel steps (negative = up) at the current pointer position. */
static void scroll_event(int steps) {
struct surface *target = g_active_constraint ? g_active_constraint->surface
: g_desktop ? g_desktop : (g_grab ? g_grab : toplevel_at(g_ptr_x, g_ptr_y));
if (!target || !steps) return;
struct wl_client *client = wl_resource_get_client(target->resource);
struct seat_pointer *sp;
if (!pointer_for(client)) return;
int tx = 0, ty = 0;
if (target != g_desktop && target->placed) { tx = target->x; ty = target->y; }
uint32_t t = now_ms();
pointer_focus(target->resource, wl_fixed_from_double(g_ptr_x - tx), wl_fixed_from_double(g_ptr_y - ty));
for_each_pointer_of(client, sp) {
/* From version 8 the discrete event is replaced by value120 and must not be sent. */
if (wl_resource_get_version(sp->ptr) >= WL_POINTER_AXIS_VALUE120_SINCE_VERSION)
wl_pointer_send_axis_value120(sp->ptr, WL_POINTER_AXIS_VERTICAL_SCROLL, steps * 120);
else if (wl_resource_get_version(sp->ptr) >= WL_POINTER_AXIS_DISCRETE_SINCE_VERSION)
wl_pointer_send_axis_discrete(sp->ptr, WL_POINTER_AXIS_VERTICAL_SCROLL, steps);
wl_pointer_send_axis(sp->ptr, t, WL_POINTER_AXIS_VERTICAL_SCROLL, wl_fixed_from_int(steps * 10));
if (wl_resource_get_version(sp->ptr) >= WL_POINTER_FRAME_SINCE_VERSION)
wl_pointer_send_frame(sp->ptr);
}
wl_display_flush_clients(g_display);
}
static void key_event(uint32_t evdev, int pressed) {
/* Keys go to the program window the user last clicked (else the topmost non-shell window),
* never to Wine's desktop surface: winewayland hands each key to the hwnd of the surface that
* holds keyboard focus, and a key handed to explorer's desktop hwnd is queued to explorer's
* thread, not to the foreground program (X11 delivers keys to the focused app window, so
* this mirrors it). The desktop itself is the last resort. */
struct surface *target = NULL;
struct wl_resource *ime = banner_ime_target();
if (ime) target = wl_resource_get_user_data(ime);
if (!target && g_key_target && g_key_target->mapped && g_key_target != g_desktop) target = g_key_target;
if (!target) target = g_desktop;
if (!target) {
struct surface *s;
wl_list_for_each_reverse(s, &g_toplevels, toplevel_link) { target = s; break; }
}
if (!target) return;
struct wl_client *client = wl_resource_get_client(target->resource);
struct seat_keyboard *sk;
if (!keyboard_for(client)) return;
keyboard_focus(target->resource);
for_each_keyboard_of(client, sk)
wl_keyboard_send_key(sk->kb, wl_display_next_serial(g_display), now_ms(), evdev,
pressed ? WL_KEYBOARD_KEY_STATE_PRESSED : WL_KEYBOARD_KEY_STATE_RELEASED);
wl_display_flush_clients(g_display);
}
/* wl event-loop callback: drain queued input events written by the Android UI thread. */
static int on_input_readable(int fd, uint32_t mask, void *data) {
struct input_msg m;
while (read(fd, &m, sizeof(m)) == (ssize_t)sizeof(m)) {
switch (m.type) {
case 1: deliver_key(&m); break;
case 2: pointer_event(m.p1, m.p2, 0, 0); break; /* scene motion */
case 3: pointer_button(m.p1, m.p2); break; /* button at pointer */
case 4: scroll_event(m.p1); break;
case 5: on_vsync(((int64_t)m.p1 << 32) | (uint32_t)m.p2); break;
case 6: pointer_delta(m.p1 / 256.0, m.p2 / 256.0); break; /* relative motion, 1/256 px */
case 7: deliver_touch(&m); break;
default: deliver_pointer(&m); break;
}
}
return 0;
}
/* Called from JNI (Android UI thread). Queues a pointer event; the compositor thread
* dispatches it. x/y are in INPUT_SPACE (0..1919, 0..1079) over the whole output. */
void banner_wayland_send_pointer(int action, int x, int y) {
if (g_input_pipe[1] < 0) return;
struct input_msg m = { 0, action, x, y, 0 };
ssize_t n = write(g_input_pipe[1], &m, sizeof(m));
(void)n;
}
void banner_wayland_send_touch(int action, int pointer_id, int x, int y) {
if (g_input_pipe[1] < 0) return;
struct input_msg m = { 7, action, pointer_id, x, y };
ssize_t n = write(g_input_pipe[1], &m, sizeof(m));
(void)n;
}
/* Called from JNI. Queues a key event. evdev = Linux input keycode (e.g. KEY_A=30); state 1=down 0=up. */
void banner_wayland_send_key(int evdev, int state) {
if (g_input_pipe[1] < 0) return;
struct input_msg m = { 1, evdev, state, 0, 0 };
ssize_t n = write(g_input_pipe[1], &m, sizeof(m));
(void)n;
}
/* Called from JNI on every screen refresh (Choreographer). */
void banner_wayland_vsync(int64_t frame_time_ns) {
if (g_input_pipe[1] < 0) return;
struct input_msg m = { 5, (int)(frame_time_ns >> 32), (int)(uint32_t)frame_time_ns, 0, 0 };
ssize_t n = write(g_input_pipe[1], &m, sizeof(m));
(void)n;
}
/* Called from JNI with the app's X-server input (on-screen controls, mouse): type 2 = motion
* to scene x,y; 3 = evdev button a pressed/released (b); 4 = a wheel steps (negative = up);
* 6 = relative motion by a,b in 1/256 pixel (the app's Relative Mouse / captured-mouse path). */
void banner_wayland_send_scene_input(int type, int a, int b) {
if (g_input_pipe[1] < 0) return;
struct input_msg m = { type, a, b, 0, 0 };
ssize_t n = write(g_input_pipe[1], &m, sizeof(m));
(void)n;
}
/* ------------------------------------------------------------------ extension module hooks
* What wl_clipboard.c / wl_text_input.c need from the scene (see banner_ext.h). */
const char *banner_client_name(struct wl_client *client) { return client_name(client); }
struct wl_display *banner_get_display(void) { return g_display; }
void banner_request_redraw(void) { schedule_render(); }
/* The surface text input follows: the last clicked mapped program window, else the topmost
* mapped window not owned by the desktop's process (explorer). NULL when there is none. */
struct wl_resource *banner_ime_target(void) {
struct surface *s;
if (g_ime_click && g_ime_click->mapped) return g_ime_click->resource;
struct wl_client *shell = g_desktop ? wl_resource_get_client(g_desktop->resource) : NULL;
wl_list_for_each_reverse(s, &g_toplevels, toplevel_link)
if (!shell || wl_resource_get_client(s->resource) != shell) return s->resource;
return NULL;
}
void banner_surface_scene_origin(struct wl_resource *surface, int *x, int *y) {
struct surface *s = surface ? wl_resource_get_user_data(surface) : NULL;
*x = 0; *y = 0;
if (s && s->placed) { *x = s->x; *y = s->y; }
}
void banner_inject_key(uint32_t evdev, int pressed) { key_event(evdev, pressed); }
/* ------------------------------------------------------------------ 10 s summary */
static struct wl_event_source *g_stats_timer;
/* The last window's zero-copy count, kept for the app (WaylandCompositor.nativeZeroCopyFrames). */
volatile unsigned g_zero_copy_last;
static int on_stats_timer(void *data) {
int windows = 0;
struct surface *s;
wl_list_for_each(s, &g_toplevels, toplevel_link) windows++;
unsigned zero_copy = ahb_swapchain_stats_take();
unsigned layer_frames = sc_layer_frames_take();
g_zero_copy_last = zero_copy;
/* Interpolated frames the compositor added (framegen_bridge.c). They are on screen, so they
* count there; they are NOT GPU frames from games and never inflate that number. */
unsigned generated = vkp_framegen_stats_take();
if (g_stat_frames || g_stat_dmabuf || g_stat_shm || generated) {
char extra[160] = "";
int off = 0;
if (g_zero_copy || zero_copy) off += snprintf(extra + off, sizeof(extra) - (size_t)off, " | %u zero-copy frames", zero_copy);
/* Frames the compositor put on a display layer through one of its OWN gralloc buffers (the
* plain layer blit, or the effects chain's result): still hardware-composed, but not
* zero-copy, so they are counted apart from the line above. */
if (layer_frames) off += snprintf(extra + off, sizeof(extra) - (size_t)off, " | %u layer frames", layer_frames);
if (generated) snprintf(extra + off, sizeof(extra) - (size_t)off, " | %u generated frames", generated);
banner_log("stats", "last 10 s: %u frames on screen (%.1f fps) | %u GPU frames from games | %u window redraws | %d windows open%s",
g_stat_frames + generated, (g_stat_frames + generated) / 10.0, g_stat_dmabuf, g_stat_shm, windows, extra);
}
/* The perf line, next to the stats line and only when something happened: the compositor thread's
* time per scene and inside the driver (avg/max), what the base surface did, and how long games
* waited for their buffers back. See the audit memory for how to read it. */
{
struct vkp_perf vp;
vkp_perf_take(&vp);
const unsigned drops = sc_layer_drops_take();
#define PERF_MS(sum, n) ((n) ? (double)(sum) / 1e6 / (double)(n) : 0.0)
if (g_perf.scenes || vp.presents || g_perf.releases || drops)
banner_log("perf", "last 10 s: %u ticks, %u scenes, %u on screen (copy %u, zero-copy %u, layer copy %u) | "
"render_scene %.2f/%.2f ms | base %u black kept, %u presented | acquire %.2f/%.2f ms | "
"present %.2f/%.2f ms (%u) | fence wait %.2f/%.2f ms (%u, %u GPU release waits) | "
"release %.2f/%.2f ms (%u, %u held) | %u pool drops",
g_perf.ticks, g_perf.scenes, g_stat_frames, g_perf.copy_scenes, zero_copy, layer_frames,
PERF_MS(g_perf.scene_ns, g_perf.scenes), (double)g_perf.scene_max_ns / 1e6,
vp.base_kept, vp.base_presents,
PERF_MS(vp.acquire_ns, vp.acquires), (double)vp.acquire_max_ns / 1e6,
PERF_MS(vp.present_ns, vp.presents), (double)vp.present_max_ns / 1e6, vp.presents,
PERF_MS(vp.wait_ns, vp.waits), (double)vp.wait_max_ns / 1e6, vp.waits, vp.gpu_release_waits,
PERF_MS(g_perf.release_ns, g_perf.releases), (double)g_perf.release_max_ns / 1e6,
g_perf.releases, g_perf.releases_held, drops);
#undef PERF_MS
memset(&g_perf, 0, sizeof(g_perf));
}
g_stat_frames = g_stat_dmabuf = g_stat_shm = 0;
banner_color_stats_tick(); /* HDR evidence for the same window (nothing when the HDR gate is closed) */
/* A program that asked for dma-buf feedback (EGL's GPU path does, first thing) but has drawn
* only wl_shm frames since is an OpenGL program whose EGL gave up on the GPU. The software
* path it fell to has no rasteriser in the Wayland layers, so what it commits is black. */
for (struct client_info *ci = g_clients; ci; ci = ci->next) {
if (!ci->asked_feedback || ci->dmabuf_buffers || ci->shm_gl_said || ci->shm_frames < 150)
continue;
ci->shm_gl_said = 1;
banner_log("opengl", "%s asked for GPU buffers but has drawn only software (shared-memory) "
"frames: its OpenGL fell back to software rendering, which the Wayland layer cannot "
"draw - expect a black picture (main device %u:%u)", ci->name,
(unsigned)major(g_main_device), (unsigned)minor(g_main_device));
}
wl_event_source_timer_update(g_stats_timer, 10000);
return 0;
}
/* ------------------------------------------------------------------ test input
* $XDG_RUNTIME_DIR/test-input is a FIFO for scripted testing from a root shell, in scene
* (desktop) coordinates: "move X Y", "click X Y", "rclick X Y", "dclick X Y", "down X Y",
* "up X Y", "rdown X Y", "rup X Y", "rel DX DY" (relative motion, like the app's Relative
* Mouse path), "key CODE" (evdev), "keydown CODE", "keyup CODE".
* It lives in the app's private directory, so only the app and root can reach it. */
static char g_test_buf[512];
static size_t g_test_len;
static void test_input_line(const char *line) {
double x, y;
unsigned code;
char cmd[16];
int n = sscanf(line, "%15s %lf %lf", cmd, &x, &y);
if (n < 2) return;
banner_log("test", "input: %s", line);
if (!strcmp(cmd, "key") || !strcmp(cmd, "keydown") || !strcmp(cmd, "keyup")) {
code = (unsigned)x;
if (strcmp(cmd, "keyup")) key_event(code, 1);
if (strcmp(cmd, "keydown")) key_event(code, 0);
return;
}
if (n < 3) return;
if (!strcmp(cmd, "move")) pointer_event(x, y, 0, 0);
else if (!strcmp(cmd, "rel")) pointer_delta(x, y);
else if (!strcmp(cmd, "down")) pointer_event(x, y, BTN_LEFT, 1);
else if (!strcmp(cmd, "up")) pointer_event(x, y, BTN_LEFT, 0);
else if (!strcmp(cmd, "rdown")) pointer_event(x, y, BTN_RIGHT, 1);
else if (!strcmp(cmd, "rup")) pointer_event(x, y, BTN_RIGHT, 0);
else if (!strcmp(cmd, "click") || !strcmp(cmd, "rclick") || !strcmp(cmd, "dclick")) {
uint32_t button = cmd[0] == 'r' ? BTN_RIGHT : BTN_LEFT;
int times = cmd[0] == 'd' ? 2 : 1;
pointer_event(x, y, 0, 0);
for (int i = 0; i < times; i++) {
pointer_event(x, y, button, 1);
pointer_event(x, y, button, 0);
}
}
}
static int on_test_input(int fd, uint32_t mask, void *data) {
ssize_t r;
while ((r = read(fd, g_test_buf + g_test_len, sizeof(g_test_buf) - 1 - g_test_len)) > 0) {
char *start = g_test_buf, *nl;
g_test_len += (size_t)r;
g_test_buf[g_test_len] = 0;
while ((nl = strchr(start, '\n'))) {
*nl = 0;
test_input_line(start);
start = nl + 1;
}
g_test_len = strlen(start);
memmove(g_test_buf, start, g_test_len);
if (g_test_len >= sizeof(g_test_buf) - 1) g_test_len = 0; /* overlong line: drop */
}
return 0;
}
static void start_test_input(struct wl_event_loop *loop) {
const char *rt = getenv("XDG_RUNTIME_DIR");
char path[512];
int fd;
if (!rt || !*rt) return;
snprintf(path, sizeof(path), "%s/test-input", rt);
unlink(path);
if (mkfifo(path, 0600) != 0) return;
/* O_RDWR keeps a writer open ourselves, so a closing test shell never leaves us at EOF. */
if ((fd = open(path, O_RDWR | O_NONBLOCK | O_CLOEXEC)) < 0) return;
wl_event_loop_add_fd(loop, fd, WL_EVENT_READABLE, on_test_input, NULL);
}
/* ------------------------------------------------------------------ entry
* Blocks in the wl event loop, so the JNI wrapper runs it on a dedicated thread.
* XDG_RUNTIME_DIR must be set by the caller before this runs. */
static struct wl_listener g_client_created = { .notify = on_client_created };
int banner_wayland_run(void) {
wl_list_init(&g_surfaces);
wl_list_init(&g_toplevels);
wl_list_init(&g_constraints);
wl_list_init(&g_relative_pointers);
open_session_log();
/* Bring the GPU up before any client can connect: loading Turnip the first time
* can take many seconds, and it must not stall a client mid-handshake. */
if (vkp_ready() != 0) WLOGE("renderer unavailable; clients will connect but nothing is drawn");
struct wl_display *display = wl_display_create();
if (!display) {
WLOGE("wl_display_create failed");
return 1;
}
/* Use a FIXED socket name so it always matches the guest's WAYLAND_DISPLAY=wayland-0
* (wl_display_add_socket_auto would drift to wayland-1/2/… if a stale socket exists,
* and the guest only ever looks for wayland-0). Unlink any stale socket+lock first so
* a previous run that didn't clean up can't block the bind. */
const char *rt = getenv("XDG_RUNTIME_DIR");
if (rt && *rt) {
char p[512];
snprintf(p, sizeof(p), "%s/wayland-0", rt); unlink(p);
snprintf(p, sizeof(p), "%s/wayland-0.lock", rt); unlink(p);
}
if (wl_display_add_socket(display, "wayland-0") != 0) {
WLOGE("add_socket(wayland-0) failed in XDG_RUNTIME_DIR=%s (errno path/perms?)",
rt ? rt : "(null)");
return 1;
}
banner_log("display", "Wayland compositor listening on %s/wayland-0", rt ? rt : "?");
wl_display_add_client_created_listener(display, &g_client_created);
wl_global_create(display, &wl_compositor_interface, 6, NULL, bind_compositor);
wl_global_create(display, &wl_subcompositor_interface, 1, NULL, bind_subcompositor);
wl_global_create(display, &wp_viewporter_interface, 1, NULL, bind_viewporter);
wl_display_init_shm(display); /* wl_shm global + pool/buffer handling */
/* libdecor binds wl_output at 4; a lower version is a protocol error for the client. */
wl_global_create(display, &wl_output_interface, 4, NULL, bind_output);
wl_global_create(display, &xdg_wm_base_interface, 1, NULL, bind_xdg_wm_base);
wl_global_create(display, &zwp_linux_dmabuf_v1_interface, 4, NULL, bind_dmabuf);
/* gamescope's Wayland backend refuses a seat older than 8. */
wl_global_create(display, &wl_seat_interface, 9, NULL, bind_seat);
wl_global_create(display, &banner_desktop_v1_interface, 1, NULL, bind_desktop);
wl_global_create(display, &wp_presentation_interface, 2, NULL, bind_presentation);
wl_global_create(display, &zwp_pointer_constraints_v1_interface, 1, NULL, bind_pointer_constraints);
wl_global_create(display, &zwp_relative_pointer_manager_v1_interface, 1, NULL, bind_relative_pointer_manager);
banner_ext_init(display); /* clipboard, text input, toplevel icons (own files, see banner_ext.h) */
ahb_swapchain_init(display); /* zero-copy layers: banner_ahb_v1, advertised whenever a display
* layer is possible; the mode event carries the live switch
* (ahb_swapchain.h) */
banner_color_init(display); /* HDR10 (opt-in): decides the gate from the app's request + the
* display + the layer path above; creates wp_color_manager_v1
* only when it is open (banner_color.h). Before any client can
* bind zwp_linux_dmabuf_v1, whose table it widens. */
wl_list_init(&g_pending_releases);
g_release_timer_fd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC);
if (g_release_timer_fd >= 0)
g_release_source = wl_event_loop_add_fd(wl_display_get_event_loop(display), g_release_timer_fd,
WL_EVENT_READABLE, on_release_timer, NULL);
/* Input injection: the Android UI thread writes events to g_input_pipe[1];
* the wl event loop drains them on this thread. */
g_display = display;
if (pipe2(g_input_pipe, O_CLOEXEC | O_NONBLOCK) == 0) {
wl_event_loop_add_fd(wl_display_get_event_loop(display), g_input_pipe[0],
WL_EVENT_READABLE, on_input_readable, NULL);
} else {
WLOGE("input pipe creation failed");
}
start_test_input(wl_display_get_event_loop(display));
if ((g_stats_timer = wl_event_loop_add_timer(wl_display_get_event_loop(display), on_stats_timer, NULL)))
wl_event_source_timer_update(g_stats_timer, 10000);
wl_display_run(display); /* blocks, dispatches the event loop */
wl_display_destroy(display);
return 0;
}