libmks/lib/mks-paintable.c
Christian Hergert ddd6a58adc lib: use g_variant_get_data_as_bytes() to get byte array
We don't want to go through get_bytestring() as this is a bit cleaner as
to ensure we're holding onto references for the rest of the function
lifetime. Otherwise, completing the operation could cause our parameters
to be released (at least theoretically).
2023-02-14 12:24:15 -08:00

628 lines
21 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-paintable-private.h"
#include "mks-qemu.h"
struct _MksPaintable
{
GObject parent_instance;
MksQemuListener *listener;
GDBusConnection *connection;
MksCairoFramebuffer *framebuffer;
guint mode : 2;
};
enum {
MODE_INITIAL = 0,
MODE_FRAMEBUFFER,
MODE_DMABUF,
};
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)
{
MksPaintable *self = MKS_PAINTABLE (paintable);
if (self->mode == MODE_FRAMEBUFFER)
return gdk_paintable_get_intrinsic_height (GDK_PAINTABLE (self->framebuffer));
return 0;
}
static int
mks_paintable_get_intrinsic_width (GdkPaintable *paintable)
{
MksPaintable *self = MKS_PAINTABLE (paintable);
if (self->mode == MODE_FRAMEBUFFER)
return gdk_paintable_get_intrinsic_width (GDK_PAINTABLE (self->framebuffer));
return 0;
}
static double
mks_paintable_get_intrinsic_aspect_ratio (GdkPaintable *paintable)
{
MksPaintable *self = MKS_PAINTABLE (paintable);
if (self->mode == MODE_FRAMEBUFFER)
return gdk_paintable_get_intrinsic_aspect_ratio (GDK_PAINTABLE (self->framebuffer));
return .0;
}
static void
mks_paintable_snapshot (GdkPaintable *paintable,
GdkSnapshot *snapshot,
double width,
double height)
{
MksPaintable *self = (MksPaintable *)paintable;
g_assert (MKS_IS_PAINTABLE (self));
g_assert (GDK_IS_SNAPSHOT (snapshot));
if (self->mode == MODE_FRAMEBUFFER)
gdk_paintable_snapshot (GDK_PAINTABLE (self->framebuffer), 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 void
mks_paintable_dispose (GObject *object)
{
MksPaintable *self = (MksPaintable *)object;
g_clear_object (&self->connection);
g_clear_object (&self->listener);
g_clear_object (&self->framebuffer);
G_OBJECT_CLASS (mks_paintable_parent_class)->dispose (object);
}
static void
mks_paintable_class_init (MksPaintableClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
object_class->dispose = mks_paintable_dispose;
}
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_framebuffer (MksPaintable *self,
MksCairoFramebuffer *framebuffer)
{
gboolean size_changed;
g_assert (MKS_IS_PAINTABLE (self));
g_assert (!framebuffer || MKS_IS_CAIRO_FRAMEBUFFER (framebuffer));
if (self->framebuffer == framebuffer)
return;
size_changed = self->framebuffer == NULL ||
framebuffer == NULL ||
mks_cairo_framebuffer_get_width (self->framebuffer) != mks_cairo_framebuffer_get_width (framebuffer) ||
mks_cairo_framebuffer_get_height (self->framebuffer) != mks_cairo_framebuffer_get_height (framebuffer);
if (self->framebuffer != NULL)
{
g_signal_handlers_disconnect_by_func (self->framebuffer,
G_CALLBACK (gdk_paintable_invalidate_size),
self);
g_signal_handlers_disconnect_by_func (self->framebuffer,
G_CALLBACK (gdk_paintable_invalidate_contents),
self);
g_clear_object (&self->framebuffer);
self->mode = MODE_INITIAL;
}
if (framebuffer != NULL)
{
self->framebuffer = g_object_ref (framebuffer);
g_signal_connect_object (self->framebuffer,
"invalidate-size",
G_CALLBACK (mks_paintable_invalidate_size_cb),
self,
G_CONNECT_SWAPPED);
g_signal_connect_object (self->framebuffer,
"invalidate-contents",
G_CALLBACK (mks_paintable_invalidate_contents_cb),
self,
G_CONNECT_SWAPPED);
self->mode = MODE_FRAMEBUFFER;
}
gdk_paintable_invalidate_contents (GDK_PAINTABLE (self));
if (size_changed)
gdk_paintable_invalidate_size (GDK_PAINTABLE (self));
}
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));
mks_qemu_listener_complete_update_dmabuf (listener, invocation);
return TRUE;
}
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_assert (MKS_IS_PAINTABLE (self));
g_assert (G_IS_DBUS_METHOD_INVOCATION (invocation));
g_assert (MKS_QEMU_IS_LISTENER (listener));
mks_qemu_listener_complete_scanout_dmabuf (listener, invocation, NULL);
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 (self->mode != MODE_FRAMEBUFFER ||
!(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 > mks_cairo_framebuffer_get_width (self->framebuffer) ||
y + height > mks_cairo_framebuffer_get_height (self->framebuffer))
{
guint max_width = MAX (mks_cairo_framebuffer_get_width (self->framebuffer), x + width);
guint max_height = MAX (mks_cairo_framebuffer_get_height (self->framebuffer), y + height);
g_autoptr(MksCairoFramebuffer) framebuffer = mks_cairo_framebuffer_new (format, max_width, max_height);
mks_cairo_framebuffer_copy_to (self->framebuffer, framebuffer);
mks_paintable_set_framebuffer (self, framebuffer);
}
source = cairo_image_surface_create_for_data ((guint8 *)data, format, width, height, stride);
cr = mks_cairo_framebuffer_update (self->framebuffer, 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->framebuffer == NULL ||
width != mks_cairo_framebuffer_get_width (self->framebuffer) ||
height != mks_cairo_framebuffer_get_height (self->framebuffer))
{
g_autoptr(MksCairoFramebuffer) framebuffer = mks_cairo_framebuffer_new (format, width, height);
mks_paintable_set_framebuffer (self, framebuffer);
}
source = cairo_image_surface_create_for_data ((guint8 *)data, format, width, height, stride);
cr = mks_cairo_framebuffer_update (self->framebuffer, 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 (self->mode == MODE_FRAMEBUFFER)
{
if (self->framebuffer != NULL)
mks_cairo_framebuffer_clear (self->framebuffer);
}
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));
}