Upstream: https://gitlab.com/qemu-project/qemu.git Upstream-Commit: d2e570cc0f97b936902a5b1b86b73c0f5998b475
457 lines
13 KiB
C
457 lines
13 KiB
C
/*
|
|
* D-Bus display listener — scanout, update and cursor handling.
|
|
*
|
|
* Copyright (C) 2026 Red Hat, Inc.
|
|
*
|
|
* SPDX-License-Identifier: GPL-2.0-or-later
|
|
*/
|
|
|
|
#include "qemu/osdep.h"
|
|
|
|
#include "qemu/sockets.h"
|
|
#include "qemu/error-report.h"
|
|
#include "ui/console-priv.h"
|
|
#include "ui/dbus-display1.h"
|
|
#include "ui/surface.h"
|
|
#include "trace.h"
|
|
#include "qemu-vnc.h"
|
|
|
|
typedef struct ConsoleData {
|
|
QemuDBusDisplay1Console *console_proxy;
|
|
QemuDBusDisplay1Keyboard *keyboard_proxy;
|
|
QemuDBusDisplay1Mouse *mouse_proxy;
|
|
QemuGraphicConsole *gfx_con;
|
|
GDBusConnection *listener_conn;
|
|
/*
|
|
* When true the surface is backed by a read-only mmap (ScanoutMap path)
|
|
* and Update messages must be rejected because compositing into the
|
|
* surface is not possible. The plain Scanout path provides a writable
|
|
* copy and clears this flag.
|
|
*/
|
|
bool read_only;
|
|
} ConsoleData;
|
|
|
|
static void display_ui_info(void *opaque, uint32_t head, QemuUIInfo *info)
|
|
{
|
|
ConsoleData *cd = opaque;
|
|
g_autoptr(GError) err = NULL;
|
|
|
|
if (!cd || !cd->console_proxy) {
|
|
return;
|
|
}
|
|
|
|
qemu_dbus_display1_console_call_set_uiinfo_sync(
|
|
cd->console_proxy,
|
|
info->width_mm, info->height_mm,
|
|
info->xoff, info->yoff,
|
|
info->width, info->height,
|
|
G_DBUS_CALL_FLAGS_NONE, -1, NULL, &err);
|
|
if (err) {
|
|
error_report("SetUIInfo failed: %s", err->message);
|
|
}
|
|
}
|
|
|
|
static void
|
|
scanout_image_destroy(pixman_image_t *image, void *data)
|
|
{
|
|
g_variant_unref(data);
|
|
}
|
|
|
|
typedef struct {
|
|
void *addr;
|
|
size_t len;
|
|
} ScanoutMapData;
|
|
|
|
static void
|
|
scanout_map_destroy(pixman_image_t *image, void *data)
|
|
{
|
|
ScanoutMapData *map = data;
|
|
munmap(map->addr, map->len);
|
|
g_free(map);
|
|
}
|
|
|
|
static gboolean
|
|
on_scanout(QemuDBusDisplay1Listener *listener,
|
|
GDBusMethodInvocation *invocation,
|
|
guint width, guint height, guint stride,
|
|
guint pixman_format, GVariant *data,
|
|
gpointer user_data)
|
|
{
|
|
ConsoleData *cd = user_data;
|
|
QemuConsole *con = QEMU_CONSOLE(cd->gfx_con);
|
|
gsize size;
|
|
const uint8_t *pixels;
|
|
pixman_image_t *image;
|
|
DisplaySurface *surface;
|
|
|
|
trace_qemu_vnc_scanout(width, height, stride, pixman_format);
|
|
|
|
pixels = g_variant_get_fixed_array(data, &size, 1);
|
|
|
|
image = pixman_image_create_bits((pixman_format_code_t)pixman_format,
|
|
width, height, (uint32_t *)pixels, stride);
|
|
assert(image);
|
|
|
|
g_variant_ref(data);
|
|
pixman_image_set_destroy_function(image, scanout_image_destroy, data);
|
|
|
|
cd->read_only = false;
|
|
surface = qemu_create_displaysurface_pixman(image);
|
|
qemu_console_set_surface(con, surface);
|
|
|
|
qemu_dbus_display1_listener_complete_scanout(listener, invocation);
|
|
return DBUS_METHOD_INVOCATION_HANDLED;
|
|
}
|
|
|
|
static gboolean
|
|
on_update(QemuDBusDisplay1Listener *listener,
|
|
GDBusMethodInvocation *invocation,
|
|
gint x, gint y, gint w, gint h,
|
|
guint stride, guint pixman_format, GVariant *data,
|
|
gpointer user_data)
|
|
{
|
|
ConsoleData *cd = user_data;
|
|
QemuConsole *con = QEMU_CONSOLE(cd->gfx_con);
|
|
DisplaySurface *surface = qemu_console_surface(con);
|
|
gsize size;
|
|
const uint8_t *pixels;
|
|
pixman_image_t *src;
|
|
|
|
trace_qemu_vnc_update(x, y, w, h, stride, pixman_format);
|
|
if (!surface || cd->read_only) {
|
|
g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR,
|
|
G_DBUS_ERROR_FAILED, "No active or writable console");
|
|
return DBUS_METHOD_INVOCATION_HANDLED;
|
|
}
|
|
|
|
pixels = g_variant_get_fixed_array(data, &size, 1);
|
|
src = pixman_image_create_bits((pixman_format_code_t)pixman_format,
|
|
w, h, (uint32_t *)pixels, stride);
|
|
assert(src);
|
|
pixman_image_composite(PIXMAN_OP_SRC, src, NULL,
|
|
surface->image,
|
|
0, 0, 0, 0, x, y, w, h);
|
|
pixman_image_unref(src);
|
|
|
|
qemu_console_update(con, x, y, w, h);
|
|
|
|
qemu_dbus_display1_listener_complete_update(listener, invocation);
|
|
return DBUS_METHOD_INVOCATION_HANDLED;
|
|
}
|
|
|
|
static gboolean
|
|
on_scanout_map(QemuDBusDisplay1ListenerUnixMap *listener,
|
|
GDBusMethodInvocation *invocation,
|
|
GUnixFDList *fd_list,
|
|
GVariant *arg_handle,
|
|
guint offset, guint width, guint height,
|
|
guint stride, guint pixman_format,
|
|
gpointer user_data)
|
|
{
|
|
ConsoleData *cd = user_data;
|
|
gint32 handle = g_variant_get_handle(arg_handle);
|
|
g_autoptr(GError) err = NULL;
|
|
DisplaySurface *surface;
|
|
int fd;
|
|
void *addr;
|
|
size_t len = (size_t)height * stride;
|
|
pixman_image_t *image;
|
|
|
|
trace_qemu_vnc_scanout_map(width, height, stride, pixman_format, offset);
|
|
|
|
fd = g_unix_fd_list_get(fd_list, handle, &err);
|
|
if (fd < 0) {
|
|
g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR,
|
|
G_DBUS_ERROR_FAILED, "Failed to get fd: %s", err->message);
|
|
return DBUS_METHOD_INVOCATION_HANDLED;
|
|
}
|
|
|
|
/* MAP_PRIVATE: we only read; avoid propagating writes back to QEMU */
|
|
addr = mmap(NULL, len, PROT_READ, MAP_PRIVATE, fd, offset);
|
|
close(fd);
|
|
if (addr == MAP_FAILED) {
|
|
g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR,
|
|
G_DBUS_ERROR_FAILED, "mmap failed: %s", g_strerror(errno));
|
|
return DBUS_METHOD_INVOCATION_HANDLED;
|
|
}
|
|
|
|
image = pixman_image_create_bits((pixman_format_code_t)pixman_format,
|
|
width, height, addr, stride);
|
|
assert(image);
|
|
{
|
|
ScanoutMapData *map = g_new0(ScanoutMapData, 1);
|
|
map->addr = addr;
|
|
map->len = len;
|
|
pixman_image_set_destroy_function(image, scanout_map_destroy, map);
|
|
}
|
|
|
|
cd->read_only = true;
|
|
surface = qemu_create_displaysurface_pixman(image);
|
|
qemu_console_set_surface(QEMU_CONSOLE(cd->gfx_con), surface);
|
|
|
|
qemu_dbus_display1_listener_unix_map_complete_scanout_map(
|
|
listener, invocation, NULL);
|
|
return DBUS_METHOD_INVOCATION_HANDLED;
|
|
}
|
|
|
|
static gboolean
|
|
on_update_map(QemuDBusDisplay1ListenerUnixMap *listener,
|
|
GDBusMethodInvocation *invocation,
|
|
guint x, guint y, guint w, guint h,
|
|
gpointer user_data)
|
|
{
|
|
ConsoleData *cd = user_data;
|
|
|
|
trace_qemu_vnc_update_map(x, y, w, h);
|
|
|
|
qemu_console_update(QEMU_CONSOLE(cd->gfx_con), x, y, w, h);
|
|
|
|
qemu_dbus_display1_listener_unix_map_complete_update_map(
|
|
listener, invocation);
|
|
return DBUS_METHOD_INVOCATION_HANDLED;
|
|
}
|
|
|
|
static gboolean
|
|
on_cursor_define(QemuDBusDisplay1Listener *listener,
|
|
GDBusMethodInvocation *invocation,
|
|
gint width, gint height,
|
|
gint hot_x, gint hot_y,
|
|
GVariant *data,
|
|
gpointer user_data)
|
|
{
|
|
ConsoleData *cd = user_data;
|
|
gsize size;
|
|
const uint8_t *pixels;
|
|
QEMUCursor *c;
|
|
|
|
trace_qemu_vnc_cursor_define(width, height, hot_x, hot_y);
|
|
|
|
c = cursor_alloc(width, height);
|
|
if (!c) {
|
|
qemu_dbus_display1_listener_complete_cursor_define(
|
|
listener, invocation);
|
|
return DBUS_METHOD_INVOCATION_HANDLED;
|
|
}
|
|
|
|
c->hot_x = hot_x;
|
|
c->hot_y = hot_y;
|
|
|
|
pixels = g_variant_get_fixed_array(data, &size, 1);
|
|
memcpy(c->data, pixels, MIN(size, (gsize)width * height * 4));
|
|
|
|
qemu_console_set_cursor(QEMU_CONSOLE(cd->gfx_con), c);
|
|
cursor_unref(c);
|
|
|
|
qemu_dbus_display1_listener_complete_cursor_define(
|
|
listener, invocation);
|
|
return DBUS_METHOD_INVOCATION_HANDLED;
|
|
}
|
|
|
|
typedef struct {
|
|
GMainLoop *loop;
|
|
GThread *thread;
|
|
GDBusConnection *listener_conn;
|
|
} ListenerSetupData;
|
|
|
|
static void
|
|
on_register_listener_finished(GObject *source_object,
|
|
GAsyncResult *res,
|
|
gpointer user_data)
|
|
{
|
|
ListenerSetupData *data = user_data;
|
|
g_autoptr(GError) err = NULL;
|
|
|
|
qemu_dbus_display1_console_call_register_listener_finish(
|
|
QEMU_DBUS_DISPLAY1_CONSOLE(source_object),
|
|
NULL,
|
|
res, &err);
|
|
|
|
if (err) {
|
|
error_report("RegisterListener failed: %s", err->message);
|
|
g_main_loop_quit(data->loop);
|
|
return;
|
|
}
|
|
|
|
data->listener_conn = g_thread_join(data->thread);
|
|
g_main_loop_quit(data->loop);
|
|
}
|
|
|
|
static GDBusConnection *
|
|
console_register_display_listener(QemuDBusDisplay1Console *console)
|
|
{
|
|
g_autoptr(GError) err = NULL;
|
|
g_autoptr(GMainLoop) loop = NULL;
|
|
g_autoptr(GUnixFDList) fd_list = NULL;
|
|
ListenerSetupData data = { 0 };
|
|
int pair[2];
|
|
int idx;
|
|
|
|
if (qemu_socketpair(AF_UNIX, SOCK_STREAM, 0, pair) < 0) {
|
|
error_report("socketpair failed: %s", strerror(errno));
|
|
return NULL;
|
|
}
|
|
|
|
fd_list = g_unix_fd_list_new();
|
|
idx = g_unix_fd_list_append(fd_list, pair[1], &err);
|
|
close(pair[1]);
|
|
if (idx < 0) {
|
|
close(pair[0]);
|
|
error_report("Failed to append fd: %s", err->message);
|
|
return NULL;
|
|
}
|
|
|
|
loop = g_main_loop_new(NULL, FALSE);
|
|
data.loop = loop;
|
|
data.thread = p2p_dbus_thread_new(pair[0]);
|
|
|
|
qemu_dbus_display1_console_call_register_listener(
|
|
console,
|
|
g_variant_new_handle(idx),
|
|
G_DBUS_CALL_FLAGS_NONE,
|
|
-1,
|
|
fd_list,
|
|
NULL,
|
|
on_register_listener_finished,
|
|
&data);
|
|
|
|
g_main_loop_run(loop);
|
|
|
|
return data.listener_conn;
|
|
}
|
|
|
|
static void
|
|
setup_display_listener(ConsoleData *cd)
|
|
{
|
|
g_autoptr(GDBusObjectSkeleton) obj = NULL;
|
|
GDBusObjectManagerServer *server;
|
|
QemuDBusDisplay1Listener *iface;
|
|
QemuDBusDisplay1ListenerUnixMap *iface_map;
|
|
|
|
server = g_dbus_object_manager_server_new(DBUS_DISPLAY1_ROOT);
|
|
obj = g_dbus_object_skeleton_new(DBUS_DISPLAY1_ROOT "/Listener");
|
|
|
|
/* Main listener interface */
|
|
iface = qemu_dbus_display1_listener_skeleton_new();
|
|
g_object_connect(iface,
|
|
"signal::handle-scanout", on_scanout, cd,
|
|
"signal::handle-update", on_update, cd,
|
|
"signal::handle-cursor-define", on_cursor_define, cd,
|
|
NULL);
|
|
g_dbus_object_skeleton_add_interface(obj,
|
|
G_DBUS_INTERFACE_SKELETON(iface));
|
|
|
|
/* Unix shared memory map interface */
|
|
iface_map = qemu_dbus_display1_listener_unix_map_skeleton_new();
|
|
g_object_connect(iface_map,
|
|
"signal::handle-scanout-map", on_scanout_map, cd,
|
|
"signal::handle-update-map", on_update_map, cd,
|
|
NULL);
|
|
g_dbus_object_skeleton_add_interface(obj,
|
|
G_DBUS_INTERFACE_SKELETON(iface_map));
|
|
|
|
{
|
|
const gchar *ifaces[] = {
|
|
"org.qemu.Display1.Listener.Unix.Map", NULL
|
|
};
|
|
g_object_set(iface, "interfaces", ifaces, NULL);
|
|
}
|
|
|
|
g_dbus_object_manager_server_export(server, obj);
|
|
g_dbus_object_manager_server_set_connection(server,
|
|
cd->listener_conn);
|
|
|
|
g_dbus_connection_start_message_processing(cd->listener_conn);
|
|
}
|
|
|
|
static const GraphicHwOps vnc_hw_ops = {
|
|
.ui_info = display_ui_info,
|
|
};
|
|
|
|
bool console_setup(GDBusConnection *bus, const char *bus_name,
|
|
const char *console_path)
|
|
{
|
|
g_autoptr(GError) err = NULL;
|
|
ConsoleData *cd;
|
|
QemuConsole *con;
|
|
|
|
cd = g_new0(ConsoleData, 1);
|
|
|
|
cd->console_proxy = qemu_dbus_display1_console_proxy_new_sync(
|
|
bus, G_DBUS_PROXY_FLAGS_NONE, bus_name,
|
|
console_path, NULL, &err);
|
|
if (!cd->console_proxy) {
|
|
error_report("Failed to create console proxy for %s: %s",
|
|
console_path, err->message);
|
|
g_free(cd);
|
|
return false;
|
|
}
|
|
|
|
cd->keyboard_proxy = QEMU_DBUS_DISPLAY1_KEYBOARD(
|
|
qemu_dbus_display1_keyboard_proxy_new_sync(
|
|
bus, G_DBUS_PROXY_FLAGS_NONE, bus_name,
|
|
console_path, NULL, &err));
|
|
if (!cd->keyboard_proxy) {
|
|
error_report("Failed to create keyboard proxy for %s: %s",
|
|
console_path, err->message);
|
|
g_object_unref(cd->console_proxy);
|
|
g_free(cd);
|
|
return false;
|
|
}
|
|
|
|
g_clear_error(&err);
|
|
cd->mouse_proxy = QEMU_DBUS_DISPLAY1_MOUSE(
|
|
qemu_dbus_display1_mouse_proxy_new_sync(
|
|
bus, G_DBUS_PROXY_FLAGS_NONE, bus_name,
|
|
console_path, NULL, &err));
|
|
if (!cd->mouse_proxy) {
|
|
error_report("Failed to create mouse proxy for %s: %s",
|
|
console_path, err->message);
|
|
g_object_unref(cd->keyboard_proxy);
|
|
g_object_unref(cd->console_proxy);
|
|
g_free(cd);
|
|
return false;
|
|
}
|
|
|
|
con = qemu_graphic_console_create(NULL, 0, &vnc_hw_ops, cd);
|
|
cd->gfx_con = QEMU_GRAPHIC_CONSOLE(con);
|
|
|
|
cd->listener_conn = console_register_display_listener(
|
|
cd->console_proxy);
|
|
if (!cd->listener_conn) {
|
|
error_report("Failed to setup D-Bus listener for %s",
|
|
console_path);
|
|
g_object_unref(cd->mouse_proxy);
|
|
g_object_unref(cd->keyboard_proxy);
|
|
g_object_unref(cd->console_proxy);
|
|
g_free(cd);
|
|
return false;
|
|
}
|
|
|
|
setup_display_listener(cd);
|
|
input_setup(cd->keyboard_proxy, cd->mouse_proxy);
|
|
|
|
return true;
|
|
}
|
|
|
|
QemuDBusDisplay1Keyboard *console_get_keyboard(const QemuConsole *con)
|
|
{
|
|
ConsoleData *cd;
|
|
|
|
if (!QEMU_IS_GRAPHIC_CONSOLE(con)) {
|
|
return NULL;
|
|
}
|
|
cd = con->hw;
|
|
return cd ? cd->keyboard_proxy : NULL;
|
|
}
|
|
|
|
QemuDBusDisplay1Mouse *console_get_mouse(const QemuConsole *con)
|
|
{
|
|
ConsoleData *cd;
|
|
|
|
if (!QEMU_IS_GRAPHIC_CONSOLE(con)) {
|
|
return NULL;
|
|
}
|
|
cd = con->hw;
|
|
return cd ? cd->mouse_proxy : NULL;
|
|
}
|