Merge tag 'ui-pull-request' of https://gitlab.com/marcandre.lureau/qemu into staging

UI-related

- vdagent migration support
- gtk: improve scale handling

# -----BEGIN PGP SIGNATURE-----
#
# iQJQBAABCgA6FiEEh6m9kz+HxgbSdvYt2ujhCXWWnOUFAmgyAywcHG1hcmNhbmRy
# ZS5sdXJlYXVAcmVkaGF0LmNvbQAKCRDa6OEJdZac5TbTD/4/3da55xtH0yVSol+G
# T6S7F1GKUbSCi0y/t+PiG2usiE17euZ+m1mKBAOT2kObehS3gWybIreNBZ3IAPZg
# Hm8RoIU6yStui2RZ/rKx2d1P2nq3mMmaB7uToHwPl6KsO2hYm75zmXCGH6oBI+ah
# muNkxzsIky9qnOaKeZRODPkPiG1LXfte0IYV6wkpBcXUqE595rQfD9WDuOJ7WKJe
# T1GiwxdClZGBM6SahDhfwdoFx1yTk8rTtTdYM0K3W7xMQGvCKLGQHDmS7JeQ1lm3
# 42H3kh760UwNqSyk4L0TWZiGrsEWCsbU7heiRLeVL/FBA8N2TWL6xKB/xSO4duJ3
# v63652/rNh18AFnSRu0CaIBwf/hXfLpU/cKv/Sqmm2X8FjD7dLMyaAHCz1BC0acU
# +VA8JKRu7F5GbsisaYyG4lY6u8IBw9sq/FhyNBflBy3MQl0v4PV5x1eqDbGT1560
# k2bQbIChumn8U4TmEA1IHDFX45cdxZmlgnt7yq+SUn5/CmRJrXAlacPRje4A4omL
# YS3Y1Qn4X7sXyIdoHvxofng2NU0nfZX9az27aXsGkRNtzjibZzjl8rJKcaCx0LGQ
# uFJwA+ArCJjTJ3EB09373iGixW0Vuv4dM4IkNp+QGoCOVH0jeER6bs9cNROG6WmS
# e1nWuW0hZgmdusw1S0ncxFc+YA==
# =SjRP
# -----END PGP SIGNATURE-----
# gpg: Signature made Sat 24 May 2025 13:34:36 EDT
# gpg:                using RSA key 87A9BD933F87C606D276F62DDAE8E10975969CE5
# gpg:                issuer "marcandre.lureau@redhat.com"
# gpg: Good signature from "Marc-André Lureau <marcandre.lureau@redhat.com>" [full]
# gpg:                 aka "Marc-André Lureau <marcandre.lureau@gmail.com>" [full]
# Primary key fingerprint: 87A9 BD93 3F87 C606 D276  F62D DAE8 E109 7596 9CE5

* tag 'ui-pull-request' of https://gitlab.com/marcandre.lureau/qemu:
  ui/gtk-egl: Render guest content with padding in fixed-scale mode
  ui/gtk-gl-area: Render guest content with padding in fixed-scale mode
  ui/gtk: Consider scaling when propagating ui info
  ui/gtk: Don't update scale in fixed scale mode in gtk-egl.c
  ui/sdl: Consider scaling in mouse event handling
  ui/gtk: Update scales in fixed-scale mode when rendering GL area
  gtk/ui: Introduce helper gd_update_scale
  ui/gtk: Use consistent naming for variables in different coordinates
  ui/gtk: Document scale and coordinate handling
  ui/vdagent: remove migration blocker
  ui/vdagent: add migration support
  ui/vdagent: factor out clipboard peer registration
  ui/vdagent: keep "connected" state
  ui/vdagent: replace Buffer with GByteArray
  ui/clipboard: delay clipboard update when not running
  ui/clipboard: add vmstate_cbinfo
  ui/clipboard: split out QemuClipboardContent
  ui/clipboard: use int for selection field
  ui/gtk: warn if setting the clipboard failed

