/* * mks-paintable.c * * Copyright 2023 Christian Hergert * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * SPDX-License-Identifier: GPL-3.0-or-later */ #include "config.h" #include #include #include #include #include #include "mks-cairo-framebuffer-private.h" #include "mks-dmabuf-paintable-private.h" #include "mks-paintable-private.h" #include "mks-qemu.h" #include "mks-util-private.h" #include "mks-marshal.h" struct _MksPaintable { GObject parent_instance; GdkGLContext *gl_context; MksQemuListener *listener; GDBusConnection *connection; GdkPaintable *child; GdkCursor *cursor; MksDmabufScanoutData *scanout_data; int mouse_x; int mouse_y; guint y_inverted : 1; }; enum { PROP_0, PROP_CURSOR, PROP_PAINTABLE, N_PROPS }; enum { MOUSE_SET, N_SIGNALS }; static GParamSpec *properties [N_PROPS]; static guint signals [N_SIGNALS]; static cairo_format_t _pixman_format_to_cairo_format (guint pixman_format) { switch (pixman_format) { #if _CAIRO_CHECK_VERSION(1, 17, 2) case PIXMAN_rgba_float: return CAIRO_FORMAT_RGBA128F; case PIXMAN_rgb_float: return CAIRO_FORMAT_RGB96F; #endif case PIXMAN_a8r8g8b8: return CAIRO_FORMAT_ARGB32; case PIXMAN_x2r10g10b10: return CAIRO_FORMAT_RGB30; case PIXMAN_x8r8g8b8: return CAIRO_FORMAT_RGB24; case PIXMAN_a8: return CAIRO_FORMAT_A8; case PIXMAN_a1: return CAIRO_FORMAT_A1; case PIXMAN_r5g6b5: return CAIRO_FORMAT_RGB16_565; default: return 0; } } static int mks_paintable_get_intrinsic_height (GdkPaintable *paintable) { GdkPaintable *child = MKS_PAINTABLE (paintable)->child; return child ? gdk_paintable_get_intrinsic_height (child) : 0; } static int mks_paintable_get_intrinsic_width (GdkPaintable *paintable) { GdkPaintable *child = MKS_PAINTABLE (paintable)->child; return child ? gdk_paintable_get_intrinsic_width (child) : 0; } static double mks_paintable_get_intrinsic_aspect_ratio (GdkPaintable *paintable) { GdkPaintable *child = MKS_PAINTABLE (paintable)->child; return child ? gdk_paintable_get_intrinsic_aspect_ratio (child) : .0; } static void mks_paintable_snapshot (GdkPaintable *paintable, GdkSnapshot *snapshot, double width, double height) { MksPaintable *self = MKS_PAINTABLE (paintable); if (self->child != NULL) { if (MKS_IS_DMABUF_PAINTABLE (self->child) && !self->y_inverted) { gtk_snapshot_save (snapshot); gtk_snapshot_translate (snapshot, &GRAPHENE_POINT_INIT (0, height)); gtk_snapshot_scale (snapshot, 1, -1); gdk_paintable_snapshot (self->child, snapshot, width, height); gtk_snapshot_restore (snapshot); } else { gdk_paintable_snapshot (self->child, snapshot, width, height); } } } static void paintable_iface_init (GdkPaintableInterface *iface) { iface->get_intrinsic_height = mks_paintable_get_intrinsic_height; iface->get_intrinsic_width = mks_paintable_get_intrinsic_width; iface->get_intrinsic_aspect_ratio = mks_paintable_get_intrinsic_aspect_ratio; iface->snapshot = mks_paintable_snapshot; } G_DEFINE_FINAL_TYPE_WITH_CODE (MksPaintable, mks_paintable, G_TYPE_OBJECT, G_IMPLEMENT_INTERFACE (GDK_TYPE_PAINTABLE, paintable_iface_init)) static GdkGLContext * mks_paintable_get_gl_context (MksPaintable *self, GError **error) { g_assert (MKS_IS_PAINTABLE (self)); if (self->gl_context == NULL) { GdkDisplay *display = gdk_display_get_default (); if (!(self->gl_context = gdk_display_create_gl_context (display, error))) return NULL; } return self->gl_context; } static void mks_paintable_dispose (GObject *object) { MksPaintable *self = (MksPaintable *)object; g_clear_object (&self->connection); g_clear_object (&self->listener); g_clear_object (&self->child); g_clear_object (&self->gl_context); g_clear_object (&self->cursor); G_OBJECT_CLASS (mks_paintable_parent_class)->dispose (object); } static void mks_paintable_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { MksPaintable *self = MKS_PAINTABLE (object); switch (prop_id) { case PROP_CURSOR: g_value_set_object (value, _mks_paintable_get_cursor (self)); break; case PROP_PAINTABLE: g_value_set_object (value, self->child); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } } static void mks_paintable_class_init (MksPaintableClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->dispose = mks_paintable_dispose; object_class->get_property = mks_paintable_get_property; properties [PROP_CURSOR] = g_param_spec_object ("cursor", NULL, NULL, GDK_TYPE_CURSOR, (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); properties [PROP_PAINTABLE] = g_param_spec_object ("paintable", NULL, NULL, GDK_TYPE_PAINTABLE, (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); g_object_class_install_properties (object_class, N_PROPS, properties); signals [MOUSE_SET] = g_signal_new ("mouse-set", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL, _mks_marshal_VOID__INT_INT, G_TYPE_NONE, 2, G_TYPE_INT, G_TYPE_INT); g_signal_set_va_marshaller (signals [MOUSE_SET], G_TYPE_FROM_CLASS (klass), _mks_marshal_VOID__INT_INTv); } static void mks_paintable_init (MksPaintable *self) { } static void mks_paintable_invalidate_contents_cb (MksPaintable *self, GdkPaintable *paintable) { g_assert (MKS_IS_PAINTABLE (self)); g_assert (GDK_IS_PAINTABLE (paintable)); gdk_paintable_invalidate_contents (GDK_PAINTABLE (self)); } static void mks_paintable_invalidate_size_cb (MksPaintable *self, GdkPaintable *paintable) { g_assert (MKS_IS_PAINTABLE (self)); g_assert (GDK_IS_PAINTABLE (paintable)); gdk_paintable_invalidate_size (GDK_PAINTABLE (self)); } static void mks_paintable_set_child (MksPaintable *self, GdkPaintable *child) { gboolean size_changed; g_assert (MKS_IS_PAINTABLE (self)); g_assert (!child || GDK_IS_PAINTABLE (child)); if (self->child == child) return; size_changed = self->child == NULL || child == NULL || gdk_paintable_get_intrinsic_width (self->child) != gdk_paintable_get_intrinsic_width (child) || gdk_paintable_get_intrinsic_height (self->child) != gdk_paintable_get_intrinsic_height (child); if (self->child != NULL) { g_signal_handlers_disconnect_by_func (self->child, G_CALLBACK (mks_paintable_invalidate_size_cb), self); g_signal_handlers_disconnect_by_func (self->child, G_CALLBACK (mks_paintable_invalidate_contents_cb), self); g_clear_object (&self->child); } if (child != NULL) { self->child = g_object_ref (child); g_signal_connect_object (self->child, "invalidate-size", G_CALLBACK (mks_paintable_invalidate_size_cb), self, G_CONNECT_SWAPPED); g_signal_connect_object (self->child, "invalidate-contents", G_CALLBACK (mks_paintable_invalidate_contents_cb), self, G_CONNECT_SWAPPED); } gdk_paintable_invalidate_contents (GDK_PAINTABLE (self)); if (size_changed) gdk_paintable_invalidate_size (GDK_PAINTABLE (self)); g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_PAINTABLE]); } static gboolean mks_paintable_listener_scanout_dmabuf (MksPaintable *self, GDBusMethodInvocation *invocation, GUnixFDList *unix_fd_list, GVariant *dmabuf, guint width, guint height, guint stride, guint fourcc, guint64 modifier, gboolean y0_top, MksQemuListener *listener) { g_autoptr(MksDmabufPaintable) child = NULL; g_autoptr(GError) error = NULL; int dmabuf_fd = -1; MksDmabufScanoutData *scanout_data; guint handle; g_assert (MKS_IS_PAINTABLE (self)); g_assert (G_IS_DBUS_METHOD_INVOCATION (invocation)); g_assert (MKS_QEMU_IS_LISTENER (listener)); g_assert (g_variant_is_of_type (dmabuf, G_VARIANT_TYPE_HANDLE)); handle = g_variant_get_handle (dmabuf); if (handle >= g_unix_fd_list_get_length (unix_fd_list)) { g_dbus_method_invocation_return_error_literal (invocation, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, "Invalid handle to DMA-BUF"); return TRUE; } if (!MKS_IS_DMABUF_PAINTABLE (self->child)) { child = mks_dmabuf_paintable_new (); mks_paintable_set_child (self, GDK_PAINTABLE (child)); } if (-1 == (dmabuf_fd = g_unix_fd_list_get (unix_fd_list, handle, &error))) { g_dbus_method_invocation_return_gerror (invocation, error); return TRUE; } self->y_inverted = !y0_top; scanout_data = g_new0 (MksDmabufScanoutData, 1); scanout_data->dmabuf_fd = dmabuf_fd; scanout_data->width = width; scanout_data->height = height; scanout_data->stride = stride; scanout_data->fourcc = fourcc; scanout_data->modifier = modifier; g_clear_pointer (&self->scanout_data, g_free); self->scanout_data = scanout_data; mks_qemu_listener_complete_scanout_dmabuf (listener, invocation, NULL); return TRUE; } static gboolean mks_paintable_listener_update_dmabuf (MksPaintable *self, GDBusMethodInvocation *invocation, int x, int y, int width, int height, MksQemuListener *listener) { cairo_region_t *region = NULL; g_autoptr(GError) error = NULL; GdkGLContext *gl_context; g_assert (MKS_IS_PAINTABLE (self)); g_assert (G_IS_DBUS_METHOD_INVOCATION (invocation)); g_assert (MKS_QEMU_IS_LISTENER (listener)); if (MKS_IS_DMABUF_PAINTABLE (self->child)) { g_assert (self->scanout_data != NULL); if (!self->y_inverted) y = self->scanout_data->height - y - height; region = cairo_region_create_rectangle (&(cairo_rectangle_int_t) { x, y, width, height }); if (!(gl_context = mks_paintable_get_gl_context (self, &error)) || !mks_dmabuf_paintable_import (MKS_DMABUF_PAINTABLE (self->child), gl_context, self->scanout_data, region, &error)) { g_dbus_method_invocation_return_gerror (invocation, error); goto cleanup; } } mks_qemu_listener_complete_update_dmabuf (listener, invocation); cleanup: g_clear_pointer (®ion, cairo_region_destroy); return TRUE; } static gboolean mks_paintable_listener_update (MksPaintable *self, GDBusMethodInvocation *invocation, int x, int y, int width, int height, guint stride, guint pixman_format, GVariant *bytestring, MksQemuListener *listener) { g_autoptr(GBytes) bytes = NULL; cairo_surface_t *source; const guint8 *data; cairo_t *cr; cairo_format_t format; gsize data_len; g_assert (MKS_IS_PAINTABLE (self)); g_assert (G_IS_DBUS_METHOD_INVOCATION (invocation)); g_assert (MKS_QEMU_IS_LISTENER (listener)); if (!MKS_IS_CAIRO_FRAMEBUFFER (self->child) || !(format = _pixman_format_to_cairo_format (pixman_format))) { g_dbus_method_invocation_return_error_literal (invocation, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, "Invalid operation"); return TRUE; } bytes = g_variant_get_data_as_bytes (bytestring); data = g_bytes_get_data (bytes, &data_len); if (data_len < cairo_format_stride_for_width (format, width) * height) { g_dbus_method_invocation_return_error_literal (invocation, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, "Stride invalid for size"); return TRUE; } /* We can get in a protocol race condition here in that we will get updates * for framebuffer content _BEFORE_ we'll get notified of property changes * about the MksQemuConsole's size. * * To overcome that, if we detect something larger than our current * framebuffer, we'll resize it and draw over the old contents in a * new framebuffer. * * When shrinking, we can do this as well and then handle it when the * console size notification arrives. * * Generally this is seen at startup during EFI/BIOS. */ if (x + width > gdk_paintable_get_intrinsic_width (self->child) || y + height > gdk_paintable_get_intrinsic_height (self->child)) { guint max_width = MAX (gdk_paintable_get_intrinsic_width (self->child), x + width); guint max_height = MAX (gdk_paintable_get_intrinsic_height (self->child), y + height); g_autoptr(MksCairoFramebuffer) framebuffer = mks_cairo_framebuffer_new (format, max_width, max_height); mks_cairo_framebuffer_copy_to (MKS_CAIRO_FRAMEBUFFER (self->child), framebuffer); mks_paintable_set_child (self, GDK_PAINTABLE (framebuffer)); } source = cairo_image_surface_create_for_data ((guint8 *)data, format, width, height, stride); cr = mks_cairo_framebuffer_update (MKS_CAIRO_FRAMEBUFFER (self->child), x, y, width, height); cairo_set_operator (cr, CAIRO_OPERATOR_SOURCE); cairo_set_source_surface (cr, source, 0, 0); cairo_rectangle (cr, 0, 0, width, height); cairo_paint (cr); cairo_destroy (cr); cairo_surface_destroy (source); mks_qemu_listener_complete_update (listener, invocation); return TRUE; } static gboolean mks_paintable_listener_scanout (MksPaintable *self, GDBusMethodInvocation *invocation, guint width, guint height, guint stride, guint pixman_format, GVariant *bytestring, MksQemuListener *listener) { g_autoptr(GBytes) bytes = NULL; cairo_surface_t *source; const guint8 *data; cairo_t *cr; cairo_format_t format; gsize data_len; g_assert (MKS_IS_PAINTABLE (self)); g_assert (G_IS_DBUS_METHOD_INVOCATION (invocation)); g_assert (MKS_QEMU_IS_LISTENER (listener)); g_assert (g_variant_is_of_type (bytestring, G_VARIANT_TYPE_BYTESTRING)); if (!(format = _pixman_format_to_cairo_format (pixman_format))) { g_dbus_method_invocation_return_error_literal (invocation, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, "Pixman format not supported"); return TRUE; } bytes = g_variant_get_data_as_bytes (bytestring); data = g_bytes_get_data (bytes, &data_len); if (data_len < cairo_format_stride_for_width (format, width) * height) { g_dbus_method_invocation_return_error_literal (invocation, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, "Stride invalid for size"); return TRUE; } if (self->child == NULL || !MKS_IS_CAIRO_FRAMEBUFFER (self->child) || width != gdk_paintable_get_intrinsic_width (self->child) || height != gdk_paintable_get_intrinsic_height (self->child)) { g_autoptr(MksCairoFramebuffer) child = mks_cairo_framebuffer_new (format, width, height); mks_paintable_set_child (self, GDK_PAINTABLE (child)); } self->y_inverted = FALSE; source = cairo_image_surface_create_for_data ((guint8 *)data, format, width, height, stride); cr = mks_cairo_framebuffer_update (MKS_CAIRO_FRAMEBUFFER (self->child), 0, 0, width, height); cairo_set_source_surface (cr, source, 0, 0); cairo_set_operator (cr, CAIRO_OPERATOR_SOURCE); cairo_rectangle (cr, 0, 0, width, height); cairo_paint (cr); cairo_destroy (cr); cairo_surface_destroy (source); mks_qemu_listener_complete_scanout (listener, invocation); return TRUE; } static gboolean mks_paintable_listener_cursor_define (MksPaintable *self, GDBusMethodInvocation *invocation, int width, int height, int hot_x, int hot_y, GVariant *bytestring, MksQemuListener *listener) { g_autoptr(GBytes) bytes = NULL; g_autoptr(GdkTexture) texture = NULL; g_autoptr(GdkCursor) cursor = NULL; gsize data_len; g_assert (MKS_IS_PAINTABLE (self)); g_assert (G_IS_DBUS_METHOD_INVOCATION (invocation)); g_assert (MKS_QEMU_IS_LISTENER (listener)); if (width < 1 || width > 512 || height < 1 || height > 512 || !(bytes = g_variant_get_data_as_bytes (bytestring))) goto failure; data_len = g_bytes_get_size (bytes); if (data_len != (4 * width * height)) goto failure; texture = gdk_memory_texture_new (width, height, #if G_BYTE_ORDER == G_LITTLE_ENDIAN GDK_MEMORY_B8G8R8A8_PREMULTIPLIED, #else GDK_MEMORY_A8R8G8B8_PREMULTIPLIED, #endif bytes, width * 4); cursor = gdk_cursor_new_from_texture (texture, hot_x, hot_y, NULL); if (g_set_object (&self->cursor, cursor)) g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_CURSOR]); failure: mks_qemu_listener_complete_cursor_define (listener, invocation); return TRUE; } static gboolean mks_paintable_listener_mouse_set (MksPaintable *self, GDBusMethodInvocation *invocation, int x, int y, int on, MksQemuListener *listener) { g_assert (MKS_IS_PAINTABLE (self)); g_assert (G_IS_DBUS_METHOD_INVOCATION (invocation)); g_assert (MKS_QEMU_IS_LISTENER (listener)); self->mouse_x = x; self->mouse_y = y; mks_qemu_listener_complete_mouse_set (listener, invocation); g_signal_emit (self, signals[MOUSE_SET], 0, x, y); return TRUE; } static gboolean mks_paintable_listener_disable (MksPaintable *self, GDBusMethodInvocation *invocation, MksQemuListener *listener) { g_assert (MKS_IS_PAINTABLE (self)); g_assert (G_IS_DBUS_METHOD_INVOCATION (invocation)); g_assert (MKS_QEMU_IS_LISTENER (listener)); if (MKS_IS_CAIRO_FRAMEBUFFER (self->child)) mks_cairo_framebuffer_clear (MKS_CAIRO_FRAMEBUFFER (self->child)); gdk_paintable_invalidate_contents (GDK_PAINTABLE (self)); mks_qemu_listener_complete_disable (listener, invocation); return TRUE; } static gboolean create_socketpair (int *us, int *them, GError **error) { int fds[2]; int rv; rv = socketpair (AF_UNIX, SOCK_STREAM|SOCK_NONBLOCK|SOCK_CLOEXEC, 0, fds); if (rv != 0) { int errsv = errno; g_set_error_literal (error, G_IO_ERROR, g_io_error_from_errno (errsv), g_strerror (errsv)); return FALSE; } *us = fds[0]; *them = fds[1]; return TRUE; } static void mks_paintable_connection_cb (GObject *object, GAsyncResult *result, gpointer user_data) { g_autoptr(MksPaintable) self = user_data; g_autoptr(GDBusConnection) connection = NULL; g_autoptr(GError) error = NULL; g_assert (MKS_IS_PAINTABLE (self)); g_assert (G_IS_ASYNC_RESULT (result)); if (!(connection = g_dbus_connection_new_finish (result, &error))) { g_warning ("Failed to create D-Bus connection: %s", error->message); return; } g_set_object (&self->connection, connection); if (!g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (self->listener), connection, "/org/qemu/Display1/Listener", &error)) { g_warning ("Failed to export listener on bus: %s", error->message); return; } g_dbus_connection_start_message_processing (connection); } GdkPaintable * _mks_paintable_new (GCancellable *cancellable, int *peer_fd, GError **error) { g_autoptr(MksPaintable) self = NULL; g_autoptr(GSocketConnection) io_stream = NULL; g_autoptr(GSocket) socket = NULL; g_autofd int us = -1; g_autofd int them = -1; g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), NULL); g_return_val_if_fail (peer_fd != NULL, NULL); *peer_fd = -1; self = g_object_new (MKS_TYPE_PAINTABLE, NULL); /* Create a socketpair() to use for D-Bus P2P protocol. We will be receiving * DMA-BUF FDs over this. */ if (!create_socketpair (&us, &them, error)) return NULL; /* Create socket for our side of the socket pair */ if (!(socket = g_socket_new_from_fd (us, error))) return NULL; us = -1; /* And convert that socket into a GIOStream */ io_stream = g_socket_connection_factory_create_connection (socket); /* Setup our listener and callbacks to process requests */ self->listener = mks_qemu_listener_skeleton_new (); g_signal_connect_object (self->listener, "handle-scanout", G_CALLBACK (mks_paintable_listener_scanout), self, G_CONNECT_SWAPPED); g_signal_connect_object (self->listener, "handle-update", G_CALLBACK (mks_paintable_listener_update), self, G_CONNECT_SWAPPED); g_signal_connect_object (self->listener, "handle-scanout-dmabuf", G_CALLBACK (mks_paintable_listener_scanout_dmabuf), self, G_CONNECT_SWAPPED); g_signal_connect_object (self->listener, "handle-update-dmabuf", G_CALLBACK (mks_paintable_listener_update_dmabuf), self, G_CONNECT_SWAPPED); g_signal_connect_object (self->listener, "handle-disable", G_CALLBACK (mks_paintable_listener_disable), self, G_CONNECT_SWAPPED); g_signal_connect_object (self->listener, "handle-cursor-define", G_CALLBACK (mks_paintable_listener_cursor_define), self, G_CONNECT_SWAPPED); g_signal_connect_object (self->listener, "handle-mouse-set", G_CALLBACK (mks_paintable_listener_mouse_set), self, G_CONNECT_SWAPPED); /* Asynchronously create connection because we can't do it synchronously * as the other side is doing AUTHENTICATION_SERVER for no good reason. */ g_dbus_connection_new (G_IO_STREAM (io_stream), NULL, G_DBUS_CONNECTION_FLAGS_DELAY_MESSAGE_PROCESSING|G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT, NULL, cancellable, mks_paintable_connection_cb, g_object_ref (self)); *peer_fd = g_steal_fd (&them); g_assert (*peer_fd != -1); g_assert (MKS_IS_PAINTABLE (self)); g_assert (MKS_QEMU_IS_LISTENER (self->listener)); return GDK_PAINTABLE (g_steal_pointer (&self)); } /** * _mks_paintable_get_cursor: * @self: a #MksPaintable * * Gets the cursor as defined by the Qemu instance. * * Returns: (transfer none) (nullable): a #GdkCursor or %NULL */ GdkCursor * _mks_paintable_get_cursor (MksPaintable *self) { g_return_val_if_fail (MKS_IS_PAINTABLE (self), NULL); return self->cursor; }