/* * mks-screen.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 "mks-device-private.h" #include "mks-enums.h" #include "mks-qemu.h" #include "mks-keyboard-private.h" #include "mks-mouse-private.h" #include "mks-paintable-private.h" #include "mks-screen-attributes-private.h" #include "mks-screen-private.h" struct _MksScreenClass { MksDeviceClass parent_class; }; struct _MksScreen { MksDevice parent_instance; MksQemuConsole *console; gulong console_notify_handler; MksKeyboard *keyboard; MksMouse *mouse; guint number; guint width; guint height; MksScreenKind kind : 2; }; G_DEFINE_FINAL_TYPE (MksScreen, mks_screen, MKS_TYPE_DEVICE) enum { PROP_0, PROP_DEVICE_ADDRESS, PROP_HEIGHT, PROP_KIND, PROP_KEYBOARD, PROP_MOUSE, PROP_NUMBER, PROP_WIDTH, N_PROPS }; static GParamSpec *properties [N_PROPS]; static void _mks_screen_set_width (MksScreen *self, guint width) { g_assert (MKS_IS_SCREEN (self)); if (self->width != width) { self->width = width; g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_WIDTH]); } } static void _mks_screen_set_height (MksScreen *self, guint height) { g_assert (MKS_IS_SCREEN (self)); if (self->height != height) { self->height = height; g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_HEIGHT]); } } static void _mks_screen_set_number (MksScreen *self, guint number) { g_assert (MKS_IS_SCREEN (self)); if (self->number != number) { self->number = number; g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_NUMBER]); } } static void mks_screen_console_notify_cb (MksScreen *self, GParamSpec *pspec, MksQemuConsole *console) { g_assert (MKS_IS_SCREEN (self)); g_assert (pspec != NULL); g_assert (MKS_QEMU_IS_CONSOLE (console)); if (strcmp (pspec->name, "label") == 0) _mks_device_set_name (MKS_DEVICE (self), mks_qemu_console_get_label (console)); else if (strcmp (pspec->name, "width") == 0) _mks_screen_set_width (self, mks_qemu_console_get_width (console)); else if (strcmp (pspec->name, "height") == 0) _mks_screen_set_height (self, mks_qemu_console_get_height (console)); else if (strcmp (pspec->name, "number") == 0) _mks_screen_set_number (self, mks_qemu_console_get_head (console)); else if (strcmp (pspec->name, "type") == 0) { const char *type; if ((type = mks_qemu_console_get_type_ ((console)))) { MksScreenKind kind = MKS_SCREEN_KIND_TEXT; if (strcmp (type, "Graphic") == 0) kind = MKS_SCREEN_KIND_GRAPHIC; if (kind != self->kind) { self->kind = kind; g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_KIND]); } } } } static void mks_screen_set_console (MksScreen *self, MksQemuConsole *console) { g_assert (MKS_IS_SCREEN (self)); g_assert (!console || MKS_QEMU_IS_CONSOLE (console)); if (self->console != NULL) return; if (g_set_object (&self->console, console)) { const char *type; _mks_device_set_name (MKS_DEVICE (self), mks_qemu_console_get_label (console)); self->console_notify_handler = g_signal_connect_object (console, "notify", G_CALLBACK (mks_screen_console_notify_cb), self, G_CONNECT_SWAPPED); if ((type = mks_qemu_console_get_type_ ((console)))) { if (strcmp (type, "Graphic") == 0) self->kind = MKS_SCREEN_KIND_GRAPHIC; } self->width = mks_qemu_console_get_width (console); self->height = mks_qemu_console_get_height (console); self->number = mks_qemu_console_get_head (console); } } static gboolean mks_screen_setup (MksDevice *device, MksQemuObject *object) { MksScreen *self = (MksScreen *)device; g_autolist(GDBusInterface) interfaces = NULL; g_assert (MKS_IS_SCREEN (self)); g_assert (MKS_QEMU_IS_OBJECT (object)); interfaces = g_dbus_object_get_interfaces (G_DBUS_OBJECT (object)); for (const GList *iter = interfaces; iter; iter = iter->next) { GDBusInterface *iface = iter->data; if (MKS_QEMU_IS_CONSOLE (iface)) mks_screen_set_console (self, MKS_QEMU_CONSOLE (iface)); else if (MKS_QEMU_IS_KEYBOARD (iface)) self->keyboard = _mks_device_new (MKS_TYPE_KEYBOARD, device->session, object); else if (MKS_QEMU_IS_MOUSE (iface)) self->mouse = _mks_device_new (MKS_TYPE_MOUSE, device->session, object); } return self->console != NULL && self->keyboard != NULL && self->mouse != NULL; } static void mks_screen_dispose (GObject *object) { MksScreen *self = (MksScreen *)object; if (self->console != NULL) { g_clear_signal_handler (&self->console_notify_handler, self->console); g_clear_object (&self->console); } g_clear_object (&self->keyboard); g_clear_object (&self->mouse); G_OBJECT_CLASS (mks_screen_parent_class)->dispose (object); } static void mks_screen_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { MksScreen *self = MKS_SCREEN (object); switch (prop_id) { case PROP_DEVICE_ADDRESS: g_value_set_string (value, mks_screen_get_device_address (self)); break; case PROP_KEYBOARD: g_value_set_object (value, mks_screen_get_keyboard (self)); break; case PROP_KIND: g_value_set_enum (value, mks_screen_get_kind (self)); break; case PROP_MOUSE: g_value_set_object (value, mks_screen_get_mouse (self)); break; case PROP_NUMBER: g_value_set_uint (value, mks_screen_get_number (self)); break; case PROP_WIDTH: g_value_set_uint (value, mks_screen_get_width (self)); break; case PROP_HEIGHT: g_value_set_uint (value, mks_screen_get_height (self)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } } static void mks_screen_class_init (MksScreenClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); MksDeviceClass *device_class = MKS_DEVICE_CLASS (klass); object_class->dispose = mks_screen_dispose; object_class->get_property = mks_screen_get_property; device_class->setup = mks_screen_setup; properties [PROP_DEVICE_ADDRESS] = g_param_spec_string ("device-address", NULL, NULL, NULL, (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); properties [PROP_KEYBOARD] = g_param_spec_object ("keyboard", NULL, NULL, MKS_TYPE_KEYBOARD, (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); properties [PROP_KIND] = g_param_spec_enum ("kind", NULL, NULL, MKS_TYPE_SCREEN_KIND, MKS_SCREEN_KIND_TEXT, (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); properties [PROP_MOUSE] = g_param_spec_object ("mouse", NULL, NULL, MKS_TYPE_MOUSE, (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); properties [PROP_NUMBER] = g_param_spec_uint ("number", NULL, NULL, 0, G_MAXUINT, 0, (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); properties [PROP_WIDTH] = g_param_spec_uint ("width", NULL, NULL, 0, G_MAXUINT, 0, (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); properties [PROP_HEIGHT] = g_param_spec_uint ("height", NULL, NULL, 0, G_MAXUINT, 0, (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); g_object_class_install_properties (object_class, N_PROPS, properties); } static void mks_screen_init (MksScreen *self) { } /** * mks_screen_get_keyboard: * @self: a #MksScreen * * Gets the #MksScreen:keyboard property. * * Returns: (transfer none): a #MksKeyboard */ MksKeyboard * mks_screen_get_keyboard (MksScreen *self) { g_return_val_if_fail (MKS_IS_SCREEN (self), NULL); return self->keyboard; } /** * mks_screen_get_mouse: * @self: a #MksScreen * * Gets the #MksScreen:mouse property. * * Returns: (transfer none): a #MksMouse */ MksMouse * mks_screen_get_mouse (MksScreen *self) { g_return_val_if_fail (MKS_IS_SCREEN (self), NULL); return self->mouse; } /** * mks_screen_get_kind: * @self: a #MksScreen * * Gets the "kind" property. * * Returns: a #MksScreenKind */ MksScreenKind mks_screen_get_kind (MksScreen *self) { g_return_val_if_fail (MKS_IS_SCREEN (self), MKS_SCREEN_KIND_TEXT); return self->kind; } /** * mks_screen_get_width: * @self: a #MksScreen * * Gets the "width" property. * * Returns: The width of the screen in pixels. */ guint mks_screen_get_width (MksScreen *self) { g_return_val_if_fail (MKS_IS_SCREEN (self), 0); return self->width; } /** * mks_screen_get_height: * @self: a #MksScreen * * Gets the "height" property. * * Returns: The height of the screen in pixels. */ guint mks_screen_get_height (MksScreen *self) { g_return_val_if_fail (MKS_IS_SCREEN (self), 0); return self->height; } /** * mks_screen_get_number: * @self: a #MksScreen * * Gets the "number" property. * * Returns: the screen number */ guint mks_screen_get_number (MksScreen *self) { g_return_val_if_fail (MKS_IS_SCREEN (self), 0); return self->number; } const char * mks_screen_get_device_address (MksScreen *self) { g_return_val_if_fail (MKS_IS_SCREEN (self), NULL); if (self->console != NULL) return mks_qemu_console_get_device_address (self->console); return NULL; } static gboolean check_console (MksScreen *self, GError **error) { if (self->console == NULL) { g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_NOT_CONNECTED, "Not connected"); return FALSE; } return TRUE; } static void mks_screen_configure_cb (GObject *object, GAsyncResult *result, gpointer user_data) { MksQemuConsole *console = (MksQemuConsole *)object; g_autoptr(GError) error = NULL; g_autoptr(GTask) task = user_data; g_assert (MKS_QEMU_IS_CONSOLE (console)); g_assert (G_IS_ASYNC_RESULT (result)); g_assert (G_IS_TASK (task)); if (!mks_qemu_console_call_set_uiinfo_finish (console, result, &error)) g_task_return_error (task, g_steal_pointer (&error)); else g_task_return_boolean (task, TRUE); } /** * mks_screen_configure_async: * @self: an #MksScreen * @attributes: (transfer full): a #MksScreenAttributes * @cancellable: (nullable): a #GCancellable * @callback: a #GAsyncReadyCallback to execute upon completion * @user_data: closure data for @callback * * Requests the Qemu instance reconfigure the screen with @attributes. * * This function takes ownership of @attributes. * * @callback is executed upon acknowledgment from the Qemu instance or * if the request timed out. * * Call mks_screen_configure_finish() to get the result. */ void mks_screen_configure (MksScreen *self, MksScreenAttributes *attributes, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) { g_autoptr(GTask) task = NULL; g_autoptr(GError) error = NULL; g_return_if_fail (MKS_IS_SCREEN (self)); g_return_if_fail (attributes != NULL); g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); task = g_task_new (self, cancellable, callback, user_data); g_task_set_source_tag (task, mks_screen_configure); if (!check_console (self, &error)) g_task_return_error (task, g_steal_pointer (&error)); else mks_qemu_console_call_set_uiinfo (self->console, attributes->width_mm, attributes->height_mm, attributes->x_offset, attributes->y_offset, attributes->width, attributes->height, cancellable, mks_screen_configure_cb, g_steal_pointer (&task)); mks_screen_attributes_free (attributes); } /** * mks_screen_configure_finish: * @self: an #MksScreen * @result: a #GAsyncResult provided to callback * @error: a location for a #GError, or %NULL * * Completes a call to mks_screen_configure(). * * Returns: %TRUE if the operation completed successfully; otherwise %FALSE * and @error is set. */ gboolean mks_screen_configure_finish (MksScreen *self, GAsyncResult *result, GError **error) { g_return_val_if_fail (MKS_IS_SCREEN (self), FALSE); g_return_val_if_fail (g_task_is_valid (result, self), FALSE); return g_task_propagate_boolean (G_TASK (result), error); } /** * mks_screen_configure_sync: * @self: a #MksScreen * @attributes: (transfer full): a #MksScreenAttributes * @cancellable: a #GCancellable * @error: a location for a #GError, or %NULL * * Requests the Qemu instance reconfigure the screen using @attributes. * * This function takes ownership of @attributes. * * Returns: %TRUE if the operation completed successfully; otherwise %FALSE * and @error is set. */ gboolean mks_screen_configure_sync (MksScreen *self, MksScreenAttributes *attributes, GCancellable *cancellable, GError **error) { g_return_val_if_fail (MKS_IS_SCREEN (self), FALSE); g_return_val_if_fail (attributes != NULL, FALSE); g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), FALSE); if (!check_console (self, error)) return FALSE; return mks_qemu_console_call_set_uiinfo_sync (self->console, attributes->width_mm, attributes->height_mm, attributes->x_offset, attributes->y_offset, attributes->width, attributes->height, cancellable, error); } static void mks_screen_attach_cb (GObject *object, GAsyncResult *result, gpointer user_data) { MksQemuConsole *console = (MksQemuConsole *)object; g_autoptr(GTask) task = user_data; g_autoptr(GError) error = NULL; g_assert (G_IS_OBJECT (object)); g_assert (G_IS_ASYNC_RESULT (result)); g_assert (G_IS_TASK (task)); if (!mks_qemu_console_call_register_listener_finish (console, NULL, result, &error)) g_task_return_error (task, g_steal_pointer (&error)); else g_task_return_pointer (task, g_object_ref (g_task_get_task_data (task)), g_object_unref); } /** * mks_screen_attach: * @self: an #MksScreen * @cancellable: (nullable): a #GCancellable * @callback: a #GAsyncReadyCallback to execute upon completion * @user_data: closure data for @callback * * Asynchronously creates a #GdkPaintable that is updated with the * contents of the screen. * * This function registers a new `socketpair()` which is shared with * the Qemu instance to receive rendering updates. Those updates are * propagated to the resulting #GdkPainable which can be retrieved * using mks_screen_attach_finish() from @callback. */ void mks_screen_attach (MksScreen *self, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) { g_autoptr(GUnixFDList) unix_fd_list = NULL; g_autoptr(GdkPaintable) paintable = NULL; g_autoptr(GTask) task = NULL; g_autoptr(GError) error = NULL; g_autofd int fd = -1; g_return_if_fail (MKS_IS_SCREEN (self)); g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); task = g_task_new (self, cancellable, callback, user_data); g_task_set_source_tag (task, mks_screen_attach); if (!check_console (self, &error) || !(paintable = _mks_paintable_new (cancellable, &fd, &error))) goto failure; g_task_set_task_data (task, g_steal_pointer (&paintable), g_object_unref); unix_fd_list = g_unix_fd_list_new_from_array (&fd, 1), fd = -1; mks_qemu_console_call_register_listener (self->console, g_variant_new_handle (0), unix_fd_list, cancellable, mks_screen_attach_cb, g_steal_pointer (&task)); return; failure: g_task_return_error (task, g_steal_pointer (&error)); } /** * mks_screen_attach_finish: * @self: an #MksScreen * @result: a #GAsyncResult provided to callback * @error: a location for a #GError, or %NULL * * Completes an asynchronous request to create a #GdkPaintable containing * the contents of #MksScreen in the Qemu instance. * * The resulting #GdkPaintable will be updated as changes are delivered * from Qemu over a private `socketpair()`. In the typical case, those * changes are propagated using a DMA-BUF and damage notifications. * * Returns: (transfer full): a #GdkPainable if successful; otherwise %NULL * and @error is set. */ GdkPaintable * mks_screen_attach_finish (MksScreen *self, GAsyncResult *result, GError **error) { g_return_val_if_fail (MKS_IS_SCREEN (self), FALSE); g_return_val_if_fail (g_task_is_valid (result, self), FALSE); return g_task_propagate_pointer (G_TASK (result), error); } /** * mks_screen_attach_sync: * @self: a #MksScreen * @cancellable: (nullable): a #GCancellable or %NULL * @error: (nullable): a location for a #GError, or %NULL * * Synchronous request to attach to screen, creating a paintable that can * be used to update display as the Qemu instance updates. * * Returns: (transfer full): a #GdkPaintable if successful; otherwise %NULL * and @error is set. */ GdkPaintable * mks_screen_attach_sync (MksScreen *self, GCancellable *cancellable, GError **error) { g_autoptr(GUnixFDList) unix_fd_list = NULL; g_autoptr(GdkPaintable) paintable = NULL; g_autofd int fd = -1; g_return_val_if_fail (MKS_IS_SCREEN (self), NULL); g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), NULL); if (!check_console (self, error) || !(paintable = _mks_paintable_new (cancellable, &fd, error))) return NULL; unix_fd_list = g_unix_fd_list_new_from_array (&fd, 1), fd = -1; if (!mks_qemu_console_call_register_listener_sync (self->console, g_variant_new_handle (0), unix_fd_list, NULL, cancellable, error)) return NULL; return g_steal_pointer (&paintable); }