Signed-off-by: Stefan Hajnoczi <stefanha@redhat.com>
This commit is contained in:
Stefan Hajnoczi
2025-05-25 09:50:54 -04:00
12 changed files with 562 additions and 149 deletions
+24 -7
View File
@@ -2,6 +2,7 @@
#define QEMU_CLIPBOARD_H
#include "qemu/notify.h"
#include "migration/vmstate.h"
/**
* DOC: Introduction
@@ -25,6 +26,9 @@ typedef enum QemuClipboardSelection QemuClipboardSelection;
typedef struct QemuClipboardPeer QemuClipboardPeer;
typedef struct QemuClipboardNotify QemuClipboardNotify;
typedef struct QemuClipboardInfo QemuClipboardInfo;
typedef struct QemuClipboardContent QemuClipboardContent;
extern const VMStateDescription vmstate_cbinfo;
/**
* enum QemuClipboardType
@@ -97,6 +101,24 @@ struct QemuClipboardNotify {
};
};
/**
* struct QemuClipboardContent
*
* @available: whether the data is available
* @requested: whether the data was requested
* @size: the size of the @data
* @data: the clipboard data
*
* Clipboard content.
*/
struct QemuClipboardContent {
bool available;
bool requested;
uint32_t size;
void *data;
};
/**
* struct QemuClipboardInfo
*
@@ -112,15 +134,10 @@ struct QemuClipboardNotify {
struct QemuClipboardInfo {
uint32_t refcount;
QemuClipboardPeer *owner;
QemuClipboardSelection selection;
int selection; /* QemuClipboardSelection */
bool has_serial;
uint32_t serial;
struct {
bool available;
bool requested;
size_t size;
void *data;
} types[QEMU_CLIPBOARD_TYPE__COUNT];
QemuClipboardContent types[QEMU_CLIPBOARD_TYPE__COUNT];
};
/**
+3 -1
View File
@@ -17,6 +17,8 @@ extern bool qemu_egl_angle_d3d;
typedef struct egl_fb {
int width;
int height;
int x;
int y;
GLuint texture;
GLuint framebuffer;
bool delete_texture;
@@ -26,7 +28,7 @@ typedef struct egl_fb {
#define EGL_FB_INIT { 0, }
void egl_fb_destroy(egl_fb *fb);
void egl_fb_setup_default(egl_fb *fb, int width, int height);
void egl_fb_setup_default(egl_fb *fb, int width, int height, int x, int y);
void egl_fb_setup_for_tex(egl_fb *fb, int width, int height,
GLuint texture, bool delete);
void egl_fb_setup_new_tex(egl_fb *fb, int width, int height);
+2
View File
@@ -224,4 +224,6 @@ int gd_gl_area_make_current(DisplayGLCtx *dgc,
/* gtk-clipboard.c */
void gd_clipboard_init(GtkDisplayState *gd);
void gd_update_scale(VirtualConsole *vc, int ww, int wh, int fbw, int fbh);
#endif /* UI_GTK_H */
+64 -2
View File
@@ -1,4 +1,5 @@
#include "qemu/osdep.h"
#include "system/runstate.h"
#include "ui/clipboard.h"
#include "trace.h"
@@ -7,8 +8,62 @@ static NotifierList clipboard_notifiers =
static QemuClipboardInfo *cbinfo[QEMU_CLIPBOARD_SELECTION__COUNT];
static VMChangeStateEntry *cb_change_state_entry = NULL;
static bool cb_reset_serial_on_resume = false;
static const VMStateDescription vmstate_cbcontent = {
.name = "clipboard/content",
.version_id = 0,
.minimum_version_id = 0,
.fields = (const VMStateField[]) {
VMSTATE_BOOL(available, QemuClipboardContent),
VMSTATE_BOOL(requested, QemuClipboardContent),
VMSTATE_UINT32(size, QemuClipboardContent),
VMSTATE_VBUFFER_ALLOC_UINT32(data, QemuClipboardContent, 0, 0, size),
VMSTATE_END_OF_LIST()
}
};
const VMStateDescription vmstate_cbinfo = {
.name = "clipboard",
.version_id = 0,
.minimum_version_id = 0,
.fields = (const VMStateField[]) {
VMSTATE_INT32(selection, QemuClipboardInfo),
VMSTATE_BOOL(has_serial, QemuClipboardInfo),
VMSTATE_UINT32(serial, QemuClipboardInfo),
VMSTATE_STRUCT_ARRAY(types, QemuClipboardInfo, QEMU_CLIPBOARD_TYPE__COUNT, 0, vmstate_cbcontent, QemuClipboardContent),
VMSTATE_END_OF_LIST()
}
};
static void qemu_clipboard_change_state(void *opaque, bool running, RunState state)
{
int i;
if (!running) {
return;
}
if (cb_reset_serial_on_resume) {
qemu_clipboard_reset_serial();
}
for (i = 0; i < QEMU_CLIPBOARD_SELECTION__COUNT; i++) {
if (cbinfo[i]) {
qemu_clipboard_update(cbinfo[i]);
}
}
}
void qemu_clipboard_peer_register(QemuClipboardPeer *peer)
{
if (cb_change_state_entry == NULL) {
cb_change_state_entry = qemu_add_vm_change_state_handler(qemu_clipboard_change_state, NULL);
}
notifier_list_add(&clipboard_notifiers, &peer->notifier);
}
@@ -83,7 +138,9 @@ void qemu_clipboard_update(QemuClipboardInfo *info)
}
}
notifier_list_notify(&clipboard_notifiers, &notify);
if (runstate_is_running() || runstate_check(RUN_STATE_SUSPENDED)) {
notifier_list_notify(&clipboard_notifiers, &notify);
}
if (cbinfo[info->selection] != info) {
qemu_clipboard_info_unref(cbinfo[info->selection]);
@@ -163,7 +220,12 @@ void qemu_clipboard_reset_serial(void)
info->serial = 0;
}
}
notifier_list_notify(&clipboard_notifiers, &notify);
if (runstate_is_running() || runstate_check(RUN_STATE_SUSPENDED)) {
notifier_list_notify(&clipboard_notifiers, &notify);
} else {
cb_reset_serial_on_resume = true;
}
}
void qemu_clipboard_set_data(QemuClipboardPeer *peer,
+8 -2
View File
@@ -93,14 +93,18 @@ void egl_fb_destroy(egl_fb *fb)
fb->width = 0;
fb->height = 0;
fb->x = 0;
fb->y = 0;
fb->texture = 0;
fb->framebuffer = 0;
}
void egl_fb_setup_default(egl_fb *fb, int width, int height)
void egl_fb_setup_default(egl_fb *fb, int width, int height, int x, int y)
{
fb->width = width;
fb->height = height;
fb->x = x;
fb->y = y;
fb->framebuffer = 0; /* default framebuffer */
}
@@ -145,6 +149,7 @@ void egl_fb_blit(egl_fb *dst, egl_fb *src, bool flip)
glBindFramebuffer(GL_READ_FRAMEBUFFER, src->framebuffer);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, dst->framebuffer);
glViewport(0, 0, dst->width, dst->height);
glClear(GL_COLOR_BUFFER_BIT);
if (src->dmabuf) {
x1 = qemu_dmabuf_get_x(src->dmabuf);
@@ -161,7 +166,8 @@ void egl_fb_blit(egl_fb *dst, egl_fb *src, bool flip)
x2 = x1 + w;
glBlitFramebuffer(x1, y1, x2, y2,
0, 0, dst->width, dst->height,
dst->x, dst->y,
dst->x + dst->width, dst->y + dst->height,
GL_COLOR_BUFFER_BIT, GL_LINEAR);
}
+8 -5
View File
@@ -19,6 +19,7 @@
*/
#include "qemu/osdep.h"
#include "qemu/error-report.h"
#include "qemu/main-loop.h"
#include "ui/gtk.h"
@@ -95,11 +96,13 @@ static void gd_clipboard_update_info(GtkDisplayState *gd,
gtk_clipboard_clear(gd->gtkcb[s]);
if (targets) {
gd->cbowner[s] = true;
gtk_clipboard_set_with_data(gd->gtkcb[s],
targets, n_targets,
gd_clipboard_get_data,
gd_clipboard_clear,
gd);
if (!gtk_clipboard_set_with_data(gd->gtkcb[s],
targets, n_targets,
gd_clipboard_get_data,
gd_clipboard_clear,
gd)) {
warn_report("Failed to set GTK clipboard");
}
gtk_target_table_free(targets, n_targets);
}
+44 -14
View File
@@ -70,16 +70,18 @@ void gd_egl_draw(VirtualConsole *vc)
QemuDmaBuf *dmabuf = vc->gfx.guest_fb.dmabuf;
int fence_fd;
#endif
int ww, wh, ws;
int ww, wh, pw, ph, gs;
if (!vc->gfx.gls) {
return;
}
window = gtk_widget_get_window(vc->gfx.drawing_area);
ws = gdk_window_get_scale_factor(window);
ww = gdk_window_get_width(window) * ws;
wh = gdk_window_get_height(window) * ws;
gs = gdk_window_get_scale_factor(window);
ww = gdk_window_get_width(window);
wh = gdk_window_get_height(window);
pw = ww * gs;
ph = wh * gs;
if (vc->gfx.scanout_mode) {
#ifdef CONFIG_GBM
@@ -93,8 +95,9 @@ void gd_egl_draw(VirtualConsole *vc)
#endif
gd_egl_scanout_flush(&vc->gfx.dcl, 0, 0, vc->gfx.w, vc->gfx.h);
vc->gfx.scale_x = (double)ww / surface_width(vc->gfx.ds);
vc->gfx.scale_y = (double)wh / surface_height(vc->gfx.ds);
gd_update_scale(vc, ww, wh,
surface_width(vc->gfx.ds),
surface_height(vc->gfx.ds));
glFlush();
#ifdef CONFIG_GBM
@@ -115,13 +118,14 @@ void gd_egl_draw(VirtualConsole *vc)
eglMakeCurrent(qemu_egl_display, vc->gfx.esurface,
vc->gfx.esurface, vc->gfx.ectx);
surface_gl_setup_viewport(vc->gfx.gls, vc->gfx.ds, ww, wh);
surface_gl_setup_viewport(vc->gfx.gls, vc->gfx.ds, pw, ph);
surface_gl_render_texture(vc->gfx.gls, vc->gfx.ds);
eglSwapBuffers(qemu_egl_display, vc->gfx.esurface);
vc->gfx.scale_x = (double)ww / surface_width(vc->gfx.ds);
vc->gfx.scale_y = (double)wh / surface_height(vc->gfx.ds);
gd_update_scale(vc, ww, wh,
surface_width(vc->gfx.ds),
surface_height(vc->gfx.ds));
glFlush();
}
@@ -336,7 +340,11 @@ void gd_egl_scanout_flush(DisplayChangeListener *dcl,
{
VirtualConsole *vc = container_of(dcl, VirtualConsole, gfx.dcl);
GdkWindow *window;
int ww, wh, ws;
int px_offset, py_offset;
int gs;
int pw_widget, ph_widget, pw_surface, ph_surface;
int ww_widget, wh_widget, ww_surface, wh_surface;
int fbw, fbh;
if (!vc->gfx.scanout_mode) {
return;
@@ -349,10 +357,32 @@ void gd_egl_scanout_flush(DisplayChangeListener *dcl,
vc->gfx.esurface, vc->gfx.ectx);
window = gtk_widget_get_window(vc->gfx.drawing_area);
ws = gdk_window_get_scale_factor(window);
ww = gdk_window_get_width(window) * ws;
wh = gdk_window_get_height(window) * ws;
egl_fb_setup_default(&vc->gfx.win_fb, ww, wh);
gs = gdk_window_get_scale_factor(window);
ww_widget = gdk_window_get_width(window);
wh_widget = gdk_window_get_height(window);
fbw = surface_width(vc->gfx.ds);
fbh = surface_height(vc->gfx.ds);
gd_update_scale(vc, ww_widget, wh_widget, fbw, fbh);
ww_surface = fbw * vc->gfx.scale_x;
wh_surface = fbh * vc->gfx.scale_y;
pw_widget = ww_widget * gs;
ph_widget = wh_widget * gs;
pw_surface = ww_surface * gs;
ph_surface = wh_surface * gs;
px_offset = 0;
py_offset = 0;
if (pw_widget > pw_surface) {
px_offset = (pw_widget - pw_surface) / 2;
}
if (ph_widget > ph_surface) {
py_offset = (ph_widget - ph_surface) / 2;
}
egl_fb_setup_default(&vc->gfx.win_fb, pw_surface, ph_surface,
px_offset, py_offset);
if (vc->gfx.cursor_fb.texture) {
egl_texture_blit(vc->gfx.gls, &vc->gfx.win_fb, &vc->gfx.guest_fb,
vc->gfx.y0_top);
+46 -7
View File
@@ -42,16 +42,37 @@ void gd_gl_area_draw(VirtualConsole *vc)
#ifdef CONFIG_GBM
QemuDmaBuf *dmabuf = vc->gfx.guest_fb.dmabuf;
#endif
int ww, wh, ws, y1, y2;
int pw, ph, gs, y1, y2;
int ww, wh;
int ww_surface, wh_surface;
int fbw, fbh;
int wx_offset, wy_offset;
if (!vc->gfx.gls) {
return;
}
gtk_gl_area_make_current(GTK_GL_AREA(vc->gfx.drawing_area));
ws = gdk_window_get_scale_factor(gtk_widget_get_window(vc->gfx.drawing_area));
ww = gtk_widget_get_allocated_width(vc->gfx.drawing_area) * ws;
wh = gtk_widget_get_allocated_height(vc->gfx.drawing_area) * ws;
gs = gdk_window_get_scale_factor(gtk_widget_get_window(vc->gfx.drawing_area));
fbw = surface_width(vc->gfx.ds);
fbh = surface_height(vc->gfx.ds);
ww = gtk_widget_get_allocated_width(vc->gfx.drawing_area);
wh = gtk_widget_get_allocated_height(vc->gfx.drawing_area);
pw = ww * gs;
ph = wh * gs;
gd_update_scale(vc, ww, wh, fbw, fbh);
ww_surface = fbw * vc->gfx.scale_x;
wh_surface = fbh * vc->gfx.scale_y;
wx_offset = wy_offset = 0;
if (ww > ww_surface) {
wx_offset = (ww - ww_surface) / 2;
}
if (wh > wh_surface) {
wy_offset = (wh - wh_surface) / 2;
}
if (vc->gfx.scanout_mode) {
if (!vc->gfx.guest_fb.framebuffer) {
@@ -71,11 +92,29 @@ void gd_gl_area_draw(VirtualConsole *vc)
glBindFramebuffer(GL_READ_FRAMEBUFFER, vc->gfx.guest_fb.framebuffer);
/* GtkGLArea sets GL_DRAW_FRAMEBUFFER for us */
glViewport(0, 0, ww, wh);
if (wx_offset > 0) {
glEnable(GL_SCISSOR_TEST);
glScissor(0, 0, wx_offset * gs, wh * gs);
glClear(GL_COLOR_BUFFER_BIT);
glScissor((ww - wx_offset) * gs, 0, wx_offset * gs, wh * gs);
glClear(GL_COLOR_BUFFER_BIT);
glDisable(GL_SCISSOR_TEST);
}
if (wy_offset > 0) {
glEnable(GL_SCISSOR_TEST);
glScissor(0, 0, ww * gs, wy_offset * gs);
glClear(GL_COLOR_BUFFER_BIT);
glScissor(0, (wh - wy_offset) * gs, ww * gs, wy_offset * gs);
glClear(GL_COLOR_BUFFER_BIT);
glDisable(GL_SCISSOR_TEST);
}
glViewport(0, 0, pw, ph);
y1 = vc->gfx.y0_top ? 0 : vc->gfx.h;
y2 = vc->gfx.y0_top ? vc->gfx.h : 0;
glBlitFramebuffer(0, y1, vc->gfx.w, y2,
0, 0, ww, wh,
wx_offset * gs, wy_offset * gs,
(ww - wx_offset) * gs, (wh - wy_offset) * gs,
GL_COLOR_BUFFER_BIT, GL_NEAREST);
#ifdef CONFIG_GBM
if (dmabuf) {
@@ -101,7 +140,7 @@ void gd_gl_area_draw(VirtualConsole *vc)
}
gtk_gl_area_make_current(GTK_GL_AREA(vc->gfx.drawing_area));
surface_gl_setup_viewport(vc->gfx.gls, vc->gfx.ds, ww, wh);
surface_gl_setup_viewport(vc->gfx.gls, vc->gfx.ds, pw, ph);
surface_gl_render_texture(vc->gfx.gls, vc->gfx.ds);
}
}
+172 -75
View File
@@ -387,16 +387,16 @@ static void *gd_win32_get_hwnd(VirtualConsole *vc)
/** DisplayState Callbacks **/
static void gd_update(DisplayChangeListener *dcl,
int x, int y, int w, int h)
int fbx, int fby, int fbw, int fbh)
{
VirtualConsole *vc = container_of(dcl, VirtualConsole, gfx.dcl);
GdkWindow *win;
int x1, x2, y1, y2;
int mx, my;
int fbw, fbh;
int ww, wh;
int wx1, wx2, wy1, wy2;
int wx_offset, wy_offset;
int ww_surface, wh_surface;
int ww_widget, wh_widget;
trace_gd_update(vc->label, x, y, w, h);
trace_gd_update(vc->label, fbx, fby, fbw, fbh);
if (!gtk_widget_get_realized(vc->gfx.drawing_area)) {
return;
@@ -405,35 +405,36 @@ static void gd_update(DisplayChangeListener *dcl,
if (vc->gfx.convert) {
pixman_image_composite(PIXMAN_OP_SRC, vc->gfx.ds->image,
NULL, vc->gfx.convert,
x, y, 0, 0, x, y, w, h);
fbx, fby, 0, 0, fbx, fby, fbw, fbh);
}
x1 = floor(x * vc->gfx.scale_x);
y1 = floor(y * vc->gfx.scale_y);
wx1 = floor(fbx * vc->gfx.scale_x);
wy1 = floor(fby * vc->gfx.scale_y);
x2 = ceil(x * vc->gfx.scale_x + w * vc->gfx.scale_x);
y2 = ceil(y * vc->gfx.scale_y + h * vc->gfx.scale_y);
wx2 = ceil(fbx * vc->gfx.scale_x + fbw * vc->gfx.scale_x);
wy2 = ceil(fby * vc->gfx.scale_y + fbh * vc->gfx.scale_y);
fbw = surface_width(vc->gfx.ds) * vc->gfx.scale_x;
fbh = surface_height(vc->gfx.ds) * vc->gfx.scale_y;
ww_surface = surface_width(vc->gfx.ds) * vc->gfx.scale_x;
wh_surface = surface_height(vc->gfx.ds) * vc->gfx.scale_y;
win = gtk_widget_get_window(vc->gfx.drawing_area);
if (!win) {
return;
}
ww = gdk_window_get_width(win);
wh = gdk_window_get_height(win);
ww_widget = gdk_window_get_width(win);
wh_widget = gdk_window_get_height(win);
mx = my = 0;
if (ww > fbw) {
mx = (ww - fbw) / 2;
wx_offset = wy_offset = 0;
if (ww_widget > ww_surface) {
wx_offset = (ww_widget - ww_surface) / 2;
}
if (wh > fbh) {
my = (wh - fbh) / 2;
if (wh_widget > wh_surface) {
wy_offset = (wh_widget - wh_surface) / 2;
}
gtk_widget_queue_draw_area(vc->gfx.drawing_area,
mx + x1, my + y1, (x2 - x1), (y2 - y1));
wx_offset + wx1, wy_offset + wy1,
(wx2 - wx1), (wy2 - wy1));
}
static void gd_refresh(DisplayChangeListener *dcl)
@@ -771,8 +772,21 @@ static void gd_resize_event(GtkGLArea *area,
gint width, gint height, gpointer *opaque)
{
VirtualConsole *vc = (void *)opaque;
double pw = width, ph = height;
double sx = vc->gfx.scale_x, sy = vc->gfx.scale_y;
GdkWindow *window = gtk_widget_get_window(GTK_WIDGET(area));
const int gs = gdk_window_get_scale_factor(window);
gd_set_ui_size(vc, width, height);
if (!vc->s->free_scale && !vc->s->full_screen) {
pw /= sx;
ph /= sy;
}
/**
* width and height here are in pixel coordinate, so we must divide it
* by global window scale (gs)
*/
gd_set_ui_size(vc, pw / gs, ph / gs);
}
#endif
@@ -800,12 +814,95 @@ void gd_update_monitor_refresh_rate(VirtualConsole *vc, GtkWidget *widget)
#endif
}
void gd_update_scale(VirtualConsole *vc, int ww, int wh, int fbw, int fbh)
{
if (!vc) {
return;
}
if (vc->s->full_screen) {
vc->gfx.scale_x = (double)ww / fbw;
vc->gfx.scale_y = (double)wh / fbh;
} else if (vc->s->free_scale) {
double sx, sy;
sx = (double)ww / fbw;
sy = (double)wh / fbh;
vc->gfx.scale_x = vc->gfx.scale_y = MIN(sx, sy);
}
}
/**
* DOC: Coordinate handling.
*
* We are coping with sizes and positions in various coordinates and the
* handling of these coordinates is somewhat confusing. It would benefit us
* all if we define these coordinates explicitly and clearly. Besides, it's
* also helpful to follow the same naming convention for variables
* representing values in different coordinates.
*
* I. Definitions
*
* - (guest) buffer coordinate: this is the coordinates that the guest will
* see. The x/y offsets and width/height specified in commands sent by
* guest is basically in buffer coordinate.
*
* - (host) pixel coordinate: this is the coordinate in pixel level on the
* host destop. A window/widget of width 300 in pixel coordinate means it
* occupies 300 pixels horizontally.
*
* - (host) logical window coordinate: the existence of global scaling
* factor in desktop level makes this kind of coordinate play a role. It
* always holds that (logical window size) * (global scale factor) =
* (pixel size).
*
* - global scale factor: this is specified in desktop level and is
* typically invariant during the life cycle of the process. Users with
* high-DPI monitors might set this scale, for example, to 2, in order to
* make the UI look larger.
*
* - zooming scale: this can be freely controlled by the QEMU user to zoom
* in/out the guest content.
*
* II. Representation
*
* We'd like to use consistent representation for variables in different
* coordinates:
* - buffer coordinate: prefix fb
* - pixel coordinate: prefix p
* - logical window coordinate: prefix w
*
* For scales:
* - global scale factor: prefix gs
* - zooming scale: prefix scale/s
*
* Example: fbw, pw, ww for width in different coordinates
*
* III. Equation
*
* - fbw * gs * scale_x = pw
* - pw = gs * ww
*
* Consequently we have
*
* - fbw * scale_x = ww
*
* Example: assuming we are running QEMU on a 3840x2160 screen and have set
* global scaling factor to 2, if the guest buffer size is 1920x1080 and the
* zooming scale is 0.5, then we have:
* - fbw = 1920, fbh = 1080
* - pw = 1920, ph = 1080
* - ww = 960, wh = 540
* A bonus of this configuration is that we can achieve pixel to pixel
* presentation of the guest content.
*/
static gboolean gd_draw_event(GtkWidget *widget, cairo_t *cr, void *opaque)
{
VirtualConsole *vc = opaque;
GtkDisplayState *s = vc->s;
int mx, my;
int ww, wh;
int wx_offset, wy_offset;
int ww_widget, wh_widget, ww_surface, wh_surface;
int fbw, fbh;
#if defined(CONFIG_OPENGL)
@@ -839,46 +936,37 @@ static gboolean gd_draw_event(GtkWidget *widget, cairo_t *cr, void *opaque)
fbw = surface_width(vc->gfx.ds);
fbh = surface_height(vc->gfx.ds);
ww = gdk_window_get_width(gtk_widget_get_window(widget));
wh = gdk_window_get_height(gtk_widget_get_window(widget));
ww_widget = gdk_window_get_width(gtk_widget_get_window(widget));
wh_widget = gdk_window_get_height(gtk_widget_get_window(widget));
if (s->full_screen) {
vc->gfx.scale_x = (double)ww / fbw;
vc->gfx.scale_y = (double)wh / fbh;
} else if (s->free_scale) {
double sx, sy;
gd_update_scale(vc, ww_widget, wh_widget, fbw, fbh);
sx = (double)ww / fbw;
sy = (double)wh / fbh;
ww_surface = fbw * vc->gfx.scale_x;
wh_surface = fbh * vc->gfx.scale_y;
vc->gfx.scale_x = vc->gfx.scale_y = MIN(sx, sy);
wx_offset = wy_offset = 0;
if (ww_widget > ww_surface) {
wx_offset = (ww_widget - ww_surface) / 2;
}
if (wh_widget > wh_surface) {
wy_offset = (wh_widget - wh_surface) / 2;
}
fbw *= vc->gfx.scale_x;
fbh *= vc->gfx.scale_y;
mx = my = 0;
if (ww > fbw) {
mx = (ww - fbw) / 2;
}
if (wh > fbh) {
my = (wh - fbh) / 2;
}
cairo_rectangle(cr, 0, 0, ww, wh);
cairo_rectangle(cr, 0, 0, ww_widget, wh_widget);
/* Optionally cut out the inner area where the pixmap
will be drawn. This avoids 'flashing' since we're
not double-buffering. Note we're using the undocumented
behaviour of drawing the rectangle from right to left
to cut out the whole */
cairo_rectangle(cr, mx + fbw, my,
-1 * fbw, fbh);
cairo_rectangle(cr, wx_offset + ww_surface, wy_offset,
-1 * ww_surface, wh_surface);
cairo_fill(cr);
cairo_scale(cr, vc->gfx.scale_x, vc->gfx.scale_y);
cairo_set_source_surface(cr, vc->gfx.surface,
mx / vc->gfx.scale_x, my / vc->gfx.scale_y);
wx_offset / vc->gfx.scale_x,
wy_offset / vc->gfx.scale_y);
cairo_paint(cr);
return TRUE;
@@ -889,19 +977,19 @@ static gboolean gd_motion_event(GtkWidget *widget, GdkEventMotion *motion,
{
VirtualConsole *vc = opaque;
GtkDisplayState *s = vc->s;
int x, y;
int mx, my;
int fbh, fbw;
int ww, wh;
int fbx, fby;
int wx_offset, wy_offset;
int wh_surface, ww_surface;
int ww_widget, wh_widget;
if (!vc->gfx.ds) {
return TRUE;
}
fbw = surface_width(vc->gfx.ds) * vc->gfx.scale_x;
fbh = surface_height(vc->gfx.ds) * vc->gfx.scale_y;
ww = gtk_widget_get_allocated_width(widget);
wh = gtk_widget_get_allocated_height(widget);
ww_surface = surface_width(vc->gfx.ds) * vc->gfx.scale_x;
wh_surface = surface_height(vc->gfx.ds) * vc->gfx.scale_y;
ww_widget = gtk_widget_get_allocated_width(widget);
wh_widget = gtk_widget_get_allocated_height(widget);
/*
* `widget` may not have the same size with the frame buffer.
@@ -909,41 +997,42 @@ static gboolean gd_motion_event(GtkWidget *widget, GdkEventMotion *motion,
* To achieve that, `vc` will be displayed at (mx, my)
* so that it is displayed at the center of the widget.
*/
mx = my = 0;
if (ww > fbw) {
mx = (ww - fbw) / 2;
wx_offset = wy_offset = 0;
if (ww_widget > ww_surface) {
wx_offset = (ww_widget - ww_surface) / 2;
}
if (wh > fbh) {
my = (wh - fbh) / 2;
if (wh_widget > wh_surface) {
wy_offset = (wh_widget - wh_surface) / 2;
}
/*
* `motion` is reported in `widget` coordinates
* so translating it to the coordinates in `vc`.
*/
x = (motion->x - mx) / vc->gfx.scale_x;
y = (motion->y - my) / vc->gfx.scale_y;
fbx = (motion->x - wx_offset) / vc->gfx.scale_x;
fby = (motion->y - wy_offset) / vc->gfx.scale_y;
trace_gd_motion_event(ww, wh, gtk_widget_get_scale_factor(widget), x, y);
trace_gd_motion_event(ww_widget, wh_widget,
gtk_widget_get_scale_factor(widget), fbx, fby);
if (qemu_input_is_absolute(vc->gfx.dcl.con)) {
if (x < 0 || y < 0 ||
x >= surface_width(vc->gfx.ds) ||
y >= surface_height(vc->gfx.ds)) {
if (fbx < 0 || fby < 0 ||
fbx >= surface_width(vc->gfx.ds) ||
fby >= surface_height(vc->gfx.ds)) {
return TRUE;
}
qemu_input_queue_abs(vc->gfx.dcl.con, INPUT_AXIS_X, x,
qemu_input_queue_abs(vc->gfx.dcl.con, INPUT_AXIS_X, fbx,
0, surface_width(vc->gfx.ds));
qemu_input_queue_abs(vc->gfx.dcl.con, INPUT_AXIS_Y, y,
qemu_input_queue_abs(vc->gfx.dcl.con, INPUT_AXIS_Y, fby,
0, surface_height(vc->gfx.ds));
qemu_input_event_sync();
} else if (s->last_set && s->ptr_owner == vc) {
qemu_input_queue_rel(vc->gfx.dcl.con, INPUT_AXIS_X, x - s->last_x);
qemu_input_queue_rel(vc->gfx.dcl.con, INPUT_AXIS_Y, y - s->last_y);
qemu_input_queue_rel(vc->gfx.dcl.con, INPUT_AXIS_X, fbx - s->last_x);
qemu_input_queue_rel(vc->gfx.dcl.con, INPUT_AXIS_Y, fby - s->last_y);
qemu_input_event_sync();
}
s->last_x = x;
s->last_y = y;
s->last_x = fbx;
s->last_y = fby;
s->last_set = TRUE;
if (!qemu_input_is_absolute(vc->gfx.dcl.con) && s->ptr_owner == vc) {
@@ -1760,8 +1849,16 @@ static gboolean gd_configure(GtkWidget *widget,
GdkEventConfigure *cfg, gpointer opaque)
{
VirtualConsole *vc = opaque;
const double sx = vc->gfx.scale_x, sy = vc->gfx.scale_y;
double width = cfg->width, height = cfg->height;
if (!vc->s->free_scale && !vc->s->full_screen) {
width /= sx;
height /= sy;
}
gd_set_ui_size(vc, width, height);
gd_set_ui_size(vc, cfg->width, cfg->height);
return FALSE;
}
+1 -1
View File
@@ -241,7 +241,7 @@ void sdl2_gl_scanout_flush(DisplayChangeListener *dcl,
SDL_GL_MakeCurrent(scon->real_window, scon->winctx);
SDL_GetWindowSize(scon->real_window, &ww, &wh);
egl_fb_setup_default(&scon->win_fb, ww, wh);
egl_fb_setup_default(&scon->win_fb, ww, wh, 0, 0);
egl_fb_blit(&scon->win_fb, &scon->guest_fb, !scon->y0_top);
SDL_GL_SwapWindow(scon->real_window);
+15 -5
View File
@@ -488,14 +488,14 @@ static void handle_mousemotion(SDL_Event *ev)
{
int max_x, max_y;
struct sdl2_console *scon = get_scon_from_window(ev->motion.windowID);
int scr_w, scr_h, surf_w, surf_h, x, y, dx, dy;
if (!scon || !qemu_console_is_graphic(scon->dcl.con)) {
return;
}
SDL_GetWindowSize(scon->real_window, &scr_w, &scr_h);
if (qemu_input_is_absolute(scon->dcl.con) || absolute_enabled) {
int scr_w, scr_h;
SDL_GetWindowSize(scon->real_window, &scr_w, &scr_h);
max_x = scr_w - 1;
max_y = scr_h - 1;
if (gui_grab && !gui_fullscreen
@@ -509,9 +509,14 @@ static void handle_mousemotion(SDL_Event *ev)
sdl_grab_start(scon);
}
}
surf_w = surface_width(scon->surface);
surf_h = surface_height(scon->surface);
x = (int64_t)ev->motion.x * surf_w / scr_w;
y = (int64_t)ev->motion.y * surf_h / scr_h;
dx = (int64_t)ev->motion.xrel * surf_w / scr_w;
dy = (int64_t)ev->motion.yrel * surf_h / scr_h;
if (gui_grab || qemu_input_is_absolute(scon->dcl.con) || absolute_enabled) {
sdl_send_mouse_event(scon, ev->motion.xrel, ev->motion.yrel,
ev->motion.x, ev->motion.y, ev->motion.state);
sdl_send_mouse_event(scon, dx, dy, x, y, ev->motion.state);
}
}
@@ -520,12 +525,17 @@ static void handle_mousebutton(SDL_Event *ev)
int buttonstate = SDL_GetMouseState(NULL, NULL);
SDL_MouseButtonEvent *bev;
struct sdl2_console *scon = get_scon_from_window(ev->button.windowID);
int scr_w, scr_h, x, y;
if (!scon || !qemu_console_is_graphic(scon->dcl.con)) {
return;
}
bev = &ev->button;
SDL_GetWindowSize(scon->real_window, &scr_w, &scr_h);
x = (int64_t)bev->x * surface_width(scon->surface) / scr_w;
y = (int64_t)bev->y * surface_height(scon->surface) / scr_h;
if (!gui_grab && !qemu_input_is_absolute(scon->dcl.con)) {
if (ev->type == SDL_MOUSEBUTTONUP && bev->button == SDL_BUTTON_LEFT) {
/* start grabbing all events */
@@ -537,7 +547,7 @@ static void handle_mousebutton(SDL_Event *ev)
} else {
buttonstate &= ~SDL_BUTTON(bev->button);
}
sdl_send_mouse_event(scon, 0, 0, bev->x, bev->y, buttonstate);
sdl_send_mouse_event(scon, 0, 0, x, y, buttonstate);
}
}
+175 -30
View File
@@ -6,10 +6,10 @@
#include "qemu/option.h"
#include "qemu/units.h"
#include "hw/qdev-core.h"
#include "migration/blocker.h"
#include "ui/clipboard.h"
#include "ui/console.h"
#include "ui/input.h"
#include "migration/vmstate.h"
#include "trace.h"
#include "qapi/qapi-types-char.h"
@@ -32,14 +32,12 @@
struct VDAgentChardev {
Chardev parent;
/* TODO: migration isn't yet supported */
Error *migration_blocker;
/* config */
bool mouse;
bool clipboard;
/* guest vdagent */
bool connected;
uint32_t caps;
VDIChunkHeader chunk;
uint32_t chunksize;
@@ -47,7 +45,7 @@ struct VDAgentChardev {
uint32_t msgsize;
uint8_t *xbuf;
uint32_t xoff, xsize;
Buffer outbuf;
GByteArray *outbuf;
/* mouse */
DeviceState mouse_dev;
@@ -142,16 +140,16 @@ static void vdagent_send_buf(VDAgentChardev *vd)
{
uint32_t len;
while (!buffer_empty(&vd->outbuf)) {
while (vd->outbuf->len) {
len = qemu_chr_be_can_write(CHARDEV(vd));
if (len == 0) {
return;
}
if (len > vd->outbuf.offset) {
len = vd->outbuf.offset;
if (len > vd->outbuf->len) {
len = vd->outbuf->len;
}
qemu_chr_be_write(CHARDEV(vd), vd->outbuf.buffer, len);
buffer_advance(&vd->outbuf, len);
qemu_chr_be_write(CHARDEV(vd), vd->outbuf->data, len);
g_byte_array_remove_range(vd->outbuf, 0, len);
}
}
@@ -166,7 +164,7 @@ static void vdagent_send_msg(VDAgentChardev *vd, VDAgentMessage *msg)
msg->protocol = VD_AGENT_PROTOCOL;
if (vd->outbuf.offset + msgsize > VDAGENT_BUFFER_LIMIT) {
if (vd->outbuf->len + msgsize > VDAGENT_BUFFER_LIMIT) {
error_report("buffer full, dropping message");
return;
}
@@ -177,9 +175,8 @@ static void vdagent_send_msg(VDAgentChardev *vd, VDAgentMessage *msg)
if (chunk.size > 1024) {
chunk.size = 1024;
}
buffer_reserve(&vd->outbuf, sizeof(chunk) + chunk.size);
buffer_append(&vd->outbuf, &chunk, sizeof(chunk));
buffer_append(&vd->outbuf, msgbuf + msgoff, chunk.size);
g_byte_array_append(vd->outbuf, (void *)&chunk, sizeof(chunk));
g_byte_array_append(vd->outbuf, msgbuf + msgoff, chunk.size);
msgoff += chunk.size;
}
vdagent_send_buf(vd);
@@ -672,10 +669,6 @@ static void vdagent_chr_open(Chardev *chr,
return;
#endif
if (migrate_add_blocker(&vd->migration_blocker, errp) != 0) {
return;
}
vd->mouse = VDAGENT_MOUSE_DEFAULT;
if (cfg->has_mouse) {
vd->mouse = cfg->mouse;
@@ -694,6 +687,18 @@ static void vdagent_chr_open(Chardev *chr,
*be_opened = true;
}
static void vdagent_clipboard_peer_register(VDAgentChardev *vd)
{
if (vd->cbpeer.notifier.notify != NULL) {
return;
}
vd->cbpeer.name = "vdagent";
vd->cbpeer.notifier.notify = vdagent_clipboard_notify;
vd->cbpeer.request = vdagent_clipboard_request;
qemu_clipboard_peer_register(&vd->cbpeer);
}
static void vdagent_chr_recv_caps(VDAgentChardev *vd, VDAgentMessage *msg)
{
VDAgentAnnounceCapabilities *caps = (void *)msg->data;
@@ -720,13 +725,9 @@ static void vdagent_chr_recv_caps(VDAgentChardev *vd, VDAgentMessage *msg)
memset(vd->last_serial, 0, sizeof(vd->last_serial));
if (have_clipboard(vd) && vd->cbpeer.notifier.notify == NULL) {
if (have_clipboard(vd)) {
qemu_clipboard_reset_serial();
vd->cbpeer.name = "vdagent";
vd->cbpeer.notifier.notify = vdagent_clipboard_notify;
vd->cbpeer.request = vdagent_clipboard_request;
qemu_clipboard_peer_register(&vd->cbpeer);
vdagent_clipboard_peer_register(vd);
}
}
@@ -859,7 +860,8 @@ static void vdagent_disconnect(VDAgentChardev *vd)
{
trace_vdagent_disconnect();
buffer_reset(&vd->outbuf);
vd->connected = false;
g_byte_array_set_size(vd->outbuf, 0);
vdagent_reset_bufs(vd);
vd->caps = 0;
if (vd->mouse_hs) {
@@ -877,6 +879,10 @@ static void vdagent_chr_set_fe_open(struct Chardev *chr, int fe_open)
trace_vdagent_fe_open(fe_open);
if (vd->connected == fe_open) {
return;
}
if (!fe_open) {
trace_vdagent_close();
vdagent_disconnect(vd);
@@ -886,6 +892,7 @@ static void vdagent_chr_set_fe_open(struct Chardev *chr, int fe_open)
return;
}
vd->connected = true;
vdagent_send_caps(vd, true);
}
@@ -916,25 +923,163 @@ static void vdagent_chr_class_init(ObjectClass *oc, const void *data)
cc->chr_accept_input = vdagent_chr_accept_input;
}
static int post_load(void *opaque, int version_id)
{
VDAgentChardev *vd = QEMU_VDAGENT_CHARDEV(opaque);
if (have_mouse(vd) && vd->mouse_hs) {
qemu_input_handler_activate(vd->mouse_hs);
}
if (have_clipboard(vd)) {
vdagent_clipboard_peer_register(vd);
}
return 0;
}
static const VMStateDescription vmstate_chunk = {
.name = "vdagent/chunk",
.version_id = 0,
.minimum_version_id = 0,
.fields = (const VMStateField[]) {
VMSTATE_UINT32(port, VDIChunkHeader),
VMSTATE_UINT32(size, VDIChunkHeader),
VMSTATE_END_OF_LIST()
}
};
static const VMStateDescription vmstate_vdba = {
.name = "vdagent/bytearray",
.version_id = 0,
.minimum_version_id = 0,
.fields = (const VMStateField[]) {
VMSTATE_UINT32(len, GByteArray),
VMSTATE_VBUFFER_ALLOC_UINT32(data, GByteArray, 0, 0, len),
VMSTATE_END_OF_LIST()
}
};
struct CBInfoArray {
uint32_t n;
QemuClipboardInfo cbinfo[QEMU_CLIPBOARD_SELECTION__COUNT];
};
static const VMStateDescription vmstate_cbinfo_array = {
.name = "cbinfoarray",
.fields = (const VMStateField[]) {
VMSTATE_UINT32(n, struct CBInfoArray),
VMSTATE_STRUCT_VARRAY_UINT32(cbinfo, struct CBInfoArray, n,
0, vmstate_cbinfo, QemuClipboardInfo),
VMSTATE_END_OF_LIST()
}
};
static int put_cbinfo(QEMUFile *f, void *pv, size_t size,
const VMStateField *field, JSONWriter *vmdesc)
{
VDAgentChardev *vd = QEMU_VDAGENT_CHARDEV(pv);
struct CBInfoArray cbinfo = {};
int i;
if (!have_clipboard(vd)) {
return 0;
}
for (i = 0; i < QEMU_CLIPBOARD_SELECTION__COUNT; i++) {
if (qemu_clipboard_peer_owns(&vd->cbpeer, i)) {
cbinfo.cbinfo[cbinfo.n++] = *qemu_clipboard_info(i);
}
}
return vmstate_save_state(f, &vmstate_cbinfo_array, &cbinfo, vmdesc);
}
static int get_cbinfo(QEMUFile *f, void *pv, size_t size,
const VMStateField *field)
{
VDAgentChardev *vd = QEMU_VDAGENT_CHARDEV(pv);
struct CBInfoArray cbinfo = {};
int i, ret;
if (!have_clipboard(vd)) {
return 0;
}
vdagent_clipboard_peer_register(vd);
ret = vmstate_load_state(f, &vmstate_cbinfo_array, &cbinfo, 0);
if (ret) {
return ret;
}
for (i = 0; i < cbinfo.n; i++) {
g_autoptr(QemuClipboardInfo) info =
qemu_clipboard_info_new(&vd->cbpeer, cbinfo.cbinfo[i].selection);
/* this will steal clipboard data pointer from cbinfo.types */
memcpy(info->types, cbinfo.cbinfo[i].types, sizeof(cbinfo.cbinfo[i].types));
qemu_clipboard_update(info);
}
return 0;
}
static const VMStateInfo vmstate_cbinfos = {
.name = "vdagent/cbinfos",
.get = get_cbinfo,
.put = put_cbinfo,
};
static const VMStateDescription vmstate_vdagent = {
.name = "vdagent",
.version_id = 0,
.minimum_version_id = 0,
.post_load = post_load,
.fields = (const VMStateField[]) {
VMSTATE_BOOL(connected, VDAgentChardev),
VMSTATE_UINT32(caps, VDAgentChardev),
VMSTATE_STRUCT(chunk, VDAgentChardev, 0, vmstate_chunk, VDIChunkHeader),
VMSTATE_UINT32(chunksize, VDAgentChardev),
VMSTATE_UINT32(msgsize, VDAgentChardev),
VMSTATE_VBUFFER_ALLOC_UINT32(msgbuf, VDAgentChardev, 0, 0, msgsize),
VMSTATE_UINT32(xsize, VDAgentChardev),
VMSTATE_UINT32(xoff, VDAgentChardev),
VMSTATE_VBUFFER_ALLOC_UINT32(xbuf, VDAgentChardev, 0, 0, xsize),
VMSTATE_STRUCT_POINTER(outbuf, VDAgentChardev, vmstate_vdba, GByteArray),
VMSTATE_UINT32(mouse_x, VDAgentChardev),
VMSTATE_UINT32(mouse_y, VDAgentChardev),
VMSTATE_UINT32(mouse_btn, VDAgentChardev),
VMSTATE_UINT32(mouse_display, VDAgentChardev),
VMSTATE_UINT32_ARRAY(last_serial, VDAgentChardev,
QEMU_CLIPBOARD_SELECTION__COUNT),
VMSTATE_UINT32_ARRAY(cbpending, VDAgentChardev,
QEMU_CLIPBOARD_SELECTION__COUNT),
{
.name = "cbinfos",
.info = &vmstate_cbinfos,
.flags = VMS_SINGLE,
},
VMSTATE_END_OF_LIST()
}
};
static void vdagent_chr_init(Object *obj)
{
VDAgentChardev *vd = QEMU_VDAGENT_CHARDEV(obj);
buffer_init(&vd->outbuf, "vdagent-outbuf");
error_setg(&vd->migration_blocker,
"The vdagent chardev doesn't yet support migration");
vd->outbuf = g_byte_array_new();
vmstate_register_any(NULL, &vmstate_vdagent, vd);
}
static void vdagent_chr_fini(Object *obj)
{
VDAgentChardev *vd = QEMU_VDAGENT_CHARDEV(obj);
migrate_del_blocker(&vd->migration_blocker);
vdagent_disconnect(vd);
if (vd->mouse_hs) {
qemu_input_handler_unregister(vd->mouse_hs);
}
buffer_free(&vd->outbuf);
g_clear_pointer(&vd->outbuf, g_byte_array_unref);
}
static const TypeInfo vdagent_chr_type_info = {