libmks/lib/mks-paintable.c
Christian Hergert 2286183747 lib: export paintable properties
This is mostly useful so that we can see things within the inspector at
runtime in case we need to debug what backends are used.
2023-02-15 07:01:32 -08:00

696 lines
23 KiB
C

/*
* mks-paintable.c
*
* Copyright 2023 Christian Hergert <chergert@redhat.com>
*
* 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 <http://www.gnu.org/licenses/>.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#include "config.h"
#include <errno.h>
#include <sys/socket.h>
#include <glib/gstdio.h>
#include <pixman.h>
#include "mks-cairo-framebuffer-private.h"
#include "mks-dmabuf-paintable-private.h"
#include "mks-paintable-private.h"
#include "mks-qemu.h"
struct _MksPaintable
{
GObject parent_instance;
GdkGLContext *gl_context;
MksQemuListener *listener;
GDBusConnection *connection;
GdkPaintable *child;
};
enum {
PROP_0,
PROP_PAINTABLE,
N_PROPS
};
static GParamSpec *properties [N_PROPS];
static cairo_format_t
_pixman_format_to_cairo_format (guint pixman_format)
{
switch (pixman_format)
{
case PIXMAN_rgba_float:
return CAIRO_FORMAT_RGBA128F;
case PIXMAN_rgb_float:
return CAIRO_FORMAT_RGB96F;
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)
{
GdkPaintable *child = MKS_PAINTABLE (paintable)->child;
if (child != NULL)
gdk_paintable_snapshot (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_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_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_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);
}
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;
g_autofd int dmabuf_fd = -1;
GdkGLContext *gl_context;
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 (-1 == (dmabuf_fd = g_unix_fd_list_get (unix_fd_list, handle, &error)) ||
!(gl_context = mks_paintable_get_gl_context (self, &error)) ||
!(child = mks_dmabuf_paintable_new (gl_context,
dmabuf_fd,
width, height,
stride, fourcc,
modifier, y0_top,
&error)))
{
g_dbus_method_invocation_return_gerror (invocation, error);
return TRUE;
}
mks_paintable_set_child (self, GDK_PAINTABLE (child));
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)
{
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))
mks_dmabuf_paintable_invalidate (MKS_DMABUF_PAINTABLE (self->child), x, y, width, height);
mks_qemu_listener_complete_update_dmabuf (listener, invocation);
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));
}
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 *bytes,
MksQemuListener *listener)
{
g_assert (MKS_IS_PAINTABLE (self));
g_assert (G_IS_DBUS_METHOD_INVOCATION (invocation));
g_assert (MKS_QEMU_IS_LISTENER (listener));
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));
mks_qemu_listener_complete_mouse_set (listener, invocation);
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));
}