commit b54066842b4bc6fd39e515dcc6b1fc4caa35f79f Author: Morgan J. Date: Fri Feb 13 13:06:50 2026 +0900 . diff --git a/grd-avc-frame-info.c b/grd-avc-frame-info.c new file mode 100644 index 0000000..5f127cc --- /dev/null +++ b/grd-avc-frame-info.c @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2024 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-avc-frame-info.h" + +struct _GrdAVCFrameInfo +{ + GrdAVCFrameType frame_type; + uint8_t qp; + uint8_t quality; +}; + +gboolean +grd_avc_frame_info_get_frame_type (GrdAVCFrameInfo *avc_frame_info) +{ + return avc_frame_info->frame_type; +} + +uint8_t +grd_avc_frame_info_get_qp (GrdAVCFrameInfo *avc_frame_info) +{ + return avc_frame_info->qp; +} + +uint8_t +grd_avc_frame_info_get_quality_value (GrdAVCFrameInfo *avc_frame_info) +{ + return avc_frame_info->quality; +} + +GrdAVCFrameInfo * +grd_avc_frame_info_new (GrdAVCFrameType frame_type, + uint8_t qp, + uint8_t quality) +{ + GrdAVCFrameInfo *avc_frame_info; + + avc_frame_info = g_new0 (GrdAVCFrameInfo, 1); + avc_frame_info->frame_type = frame_type; + avc_frame_info->qp = qp; + avc_frame_info->quality = quality; + + return avc_frame_info; +} diff --git a/grd-avc-frame-info.h b/grd-avc-frame-info.h new file mode 100644 index 0000000..fa69b07 --- /dev/null +++ b/grd-avc-frame-info.h @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2024 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include +#include + +#include "grd-types.h" + +typedef enum +{ + GRD_AVC_FRAME_TYPE_P, + GRD_AVC_FRAME_TYPE_I, +} GrdAVCFrameType; + +GrdAVCFrameInfo *grd_avc_frame_info_new (GrdAVCFrameType frame_type, + uint8_t qp, + uint8_t quality); + +gboolean grd_avc_frame_info_get_frame_type (GrdAVCFrameInfo *avc_frame_info); + +uint8_t grd_avc_frame_info_get_qp (GrdAVCFrameInfo *avc_frame_info); + +uint8_t grd_avc_frame_info_get_quality_value (GrdAVCFrameInfo *avc_frame_info); diff --git a/grd-bitstream.c b/grd-bitstream.c new file mode 100644 index 0000000..8fd7675 --- /dev/null +++ b/grd-bitstream.c @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2024 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-bitstream.h" + +#include + +struct _GrdBitstream +{ + uint8_t *data; + uint32_t data_size; + + GrdAVCFrameInfo *avc_frame_info; +}; + +uint8_t * +grd_bitstream_get_data (GrdBitstream *bitstream) +{ + return bitstream->data; +} + +uint32_t +grd_bitstream_get_data_size (GrdBitstream *bitstream) +{ + return bitstream->data_size; +} + +GrdAVCFrameInfo * +grd_bitstream_get_avc_frame_info (GrdBitstream *bitstream) +{ + return bitstream->avc_frame_info; +} + +void +grd_bitstream_set_avc_frame_info (GrdBitstream *bitstream, + GrdAVCFrameInfo *avc_frame_info) +{ + g_assert (!bitstream->avc_frame_info); + + bitstream->avc_frame_info = avc_frame_info; +} + +GrdBitstream * +grd_bitstream_new (uint8_t *data, + uint32_t data_size) +{ + GrdBitstream *bitstream; + + bitstream = g_new0 (GrdBitstream, 1); + bitstream->data = data; + bitstream->data_size = data_size; + + return bitstream; +} + +void +grd_bitstream_free (GrdBitstream *bitstream) +{ + g_clear_pointer (&bitstream->avc_frame_info, g_free); + g_free (bitstream); +} diff --git a/grd-bitstream.h b/grd-bitstream.h new file mode 100644 index 0000000..6d480b5 --- /dev/null +++ b/grd-bitstream.h @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2024 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include + +#include "grd-types.h" + +GrdBitstream *grd_bitstream_new (uint8_t *data, + uint32_t data_size); + +void grd_bitstream_free (GrdBitstream *bitstream); + +uint8_t *grd_bitstream_get_data (GrdBitstream *bitstream); + +uint32_t grd_bitstream_get_data_size (GrdBitstream *bitstream); + +GrdAVCFrameInfo *grd_bitstream_get_avc_frame_info (GrdBitstream *bitstream); + +void grd_bitstream_set_avc_frame_info (GrdBitstream *bitstream, + GrdAVCFrameInfo *avc_frame_info); diff --git a/grd-clipboard-rdp.c b/grd-clipboard-rdp.c new file mode 100644 index 0000000..6e1718a --- /dev/null +++ b/grd-clipboard-rdp.c @@ -0,0 +1,2674 @@ +/* + * Copyright (C) 2020-2021 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-clipboard-rdp.h" + +#include +#include + +#include "grd-rdp-fuse-clipboard.h" +#include "grd-session-rdp.h" + +#define CLIPRDR_FILEDESCRIPTOR_SIZE (4 + 32 + 4 + 16 + 8 + 4 + 4 + 520) +#define MAX_WAIT_TIME_MS 4000 + +/* Format strings of formats with dynamically assigned ids */ +#define GRD_CFSTR_MIME_HTML "HTML Format" +#define GRD_CFSTR_MIME_TEXT_HTML "text/html" +#define GRD_CFSTR_MIME_PNG "image/png" +#define GRD_CFSTR_MIME_JPEG "image/jpeg" +#define GRD_CFSTR_MIME_GIF "image/gif" + +#define GRD_CF_HTML 0xD010 +#define GRD_CF_PNG 0xD011 +#define GRD_CF_JPEG 0xD012 +#define GRD_CF_GIF 0xD013 +#define GRD_CF_TEXT_URILIST 0xD014 + +typedef struct _ServerFormatListUpdateContext +{ + GrdClipboardRdp *clipboard_rdp; + GList *mime_type_tables; +} ServerFormatListUpdateContext; + +typedef struct _ServerFormatDataRequestContext +{ + GrdClipboardRdp *clipboard_rdp; + GrdMimeType mime_type; + uint32_t src_format_id; + uint32_t dst_format_id; + gboolean needs_null_terminator; + gboolean needs_conversion; +} ServerFormatDataRequestContext; + +typedef struct _ClientFormatDataRequestContext +{ + GrdMimeTypeTable mime_type_table; + + gboolean has_clip_data_id; + uint32_t clip_data_id; +} ClientFormatDataRequestContext; + +typedef struct _ClipDataEntry +{ + uint32_t id; + + wClipboard *system; + wClipboardDelegate *delegate; + uint64_t serial; + + gboolean is_independent; + gboolean has_file_list; + gboolean requests_allowed; +} ClipDataEntry; + +typedef struct _FormatData +{ + uint8_t *data; + uint32_t size; +} FormatData; + +struct _GrdClipboardRdp +{ + GrdClipboard parent; + + CliprdrServerContext *cliprdr_context; + gboolean protocol_stopped; + + gboolean relieve_filename_restriction; + + wClipboard *system; + wClipboardDelegate *delegate; + gboolean has_file_list; + uint64_t serial; + + GHashTable *allowed_server_formats; + GList *pending_server_formats; + GList *queued_server_formats; + gboolean server_file_contents_requests_allowed; + uint16_t format_list_response_msg_flags; + + GHashTable *serial_entry_table; + GHashTable *clip_data_table; + struct + { + ClipDataEntry *entry; + ClipDataEntry *entry_to_replace; + gboolean serial_already_in_use; + } clipboard_retrieval_context; + struct + { + ClipDataEntry *entry; + } clipboard_destruction_context; + + char *fuse_mount_path; + GrdRdpFuseClipboard *rdp_fuse_clipboard; + GHashTable *format_data_cache; + + GrdMimeType which_unicode_format; + ServerFormatDataRequestContext *format_data_request_context; + + GHashTable *pending_client_requests; + GQueue *ordered_client_requests; + + GMutex client_request_mutex; + ClientFormatDataRequestContext *current_client_request; + CLIPRDR_FORMAT_DATA_RESPONSE *format_data_response; + unsigned int client_format_data_response_id; + + unsigned int pending_server_formats_drop_id; + unsigned int client_request_abort_id; + + GCond completion_cond; + GMutex completion_mutex; + + GMutex clip_data_entry_mutex; + unsigned int clipboard_retrieval_id; + unsigned int clipboard_destruction_id; + gboolean completed_clip_data_entry; + + GMutex server_format_list_update_mutex; + unsigned int server_format_list_update_id; + gboolean completed_format_list; + + GMutex client_format_list_response_mutex; + unsigned int client_format_list_response_id; + + GMutex server_format_data_request_mutex; + unsigned int server_format_data_request_id; + gboolean completed_format_data_request; +}; + +G_DEFINE_TYPE (GrdClipboardRdp, grd_clipboard_rdp, GRD_TYPE_CLIPBOARD) + +static gboolean +send_mime_type_content_request (GrdClipboardRdp *clipboard_rdp, + GrdMimeTypeTable *mime_type_table); + +static void +create_new_winpr_clipboard (GrdClipboardRdp *clipboard_rdp); + +static void +update_allowed_server_formats (GrdClipboardRdp *clipboard_rdp, + gboolean received_response) +{ + GList *l; + + if (received_response) + g_debug ("[RDP.CLIPRDR] Handling format list response from client"); + + if (received_response && + clipboard_rdp->format_list_response_msg_flags & CB_RESPONSE_OK) + { + for (l = clipboard_rdp->pending_server_formats; l; l = l->next) + { + if (GPOINTER_TO_UINT (l->data) == GRD_MIME_TYPE_TEXT_URILIST) + { + ClipDataEntry *entry; + + if (g_hash_table_lookup_extended (clipboard_rdp->serial_entry_table, + GUINT_TO_POINTER (clipboard_rdp->serial), + NULL, (gpointer *) &entry)) + entry->requests_allowed = TRUE; + + clipboard_rdp->server_file_contents_requests_allowed = TRUE; + } + + g_hash_table_add (clipboard_rdp->allowed_server_formats, l->data); + } + + g_clear_pointer (&clipboard_rdp->pending_server_formats, g_list_free); + + return; + } + + if (received_response && + !(clipboard_rdp->format_list_response_msg_flags & CB_RESPONSE_FAIL)) + { + g_warning ("[RDP.CLIPRDR] Protocol violation: Client did not set response " + "flag. Assuming CB_RESPONSE_FAIL"); + } + + clipboard_rdp->server_file_contents_requests_allowed = FALSE; + g_hash_table_remove_all (clipboard_rdp->allowed_server_formats); + g_clear_pointer (&clipboard_rdp->pending_server_formats, g_list_free); +} + +static void +remove_clipboard_format_data_for_mime_type (GrdClipboardRdp *clipboard_rdp, + GrdMimeType mime_type) +{ + FormatData *format_data; + + format_data = g_hash_table_lookup (clipboard_rdp->format_data_cache, + GUINT_TO_POINTER (mime_type)); + if (!format_data) + return; + + g_free (format_data->data); + g_free (format_data); + + g_hash_table_remove (clipboard_rdp->format_data_cache, + GUINT_TO_POINTER (mime_type)); +} + +static void +remove_duplicated_clipboard_mime_types (GrdClipboardRdp *clipboard_rdp, + GList **mime_type_list) +{ + GrdMimeType mime_type; + + /** + * Remove the "x-special/gnome-copied-files" mimetype, since we use the + * "text/uri-list" mimetype instead. + */ + mime_type = GRD_MIME_TYPE_XS_GNOME_COPIED_FILES; + if (g_list_find (*mime_type_list, GUINT_TO_POINTER (mime_type))) + { + *mime_type_list = g_list_remove (*mime_type_list, GUINT_TO_POINTER (mime_type)); + remove_clipboard_format_data_for_mime_type (clipboard_rdp, mime_type); + } + + /** + * We can only advertise CF_UNICODETEXT as remote format once, so ignore the + * other local format if it exists. + */ + if (g_list_find (*mime_type_list, GUINT_TO_POINTER (GRD_MIME_TYPE_TEXT_PLAIN_UTF8)) && + g_list_find (*mime_type_list, GUINT_TO_POINTER (GRD_MIME_TYPE_TEXT_UTF8_STRING))) + { + mime_type = GRD_MIME_TYPE_TEXT_PLAIN_UTF8; + *mime_type_list = g_list_remove (*mime_type_list, GUINT_TO_POINTER (mime_type)); + remove_clipboard_format_data_for_mime_type (clipboard_rdp, mime_type); + } +} + +static void +update_clipboard_serial (GrdClipboardRdp *clipboard_rdp) +{ + ++clipboard_rdp->serial; + while (g_hash_table_contains (clipboard_rdp->serial_entry_table, + GUINT_TO_POINTER (clipboard_rdp->serial))) + ++clipboard_rdp->serial; + + g_debug ("[RDP.CLIPRDR] Updated clipboard serial to %lu", clipboard_rdp->serial); +} + +static void +send_mime_type_list (GrdClipboardRdp *clipboard_rdp, + GList *mime_type_list) +{ + GrdRdpFuseClipboard *rdp_fuse_clipboard = clipboard_rdp->rdp_fuse_clipboard; + CliprdrServerContext *cliprdr_context = clipboard_rdp->cliprdr_context; + CLIPRDR_FORMAT_LIST format_list = {0}; + CLIPRDR_FORMAT *cliprdr_formats; + ClipDataEntry *entry; + GrdMimeType mime_type; + uint32_t n_formats; + uint32_t i; + GList *l; + + if (g_hash_table_lookup_extended (clipboard_rdp->serial_entry_table, + GUINT_TO_POINTER (clipboard_rdp->serial), + NULL, (gpointer *) &entry)) + { + g_debug ("[RDP.CLIPRDR] ClipDataEntry with id %u and serial %lu is now " + "independent", entry->id, entry->serial); + entry->is_independent = TRUE; + + create_new_winpr_clipboard (clipboard_rdp); + } + update_clipboard_serial (clipboard_rdp); + + n_formats = g_list_length (mime_type_list); + cliprdr_formats = g_malloc0 (n_formats * sizeof (CLIPRDR_FORMAT)); + for (i = 0, l = mime_type_list; i < n_formats; ++i, l = l->next) + { + mime_type = GPOINTER_TO_UINT (l->data); + switch (mime_type) + { + case GRD_MIME_TYPE_TEXT_PLAIN: + cliprdr_formats[i].formatId = CF_TEXT; + break; + case GRD_MIME_TYPE_TEXT_PLAIN_UTF8: + case GRD_MIME_TYPE_TEXT_UTF8_STRING: + cliprdr_formats[i].formatId = CF_UNICODETEXT; + clipboard_rdp->which_unicode_format = mime_type; + break; + case GRD_MIME_TYPE_TEXT_HTML: + cliprdr_formats[i].formatId = GRD_CF_HTML; + cliprdr_formats[i].formatName = GRD_CFSTR_MIME_HTML; + break; + case GRD_MIME_TYPE_IMAGE_BMP: + cliprdr_formats[i].formatId = CF_DIB; + break; + case GRD_MIME_TYPE_IMAGE_TIFF: + cliprdr_formats[i].formatId = CF_TIFF; + break; + case GRD_MIME_TYPE_IMAGE_GIF: + cliprdr_formats[i].formatId = GRD_CF_GIF; + cliprdr_formats[i].formatName = GRD_CFSTR_MIME_GIF; + break; + case GRD_MIME_TYPE_IMAGE_JPEG: + cliprdr_formats[i].formatId = GRD_CF_JPEG; + cliprdr_formats[i].formatName = GRD_CFSTR_MIME_JPEG; + break; + case GRD_MIME_TYPE_IMAGE_PNG: + cliprdr_formats[i].formatId = GRD_CF_PNG; + cliprdr_formats[i].formatName = GRD_CFSTR_MIME_PNG; + break; + case GRD_MIME_TYPE_TEXT_URILIST: + /** + * Most formats don't have a consistent format id, but a dynamically + * assigned one. These formats are identified by their name. + * + * When the client requests the content of file lists, it MUST use the + * id that we told the client before when the clipboard format with + * the name "FileGroupDescriptorW" was advertised. + * + * See also 1.3.1.2 Clipboard Format + */ + cliprdr_formats[i].formatId = GRD_CF_TEXT_URILIST; + cliprdr_formats[i].formatName = "FileGroupDescriptorW"; + clipboard_rdp->server_file_contents_requests_allowed = FALSE; + grd_rdp_fuse_clipboard_clear_no_cdi_selection (rdp_fuse_clipboard); + grd_rdp_fuse_clipboard_lazily_clear_all_cdi_selections (rdp_fuse_clipboard); + break; + default: + g_assert_not_reached (); + } + + remove_clipboard_format_data_for_mime_type (clipboard_rdp, mime_type); + g_hash_table_remove (clipboard_rdp->allowed_server_formats, l->data); + + clipboard_rdp->pending_server_formats = + g_list_append (clipboard_rdp->pending_server_formats, l->data); + } + + format_list.common.msgType = CB_FORMAT_LIST; + format_list.formats = cliprdr_formats; + format_list.numFormats = n_formats; + + g_debug ("[RDP.CLIPRDR] Sending FormatList"); + cliprdr_context->ServerFormatList (cliprdr_context, &format_list); + + g_free (cliprdr_formats); + g_list_free (mime_type_list); +} + +static gboolean +drop_pending_server_formats (gpointer user_data) +{ + GrdClipboardRdp *clipboard_rdp = user_data; + GList *queued_server_formats; + + g_warning ("[RDP.CLIPRDR] Possible protocol violation: Client did not send " + "format list response (Timeout reached)"); + update_allowed_server_formats (clipboard_rdp, FALSE); + + queued_server_formats = g_steal_pointer (&clipboard_rdp->queued_server_formats); + if (queued_server_formats) + send_mime_type_list (clipboard_rdp, queued_server_formats); + + clipboard_rdp->pending_server_formats_drop_id = 0; + + return G_SOURCE_REMOVE; +} + +static void +grd_clipboard_rdp_update_client_mime_type_list (GrdClipboard *clipboard, + GList *mime_type_list) +{ + GrdClipboardRdp *clipboard_rdp = GRD_CLIPBOARD_RDP (clipboard); + + remove_duplicated_clipboard_mime_types (clipboard_rdp, &mime_type_list); + if (!mime_type_list) + return; + + if (clipboard_rdp->pending_server_formats) + { + if (clipboard_rdp->queued_server_formats) + g_debug ("[RDP.CLIPRDR] Replacing queued server FormatList"); + g_clear_pointer (&clipboard_rdp->queued_server_formats, g_list_free); + + g_debug ("[RDP.CLIPRDR] Queueing new FormatList"); + clipboard_rdp->queued_server_formats = mime_type_list; + + if (!clipboard_rdp->pending_server_formats_drop_id) + { + clipboard_rdp->pending_server_formats_drop_id = + g_timeout_add (MAX_WAIT_TIME_MS, drop_pending_server_formats, clipboard_rdp); + } + + return; + } + + send_mime_type_list (clipboard_rdp, mime_type_list); +} + +void +grd_clipboard_rdp_lock_remote_clipboard_data (GrdClipboardRdp *clipboard_rdp, + uint32_t clip_data_id) +{ + CliprdrServerContext *cliprdr_context = clipboard_rdp->cliprdr_context; + CLIPRDR_LOCK_CLIPBOARD_DATA lock_clipboard_data = {0}; + + g_debug ("[RDP.CLIPRDR] Locking clients clipboard data with clipDataId %u", + clip_data_id); + + lock_clipboard_data.common.msgType = CB_LOCK_CLIPDATA; + lock_clipboard_data.clipDataId = clip_data_id; + + cliprdr_context->ServerLockClipboardData (cliprdr_context, + &lock_clipboard_data); +} + +void +grd_clipboard_rdp_unlock_remote_clipboard_data (GrdClipboardRdp *clipboard_rdp, + uint32_t clip_data_id) +{ + CliprdrServerContext *cliprdr_context = clipboard_rdp->cliprdr_context; + CLIPRDR_UNLOCK_CLIPBOARD_DATA unlock_clipboard_data = {0}; + + g_debug ("[RDP.CLIPRDR] Unlocking clients clipboard data associated to " + "clipDataId %u", clip_data_id); + + unlock_clipboard_data.common.msgType = CB_UNLOCK_CLIPDATA; + unlock_clipboard_data.clipDataId = clip_data_id; + + cliprdr_context->ServerUnlockClipboardData (cliprdr_context, + &unlock_clipboard_data); +} + +void +grd_clipboard_rdp_request_remote_file_size_async (GrdClipboardRdp *clipboard_rdp, + uint32_t stream_id, + uint32_t list_index, + gboolean has_clip_data_id, + uint32_t clip_data_id) +{ + CliprdrServerContext *cliprdr_context = clipboard_rdp->cliprdr_context; + CLIPRDR_FILE_CONTENTS_REQUEST file_contents_request = {0}; + + file_contents_request.common.msgType = CB_FILECONTENTS_REQUEST; + file_contents_request.streamId = stream_id; + file_contents_request.listIndex = list_index; + file_contents_request.dwFlags = FILECONTENTS_SIZE; + file_contents_request.cbRequested = 0x8; + file_contents_request.haveClipDataId = has_clip_data_id; + file_contents_request.clipDataId = clip_data_id; + + cliprdr_context->ServerFileContentsRequest (cliprdr_context, + &file_contents_request); +} + +void +grd_clipboard_rdp_request_remote_file_range_async (GrdClipboardRdp *clipboard_rdp, + uint32_t stream_id, + uint32_t list_index, + uint64_t offset, + uint32_t requested_size, + gboolean has_clip_data_id, + uint32_t clip_data_id) +{ + CliprdrServerContext *cliprdr_context = clipboard_rdp->cliprdr_context; + CLIPRDR_FILE_CONTENTS_REQUEST file_contents_request = {0}; + + file_contents_request.common.msgType = CB_FILECONTENTS_REQUEST; + file_contents_request.streamId = stream_id; + file_contents_request.listIndex = list_index; + file_contents_request.dwFlags = FILECONTENTS_RANGE; + file_contents_request.nPositionLow = offset & 0xFFFFFFFF; + file_contents_request.nPositionHigh = offset >> 32 & 0xFFFFFFFF; + file_contents_request.cbRequested = requested_size; + file_contents_request.haveClipDataId = has_clip_data_id; + file_contents_request.clipDataId = clip_data_id; + + cliprdr_context->ServerFileContentsRequest (cliprdr_context, + &file_contents_request); +} + +static void +track_serial_for_mime_type (GrdClipboardRdp *clipboard_rdp, + GrdMimeType mime_type, + unsigned int serial) +{ + GList *serials; + + serials = g_hash_table_lookup (clipboard_rdp->pending_client_requests, + GUINT_TO_POINTER (mime_type)); + serials = g_list_append (serials, GUINT_TO_POINTER (serial)); + + g_hash_table_insert (clipboard_rdp->pending_client_requests, + GUINT_TO_POINTER (mime_type), serials); +} + +static void +enqueue_mime_type_content_request (GrdClipboardRdp *clipboard_rdp, + GrdMimeTypeTable *mime_type_table, + unsigned int serial) +{ + GrdMimeType mime_type = mime_type_table->mime_type; + GrdMimeTypeTable *mime_type_table_copy; + + g_debug ("[RDP.CLIPRDR] Queueing mime type content request for mime type %s " + "with serial %u", grd_mime_type_to_string (mime_type), serial); + + track_serial_for_mime_type (clipboard_rdp, mime_type, serial); + + mime_type_table_copy = g_malloc0 (sizeof (GrdMimeTypeTable)); + *mime_type_table_copy = *mime_type_table; + g_queue_push_tail (clipboard_rdp->ordered_client_requests, + GUINT_TO_POINTER (mime_type_table_copy)); +} + +static void +abort_client_requests_for_serials (GrdClipboardRdp *clipboard_rdp, + GList *serials) +{ + GrdClipboard *clipboard = GRD_CLIPBOARD (clipboard_rdp); + unsigned int serial; + GList *l; + + for (l = serials; l; l = l->next) + { + serial = GPOINTER_TO_UINT (l->data); + grd_clipboard_submit_client_content_for_mime_type (clipboard, serial, + NULL, 0); + } + g_list_free (serials); +} + +static void +abort_client_requests_for_mime_type (GrdClipboardRdp *clipboard_rdp, + GrdMimeType mime_type) +{ + GList *serials; + + if (!g_hash_table_steal_extended (clipboard_rdp->pending_client_requests, + GUINT_TO_POINTER (mime_type), + NULL, (gpointer *) &serials)) + return; + + abort_client_requests_for_serials (clipboard_rdp, serials); +} + +static void +abort_client_requests_for_context (GrdClipboardRdp *clipboard_rdp, + ClientFormatDataRequestContext *request_context) +{ + GrdRdpFuseClipboard *rdp_fuse_clipboard = clipboard_rdp->rdp_fuse_clipboard; + GrdMimeTypeTable *mime_type_table = &request_context->mime_type_table; + GrdMimeType mime_type = mime_type_table->mime_type; + uint32_t clip_data_id = request_context->clip_data_id; + + g_debug ("[RDP.CLIPRDR] Aborting FormatDataRequest for mime type %s", + grd_mime_type_to_string (mime_type)); + + if (request_context->has_clip_data_id) + grd_rdp_fuse_clipboard_clip_data_id_free (rdp_fuse_clipboard, clip_data_id); + + abort_client_requests_for_mime_type (clipboard_rdp, mime_type); +} + +static void +maybe_send_next_mime_type_content_request (GrdClipboardRdp *clipboard_rdp) +{ + GrdMimeTypeTable *mime_type_table; + GrdMimeType mime_type; + GList *serials = NULL; + + mime_type_table = g_queue_pop_head (clipboard_rdp->ordered_client_requests); + if (!mime_type_table) + return; + + mime_type = mime_type_table->mime_type; + serials = g_hash_table_lookup (clipboard_rdp->pending_client_requests, + GUINT_TO_POINTER (mime_type)); + if (!serials) + { + g_free (mime_type_table); + maybe_send_next_mime_type_content_request (clipboard_rdp); + return; + } + + if (!send_mime_type_content_request (clipboard_rdp, mime_type_table)) + { + abort_client_requests_for_mime_type (clipboard_rdp, mime_type); + g_free (mime_type_table); + + maybe_send_next_mime_type_content_request (clipboard_rdp); + return; + } + + g_free (mime_type_table); +} + +static gboolean +abort_current_client_request (gpointer user_data) +{ + GrdClipboardRdp *clipboard_rdp = user_data; + ClientFormatDataRequestContext *request_context; + + g_mutex_lock (&clipboard_rdp->client_request_mutex); + request_context = g_steal_pointer (&clipboard_rdp->current_client_request); + + g_clear_handle_id (&clipboard_rdp->client_format_data_response_id, + g_source_remove); + g_mutex_unlock (&clipboard_rdp->client_request_mutex); + + g_warning ("[RDP.CLIPRDR] Possible protocol violation: Client did not send " + "format data response (Timeout reached)"); + abort_client_requests_for_context (clipboard_rdp, request_context); + g_free (request_context); + + clipboard_rdp->client_request_abort_id = 0; + maybe_send_next_mime_type_content_request (clipboard_rdp); + + return G_SOURCE_REMOVE; +} + +static gboolean +send_mime_type_content_request (GrdClipboardRdp *clipboard_rdp, + GrdMimeTypeTable *mime_type_table) +{ + GrdRdpFuseClipboard *rdp_fuse_clipboard = clipboard_rdp->rdp_fuse_clipboard; + CliprdrServerContext *cliprdr_context = clipboard_rdp->cliprdr_context; + CLIPRDR_FORMAT_DATA_REQUEST format_data_request = {0}; + GrdMimeType mime_type = mime_type_table->mime_type; + ClientFormatDataRequestContext *request_context; + uint32_t clip_data_id = 0; + + g_assert (mime_type != GRD_MIME_TYPE_NONE); + g_assert (!clipboard_rdp->current_client_request); + g_assert (!clipboard_rdp->client_format_data_response_id); + g_assert (!clipboard_rdp->client_request_abort_id); + + request_context = g_malloc0 (sizeof (ClientFormatDataRequestContext)); + request_context->mime_type_table = *mime_type_table; + + if (clipboard_rdp->cliprdr_context->canLockClipData && + (mime_type == GRD_MIME_TYPE_TEXT_URILIST || + mime_type == GRD_MIME_TYPE_XS_GNOME_COPIED_FILES)) + { + clip_data_id = grd_rdp_fuse_clipboard_clip_data_id_new (rdp_fuse_clipboard); + request_context->clip_data_id = clip_data_id; + request_context->has_clip_data_id = TRUE; + } + + format_data_request.common.msgType = CB_FORMAT_DATA_REQUEST; + format_data_request.common.dataLen = 4; + format_data_request.requestedFormatId = mime_type_table->rdp.format_id; + + g_mutex_lock (&clipboard_rdp->client_request_mutex); + if (cliprdr_context->ServerFormatDataRequest (cliprdr_context, + &format_data_request)) + { + g_mutex_unlock (&clipboard_rdp->client_request_mutex); + + if (request_context->has_clip_data_id) + grd_rdp_fuse_clipboard_clip_data_id_free (rdp_fuse_clipboard, clip_data_id); + + g_free (request_context); + + return FALSE; + } + g_debug ("[RDP.CLIPRDR] Sent FormatDataRequest for mime type %s", + grd_mime_type_to_string (mime_type)); + + clipboard_rdp->current_client_request = request_context; + g_mutex_unlock (&clipboard_rdp->client_request_mutex); + + clipboard_rdp->client_request_abort_id = + g_timeout_add (MAX_WAIT_TIME_MS, abort_current_client_request, clipboard_rdp); + + return TRUE; +} + +static void +grd_clipboard_rdp_request_client_content_for_mime_type (GrdClipboard *clipboard, + GrdMimeTypeTable *mime_type_table, + unsigned int serial) +{ + GrdClipboardRdp *clipboard_rdp = GRD_CLIPBOARD_RDP (clipboard); + GrdMimeType mime_type = mime_type_table->mime_type; + gboolean pending_format_list_update; + FormatData *format_data; + + if (mime_type == GRD_MIME_TYPE_NONE) + { + grd_clipboard_submit_client_content_for_mime_type (clipboard, serial, + NULL, 0); + return; + } + + if (g_hash_table_lookup_extended (clipboard_rdp->format_data_cache, + GUINT_TO_POINTER (mime_type), + NULL, (gpointer *) &format_data)) + { + grd_clipboard_submit_client_content_for_mime_type (clipboard, serial, + format_data->data, + format_data->size); + return; + } + + g_mutex_lock (&clipboard_rdp->server_format_list_update_mutex); + pending_format_list_update = clipboard_rdp->server_format_list_update_id != 0; + g_mutex_unlock (&clipboard_rdp->server_format_list_update_mutex); + + if (pending_format_list_update) + { + grd_clipboard_submit_client_content_for_mime_type (clipboard, serial, + NULL, 0); + return; + } + + if (clipboard_rdp->current_client_request) + { + enqueue_mime_type_content_request (clipboard_rdp, mime_type_table, serial); + return; + } + + g_assert (g_queue_is_empty (clipboard_rdp->ordered_client_requests)); + if (!send_mime_type_content_request (clipboard_rdp, mime_type_table)) + { + grd_clipboard_submit_client_content_for_mime_type (clipboard, serial, + NULL, 0); + return; + } + + track_serial_for_mime_type (clipboard_rdp, mime_type, serial); +} + +static void +serialize_file_list (FILEDESCRIPTORW *files, + uint32_t n_files, + uint8_t **dst_data, + uint32_t *dst_size) +{ + FILEDESCRIPTORW *file; + wStream* s = NULL; + uint64_t last_write_time; + uint32_t i, j; + + if (!files || !dst_data || !dst_size) + return; + + if (!(s = Stream_New (NULL, 4 + n_files * CLIPRDR_FILEDESCRIPTOR_SIZE))) + return; + + Stream_Write_UINT32 (s, n_files); /* cItems */ + for (i = 0; i < n_files; ++i) + { + file = &files[i]; + + Stream_Write_UINT32 (s, file->dwFlags); /* flags */ + Stream_Zero (s, 32); /* reserved1 */ + Stream_Write_UINT32 (s, file->dwFileAttributes); /* fileAttributes */ + Stream_Zero (s, 16); /* reserved2 */ + + last_write_time = file->ftLastWriteTime.dwHighDateTime; + last_write_time <<= 32; + last_write_time += file->ftLastWriteTime.dwLowDateTime; + + Stream_Write_UINT64 (s, last_write_time); /* lastWriteTime */ + Stream_Write_UINT32 (s, file->nFileSizeHigh); /* fileSizeHigh */ + Stream_Write_UINT32 (s, file->nFileSizeLow); /* fileSizeLow */ + + for (j = 0; j < 260; j++) /* cFileName */ + Stream_Write_UINT16 (s, file->cFileName[j]); + } + + Stream_SealLength (s); + Stream_GetLength (s, *dst_size); + Stream_GetBuffer (s, *dst_data); + + Stream_Free (s, FALSE); +} + +static void +grd_clipboard_rdp_submit_requested_server_content (GrdClipboard *clipboard, + uint8_t *src_data, + uint32_t src_size) +{ + GrdClipboardRdp *clipboard_rdp = GRD_CLIPBOARD_RDP (clipboard); + CliprdrServerContext *cliprdr_context = clipboard_rdp->cliprdr_context; + ServerFormatDataRequestContext *request_context; + CLIPRDR_FORMAT_DATA_RESPONSE format_data_response = {0}; + GrdMimeType mime_type; + uint32_t src_format_id; + uint32_t dst_format_id; + uint8_t *dst_data = NULL; + uint32_t dst_size = 0; + BOOL success; + + request_context = g_steal_pointer (&clipboard_rdp->format_data_request_context); + mime_type = request_context->mime_type; + src_format_id = request_context->src_format_id; + dst_format_id = request_context->dst_format_id; + + if (src_data) + { + if (request_context->needs_conversion) + { + if (request_context->needs_null_terminator) + { + char *pnull_terminator; + + src_data = g_realloc (src_data, src_size + 1); + pnull_terminator = (char *) src_data + src_size; + *pnull_terminator = '\0'; + ++src_size; + } + + success = ClipboardSetData (clipboard_rdp->system, + src_format_id, src_data, src_size); + if (!success) + { + g_warning ("[RDP.CLIPRDR] Converting clipboard content failed: " + "Failed to set data to convert"); + } + else + { + dst_data = ClipboardGetData (clipboard_rdp->system, + dst_format_id, &dst_size); + + if (dst_data && mime_type == GRD_MIME_TYPE_TEXT_URILIST) + { + uint64_t serial = clipboard_rdp->serial; + ClipDataEntry *entry; + FILEDESCRIPTORW *files; + uint32_t n_files; + + files = (FILEDESCRIPTORW *) dst_data; + n_files = dst_size / sizeof (FILEDESCRIPTORW); + + dst_data = NULL; + dst_size = 0; + serialize_file_list (files, n_files, &dst_data, &dst_size); + + g_free (files); + + clipboard_rdp->has_file_list = TRUE; + if (g_hash_table_lookup_extended (clipboard_rdp->serial_entry_table, + GUINT_TO_POINTER (serial), + NULL, (gpointer *) &entry)) + entry->has_file_list = TRUE; + } + } + if (!dst_data) + { + g_warning ("[RDP.CLIPRDR] Converting clipboard content for " + "client failed: Failed to get converted data"); + } + } + else + { + dst_data = g_steal_pointer (&src_data); + dst_size = src_size; + } + } + + format_data_response.common.msgType = CB_FORMAT_DATA_RESPONSE; + format_data_response.common.msgFlags = dst_data ? CB_RESPONSE_OK + : CB_RESPONSE_FAIL; + format_data_response.common.dataLen = dst_size; + format_data_response.requestedFormatData = dst_data; + + cliprdr_context->ServerFormatDataResponse (cliprdr_context, + &format_data_response); + + g_free (src_data); + g_free (dst_data); + g_free (request_context); + + g_mutex_lock (&clipboard_rdp->completion_mutex); + clipboard_rdp->completed_format_data_request = TRUE; + g_cond_signal (&clipboard_rdp->completion_cond); + g_mutex_unlock (&clipboard_rdp->completion_mutex); +} + +/** + * FreeRDP already updated our capabilites after the client told us + * about its capabilities, there is nothing to do here + */ +static uint32_t +cliprdr_client_capabilities (CliprdrServerContext *cliprdr_context, + const CLIPRDR_CAPABILITIES *capabilities) +{ + g_autoptr (GStrvBuilder) client_capabilities = NULL; + char **client_caps_strings; + g_autofree char *caps_string = NULL; + + client_capabilities = g_strv_builder_new (); + + if (cliprdr_context->useLongFormatNames) + g_strv_builder_add (client_capabilities, "long format names"); + if (cliprdr_context->streamFileClipEnabled) + g_strv_builder_add (client_capabilities, "stream file clip"); + if (cliprdr_context->fileClipNoFilePaths) + g_strv_builder_add (client_capabilities, "file clip no file paths"); + if (cliprdr_context->canLockClipData) + g_strv_builder_add (client_capabilities, "can lock clip data"); + if (cliprdr_context->hasHugeFileSupport) + g_strv_builder_add (client_capabilities, "huge file support"); + + client_caps_strings = g_strv_builder_end (client_capabilities); + caps_string = g_strjoinv (", ", client_caps_strings); + g_message ("[RDP.CLIPRDR] Client capabilities: %s", caps_string); + + g_strfreev (client_caps_strings); + + return CHANNEL_RC_OK; +} + +/** + * Client sent us a Temporary Directory PDU. + * We don't handle the CF_HDROP format however. It's a relict of the past. + */ +static uint32_t +cliprdr_temp_directory (CliprdrServerContext *cliprdr_context, + const CLIPRDR_TEMP_DIRECTORY *temp_directory) +{ + g_debug ("[RDP.CLIPRDR] Client sent a Temporary Directory PDU with path \"%s\"", + temp_directory->szTempDir); + + return CHANNEL_RC_OK; +} + +static void +abort_all_client_requests (GrdClipboardRdp *clipboard_rdp) +{ + ClientFormatDataRequestContext *request_context; + GHashTableIter iter; + GList *serials; + + g_debug ("[RDP.CLIPRDR] Aborting all pending client requests"); + + g_mutex_lock (&clipboard_rdp->client_request_mutex); + request_context = g_steal_pointer (&clipboard_rdp->current_client_request); + + g_clear_handle_id (&clipboard_rdp->client_format_data_response_id, + g_source_remove); + g_mutex_unlock (&clipboard_rdp->client_request_mutex); + + if (request_context) + abort_client_requests_for_context (clipboard_rdp, request_context); + + g_free (request_context); + g_clear_handle_id (&clipboard_rdp->client_request_abort_id, g_source_remove); + + g_hash_table_iter_init (&iter, clipboard_rdp->pending_client_requests); + while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &serials)) + { + abort_client_requests_for_serials (clipboard_rdp, serials); + g_hash_table_iter_remove (&iter); + } + + g_queue_clear_full (clipboard_rdp->ordered_client_requests, g_free); +} + +static gboolean +update_server_format_list (gpointer user_data) +{ + ServerFormatListUpdateContext *update_context = user_data; + GrdClipboardRdp *clipboard_rdp = update_context->clipboard_rdp; + GrdClipboard *clipboard = GRD_CLIPBOARD (update_context->clipboard_rdp); + GrdRdpFuseClipboard *rdp_fuse_clipboard = clipboard_rdp->rdp_fuse_clipboard; + CliprdrServerContext *cliprdr_context = clipboard_rdp->cliprdr_context; + CLIPRDR_FORMAT_LIST_RESPONSE format_list_response = {0}; + GList *mime_type_tables; + GrdMimeTypeTable *mime_type_table; + GList *l; + + mime_type_tables = g_steal_pointer (&update_context->mime_type_tables); + for (l = mime_type_tables; l; l = l->next) + { + mime_type_table = l->data; + + /* Also indirectly handles the GRD_MIME_TYPE_XS_GNOME_COPIED_FILES case */ + if (mime_type_table->mime_type == GRD_MIME_TYPE_TEXT_URILIST) + { + clipboard_rdp->server_file_contents_requests_allowed = FALSE; + + grd_rdp_fuse_clipboard_clear_no_cdi_selection (rdp_fuse_clipboard); + grd_rdp_fuse_clipboard_lazily_clear_all_cdi_selections (rdp_fuse_clipboard); + } + + remove_clipboard_format_data_for_mime_type (clipboard_rdp, + mime_type_table->mime_type); + g_hash_table_remove (clipboard_rdp->allowed_server_formats, + GUINT_TO_POINTER (mime_type_table->mime_type)); + } + + abort_all_client_requests (clipboard_rdp); + grd_clipboard_update_server_mime_type_list (clipboard, mime_type_tables); + + format_list_response.common.msgType = CB_FORMAT_LIST_RESPONSE; + format_list_response.common.msgFlags = CB_RESPONSE_OK; + + cliprdr_context->ServerFormatListResponse (cliprdr_context, + &format_list_response); + + /** + * Any FileContentsRequest that is still waiting for a FileContentsResponse + * won't get a response any more, when the client does not support clipboard + * data locking. So, drop all requests without clipDataId here. + */ + if (!cliprdr_context->canLockClipData) + grd_rdp_fuse_clipboard_dismiss_all_no_cdi_requests (rdp_fuse_clipboard); + + g_mutex_lock (&clipboard_rdp->server_format_list_update_mutex); + clipboard_rdp->server_format_list_update_id = 0; + g_mutex_unlock (&clipboard_rdp->server_format_list_update_mutex); + + g_mutex_lock (&clipboard_rdp->completion_mutex); + clipboard_rdp->completed_format_list = TRUE; + g_cond_signal (&clipboard_rdp->completion_cond); + g_mutex_unlock (&clipboard_rdp->completion_mutex); + + return G_SOURCE_REMOVE; +} + +static void +update_context_free (gpointer data) +{ + ServerFormatListUpdateContext *update_context = data; + + g_clear_list (&update_context->mime_type_tables, g_free); + + g_free (update_context); +} + +/** + * Client notifies us that its clipboard is updated with new clipboard data + */ +static uint32_t +cliprdr_client_format_list (CliprdrServerContext *cliprdr_context, + const CLIPRDR_FORMAT_LIST *format_list) +{ + GrdClipboardRdp *clipboard_rdp = cliprdr_context->custom; + ServerFormatListUpdateContext *update_context; + GHashTable *found_mime_types; + GrdMimeTypeTable *mime_type_table = NULL; + GList *mime_type_tables = NULL; + GrdMimeType mime_type; + gboolean already_has_text_format = FALSE; + uint32_t i; + + found_mime_types = g_hash_table_new (NULL, NULL); + + /** + * The text format CF_TEXT can depend on the CF_LOCALE content. In such + * situations, we might not get the correct content from WinPR. + * CF_UNICODETEXT is not affected by the CF_LOCALE content, so try to find + * the CF_UNICODETEXT first to ensure that we use CF_UNICODETEXT, when both + * formats are available and CF_TEXT is listed first. + */ + for (i = 0; i < format_list->numFormats; ++i) + { + if (format_list->formats[i].formatId == CF_UNICODETEXT) + { + mime_type = GRD_MIME_TYPE_TEXT_PLAIN_UTF8; + + mime_type_table = g_malloc0 (sizeof (GrdMimeTypeTable)); + mime_type_table->mime_type = mime_type; + mime_type_table->rdp.format_id = format_list->formats[i].formatId; + mime_type_tables = g_list_append (mime_type_tables, mime_type_table); + + g_hash_table_add (found_mime_types, GUINT_TO_POINTER (mime_type)); + mime_type = GRD_MIME_TYPE_TEXT_UTF8_STRING; + + mime_type_table = g_malloc0 (sizeof (GrdMimeTypeTable)); + mime_type_table->mime_type = mime_type; + mime_type_table->rdp.format_id = format_list->formats[i].formatId; + mime_type_tables = g_list_append (mime_type_tables, mime_type_table); + + g_hash_table_add (found_mime_types, GUINT_TO_POINTER (mime_type)); + already_has_text_format = TRUE; + g_debug ("[RDP.CLIPRDR] Force using CF_UNICODETEXT over CF_TEXT as " + "external format for text/plain;charset=utf-8 and UTF8_STRING"); + break; + } + } + + for (i = 0; i < format_list->numFormats; ++i) + { + mime_type = GRD_MIME_TYPE_NONE; + + /** + * First check formats with dynamically assigned format ids. These formats + * don't have consistent ids. Their name, however, is always the same. + * + * If the client uses short format names, the formatName is truncated to + * either 32 ASCII 8 characters or 16 UTF-16 characters. + */ + if (format_list->formats[i].formatName && + (strcmp (format_list->formats[i].formatName, "FileGroupDescriptorW") == 0 || + strcmp (format_list->formats[i].formatName, "FileGroupDescri") == 0)) + { + if (!g_hash_table_contains (found_mime_types, + GUINT_TO_POINTER (GRD_MIME_TYPE_TEXT_URILIST))) + { + /** + * Advertise the "x-special/gnome-copied-files" format in addition to + * the "text/uri-list" format + */ + mime_type = GRD_MIME_TYPE_XS_GNOME_COPIED_FILES; + + mime_type_table = g_malloc0 (sizeof (GrdMimeTypeTable)); + mime_type_table->mime_type = mime_type; + mime_type_table->rdp.format_id = format_list->formats[i].formatId; + mime_type_tables = g_list_append (mime_type_tables, mime_type_table); + + g_hash_table_add (found_mime_types, GUINT_TO_POINTER (mime_type)); + } + + mime_type = GRD_MIME_TYPE_TEXT_URILIST; + } + else if (format_list->formats[i].formatName && + strcmp (format_list->formats[i].formatName, + GRD_CFSTR_MIME_HTML) == 0) + { + mime_type = GRD_MIME_TYPE_TEXT_HTML; + } + else if (format_list->formats[i].formatName && + strcmp (format_list->formats[i].formatName, + GRD_CFSTR_MIME_GIF) == 0) + { + mime_type = GRD_MIME_TYPE_IMAGE_GIF; + } + else if (format_list->formats[i].formatName && + strcmp (format_list->formats[i].formatName, + GRD_CFSTR_MIME_PNG) == 0) + { + mime_type = GRD_MIME_TYPE_IMAGE_PNG; + } + else if (format_list->formats[i].formatName && + strcmp (format_list->formats[i].formatName, + GRD_CFSTR_MIME_JPEG) == 0) + { + mime_type = GRD_MIME_TYPE_IMAGE_JPEG; + } + else + { + switch (format_list->formats[i].formatId) + { + case CF_TEXT: + case CF_UNICODETEXT: + case CF_OEMTEXT: + if (!already_has_text_format) + { + mime_type = GRD_MIME_TYPE_TEXT_PLAIN_UTF8; + + g_assert (!g_hash_table_contains (found_mime_types, + GUINT_TO_POINTER (mime_type))); + + mime_type_table = g_malloc0 (sizeof (GrdMimeTypeTable)); + mime_type_table->mime_type = mime_type; + mime_type_table->rdp.format_id = format_list->formats[i].formatId; + mime_type_tables = g_list_append (mime_type_tables, + mime_type_table); + g_hash_table_add (found_mime_types, GUINT_TO_POINTER (mime_type)); + + mime_type = GRD_MIME_TYPE_TEXT_UTF8_STRING; + already_has_text_format = TRUE; + g_debug ("[RDP.CLIPRDR] Client advertised data for " + "text/plain;charset=utf-8 and UTF8_STRING " + "(external format: id: %u, name: %s)", + format_list->formats[i].formatId, + format_list->formats[i].formatName); + } + break; + case CF_DIB: + mime_type = GRD_MIME_TYPE_IMAGE_BMP; + break; + case CF_TIFF: + mime_type = GRD_MIME_TYPE_IMAGE_TIFF; + break; + default: + g_debug ("[RDP.CLIPRDR] Client advertised unknown format: id: %u, " + "name: %s", format_list->formats[i].formatId, + format_list->formats[i].formatName); + } + } + + if (mime_type != GRD_MIME_TYPE_NONE && + !g_hash_table_contains (found_mime_types, GUINT_TO_POINTER (mime_type))) + { + mime_type_table = g_malloc0 (sizeof (GrdMimeTypeTable)); + mime_type_table->mime_type = mime_type; + mime_type_table->rdp.format_id = format_list->formats[i].formatId; + mime_type_tables = g_list_append (mime_type_tables, mime_type_table); + + g_hash_table_add (found_mime_types, GUINT_TO_POINTER (mime_type)); + } + else if (mime_type != GRD_MIME_TYPE_NONE) + { + g_debug ("[RDP.CLIPRDR] Ignoring duplicated format: id: %u, name: %s", + format_list->formats[i].formatId, + format_list->formats[i].formatName); + } + } + + g_hash_table_destroy (found_mime_types); + + if (clipboard_rdp->server_format_list_update_id) + { + g_debug ("[RDP.CLIPRDR] Wrong message sequence: Got new format list " + "without being able to response to last update first"); + } + + g_mutex_lock (&clipboard_rdp->completion_mutex); + while (!clipboard_rdp->protocol_stopped && + !clipboard_rdp->completed_format_list) + { + g_cond_wait (&clipboard_rdp->completion_cond, + &clipboard_rdp->completion_mutex); + } + g_mutex_unlock (&clipboard_rdp->completion_mutex); + + if (clipboard_rdp->protocol_stopped) + { + g_list_free_full (mime_type_tables, g_free); + return CHANNEL_RC_OK; + } + + g_assert (clipboard_rdp->completed_format_list); + clipboard_rdp->completed_format_list = FALSE; + + update_context = g_malloc0 (sizeof (ServerFormatListUpdateContext)); + update_context->clipboard_rdp = clipboard_rdp; + update_context->mime_type_tables = mime_type_tables; + + g_mutex_lock (&clipboard_rdp->server_format_list_update_mutex); + clipboard_rdp->server_format_list_update_id = + g_idle_add_full (G_PRIORITY_DEFAULT_IDLE, update_server_format_list, + update_context, update_context_free); + g_mutex_unlock (&clipboard_rdp->server_format_list_update_mutex); + + return CHANNEL_RC_OK; +} + +static gboolean +handle_format_list_response (gpointer user_data) +{ + GrdClipboardRdp *clipboard_rdp = user_data; + GList *queued_server_formats; + + if (clipboard_rdp->pending_server_formats) + update_allowed_server_formats (clipboard_rdp, TRUE); + + g_clear_handle_id (&clipboard_rdp->pending_server_formats_drop_id, + g_source_remove); + + g_mutex_lock (&clipboard_rdp->client_format_list_response_mutex); + clipboard_rdp->client_format_list_response_id = 0; + g_mutex_unlock (&clipboard_rdp->client_format_list_response_mutex); + + queued_server_formats = g_steal_pointer (&clipboard_rdp->queued_server_formats); + if (queued_server_formats) + send_mime_type_list (clipboard_rdp, queued_server_formats); + + return G_SOURCE_REMOVE; +} + +/** + * Client sent us a response to our format list pdu + */ +static uint32_t +cliprdr_client_format_list_response (CliprdrServerContext *cliprdr_context, + const CLIPRDR_FORMAT_LIST_RESPONSE *format_list_response) +{ + GrdClipboardRdp *clipboard_rdp = cliprdr_context->custom; + g_autoptr (GMutexLocker) locker = NULL; + + locker = g_mutex_locker_new (&clipboard_rdp->client_format_list_response_mutex); + if (clipboard_rdp->client_format_list_response_id != 0) + { + g_warning ("[RDP.CLIPRDR] Wrong message sequence: Received an unexpected " + "format list response. Ignoring..."); + return CHANNEL_RC_OK; + } + + clipboard_rdp->format_list_response_msg_flags = + format_list_response->common.msgFlags; + + clipboard_rdp->client_format_list_response_id = + g_idle_add (handle_format_list_response, clipboard_rdp); + + return CHANNEL_RC_OK; +} + +static gboolean +retrieve_current_clipboard (gpointer user_data) +{ + GrdClipboardRdp *clipboard_rdp = user_data; + ClipDataEntry *entry = clipboard_rdp->clipboard_retrieval_context.entry; + ClipDataEntry *entry_to_replace; + uint64_t serial = clipboard_rdp->serial; + gboolean serial_already_in_use; + + g_debug ("[RDP.CLIPRDR] Tracking serial %lu for ClipDataEntry", + clipboard_rdp->serial); + + entry_to_replace = clipboard_rdp->clipboard_retrieval_context.entry_to_replace; + if (entry_to_replace) + { + g_hash_table_remove (clipboard_rdp->serial_entry_table, + GUINT_TO_POINTER (entry_to_replace->serial)); + } + + serial_already_in_use = g_hash_table_contains (clipboard_rdp->serial_entry_table, + GUINT_TO_POINTER (serial)); + if (!serial_already_in_use) + { + g_hash_table_insert (clipboard_rdp->serial_entry_table, + GUINT_TO_POINTER (serial), entry); + } + clipboard_rdp->clipboard_retrieval_context.serial_already_in_use = + serial_already_in_use; + + entry->serial = serial; + entry->has_file_list = clipboard_rdp->has_file_list; + entry->requests_allowed = clipboard_rdp->server_file_contents_requests_allowed; + + entry->system = clipboard_rdp->system; + entry->delegate = clipboard_rdp->delegate; + + g_mutex_lock (&clipboard_rdp->clip_data_entry_mutex); + clipboard_rdp->clipboard_retrieval_id = 0; + g_mutex_unlock (&clipboard_rdp->clip_data_entry_mutex); + + g_mutex_lock (&clipboard_rdp->completion_mutex); + clipboard_rdp->completed_clip_data_entry = TRUE; + g_cond_signal (&clipboard_rdp->completion_cond); + g_mutex_unlock (&clipboard_rdp->completion_mutex); + + return G_SOURCE_REMOVE; +} + +static gboolean +is_clip_data_entry_user_of_serial (gpointer key, + gpointer value, + gpointer user_data) +{ + ClipDataEntry *entry = value; + uint64_t serial = GPOINTER_TO_UINT (user_data); + + return entry->serial == serial; +} + +/** + * Client requests us to retain all file stream data on the clipboard + */ +static uint32_t +cliprdr_client_lock_clipboard_data (CliprdrServerContext *cliprdr_context, + const CLIPRDR_LOCK_CLIPBOARD_DATA *lock_clipboard_data) +{ + GrdClipboardRdp *clipboard_rdp = cliprdr_context->custom; + uint32_t clip_data_id = lock_clipboard_data->clipDataId; + g_autofree ClipDataEntry *entry = NULL; + + if (clipboard_rdp->protocol_stopped) + return CHANNEL_RC_OK; + + g_debug ("[RDP.CLIPRDR] Client requested a lock with clipDataId: %u", + clip_data_id); + + clipboard_rdp->clipboard_retrieval_context.entry_to_replace = NULL; + if (g_hash_table_lookup_extended (clipboard_rdp->clip_data_table, + GUINT_TO_POINTER (clip_data_id), + NULL, (gpointer *) &entry)) + { + g_warning ("[RDP.CLIPRDR] Protocol violation: Client requested a lock with" + " an existing clipDataId %u. Replacing existing ClipDataEntry", + clip_data_id); + + clipboard_rdp->clipboard_retrieval_context.entry_to_replace = entry; + } + + g_assert (clipboard_rdp->completed_clip_data_entry); + clipboard_rdp->completed_clip_data_entry = FALSE; + + entry = g_malloc0 (sizeof (ClipDataEntry)); + clipboard_rdp->clipboard_retrieval_context.entry = entry; + + g_mutex_lock (&clipboard_rdp->clip_data_entry_mutex); + clipboard_rdp->clipboard_retrieval_id = + g_idle_add (retrieve_current_clipboard, clipboard_rdp); + g_mutex_unlock (&clipboard_rdp->clip_data_entry_mutex); + + g_mutex_lock (&clipboard_rdp->completion_mutex); + while (!clipboard_rdp->protocol_stopped && + !clipboard_rdp->completed_clip_data_entry) + { + g_cond_wait (&clipboard_rdp->completion_cond, + &clipboard_rdp->completion_mutex); + } + g_mutex_unlock (&clipboard_rdp->completion_mutex); + + if (clipboard_rdp->protocol_stopped) + return CHANNEL_RC_OK; + + g_assert (clipboard_rdp->completed_clip_data_entry); + + if (clipboard_rdp->clipboard_retrieval_context.serial_already_in_use) + { + uint64_t serial = entry->serial; + + g_warning ("[RDP.CLIPRDR] Protocol violation: Double lock detected (" + "Clipboard serial already in use). Replacing the clipDataId."); + + g_free (entry); + entry = g_hash_table_find (clipboard_rdp->clip_data_table, + is_clip_data_entry_user_of_serial, + GUINT_TO_POINTER (serial)); + g_hash_table_steal (clipboard_rdp->clip_data_table, + GUINT_TO_POINTER (entry->id)); + } + + entry->id = clip_data_id; + + g_debug ("[RDP.CLIPRDR] Tracking lock with clipDataId %u", clip_data_id); + g_hash_table_insert (clipboard_rdp->clip_data_table, + GUINT_TO_POINTER (clip_data_id), + g_steal_pointer (&entry)); + + return CHANNEL_RC_OK; +} + +static gboolean +handle_clip_data_entry_destruction (gpointer user_data) +{ + GrdClipboardRdp *clipboard_rdp = user_data; + ClipDataEntry *entry; + + entry = g_steal_pointer (&clipboard_rdp->clipboard_destruction_context.entry); + + g_debug ("[RDP.CLIPRDR] Deleting ClipDataEntry with serial %lu", entry->serial); + g_hash_table_remove (clipboard_rdp->serial_entry_table, + GUINT_TO_POINTER (entry->serial)); + + g_mutex_lock (&clipboard_rdp->clip_data_entry_mutex); + clipboard_rdp->clipboard_destruction_id = 0; + g_mutex_unlock (&clipboard_rdp->clip_data_entry_mutex); + + g_mutex_lock (&clipboard_rdp->completion_mutex); + clipboard_rdp->completed_clip_data_entry = TRUE; + g_cond_signal (&clipboard_rdp->completion_cond); + g_mutex_unlock (&clipboard_rdp->completion_mutex); + + return G_SOURCE_REMOVE; +} + +/** + * Client notifies us that the file stream data for a specific clip data id MUST + * now be released + */ +static uint32_t +cliprdr_client_unlock_clipboard_data (CliprdrServerContext *cliprdr_context, + const CLIPRDR_UNLOCK_CLIPBOARD_DATA *unlock_clipboard_data) +{ + GrdClipboardRdp *clipboard_rdp = cliprdr_context->custom; + uint32_t clip_data_id = unlock_clipboard_data->clipDataId; + ClipDataEntry *entry; + + if (clipboard_rdp->protocol_stopped) + return CHANNEL_RC_OK; + + g_debug ("[RDP.CLIPRDR] Client requested an unlock with clipDataId: %u", + clip_data_id); + if (!g_hash_table_lookup_extended (clipboard_rdp->clip_data_table, + GUINT_TO_POINTER (clip_data_id), + NULL, (gpointer *) &entry)) + { + g_warning ("[RDP.CLIPRDR] Protocol violation: ClipDataEntry with id %u " + "does not exist", clip_data_id); + return CHANNEL_RC_OK; + } + + g_debug ("[RDP.CLIPRDR] Removing lock with clipDataId: %u", clip_data_id); + + g_assert (clipboard_rdp->completed_clip_data_entry); + clipboard_rdp->completed_clip_data_entry = FALSE; + + clipboard_rdp->clipboard_destruction_context.entry = entry; + + g_mutex_lock (&clipboard_rdp->clip_data_entry_mutex); + clipboard_rdp->clipboard_destruction_id = + g_idle_add (handle_clip_data_entry_destruction, clipboard_rdp); + g_mutex_unlock (&clipboard_rdp->clip_data_entry_mutex); + + g_mutex_lock (&clipboard_rdp->completion_mutex); + while (!clipboard_rdp->protocol_stopped && + !clipboard_rdp->completed_clip_data_entry) + { + g_cond_wait (&clipboard_rdp->completion_cond, + &clipboard_rdp->completion_mutex); + } + g_mutex_unlock (&clipboard_rdp->completion_mutex); + + g_debug ("[RDP.CLIPRDR] Untracking lock with clipDataId %u", clip_data_id); + g_hash_table_remove (clipboard_rdp->clip_data_table, + GUINT_TO_POINTER (clip_data_id)); + + return CHANNEL_RC_OK; +} + +static gboolean +request_server_format_data (gpointer user_data) +{ + GrdClipboardRdp *clipboard_rdp = user_data; + GrdClipboard *clipboard = GRD_CLIPBOARD (clipboard_rdp); + CliprdrServerContext *cliprdr_context = clipboard_rdp->cliprdr_context; + ServerFormatDataRequestContext *request_context; + GrdMimeType mime_type; + + request_context = clipboard_rdp->format_data_request_context; + mime_type = request_context->mime_type; + + if (!g_hash_table_contains (clipboard_rdp->allowed_server_formats, + GUINT_TO_POINTER (mime_type))) + { + CLIPRDR_FORMAT_DATA_RESPONSE format_data_response = {0}; + + format_data_response.common.msgType = CB_FORMAT_DATA_RESPONSE; + format_data_response.common.msgFlags = CB_RESPONSE_FAIL; + + cliprdr_context->ServerFormatDataResponse (cliprdr_context, + &format_data_response); + + g_clear_pointer (&clipboard_rdp->format_data_request_context, g_free); + + g_mutex_lock (&clipboard_rdp->server_format_data_request_mutex); + clipboard_rdp->server_format_data_request_id = 0; + g_mutex_unlock (&clipboard_rdp->server_format_data_request_mutex); + + g_mutex_lock (&clipboard_rdp->completion_mutex); + clipboard_rdp->completed_format_data_request = TRUE; + g_cond_signal (&clipboard_rdp->completion_cond); + g_mutex_unlock (&clipboard_rdp->completion_mutex); + + return G_SOURCE_REMOVE; + } + + grd_clipboard_request_server_content_for_mime_type_async (clipboard, + mime_type); + + g_mutex_lock (&clipboard_rdp->server_format_data_request_mutex); + clipboard_rdp->server_format_data_request_id = 0; + g_mutex_unlock (&clipboard_rdp->server_format_data_request_mutex); + + return G_SOURCE_REMOVE; +} + +/** + * Client knows our format list, it requests now the data from our clipboard + */ +static uint32_t +cliprdr_client_format_data_request (CliprdrServerContext *cliprdr_context, + const CLIPRDR_FORMAT_DATA_REQUEST *format_data_request) +{ + GrdClipboardRdp *clipboard_rdp = cliprdr_context->custom; + ServerFormatDataRequestContext *request_context; + CLIPRDR_FORMAT_DATA_RESPONSE format_data_response = {0}; + GrdMimeType mime_type = GRD_MIME_TYPE_NONE; + uint32_t src_format_id = 0; + uint32_t dst_format_id; + gboolean needs_null_terminator = FALSE; + gboolean needs_conversion = FALSE; + + dst_format_id = format_data_request->requestedFormatId; + switch (dst_format_id) + { + case CF_TEXT: + mime_type = GRD_MIME_TYPE_TEXT_PLAIN; + needs_null_terminator = TRUE; + needs_conversion = TRUE; + src_format_id = ClipboardGetFormatId (clipboard_rdp->system, + "text/plain"); + break; + case CF_UNICODETEXT: + mime_type = clipboard_rdp->which_unicode_format; + needs_null_terminator = TRUE; + needs_conversion = TRUE; + src_format_id = ClipboardGetFormatId (clipboard_rdp->system, + "text/plain"); + break; + case GRD_CF_HTML: + mime_type = GRD_MIME_TYPE_TEXT_HTML; + needs_null_terminator = TRUE; + needs_conversion = TRUE; + src_format_id = ClipboardGetFormatId (clipboard_rdp->system, + GRD_CFSTR_MIME_TEXT_HTML); + dst_format_id = ClipboardGetFormatId (clipboard_rdp->system, + GRD_CFSTR_MIME_HTML); + break; + case CF_DIB: + mime_type = GRD_MIME_TYPE_IMAGE_BMP; + needs_conversion = TRUE; + src_format_id = ClipboardGetFormatId (clipboard_rdp->system, + "image/bmp"); + break; + case CF_TIFF: + mime_type = GRD_MIME_TYPE_IMAGE_TIFF; + break; + case GRD_CF_GIF: + mime_type = GRD_MIME_TYPE_IMAGE_GIF; + break; + case GRD_CF_JPEG: + mime_type = GRD_MIME_TYPE_IMAGE_JPEG; + break; + case GRD_CF_PNG: + mime_type = GRD_MIME_TYPE_IMAGE_PNG; + break; + case GRD_CF_TEXT_URILIST: + mime_type = GRD_MIME_TYPE_TEXT_URILIST; + needs_conversion = TRUE; + src_format_id = ClipboardGetFormatId (clipboard_rdp->system, + "text/uri-list"); + dst_format_id = ClipboardGetFormatId (clipboard_rdp->system, + "FileGroupDescriptorW"); + break; + } + + if (clipboard_rdp->server_format_data_request_id) + { + g_debug ("[RDP.CLIPRDR] Wrong message sequence: Got new format data " + "request without being able to push response for last request " + "yet."); + } + + g_mutex_lock (&clipboard_rdp->completion_mutex); + while (!clipboard_rdp->protocol_stopped && + !clipboard_rdp->completed_format_data_request) + { + g_cond_wait (&clipboard_rdp->completion_cond, + &clipboard_rdp->completion_mutex); + } + g_mutex_unlock (&clipboard_rdp->completion_mutex); + + if (clipboard_rdp->protocol_stopped) + return CHANNEL_RC_OK; + + if (mime_type != GRD_MIME_TYPE_NONE) + { + g_assert (clipboard_rdp->completed_format_data_request); + clipboard_rdp->completed_format_data_request = FALSE; + + request_context = g_malloc0 (sizeof (ServerFormatDataRequestContext)); + request_context->clipboard_rdp = clipboard_rdp; + request_context->mime_type = mime_type; + request_context->src_format_id = src_format_id; + request_context->dst_format_id = dst_format_id; + request_context->needs_null_terminator = needs_null_terminator; + request_context->needs_conversion = needs_conversion; + + g_assert (!clipboard_rdp->format_data_request_context); + clipboard_rdp->format_data_request_context = request_context; + + g_mutex_lock (&clipboard_rdp->server_format_data_request_mutex); + clipboard_rdp->server_format_data_request_id = + g_idle_add (request_server_format_data, clipboard_rdp); + g_mutex_unlock (&clipboard_rdp->server_format_data_request_mutex); + + return CHANNEL_RC_OK; + } + + format_data_response.common.msgType = CB_FORMAT_DATA_RESPONSE; + format_data_response.common.msgFlags = CB_RESPONSE_FAIL; + format_data_response.common.dataLen = 0; + format_data_response.requestedFormatData = NULL; + + return cliprdr_context->ServerFormatDataResponse (cliprdr_context, + &format_data_response); +} + +static gboolean +extract_format_data_response (GrdClipboardRdp *clipboard_rdp, + CLIPRDR_FORMAT_DATA_RESPONSE *format_data_response, + uint8_t **data, + uint32_t *size) +{ + gboolean response_ok; + + *data = (uint8_t *) format_data_response->requestedFormatData; + *size = format_data_response->common.dataLen; + + response_ok = format_data_response->common.msgFlags & CB_RESPONSE_OK; + *size = response_ok && *data ? *size : 0; + + if (!response_ok) + g_clear_pointer (data, g_free); + + return response_ok && *data; +} + +static gboolean +filedescriptorw_filename_is_valid (const WCHAR *filename) +{ + uint16_t i; + + if (filename[0] == L'\0') + return FALSE; + + for (i = 1; i < 260; ++i) + { + if (filename[i] == L'\0') + return TRUE; + } + + return FALSE; +} + +static uint8_t * +get_uri_list_from_packet_file_list (GrdClipboardRdp *clipboard_rdp, + uint8_t *src_data, + uint32_t src_size, + uint32_t *dst_size, + gboolean has_clip_data_id, + uint32_t clip_data_id) +{ + g_autofree FILEDESCRIPTORW *files = NULL; + FILEDESCRIPTORW *file; + uint32_t n_files = 0; + g_autofree char *clip_data_dir_name = NULL; + char *filename = NULL; + char *escaped_name; + char *file_uri; + g_autoptr (GArray) dst_data = NULL; + uint32_t i; + + clip_data_dir_name = has_clip_data_id ? + g_strdup_printf ("%u", clip_data_id) : + g_strdup_printf ("%lu", GRD_RDP_FUSE_CLIPBOARD_NO_CLIP_DATA_ID); + + *dst_size = 0; + dst_data = g_array_new (TRUE, TRUE, sizeof (char)); + + if (cliprdr_parse_file_list (src_data, src_size, &files, &n_files)) + return NULL; + + for (i = 0; i < n_files; ++i) + { + file = &files[i]; + + if (!filedescriptorw_filename_is_valid (file->cFileName)) + return NULL; + + filename = ConvertWCharToUtf8Alloc (file->cFileName, NULL); + if (!filename) + return NULL; + if (strchr (filename, '\\') || strchr (filename, '/')) + { + g_free (filename); + continue; + } + + escaped_name = g_uri_escape_string (filename, + G_URI_RESERVED_CHARS_ALLOWED_IN_PATH, + TRUE); + file_uri = g_strdup_printf ("file://%s/%s/%s\r\n", + clipboard_rdp->fuse_mount_path, + clip_data_dir_name, escaped_name); + g_array_append_vals (dst_data, file_uri, strlen (file_uri)); + + g_free (file_uri); + g_free (escaped_name); + g_free (filename); + } + + *dst_size = dst_data->len; + + return (uint8_t *) g_array_free (g_steal_pointer (&dst_data), FALSE); +} + +static uint8_t * +convert_client_content_for_server (GrdClipboardRdp *clipboard_rdp, + uint8_t *src_data, + uint32_t src_size, + GrdMimeType mime_type, + uint32_t src_format_id, + uint32_t *dst_size, + gboolean has_clip_data_id, + uint32_t clip_data_id) +{ + GrdRdpFuseClipboard *rdp_fuse_clipboard = clipboard_rdp->rdp_fuse_clipboard; + uint32_t dst_format_id; + uint8_t *dst_data; + gboolean is_null_terminated = FALSE; + BOOL success; + + *dst_size = 0; + + switch (mime_type) + { + case GRD_MIME_TYPE_TEXT_PLAIN_UTF8: + case GRD_MIME_TYPE_TEXT_UTF8_STRING: + is_null_terminated = TRUE; + dst_format_id = ClipboardGetFormatId (clipboard_rdp->system, + "text/plain"); + break; + case GRD_MIME_TYPE_TEXT_HTML: + is_null_terminated = TRUE; + src_format_id = ClipboardGetFormatId (clipboard_rdp->system, + GRD_CFSTR_MIME_HTML); + dst_format_id = ClipboardGetFormatId (clipboard_rdp->system, + GRD_CFSTR_MIME_TEXT_HTML); + break; + case GRD_MIME_TYPE_IMAGE_BMP: + dst_format_id = ClipboardGetFormatId (clipboard_rdp->system, + "image/bmp"); + break; + case GRD_MIME_TYPE_TEXT_URILIST: + case GRD_MIME_TYPE_XS_GNOME_COPIED_FILES: + src_format_id = ClipboardGetFormatId (clipboard_rdp->system, + "FileGroupDescriptorW"); + dst_format_id = ClipboardGetFormatId (clipboard_rdp->system, + "text/uri-list"); + break; + default: + g_assert_not_reached (); + } + + success = ClipboardSetData (clipboard_rdp->system, + src_format_id, src_data, src_size); + if (!success) + { + g_warning ("[RDP.CLIPRDR] Converting clipboard content failed: " + "Failed to set data to convert"); + return NULL; + } + + if (mime_type == GRD_MIME_TYPE_TEXT_URILIST || + mime_type == GRD_MIME_TYPE_XS_GNOME_COPIED_FILES) + { + dst_data = get_uri_list_from_packet_file_list (clipboard_rdp, + src_data, src_size, + dst_size, has_clip_data_id, + clip_data_id); + } + else + { + dst_data = ClipboardGetData (clipboard_rdp->system, dst_format_id, + dst_size); + } + if (!dst_data) + { + g_warning ("[RDP.CLIPRDR] Converting clipboard content failed: " + "Failed to get converted data"); + return NULL; + } + + if (is_null_terminated) + { + uint8_t *null_terminator_pos = memchr (dst_data, '\0', *dst_size); + if (null_terminator_pos) + *dst_size = null_terminator_pos - dst_data; + } + + if (mime_type == GRD_MIME_TYPE_TEXT_URILIST || + mime_type == GRD_MIME_TYPE_XS_GNOME_COPIED_FILES) + { + FILEDESCRIPTORW *files = NULL; + FILEDESCRIPTORW *file; + uint32_t n_files = 0; + gboolean result; + g_autofree char *clip_data_dir_name = NULL; + char *filename = NULL; + char *escaped_name; + char *full_filepath; + GrdMimeType second_mime_type; + FormatData *format_data; + GArray *data_nautilus; + const char *nautilus_header; + uint8_t *dst_data_nautilus; + uint32_t dst_size_nautilus; + uint32_t i; + + if (cliprdr_parse_file_list (src_data, src_size, &files, &n_files)) + { + g_free (dst_data); + return NULL; + } + + if (has_clip_data_id) + { + result = grd_rdp_fuse_clipboard_set_cdi_selection (rdp_fuse_clipboard, + files, n_files, + clip_data_id); + } + else + { + result = grd_rdp_fuse_clipboard_set_no_cdi_selection (rdp_fuse_clipboard, + files, n_files); + } + if (!result) + { + g_free (files); + g_free (dst_data); + return NULL; + } + + clip_data_dir_name = has_clip_data_id ? + g_strdup_printf ("%u", clip_data_id) : + g_strdup_printf ("%lu", GRD_RDP_FUSE_CLIPBOARD_NO_CLIP_DATA_ID); + + data_nautilus = g_array_new (TRUE, TRUE, sizeof (char)); + nautilus_header = "copy"; + g_array_append_vals (data_nautilus, nautilus_header, + strlen (nautilus_header)); + for (i = 0; i < n_files; ++i) + { + file = &files[i]; + + filename = ConvertWCharToUtf8Alloc (file->cFileName, NULL); + if (!filename) + { + g_array_free (data_nautilus, TRUE); + grd_rdp_fuse_clipboard_clear_no_cdi_selection (rdp_fuse_clipboard); + g_free (files); + g_free (dst_data); + return NULL; + } + if (strchr (filename, '\\') || strchr (filename, '/')) + { + g_free (filename); + continue; + } + + escaped_name = g_uri_escape_string (filename, + G_URI_RESERVED_CHARS_ALLOWED_IN_PATH, TRUE); + full_filepath = g_strdup_printf ("\nfile://%s/%s/%s", + clipboard_rdp->fuse_mount_path, + clip_data_dir_name, escaped_name); + g_array_append_vals (data_nautilus, full_filepath, + strlen (full_filepath)); + + g_free (full_filepath); + g_free (escaped_name); + g_free (filename); + } + + dst_size_nautilus = data_nautilus->len; + dst_data_nautilus = (uint8_t *) g_array_free (data_nautilus, FALSE); + + format_data = g_malloc0 (sizeof (FormatData)); + if (mime_type == GRD_MIME_TYPE_TEXT_URILIST) + { + /** + * Also create the format data for the + * "x-special/gnome-copied-files" mimetype + */ + second_mime_type = GRD_MIME_TYPE_XS_GNOME_COPIED_FILES; + format_data->size = dst_size_nautilus; + format_data->data = dst_data_nautilus; + } + else if (mime_type == GRD_MIME_TYPE_XS_GNOME_COPIED_FILES) + { + /** + * Also create the format data for the "text/uri-list" mimetype + */ + second_mime_type = GRD_MIME_TYPE_TEXT_URILIST; + format_data->size = *dst_size; + format_data->data = dst_data; + + *dst_size = dst_size_nautilus; + dst_data = dst_data_nautilus; + } + else + { + g_assert_not_reached (); + } + + g_hash_table_insert (clipboard_rdp->format_data_cache, + GUINT_TO_POINTER (second_mime_type), + format_data); + + g_free (files); + } + + if (!dst_data) + g_warning ("[RDP.CLIPRDR] Converting clipboard content failed"); + + return dst_data; +} + +static gboolean +prepare_client_content_for_server (GrdClipboardRdp *clipboard_rdp, + uint8_t **src_data, + uint32_t src_size, + GrdMimeTypeTable *mime_type_table, + gboolean has_clip_data_id, + uint32_t clip_data_id) +{ + GrdMimeType mime_type = mime_type_table->mime_type; + uint32_t src_format_id = mime_type_table->rdp.format_id; + FormatData *format_data; + uint8_t *dst_data = NULL; + uint32_t dst_size = 0; + + switch (mime_type) + { + case GRD_MIME_TYPE_TEXT_PLAIN_UTF8: + case GRD_MIME_TYPE_TEXT_UTF8_STRING: + case GRD_MIME_TYPE_TEXT_HTML: + case GRD_MIME_TYPE_IMAGE_BMP: + case GRD_MIME_TYPE_TEXT_URILIST: + case GRD_MIME_TYPE_XS_GNOME_COPIED_FILES: + dst_data = convert_client_content_for_server (clipboard_rdp, + *src_data, src_size, + mime_type, src_format_id, + &dst_size, has_clip_data_id, + clip_data_id); + break; + default: + dst_data = g_steal_pointer (src_data); + dst_size = src_size; + } + + if (!dst_data) + return FALSE; + + format_data = g_malloc0 (sizeof (FormatData)); + format_data->size = dst_size; + format_data->data = dst_data; + + g_hash_table_insert (clipboard_rdp->format_data_cache, + GUINT_TO_POINTER (mime_type), + format_data); + + return TRUE; +} + +static void +serve_mime_type_content (GrdClipboardRdp *clipboard_rdp, + GrdMimeType mime_type) +{ + GrdClipboard *clipboard = GRD_CLIPBOARD (clipboard_rdp); + FormatData *format_data; + GList *serials; + unsigned int serial; + GList *l; + + if (!g_hash_table_steal_extended (clipboard_rdp->pending_client_requests, + GUINT_TO_POINTER (mime_type), + NULL, (gpointer *) &serials)) + return; + + if (!g_hash_table_lookup_extended (clipboard_rdp->format_data_cache, + GUINT_TO_POINTER (mime_type), + NULL, (gpointer *) &format_data)) + g_assert_not_reached (); + + for (l = serials; l; l = l->next) + { + serial = GPOINTER_TO_UINT (l->data); + grd_clipboard_submit_client_content_for_mime_type (clipboard, serial, + format_data->data, + format_data->size); + } + g_list_free (serials); +} + +static gboolean +handle_format_data_response (gpointer user_data) +{ + GrdClipboardRdp *clipboard_rdp = user_data; + ClientFormatDataRequestContext *request_context; + CLIPRDR_FORMAT_DATA_RESPONSE *format_data_response; + GrdMimeTypeTable *mime_type_table; + GrdMimeType mime_type; + uint8_t *src_data; + uint32_t src_size; + + g_mutex_lock (&clipboard_rdp->client_request_mutex); + request_context = g_steal_pointer (&clipboard_rdp->current_client_request); + format_data_response = g_steal_pointer (&clipboard_rdp->format_data_response); + + clipboard_rdp->client_format_data_response_id = 0; + g_mutex_unlock (&clipboard_rdp->client_request_mutex); + + mime_type_table = &request_context->mime_type_table; + mime_type = mime_type_table->mime_type; + + if (extract_format_data_response (clipboard_rdp, format_data_response, + &src_data, &src_size) && + prepare_client_content_for_server (clipboard_rdp, &src_data, src_size, + mime_type_table, + request_context->has_clip_data_id, + request_context->clip_data_id)) + { + serve_mime_type_content (clipboard_rdp, mime_type); + + if (mime_type == GRD_MIME_TYPE_TEXT_URILIST) + serve_mime_type_content (clipboard_rdp, GRD_MIME_TYPE_XS_GNOME_COPIED_FILES); + else if (mime_type == GRD_MIME_TYPE_XS_GNOME_COPIED_FILES) + serve_mime_type_content (clipboard_rdp, GRD_MIME_TYPE_TEXT_URILIST); + } + else + { + abort_client_requests_for_context (clipboard_rdp, request_context); + } + g_free (src_data); + g_free (format_data_response); + g_free (request_context); + + g_clear_handle_id (&clipboard_rdp->client_request_abort_id, g_source_remove); + + maybe_send_next_mime_type_content_request (clipboard_rdp); + + return G_SOURCE_REMOVE; +} + +/** + * Clients response to our data request that we sent + */ +static uint32_t +cliprdr_client_format_data_response (CliprdrServerContext *cliprdr_context, + const CLIPRDR_FORMAT_DATA_RESPONSE *format_data_response) +{ + GrdClipboardRdp *clipboard_rdp = cliprdr_context->custom; + + g_mutex_lock (&clipboard_rdp->client_request_mutex); + if (!clipboard_rdp->current_client_request) + { + g_mutex_unlock (&clipboard_rdp->client_request_mutex); + return CHANNEL_RC_OK; + } + + if (clipboard_rdp->format_data_response) + { + g_free ((uint8_t *) clipboard_rdp->format_data_response->requestedFormatData); + g_clear_pointer (&clipboard_rdp->format_data_response, g_free); + } + + clipboard_rdp->format_data_response = + g_memdup2 (format_data_response, + sizeof (CLIPRDR_FORMAT_DATA_RESPONSE)); + clipboard_rdp->format_data_response->requestedFormatData = + g_memdup2 (format_data_response->requestedFormatData, + format_data_response->common.dataLen); + + if (!clipboard_rdp->client_format_data_response_id) + { + clipboard_rdp->client_format_data_response_id = + g_idle_add (handle_format_data_response, clipboard_rdp); + } + g_mutex_unlock (&clipboard_rdp->client_request_mutex); + + return CHANNEL_RC_OK; +} + +static uint32_t +delegate_request_file_contents_size (wClipboardDelegate *delegate, + const CLIPRDR_FILE_CONTENTS_REQUEST *file_contents_request) +{ + wClipboardFileSizeRequest file_size_request = {0}; + + file_size_request.streamId = file_contents_request->streamId; + file_size_request.listIndex = file_contents_request->listIndex; + + return delegate->ClientRequestFileSize (delegate, &file_size_request); +} + +static uint32_t +delegate_request_file_contents_range (wClipboardDelegate *delegate, + const CLIPRDR_FILE_CONTENTS_REQUEST *file_contents_request) +{ + wClipboardFileRangeRequest file_range_request = {0}; + + file_range_request.streamId = file_contents_request->streamId; + file_range_request.listIndex = file_contents_request->listIndex; + file_range_request.nPositionLow = file_contents_request->nPositionLow; + file_range_request.nPositionHigh = file_contents_request->nPositionHigh; + file_range_request.cbRequested = file_contents_request->cbRequested; + + return delegate->ClientRequestFileRange (delegate, &file_range_request); +} + +static uint32_t +send_file_contents_response_failure (CliprdrServerContext *cliprdr_context, + uint32_t stream_id) +{ + CLIPRDR_FILE_CONTENTS_RESPONSE file_contents_response = {0}; + + file_contents_response.common.msgType = CB_FILECONTENTS_RESPONSE; + file_contents_response.common.msgFlags = CB_RESPONSE_FAIL; + file_contents_response.streamId = stream_id; + + return cliprdr_context->ServerFileContentsResponse (cliprdr_context, + &file_contents_response); +} + +/** + * Client requests us to send either the size of the remote file or a portion + * of the data in the file + */ +static uint32_t +cliprdr_client_file_contents_request (CliprdrServerContext *cliprdr_context, + const CLIPRDR_FILE_CONTENTS_REQUEST *file_contents_request) +{ + GrdClipboardRdp *clipboard_rdp = cliprdr_context->custom; + wClipboardDelegate *delegate = clipboard_rdp->delegate; + gboolean has_file_list = clipboard_rdp->has_file_list; + gboolean requests_allowed = clipboard_rdp->server_file_contents_requests_allowed; + uint32_t clip_data_id = file_contents_request->clipDataId; + uint32_t stream_id = file_contents_request->streamId; + ClipDataEntry *entry = NULL; + uint32_t error = NO_ERROR; + + if (file_contents_request->haveClipDataId) + g_debug ("[RDP.CLIPRDR] FileContentsRequest has clipDataId %u", clip_data_id); + else + g_debug ("[RDP.CLIPRDR] FileContentsRequest does not have a clipDataId"); + + if (file_contents_request->haveClipDataId) + { + if (!g_hash_table_lookup_extended (clipboard_rdp->clip_data_table, + GUINT_TO_POINTER (clip_data_id), + NULL, (gpointer *) &entry)) + return send_file_contents_response_failure (cliprdr_context, stream_id); + + delegate = entry->delegate; + has_file_list = entry->has_file_list; + requests_allowed = entry->requests_allowed; + + if (!requests_allowed) + { + g_debug ("[RDP.CLIPRDR] ClipDataEntry with id %u is not eligible of " + "requesting file contents.", clip_data_id); + } + } + + if (!requests_allowed || !has_file_list) + return send_file_contents_response_failure (cliprdr_context, stream_id); + + if (file_contents_request->dwFlags & FILECONTENTS_SIZE && + file_contents_request->dwFlags & FILECONTENTS_RANGE) + { + /** + * FILECONTENTS_SIZE and FILECONTENTS_RANGE are not allowed + * to be set at the same time + */ + return send_file_contents_response_failure (cliprdr_context, stream_id); + } + + if (file_contents_request->dwFlags & FILECONTENTS_SIZE) + { + error = delegate_request_file_contents_size (delegate, + file_contents_request); + } + else if (file_contents_request->dwFlags & FILECONTENTS_RANGE) + { + error = delegate_request_file_contents_range (delegate, + file_contents_request); + } + else + { + error = ERROR_INVALID_DATA; + } + + if (error) + return send_file_contents_response_failure (cliprdr_context, stream_id); + + return CHANNEL_RC_OK; +} + +/** + * Clients response to our file contents request that we sent + */ +static uint32_t +cliprdr_client_file_contents_response (CliprdrServerContext *cliprdr_context, + const CLIPRDR_FILE_CONTENTS_RESPONSE *file_contents_response) +{ + GrdClipboardRdp *clipboard_rdp = cliprdr_context->custom; + GrdRdpFuseClipboard *rdp_fuse_clipboard = clipboard_rdp->rdp_fuse_clipboard; + + grd_rdp_fuse_clipboard_submit_file_contents_response ( + rdp_fuse_clipboard, file_contents_response->streamId, + file_contents_response->common.msgFlags & CB_RESPONSE_OK, + file_contents_response->requestedData, + file_contents_response->cbRequested); + + return CHANNEL_RC_OK; +} + +static uint32_t +cliprdr_file_size_success (wClipboardDelegate *delegate, + const wClipboardFileSizeRequest *file_size_request, + uint64_t file_size) +{ + GrdClipboardRdp *clipboard_rdp = delegate->custom; + CliprdrServerContext *cliprdr_context = clipboard_rdp->cliprdr_context; + CLIPRDR_FILE_CONTENTS_RESPONSE file_contents_response = {0}; + + file_contents_response.common.msgType = CB_FILECONTENTS_RESPONSE; + file_contents_response.common.msgFlags = CB_RESPONSE_OK; + file_contents_response.streamId = file_size_request->streamId; + file_contents_response.cbRequested = sizeof (uint64_t); + file_contents_response.requestedData = (uint8_t *) &file_size; + + return cliprdr_context->ServerFileContentsResponse (cliprdr_context, + &file_contents_response); +} + +static uint32_t +cliprdr_file_size_failure (wClipboardDelegate *delegate, + const wClipboardFileSizeRequest *file_size_request, + uint32_t error) +{ + GrdClipboardRdp *clipboard_rdp = delegate->custom; + + return send_file_contents_response_failure (clipboard_rdp->cliprdr_context, + file_size_request->streamId); +} + +static uint32_t +cliprdr_file_range_success (wClipboardDelegate *delegate, + const wClipboardFileRangeRequest *file_range_request, + const uint8_t *data, + uint32_t size) +{ + GrdClipboardRdp *clipboard_rdp = delegate->custom; + CliprdrServerContext *cliprdr_context = clipboard_rdp->cliprdr_context; + CLIPRDR_FILE_CONTENTS_RESPONSE file_contents_response = {0}; + + file_contents_response.common.msgType = CB_FILECONTENTS_RESPONSE; + file_contents_response.common.msgFlags = CB_RESPONSE_OK; + file_contents_response.streamId = file_range_request->streamId; + file_contents_response.cbRequested = size; + file_contents_response.requestedData = data; + + return cliprdr_context->ServerFileContentsResponse (cliprdr_context, + &file_contents_response); +} + +static uint32_t +cliprdr_file_range_failure (wClipboardDelegate *delegate, + const wClipboardFileRangeRequest *file_range_request, + uint32_t error) +{ + GrdClipboardRdp *clipboard_rdp = delegate->custom; + + return send_file_contents_response_failure (clipboard_rdp->cliprdr_context, + file_range_request->streamId); +} + +static BOOL +is_valid_unix_filename (LPCWSTR filename) +{ + LPCWSTR c; + + if (!filename) + return FALSE; + + if (filename[0] == L'\0') + return FALSE; + + /* Reserved characters */ + for (c = filename; *c; ++c) + { + if (*c == L'/') + return FALSE; + } + + return TRUE; +} + +static void +create_new_winpr_clipboard (GrdClipboardRdp *clipboard_rdp) +{ + g_debug ("[RDP.CLIPRDR] Creating new WinPR clipboard"); + + clipboard_rdp->system = ClipboardCreate (); + clipboard_rdp->delegate = ClipboardGetDelegate (clipboard_rdp->system); + clipboard_rdp->delegate->ClipboardFileSizeSuccess = cliprdr_file_size_success; + clipboard_rdp->delegate->ClipboardFileSizeFailure = cliprdr_file_size_failure; + clipboard_rdp->delegate->ClipboardFileRangeSuccess = cliprdr_file_range_success; + clipboard_rdp->delegate->ClipboardFileRangeFailure = cliprdr_file_range_failure; + clipboard_rdp->delegate->basePath = NULL; + clipboard_rdp->delegate->custom = clipboard_rdp; + + if (clipboard_rdp->relieve_filename_restriction) + clipboard_rdp->delegate->IsFileNameComponentValid = is_valid_unix_filename; + + clipboard_rdp->has_file_list = FALSE; +} + +GrdClipboardRdp * +grd_clipboard_rdp_new (GrdSessionRdp *session_rdp, + HANDLE vcm, + gboolean relieve_filename_restriction) +{ + g_autoptr (GrdClipboardRdp) clipboard_rdp = NULL; + GrdClipboard *clipboard; + CliprdrServerContext *cliprdr_context; + + clipboard_rdp = g_object_new (GRD_TYPE_CLIPBOARD_RDP, NULL); + cliprdr_context = cliprdr_server_context_new (vcm); + if (!cliprdr_context) + { + g_warning ("[RDP.CLIPRDR] Failed to create server context"); + return NULL; + } + + clipboard_rdp->cliprdr_context = cliprdr_context; + clipboard_rdp->relieve_filename_restriction = relieve_filename_restriction; + + clipboard = GRD_CLIPBOARD (clipboard_rdp); + grd_clipboard_initialize (clipboard, GRD_SESSION (session_rdp)); + + cliprdr_context->useLongFormatNames = TRUE; + cliprdr_context->streamFileClipEnabled = TRUE; + cliprdr_context->fileClipNoFilePaths = TRUE; + cliprdr_context->canLockClipData = TRUE; + cliprdr_context->hasHugeFileSupport = TRUE; + + cliprdr_context->ClientCapabilities = cliprdr_client_capabilities; + cliprdr_context->TempDirectory = cliprdr_temp_directory; + cliprdr_context->ClientFormatList = cliprdr_client_format_list; + cliprdr_context->ClientFormatListResponse = cliprdr_client_format_list_response; + cliprdr_context->ClientLockClipboardData = cliprdr_client_lock_clipboard_data; + cliprdr_context->ClientUnlockClipboardData = cliprdr_client_unlock_clipboard_data; + cliprdr_context->ClientFormatDataRequest = cliprdr_client_format_data_request; + cliprdr_context->ClientFormatDataResponse = cliprdr_client_format_data_response; + cliprdr_context->ClientFileContentsRequest = cliprdr_client_file_contents_request; + cliprdr_context->ClientFileContentsResponse = cliprdr_client_file_contents_response; + cliprdr_context->custom = clipboard_rdp; + + if (relieve_filename_restriction) + { + g_message ("[RDP.CLIPRDR] Relieving CLIPRDR filename restriction"); + clipboard_rdp->delegate->IsFileNameComponentValid = is_valid_unix_filename; + } + + if (cliprdr_context->Start (cliprdr_context)) + { + g_warning ("[RDP.CLIPRDR] Failed to open CLIPRDR channel"); + g_clear_pointer (&clipboard_rdp->cliprdr_context, + cliprdr_server_context_free); + return NULL; + } + + return g_steal_pointer (&clipboard_rdp); +} + +static gboolean +clear_format_data (gpointer key, + gpointer value, + gpointer user_data) +{ + FormatData *format_data = value; + + g_free (format_data->data); + g_free (format_data); + + return TRUE; +} + +static gboolean +clear_client_requests (gpointer key, + gpointer value, + gpointer user_data) +{ + GList *serials = value; + + g_list_free (serials); + + return TRUE; +} + +static void +grd_clipboard_rdp_dispose (GObject *object) +{ + GrdClipboardRdp *clipboard_rdp = GRD_CLIPBOARD_RDP (object); + GrdClipboard *clipboard = GRD_CLIPBOARD (clipboard_rdp); + + g_mutex_lock (&clipboard_rdp->completion_mutex); + clipboard_rdp->protocol_stopped = TRUE; + g_cond_signal (&clipboard_rdp->completion_cond); + g_mutex_unlock (&clipboard_rdp->completion_mutex); + + if (clipboard_rdp->cliprdr_context) + { + clipboard_rdp->cliprdr_context->Stop (clipboard_rdp->cliprdr_context); + grd_clipboard_disable_clipboard (clipboard); + } + + g_clear_pointer (&clipboard_rdp->current_client_request, g_free); + if (clipboard_rdp->format_data_response) + { + g_free ((uint8_t *) clipboard_rdp->format_data_response->requestedFormatData); + g_clear_pointer (&clipboard_rdp->format_data_response, g_free); + } + + if (clipboard_rdp->clipboard_retrieval_id) + g_clear_pointer (&clipboard_rdp->clipboard_retrieval_context.entry, g_free); + + g_clear_pointer (&clipboard_rdp->format_data_request_context, g_free); + g_clear_pointer (&clipboard_rdp->queued_server_formats, g_list_free); + g_clear_pointer (&clipboard_rdp->pending_server_formats, g_list_free); + + if (clipboard_rdp->ordered_client_requests) + { + g_queue_free_full (clipboard_rdp->ordered_client_requests, g_free); + clipboard_rdp->ordered_client_requests = NULL; + } + g_hash_table_foreach_remove (clipboard_rdp->pending_client_requests, + clear_client_requests, + NULL); + g_hash_table_foreach_remove (clipboard_rdp->format_data_cache, + clear_format_data, + NULL); + + g_clear_object (&clipboard_rdp->rdp_fuse_clipboard); + rmdir (clipboard_rdp->fuse_mount_path); + g_clear_pointer (&clipboard_rdp->fuse_mount_path, g_free); + + g_assert (g_hash_table_size (clipboard_rdp->pending_client_requests) == 0); + g_assert (g_hash_table_size (clipboard_rdp->format_data_cache) == 0); + g_clear_pointer (&clipboard_rdp->pending_client_requests, g_hash_table_unref); + g_clear_pointer (&clipboard_rdp->format_data_cache, g_hash_table_unref); + g_clear_pointer (&clipboard_rdp->clip_data_table, g_hash_table_destroy); + g_clear_pointer (&clipboard_rdp->serial_entry_table, g_hash_table_destroy); + g_clear_pointer (&clipboard_rdp->allowed_server_formats, g_hash_table_destroy); + g_clear_pointer (&clipboard_rdp->system, ClipboardDestroy); + g_clear_pointer (&clipboard_rdp->cliprdr_context, cliprdr_server_context_free); + + g_clear_handle_id (&clipboard_rdp->pending_server_formats_drop_id, g_source_remove); + g_clear_handle_id (&clipboard_rdp->client_request_abort_id, g_source_remove); + g_clear_handle_id (&clipboard_rdp->clipboard_retrieval_id, g_source_remove); + g_clear_handle_id (&clipboard_rdp->clipboard_destruction_id, g_source_remove); + g_clear_handle_id (&clipboard_rdp->server_format_list_update_id, g_source_remove); + g_clear_handle_id (&clipboard_rdp->server_format_data_request_id, g_source_remove); + g_clear_handle_id (&clipboard_rdp->client_format_list_response_id, g_source_remove); + g_clear_handle_id (&clipboard_rdp->client_format_data_response_id, g_source_remove); + + G_OBJECT_CLASS (grd_clipboard_rdp_parent_class)->dispose (object); +} + +static void +grd_clipboard_rdp_finalize (GObject *object) +{ + GrdClipboardRdp *clipboard_rdp = GRD_CLIPBOARD_RDP (object); + + g_cond_clear (&clipboard_rdp->completion_cond); + g_mutex_clear (&clipboard_rdp->completion_mutex); + g_mutex_clear (&clipboard_rdp->server_format_data_request_mutex); + g_mutex_clear (&clipboard_rdp->client_format_list_response_mutex); + g_mutex_clear (&clipboard_rdp->server_format_list_update_mutex); + g_mutex_clear (&clipboard_rdp->clip_data_entry_mutex); + g_mutex_clear (&clipboard_rdp->client_request_mutex); + + G_OBJECT_CLASS (grd_clipboard_rdp_parent_class)->finalize (object); +} + +static void +clip_data_entry_free (gpointer data) +{ + ClipDataEntry *entry = data; + + g_debug ("[RDP.CLIPRDR] Freeing ClipDataEntry with id %u and serial %lu. " + "ClipDataEntry is independent: %s", entry->id, entry->serial, + entry->is_independent ? "true" : "false"); + + if (entry->is_independent) + g_clear_pointer (&entry->system, ClipboardDestroy); + + g_free (entry); +} + +static void +grd_clipboard_rdp_init (GrdClipboardRdp *clipboard_rdp) +{ + const char *grd_path = "/gnome-remote-desktop"; + const char *cliprdr_template = "/cliprdr-XXXXXX"; + g_autofree char *base_path = NULL; + g_autofree char *template_path = NULL; + + base_path = g_strdup_printf ("%s%s", g_get_user_runtime_dir (), grd_path); + template_path = g_strdup_printf ("%s%s", base_path, cliprdr_template); + + if (g_access (base_path, F_OK)) + { + if (mkdir (base_path, 0700)) + { + g_error ("Failed to create base runtime directory for " + "gnome-remote-desktop: %s", g_strerror (errno)); + } + } + if (!mkdtemp (template_path)) + { + g_error ("Failed to create clipboard file directory %s: %s", + template_path, g_strerror (errno)); + } + + clipboard_rdp->system = ClipboardCreate (); + clipboard_rdp->delegate = ClipboardGetDelegate (clipboard_rdp->system); + clipboard_rdp->delegate->ClipboardFileSizeSuccess = cliprdr_file_size_success; + clipboard_rdp->delegate->ClipboardFileSizeFailure = cliprdr_file_size_failure; + clipboard_rdp->delegate->ClipboardFileRangeSuccess = cliprdr_file_range_success; + clipboard_rdp->delegate->ClipboardFileRangeFailure = cliprdr_file_range_failure; + clipboard_rdp->delegate->basePath = NULL; + clipboard_rdp->delegate->custom = clipboard_rdp; + + clipboard_rdp->completed_clip_data_entry = TRUE; + clipboard_rdp->completed_format_list = TRUE; + clipboard_rdp->completed_format_data_request = TRUE; + + clipboard_rdp->allowed_server_formats = g_hash_table_new (NULL, NULL); + clipboard_rdp->serial_entry_table = g_hash_table_new_full (NULL, NULL, NULL, + clip_data_entry_free); + clipboard_rdp->clip_data_table = g_hash_table_new (NULL, NULL); + clipboard_rdp->format_data_cache = g_hash_table_new (NULL, NULL); + clipboard_rdp->pending_client_requests = g_hash_table_new (NULL, NULL); + clipboard_rdp->ordered_client_requests = g_queue_new (); + + g_mutex_init (&clipboard_rdp->client_request_mutex); + g_mutex_init (&clipboard_rdp->clip_data_entry_mutex); + g_mutex_init (&clipboard_rdp->server_format_list_update_mutex); + g_mutex_init (&clipboard_rdp->client_format_list_response_mutex); + g_mutex_init (&clipboard_rdp->server_format_data_request_mutex); + g_mutex_init (&clipboard_rdp->completion_mutex); + g_cond_init (&clipboard_rdp->completion_cond); + + clipboard_rdp->fuse_mount_path = g_steal_pointer (&template_path); + clipboard_rdp->rdp_fuse_clipboard = + grd_rdp_fuse_clipboard_new (clipboard_rdp, clipboard_rdp->fuse_mount_path); +} + +static void +grd_clipboard_rdp_class_init (GrdClipboardRdpClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GrdClipboardClass *clipboard_class = GRD_CLIPBOARD_CLASS (klass); + + object_class->dispose = grd_clipboard_rdp_dispose; + object_class->finalize = grd_clipboard_rdp_finalize; + + clipboard_class->update_client_mime_type_list = + grd_clipboard_rdp_update_client_mime_type_list; + clipboard_class->request_client_content_for_mime_type = + grd_clipboard_rdp_request_client_content_for_mime_type; + clipboard_class->submit_requested_server_content = + grd_clipboard_rdp_submit_requested_server_content; +} diff --git a/grd-clipboard-rdp.h b/grd-clipboard-rdp.h new file mode 100644 index 0000000..bfe3adf --- /dev/null +++ b/grd-clipboard-rdp.h @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2020 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include + +#include "grd-clipboard.h" + +#define GRD_TYPE_CLIPBOARD_RDP (grd_clipboard_rdp_get_type ()) +G_DECLARE_FINAL_TYPE (GrdClipboardRdp, + grd_clipboard_rdp, + GRD, CLIPBOARD_RDP, + GrdClipboard) + +GrdClipboardRdp *grd_clipboard_rdp_new (GrdSessionRdp *session_rdp, + HANDLE vcm, + gboolean relieve_filename_restriction); + +void grd_clipboard_rdp_lock_remote_clipboard_data (GrdClipboardRdp *clipboard_rdp, + uint32_t clip_data_id); + +void grd_clipboard_rdp_unlock_remote_clipboard_data (GrdClipboardRdp *clipboard_rdp, + uint32_t clip_data_id); + +void grd_clipboard_rdp_request_remote_file_size_async (GrdClipboardRdp *clipboard_rdp, + uint32_t stream_id, + uint32_t list_index, + gboolean has_clip_data_id, + uint32_t clip_data_id); + +void grd_clipboard_rdp_request_remote_file_range_async (GrdClipboardRdp *clipboard_rdp, + uint32_t stream_id, + uint32_t list_index, + uint64_t offset, + uint32_t requested_size, + gboolean has_clip_data_id, + uint32_t clip_data_id); diff --git a/grd-clipboard-vnc.c b/grd-clipboard-vnc.c new file mode 100644 index 0000000..c6ae813 --- /dev/null +++ b/grd-clipboard-vnc.c @@ -0,0 +1,208 @@ +/* + * Copyright (C) 2020-2021 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-clipboard-vnc.h" + +#include "grd-session-vnc.h" + +struct _GrdClipboardVnc +{ + GrdClipboard parent; + + GrdSessionVnc *session_vnc; + + char *clipboard_utf8_string; +}; + +G_DEFINE_TYPE (GrdClipboardVnc, grd_clipboard_vnc, GRD_TYPE_CLIPBOARD) + +static void +grd_clipboard_vnc_update_client_mime_type_list (GrdClipboard *clipboard, + GList *mime_type_list) +{ + GrdClipboardVnc *clipboard_vnc = GRD_CLIPBOARD_VNC (clipboard); + gboolean found_utf8_string = FALSE; + GrdMimeType mime_type; + GList *l; + + for (l = mime_type_list; l && !found_utf8_string; l = l->next) + { + mime_type = GPOINTER_TO_UINT (l->data); + switch (mime_type) + { + case GRD_MIME_TYPE_TEXT_PLAIN: + break; + case GRD_MIME_TYPE_TEXT_PLAIN_UTF8: + case GRD_MIME_TYPE_TEXT_UTF8_STRING: + found_utf8_string = TRUE; + break; + case GRD_MIME_TYPE_TEXT_HTML: + case GRD_MIME_TYPE_IMAGE_BMP: + case GRD_MIME_TYPE_IMAGE_TIFF: + case GRD_MIME_TYPE_IMAGE_GIF: + case GRD_MIME_TYPE_IMAGE_JPEG: + case GRD_MIME_TYPE_IMAGE_PNG: + case GRD_MIME_TYPE_TEXT_URILIST: + case GRD_MIME_TYPE_XS_GNOME_COPIED_FILES: + break; + default: + g_assert_not_reached (); + } + } + + if (found_utf8_string) + { + g_clear_pointer (&clipboard_vnc->clipboard_utf8_string, g_free); + + grd_clipboard_request_server_content_for_mime_type_async (clipboard, + mime_type); + } + + g_list_free (mime_type_list); +} + +static void +grd_clipboard_vnc_request_client_content_for_mime_type (GrdClipboard *clipboard, + GrdMimeTypeTable *mime_type_table, + unsigned int serial) +{ + GrdClipboardVnc *clipboard_vnc = GRD_CLIPBOARD_VNC (clipboard); + uint32_t size; + + size = strlen (clipboard_vnc->clipboard_utf8_string); + grd_clipboard_submit_client_content_for_mime_type ( + clipboard, serial, (uint8_t *) clipboard_vnc->clipboard_utf8_string, size); +} + +static void +grd_clipboard_vnc_submit_requested_server_content (GrdClipboard *clipboard, + uint8_t *src_data, + uint32_t src_size) +{ + GrdClipboardVnc *clipboard_vnc = GRD_CLIPBOARD_VNC (clipboard); + g_autoptr (GError) error = NULL; + char *dst_data; + + if (!src_data) + return; + + dst_data = g_convert ((char *) src_data, src_size, + "iso8859-1", "utf-8", + NULL, NULL, &error); + if (!dst_data) + { + g_warning ("[VNC.Clipboard] Failed to convert clipboard content: %s", + error->message); + g_free (src_data); + return; + } + + grd_session_vnc_set_client_clipboard_text (clipboard_vnc->session_vnc, + dst_data, strlen (dst_data)); + + g_free (src_data); + g_free (dst_data); +} + +void +grd_clipboard_vnc_maybe_enable_clipboard (GrdClipboardVnc *clipboard_vnc) +{ + grd_clipboard_maybe_enable_clipboard (GRD_CLIPBOARD (clipboard_vnc)); +} + +void +grd_clipboard_vnc_set_clipboard_text (GrdClipboardVnc *clipboard_vnc, + char *text, + int text_length) +{ + GrdClipboard *clipboard = GRD_CLIPBOARD (clipboard_vnc); + g_autoptr (GError) error = NULL; + GrdMimeTypeTable *mime_type_table; + GList *mime_type_tables = NULL; + + g_clear_pointer (&clipboard_vnc->clipboard_utf8_string, g_free); + + clipboard_vnc->clipboard_utf8_string = g_convert (text, text_length, + "utf-8", "iso8859-1", + NULL, NULL, &error); + if (!clipboard_vnc->clipboard_utf8_string) + { + g_warning ("[VNC.Clipboard] Failed to convert clipboard content: %s", + error->message); + return; + } + + mime_type_table = g_malloc0 (sizeof (GrdMimeTypeTable)); + mime_type_table->mime_type = GRD_MIME_TYPE_TEXT_PLAIN_UTF8; + mime_type_tables = g_list_append (mime_type_tables, mime_type_table); + + mime_type_table = g_malloc0 (sizeof (GrdMimeTypeTable)); + mime_type_table->mime_type = GRD_MIME_TYPE_TEXT_UTF8_STRING; + mime_type_tables = g_list_append (mime_type_tables, mime_type_table); + + grd_clipboard_update_server_mime_type_list (clipboard, mime_type_tables); +} + +GrdClipboardVnc * +grd_clipboard_vnc_new (GrdSessionVnc *session_vnc) +{ + GrdClipboardVnc *clipboard_vnc; + + clipboard_vnc = g_object_new (GRD_TYPE_CLIPBOARD_VNC, NULL); + clipboard_vnc->session_vnc = session_vnc; + + grd_clipboard_initialize (GRD_CLIPBOARD (clipboard_vnc), + GRD_SESSION (session_vnc)); + + return clipboard_vnc; +} + +static void +grd_clipboard_vnc_dispose (GObject *object) +{ + GrdClipboardVnc *clipboard_vnc = GRD_CLIPBOARD_VNC (object); + + grd_clipboard_disable_clipboard (GRD_CLIPBOARD (clipboard_vnc)); + g_clear_pointer (&clipboard_vnc->clipboard_utf8_string, g_free); + + G_OBJECT_CLASS (grd_clipboard_vnc_parent_class)->dispose (object); +} + +static void +grd_clipboard_vnc_init (GrdClipboardVnc *clipboard_vnc) +{ +} + +static void +grd_clipboard_vnc_class_init (GrdClipboardVncClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GrdClipboardClass *clipboard_class = GRD_CLIPBOARD_CLASS (klass); + + object_class->dispose = grd_clipboard_vnc_dispose; + + clipboard_class->update_client_mime_type_list = + grd_clipboard_vnc_update_client_mime_type_list; + clipboard_class->request_client_content_for_mime_type = + grd_clipboard_vnc_request_client_content_for_mime_type; + clipboard_class->submit_requested_server_content = + grd_clipboard_vnc_submit_requested_server_content; +} diff --git a/grd-clipboard-vnc.h b/grd-clipboard-vnc.h new file mode 100644 index 0000000..ceb068a --- /dev/null +++ b/grd-clipboard-vnc.h @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2020 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include "grd-clipboard.h" + +#define GRD_TYPE_CLIPBOARD_VNC (grd_clipboard_vnc_get_type ()) +G_DECLARE_FINAL_TYPE (GrdClipboardVnc, + grd_clipboard_vnc, + GRD, CLIPBOARD_VNC, + GrdClipboard) + +GrdClipboardVnc *grd_clipboard_vnc_new (GrdSessionVnc *session_vnc); + +void grd_clipboard_vnc_maybe_enable_clipboard (GrdClipboardVnc *clipboard_vnc); + +void grd_clipboard_vnc_set_clipboard_text (GrdClipboardVnc *clipboard_vnc, + char *text, + int text_length); diff --git a/grd-clipboard.c b/grd-clipboard.c new file mode 100644 index 0000000..e73ddaf --- /dev/null +++ b/grd-clipboard.c @@ -0,0 +1,497 @@ +/* + * Copyright (C) 2020-2021 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-clipboard.h" + +#include +#include + +#include "grd-session.h" + +#define MAX_READ_TIME_MS 4000 + +typedef struct _ReadMimeTypeContentContext +{ + GrdClipboard *clipboard; + int fd; + GCancellable *cancellable; +} ReadMimeTypeContentContext; + +typedef struct _ReadMimeTypeContentResult +{ + uint8_t *data; + uint32_t size; +} ReadMimeTypeContentResult; + +typedef struct _GrdClipboardPrivate +{ + GrdSession *session; + + gboolean enabled; + + GHashTable *client_mime_type_tables; + + ReadMimeTypeContentResult *read_result; + GCancellable *read_cancellable; + unsigned int abort_read_source_id; + gboolean has_pending_read_operation; + + GCond pending_read_cond; + GMutex pending_read_mutex; +} GrdClipboardPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (GrdClipboard, grd_clipboard, G_TYPE_OBJECT) + +static void +handle_read_result (GrdClipboard *clipboard, + ReadMimeTypeContentResult *read_result) +{ + GrdClipboardClass *klass = GRD_CLIPBOARD_GET_CLASS (clipboard); + GrdClipboardPrivate *priv = grd_clipboard_get_instance_private (clipboard); + + priv->has_pending_read_operation = FALSE; + + /* Discard the read_result, if the clipboard is already disabled. */ + if (!priv->enabled) + return; + + if (read_result->data) + g_debug ("Clipboard[SelectionRead]: Request successful"); + else + g_debug ("Clipboard[SelectionRead]: Request failed"); + + klass->submit_requested_server_content (clipboard, read_result->data, + read_result->size); +} + +static void +flush_pending_read_result (GrdClipboard *clipboard) +{ + GrdClipboardPrivate *priv = grd_clipboard_get_instance_private (clipboard); + ReadMimeTypeContentResult *read_result; + + g_mutex_lock (&priv->pending_read_mutex); + while (!priv->read_result) + g_cond_wait (&priv->pending_read_cond, &priv->pending_read_mutex); + g_mutex_unlock (&priv->pending_read_mutex); + + read_result = g_steal_pointer (&priv->read_result); + + handle_read_result (clipboard, read_result); + g_free (read_result); +} + +static void +abort_current_read_operation (GrdClipboard *clipboard) +{ + GrdClipboardPrivate *priv = grd_clipboard_get_instance_private (clipboard); + + if (!priv->has_pending_read_operation) + return; + + g_debug ("Clipboard[SelectionRead]: Aborting current read operation"); + g_cancellable_cancel (priv->read_cancellable); + + g_clear_object (&priv->read_cancellable); + g_clear_handle_id (&priv->abort_read_source_id, g_source_remove); + + flush_pending_read_result (clipboard); +} + +void +grd_clipboard_update_server_mime_type_list (GrdClipboard *clipboard, + GList *mime_type_tables) +{ + GrdClipboardPrivate *priv = grd_clipboard_get_instance_private (clipboard); + GList *l; + + g_debug ("Clipboard[SetSelection]: Updating servers clipboard"); + for (l = mime_type_tables; l; l = l->next) + { + GrdMimeTypeTable *mime_type_table = l->data; + GrdMimeType mime_type; + + mime_type = mime_type_table->mime_type; + g_debug ("Clipboard[SetSelection]: Update contains mime type %s", + grd_mime_type_to_string (mime_type)); + + g_hash_table_insert (priv->client_mime_type_tables, + GUINT_TO_POINTER (mime_type), mime_type_table); + } + + if (!priv->enabled) + { + g_debug ("Clipboard[EnableClipboard]: Enabling clipboard"); + priv->enabled = grd_session_enable_clipboard (priv->session, + clipboard, mime_type_tables); + if (priv->enabled) + g_debug ("Clipboard[EnableClipboard]: Clipboard enabled"); + else + g_debug ("Clipboard[EnableClipboard]: Clipboard could not be enabled"); + } + else + { + abort_current_read_operation (clipboard); + + if (mime_type_tables) + grd_session_set_selection (priv->session, mime_type_tables); + } + g_debug ("Clipboard[SetSelection]: Update complete"); + + g_list_free (mime_type_tables); +} + +static void +async_read_operation_complete (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GrdClipboard *clipboard = user_data; + GrdClipboardPrivate *priv; + ReadMimeTypeContentContext *read_context = + g_task_get_task_data (G_TASK (result)); + ReadMimeTypeContentResult *read_result; + + if (g_cancellable_is_cancelled (read_context->cancellable)) + return; + + priv = grd_clipboard_get_instance_private (clipboard); + g_assert (priv->has_pending_read_operation); + + g_clear_object (&priv->read_cancellable); + g_clear_handle_id (&priv->abort_read_source_id, g_source_remove); + + read_result = g_steal_pointer (&priv->read_result); + + handle_read_result (clipboard, read_result); + g_free (read_result); +} + +static void +clear_read_context (gpointer data) +{ + ReadMimeTypeContentContext *read_context = data; + + g_object_unref (read_context->cancellable); + + g_free (data); +} + +static void +read_mime_type_content_in_thread (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + ReadMimeTypeContentContext *read_context = task_data; + GrdClipboard *clipboard = read_context->clipboard; + GrdClipboardPrivate *priv = grd_clipboard_get_instance_private (clipboard); + ReadMimeTypeContentResult *read_result; + GInputStream *input_stream; + GArray *data; + gboolean success = FALSE; + g_autoptr (GError) error = NULL; + + input_stream = g_unix_input_stream_new (read_context->fd, TRUE); + data = g_array_new (FALSE, TRUE, sizeof (uint8_t)); + + while (TRUE) + { + int len; + uint8_t buffer[1024]; + + len = g_input_stream_read (input_stream, buffer, G_N_ELEMENTS (buffer), + read_context->cancellable, &error); + if (len < 0) + { + g_warning ("Clipboard[SelectionRead]: Failed to read mime type " + "content: %s", error->message); + break; + } + else if (len == 0) + { + success = TRUE; + break; + } + else + { + g_array_append_vals (data, buffer, len); + } + } + + read_result = g_malloc0 (sizeof (ReadMimeTypeContentResult)); + if (success && data->len > 0) + { + read_result->size = data->len; + read_result->data = (uint8_t *) g_array_free (data, FALSE); + } + else + { + g_array_free (data, TRUE); + } + + g_object_unref (input_stream); + + g_mutex_lock (&priv->pending_read_mutex); + priv->read_result = read_result; + g_cond_signal (&priv->pending_read_cond); + g_mutex_unlock (&priv->pending_read_mutex); + + g_task_return_pointer (task, NULL, NULL); +} + +static gboolean +abort_mime_type_content_read (gpointer user_data) +{ + GrdClipboard *clipboard = user_data; + GrdClipboardPrivate *priv = grd_clipboard_get_instance_private (clipboard); + + g_debug ("Clipboard[SelectionRead]: Aborting current read operation " + "(Timeout reached)"); + + g_assert (priv->has_pending_read_operation); + g_assert (priv->abort_read_source_id); + + priv->abort_read_source_id = 0; + abort_current_read_operation (clipboard); + + return G_SOURCE_REMOVE; +} + +static void +read_mime_type_content_async (GrdClipboard *clipboard, + int fd) +{ + GrdClipboardPrivate *priv = grd_clipboard_get_instance_private (clipboard); + ReadMimeTypeContentContext *read_context; + GTask *task; + + abort_current_read_operation (clipboard); + priv->read_cancellable = g_cancellable_new (); + priv->has_pending_read_operation = TRUE; + g_assert (!priv->read_result); + + read_context = g_malloc0 (sizeof (ReadMimeTypeContentContext)); + read_context->clipboard = clipboard; + read_context->fd = fd; + read_context->cancellable = g_object_ref (priv->read_cancellable); + + task = g_task_new (NULL, NULL, async_read_operation_complete, clipboard); + g_task_set_task_data (task, read_context, clear_read_context); + g_task_run_in_thread (task, read_mime_type_content_in_thread); + g_object_unref (task); + + priv->abort_read_source_id = + g_timeout_add (MAX_READ_TIME_MS, abort_mime_type_content_read, clipboard); +} + +void +grd_clipboard_request_server_content_for_mime_type_async (GrdClipboard *clipboard, + GrdMimeType mime_type) +{ + GrdClipboardClass *klass = GRD_CLIPBOARD_GET_CLASS (clipboard); + GrdClipboardPrivate *priv = grd_clipboard_get_instance_private (clipboard); + int fd; + + g_return_if_fail (klass->submit_requested_server_content); + + g_assert (priv->enabled); + + g_debug ("Clipboard[SelectionRead]: Requesting data from servers clipboard" + " (mime type: %s)", grd_mime_type_to_string (mime_type)); + fd = grd_session_selection_read (priv->session, mime_type); + if (fd == -1) + { + g_debug ("Clipboard[SelectionRead]: Request failed"); + klass->submit_requested_server_content (clipboard, NULL, 0); + + return; + } + + read_mime_type_content_async (clipboard, fd); +} + +void +grd_clipboard_initialize (GrdClipboard *clipboard, + GrdSession *session) +{ + GrdClipboardPrivate *priv = grd_clipboard_get_instance_private (clipboard); + + priv->session = session; +} + +void +grd_clipboard_maybe_enable_clipboard (GrdClipboard *clipboard) +{ + GrdClipboardPrivate *priv = grd_clipboard_get_instance_private (clipboard); + + g_debug ("Clipboard[EnableClipboard]: Enabling clipboard"); + if (priv->enabled) + { + g_debug ("Clipboard[EnableClipboard]: Clipboard already enabled"); + return; + } + + priv->enabled = grd_session_enable_clipboard (priv->session, clipboard, NULL); + if (priv->enabled) + g_debug ("Clipboard[EnableClipboard]: Clipboard enabled"); + else + g_debug ("Clipboard[EnableClipboard]: Clipboard could not be enabled"); +} + +void +grd_clipboard_disable_clipboard (GrdClipboard *clipboard) +{ + GrdClipboardPrivate *priv = grd_clipboard_get_instance_private (clipboard); + + if (!priv->enabled) + return; + + g_debug ("Clipboard[DisableClipboard]: Disabling clipboard"); + grd_session_disable_clipboard (priv->session); + priv->enabled = FALSE; +} + +void +grd_clipboard_update_client_mime_type_list (GrdClipboard *clipboard, + GList *mime_type_list) +{ + GrdClipboardClass *klass = GRD_CLIPBOARD_GET_CLASS (clipboard); + GrdClipboardPrivate *priv = grd_clipboard_get_instance_private (clipboard); + + /** + * Ensure that the response with the read mime type content is sent to the + * client first, before sending the new mime type list + */ + abort_current_read_operation (clipboard); + + g_assert (priv->enabled); + + if (!klass->update_client_mime_type_list) + return; + + g_hash_table_remove_all (priv->client_mime_type_tables); + + g_debug ("Clipboard[SelectionOwnerChanged]: Updating clients clipboard"); + klass->update_client_mime_type_list (clipboard, mime_type_list); + g_debug ("Clipboard[SelectionOwnerChanged]: Update complete"); +} + +void +grd_clipboard_submit_client_content_for_mime_type (GrdClipboard *clipboard, + unsigned int serial, + const uint8_t *data, + uint32_t size) +{ + GrdClipboardPrivate *priv = grd_clipboard_get_instance_private (clipboard); + + g_assert (priv->enabled); + + if (data && size) + g_debug ("Clipboard[SelectionTransfer]: Request for serial %u was successful", serial); + else + g_debug ("Clipboard[SelectionTransfer]: Request for serial %u failed", serial); + + grd_session_selection_write (priv->session, serial, data, size); +} + +void +grd_clipboard_request_client_content_for_mime_type (GrdClipboard *clipboard, + GrdMimeType mime_type, + unsigned int serial) +{ + GrdClipboardClass *klass = GRD_CLIPBOARD_GET_CLASS (clipboard); + GrdClipboardPrivate *priv = grd_clipboard_get_instance_private (clipboard); + GrdMimeTypeTable *mime_type_table = NULL; + + g_assert (priv->enabled); + + g_return_if_fail (klass->request_client_content_for_mime_type); + + g_debug ("Clipboard[SelectionTransfer]: Requesting data from clients clipboard" + " (mime type: %s, serial: %u)", + grd_mime_type_to_string (mime_type), serial); + mime_type_table = g_hash_table_lookup (priv->client_mime_type_tables, + GUINT_TO_POINTER (mime_type)); + if (!mime_type_table) + { + grd_clipboard_submit_client_content_for_mime_type (clipboard, serial, + NULL, 0); + return; + } + + klass->request_client_content_for_mime_type (clipboard, mime_type_table, + serial); +} + +static void +free_mime_type_table (gpointer data) +{ + GrdMimeTypeTable *mime_type_table = data; + + g_free (mime_type_table); +} + +static void +grd_clipboard_dispose (GObject *object) +{ + GrdClipboard *clipboard = GRD_CLIPBOARD (object); + GrdClipboardPrivate *priv = grd_clipboard_get_instance_private (clipboard); + + abort_current_read_operation (clipboard); + + g_clear_pointer (&priv->client_mime_type_tables, g_hash_table_destroy); + + G_OBJECT_CLASS (grd_clipboard_parent_class)->dispose (object); +} + +static void +grd_clipboard_finalize (GObject *object) +{ + GrdClipboard *clipboard = GRD_CLIPBOARD (object); + GrdClipboardPrivate *priv = grd_clipboard_get_instance_private (clipboard); + + g_mutex_clear (&priv->pending_read_mutex); + g_cond_clear (&priv->pending_read_cond); + + G_OBJECT_CLASS (grd_clipboard_parent_class)->finalize (object); +} + +static void +grd_clipboard_init (GrdClipboard *clipboard) +{ + GrdClipboardPrivate *priv = grd_clipboard_get_instance_private (clipboard); + + priv->client_mime_type_tables = g_hash_table_new_full (NULL, NULL, NULL, + free_mime_type_table); + + g_cond_init (&priv->pending_read_cond); + g_mutex_init (&priv->pending_read_mutex); +} + +static void +grd_clipboard_class_init (GrdClipboardClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = grd_clipboard_dispose; + object_class->finalize = grd_clipboard_finalize; +} diff --git a/grd-clipboard.h b/grd-clipboard.h new file mode 100644 index 0000000..d96a457 --- /dev/null +++ b/grd-clipboard.h @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2020-2021 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include +#include + +#include "grd-mime-type.h" +#include "grd-types.h" + +#define GRD_TYPE_CLIPBOARD (grd_clipboard_get_type ()) +G_DECLARE_DERIVABLE_TYPE (GrdClipboard, grd_clipboard, GRD, CLIPBOARD, GObject) + +struct _GrdClipboardClass +{ + GObjectClass parent_class; + + void (*update_client_mime_type_list) (GrdClipboard *clipboard, + GList *mime_type_list); + void (*request_client_content_for_mime_type) (GrdClipboard *clipboard, + GrdMimeTypeTable *mime_type_table, + unsigned int serial); + void (*submit_requested_server_content) (GrdClipboard *clipboard, + uint8_t *data, + uint32_t size); +}; + +void grd_clipboard_update_server_mime_type_list (GrdClipboard *clipboard, + GList *mime_type_tables); + +void grd_clipboard_request_server_content_for_mime_type_async (GrdClipboard *clipboard, + GrdMimeType mime_type); + +void grd_clipboard_initialize (GrdClipboard *clipboard, + GrdSession *session); + +void grd_clipboard_maybe_enable_clipboard (GrdClipboard *clipboard); + +void grd_clipboard_disable_clipboard (GrdClipboard *clipboard); + +void grd_clipboard_update_client_mime_type_list (GrdClipboard *clipboard, + GList *mime_type_list); + +void grd_clipboard_submit_client_content_for_mime_type (GrdClipboard *clipboard, + unsigned int serial, + const uint8_t *data, + uint32_t size); + +void grd_clipboard_request_client_content_for_mime_type (GrdClipboard *clipboard, + GrdMimeType mime_type, + unsigned int serial); diff --git a/grd-configuration.c b/grd-configuration.c new file mode 100644 index 0000000..d48b40c --- /dev/null +++ b/grd-configuration.c @@ -0,0 +1,730 @@ +/* + * Copyright (C) 2024 SUSE Software Solutions Germany GmbH + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + * Written by: + * Joan Torres + */ + +#include "config.h" + +#include +#include +#include +#include +#include + +#ifdef HAVE_RDP +#include +#endif + +#include "grd-dbus-remote-desktop.h" +#include "grd-private.h" +#include "grd-settings-system.h" +#include "grd-utils.h" + +#define GRD_CONFIGURATION_TIMEOUT_S 10 +#define GRD_SYSTEMD_SERVICE "gnome-remote-desktop.service" +#define GRD_SERVER_USER_CERT_SUBDIR "certificates" +#define GRD_CONFIGURE_SYSTEM_DAEMON_POLKIT_ACTION "org.gnome.remotedesktop.configure-system-daemon" +#define GRD_MAX_CERTIFICATE_FILE_SIZE_BYTES (50 * 1024) +#define GRD_MAX_PRIVATE_KEY_FILE_SIZE_BYTES (50 * 1024) + +typedef struct +{ + unsigned int *timeout_source_id; + GSourceFunc function; + gpointer data; +} TimeoutLocker; + +struct _GrdConfiguration +{ + GApplication parent; + + PolkitAuthority *authority; + + GrdSettingsSystem *settings; + + GrdDBusRemoteDesktopConfigurationRdpServer *configuration_rdp_server; + unsigned int own_name_source_id; + + GDBusProxy *unit_proxy; + GrdSystemdUnitActiveState unit_state; + + unsigned int timeout_source_id; + unsigned int sigint_source_id; + unsigned int sigterm_source_id; +}; + +#define GRD_TYPE_CONFIGURATION (grd_configuration_get_type ()) +G_DECLARE_FINAL_TYPE (GrdConfiguration, + grd_configuration, + GRD, CONFIGURATION, + GApplication) + +G_DEFINE_TYPE (GrdConfiguration, grd_configuration, G_TYPE_APPLICATION) + +static void +timeout_locker_free (TimeoutLocker *locker); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (TimeoutLocker, timeout_locker_free) + +#ifdef HAVE_RDP +G_DEFINE_AUTOPTR_CLEANUP_FUNC (rdpCertificate, freerdp_certificate_free) +G_DEFINE_AUTOPTR_CLEANUP_FUNC (rdpPrivateKey, freerdp_key_free) +#endif + +static TimeoutLocker * +timeout_locker_new (unsigned int *timeout_source_id, + GSourceFunc function, + gpointer data) +{ + TimeoutLocker *locker = g_new0 (TimeoutLocker, 1); + + locker->timeout_source_id = timeout_source_id; + locker->function = function; + locker->data = data; + + g_clear_handle_id (locker->timeout_source_id, g_source_remove); + + return locker; +} + +static void +timeout_locker_free (TimeoutLocker *locker) +{ + if (*locker->timeout_source_id == 0) + { + *locker->timeout_source_id = + g_timeout_add_seconds (GRD_CONFIGURATION_TIMEOUT_S, + locker->function, + locker->data); + } + + g_free (locker); +} + +static void +grd_configuration_init (GrdConfiguration *app) +{ +} + +static void +on_unit_active_state_changed (GrdConfiguration *configuration) +{ + if (!grd_systemd_unit_get_active_state (configuration->unit_proxy, + &configuration->unit_state, + NULL)) + return; + + g_object_notify (G_OBJECT (configuration->settings), "rdp-enabled"); +} + +static void +watch_grd_system_unit_active_state (GrdConfiguration *configuration) +{ + GDBusProxy *unit_proxy = NULL; + g_autoptr (GError) error = NULL; + + if (!grd_systemd_get_unit (G_BUS_TYPE_SYSTEM, + GRD_SYSTEMD_SERVICE, + &unit_proxy, + &error)) + { + g_warning ("Could not load %s: %s", + GRD_SYSTEMD_SERVICE, error->message); + return; + } + + grd_systemd_unit_get_active_state (unit_proxy, + &configuration->unit_state, + NULL); + + g_signal_connect_object (G_OBJECT (unit_proxy), + "g-properties-changed", + G_CALLBACK (on_unit_active_state_changed), + configuration, + G_CONNECT_SWAPPED); + + configuration->unit_proxy = unit_proxy; +} + +static gboolean +transform_enabled (GBinding *binding, + const GValue *from_value, + GValue *to_value, + gpointer user_data) +{ + GrdConfiguration *configuration = user_data; + gboolean enabled; + GrdSystemdUnitActiveState active_state; + + enabled = g_value_get_boolean (from_value); + active_state = configuration->unit_state; + + g_value_set_boolean (to_value, + enabled && + (active_state == GRD_SYSTEMD_UNIT_ACTIVE_STATE_ACTIVE || + active_state == GRD_SYSTEMD_UNIT_ACTIVE_STATE_ACTIVATING)); + + return TRUE; +} + +static gboolean +on_handle_enable (GrdDBusRemoteDesktopConfigurationRdpServer *configuration_rdp_server, + GDBusMethodInvocation *invocation, + GrdConfiguration *configuration) +{ + g_autoptr (GError) error = NULL; + + if (!grd_toggle_systemd_unit (GRD_RUNTIME_MODE_SYSTEM, TRUE, &error)) + { + g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Failed enabling %s: %s", + GRD_SYSTEMD_SERVICE, + error->message); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + g_object_set (G_OBJECT (configuration->settings), "rdp-enabled", TRUE, NULL); + + grd_dbus_remote_desktop_configuration_rdp_server_complete_enable ( + configuration_rdp_server, + invocation); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +on_handle_disable (GrdDBusRemoteDesktopConfigurationRdpServer *configuration_rdp_server, + GDBusMethodInvocation *invocation, + GrdConfiguration *configuration) +{ + g_autoptr (GError) error = NULL; + + if (!grd_toggle_systemd_unit (GRD_RUNTIME_MODE_SYSTEM, FALSE, &error)) + { + g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Failed disabling %s: %s", + GRD_SYSTEMD_SERVICE, + error->message); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + g_object_set (G_OBJECT (configuration->settings), "rdp-enabled", FALSE, NULL); + + grd_dbus_remote_desktop_configuration_rdp_server_complete_disable ( + configuration_rdp_server, + invocation); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +on_handle_get_credentials (GrdDBusRemoteDesktopConfigurationRdpServer *configuration_rdp_server, + GDBusMethodInvocation *invocation, + GrdConfiguration *configuration) +{ + g_autofree char *username = NULL; + g_autofree char *password = NULL; + g_autoptr (GError) error = NULL; + GVariantBuilder credentials; + + g_variant_builder_init (&credentials, G_VARIANT_TYPE ("a{sv}")); + + grd_settings_get_rdp_credentials (GRD_SETTINGS (configuration->settings), + &username, &password, + &error); + if (error) + g_warning ("[Configuration] Failed to get credentials: %s", error->message); + + if (!username) + username = g_strdup (""); + + if (!password) + password = g_strdup (""); + + g_variant_builder_add (&credentials, "{sv}", "username", + g_variant_new_string (username)); + g_variant_builder_add (&credentials, "{sv}", "password", + g_variant_new_string (password)); + + grd_dbus_remote_desktop_configuration_rdp_server_complete_get_credentials ( + configuration_rdp_server, + invocation, + g_variant_builder_end (&credentials)); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +on_handle_set_credentials (GrdDBusRemoteDesktopConfigurationRdpServer *configuration_rdp_server, + GDBusMethodInvocation *invocation, + GVariant *credentials, + GrdConfiguration *configuration) +{ + g_autofree char *old_username = NULL; + g_autofree char *old_password = NULL; + g_autofree char *username = NULL; + g_autofree char *password = NULL; + g_autoptr (GError) error = NULL; + + g_variant_lookup (credentials, "username", "s", &username); + g_variant_lookup (credentials, "password", "s", &password); + + if (!username && !password) + { + g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, + G_DBUS_ERROR_INVALID_ARGS, + "Username or password expected " + "in credentials"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + if (!username || !password) + { + grd_settings_get_rdp_credentials (GRD_SETTINGS (configuration->settings), + &old_username, &old_password, + NULL); + } + + if (!username) + username = g_steal_pointer (&old_username); + + if (!password) + password = g_steal_pointer (&old_password); + + if (!grd_settings_set_rdp_credentials (GRD_SETTINGS (configuration->settings), + username, password, + &error)) + { + g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Failed to set credentials: %s", + error->message); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + grd_dbus_remote_desktop_configuration_rdp_server_complete_set_credentials ( + configuration_rdp_server, + invocation); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +on_handle_import_certificate (GrdDBusRemoteDesktopConfigurationRdpServer *configuration_rdp_server, + GDBusMethodInvocation *invocation, + GUnixFDList *fd_list, + GVariant *certificate, + GVariant *private_key, + GrdConfiguration *configuration) +{ + g_autoptr (rdpCertificate) rdp_certificate = NULL; + g_autoptr (rdpPrivateKey) rdp_private_key = NULL; + g_autofree char *certificate_filename = NULL; + g_autofree char *key_filename = NULL; + g_autoptr (GError) error = NULL; + g_autofd int certificate_fd = -1; + g_autofd int key_fd = -1; + GFileTest fd_test_results; + int certificate_fd_index = -1; + int key_fd_index = -1; + gboolean success; + + g_variant_get (certificate, "(sh)", &certificate_filename, + &certificate_fd_index); + g_variant_get (private_key, "(sh)", &key_filename, + &key_fd_index); + + if (!G_IS_UNIX_FD_LIST (fd_list)) + { + g_set_error (&error, G_IO_ERROR, G_IO_ERROR_FAILED, "Failed to acquire " + "file descriptor for certificate and private key: " + "The sender supplied an invalid fd list"); + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + if (certificate_fd_index < 0 || + certificate_fd_index >= g_unix_fd_list_get_length (fd_list)) + { + g_set_error (&error, G_IO_ERROR, G_IO_ERROR_FAILED, "Failed to acquire " + "file descriptor for certificate: " + "The sender supplied an invalid fd index"); + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + if (key_fd_index < 0 || + key_fd_index >= g_unix_fd_list_get_length (fd_list)) + { + g_set_error (&error, G_IO_ERROR, G_IO_ERROR_FAILED, "Failed to acquire " + "file descriptor for private key: " + "The sender supplied an invalid fd index"); + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + certificate_fd = g_unix_fd_list_get (fd_list, certificate_fd_index, &error); + if (certificate_fd == -1) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + success = grd_test_fd (certificate_fd, GRD_MAX_CERTIFICATE_FILE_SIZE_BYTES, + &fd_test_results, &error); + if (!success) + { + g_prefix_error (&error, "Could not inspect certificate file descriptor"); + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + if (!(fd_test_results & G_FILE_TEST_IS_REGULAR)) + { + g_dbus_method_invocation_return_error (invocation, + G_IO_ERROR, + G_IO_ERROR_NOT_REGULAR_FILE, + "Invalid certificate file"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + key_fd = g_unix_fd_list_get (fd_list, key_fd_index, &error); + if (key_fd == -1) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + success = grd_test_fd (key_fd, GRD_MAX_PRIVATE_KEY_FILE_SIZE_BYTES, + &fd_test_results, &error); + if (!success) + { + g_prefix_error (&error, "Could not inspect private key file descriptor"); + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + if (!(fd_test_results & G_FILE_TEST_IS_REGULAR)) + { + g_dbus_method_invocation_return_error (invocation, + G_IO_ERROR, + G_IO_ERROR_NOT_REGULAR_FILE, + "Invalid private key file"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + grd_rewrite_path_to_user_data_dir (&certificate_filename, + GRD_SERVER_USER_CERT_SUBDIR, + "rdp-tls.crt"); + if (!grd_write_fd_to_file (certificate_fd, certificate_filename, + NULL, &error)) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + if (certificate_filename) + rdp_certificate = freerdp_certificate_new_from_file (certificate_filename); + + if (!rdp_certificate) + { + g_dbus_method_invocation_return_error (invocation, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + "Invalid certificate"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + grd_rewrite_path_to_user_data_dir (&key_filename, + GRD_SERVER_USER_CERT_SUBDIR, + "rdp-tls.key"); + if (!grd_write_fd_to_file (key_fd, key_filename, NULL, &error)) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + if (key_filename) + rdp_private_key = freerdp_key_new_from_file (key_filename); + + if (!rdp_private_key) + { + g_dbus_method_invocation_return_error (invocation, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + "Invalid private key"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + g_object_set (configuration->settings, + "rdp-server-cert-path", certificate_filename, + "rdp-server-key-path", key_filename, + NULL); + + grd_dbus_remote_desktop_configuration_rdp_server_complete_import_certificate ( + configuration_rdp_server, + invocation, + fd_list); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +terminate (gpointer user_data) +{ + GrdConfiguration *configuration = user_data; + + g_clear_handle_id (&configuration->timeout_source_id, g_source_remove); + g_clear_handle_id (&configuration->sigint_source_id, g_source_remove); + g_clear_handle_id (&configuration->sigterm_source_id, g_source_remove); + + g_application_release (G_APPLICATION (configuration)); + + return G_SOURCE_REMOVE; +} + +static gboolean +ensure_polkit_authority (GrdConfiguration *configuration, + GError **error) +{ + if (configuration->authority) + return TRUE; + + configuration->authority = polkit_authority_get_sync (NULL, error); + + return configuration->authority != NULL; +} + +static gboolean +on_authorize_method (GrdDBusRemoteDesktopConfigurationRdpServer *configuration_rdp_server, + GDBusMethodInvocation *invocation, + GrdConfiguration *configuration) +{ + g_autoptr (PolkitAuthorizationResult) result = NULL; + g_autoptr (PolkitSubject) subject = NULL; + g_autoptr (TimeoutLocker) locker = NULL; + PolkitCheckAuthorizationFlags flags; + g_autoptr (GError) error = NULL; + const char *sender = NULL; + const char *action = NULL; + + locker = timeout_locker_new (&configuration->timeout_source_id, terminate, configuration); + + if (!ensure_polkit_authority (configuration, &error)) + { + g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Couldn't get polkit authority: %s", + error->message); + return FALSE; + } + + sender = g_dbus_method_invocation_get_sender (invocation); + subject = polkit_system_bus_name_new (sender); + action = GRD_CONFIGURE_SYSTEM_DAEMON_POLKIT_ACTION; + flags = POLKIT_CHECK_AUTHORIZATION_FLAGS_ALLOW_USER_INTERACTION; + result = polkit_authority_check_authorization_sync (configuration->authority, + subject, action, + NULL, flags, NULL, + &error); + if (!result) + { + g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Failed to check authorization: %s", + error->message); + return FALSE; + } + + if (!polkit_authorization_result_get_is_authorized (result)) + { + g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Not authorized for action %s", + action); + return FALSE; + } + + return TRUE; +} + +static void +on_bus_acquired (GDBusConnection *connection, + const char *name, + gpointer user_data) +{ + GrdConfiguration *configuration = user_data; + + g_debug ("[Configuration] Now on system bus"); + + g_dbus_interface_skeleton_export ( + G_DBUS_INTERFACE_SKELETON (configuration->configuration_rdp_server), + connection, + REMOTE_DESKTOP_CONFIGURATION_OBJECT_PATH, + NULL); +} + +static void +on_name_acquired (GDBusConnection *connection, + const char *name, + gpointer user_data) +{ + g_debug ("[Configuration] Owned %s name", name); +} + +static void +on_name_lost (GDBusConnection *connection, + const char *name, + gpointer user_data) +{ + g_debug ("[Configuration] Lost owned %s name", name); +} + +static void +register_signals (GrdConfiguration *configuration) +{ + configuration->timeout_source_id = + g_timeout_add_seconds (GRD_CONFIGURATION_TIMEOUT_S, + terminate, + configuration); + configuration->sigint_source_id = g_unix_signal_add (SIGINT, + terminate, + configuration); + configuration->sigterm_source_id = g_unix_signal_add (SIGTERM, + terminate, + configuration); +} + +static void +grd_configuration_startup (GApplication *application) +{ + GrdConfiguration *configuration = GRD_CONFIGURATION (application); + + configuration->settings = grd_settings_system_new (); + grd_settings_system_use_local_state (configuration->settings); + + configuration->configuration_rdp_server = + grd_dbus_remote_desktop_configuration_rdp_server_skeleton_new (); + + watch_grd_system_unit_active_state (configuration); + + g_object_bind_property_full (configuration->settings, "rdp-enabled", + configuration->configuration_rdp_server, "enabled", + G_BINDING_SYNC_CREATE, + transform_enabled, + NULL, + configuration, + NULL); + g_object_bind_property (configuration->settings, "rdp-port", + configuration->configuration_rdp_server, "port", + G_BINDING_SYNC_CREATE); + g_object_bind_property (configuration->settings, "rdp-server-cert-path", + configuration->configuration_rdp_server, "tls-cert", + G_BINDING_SYNC_CREATE); + g_object_bind_property (configuration->settings, "rdp-server-fingerprint", + configuration->configuration_rdp_server, "tls-fingerprint", + G_BINDING_SYNC_CREATE); + g_object_bind_property (configuration->settings, "rdp-server-key-path", + configuration->configuration_rdp_server, "tls-key", + G_BINDING_SYNC_CREATE); + g_object_bind_property (configuration->settings, "rdp-auth-methods", + configuration->configuration_rdp_server, "auth-methods", + G_BINDING_SYNC_CREATE); + g_signal_connect_object (configuration->configuration_rdp_server, "handle-enable", + G_CALLBACK (on_handle_enable), + configuration, 0); + g_signal_connect_object (configuration->configuration_rdp_server, "handle-disable", + G_CALLBACK (on_handle_disable), + configuration, 0); + g_signal_connect_object (configuration->configuration_rdp_server, "handle-get-credentials", + G_CALLBACK (on_handle_get_credentials), + configuration, 0); + g_signal_connect_object (configuration->configuration_rdp_server, "handle-set-credentials", + G_CALLBACK (on_handle_set_credentials), + configuration, 0); + g_signal_connect_object (configuration->configuration_rdp_server, "handle-import-certificate", + G_CALLBACK (on_handle_import_certificate), + configuration, 0); + g_signal_connect_object (configuration->configuration_rdp_server, "g-authorize-method", + G_CALLBACK (on_authorize_method), + configuration, 0); + + configuration->own_name_source_id = + g_bus_own_name (G_BUS_TYPE_SYSTEM, + REMOTE_DESKTOP_CONFIGURATION_BUS_NAME, + G_BUS_NAME_OWNER_FLAGS_NONE, + on_bus_acquired, + on_name_acquired, + on_name_lost, + configuration, NULL); + + register_signals (configuration); + + g_application_hold (application); + + G_APPLICATION_CLASS (grd_configuration_parent_class)->startup (application); +} + +static void +grd_configuration_shutdown (GApplication *application) +{ + GrdConfiguration *configuration = GRD_CONFIGURATION (application); + + g_clear_object (&configuration->authority); + + g_clear_object (&configuration->settings); + g_clear_object (&configuration->unit_proxy); + + g_dbus_interface_skeleton_unexport ( + G_DBUS_INTERFACE_SKELETON (configuration->configuration_rdp_server)); + g_clear_object (&configuration->configuration_rdp_server); + + g_clear_handle_id (&configuration->own_name_source_id, g_bus_unown_name); + + g_clear_handle_id (&configuration->timeout_source_id, g_source_remove); + g_clear_handle_id (&configuration->sigint_source_id, g_source_remove); + g_clear_handle_id (&configuration->sigterm_source_id, g_source_remove); + + G_APPLICATION_CLASS (grd_configuration_parent_class)->shutdown (application); +} + +static void +grd_configuration_class_init (GrdConfigurationClass *klass) +{ + GApplicationClass *g_application_class = G_APPLICATION_CLASS (klass); + + g_application_class->startup = grd_configuration_startup; + g_application_class->shutdown = grd_configuration_shutdown; +} + +int +main (int argc, + char **argv) +{ + g_autoptr (GApplication) app = NULL; + + app = g_object_new (grd_configuration_get_type (), + "application-id", REMOTE_DESKTOP_CONFIGURATION_BUS_NAME, + "flags", G_APPLICATION_IS_SERVICE, + NULL); + + return g_application_run (app, argc, argv); +} diff --git a/grd-context.c b/grd-context.c new file mode 100644 index 0000000..31d73c9 --- /dev/null +++ b/grd-context.c @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2015 Red Hat Inc. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + * Written by: + * Jonas Ådahl + */ + +#include "config.h" + +#include "grd-context.h" + +#include "grd-egl-thread.h" +#include "grd-settings-handover.h" +#include "grd-settings-headless.h" +#include "grd-settings-system.h" +#include "grd-settings-user.h" + +#include "grd-dbus-mutter-remote-desktop.h" +#include "grd-dbus-mutter-screen-cast.h" + +struct _GrdContext +{ + GObject parent; + + GrdDBusMutterRemoteDesktop *mutter_remote_desktop_proxy; + GrdDBusMutterScreenCast *mutter_screen_cast_proxy; + + GrdEglThread *egl_thread; + + GrdSettings *settings; + + GrdRuntimeMode runtime_mode; + + GrdDBusRemoteDesktopRdpServer *rdp_server_iface; + GrdDBusRemoteDesktopVncServer *vnc_server_iface; +}; + +G_DEFINE_TYPE (GrdContext, grd_context, G_TYPE_OBJECT) + +GrdDBusMutterRemoteDesktop * +grd_context_get_mutter_remote_desktop_proxy (GrdContext *context) +{ + return context->mutter_remote_desktop_proxy; +} + +GrdDBusMutterScreenCast * +grd_context_get_mutter_screen_cast_proxy (GrdContext *context) +{ + return context->mutter_screen_cast_proxy; +} + +GrdDBusRemoteDesktopRdpServer * +grd_context_get_rdp_server_interface (GrdContext *context) +{ + return context->rdp_server_iface; +} + +GrdDBusRemoteDesktopVncServer * +grd_context_get_vnc_server_interface (GrdContext *context) +{ + return context->vnc_server_iface; +} + +void +grd_context_set_mutter_remote_desktop_proxy (GrdContext *context, + GrdDBusMutterRemoteDesktop *proxy) +{ + g_clear_object (&context->mutter_remote_desktop_proxy); + context->mutter_remote_desktop_proxy = proxy; +} + +void +grd_context_set_mutter_screen_cast_proxy (GrdContext *context, + GrdDBusMutterScreenCast *proxy) +{ + g_clear_object (&context->mutter_screen_cast_proxy); + context->mutter_screen_cast_proxy = proxy; +} + +void +grd_context_set_rdp_server_interface ( + GrdContext *context, + GrdDBusRemoteDesktopRdpServer *rdp_server_iface) +{ + g_clear_object (&context->rdp_server_iface); + context->rdp_server_iface = rdp_server_iface; +} + +void +grd_context_set_vnc_server_interface ( + GrdContext *context, + GrdDBusRemoteDesktopVncServer *vnc_server_iface) +{ + g_clear_object (&context->vnc_server_iface); + context->vnc_server_iface = vnc_server_iface; +} + +GrdSettings * +grd_context_get_settings (GrdContext *context) +{ + return context->settings; +} + +GrdEglThread * +grd_context_get_egl_thread (GrdContext *context) +{ + return context->egl_thread; +} + +GrdRuntimeMode +grd_context_get_runtime_mode (GrdContext *context) +{ + return context->runtime_mode; +} + +void +grd_context_notify_daemon_ready (GrdContext *context) +{ + g_autoptr (GError) error = NULL; + + if (context->egl_thread || + context->runtime_mode == GRD_RUNTIME_MODE_SYSTEM) + return; + + context->egl_thread = grd_egl_thread_new (&error); + if (!context->egl_thread) + g_debug ("Failed to create EGL thread: %s", error->message); +} + +GrdContext * +grd_context_new (GrdRuntimeMode runtime_mode, + GError **error) +{ + g_autoptr (GrdContext) context = NULL; + + context = g_object_new (GRD_TYPE_CONTEXT, NULL); + context->runtime_mode = runtime_mode; + + switch (runtime_mode) + { + case GRD_RUNTIME_MODE_SCREEN_SHARE: + context->settings = GRD_SETTINGS (grd_settings_user_new ()); + break; + case GRD_RUNTIME_MODE_HEADLESS: + context->settings = GRD_SETTINGS (grd_settings_headless_new ()); + break; + case GRD_RUNTIME_MODE_SYSTEM: + context->settings = GRD_SETTINGS (grd_settings_system_new ()); + break; + case GRD_RUNTIME_MODE_HANDOVER: + context->settings = GRD_SETTINGS (grd_settings_handover_new ()); + break; + } + + if (!context->settings) + return NULL; + + return g_steal_pointer (&context); +} + +static void +grd_context_finalize (GObject *object) +{ + GrdContext *context = GRD_CONTEXT (object); + + g_clear_object (&context->mutter_remote_desktop_proxy); + g_clear_object (&context->mutter_screen_cast_proxy); + g_clear_pointer (&context->egl_thread, grd_egl_thread_free); + g_clear_object (&context->settings); + + G_OBJECT_CLASS (grd_context_parent_class)->finalize (object); +} + +static void +grd_context_init (GrdContext *context) +{ +} + +static void +grd_context_class_init (GrdContextClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = grd_context_finalize; +} diff --git a/grd-context.h b/grd-context.h new file mode 100644 index 0000000..19f979a --- /dev/null +++ b/grd-context.h @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2015 Red Hat Inc. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + * Written by: + * Jonas Ådahl + */ + +#pragma once + +#include + +#include "grd-dbus-mutter-remote-desktop.h" +#include "grd-dbus-mutter-screen-cast.h" +#include "grd-dbus-remote-desktop.h" +#include "grd-settings.h" +#include "grd-types.h" + +#define GRD_TYPE_CONTEXT (grd_context_get_type ()) +G_DECLARE_FINAL_TYPE (GrdContext, grd_context, GRD, CONTEXT, GObject) + +GrdContext * grd_context_new (GrdRuntimeMode runtime_mode, + GError **error); + +GrdDBusMutterRemoteDesktop * grd_context_get_mutter_remote_desktop_proxy (GrdContext *context); + +GrdDBusMutterScreenCast * grd_context_get_mutter_screen_cast_proxy (GrdContext *context); + +GrdDBusRemoteDesktopRdpServer * grd_context_get_rdp_server_interface (GrdContext *context); + +GrdDBusRemoteDesktopVncServer * grd_context_get_vnc_server_interface (GrdContext *context); + +void grd_context_set_mutter_remote_desktop_proxy (GrdContext *context, + GrdDBusMutterRemoteDesktop *proxy); + +void grd_context_set_mutter_screen_cast_proxy (GrdContext *context, + GrdDBusMutterScreenCast *proxy); + +void grd_context_set_rdp_server_interface (GrdContext *context, + GrdDBusRemoteDesktopRdpServer *rdp_server_iface); + +void grd_context_set_vnc_server_interface (GrdContext *context, + GrdDBusRemoteDesktopVncServer *vnc_server_iface); + +GrdSettings * grd_context_get_settings (GrdContext *context); + +GrdEglThread * grd_context_get_egl_thread (GrdContext *context); + +GrdRuntimeMode grd_context_get_runtime_mode (GrdContext *context); + +void grd_context_notify_daemon_ready (GrdContext *context); diff --git a/grd-control.c b/grd-control.c new file mode 100644 index 0000000..284cb11 --- /dev/null +++ b/grd-control.c @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2015 Red Hat Inc. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + * Written by: + * Jonas Ådahl + */ + +#include "config.h" + +#include +#include +#include + +#include "grd-private.h" + +int +main (int argc, char **argv) +{ + g_autoptr(GApplication) app = NULL; + gboolean terminate = FALSE; + gboolean headless = FALSE; + gboolean system = FALSE; + gboolean handover = FALSE; + GOptionEntry entries[] = { + { "terminate", 0, 0, G_OPTION_ARG_NONE, &terminate, + "Terminate the daemon", NULL }, + { "headless", 0, 0, G_OPTION_ARG_NONE, &headless, + "Control headless daemon", NULL }, +#if defined(HAVE_RDP) && defined(HAVE_LIBSYSTEMD) + { "system", 0, 0, G_OPTION_ARG_NONE, &system, + "Control system daemon", NULL }, + { "handover", 0, 0, G_OPTION_ARG_NONE, &handover, + "Control handover daemon", NULL }, +#endif /* HAVE_RDP && HAVE_LIBSYSTEMD */ + { NULL } + }; + GError *error = NULL; + GOptionContext *context; + const char *app_id; + + context = g_option_context_new ("- control gnome-remote-desktop"); + g_option_context_add_main_entries (context, entries, NULL); + if (!g_option_context_parse (context, &argc, &argv, &error)) + { + g_printerr ("Invalid option: %s\n", error->message); + g_error_free (error); + return 1; + } + + if (!terminate) + { + g_printerr ("%s", g_option_context_get_help (context, TRUE, NULL)); + return 1; + } + + if (headless) + app_id = GRD_DAEMON_HEADLESS_APPLICATION_ID; + else if (system) + app_id = GRD_DAEMON_SYSTEM_APPLICATION_ID; + else if (handover) + app_id = GRD_DAEMON_HANDOVER_APPLICATION_ID; + else + app_id = GRD_DAEMON_USER_APPLICATION_ID; + + app = g_application_new (app_id, G_APPLICATION_DEFAULT_FLAGS); + if (!g_application_register (app, NULL, NULL)) + { + g_warning ("Failed to register with application\n"); + return 1; + } + if (!g_application_get_is_registered (app)) + { + g_warning ("Not registered\n"); + return 1; + } + if (!g_application_get_is_remote (app)) + { + g_warning ("Failed to connect to application\n"); + return 1; + } + + if (terminate) + g_action_group_activate_action (G_ACTION_GROUP (app), + "terminate", NULL); + else + g_assert_not_reached (); + + return 0; +} diff --git a/grd-credentials-file.c b/grd-credentials-file.c new file mode 100644 index 0000000..0093239 --- /dev/null +++ b/grd-credentials-file.c @@ -0,0 +1,208 @@ +/* + * Copyright (C) 2022 SUSE LLC + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + * Written by: + * Alynx Zhou + */ + +#include "config.h" + +#include "grd-credentials-file.h" + +#include + +#define GRD_CREDENTIALS_FILE_KEY "credentials" + +struct _GrdCredentialsFile +{ + GrdCredentials parent; + + char *filename; + GKeyFile *key_file; +}; + +G_DEFINE_TYPE (GrdCredentialsFile, grd_credentials_file, GRD_TYPE_CREDENTIALS) + +static const char * +group_name_from_type (GrdCredentialsType type) +{ + switch (type) + { + case GRD_CREDENTIALS_TYPE_RDP: + return "RDP"; + case GRD_CREDENTIALS_TYPE_VNC: + return "VNC"; + } + + g_assert_not_reached (); +} + +static gboolean +grd_credentials_file_store (GrdCredentials *credentials, + GrdCredentialsType type, + GVariant *variant, + GError **error) +{ + GrdCredentialsFile *credentials_file = GRD_CREDENTIALS_FILE (credentials); + g_autofree const char *serialized = NULL; + g_autofree char *contents = NULL; + g_autoptr (GFile) file = NULL; + size_t length; + + g_variant_ref_sink (variant); + serialized = g_variant_print (variant, TRUE); + g_variant_unref (variant); + + g_key_file_set_string (credentials_file->key_file, + group_name_from_type (type), + GRD_CREDENTIALS_FILE_KEY, serialized); + + contents = g_key_file_to_data (credentials_file->key_file, &length, error); + if (!contents) + return FALSE; + + file = g_file_new_for_path (credentials_file->filename); + return g_file_replace_contents (file, contents, length, NULL, FALSE, + G_FILE_CREATE_REPLACE_DESTINATION | + G_FILE_CREATE_PRIVATE, NULL, NULL, error); +} + +static GVariant * +grd_credentials_file_lookup (GrdCredentials *credentials, + GrdCredentialsType type, + GError **error) +{ + GrdCredentialsFile *credentials_file = GRD_CREDENTIALS_FILE (credentials); + g_autofree const char *serialized = NULL; + g_autoptr (GError) local_error = NULL; + + serialized = g_key_file_get_string (credentials_file->key_file, + group_name_from_type (type), + GRD_CREDENTIALS_FILE_KEY, &local_error); + if (!serialized) + { + if (!g_error_matches (local_error, + G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_GROUP_NOT_FOUND) && + !g_error_matches (local_error, + G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_KEY_NOT_FOUND)) + g_propagate_error (error, g_steal_pointer (&local_error)); + + return NULL; + } + + return g_variant_parse (NULL, serialized, NULL, NULL, error); +} + +static gboolean +grd_credentials_file_clear (GrdCredentials *credentials, + GrdCredentialsType type, + GError **error) +{ + GrdCredentialsFile *credentials_file = GRD_CREDENTIALS_FILE (credentials); + g_autoptr (GError) local_error = NULL; + gboolean removed; + + removed = g_key_file_remove_key (credentials_file->key_file, + group_name_from_type (type), + GRD_CREDENTIALS_FILE_KEY, &local_error); + if (!removed) + { + if (!g_error_matches (local_error, + G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_GROUP_NOT_FOUND) && + !g_error_matches (local_error, + G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_KEY_NOT_FOUND)) + { + g_propagate_error (error, g_steal_pointer (&local_error)); + return FALSE; + } + else + { + return TRUE; + } + } + + return g_key_file_save_to_file (credentials_file->key_file, + credentials_file->filename, error); +} + +GrdCredentialsFile * +grd_credentials_file_new (GError **error) +{ + g_autoptr (GrdCredentialsFile) credentials_file = NULL; + g_autofree char *dir_path = NULL; + g_autoptr (GFile) dir = NULL; + + credentials_file = g_object_new (GRD_TYPE_CREDENTIALS_FILE, NULL); + + dir_path = g_build_path ("/", + g_get_user_data_dir (), + "gnome-remote-desktop", + NULL); + dir = g_file_new_for_path (dir_path); + if (!g_file_query_exists (dir, NULL)) + { + if (!g_file_make_directory_with_parents (dir, NULL, error)) + return NULL; + } + credentials_file->filename = g_build_path ("/", + dir_path, + "credentials.ini", + NULL); + + if (!g_file_test (credentials_file->filename, G_FILE_TEST_IS_REGULAR)) + if (!g_file_set_contents (credentials_file->filename, "", -1, error)) + return NULL; + + if (!g_key_file_load_from_file (credentials_file->key_file, + credentials_file->filename, + G_KEY_FILE_KEEP_COMMENTS | G_KEY_FILE_KEEP_TRANSLATIONS, + error)) + return NULL; + + return g_steal_pointer (&credentials_file); +} + +static void +grd_credentials_file_finalize (GObject *object) +{ + GrdCredentialsFile *credentials_file = GRD_CREDENTIALS_FILE (object); + + g_clear_pointer (&credentials_file->filename, g_free); + g_clear_pointer (&credentials_file->key_file, g_key_file_unref); + + G_OBJECT_CLASS (grd_credentials_file_parent_class)->finalize (object); +} + +static void +grd_credentials_file_init (GrdCredentialsFile *credentials_file) +{ + credentials_file->key_file = g_key_file_new (); +} + +static void +grd_credentials_file_class_init (GrdCredentialsFileClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GrdCredentialsClass *credentials_class = GRD_CREDENTIALS_CLASS (klass); + + object_class->finalize = grd_credentials_file_finalize; + + credentials_class->store = grd_credentials_file_store; + credentials_class->lookup = grd_credentials_file_lookup; + credentials_class->clear = grd_credentials_file_clear; +} diff --git a/grd-credentials-file.h b/grd-credentials-file.h new file mode 100644 index 0000000..2dbb1f4 --- /dev/null +++ b/grd-credentials-file.h @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2022 SUSE LLC + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + * Written by: + * Alynx Zhou + */ + +#pragma once + +#include "grd-credentials.h" + +#define GRD_TYPE_CREDENTIALS_FILE (grd_credentials_file_get_type ()) +G_DECLARE_FINAL_TYPE (GrdCredentialsFile, grd_credentials_file, GRD, + CREDENTIALS_FILE, GrdCredentials) + +GrdCredentialsFile * grd_credentials_file_new (GError **error); diff --git a/grd-credentials-libsecret.c b/grd-credentials-libsecret.c new file mode 100644 index 0000000..5078738 --- /dev/null +++ b/grd-credentials-libsecret.c @@ -0,0 +1,252 @@ +/* + * Copyright (C) 2018-2022 Red Hat Inc. + * Copyright (C) 2020 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + */ + +#include "config.h" + +#include "grd-credentials-libsecret.h" + +#include +#include +#include + +#define GRD_RDP_CREDENTIALS_SCHEMA (get_rdp_schema ()) + +#define GRD_VNC_PASSWORD_SCHEMA (get_vnc_schema ()) +#define GRD_VNC_LEGACY_PASSWORD_SCHEMA (get_legacy_vnc_schema ()) + +struct _GrdCredentialsLibsecret +{ + GrdCredentials parent; +}; + +G_DEFINE_TYPE (GrdCredentialsLibsecret, + grd_credentials_libsecret, + GRD_TYPE_CREDENTIALS) + +static const SecretSchema * +get_rdp_schema (void) +{ + static const SecretSchema grd_rdp_credentials_schema = { + .name = "org.gnome.RemoteDesktop.RdpCredentials", + .flags = SECRET_SCHEMA_NONE, + .attributes = { + { "credentials", SECRET_SCHEMA_ATTRIBUTE_STRING }, + { "NULL", 0 }, + }, + }; + + return &grd_rdp_credentials_schema; +} + +static const SecretSchema * +get_legacy_vnc_schema (void) +{ + static const SecretSchema grd_vnc_password_schema = { + .name = "org.gnome.RemoteDesktop.VncPassword", + .flags = SECRET_SCHEMA_NONE, + .attributes = { + { "password", SECRET_SCHEMA_ATTRIBUTE_STRING }, + { "NULL", 0 }, + }, + }; + + return &grd_vnc_password_schema; +} + +static const SecretSchema * +get_vnc_schema (void) +{ + static const SecretSchema grd_vnc_password_schema = { + .name = "org.gnome.RemoteDesktop.VncCredentials", + .flags = SECRET_SCHEMA_NONE, + .attributes = { + { "password", SECRET_SCHEMA_ATTRIBUTE_STRING }, + { "NULL", 0 }, + }, + }; + + return &grd_vnc_password_schema; +} + +static const SecretSchema * +schema_from_type (GrdCredentialsType type) +{ + switch (type) + { + case GRD_CREDENTIALS_TYPE_RDP: + return GRD_RDP_CREDENTIALS_SCHEMA; + case GRD_CREDENTIALS_TYPE_VNC: + return GRD_VNC_PASSWORD_SCHEMA; + } + + g_assert_not_reached (); +} + +static const char * +description_from_type (GrdCredentialsType type) +{ + switch (type) + { + case GRD_CREDENTIALS_TYPE_RDP: + return "GNOME Remote Desktop RDP credentials"; + case GRD_CREDENTIALS_TYPE_VNC: + return "GNOME Remote Desktop VNC password"; + } + + g_assert_not_reached (); +} + +static gboolean +grd_credentials_libsecret_store (GrdCredentials *credentials, + GrdCredentialsType type, + GVariant *variant, + GError **error) +{ + g_autofree char *serialized = NULL; + + g_variant_ref_sink (variant); + serialized = g_variant_print (variant, TRUE); + g_variant_unref (variant); + + return secret_password_store_sync (schema_from_type (type), + SECRET_COLLECTION_DEFAULT, + description_from_type (type), + serialized, + NULL, error, + NULL); +} + +static GVariant * +grd_credentials_libsecret_lookup (GrdCredentials *credentials, + GrdCredentialsType type, + GError **error) +{ + g_autofree char *serialized = NULL; + + serialized = secret_password_lookup_sync (schema_from_type (type), + NULL, error, + NULL); + if (!serialized) + return NULL; + + return g_variant_parse (NULL, serialized, NULL, NULL, error); +} + +static gboolean +grd_credentials_libsecret_clear (GrdCredentials *credentials, + GrdCredentialsType type, + GError **error) +{ + g_autoptr (GError) local_error = NULL; + + secret_password_clear_sync (schema_from_type (type), + NULL, &local_error, + NULL); + if (local_error) + { + g_propagate_error (error, g_steal_pointer (&local_error)); + return FALSE; + } + else + { + return TRUE; + } +} + +GrdCredentialsLibsecret * +grd_credentials_libsecret_new (void) +{ + return g_object_new (GRD_TYPE_CREDENTIALS_LIBSECRET, NULL); +} + +static void +grd_credentials_libsecret_init (GrdCredentialsLibsecret *credentials_libsecret) +{ +} + +static void +maybe_migrate_legacy_vnc_password (GrdCredentials *credentials) +{ + g_autoptr (GError) error = NULL; + g_autofree char *password = NULL; + + password = secret_password_lookup_sync (GRD_VNC_LEGACY_PASSWORD_SCHEMA, + NULL, &error, + NULL); + if (!password) + { + if (error) + { + g_printerr ("Failed to lookup legacy VNC password schema: %s\n", + error->message); + } + } + else + { + g_printerr ("Migrating VNC password to new schema... "); + + if (!grd_credentials_store (credentials, + GRD_CREDENTIALS_TYPE_VNC, + g_variant_new_string (password), + &error)) + { + g_printerr ("Failed to migrate VNC password to new schema: %s\n", + error->message); + } + else + { + if (!secret_password_clear_sync (GRD_VNC_LEGACY_PASSWORD_SCHEMA, + NULL, &error, NULL) && + error) + { + g_printerr ("Failed to clear VNC password from old schema: %s\n", + error->message); + } + else + { + g_printerr ("OK\n"); + } + } + } +} + +static void +grd_credentials_libsecret_constructed (GObject *object) +{ + GrdCredentials *credentials = GRD_CREDENTIALS (object); + + maybe_migrate_legacy_vnc_password (credentials); + + G_OBJECT_CLASS (grd_credentials_libsecret_parent_class)->constructed (object); +} + +static void +grd_credentials_libsecret_class_init (GrdCredentialsLibsecretClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GrdCredentialsClass *credentials_class = GRD_CREDENTIALS_CLASS (klass); + + object_class->constructed = grd_credentials_libsecret_constructed; + + credentials_class->store = grd_credentials_libsecret_store; + credentials_class->lookup = grd_credentials_libsecret_lookup; + credentials_class->clear = grd_credentials_libsecret_clear; +} diff --git a/grd-credentials-libsecret.h b/grd-credentials-libsecret.h new file mode 100644 index 0000000..e2922bb --- /dev/null +++ b/grd-credentials-libsecret.h @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2018-2022 Red Hat Inc. + * Copyright (C) 2020 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + */ + +#pragma once + +#include "grd-credentials.h" + +#define GRD_TYPE_CREDENTIALS_LIBSECRET (grd_credentials_libsecret_get_type ()) +G_DECLARE_FINAL_TYPE (GrdCredentialsLibsecret, grd_credentials_libsecret, + GRD, CREDENTIALS_LIBSECRET, GrdCredentials) + +GrdCredentialsLibsecret * grd_credentials_libsecret_new (void); diff --git a/grd-credentials-one-time.c b/grd-credentials-one-time.c new file mode 100644 index 0000000..7c20d7e --- /dev/null +++ b/grd-credentials-one-time.c @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2022 SUSE Software Solutions Germany GmbH + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + * Written by: + * Joan Torres + */ + +#include "config.h" + +#include +#include + +#include "grd-credentials-one-time.h" + +struct _GrdCredentialsOneTime +{ + GrdCredentials parent; + + struct + { + char *username; + char *password; + } rdp; +}; + +G_DEFINE_TYPE (GrdCredentialsOneTime, + grd_credentials_one_time, + GRD_TYPE_CREDENTIALS) + +static char * +grd_generate_random_bytes (size_t size, + GError **error) +{ + int fd; + int ret; + char *bytes; + + errno = 0; + fd = open ("/dev/urandom", O_RDONLY); + if (fd < 0) + { + g_set_error_literal (error, + G_FILE_ERROR, + g_file_error_from_errno (errno), + g_strerror (errno)); + return NULL; + } + + bytes = g_malloc (size); + do + ret = read (fd, bytes, size); + while ((ret == -1 && + (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK)) || + ret < size); + + close (fd); + + return bytes; +} + +static char * +generate_random_utf8_bytes (size_t len, + GError **error) +{ + char *random_bytes; + int i; + + random_bytes = grd_generate_random_bytes (len + 1, error); + if (!random_bytes) + return NULL; + + /* UTF-8 chars defined with 1 byte always have the MSB to 0. + * Do not use ASCII control characters (0 - 32, 127). */ + for (i = 0; i < len; ++i) + { + random_bytes[i] &= 127; + random_bytes[i] = (random_bytes[i] % 94) + 33; + } + random_bytes[len] = '\0'; + + return random_bytes; +} + +static char * +generate_random_username (size_t len, + GError **error) +{ + char *username; + + username = generate_random_utf8_bytes (len, error); + if (!username) + return NULL; + + /* The use of # at the beggining or : at any position, + * makes an error when looking up the user on the SAM file. */ + return g_strdelimit (username, "#:", '_'); +} + +static gboolean +grd_credentials_one_time_store (GrdCredentials *credentials, + GrdCredentialsType type, + GVariant *variant, + GError **error) +{ + GrdCredentialsOneTime *credentials_one_time = + GRD_CREDENTIALS_ONE_TIME (credentials); + g_autofree char *username = NULL; + g_autofree char *password = NULL; + + g_assert (type == GRD_CREDENTIALS_TYPE_RDP); + + g_variant_lookup (variant, "username", "s", &username); + if (!username) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND, + "Username not set"); + return FALSE; + } + + g_variant_lookup (variant, "password", "s", &password); + if (!password) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND, + "Password not set"); + return FALSE; + } + + g_clear_pointer (&credentials_one_time->rdp.username, g_free); + g_clear_pointer (&credentials_one_time->rdp.password, g_free); + + credentials_one_time->rdp.username = g_steal_pointer (&username); + credentials_one_time->rdp.password = g_steal_pointer (&password); + + return TRUE; +} + +static GVariant * +grd_credentials_one_time_lookup (GrdCredentials *credentials, + GrdCredentialsType type, + GError **error) +{ + GrdCredentialsOneTime *credentials_one_time = + GRD_CREDENTIALS_ONE_TIME (credentials); + const char *rdp_username = credentials_one_time->rdp.username; + const char *rdp_password = credentials_one_time->rdp.password; + GVariantBuilder builder; + + switch (type) + { + case GRD_CREDENTIALS_TYPE_RDP: + g_variant_builder_init (&builder, G_VARIANT_TYPE ("a{sv}")); + + g_variant_builder_add (&builder, "{sv}", + "username", g_variant_new_string (rdp_username)); + g_variant_builder_add (&builder, "{sv}", + "password", g_variant_new_string (rdp_password)); + + return g_variant_builder_end (&builder); + default: + g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND, + "Credentials type not found"); + return NULL; + } +} + +GrdCredentialsOneTime * +grd_credentials_one_time_new (void) +{ + g_autoptr (GrdCredentialsOneTime) credentials_one_time = NULL; + g_autoptr (GError) error = NULL; + + credentials_one_time = g_object_new (GRD_TYPE_CREDENTIALS_ONE_TIME, NULL); + + credentials_one_time->rdp.username = generate_random_username (16, &error); + if (!credentials_one_time->rdp.username) + { + g_warning ("Failed to generate one time RDP username: %s", error->message); + return NULL; + } + + credentials_one_time->rdp.password = generate_random_utf8_bytes (16, &error); + if (!credentials_one_time->rdp.password) + { + g_warning ("Failed to generate one time RDP password: %s", error->message); + return NULL; + } + + return g_steal_pointer (&credentials_one_time); +} + +static void +grd_credentials_one_time_finalize (GObject *object) +{ + GrdCredentialsOneTime *credentials_one_time = GRD_CREDENTIALS_ONE_TIME (object); + + g_clear_pointer (&credentials_one_time->rdp.username, g_free); + g_clear_pointer (&credentials_one_time->rdp.password, g_free); + + G_OBJECT_CLASS (grd_credentials_one_time_parent_class)->finalize (object); +} + +static void +grd_credentials_one_time_init (GrdCredentialsOneTime *credentials_one_time) +{ +} + +static void +grd_credentials_one_time_class_init (GrdCredentialsOneTimeClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GrdCredentialsClass *credentials_class = GRD_CREDENTIALS_CLASS (klass); + + object_class->finalize = grd_credentials_one_time_finalize; + + credentials_class->store = grd_credentials_one_time_store; + credentials_class->lookup = grd_credentials_one_time_lookup; +} diff --git a/grd-credentials-one-time.h b/grd-credentials-one-time.h new file mode 100644 index 0000000..103d954 --- /dev/null +++ b/grd-credentials-one-time.h @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2022 SUSE Software Solutions Germany GmbH + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + * Written by: + * Joan Torres + */ + +#pragma once + +#include "grd-credentials.h" + +#define GRD_TYPE_CREDENTIALS_ONE_TIME (grd_credentials_one_time_get_type ()) +G_DECLARE_FINAL_TYPE (GrdCredentialsOneTime, grd_credentials_one_time, + GRD, CREDENTIALS_ONE_TIME, GrdCredentials) + +GrdCredentialsOneTime *grd_credentials_one_time_new (void); diff --git a/grd-credentials-tpm.c b/grd-credentials-tpm.c new file mode 100644 index 0000000..9903655 --- /dev/null +++ b/grd-credentials-tpm.c @@ -0,0 +1,243 @@ +/* + * Copyright (C) 2022 Red Hat Inc. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + */ + +#include "config.h" + +#include "grd-credentials-tpm.h" + +#include +#include +#include + +#include "grd-tpm.h" + +struct _GrdCredentialsTpm +{ + GrdCredentials parent; +}; + +G_DEFINE_TYPE (GrdCredentialsTpm, grd_credentials_tpm, GRD_TYPE_CREDENTIALS) + +static const char * +secret_file_name_from_type (GrdCredentialsType type) +{ + switch (type) + { + case GRD_CREDENTIALS_TYPE_RDP: + return "rdp-credentials.priv"; + case GRD_CREDENTIALS_TYPE_VNC: + return "vnc-credentials.priv"; + } + + g_assert_not_reached (); +} + +static gboolean +grd_credentials_tpm_store (GrdCredentials *credentials, + GrdCredentialsType type, + GVariant *variant, + GError **error) +{ + g_autoptr (GrdTpm) tpm = NULL; + g_autofree const char *serialized = NULL; + ESYS_TR primary_handle = 0; + g_autofree TPMS_CONTEXT *primary = NULL; + g_autofree TPML_PCR_SELECTION *pcr_selection = NULL; + g_autofree TPML_DIGEST *pcr_digest = NULL; + g_autofree TPMS_CONTEXT *secret_tpms_context = NULL; + g_autoptr (GVariant) secret_variant = NULL; + g_autofree char *secret_serialized = NULL; + g_autofree char *dir_path = NULL; + g_autoptr (GFile) dir = NULL; + const char *secret_file_name; + g_autofree char *secret_path = NULL; + g_autoptr (GFile) secret_file = NULL; + + g_variant_ref_sink (variant); + serialized = g_variant_print (variant, TRUE); + g_variant_unref (variant); + + tpm = grd_tpm_new (GRD_TPM_MODE_WRITE, error); + if (!tpm) + return FALSE; + + if (!grd_tpm_create_primary (tpm, &primary_handle, error)) + return FALSE; + + if (!grd_tpm_read_pcr (tpm, &pcr_selection, &pcr_digest, error)) + return FALSE; + + if (!grd_tpm_store_secret (tpm, + serialized, + primary_handle, + pcr_selection, + pcr_digest, + &secret_tpms_context, + error)) + return FALSE; + + secret_variant = grd_tpms_context_to_variant (secret_tpms_context); + + dir_path = g_build_path ("/", + g_get_user_data_dir (), + "gnome-remote-desktop", + NULL); + dir = g_file_new_for_path (dir_path); + if (!g_file_query_exists (dir, NULL)) + { + if (!g_file_make_directory_with_parents (dir, NULL, error)) + return FALSE; + } + + secret_file_name = secret_file_name_from_type (type); + secret_path = g_build_path ("/", dir_path, secret_file_name, NULL); + secret_file = g_file_new_for_path (secret_path); + + secret_serialized = g_variant_print (secret_variant, TRUE); + + return g_file_replace_contents (secret_file, + secret_serialized, + strlen (secret_serialized) + 1, + NULL, FALSE, + G_FILE_CREATE_PRIVATE | + G_FILE_CREATE_REPLACE_DESTINATION, + NULL, NULL, error); +} + +static GVariant * +grd_credentials_tpm_lookup (GrdCredentials *credentials, + GrdCredentialsType type, + GError **error) +{ + g_autoptr (GrdTpm) tpm = NULL; + const char *secret_file_name; + g_autofree char *secret_path = NULL; + g_autofree char *serialized = NULL; + g_autofree TPML_PCR_SELECTION *pcr_selection = NULL; + g_autofree TPML_DIGEST *pcr_digest = NULL; + g_autoptr (GVariant) secret_variant = NULL; + g_autoptr (GError) local_error = NULL; + g_autofree char *credentials_string = NULL; + + secret_file_name = secret_file_name_from_type (type); + secret_path = g_build_path ("/", + g_get_user_data_dir (), + "gnome-remote-desktop", + secret_file_name, + NULL); + + if (!g_file_get_contents (secret_path, + &serialized, + NULL, + &local_error)) + { + if (g_error_matches (local_error, G_FILE_ERROR, G_FILE_ERROR_NOENT)) + return NULL; + + g_propagate_error (error, g_steal_pointer (&local_error)); + return NULL; + } + + secret_variant = g_variant_parse (G_VARIANT_TYPE ("(uutqs)"), serialized, + NULL, NULL, error); + if (!secret_variant) + return NULL; + + tpm = grd_tpm_new (GRD_TPM_MODE_READ, error); + if (!tpm) + return FALSE; + + if (!grd_tpm_read_pcr (tpm, &pcr_selection, &pcr_digest, error)) + return NULL; + + credentials_string = grd_tpm_restore_secret (tpm, + secret_variant, + pcr_selection, + pcr_digest, + error); + if (!credentials_string) + return NULL; + + return g_variant_parse (NULL, credentials_string, NULL, NULL, error); +} + +static gboolean +grd_credentials_tpm_clear (GrdCredentials *credentials, + GrdCredentialsType type, + GError **error) +{ + const char *secret_file_name; + g_autofree char *secret_path = NULL; + g_autoptr (GFile) secret_file = NULL; + g_autoptr (GError) local_error = NULL; + + secret_file_name = secret_file_name_from_type (type); + secret_path = g_build_path ("/", + g_get_user_data_dir (), + "gnome-remote-desktop", + secret_file_name, + NULL); + secret_file = g_file_new_for_path (secret_path); + + if (!g_file_delete (secret_file, NULL, error)) + { + if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) + { + return TRUE; + } + else + { + g_propagate_error (error, g_steal_pointer (&local_error)); + return FALSE; + } + } + + return TRUE; +} + +GrdCredentialsTpm * +grd_credentials_tpm_new (GError **error) +{ + g_autoptr (GrdTpm) tpm = NULL; + + tpm = grd_tpm_new (GRD_TPM_MODE_NONE, error); + if (!tpm) + return NULL; + + if (!grd_tpm_check_capabilities (tpm, error)) + return NULL; + + return g_object_new (GRD_TYPE_CREDENTIALS_TPM, NULL); +} + +static void +grd_credentials_tpm_init (GrdCredentialsTpm *credentials_tpm) +{ +} + +static void +grd_credentials_tpm_class_init (GrdCredentialsTpmClass *klass) +{ + GrdCredentialsClass *credentials_class = GRD_CREDENTIALS_CLASS (klass); + + credentials_class->store = grd_credentials_tpm_store; + credentials_class->lookup = grd_credentials_tpm_lookup; + credentials_class->clear = grd_credentials_tpm_clear; +} diff --git a/grd-credentials-tpm.h b/grd-credentials-tpm.h new file mode 100644 index 0000000..f1d4b03 --- /dev/null +++ b/grd-credentials-tpm.h @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2022 Red Hat Inc. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + */ + +#pragma once + +#include "grd-credentials.h" + +#define GRD_TYPE_CREDENTIALS_TPM (grd_credentials_tpm_get_type ()) +G_DECLARE_FINAL_TYPE (GrdCredentialsTpm, grd_credentials_tpm, + GRD, CREDENTIALS_TPM, GrdCredentials) + +GrdCredentialsTpm * grd_credentials_tpm_new (GError **error); diff --git a/grd-credentials.c b/grd-credentials.c new file mode 100644 index 0000000..b1ccddb --- /dev/null +++ b/grd-credentials.c @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2022 Red Hat Inc. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + */ + +#include "config.h" + +#include "grd-credentials.h" + +G_DEFINE_ABSTRACT_TYPE (GrdCredentials, grd_credentials, G_TYPE_OBJECT) + +GVariant * +grd_credentials_lookup (GrdCredentials *credentials, + GrdCredentialsType type, + GError **error) +{ + return GRD_CREDENTIALS_GET_CLASS (credentials)->lookup (credentials, + type, error); +} + +gboolean +grd_credentials_clear (GrdCredentials *credentials, + GrdCredentialsType type, + GError **error) +{ + return GRD_CREDENTIALS_GET_CLASS (credentials)->clear (credentials, + type, error); +} + +gboolean +grd_credentials_store (GrdCredentials *credentials, + GrdCredentialsType type, + GVariant *variant, + GError **error) +{ + return GRD_CREDENTIALS_GET_CLASS (credentials)->store (credentials, + type, variant, error); +} + +static void +grd_credentials_init (GrdCredentials *credentials) +{ +} + +static void +grd_credentials_class_init (GrdCredentialsClass *klass) +{ +} diff --git a/grd-credentials.h b/grd-credentials.h new file mode 100644 index 0000000..f16ac78 --- /dev/null +++ b/grd-credentials.h @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2022 Red Hat Inc. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + */ + +#pragma once + +#include + +typedef enum _GrdCredentialsType +{ + GRD_CREDENTIALS_TYPE_RDP, + GRD_CREDENTIALS_TYPE_VNC, +} GrdCredentialsType; + +#define GRD_TYPE_CREDENTIALS (grd_credentials_get_type ()) +G_DECLARE_DERIVABLE_TYPE (GrdCredentials, grd_credentials, + GRD, CREDENTIALS, GObject) + +struct _GrdCredentialsClass +{ + GObjectClass parent_class; + + gboolean (* store) (GrdCredentials *credentials, + GrdCredentialsType type, + GVariant *variant, + GError **error); + + GVariant * (* lookup) (GrdCredentials *credentials, + GrdCredentialsType type, + GError **error); + + gboolean (* clear) (GrdCredentials *credentials, + GrdCredentialsType type, + GError **error); +}; + +gboolean grd_credentials_store (GrdCredentials *credentials, + GrdCredentialsType type, + GVariant *variant, + GError **error); + +GVariant * grd_credentials_lookup (GrdCredentials *credentials, + GrdCredentialsType type, + GError **error); + +gboolean grd_credentials_clear (GrdCredentials *credentials, + GrdCredentialsType type, + GError **error); diff --git a/grd-ctl.c b/grd-ctl.c new file mode 100644 index 0000000..8bb1697 --- /dev/null +++ b/grd-ctl.c @@ -0,0 +1,1199 @@ +/* + * Copyright (C) 2021-2022 Red Hat Inc. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "grd-enums.h" +#include "grd-settings-headless.h" +#include "grd-settings-system.h" +#include "grd-settings-user.h" +#include "grd-utils.h" + +#define GRD_SYSTEMD_SERVICE "gnome-remote-desktop.service" +#define GRD_SYSTEMD_HEADLESS_SERVICE "gnome-remote-desktop-headless.service" + +typedef enum +{ + PROMPT_TYPE_VISIBLE_TEXT, + PROMPT_TYPE_HIDDEN_TEXT +} PromptType; + +typedef struct _SubCommand +{ + const char *subcommand; + gboolean (* process) (GrdSettings *settings, + int argc, + char **argv, + GError **error); + int n_args; +} SubCommand; + +static void +log_handler (const char *log_domain, + GLogLevelFlags log_level, + const char *message, + gpointer user_data) +{ + fprintf (stderr, "%s.\n", message); +} + +static void +print_usage (void) +{ + printf (_("Usage: %s [OPTIONS...] COMMAND [SUBCOMMAND]...\n"), + g_get_prgname ()); +} + +static int +process_options (GrdSettings *settings, + int argc, + char **argv, + const SubCommand *subcommands, + int n_subcommands) +{ + gboolean subcommand_exists = FALSE; + int i; + + if (argc <= 0) + return EX_USAGE; + + for (i = 0; i < n_subcommands; i++) + { + g_autoptr (GError) error = NULL; + + if (g_strcmp0 (argv[0], subcommands[i].subcommand) != 0) + continue; + + subcommand_exists = TRUE; + + if (subcommands[i].n_args != argc - 1) + continue; + + if (!subcommands[i].process (settings, + argc - 1, argv + 1, &error)) + { + g_printerr ("%s\n", error->message); + return EXIT_FAILURE; + } + + if (!GRD_IS_SETTINGS_SYSTEM (settings)) + g_settings_sync (); + + return EXIT_SUCCESS; + } + + if (subcommand_exists) + g_printerr ("Wrong number of arguments for subcommand '%s'\n", argv[0]); + else + g_printerr ("Unknown subcommand '%s'\n", argv[0]); + + return EX_USAGE; +} + +static gboolean +is_numeric (const char *s) +{ + while (*s) + { + if (isdigit (*s++) == 0) + return FALSE; + } + + return TRUE; +} + +static char * +prompt_for_input (const char *prompt, + PromptType prompt_type, + GError **error) +{ + g_autoptr (GInputStream) input_stream = NULL; + g_autoptr (GOutputStream) output_stream = NULL; + g_autoptr (GDataInputStream) data_input_stream = NULL; + gboolean terminal_attributes_changed = FALSE; + struct termios terminal_attributes; + g_autofree char *input = NULL; + gboolean success; + int output_fd; + int input_fd; + + input_fd = STDIN_FILENO; + output_fd = STDOUT_FILENO; + + input_stream = g_unix_input_stream_new (input_fd, FALSE); + data_input_stream = g_data_input_stream_new (input_stream); + + if (isatty (output_fd)) + output_stream = g_unix_output_stream_new (output_fd, FALSE); + + if (output_stream) + { + success = g_output_stream_write_all (output_stream, prompt, strlen (prompt), NULL, NULL, error); + if (!success) + return NULL; + + if (prompt_type == PROMPT_TYPE_HIDDEN_TEXT && isatty (input_fd)) + { + struct termios updated_terminal_attributes; + int ret; + + ret = tcgetattr (input_fd, &terminal_attributes); + if (ret < 0) + { + g_set_error (error, G_IO_ERROR, g_io_error_from_errno (errno), + "Could not query terminal attributes"); + return NULL; + } + + updated_terminal_attributes = terminal_attributes; + updated_terminal_attributes.c_lflag &= ~ECHO; + + ret = tcsetattr (input_fd, TCSANOW, &updated_terminal_attributes); + if (ret < 0) + { + g_set_error (error, G_IO_ERROR, g_io_error_from_errno (errno), + "Could not disable input echo in terminal"); + return NULL; + } + + terminal_attributes_changed = TRUE; + } + } + + input = g_data_input_stream_read_line_utf8 (data_input_stream, NULL, NULL, error); + + if (terminal_attributes_changed) + { + g_output_stream_write_all (output_stream, "\n", strlen ("\n"), NULL, NULL, NULL); + tcsetattr (input_fd, TCSANOW, &terminal_attributes); + } + + return g_steal_pointer (&input); +} + +#ifdef HAVE_RDP +static gboolean +rdp_set_port (GrdSettings *settings, + int argc, + char **argv, + GError **error) +{ + int port; + + if (!is_numeric (argv[0])) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, + "The port must be an integer"); + return FALSE; + } + + port = strtol (argv[0], NULL, 10); + g_object_set (G_OBJECT (settings), "rdp-port", port, NULL); + return TRUE; +} + +static gboolean +rdp_enable (GrdSettings *settings, + int argc, + char **argv, + GError **error) +{ + GrdRuntimeMode runtime_mode = grd_settings_get_runtime_mode (settings); + + if (!grd_toggle_systemd_unit (runtime_mode, TRUE, error)) + return FALSE; + + g_object_set (G_OBJECT (settings), "rdp-enabled", TRUE, NULL); + + return TRUE; +} + +static gboolean +rdp_disable (GrdSettings *settings, + int argc, + char **argv, + GError **error) +{ + GrdRuntimeMode runtime_mode = grd_settings_get_runtime_mode (settings); + gboolean vnc_enabled; + + g_object_set (G_OBJECT (settings), "rdp-enabled", FALSE, NULL); + + g_object_get (G_OBJECT (settings), "vnc-enabled", &vnc_enabled, NULL); + if (!vnc_enabled) + return grd_toggle_systemd_unit (runtime_mode, FALSE, error); + + return TRUE; +} + +static gboolean +rdp_set_auth_methods (GrdSettings *settings, + int argc, + char **argv, + GError **error) +{ + char *auth_methods_string = argv[0]; + g_auto (GStrv) auth_method_strings = NULL; + char **auth_method; + GrdRdpAuthMethods auth_methods = 0; + + auth_method_strings = g_strsplit (auth_methods_string, ",", -1); + for (auth_method = auth_method_strings; *auth_method; auth_method++) + { + if (g_strcmp0 (*auth_method, "credentials") == 0) + { + auth_methods |= GRD_RDP_AUTH_METHOD_CREDENTIALS; + } + else if (g_strcmp0 (*auth_method, "kerberos") == 0) + { + auth_methods |= GRD_RDP_AUTH_METHOD_KERBEROS; + } + else + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, + "Invalid auth method '%s'", *auth_method); + return FALSE; + } + } + + if (!auth_methods) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, + "No auth method selected"); + return FALSE; + } + + if ((auth_methods & GRD_RDP_AUTH_METHOD_KERBEROS) && + GRD_IS_SETTINGS_SYSTEM (settings)) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, + "Kerberos not supported in the system daemon"); + return FALSE; + } + + g_object_set (G_OBJECT (settings), "rdp-auth-methods", auth_methods, NULL); + return TRUE; +} + +static gboolean +rdp_set_tls_cert (GrdSettings *settings, + int argc, + char **argv, + GError **error) +{ + char *path = argv[0]; + + if (!g_path_is_absolute (path)) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, + "Path to certificate file not absolute"); + return FALSE; + } + + g_object_set (G_OBJECT (settings), "rdp-server-cert-path", argv[0], NULL); + return TRUE; +} + +static gboolean +rdp_set_tls_key (GrdSettings *settings, + int argc, + char **argv, + GError **error) +{ + char *path = argv[0]; + + if (!g_path_is_absolute (path)) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, + "Path to certificate file not absolute"); + return FALSE; + } + + g_object_set (G_OBJECT (settings), "rdp-server-key-path", argv[0], NULL); + return TRUE; +} + +static gboolean +rdp_set_kerberos_keytab (GrdSettings *settings, + int argc, + char **argv, + GError **error) +{ + char *path = argv[0]; + + if (GRD_IS_SETTINGS_SYSTEM (settings)) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, + "Kerberos not supported in the system daemon"); + return FALSE; + } + + if (!g_path_is_absolute (path)) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, + "Path to certificate file not absolute"); + return FALSE; + } + + g_object_set (G_OBJECT (settings), "rdp-kerberos-keytab", argv[0], NULL); + return TRUE; +} + +static gboolean +rdp_set_credentials (GrdSettings *settings, + int argc, + char **argv, + GError **error) +{ + g_autofree char *username = NULL; + g_autofree char *password = NULL; + + if (argc < 1) + { + username = prompt_for_input (_("Username: "), PROMPT_TYPE_VISIBLE_TEXT, + error); + if (!username) + return FALSE; + } + else + { + username = g_strdup (argv[0]); + } + + if (argc < 2) + { + password = prompt_for_input (_("Password: "), PROMPT_TYPE_HIDDEN_TEXT, + error); + if (!password) + return FALSE; + } + else + { + password = g_strdup (argv[1]); + } + + return grd_settings_set_rdp_credentials (settings, username, password, error); +} + +static gboolean +rdp_clear_credentials (GrdSettings *settings, + int argc, + char **argv, + GError **error) +{ + return grd_settings_clear_rdp_credentials (settings, error); +} + +static gboolean +rdp_enable_view_only (GrdSettings *settings, + int argc, + char **argv, + GError **error) +{ + g_object_set (G_OBJECT (settings), "rdp-view-only", TRUE, NULL); + return TRUE; +} + +static gboolean +rdp_disable_view_only (GrdSettings *settings, + int argc, + char **argv, + GError **error) +{ + g_object_set (G_OBJECT (settings), "rdp-view-only", FALSE, NULL); + return TRUE; +} + +static gboolean +rdp_enable_port_negotiation (GrdSettings *settings, + int argc, + char **argv, + GError **error) +{ + g_object_set (G_OBJECT (settings), "rdp-negotiate-port", TRUE, NULL); + return TRUE; +} + +static gboolean +rdp_disable_port_negotiation (GrdSettings *settings, + int argc, + char **argv, + GError **error) +{ + g_object_set (G_OBJECT (settings), "rdp-negotiate-port", FALSE, NULL); + return TRUE; +} + +static const SubCommand rdp_subcommands[] = { + { "set-port", rdp_set_port, 1 }, + { "enable", rdp_enable, 0 }, + { "disable", rdp_disable, 0 }, + { "set-auth-methods", rdp_set_auth_methods, 1 }, + { "set-tls-cert", rdp_set_tls_cert, 1 }, + { "set-tls-key", rdp_set_tls_key, 1 }, + { "set-kerberos-keytab", rdp_set_kerberos_keytab, 1 }, + { "set-credentials", rdp_set_credentials, 0 }, + { "set-credentials", rdp_set_credentials, 1 }, + { "set-credentials", rdp_set_credentials, 2 }, + { "clear-credentials", rdp_clear_credentials, 0 }, + { "enable-view-only", rdp_enable_view_only, 0 }, + { "disable-view-only", rdp_disable_view_only, 0 }, + { "enable-port-negotiation", rdp_enable_port_negotiation, 0 }, + { "disable-port-negotiation", rdp_disable_port_negotiation, 0 }, +}; + +static int +process_rdp_options (GrdSettings *settings, + int argc, + char **argv) +{ + return process_options (settings, + argc, argv, + rdp_subcommands, + G_N_ELEMENTS (rdp_subcommands)); +} +#endif /* HAVE_RDP */ + +#ifdef HAVE_VNC +static gboolean +vnc_set_port (GrdSettings *settings, + int argc, + char **argv, + GError **error) +{ + int port; + + if (!is_numeric (argv[0])) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, + "The port must be an integer"); + return FALSE; + } + + port = strtol (argv[0], NULL, 10); + g_object_set (G_OBJECT (settings), "vnc-port", port, NULL); + return TRUE; +} + +static gboolean +vnc_enable (GrdSettings *settings, + int argc, + char **argv, + GError **error) +{ + GrdRuntimeMode runtime_mode = grd_settings_get_runtime_mode (settings); + + if (!grd_toggle_systemd_unit (runtime_mode, TRUE, error)) + return FALSE; + + g_object_set (G_OBJECT (settings), "vnc-enabled", TRUE, NULL); + return TRUE; +} + +static gboolean +vnc_disable (GrdSettings *settings, + int argc, + char **argv, + GError **error) +{ + GrdRuntimeMode runtime_mode = grd_settings_get_runtime_mode (settings); + gboolean rdp_enabled; + + g_object_set (G_OBJECT (settings), "vnc-enabled", FALSE, NULL); + + g_object_get (G_OBJECT (settings), "rdp-enabled", &rdp_enabled, NULL); + if (!rdp_enabled) + return grd_toggle_systemd_unit (runtime_mode, FALSE, error); + + return TRUE; +} + +#define MAX_VNC_PASSWORD_SIZE 8 + +static gboolean +vnc_set_credentials (GrdSettings *settings, + int argc, + char **argv, + GError **error) +{ + g_autofree char *password = NULL; + + if (argc < 1) + { + password = prompt_for_input (_("Password: "), PROMPT_TYPE_HIDDEN_TEXT, + error); + if (!password) + return FALSE; + } + else + { + password = g_strdup (argv[0]); + } + + if (strlen (password) > MAX_VNC_PASSWORD_SIZE) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, + "Password is too long"); + return FALSE; + } + + return grd_settings_set_vnc_password (settings, password, error); +} + +static gboolean +vnc_clear_credentials (GrdSettings *settings, + int argc, + char **argv, + GError **error) +{ + return grd_settings_clear_vnc_password (settings, error); +} + +static gboolean +vnc_set_auth_method (GrdSettings *settings, + int argc, + char **argv, + GError **error) +{ + char *auth_method = argv[0]; + + if (strcmp (auth_method, "prompt") == 0) + { + g_object_set (G_OBJECT (settings), + "vnc-auth-method", GRD_VNC_AUTH_METHOD_PROMPT, + NULL); + return TRUE; + } + else if (strcmp (auth_method, "password") == 0) + { + g_object_set (G_OBJECT (settings), + "vnc-auth-method", GRD_VNC_AUTH_METHOD_PASSWORD, + NULL); + return TRUE; + } + + g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, + "Invalid auth method '%s'", auth_method); + + return FALSE; +} + +static gboolean +vnc_enable_view_only (GrdSettings *settings, + int argc, + char **argv, + GError **error) +{ + g_object_set (G_OBJECT (settings), "vnc-view-only", TRUE, NULL); + return TRUE; +} + +static gboolean +vnc_disable_view_only (GrdSettings *settings, + int argc, + char **argv, + GError **error) +{ + g_object_set (G_OBJECT (settings), "vnc-view-only", FALSE, NULL); + return TRUE; +} + +static gboolean +vnc_enable_port_negotiation (GrdSettings *settings, + int argc, + char **argv, + GError **error) +{ + g_object_set (G_OBJECT (settings), "vnc-negotiate-port", TRUE, NULL); + return TRUE; +} + +static gboolean +vnc_disable_port_negotiation (GrdSettings *settings, + int argc, + char **argv, + GError **error) +{ + g_object_set (G_OBJECT (settings), "vnc-negotiate-port", FALSE, NULL); + return TRUE; +} + + +static const SubCommand vnc_subcommands[] = { + { "set-port", vnc_set_port, 1 }, + { "enable", vnc_enable, 0 }, + { "disable", vnc_disable, 0 }, + { "set-password", vnc_set_credentials, 0 }, + { "set-password", vnc_set_credentials, 1 }, + { "clear-password", vnc_clear_credentials, 0 }, + { "set-auth-method", vnc_set_auth_method, 1 }, + { "enable-view-only", vnc_enable_view_only, 0 }, + { "disable-view-only", vnc_disable_view_only, 0 }, + { "enable-port-negotiation", vnc_enable_port_negotiation, 0 }, + { "disable-port-negotiation", vnc_disable_port_negotiation, 0 }, +}; + +static int +process_vnc_options (GrdSettings *settings, + int argc, + char **argv) +{ + return process_options (settings, + argc, argv, + vnc_subcommands, + G_N_ELEMENTS (vnc_subcommands)); +} +#endif /* HAVE_VNC */ + +static void +print_help (void) +{ + /* For translators: First line of --help after usage line */ + const char *help_header = + _("Commands:\n"); +#ifdef HAVE_RDP + /* For translators: This first words on each line is the command; + * don't translate. Try to fit each line within 80 characters. */ + const char *help_rdp = + _(" rdp - RDP subcommands:\n" + " set-port - Set port the server binds to\n" + " enable - Enable the RDP backend\n" + " disable - Disable the RDP backend\n" + " set-auth-methods - Set a comma separated list of\n" + " allowed authentication methods\n" + " (credentials, kerberos)\n" + " set-tls-cert - Set path to TLS certificate\n" + " set-tls-key - Set path to TLS key\n" + " set-kerberos-keytab - Set path to a Kerberos keytab\n" + " set-credentials [ []] - Set username and password\n" + " credentials\n" + " clear-credentials - Clear username and password\n" + " credentials\n" + " enable-view-only - Disable remote control of input\n" + " devices\n" + " disable-view-only - Enable remote control of input\n" + " devices\n" + " enable-port-negotiation - If unavailable, listen to\n" + " a different port\n" + " disable-port-negotiation - If unavailable, don't listen\n" + " to a different port\n" + "\n"); +#endif /* HAVE_RDP */ +#ifdef HAVE_VNC + /* For translators: This first words on each line is the command; + * don't translate. Try to fit each line within 80 characters. */ + const char *help_vnc = + _(" vnc - VNC subcommands:\n" + " set-port - Set port the server binds to\n" + " enable - Enable the VNC backend\n" + " disable - Disable the VNC backend\n" + " set-password [] - Set the VNC password\n" + " clear-password - Clear the VNC password\n" + " set-auth-method password|prompt - Set the authorization method\n" + " enable-view-only - Disable remote control of input\n" + " devices\n" + " disable-view-only - Enable remote control of input\n" + " devices\n" + " enable-port-negotiation - If unavailable, listen to\n" + " a different port\n" + " disable-port-negotiation - If unavailable, don't listen\n" + " to a different port\n" + "\n"); +#endif /* HAVE_VNC */ + /* For translators: This first words on each line is the command; + * don't translate. Try to fit each line within 80 characters. */ + const char *help_other = + _(" status [--show-credentials] - Show current status\n" + "\n" + "Options:\n" + " --headless - Configure headless daemon\n" + " running on a user session\n" + " --system - Configure system daemon\n" + " for remote login\n" + " --help - Print this help text\n"); + + print_usage (); + printf ("%s" +#ifdef HAVE_RDP + "%s" +#endif +#ifdef HAVE_VNC + "%s" +#endif + "%s", + help_header, +#ifdef HAVE_RDP + help_rdp, +#endif +#ifdef HAVE_VNC + help_vnc, +#endif + help_other); +} + +static GrdSettings * +create_settings (GrdRuntimeMode runtime_mode) +{ + switch (runtime_mode) + { + case GRD_RUNTIME_MODE_SCREEN_SHARE: + return GRD_SETTINGS (grd_settings_user_new ()); + case GRD_RUNTIME_MODE_HEADLESS: + return GRD_SETTINGS (grd_settings_headless_new ()); + case GRD_RUNTIME_MODE_SYSTEM: + return GRD_SETTINGS (grd_settings_system_new ()); + case GRD_RUNTIME_MODE_HANDOVER: + g_assert_not_reached (); + } + + g_assert_not_reached (); +} + +static const char * +status_to_string (gboolean enabled, + gboolean use_colors) +{ + if (use_colors) + { + if (enabled) + return "\x1b[1menabled\033[m"; + else + return "\x1b[1mdisabled\033[m"; + } + else + { + if (enabled) + return "enabled"; + else + return "disabled"; + } +} + +#ifdef HAVE_RDP + +static char * +generate_auth_methods_string (GrdRdpAuthMethods auth_methods) +{ + g_autoptr (GStrvBuilder) builder = NULL; + g_auto (GStrv) auth_method_strings = NULL; + + builder = g_strv_builder_new (); + if (auth_methods & GRD_RDP_AUTH_METHOD_CREDENTIALS) + g_strv_builder_add (builder, "credentials"); + if (auth_methods & GRD_RDP_AUTH_METHOD_KERBEROS) + g_strv_builder_add (builder, "kerberos"); + + auth_method_strings = g_strv_builder_end (builder); + + return g_strjoinv (", ", auth_method_strings); +} + +static void +print_rdp_status (GrdSettings *settings, + gboolean use_colors, + gboolean show_credentials) +{ + GrdRdpAuthMethods auth_methods = 0; + g_autofree char *auth_methods_string = NULL; + g_autofree char *tls_fingerprint = NULL; + g_autofree char *tls_cert = NULL; + g_autofree char *tls_key = NULL; + g_autofree char *kerberos_keytab = NULL; + g_autofree char *username = NULL; + g_autofree char *password = NULL; + g_autoptr (GError) error = NULL; + gboolean negotiate_port; + gboolean view_only; + gboolean enabled; + int port; + + g_object_get (G_OBJECT (settings), + "rdp-auth-methods", &auth_methods, + "rdp-server-fingerprint", &tls_fingerprint, + "rdp-server-cert-path", &tls_cert, + "rdp-server-key-path", &tls_key, + "rdp-kerberos-keytab", &kerberos_keytab, + "rdp-negotiate-port", &negotiate_port, + "rdp-view-only", &view_only, + "rdp-enabled", &enabled, + "rdp-port", &port, + NULL); + + auth_methods_string = generate_auth_methods_string (auth_methods); + + printf ("RDP:\n"); + printf ("\tStatus: %s\n", status_to_string (enabled, use_colors)); + printf ("\tPort: %d\n", port); + printf ("\tAuthentication methods: %s\n", auth_methods_string); + printf ("\tTLS certificate: %s\n", tls_cert); + printf ("\tTLS fingerprint: %s\n", tls_fingerprint); + printf ("\tTLS key: %s\n", tls_key); + printf ("\tKerberos keytab: %s\n", kerberos_keytab); + if (!GRD_IS_SETTINGS_SYSTEM (settings) && !GRD_IS_SETTINGS_HEADLESS (settings)) + printf ("\tView-only: %s\n", view_only ? "yes" : "no"); + if (!GRD_IS_SETTINGS_SYSTEM (settings)) + printf ("\tNegotiate port: %s\n", negotiate_port ? "yes" : "no"); + + grd_settings_get_rdp_credentials (settings, + &username, &password, + &error); + if (error) + { + fprintf (stderr, "Failed to read credentials: %s.\n", error->message); + return; + } + + if (show_credentials) + { + printf ("\tUsername: %s\n", username); + printf ("\tPassword: %s\n", password); + } + else + { + printf ("\tUsername: %s\n", + (username && strlen (username) > 1) ? "(hidden)" : "(empty)"); + printf ("\tPassword: %s\n", + (password && strlen (password) > 1) ? "(hidden)" : "(empty)"); + } +} +#endif /* HAVE_RDP */ + +#ifdef HAVE_VNC +static void +print_vnc_status (GrdSettings *settings, + gboolean use_colors, + gboolean show_credentials) +{ + int port; + gboolean enabled; + gboolean view_only; + gboolean negotiate_port; + GrdVncAuthMethod auth_method; + g_autofree char *password = NULL; + g_autoptr (GError) error = NULL; + + g_object_get (G_OBJECT (settings), + "vnc-port", &port, + "vnc-enabled", &enabled, + "vnc-view-only", &view_only, + "vnc-auth-method", &auth_method, + "vnc-negotiate-port", &negotiate_port, + NULL); + + password = grd_settings_get_vnc_password (settings, &error); + if (error) + { + fprintf (stderr, "Failed to lookup VNC credentials: %s\n", error->message); + return; + } + + printf ("VNC:\n"); + printf ("\tStatus: %s\n", status_to_string (enabled, use_colors)); + + printf ("\tPort: %d\n", port); + if (!GRD_IS_SETTINGS_HEADLESS (settings)) + { + if (auth_method == GRD_VNC_AUTH_METHOD_PROMPT) + printf ("\tAuth method: prompt\n"); + else if (auth_method == GRD_VNC_AUTH_METHOD_PASSWORD) + printf ("\tAuth method: password\n"); + } + if (!GRD_IS_SETTINGS_HEADLESS (settings)) + printf ("\tView-only: %s\n", view_only ? "yes" : "no"); + printf ("\tNegotiate port: %s\n", negotiate_port ? "yes" : "no"); + if (show_credentials) + { + printf ("\tPassword: %s\n", password); + } + else + { + printf ("\tPassword: %s\n", + (password && strlen (password) > 1) ? "(hidden)" : "(empty)"); + } +} +#endif /* HAVE_VNC */ + +static char * +unit_active_state_to_string (GrdSystemdUnitActiveState active_state, + gboolean use_colors) +{ + char *state = ""; + + switch (active_state) + { + case GRD_SYSTEMD_UNIT_ACTIVE_STATE_ACTIVE: + state = "active"; + break; + case GRD_SYSTEMD_UNIT_ACTIVE_STATE_RELOADING: + state = "reloading"; + break; + case GRD_SYSTEMD_UNIT_ACTIVE_STATE_INACTIVE: + state = "inactive"; + break; + case GRD_SYSTEMD_UNIT_ACTIVE_STATE_FAILED: + state = "failed"; + break; + case GRD_SYSTEMD_UNIT_ACTIVE_STATE_ACTIVATING: + state = "activating"; + break; + case GRD_SYSTEMD_UNIT_ACTIVE_STATE_DEACTIVATING: + state = "deactivating"; + break; + case GRD_SYSTEMD_UNIT_ACTIVE_STATE_UNKNOWN: + state = "unknown"; + break; + } + + if (use_colors) + return g_strdup_printf ("\x1b[1m%s\033[m", state); + else + return g_strdup (state); +} + +static void +print_service_status (GrdSettings *settings, + gboolean use_colors) +{ + GBusType bus_type; + GrdSystemdUnitActiveState active_state; + g_autofree char *active_state_str = NULL; + g_autoptr (GDBusProxy) unit_proxy = NULL; + const char *unit; + + switch (grd_settings_get_runtime_mode (settings)) + { + case GRD_RUNTIME_MODE_HEADLESS: + bus_type = G_BUS_TYPE_SESSION; + unit = GRD_SYSTEMD_HEADLESS_SERVICE; + break; + case GRD_RUNTIME_MODE_SYSTEM: + bus_type = G_BUS_TYPE_SYSTEM; + unit = GRD_SYSTEMD_SERVICE; + break; + case GRD_RUNTIME_MODE_SCREEN_SHARE: + bus_type = G_BUS_TYPE_SESSION; + unit = GRD_SYSTEMD_SERVICE; + break; + default: + g_assert_not_reached (); + } + + if (!grd_systemd_get_unit (bus_type, + unit, + &unit_proxy, + NULL)) + return; + + if (!grd_systemd_unit_get_active_state (unit_proxy, + &active_state, + NULL)) + return; + + active_state_str = unit_active_state_to_string (active_state, use_colors); + + printf ("Overall:\n"); + printf ("\tUnit status: %s\n", active_state_str); +} + +static int +print_status (GrdSettings *settings, + int argc, + char **argv) +{ + gboolean use_colors; + gboolean show_credentials = FALSE; + + if (argc > 1) + { + return EX_USAGE; + } + else if (argc == 1) + { + if (strcmp (argv[0], "--show-credentials") == 0) + { + show_credentials = TRUE; + } + else + { + return EX_USAGE; + } + } + + use_colors = isatty (fileno (stdout)); + + print_service_status (settings, use_colors); + +#ifdef HAVE_RDP + print_rdp_status (settings, use_colors, show_credentials); +#endif /* HAVE_RDP */ +#ifdef HAVE_VNC + if (!GRD_IS_SETTINGS_SYSTEM (settings)) + print_vnc_status (settings, use_colors, show_credentials); +#endif /* HAVE_VNC */ + + return EXIT_SUCCESS; +} + +int +main (int argc, + char *argv[]) +{ + g_autoptr (GrdSettings) settings = NULL; + GrdRuntimeMode runtime_mode; + int i; + int arg_shift; + int exit_code = EX_USAGE; + struct passwd *pw; + + g_set_prgname (argv[0]); + g_log_set_handler (NULL, G_LOG_LEVEL_WARNING, log_handler, NULL); + + if (argc < 2) + goto done; + + if (argc == 2 && strcmp (argv[1], "--help") == 0) + { + print_help (); + exit_code = EXIT_SUCCESS; + goto done; + } + + if (argc > 2 && strcmp (argv[1], "--headless") == 0) + { + runtime_mode = GRD_RUNTIME_MODE_HEADLESS; + i = 2; + } + else if (argc > 1 && strcmp (argv[1], "--system") == 0) + { + runtime_mode = GRD_RUNTIME_MODE_SYSTEM; + i = 2; + } + else + { + runtime_mode = GRD_RUNTIME_MODE_SCREEN_SHARE; + i = 1; + } + + arg_shift = i + 1; + + if (runtime_mode == GRD_RUNTIME_MODE_SYSTEM) + { + g_autoptr (GStrvBuilder) builder = NULL; + g_auto (GStrv) new_argv = NULL; + g_autoptr (GError) error = NULL; + int wait_status; + int j; + + builder = g_strv_builder_new (); + + g_strv_builder_add (builder, "pkexec"); + g_strv_builder_add (builder, "--user"); + g_strv_builder_add (builder, GRD_USERNAME); + g_strv_builder_add (builder, argv[0]); + + for (j = 2; j < argc; j++) + g_strv_builder_add (builder, argv[j]); + + new_argv = g_strv_builder_end (builder); + + g_spawn_sync (NULL, + new_argv, + NULL, + G_SPAWN_SEARCH_PATH + | G_SPAWN_CHILD_INHERITS_STDIN + | G_SPAWN_CHILD_INHERITS_STDOUT, + NULL, + NULL, + NULL, + NULL, + &wait_status, + &error); + if (error) + { + fprintf (stderr, "Failed to start the configuration of the system daemon: %s\n", + error->message); + + if (WIFEXITED (wait_status)) + exit_code = WEXITSTATUS (wait_status); + else + exit_code = EX_SOFTWARE; + } + else + { + exit_code = EXIT_SUCCESS; + } + + goto done; + } + + pw = getpwnam (GRD_USERNAME); + if (geteuid () == pw->pw_uid) + runtime_mode = GRD_RUNTIME_MODE_SYSTEM; + + settings = create_settings (runtime_mode); + if (!settings) + { + fprintf (stderr, "Failed to initialize settings.\n"); + exit_code = EXIT_FAILURE; + goto done; + } + +#ifdef HAVE_RDP + if (strcmp (argv[i], "rdp") == 0) + { + exit_code = process_rdp_options (settings, + argc - arg_shift, argv + arg_shift); + goto done; + } +#endif +#ifdef HAVE_VNC + if (strcmp (argv[i], "vnc") == 0) + { + exit_code = process_vnc_options (settings, + argc - arg_shift, argv + arg_shift); + goto done; + } +#endif + + if (strcmp (argv[i], "status") == 0) + { + exit_code = print_status (settings, + argc - arg_shift, argv + arg_shift); + goto done; + } + +done: + if (exit_code == EX_USAGE) + print_usage (); + + return exit_code; +} diff --git a/grd-cuda-avc-utils.cu b/grd-cuda-avc-utils.cu new file mode 100644 index 0000000..2f1e197 --- /dev/null +++ b/grd-cuda-avc-utils.cu @@ -0,0 +1,178 @@ +/* + * Copyright (C) 2021 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +/* + * Generate the PTX instructions with: + * clang --cuda-gpu-arch=sm_30 -S src/grd-cuda-avc-utils.cu -o data/grd-cuda-avc-utils_30.ptx --no-cuda-version-check -O3 --cuda-device-only -Wall -Wextra + * + * or + * + * nvcc -arch=compute_30 -ptx grd-cuda-avc-utils.cu -o grd-cuda-avc-utils_30.ptx + * + * Note: This requires CUDA < 11, since the generation of Kepler capable + * PTX code was removed from CUDA 11. + */ + +#include + +extern "C" +{ + __device__ uint8_t + rgb_to_y (uint8_t r, + uint8_t g, + uint8_t b) + { + return (54 * r + 183 * g + 18 * b) >> 8; + } + + __device__ uint8_t + rgb_to_u (uint8_t r, + uint8_t g, + uint8_t b) + { + return ((-29 * r - 99 * g + 128 * b) >> 8) + 128; + } + + __device__ uint8_t + rgb_to_v (uint8_t r, + uint8_t g, + uint8_t b) + { + return ((128 * r - 116 * g - 12 * b) >> 8) + 128; + } + + __global__ void + convert_2x2_bgrx_area_to_yuv420_nv12 (uint8_t *dst_data, + uint32_t *src_data, + uint16_t src_width, + uint16_t src_height, + uint16_t aligned_width, + uint16_t aligned_height, + uint16_t aligned_stride) + { + uint8_t *dst_y0, *dst_y1, *dst_y2, *dst_y3, *dst_u, *dst_v; + uint32_t *src_u32; + uint16_t s0, s1, s2, s3; + uint32_t bgrx; + int32_t r_a, g_a, b_a; + uint8_t r, g, b; + uint16_t x_1x1, y_1x1; + uint16_t x_2x2, y_2x2; + + x_2x2 = blockIdx.x * blockDim.x + threadIdx.x; + y_2x2 = blockIdx.y * blockDim.y + threadIdx.y; + + if (x_2x2 >= aligned_width >> 1 || y_2x2 >= aligned_height >> 1) + return; + + /* + * ------------- + * | d_0 | d_1 | + * ------------- + * | d_2 | d_3 | + * ------------- + */ + s0 = 0; + s1 = 1; + s2 = src_width; + s3 = src_width + 1; + + x_1x1 = x_2x2 << 1; + y_1x1 = y_2x2 << 1; + src_u32 = src_data + y_1x1 * src_width + x_1x1; + + dst_y0 = dst_data + y_1x1 * aligned_stride + x_1x1; + dst_y1 = dst_y0 + 1; + dst_y2 = dst_data + (y_1x1 + 1) * aligned_stride + x_1x1; + dst_y3 = dst_y2 + 1; + dst_u = dst_data + aligned_height * aligned_stride + + y_2x2 * aligned_stride + x_1x1; + dst_v = dst_u + 1; + + /* d_0 */ + if (x_1x1 < src_width && y_1x1 < src_height) + { + bgrx = src_u32[s0]; + + b_a = b = *(((uint8_t *) &bgrx) + 0); + g_a = g = *(((uint8_t *) &bgrx) + 1); + r_a = r = *(((uint8_t *) &bgrx) + 2); + *dst_y0 = rgb_to_y (r, g, b); + } + else + { + b_a = b = 0; + g_a = g = 0; + r_a = r = 0; + *dst_y0 = 0; + } + + if (x_1x1 + 1 < src_width && y_1x1 < src_height) + { + bgrx = src_u32[s1]; + + /* d_1 */ + b_a += b = *(((uint8_t *) &bgrx) + 0); + g_a += g = *(((uint8_t *) &bgrx) + 1); + r_a += r = *(((uint8_t *) &bgrx) + 2); + *dst_y1 = rgb_to_y (r, g, b); + } + else + { + *dst_y1 = 0; + } + + if (x_1x1 < src_width && y_1x1 + 1 < src_height) + { + bgrx = src_u32[s2]; + + /* d_2 */ + b_a += b = *(((uint8_t *) &bgrx) + 0); + g_a += g = *(((uint8_t *) &bgrx) + 1); + r_a += r = *(((uint8_t *) &bgrx) + 2); + *dst_y2 = rgb_to_y (r, g, b); + + if (x_1x1 + 1 < src_width) + { + bgrx = src_u32[s3]; + + /* d_3 */ + b_a += b = *(((uint8_t *) &bgrx) + 0); + g_a += g = *(((uint8_t *) &bgrx) + 1); + r_a += r = *(((uint8_t *) &bgrx) + 2); + *dst_y3 = rgb_to_y (r, g, b); + } + else + { + *dst_y3 = 0; + } + } + else + { + *dst_y2 = 0; + *dst_y3 = 0; + } + + b_a >>= 2; + g_a >>= 2; + r_a >>= 2; + *dst_u = rgb_to_u (r_a, g_a, b_a); + *dst_v = rgb_to_v (r_a, g_a, b_a); + } +} diff --git a/grd-cuda-damage-utils.cu b/grd-cuda-damage-utils.cu new file mode 100644 index 0000000..d5913f0 --- /dev/null +++ b/grd-cuda-damage-utils.cu @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2021 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include + +extern "C" +{ + __global__ void + check_damaged_pixel (uint8_t *damage_array, + uint8_t *region_is_damaged, + uint32_t *current_data, + uint32_t *previous_data, + uint32_t damage_array_stride, + uint32_t data_width, + uint32_t data_height, + uint32_t data_stride) + { + uint32_t data_pos; + uint8_t damaged = 0; + uint32_t x, y; + + x = blockIdx.x * blockDim.x + threadIdx.x; + y = blockIdx.y * blockDim.y + threadIdx.y; + + if (x >= data_width || y >= data_height) + return; + + data_pos = y * data_stride + x; + if (previous_data[data_pos] != current_data[data_pos]) + { + damaged = 1; + *region_is_damaged = 1; + } + + damage_array[y * damage_array_stride + x] = damaged; + } + + __global__ void + combine_damage_array_cols (uint8_t *damage_array, + uint32_t damage_array_width, + uint32_t damage_array_height, + uint32_t damage_array_stride, + uint32_t combine_shift) + { + uint32_t data_pos; + uint32_t neighbour_offset; + uint32_t x, y; + uint32_t sx; + + sx = blockIdx.x * blockDim.x + threadIdx.x; + y = blockIdx.y * blockDim.y + threadIdx.y; + + x = sx << combine_shift + 1; + + if (x >= damage_array_width || y >= damage_array_height) + return; + + neighbour_offset = 1 << combine_shift; + if (x + neighbour_offset >= damage_array_width) + return; + + data_pos = y * damage_array_stride + x; + if (damage_array[data_pos + neighbour_offset]) + damage_array[data_pos] = 1; + } + + __global__ void + combine_damage_array_rows (uint8_t *damage_array, + uint32_t damage_array_width, + uint32_t damage_array_height, + uint32_t damage_array_stride, + uint32_t combine_shift) + { + uint32_t data_pos; + uint32_t neighbour_offset; + uint32_t x, y; + uint32_t sy; + + x = blockIdx.x * blockDim.x + threadIdx.x; + sy = blockIdx.y * blockDim.y + threadIdx.y; + + y = sy << combine_shift + 1; + + if (x >= damage_array_width || y >= damage_array_height) + return; + + neighbour_offset = 1 << combine_shift; + if (y + neighbour_offset >= damage_array_height) + return; + + data_pos = y * damage_array_stride + x; + if (damage_array[data_pos + neighbour_offset * damage_array_stride]) + damage_array[data_pos] = 1; + } + + __global__ void + simplify_damage_array (uint8_t *dst_damage_array, + uint8_t *src_damage_array, + uint32_t dst_damage_array_stride, + uint32_t src_damage_array_width, + uint32_t src_damage_array_height, + uint32_t src_damage_array_stride) + { + uint32_t src_data_pos, dst_data_pos; + uint32_t sx, sy; + uint32_t x, y; + + sx = blockIdx.x * blockDim.x + threadIdx.x; + sy = blockIdx.y * blockDim.y + threadIdx.y; + + x = sx << 6; + y = sy << 6; + + if (x >= src_damage_array_width || y >= src_damage_array_height) + return; + + src_data_pos = y * src_damage_array_stride + x; + dst_data_pos = sy * dst_damage_array_stride + sx; + + dst_damage_array[dst_data_pos] = src_damage_array[src_data_pos]; + } +} diff --git a/grd-daemon-handover.c b/grd-daemon-handover.c new file mode 100644 index 0000000..709f14b --- /dev/null +++ b/grd-daemon-handover.c @@ -0,0 +1,911 @@ +/* + * Copyright (C) 2023 SUSE Software Solutions Germany GmbH + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + * Written by: + * Joan Torres + */ + +#include "config.h" + +#include "grd-daemon-handover.h" + +#include +#include +#include + +#include "grd-context.h" +#include "grd-daemon.h" +#include "grd-daemon-utils.h" +#include "grd-dbus-remote-desktop.h" +#include "grd-private.h" +#include "grd-prompt.h" +#include "grd-rdp-server.h" +#include "grd-session-rdp.h" +#include "grd-settings.h" +#include "grd-shell-dialog.h" + +struct _GrdDaemonHandover +{ + GrdDaemon parent; + + GrdDBusRemoteDesktopRdpDispatcher *remote_desktop_dispatcher; + GrdDBusRemoteDesktopRdpHandover *remote_desktop_handover; + + GDBusObjectManager *handover_object_manager; + + GrdSession *session; + + gboolean use_system_credentials; + GrdPrompt *prompt; + GCancellable *prompt_cancellable; + + GrdShellDialog *dialog; + + unsigned int gnome_remote_desktop_watch_name_id; +}; + +G_DEFINE_TYPE (GrdDaemonHandover, grd_daemon_handover, GRD_TYPE_DAEMON) + +static void +on_remote_desktop_rdp_dispatcher_handover_requested (GObject *object, + GAsyncResult *result, + gpointer user_data); + +static gboolean +grd_daemon_handover_is_ready (GrdDaemon *daemon) +{ + GrdContext *context = grd_daemon_get_context (daemon); + + if (!grd_context_get_mutter_remote_desktop_proxy (context) || + !grd_context_get_mutter_screen_cast_proxy (context) || + !GRD_DAEMON_HANDOVER (daemon)->remote_desktop_handover) + return FALSE; + + return TRUE; +} + +static void +on_take_client_finished (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + int fd; + int fd_idx = -1; + g_autoptr (GError) error = NULL; + g_autoptr (GSocket) socket = NULL; + g_autoptr (GVariant) fd_variant = NULL; + g_autoptr (GUnixFDList) fd_list = NULL; + GrdDBusRemoteDesktopRdpHandover *proxy; + GrdDaemonHandover *daemon_handover; + GrdRdpServer *rdp_server; + GSocketConnection *socket_connection; + + proxy = GRD_DBUS_REMOTE_DESKTOP_RDP_HANDOVER (object); + if (!grd_dbus_remote_desktop_rdp_handover_call_take_client_finish ( + proxy, &fd_variant, &fd_list, result, &error)) + { + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return; + + g_warning ("[DaemonHandover] An error occurred while calling " + "TakeClient: %s", error->message); + return; + } + + g_variant_get (fd_variant, "h", &fd_idx); + if (!G_IS_UNIX_FD_LIST (fd_list) || + fd_idx < 0 || fd_idx >= g_unix_fd_list_get_length (fd_list)) + { + g_warning ("Failed to acquire file descriptor: The fd list or fd index " + "is invalid"); + return; + } + + fd = g_unix_fd_list_get (fd_list, fd_idx, &error); + if (fd == -1) + { + g_warning ("[DaemonHandover] Failed to acquire file descriptor: %s", + error->message); + return; + } + + socket = g_socket_new_from_fd (fd, &error); + if (!socket) + { + g_warning ("[DaemonHandover] Failed to create socket from fd: %s", + error->message); + return; + } + + daemon_handover = GRD_DAEMON_HANDOVER (user_data); + rdp_server = grd_daemon_get_rdp_server (GRD_DAEMON (daemon_handover)); + socket_connection = g_socket_connection_factory_create_connection (socket); + + grd_rdp_server_notify_incoming (G_SOCKET_SERVICE (rdp_server), + socket_connection); +} + +static void +on_get_system_credentials_finished (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + GrdDBusRemoteDesktopRdpHandover *remote_desktop_handover; + g_autoptr (GError) error = NULL; + g_autofree char *username = NULL; + g_autofree char *password = NULL; + GrdDaemonHandover *daemon_handover; + GrdContext *context; + GrdSettings *settings; + GCancellable *cancellable; + const char* object_path; + + remote_desktop_handover = GRD_DBUS_REMOTE_DESKTOP_RDP_HANDOVER (object); + if (!grd_dbus_remote_desktop_rdp_handover_call_get_system_credentials_finish ( + remote_desktop_handover, + &username, + &password, + result, + &error)) + { + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return; + + g_warning ("[DaemonHandover] Failed to get system credentials: %s", + error->message); + return; + } + + daemon_handover = GRD_DAEMON_HANDOVER (user_data); + context = grd_daemon_get_context (GRD_DAEMON (daemon_handover)); + settings = grd_context_get_settings (context); + + if (!grd_settings_set_rdp_credentials (settings, username, password, &error)) + { + g_warning ("[DaemonHanodver] Failed to overwrite credentials: %s", + error->message); + return; + } + + object_path = + g_dbus_proxy_get_object_path (G_DBUS_PROXY (remote_desktop_handover)); + + g_debug ("[DaemonHandover] At: %s, calling TakeClient", object_path); + + cancellable = grd_daemon_get_cancellable (GRD_DAEMON (daemon_handover)); + grd_dbus_remote_desktop_rdp_handover_call_take_client ( + remote_desktop_handover, + NULL, + cancellable, + on_take_client_finished, + daemon_handover); +} + +static void +on_take_client_ready (GrdDBusRemoteDesktopRdpHandover *interface, + gboolean use_system_credentials, + GrdDaemonHandover *daemon_handover) +{ + GCancellable *cancellable = + grd_daemon_get_cancellable (GRD_DAEMON (daemon_handover)); + const char* object_path; + + object_path = g_dbus_proxy_get_object_path (G_DBUS_PROXY (interface)); + + g_debug ("[DaemonHandover] At: %s, received TakeClientReady signal", + object_path); + + daemon_handover->use_system_credentials = use_system_credentials; + + if (use_system_credentials) + { + g_debug ("[DaemonHandover] At: %s, calling GetSystemCredentials", + object_path); + + grd_dbus_remote_desktop_rdp_handover_call_get_system_credentials ( + interface, + cancellable, + on_get_system_credentials_finished, + daemon_handover); + + return; + } + + g_debug ("[DaemonHandover] At: %s, calling TakeClient", object_path); + + grd_dbus_remote_desktop_rdp_handover_call_take_client ( + interface, + NULL, + cancellable, + on_take_client_finished, + daemon_handover); +} + +static void +on_start_handover_finished (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + GrdDBusRemoteDesktopRdpHandover *proxy; + g_autofree char *certificate = NULL; + g_autofree char *key = NULL; + g_autoptr (GError) error = NULL; + GrdDaemonHandover *daemon_handover; + GrdContext *context; + GrdSettings *settings; + + proxy = GRD_DBUS_REMOTE_DESKTOP_RDP_HANDOVER (object); + if (!grd_dbus_remote_desktop_rdp_handover_call_start_handover_finish ( + proxy, &certificate, &key, result, &error)) + { + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return; + + g_warning ("[DaemonHandover] Failed to start handover: %s", + error->message); + return; + } + + daemon_handover = GRD_DAEMON_HANDOVER (user_data); + context = grd_daemon_get_context (GRD_DAEMON (daemon_handover)); + settings = grd_context_get_settings (context); + + g_object_set (G_OBJECT (settings), + "rdp-server-cert", certificate, + "rdp-server-key", key, + NULL); +} + +static void +start_handover (GrdDaemonHandover *daemon_handover) +{ + GrdDaemon *daemon = GRD_DAEMON (daemon_handover); + GrdContext *context = grd_daemon_get_context (daemon); + GrdSettings *settings = grd_context_get_settings (context); + GCancellable *cancellable = grd_daemon_get_cancellable (daemon); + g_autofree char *username = NULL; + g_autofree char *password = NULL; + const char *object_path; + + if (!grd_settings_get_rdp_credentials (settings, + &username, &password, + NULL)) + g_assert_not_reached (); + + object_path = g_dbus_proxy_get_object_path ( + G_DBUS_PROXY (daemon_handover->remote_desktop_handover)); + + g_debug ("[DaemonHandover] At: %s, calling StartHandover", object_path); + + grd_dbus_remote_desktop_rdp_handover_call_start_handover ( + daemon_handover->remote_desktop_handover, + username, + password, + cancellable, + on_start_handover_finished, + daemon_handover); +} + +static void +on_session_stopped (GrdSession *session, + GrdDaemonHandover *daemon_handover) +{ + if (grd_is_remote_login ()) + grd_session_manager_call_logout_sync (); + + daemon_handover->session = NULL; +} + +static gboolean +show_insecure_connection_dialog (GrdDaemonHandover *daemon_handover) +{ + GCancellable *cancellable = + grd_daemon_get_cancellable (GRD_DAEMON (daemon_handover)); + + g_assert (!daemon_handover->dialog); + + daemon_handover->dialog = grd_shell_dialog_new (cancellable); + if (!daemon_handover->dialog) + return FALSE; + + g_signal_connect_swapped (daemon_handover->dialog, "cancelled", + G_CALLBACK (grd_session_stop), + daemon_handover->session); + + grd_shell_dialog_open (daemon_handover->dialog, + _("Continue With Insecure Connection?"), + /* Translators: Don't translate “use redirection server name:i:1”. + * It's a menu option, and it's the same for all languages. */ + _("This Remote Desktop connection is insecure. " + "To secure this connection, enable RDSTLS Security " + "in your client by saving the connection settings " + "in your client as an RDP file and set " + "“use redirection server name:i:1” in it."), + _("Disconnect"), + _("Continue")); + + return TRUE; +} + +static void +prompt_response_callback (GObject *source_object, + GAsyncResult *async_result, + gpointer user_data) +{ + GrdDaemonHandover *daemon_handover; + g_autoptr (GError) error = NULL; + GrdPromptResponse response; + + if (!grd_prompt_query_finish (GRD_PROMPT (source_object), + async_result, + &response, + &error)) + { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + { + daemon_handover = GRD_DAEMON_HANDOVER (user_data); + + g_warning ("Failed to query user about session: %s", error->message); + grd_session_stop (daemon_handover->session); + } + + return; + } + + daemon_handover = GRD_DAEMON_HANDOVER (user_data); + + switch (response) + { + case GRD_PROMPT_RESPONSE_ACCEPT: + return; + case GRD_PROMPT_RESPONSE_CANCEL: + grd_session_stop (daemon_handover->session); + return; + } + + g_assert_not_reached (); +} + +static void +show_insecure_connection_prompt (GrdDaemonHandover *daemon_handover) +{ + g_autoptr (GrdPromptDefinition) prompt_definition = NULL; + + g_assert (!daemon_handover->prompt); + + daemon_handover->prompt = g_object_new (GRD_TYPE_PROMPT, NULL); + daemon_handover->prompt_cancellable = g_cancellable_new (); + + prompt_definition = g_new0 (GrdPromptDefinition, 1); + prompt_definition->summary = + g_strdup_printf (_("Continue With Insecure Connection?")); + prompt_definition->body = + /* Translators: Don't translate “use redirection server name:i:1”. + * It's a menu option, and it's the same for all languages. */ + g_strdup_printf (_("This Remote Desktop connection is insecure. " + "To secure this connection, enable RDSTLS Security " + "in your client by saving the connection settings " + "in your client as an RDP file and set " + "“use redirection server name:i:1” in it.")); + prompt_definition->cancel_label = g_strdup_printf (_("Disconnect")); + prompt_definition->accept_label = g_strdup_printf (_("Continue")); + + grd_prompt_query_async (daemon_handover->prompt, + prompt_definition, + daemon_handover->prompt_cancellable, + prompt_response_callback, + daemon_handover); +} + +static void +inform_about_insecure_connection (GrdDaemonHandover *daemon_handover) +{ + gboolean could_display_dialog; + + could_display_dialog = show_insecure_connection_dialog (daemon_handover); + if (!could_display_dialog) + show_insecure_connection_prompt (daemon_handover); +} + +static void +on_redirect_client (GrdDBusRemoteDesktopRdpHandover *interface, + const char *routing_token, + const char *username, + const char *password, + GrdDaemonHandover *daemon_handover) +{ + const char *object_path = + g_dbus_proxy_get_object_path (G_DBUS_PROXY (interface)); + GrdContext *context = grd_daemon_get_context (GRD_DAEMON (daemon_handover)); + GrdSettings *settings = grd_context_get_settings (context); + GrdSessionRdp *session_rdp = GRD_SESSION_RDP (daemon_handover->session); + g_autofree char *certificate = NULL; + + g_debug ("[DaemonHandover] At: %s, received RedirectClient signal", + object_path); + + g_object_get (G_OBJECT (settings), + "rdp-server-cert", &certificate, + NULL); + + if (!grd_session_rdp_send_server_redirection (session_rdp, routing_token, + username, password, + certificate)) + grd_session_stop (daemon_handover->session); +} + +static void +on_handover_is_waiting_changed (GrdDBusRemoteDesktopRdpHandover *proxy, + GParamSpec *pspec, + GrdDaemonHandover *daemon_handover) +{ + gboolean handover_is_waiting; + + handover_is_waiting = + grd_dbus_remote_desktop_rdp_handover_get_handover_is_waiting ( + daemon_handover->remote_desktop_handover); + + if (!handover_is_waiting) + return; + + start_handover (daemon_handover); +} + +static void +on_incoming_new_connection (GrdRdpServer *rdp_server, + GrdSession *session, + GrdDaemonHandover *daemon_handover) +{ + GrdContext *context = grd_daemon_get_context (GRD_DAEMON (daemon_handover)); + GrdSettings *settings = grd_context_get_settings (context); + + if (daemon_handover->session) + { + g_signal_handlers_disconnect_by_func (daemon_handover->session, + G_CALLBACK (on_session_stopped), + daemon_handover); + grd_session_stop (daemon_handover->session); + } + + g_signal_connect (session, "stopped", + G_CALLBACK (on_session_stopped), + daemon_handover); + + daemon_handover->session = session; + + grd_settings_recreate_rdp_credentials (settings); + + if (daemon_handover->use_system_credentials && grd_is_remote_login ()) + inform_about_insecure_connection (daemon_handover); +} + +static void +setup_handover (GrdDaemonHandover *daemon_handover) +{ + GrdRdpServer *rdp_server; + gboolean handover_is_waiting; + + if (!daemon_handover->remote_desktop_handover) + return; + + rdp_server = grd_daemon_get_rdp_server (GRD_DAEMON (daemon_handover)); + if (!rdp_server) + return; + + g_signal_connect (daemon_handover->remote_desktop_handover, "take-client-ready", + G_CALLBACK (on_take_client_ready), daemon_handover); + g_signal_connect (daemon_handover->remote_desktop_handover, "redirect-client", + G_CALLBACK (on_redirect_client), daemon_handover); + g_signal_connect (daemon_handover->remote_desktop_handover, "notify::handover-is-waiting", + G_CALLBACK (on_handover_is_waiting_changed), daemon_handover); + + g_signal_connect (rdp_server, "incoming-new-connection", + G_CALLBACK (on_incoming_new_connection), daemon_handover); + + handover_is_waiting = grd_dbus_remote_desktop_rdp_handover_get_handover_is_waiting ( + daemon_handover->remote_desktop_handover); + + if (handover_is_waiting) + start_handover (daemon_handover); +} + +static void +teardown_handover (GrdDaemonHandover *daemon_handover) +{ + GrdRdpServer *rdp_server = + grd_daemon_get_rdp_server (GRD_DAEMON (daemon_handover)); + + if (daemon_handover->remote_desktop_handover) + { + g_signal_handlers_disconnect_by_func (daemon_handover->remote_desktop_handover, + G_CALLBACK (on_take_client_ready), + daemon_handover); + g_signal_handlers_disconnect_by_func (daemon_handover->remote_desktop_handover, + G_CALLBACK (on_redirect_client), + daemon_handover); + g_signal_handlers_disconnect_by_func (daemon_handover->remote_desktop_handover, + G_CALLBACK (on_handover_is_waiting_changed), + daemon_handover); + } + + if (rdp_server) + { + g_signal_handlers_disconnect_by_func (rdp_server, + G_CALLBACK (on_incoming_new_connection), + daemon_handover); + } +} + +static void +on_rdp_server_started (GrdDaemonHandover *daemon_handover) +{ + setup_handover (daemon_handover); +} + +static void +on_rdp_server_stopped (GrdDaemonHandover *daemon_handover) +{ + teardown_handover (daemon_handover); +} + +static void +on_handover_object_added (GDBusObjectManager *manager, + GDBusObject *object, + GrdDaemonHandover *daemon_handover) +{ + g_autofree char *session_id = NULL; + g_autofree char *expected_object_path = NULL; + const char *object_path; + GCancellable *cancellable; + + session_id = grd_get_session_id_from_pid (getpid ()); + if (!session_id) + { + g_warning ("[DaemonHandover] Could not get session id"); + return; + } + + expected_object_path = g_strdup_printf ("%s/session%s", + REMOTE_DESKTOP_HANDOVERS_OBJECT_PATH, + session_id); + object_path = g_dbus_object_get_object_path (object); + + if (g_strcmp0 (object_path, expected_object_path) != 0) + { + g_debug ("[DaemonHandover] Ignoring handover object at: %s, " + "expected: %s", object_path, expected_object_path); + return; + } + + cancellable = grd_daemon_get_cancellable (GRD_DAEMON (daemon_handover)); + grd_dbus_remote_desktop_rdp_dispatcher_call_request_handover ( + daemon_handover->remote_desktop_dispatcher, + cancellable, + on_remote_desktop_rdp_dispatcher_handover_requested, + daemon_handover); +} + +static void +on_handover_object_manager_acquired (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr (GDBusObjectManager) manager = NULL; + g_autoptr (GError) error = NULL; + GrdDaemonHandover *daemon_handover; + + manager = + grd_dbus_remote_desktop_object_manager_client_new_for_bus_finish (result, + &error); + if (!manager) + { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("[DaemonHandover] Failed to create handover object manager: %s", + error->message); + return; + } + + daemon_handover = GRD_DAEMON_HANDOVER (user_data); + daemon_handover->handover_object_manager = g_steal_pointer (&manager); + + g_debug ("[DaemonHandover] Watching handover objects"); + + g_signal_connect (daemon_handover->handover_object_manager, "object-added", + G_CALLBACK (on_handover_object_added), daemon_handover); +} + +static void +start_watching_handover_objects (GrdDaemonHandover *daemon_handover) +{ + GCancellable *cancellable; + + if (daemon_handover->handover_object_manager) + return; + + cancellable = grd_daemon_get_cancellable (GRD_DAEMON (daemon_handover)); + + grd_dbus_remote_desktop_object_manager_client_new_for_bus ( + G_BUS_TYPE_SYSTEM, + G_DBUS_OBJECT_MANAGER_CLIENT_FLAGS_DO_NOT_AUTO_START, + REMOTE_DESKTOP_BUS_NAME, + REMOTE_DESKTOP_HANDOVERS_OBJECT_PATH, + cancellable, + on_handover_object_manager_acquired, + daemon_handover); +} + +static void +stop_watching_handover_objects (GrdDaemonHandover *daemon_handover) +{ + if (!daemon_handover->handover_object_manager) + return; + + g_signal_handlers_disconnect_by_func (daemon_handover->handover_object_manager, + G_CALLBACK (on_handover_object_added), + daemon_handover); + + g_clear_object (&daemon_handover->handover_object_manager); +} + +static void +on_remote_desktop_rdp_handover_proxy_acquired (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + GrdDaemonHandover *daemon_handover; + g_autoptr (GrdDBusRemoteDesktopRdpHandover) proxy = NULL; + g_autoptr (GError) error = NULL; + + proxy = + grd_dbus_remote_desktop_rdp_handover_proxy_new_for_bus_finish (result, + &error); + if (!proxy) + { + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return; + + g_warning ("[DaemonHandover] Failed to create remote desktop handover " + "proxy: %s", error->message); + return; + } + + daemon_handover = GRD_DAEMON_HANDOVER (user_data); + + stop_watching_handover_objects (daemon_handover); + + daemon_handover->remote_desktop_handover = g_steal_pointer (&proxy); + + setup_handover (daemon_handover); + + grd_daemon_maybe_enable_services (GRD_DAEMON (daemon_handover)); +} + +static void +on_remote_desktop_rdp_dispatcher_handover_requested (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + GrdDBusRemoteDesktopRdpDispatcher *remote_desktop_dispatcher; + g_autofree char *object_path = NULL; + g_autoptr (GError) error = NULL; + GrdDaemonHandover *daemon_handover; + GCancellable *cancellable; + gboolean success; + + remote_desktop_dispatcher = GRD_DBUS_REMOTE_DESKTOP_RDP_DISPATCHER (object); + success = + grd_dbus_remote_desktop_rdp_dispatcher_call_request_handover_finish ( + remote_desktop_dispatcher, + &object_path, + result, + &error); + if (!success) + { + if (g_error_matches (error, GRD_DBUS_ERROR, GRD_DBUS_ERROR_NO_HANDOVER)) + { + daemon_handover = GRD_DAEMON_HANDOVER (user_data); + start_watching_handover_objects (daemon_handover); + } + else if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + { + g_warning ("[DaemonHandover] Failed to request remote desktop " + "handover: %s", error->message); + } + return; + } + + g_debug ("[DaemonHandover] Using: %s, from dispatcher request", + object_path); + + daemon_handover = GRD_DAEMON_HANDOVER (user_data); + cancellable = grd_daemon_get_cancellable (GRD_DAEMON (daemon_handover)); + + grd_dbus_remote_desktop_rdp_handover_proxy_new_for_bus ( + G_BUS_TYPE_SYSTEM, + G_DBUS_PROXY_FLAGS_NONE, + REMOTE_DESKTOP_BUS_NAME, + object_path, + cancellable, + on_remote_desktop_rdp_handover_proxy_acquired, + daemon_handover); +} + +static void +on_remote_desktop_rdp_dispatcher_proxy_acquired (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr (GrdDBusRemoteDesktopRdpDispatcher) proxy = NULL; + g_autoptr (GError) error = NULL; + GrdDaemonHandover *daemon_handover; + GCancellable *cancellable; + + proxy = + grd_dbus_remote_desktop_rdp_dispatcher_proxy_new_finish (result, &error); + if (!proxy) + { + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return; + + g_warning ("[DaemonHandover] Failed to create remote desktop " + "dispatcher proxy: %s", error->message); + return; + } + + daemon_handover = GRD_DAEMON_HANDOVER (user_data); + + g_assert (!daemon_handover->remote_desktop_dispatcher); + daemon_handover->remote_desktop_dispatcher = g_steal_pointer (&proxy); + + cancellable = grd_daemon_get_cancellable (GRD_DAEMON (daemon_handover)); + grd_dbus_remote_desktop_rdp_dispatcher_call_request_handover ( + daemon_handover->remote_desktop_dispatcher, + cancellable, + on_remote_desktop_rdp_dispatcher_handover_requested, + daemon_handover); +} + +static void +on_gnome_remote_desktop_name_appeared (GDBusConnection *connection, + const char *name, + const char *name_owner, + gpointer user_data) +{ + GrdDaemonHandover *daemon_handover = user_data; + GCancellable *cancellable = + grd_daemon_get_cancellable (GRD_DAEMON (daemon_handover)); + + g_debug ("[DaemonHandover] %s name appeared", name); + + grd_dbus_remote_desktop_rdp_dispatcher_proxy_new ( + connection, + G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START, + REMOTE_DESKTOP_BUS_NAME, + REMOTE_DESKTOP_DISPATCHER_OBJECT_PATH, + cancellable, + on_remote_desktop_rdp_dispatcher_proxy_acquired, + daemon_handover); +} + +static void +on_gnome_remote_desktop_name_vanished (GDBusConnection *connection, + const char *name, + gpointer user_data) +{ + GrdDaemonHandover *daemon_handover = user_data; + + g_warning ("[DaemonHandover] %s name vanished", name); + + teardown_handover (daemon_handover); + + g_clear_object (&daemon_handover->remote_desktop_handover); + g_clear_object (&daemon_handover->remote_desktop_dispatcher); + + stop_watching_handover_objects (daemon_handover); + + if (grd_is_remote_login ()) + grd_session_manager_call_logout_sync (); +} + +GrdDaemonHandover * +grd_daemon_handover_new (GError **error) +{ + GrdContext *context; + + context = grd_context_new (GRD_RUNTIME_MODE_HANDOVER, error); + if (!context) + return NULL; + + return g_object_new (GRD_TYPE_DAEMON_HANDOVER, + "application-id", GRD_DAEMON_HANDOVER_APPLICATION_ID, + "flags", G_APPLICATION_IS_SERVICE, + "context", context, + NULL); +} + +static void +grd_daemon_handover_init (GrdDaemonHandover *daemon_handover) +{ +} + +static void +grd_daemon_handover_startup (GApplication *app) +{ + GrdDaemonHandover *daemon_handover = GRD_DAEMON_HANDOVER (app); + + grd_daemon_acquire_mutter_dbus_proxies (GRD_DAEMON (daemon_handover)); + + g_signal_connect (daemon_handover, "mutter-proxy-acquired", + G_CALLBACK (grd_daemon_maybe_enable_services), NULL); + + daemon_handover->gnome_remote_desktop_watch_name_id = + g_bus_watch_name (G_BUS_TYPE_SYSTEM, + REMOTE_DESKTOP_BUS_NAME, + G_BUS_NAME_WATCHER_FLAGS_NONE, + on_gnome_remote_desktop_name_appeared, + on_gnome_remote_desktop_name_vanished, + daemon_handover, NULL); + + g_signal_connect (daemon_handover, "rdp-server-started", + G_CALLBACK (on_rdp_server_started), NULL); + + g_signal_connect (daemon_handover, "rdp-server-stopped", + G_CALLBACK (on_rdp_server_stopped), NULL); + + G_APPLICATION_CLASS (grd_daemon_handover_parent_class)->startup (app); +} + +static void +grd_daemon_handover_shutdown (GApplication *app) +{ + GrdDaemonHandover *daemon_handover = GRD_DAEMON_HANDOVER (app); + GrdDaemon *daemon = GRD_DAEMON (daemon_handover); + + g_cancellable_cancel (grd_daemon_get_cancellable (daemon)); + + g_clear_object (&daemon_handover->remote_desktop_handover); + g_clear_object (&daemon_handover->remote_desktop_dispatcher); + + stop_watching_handover_objects (daemon_handover); + + g_clear_handle_id (&daemon_handover->gnome_remote_desktop_watch_name_id, + g_bus_unwatch_name); + + if (daemon_handover->prompt_cancellable) + { + g_cancellable_cancel (daemon_handover->prompt_cancellable); + g_clear_object (&daemon_handover->prompt_cancellable); + } + g_clear_object (&daemon_handover->prompt); + + g_clear_object (&daemon_handover->dialog); + + G_APPLICATION_CLASS (grd_daemon_handover_parent_class)->shutdown (app); +} + +static void +grd_daemon_handover_class_init (GrdDaemonHandoverClass *klass) +{ + GApplicationClass *g_application_class = G_APPLICATION_CLASS (klass); + GrdDaemonClass *daemon_class = GRD_DAEMON_CLASS (klass); + + g_application_class->startup = grd_daemon_handover_startup; + g_application_class->shutdown = grd_daemon_handover_shutdown; + + daemon_class->is_daemon_ready = grd_daemon_handover_is_ready; +} diff --git a/grd-daemon-handover.h b/grd-daemon-handover.h new file mode 100644 index 0000000..2c6a51c --- /dev/null +++ b/grd-daemon-handover.h @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2023 SUSE Software Solutions Germany GmbH + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + * Written by: + * Joan Torres + */ + +#pragma once + +#include "grd-daemon.h" + +#define GRD_TYPE_DAEMON_HANDOVER (grd_daemon_handover_get_type ()) +G_DECLARE_FINAL_TYPE (GrdDaemonHandover, grd_daemon_handover, + GRD, DAEMON_HANDOVER, GrdDaemon) + +GrdDaemonHandover *grd_daemon_handover_new (GError **error); diff --git a/grd-daemon-system.c b/grd-daemon-system.c new file mode 100644 index 0000000..58b1f71 --- /dev/null +++ b/grd-daemon-system.c @@ -0,0 +1,1421 @@ +/* + * Copyright (C) 2023 SUSE Software Solutions Germany GmbH + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + * Written by: + * Joan Torres + */ + +#include "config.h" + +#include "grd-daemon-system.h" + +#include + +#include "grd-context.h" +#include "grd-daemon.h" +#include "grd-daemon-utils.h" +#include "grd-dbus-gdm.h" +#include "grd-dbus-remote-desktop.h" +#include "grd-private.h" +#include "grd-rdp-server.h" +#include "grd-session-rdp.h" +#include "grd-settings.h" +#include "grd-utils.h" + +#define MAX_HANDOVER_WAIT_TIME_S 30 + +typedef struct +{ + GrdDBusRemoteDesktopRdpHandover *interface; + GDBusObjectSkeleton *skeleton; + char *object_path; + char *sender_name; +} HandoverInterface; + +typedef struct +{ + GrdDaemonSystem *daemon_system; + + char *id; + + GrdSession *session; + GSocketConnection *socket_connection; + + HandoverInterface *handover_src; + HandoverInterface *handover_dst; + + unsigned int abort_handover_source_id; + + gboolean is_client_mstsc; + gboolean use_system_credentials; + gboolean needs_handover; + + GrdDBusGdmRemoteDisplay *remote_display; +} GrdRemoteClient; + +struct _GrdDaemonSystem +{ + GrdDaemon parent; + + GrdDBusGdmRemoteDisplayFactory *remote_display_factory_proxy; + GDBusObjectManager *display_objects; + + unsigned int system_grd_name_id; + GrdDBusRemoteDesktopRdpDispatcher *dispatcher_skeleton; + GDBusObjectManagerServer *handover_manager_server; + + GHashTable *remote_clients; +}; + +G_DEFINE_TYPE (GrdDaemonSystem, grd_daemon_system, GRD_TYPE_DAEMON) + +typedef void (*ForeachRemoteDisplayCallback) (GrdDaemonSystem *daemon_system, + GrdDBusGdmRemoteDisplay *remote_display); + +static void +grd_remote_client_free (GrdRemoteClient *remote_client); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (GrdRemoteClient, grd_remote_client_free) + +static void +on_remote_display_remote_id_changed (GrdDBusGdmRemoteDisplay *remote_display, + GParamSpec *pspec, + GrdRemoteClient *remote_client); + +static void +on_gdm_remote_display_session_id_changed (GrdDBusGdmRemoteDisplay *remote_display, + GParamSpec *pspec, + GrdRemoteClient *remote_client); + +static void +disconnect_from_remote_display (GrdRemoteClient *remote_client) +{ + if (!remote_client->remote_display) + return; + + g_signal_handlers_disconnect_by_func (remote_client->remote_display, + G_CALLBACK (on_remote_display_remote_id_changed), + remote_client); + g_signal_handlers_disconnect_by_func (remote_client->remote_display, + G_CALLBACK (on_gdm_remote_display_session_id_changed), + remote_client); + + g_clear_object (&remote_client->remote_display); +} + +static gboolean +grd_daemon_system_is_ready (GrdDaemon *daemon) +{ + GrdDaemonSystem *daemon_system = GRD_DAEMON_SYSTEM (daemon); + GDBusProxy *remote_display_factory_proxy = + G_DBUS_PROXY (daemon_system->remote_display_factory_proxy); + GDBusObjectManagerClient *display_objects_manager = + G_DBUS_OBJECT_MANAGER_CLIENT (daemon_system->display_objects); + GDBusInterfaceSkeleton *dispatcher_skeleton = + G_DBUS_INTERFACE_SKELETON (daemon_system->dispatcher_skeleton); + g_autofree const char *gdm_remote_display_factory_name_owner = NULL; + g_autofree const char *gdm_display_objects_name_owner = NULL; + g_autoptr (GDBusConnection) manager_connection = NULL; + GDBusConnection *dispatcher_connection = NULL; + + if (!daemon_system->remote_display_factory_proxy || + !daemon_system->display_objects || + !daemon_system->handover_manager_server || + !daemon_system->dispatcher_skeleton) + return FALSE; + + gdm_remote_display_factory_name_owner = g_dbus_proxy_get_name_owner ( + remote_display_factory_proxy); + gdm_display_objects_name_owner = g_dbus_object_manager_client_get_name_owner ( + display_objects_manager); + manager_connection = g_dbus_object_manager_server_get_connection ( + daemon_system->handover_manager_server); + dispatcher_connection = g_dbus_interface_skeleton_get_connection ( + dispatcher_skeleton); + + if (!gdm_remote_display_factory_name_owner || + !gdm_display_objects_name_owner || + !manager_connection || + !dispatcher_connection) + return FALSE; + + return TRUE; +} + +static gboolean +on_handle_take_client (GrdDBusRemoteDesktopRdpHandover *interface, + GDBusMethodInvocation *invocation, + GUnixFDList *list, + GrdRemoteClient *remote_client) +{ + GSocket *socket = NULL; + GVariant *fd_variant = NULL; + g_autoptr (GUnixFDList) fd_list = NULL; + int fd; + int fd_idx; + + g_assert (interface == remote_client->handover_dst->interface); + + g_debug ("[DaemonSystem] At: %s, received TakeClient call", + remote_client->handover_dst->object_path); + + socket = g_socket_connection_get_socket (remote_client->socket_connection); + fd = g_socket_get_fd (socket); + + fd_list = g_unix_fd_list_new (); + fd_idx = g_unix_fd_list_append (fd_list, fd, NULL); + fd_variant = g_variant_new_handle (fd_idx); + + grd_dbus_remote_desktop_rdp_handover_complete_take_client (interface, + invocation, + fd_list, + fd_variant); + + grd_close_connection_and_notify (remote_client->socket_connection); + g_clear_object (&remote_client->socket_connection); + g_clear_handle_id (&remote_client->abort_handover_source_id, g_source_remove); + + grd_dbus_remote_desktop_rdp_handover_set_handover_is_waiting (interface, + FALSE); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static void +abort_handover (gpointer user_data) +{ + GrdRemoteClient *remote_client = user_data; + GrdDaemonSystem *daemon_system = remote_client->daemon_system; + + g_warning ("[DaemonSystem] Aborting handover, removing remote client with " + "remote id %s", remote_client->id); + + if (remote_client->session) + { + grd_session_rdp_notify_error (GRD_SESSION_RDP (remote_client->session), + GRD_SESSION_RDP_ERROR_SERVER_REDIRECTION); + } + + g_hash_table_remove (daemon_system->remote_clients, remote_client->id); +} + +static char * +get_id_from_routing_token (uint32_t routing_token) +{ + return g_strdup_printf (REMOTE_DESKTOP_CLIENT_OBJECT_PATH "/%u", + routing_token); +} + +static char * +get_routing_token_from_id (const char *id) +{ + g_assert (strlen (id) > strlen (REMOTE_DESKTOP_CLIENT_OBJECT_PATH "/")); + + return g_strdup (id + strlen (REMOTE_DESKTOP_CLIENT_OBJECT_PATH "/")); +} + +static char * +get_session_id_of_sender (GDBusConnection *connection, + const char *name, + GCancellable *cancellable, + GError **error) +{ + char *session_id = NULL; + gboolean success; + pid_t pid = 0; + uid_t uid = 0; + + success = grd_get_pid_of_sender_sync (connection, + name, + &pid, + cancellable, + error); + if (!success) + return NULL; + + session_id = grd_get_session_id_from_pid (pid); + if (session_id) + return session_id; + + success = grd_get_uid_of_sender_sync (connection, + name, + &uid, + cancellable, + error); + if (!success) + return NULL; + + session_id = grd_get_session_id_from_uid (uid); + if (!session_id) + { + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_NOT_FOUND, + "Could not find a session for user %d", + (int) uid); + } + + return session_id; +} + +static char * +get_handover_object_path_for_call (GrdDaemonSystem *daemon_system, + GDBusMethodInvocation *invocation, + GError **error) +{ + g_autoptr (GDBusObject) object = NULL; + g_autofree char *object_path = NULL; + g_autofree char *session_id = NULL; + GDBusConnection *connection = NULL; + const char *sender = NULL; + GCancellable *cancellable; + + connection = g_dbus_method_invocation_get_connection (invocation); + sender = g_dbus_method_invocation_get_sender (invocation); + cancellable = grd_daemon_get_cancellable (GRD_DAEMON (daemon_system)); + + session_id = get_session_id_of_sender (connection, + sender, + cancellable, + error); + if (!session_id) + return NULL; + + object_path = g_strdup_printf ("%s/session%s", + REMOTE_DESKTOP_HANDOVERS_OBJECT_PATH, + session_id); + + object = g_dbus_object_manager_get_object ( + G_DBUS_OBJECT_MANAGER (daemon_system->handover_manager_server), + object_path); + if (!object) + { + g_set_error (error, + GRD_DBUS_ERROR, + GRD_DBUS_ERROR_NO_HANDOVER, + "No handover interface for session"); + return NULL; + } + + return g_steal_pointer (&object_path); +} + +static gboolean +on_authorize_handover_method (GrdDBusRemoteDesktopRdpHandover *interface, + GDBusMethodInvocation *invocation, + gpointer user_data) +{ + GrdRemoteClient *remote_client = user_data; + g_autofree char *object_path = NULL; + g_autoptr (GError) error = NULL; + + g_assert (interface == remote_client->handover_dst->interface); + g_assert (remote_client->handover_dst->object_path); + + object_path = get_handover_object_path_for_call (remote_client->daemon_system, + invocation, + &error); + if (!object_path) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return FALSE; + } + + if (g_strcmp0 (object_path, remote_client->handover_dst->object_path) != 0) + { + g_dbus_method_invocation_return_error_literal (invocation, G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Caller using incorrect " + "handover"); + return FALSE; + } + + return TRUE; +} + +static gboolean +on_handle_start_handover (GrdDBusRemoteDesktopRdpHandover *interface, + GDBusMethodInvocation *invocation, + const char *username, + const char *password, + GrdRemoteClient *remote_client) +{ + GrdDaemon *daemon = GRD_DAEMON (remote_client->daemon_system); + GrdContext *context = grd_daemon_get_context (daemon); + GrdSettings *settings = grd_context_get_settings (context); + g_autofree char *key = NULL; + g_autofree char *certificate = NULL; + g_autofree char *routing_token = NULL; + g_autoptr (GVariant) redirect_variant = NULL; + GDBusConnection *connection; + const char *sender; + + g_assert (interface == remote_client->handover_dst->interface); + + g_debug ("[DaemonSystem] At: %s, received StartHandover call", + remote_client->handover_dst->object_path ); + + if (!remote_client->session && !remote_client->handover_src) + { + g_warning ("[DaemonSystem] RDP client disconnected during the handover"); + goto err; + } + + routing_token = get_routing_token_from_id (remote_client->id); + + g_object_get (G_OBJECT (settings), + "rdp-server-cert", &certificate, + "rdp-server-key", &key, + NULL); + + /* The remote client is at daemon-system */ + if (remote_client->session) + { + if (!grd_session_rdp_send_server_redirection ( + GRD_SESSION_RDP (remote_client->session), + routing_token, + username, + password, + certificate)) + goto err; + } + else + { + connection = g_dbus_interface_skeleton_get_connection ( + G_DBUS_INTERFACE_SKELETON ( + remote_client->handover_src->interface)); + redirect_variant = g_variant_new ("(sss)", + routing_token, + username, + password); + g_dbus_connection_emit_signal (connection, + remote_client->handover_src->sender_name, + remote_client->handover_src->object_path, + "org.gnome.RemoteDesktop.Rdp.Handover", + "RedirectClient", + redirect_variant, + NULL); + } + + sender = g_dbus_method_invocation_get_sender (invocation); + remote_client->handover_dst->sender_name = g_strdup (sender); + + grd_dbus_remote_desktop_rdp_handover_complete_start_handover ( + remote_client->handover_dst->interface, + invocation, + certificate, + key); + + if (remote_client->abort_handover_source_id == 0) + { + remote_client->abort_handover_source_id = + g_timeout_add_seconds_once (MAX_HANDOVER_WAIT_TIME_S, + abort_handover, + remote_client); + } + + return G_DBUS_METHOD_INVOCATION_HANDLED; + +err: + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_UNKNOWN_OBJECT, + "Failed to start handover"); + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +on_handle_get_system_credentials (GrdDBusRemoteDesktopRdpHandover *interface, + GDBusMethodInvocation *invocation, + GrdRemoteClient *remote_client) +{ + GrdDaemon *daemon = GRD_DAEMON (remote_client->daemon_system); + GrdContext *context = grd_daemon_get_context (daemon); + GrdSettings *settings = grd_context_get_settings (context); + g_autofree char *username = NULL; + g_autofree char *password = NULL; + + g_assert (interface == remote_client->handover_dst->interface); + + g_debug ("[DaemonSystem] At: %s, received GetSystemCredentials call", + remote_client->handover_dst->object_path); + + if (!remote_client->use_system_credentials) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_IO_ERROR, + "This handover daemon isn't " + "allowed to use system credentials"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + if (!grd_settings_get_rdp_credentials (settings, + &username, &password, + NULL)) + g_assert_not_reached (); + + grd_dbus_remote_desktop_rdp_handover_complete_get_system_credentials ( + remote_client->handover_dst->interface, + invocation, + username, + password); + + remote_client->use_system_credentials = FALSE; + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static void +handover_iface_free (HandoverInterface *handover) +{ + g_clear_object (&handover->skeleton); + g_clear_object (&handover->interface); + g_clear_pointer (&handover->object_path, g_free); + g_clear_pointer (&handover->sender_name, g_free); + g_free (handover); +} + +static HandoverInterface * +handover_iface_new (const char *session_id, + GrdRemoteClient *remote_client) +{ + HandoverInterface *handover; + + handover = g_new0 (HandoverInterface, 1); + + handover->object_path = g_strdup_printf ("%s/session%s", + REMOTE_DESKTOP_HANDOVERS_OBJECT_PATH, + session_id); + + handover->skeleton = g_dbus_object_skeleton_new (handover->object_path); + + handover->interface = grd_dbus_remote_desktop_rdp_handover_skeleton_new (); + g_signal_connect (handover->interface, "g-authorize-method", + G_CALLBACK (on_authorize_handover_method), remote_client); + g_signal_connect (handover->interface, "handle-start-handover", + G_CALLBACK (on_handle_start_handover), remote_client); + g_signal_connect (handover->interface, "handle-take-client", + G_CALLBACK (on_handle_take_client), remote_client); + g_signal_connect (handover->interface, "handle-get-system-credentials", + G_CALLBACK (on_handle_get_system_credentials), remote_client); + + grd_dbus_remote_desktop_rdp_handover_set_handover_is_waiting ( + handover->interface, remote_client->needs_handover); + + return handover; +} + +static void +register_handover_iface (GrdRemoteClient *remote_client, + const char *session_id) +{ + GrdDaemonSystem *daemon_system = remote_client->daemon_system; + HandoverInterface *handover; + + handover = handover_iface_new (session_id, remote_client); + + g_debug ("[DaemonSystem] Registering handover at: %s", + handover->object_path); + + g_dbus_object_manager_server_export (daemon_system->handover_manager_server, + handover->skeleton); + + g_dbus_object_skeleton_add_interface ( + handover->skeleton, + G_DBUS_INTERFACE_SKELETON (handover->interface)); + + g_clear_pointer (&remote_client->handover_src, handover_iface_free); + remote_client->handover_src = remote_client->handover_dst; + remote_client->handover_dst = handover; +} + +static void +unregister_handover_iface (GrdRemoteClient *remote_client, + HandoverInterface *handover) +{ + GrdDaemonSystem *daemon_system = remote_client->daemon_system; + + if (!handover) + return; + + g_debug ("[DaemonSystem] Unregistering handover at: %s", + handover->object_path); + + g_dbus_object_skeleton_remove_interface ( + handover->skeleton, + G_DBUS_INTERFACE_SKELETON (handover->interface)); + + g_dbus_object_manager_server_unexport (daemon_system->handover_manager_server, + handover->object_path); +} + +static void +grd_remote_client_free (GrdRemoteClient *remote_client) +{ + disconnect_from_remote_display (remote_client); + + g_clear_pointer (&remote_client->id, g_free); + if (remote_client->socket_connection) + grd_close_connection_and_notify (remote_client->socket_connection); + g_clear_object (&remote_client->socket_connection); + unregister_handover_iface (remote_client, remote_client->handover_src); + unregister_handover_iface (remote_client, remote_client->handover_dst); + g_clear_pointer (&remote_client->handover_src, handover_iface_free); + g_clear_pointer (&remote_client->handover_dst, handover_iface_free); + g_clear_handle_id (&remote_client->abort_handover_source_id, g_source_remove); + + g_free (remote_client); +} + +static char * +get_next_available_id (GrdDaemonSystem *daemon_system) +{ + uint32_t routing_token = 0; + + while (routing_token == 0) + { + g_autofree char *id = NULL; + + routing_token = g_random_int (); + id = get_id_from_routing_token (routing_token); + + if (!g_hash_table_contains (daemon_system->remote_clients, id)) + break; + + routing_token = 0; + } + + return get_id_from_routing_token (routing_token); +} + +static void +session_disposed (GrdRemoteClient *remote_client) +{ + remote_client->session = NULL; +} + +static GrdRemoteClient * +remote_client_new (GrdDaemonSystem *daemon_system, + GrdSession *session) +{ + GrdRemoteClient *remote_client; + + remote_client = g_new0 (GrdRemoteClient, 1); + remote_client->id = get_next_available_id (daemon_system); + remote_client->daemon_system = daemon_system; + remote_client->needs_handover = session != NULL; + + if (!session) + return remote_client; + + remote_client->is_client_mstsc = grd_session_rdp_is_client_mstsc (GRD_SESSION_RDP (session)); + remote_client->session = session; + g_object_weak_ref (G_OBJECT (session), + (GWeakNotify) session_disposed, + remote_client); + + remote_client->abort_handover_source_id = + g_timeout_add_seconds_once (MAX_HANDOVER_WAIT_TIME_S, + abort_handover, + remote_client); + + return remote_client; +} + +static void +on_create_remote_display_finished (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + GrdDBusGdmRemoteDisplayFactory *proxy = + GRD_DBUS_GDM_REMOTE_DISPLAY_FACTORY (object); + GrdRemoteClient *remote_client = user_data; + g_autoptr (GError) error = NULL; + + if (!grd_dbus_gdm_remote_display_factory_call_create_remote_display_finish ( + proxy, result, &error)) + { + g_warning ("[DaemonSystem] Error while calling CreateRemoteDisplay on " + "DisplayMananger: %s", error->message); + abort_handover (remote_client); + } +} + +static void +on_incoming_redirected_connection (GrdRdpServer *rdp_server, + const char *routing_token_str, + gboolean requested_rdstls, + GSocketConnection *connection, + GrdDaemonSystem *daemon_system) +{ + g_autofree char *remote_id = NULL; + GrdRemoteClient *remote_client; + uint64_t routing_token; + gboolean success; + + success = g_ascii_string_to_unsigned (routing_token_str, 10, 0, UINT32_MAX, + &routing_token, NULL); + if (!success) + { + g_autofree char *encoded_token = NULL; + + g_warning ("[DaemonSystem] Incoming client connection used " + "invalid routing token"); + + encoded_token = g_base64_encode ((unsigned char *) routing_token_str, + strlen (routing_token_str)); + g_debug ("[DaemonSystem] Invalid routing token: %s", encoded_token); + return; + } + + g_debug ("[DaemonSystem] Incoming connection with routing token: %u", + (uint32_t) routing_token); + + remote_id = get_id_from_routing_token ((uint32_t) routing_token); + if (!g_hash_table_lookup_extended (daemon_system->remote_clients, + remote_id, NULL, + (gpointer *) &remote_client)) + { + g_warning ("[DaemonSystem] Could not find routing token on " + "remote_clients list"); + return; + } + + remote_client->socket_connection = g_object_ref (connection); + remote_client->use_system_credentials = remote_client->is_client_mstsc && + !requested_rdstls; + + g_debug ("[DaemonSystem] At: %s, emitting TakeClientReady signal", + remote_client->handover_dst->object_path); + + grd_dbus_remote_desktop_rdp_handover_emit_take_client_ready ( + remote_client->handover_dst->interface, + remote_client->use_system_credentials); +} + +static void +on_incoming_new_connection (GrdRdpServer *rdp_server, + GrdSession *session, + GrdDaemonSystem *daemon_system) +{ + GCancellable *cancellable = + grd_daemon_get_cancellable (GRD_DAEMON (daemon_system)); + GrdRemoteClient *remote_client; + + g_debug ("[DaemonSystem] Incoming connection without routing token"); + + remote_client = remote_client_new (daemon_system, session); + + g_hash_table_insert (daemon_system->remote_clients, + remote_client->id, + remote_client); + + g_debug ("[DaemonSystem] Creating remote display with remote id: %s", + remote_client->id); + + grd_dbus_gdm_remote_display_factory_call_create_remote_display ( + daemon_system->remote_display_factory_proxy, + remote_client->id, + cancellable, + on_create_remote_display_finished, + remote_client); +} + +static void +inform_configuration_service (void) +{ + g_autoptr (GrdDBusRemoteDesktopConfigurationRdpServer) configuration = NULL; + + configuration = + grd_dbus_remote_desktop_configuration_rdp_server_proxy_new_for_bus_sync ( + G_BUS_TYPE_SYSTEM, + G_DBUS_PROXY_FLAGS_NONE, + REMOTE_DESKTOP_CONFIGURATION_BUS_NAME, + REMOTE_DESKTOP_CONFIGURATION_OBJECT_PATH, + NULL, + NULL); +} + +static void +on_rdp_server_started (GrdDaemonSystem *daemon_system) +{ + GrdRdpServer *rdp_server = + grd_daemon_get_rdp_server (GRD_DAEMON (daemon_system)); + + g_signal_connect (rdp_server, "incoming-new-connection", + G_CALLBACK (on_incoming_new_connection), + daemon_system); + g_signal_connect (rdp_server, "incoming-redirected-connection", + G_CALLBACK (on_incoming_redirected_connection), + daemon_system); + + inform_configuration_service (); +} + +static void +on_rdp_server_stopped (GrdDaemonSystem *daemon_system) +{ + GrdRdpServer *rdp_server = + grd_daemon_get_rdp_server (GRD_DAEMON (daemon_system)); + + g_signal_handlers_disconnect_by_func (rdp_server, + G_CALLBACK (on_incoming_new_connection), + daemon_system); + g_signal_handlers_disconnect_by_func (rdp_server, + G_CALLBACK (on_incoming_redirected_connection), + daemon_system); + + inform_configuration_service (); +} + +static void +get_handover_object_path_for_call_in_thread (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + GDBusMethodInvocation *invocation = task_data; + GrdDaemonSystem *daemon_system = source_object; + GError *error = NULL; + char *object_path; + + object_path = get_handover_object_path_for_call (daemon_system, + invocation, + &error); + if (error) + { + g_task_return_error (task, error); + return; + } + + g_task_return_pointer (task, object_path, g_free); +} + +static void +get_handover_object_path_for_call_async (gpointer source_object, + gpointer user_data, + GAsyncReadyCallback on_finished_callback) +{ + GrdDaemonSystem *daemon_system = source_object; + GCancellable *cancellable = + grd_daemon_get_cancellable (GRD_DAEMON (daemon_system)); + GTask *task; + + task = g_task_new (daemon_system, cancellable, on_finished_callback, user_data); + g_task_set_task_data (task, user_data, NULL); + g_task_run_in_thread (task, get_handover_object_path_for_call_in_thread); + g_object_unref (task); +} + +static void +on_get_handover_object_path_finished (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GrdDaemonSystem *daemon_system = GRD_DAEMON_SYSTEM (source_object); + GDBusMethodInvocation *invocation = user_data; + g_autofree char *object_path = NULL; + g_autoptr (GError) error = NULL; + + object_path = g_task_propagate_pointer (G_TASK (result), &error); + if (error) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return; + } + + grd_dbus_remote_desktop_rdp_dispatcher_complete_request_handover ( + daemon_system->dispatcher_skeleton, + invocation, + object_path); +} + +static gboolean +on_handle_request_handover (GrdDBusRemoteDesktopRdpDispatcher *skeleton, + GDBusMethodInvocation *invocation, + gpointer user_data) +{ + GrdDaemonSystem *daemon_system = user_data; + + get_handover_object_path_for_call_async (daemon_system, + invocation, + on_get_handover_object_path_finished); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static void +on_system_grd_bus_acquired (GDBusConnection *connection, + const char *name, + gpointer user_data) +{ + GrdDaemonSystem *daemon_system = user_data; + + g_debug ("[DaemonSystem] Now on system bus"); + + g_dbus_object_manager_server_set_connection ( + daemon_system->handover_manager_server, + connection); + + g_dbus_interface_skeleton_export ( + G_DBUS_INTERFACE_SKELETON (daemon_system->dispatcher_skeleton), + connection, + REMOTE_DESKTOP_DISPATCHER_OBJECT_PATH, + NULL); + + grd_daemon_maybe_enable_services (GRD_DAEMON (daemon_system)); +} + +static void +on_system_grd_name_acquired (GDBusConnection *connection, + const char *name, + gpointer user_data) +{ + g_debug ("[DaemonSystem] Owned %s name", name); +} + +static void +on_system_grd_name_lost (GDBusConnection *connection, + const char *name, + gpointer user_data) +{ + g_debug ("[DaemonSystem] Lost owned %s name", name); +} + +static void +on_remote_display_factory_name_owner_changed (GrdDBusGdmRemoteDisplayFactory *remote_display_factory_proxy, + GParamSpec *pspec, + GrdDaemonSystem *daemon_system) +{ + g_autofree const char *name_owner = NULL; + + g_object_get (G_OBJECT (remote_display_factory_proxy), + "g-name-owner", &name_owner, + NULL); + if (!name_owner) + { + grd_daemon_disable_services (GRD_DAEMON (daemon_system)); + return; + } + + grd_daemon_maybe_enable_services (GRD_DAEMON (daemon_system)); +} + +static void +on_remote_display_factory_proxy_acquired (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + GrdDaemonSystem *daemon_system = user_data; + g_autoptr (GError) error = NULL; + + daemon_system->remote_display_factory_proxy = + grd_dbus_gdm_remote_display_factory_proxy_new_for_bus_finish (result, + &error); + if (!daemon_system->remote_display_factory_proxy) + { + g_warning ("[DaemonSystem] Failed to acquire GDM remote display " + "factory proxy: %s", error->message); + return; + } + + g_signal_connect (G_OBJECT (daemon_system->remote_display_factory_proxy), + "notify::g-name-owner", + G_CALLBACK (on_remote_display_factory_name_owner_changed), + daemon_system); + + grd_daemon_maybe_enable_services (GRD_DAEMON (daemon_system)); +} + +static void +steal_handover_from_client (GrdRemoteClient *new_remote_client, + GrdRemoteClient *old_remote_client) +{ + HandoverInterface *handover; + + handover = g_steal_pointer (&old_remote_client->handover_dst); + + g_signal_handlers_disconnect_by_func (handover->interface, + G_CALLBACK (on_authorize_handover_method), + old_remote_client); + g_signal_handlers_disconnect_by_func (handover->interface, + G_CALLBACK (on_handle_start_handover), + old_remote_client); + g_signal_handlers_disconnect_by_func (handover->interface, + G_CALLBACK (on_handle_take_client), + old_remote_client); + g_signal_handlers_disconnect_by_func (handover->interface, + G_CALLBACK (on_handle_get_system_credentials), + old_remote_client); + + g_signal_connect (handover->interface, "g-authorize-method", + G_CALLBACK (on_authorize_handover_method), + new_remote_client); + g_signal_connect (handover->interface, "handle-start-handover", + G_CALLBACK (on_handle_start_handover), + new_remote_client); + g_signal_connect (handover->interface, "handle-take-client", + G_CALLBACK (on_handle_take_client), + new_remote_client); + g_signal_connect (handover->interface, "handle-get-system-credentials", + G_CALLBACK (on_handle_get_system_credentials), + new_remote_client); + + g_clear_pointer (&new_remote_client->handover_src, handover_iface_free); + new_remote_client->handover_src = new_remote_client->handover_dst; + new_remote_client->handover_dst = handover; +} + +static void +on_remote_display_remote_id_changed (GrdDBusGdmRemoteDisplay *remote_display, + GParamSpec *pspec, + GrdRemoteClient *remote_client) +{ + GrdDaemonSystem *daemon_system = remote_client->daemon_system; + GrdRemoteClient *new_remote_client; + const char *remote_id; + + remote_id = grd_dbus_gdm_remote_display_get_remote_id (remote_display); + if (!g_hash_table_lookup_extended (daemon_system->remote_clients, + remote_id, NULL, + (gpointer *) &new_remote_client)) + { + g_debug ("[DaemonSystem] GDM set to a remote display a remote " + "id %s we didn't know about", remote_id); + return; + } + + if (remote_client == new_remote_client) + return; + + g_debug ("[DaemonSystem] GDM updated a remote display with a new remote id: " + "%s", remote_id); + + disconnect_from_remote_display (new_remote_client); + new_remote_client->remote_display = g_object_ref (remote_display); + g_signal_connect (remote_display, "notify::remote-id", + G_CALLBACK (on_remote_display_remote_id_changed), + new_remote_client); + + steal_handover_from_client (new_remote_client, remote_client); + + grd_dbus_remote_desktop_rdp_handover_set_handover_is_waiting ( + new_remote_client->handover_dst->interface, TRUE); + + g_hash_table_remove (daemon_system->remote_clients, remote_client->id); +} + +static void +on_gdm_remote_display_session_id_changed (GrdDBusGdmRemoteDisplay *remote_display, + GParamSpec *pspec, + GrdRemoteClient *remote_client) +{ + const char *session_id; + + session_id = grd_dbus_gdm_remote_display_get_session_id (remote_display); + + g_signal_handlers_disconnect_by_func (remote_display, + G_CALLBACK (on_gdm_remote_display_session_id_changed), + remote_client); + + if (!session_id || g_str_equal (session_id, "")) + return; + + g_debug ("[DaemonSystem] Found a new remote display with remote id: %s " + "and session: %s", + remote_client->id, + session_id); + + g_signal_connect (remote_display, "notify::remote-id", + G_CALLBACK (on_remote_display_remote_id_changed), + remote_client); + + register_handover_iface (remote_client, session_id); +} + +static GrdRemoteClient * +remote_client_new_from_display (GrdDaemonSystem *daemon_system, + GrdDBusGdmRemoteDisplay *remote_display) +{ + g_autoptr (GrdRemoteClient) remote_client = NULL; + g_autoptr (GError) error = NULL; + + remote_client = remote_client_new (daemon_system, NULL); + + if (!grd_dbus_gdm_remote_display_call_set_remote_id_sync (remote_display, + remote_client->id, + NULL, + &error)) + { + g_warning ("[DaemonSystem] Failed to set remote_id on display: %s", + error->message); + return NULL; + } + + return g_steal_pointer (&remote_client); +} + +static GrdRemoteClient * +get_remote_client_from_remote_display (GrdDaemonSystem *daemon_system, + GrdDBusGdmRemoteDisplay *remote_display) +{ + GrdRemoteClient *remote_client = NULL; + g_autoptr (GError) error = NULL; + const char *remote_id; + + remote_id = grd_dbus_gdm_remote_display_get_remote_id (remote_display); + if (g_hash_table_lookup_extended (daemon_system->remote_clients, + remote_id, NULL, + (gpointer *) &remote_client)) + return remote_client; + + remote_client = remote_client_new_from_display (daemon_system, + remote_display); + if (!remote_client) + return NULL; + + g_hash_table_insert (daemon_system->remote_clients, + remote_client->id, + remote_client); + + return remote_client; +} + +static void +register_handover_for_display (GrdDaemonSystem *daemon_system, + GrdDBusGdmRemoteDisplay *remote_display) +{ + GrdRemoteClient *remote_client; + const char *session_id; + + remote_client = get_remote_client_from_remote_display (daemon_system, + remote_display); + if (!remote_client) + return; + + disconnect_from_remote_display (remote_client); + remote_client->remote_display = g_object_ref (remote_display); + + session_id = grd_dbus_gdm_remote_display_get_session_id (remote_display); + if (!session_id || strcmp (session_id, "") == 0) + { + g_signal_connect (remote_display, "notify::session-id", + G_CALLBACK (on_gdm_remote_display_session_id_changed), + remote_client); + return; + } + + g_debug ("[DaemonSystem] Found a new remote display with remote id: %s " + "and session: %s", + remote_client->id, + session_id); + + g_signal_connect (remote_display, "notify::remote-id", + G_CALLBACK (on_remote_display_remote_id_changed), + remote_client); + + register_handover_iface (remote_client, session_id); +} + +static void +unregister_handover_for_display (GrdDaemonSystem *daemon_system, + GrdDBusGdmRemoteDisplay *remote_display) +{ + g_autofree const char *object_path = NULL; + GrdRemoteClient *remote_client; + const char *session_id; + const char *remote_id; + + remote_id = grd_dbus_gdm_remote_display_get_remote_id (remote_display); + if (!g_hash_table_lookup_extended (daemon_system->remote_clients, + remote_id, NULL, + (gpointer *) &remote_client)) + { + g_debug ("[DaemonSystem] Tried to unregister handover for a remote " + "display with remote id %s we didn't know about", remote_id); + return; + } + + session_id = grd_dbus_gdm_remote_display_get_session_id (remote_display); + if (!session_id || g_str_equal (session_id, "")) + { + if (!remote_client->handover_src && !remote_client->handover_dst) + g_hash_table_remove (daemon_system->remote_clients, remote_id); + + return; + } + + object_path = g_strdup_printf ("%s/session%s", + REMOTE_DESKTOP_HANDOVERS_OBJECT_PATH, + session_id); + + g_debug ("[DaemonSystem] Unregistering handover for remote display with " + "remote id: %s and session: %s", remote_client->id, session_id); + + if (remote_client->handover_src) + { + if (strcmp (object_path, remote_client->handover_src->object_path) == 0) + { + unregister_handover_iface (remote_client, remote_client->handover_src); + g_clear_pointer (&remote_client->handover_src, handover_iface_free); + } + } + + if (remote_client->handover_dst) + { + if (strcmp (object_path, remote_client->handover_dst->object_path) == 0) + g_hash_table_remove (daemon_system->remote_clients, remote_id); + } +} + +static void +on_gdm_object_remote_display_interface_changed (GrdDaemonSystem *daemon_system, + GParamSpec *param_spec, + GrdDBusGdmObject *object) +{ + GrdDBusGdmRemoteDisplay *remote_display; + + remote_display = grd_dbus_gdm_object_peek_remote_display (object); + if (remote_display) + register_handover_for_display (daemon_system, remote_display); +} + +static void +foreach_remote_display (GrdDaemonSystem *daemon_system, + ForeachRemoteDisplayCallback callback) +{ + GList *objects, *l; + + objects = g_dbus_object_manager_get_objects (daemon_system->display_objects); + for (l = objects; l; l = l->next) + { + GrdDBusGdmObject *object = l->data; + GrdDBusGdmRemoteDisplay *remote_display; + + remote_display = grd_dbus_gdm_object_peek_remote_display (object); + if (remote_display) + callback (daemon_system, remote_display); + } + + g_list_free_full (objects, g_object_unref); +} + +static void +on_gdm_object_added (GrdDaemonSystem *daemon_system, + GrdDBusGdmObject *object) +{ + GrdDBusGdmRemoteDisplay *remote_display; + + remote_display = grd_dbus_gdm_object_peek_remote_display (object); + if (remote_display) + { + register_handover_for_display (daemon_system, remote_display); + return; + } + + g_signal_connect_object ( + G_OBJECT (object), + "notify::remote-display", + G_CALLBACK (on_gdm_object_remote_display_interface_changed), + daemon_system, + G_CONNECT_SWAPPED); +} + +static void +on_gdm_object_removed (GrdDaemonSystem *daemon_system, + GrdDBusGdmObject *object) +{ + GrdDBusGdmRemoteDisplay *remote_display; + + remote_display = grd_dbus_gdm_object_peek_remote_display (object); + if (remote_display) + unregister_handover_for_display (daemon_system, remote_display); + + g_signal_handlers_disconnect_by_func ( + G_OBJECT (object), + G_CALLBACK (on_gdm_object_remote_display_interface_changed), + daemon_system); +} + +static void +on_gdm_display_objects_name_owner_changed (GDBusObjectManager *display_objects, + GParamSpec *pspec, + GrdDaemonSystem *daemon_system) +{ + g_autofree const char *name_owner = NULL; + + g_object_get (G_OBJECT (display_objects), "name-owner", &name_owner, NULL); + if (!name_owner) + { + grd_daemon_disable_services (GRD_DAEMON (daemon_system)); + return; + } + + grd_daemon_maybe_enable_services (GRD_DAEMON (daemon_system)); +} + +static void +on_gdm_object_manager_client_acquired (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GrdDaemonSystem *daemon_system = user_data; + g_autoptr (GError) error = NULL; + + daemon_system->display_objects = + grd_dbus_gdm_object_manager_client_new_for_bus_finish (result, &error); + if (!daemon_system->display_objects) + { + g_warning ("[DaemonSystem] Error connecting to display manager: %s", + error->message); + return; + } + + foreach_remote_display (daemon_system, register_handover_for_display); + + g_signal_connect_object (daemon_system->display_objects, + "object-added", + G_CALLBACK (on_gdm_object_added), + daemon_system, + G_CONNECT_SWAPPED); + + g_signal_connect_object (daemon_system->display_objects, + "object-removed", + G_CALLBACK (on_gdm_object_removed), + daemon_system, + G_CONNECT_SWAPPED); + + g_signal_connect (G_OBJECT (daemon_system->display_objects), + "notify::name-owner", + G_CALLBACK (on_gdm_display_objects_name_owner_changed), + daemon_system); + + grd_daemon_maybe_enable_services (GRD_DAEMON (daemon_system)); +} + +GrdDaemonSystem * +grd_daemon_system_new (GError **error) +{ + GrdContext *context; + + context = grd_context_new (GRD_RUNTIME_MODE_SYSTEM, error); + if (!context) + return NULL; + + return g_object_new (GRD_TYPE_DAEMON_SYSTEM, + "application-id", GRD_DAEMON_SYSTEM_APPLICATION_ID, + "flags", G_APPLICATION_IS_SERVICE, + "context", context, + NULL); +} + +static void +grd_daemon_system_init (GrdDaemonSystem *daemon_system) +{ + daemon_system->remote_clients = + g_hash_table_new_full (g_str_hash, g_str_equal, + NULL, (GDestroyNotify) grd_remote_client_free); +} + +static void +grd_daemon_system_startup (GApplication *app) +{ + GrdDaemonSystem *daemon_system = GRD_DAEMON_SYSTEM (app); + GCancellable *cancellable = + grd_daemon_get_cancellable (GRD_DAEMON (daemon_system)); + g_autoptr (GError) error = NULL; + + daemon_system->dispatcher_skeleton = + grd_dbus_remote_desktop_rdp_dispatcher_skeleton_new (); + g_signal_connect (daemon_system->dispatcher_skeleton, + "handle-request-handover", + G_CALLBACK (on_handle_request_handover), + daemon_system); + + daemon_system->handover_manager_server = + g_dbus_object_manager_server_new (REMOTE_DESKTOP_HANDOVERS_OBJECT_PATH); + + daemon_system->system_grd_name_id = + g_bus_own_name (G_BUS_TYPE_SYSTEM, + REMOTE_DESKTOP_BUS_NAME, + G_BUS_NAME_OWNER_FLAGS_NONE, + on_system_grd_bus_acquired, + on_system_grd_name_acquired, + on_system_grd_name_lost, + daemon_system, NULL); + + grd_dbus_gdm_remote_display_factory_proxy_new_for_bus ( + G_BUS_TYPE_SYSTEM, + G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START, + GDM_BUS_NAME, + GDM_REMOTE_DISPLAY_FACTORY_OBJECT_PATH, + cancellable, + on_remote_display_factory_proxy_acquired, + daemon_system); + + grd_dbus_gdm_object_manager_client_new_for_bus ( + G_BUS_TYPE_SYSTEM, + G_DBUS_OBJECT_MANAGER_CLIENT_FLAGS_DO_NOT_AUTO_START, + GDM_BUS_NAME, + GDM_OBJECT_MANAGER_OBJECT_PATH, + cancellable, + on_gdm_object_manager_client_acquired, + daemon_system); + + g_signal_connect (daemon_system, "rdp-server-started", + G_CALLBACK (on_rdp_server_started), NULL); + + g_signal_connect (daemon_system, "rdp-server-stopped", + G_CALLBACK (on_rdp_server_stopped), NULL); + + G_APPLICATION_CLASS (grd_daemon_system_parent_class)->startup (app); +} + +static void +grd_daemon_system_shutdown (GApplication *app) +{ + GrdDaemonSystem *daemon_system = GRD_DAEMON_SYSTEM (app); + + g_clear_pointer (&daemon_system->remote_clients, g_hash_table_unref); + + g_clear_object (&daemon_system->display_objects); + g_clear_object (&daemon_system->remote_display_factory_proxy); + + g_dbus_interface_skeleton_unexport ( + G_DBUS_INTERFACE_SKELETON (daemon_system->dispatcher_skeleton)); + g_clear_object (&daemon_system->dispatcher_skeleton); + + g_clear_object (&daemon_system->handover_manager_server); + g_clear_handle_id (&daemon_system->system_grd_name_id, + g_bus_unown_name); + + G_APPLICATION_CLASS (grd_daemon_system_parent_class)->shutdown (app); +} + +static void +grd_daemon_system_class_init (GrdDaemonSystemClass *klass) +{ + GApplicationClass *g_application_class = G_APPLICATION_CLASS (klass); + GrdDaemonClass *daemon_class = GRD_DAEMON_CLASS (klass); + + g_application_class->startup = grd_daemon_system_startup; + g_application_class->shutdown = grd_daemon_system_shutdown; + + daemon_class->is_daemon_ready = grd_daemon_system_is_ready; +} diff --git a/grd-daemon-system.h b/grd-daemon-system.h new file mode 100644 index 0000000..3e8fbf5 --- /dev/null +++ b/grd-daemon-system.h @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2023 SUSE Software Solutions Germany GmbH + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + * Written by: + * Joan Torres + */ + +#pragma once + +#include "grd-daemon.h" + +#define GRD_TYPE_DAEMON_SYSTEM (grd_daemon_system_get_type ()) +G_DECLARE_FINAL_TYPE (GrdDaemonSystem, grd_daemon_system, + GRD, DAEMON_SYSTEM, GrdDaemon) + +GrdDaemonSystem *grd_daemon_system_new (GError **error); diff --git a/grd-daemon-user.c b/grd-daemon-user.c new file mode 100644 index 0000000..29b068f --- /dev/null +++ b/grd-daemon-user.c @@ -0,0 +1,291 @@ +/* + * Copyright (C) 2022 Red Hat Inc. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + * Written by: + * Jonas Ådahl + */ + +#include "config.h" + +#include "grd-daemon-user.h" + +#include + +#include "grd-private.h" +#include "grd-prompt.h" + +struct _GrdDaemonUser +{ + GrdDaemon parent; + +#ifdef HAVE_RDP + GrdDBusRemoteDesktopRdpServer *system_rdp_server; + + GrdPrompt *prompt; + GCancellable *prompt_cancellable; +#endif /* HAVE_RDP */ +}; + +G_DEFINE_TYPE (GrdDaemonUser, grd_daemon_user, GRD_TYPE_DAEMON) + +#ifdef HAVE_RDP +static void +on_rdp_server_stopped (GrdDaemonUser *daemon_user) +{ + if (daemon_user->prompt_cancellable) + { + g_cancellable_cancel (daemon_user->prompt_cancellable); + g_clear_object (&daemon_user->prompt_cancellable); + } + g_clear_object (&daemon_user->prompt); + + g_signal_handlers_disconnect_by_func (daemon_user, + G_CALLBACK (on_rdp_server_stopped), + NULL); +} + +static void +prompt_response_callback (GObject *source_object, + GAsyncResult *async_result, + gpointer user_data) +{ + GrdDaemonUser *daemon_user = GRD_DAEMON_USER (user_data); + g_autoptr (GError) error = NULL; + GrdPromptResponse response; + + if (!grd_prompt_query_finish (daemon_user->prompt, + async_result, + &response, + &error)) + { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + { + g_warning ("Failed to query user about port change: %s", + error->message); + } + } + + g_clear_object (&daemon_user->prompt); + g_clear_object (&daemon_user->prompt_cancellable); +} + +static void +show_port_changed_prompt (GrdDaemonUser *daemon_user, + const int system_rdp_port, + const int selected_rdp_port) +{ + g_autoptr (GrdPromptDefinition) prompt_definition = NULL; + + daemon_user->prompt = g_object_new (GRD_TYPE_PROMPT, NULL); + daemon_user->prompt_cancellable = g_cancellable_new (); + + prompt_definition = g_new0 (GrdPromptDefinition, 1); + prompt_definition->summary = + g_strdup_printf (_("Desktop Sharing port changed")); + prompt_definition->body = + g_strdup_printf (_("The Desktop Sharing port has changed from %i to " + "%i.\n" + "This is because the Remote Sessions service has been " + "activated and it has a higher priority to use port " + "%i."), + system_rdp_port, selected_rdp_port, system_rdp_port); + prompt_definition->accept_label = g_strdup_printf (_("Accept")); + + grd_prompt_query_async (daemon_user->prompt, + prompt_definition, + daemon_user->prompt_cancellable, + prompt_response_callback, + daemon_user); +} + +static void +on_rdp_server_started (GrdDaemonUser *daemon_user) +{ + GrdContext *context = grd_daemon_get_context (GRD_DAEMON (daemon_user)); + GrdDBusRemoteDesktopRdpServer *rdp_server_iface = + grd_context_get_rdp_server_interface (context); + int selected_rdp_port = + grd_dbus_remote_desktop_rdp_server_get_port (rdp_server_iface); + int system_rdp_port = + grd_dbus_remote_desktop_rdp_server_get_port (daemon_user->system_rdp_server); + + g_assert (selected_rdp_port != system_rdp_port); + + show_port_changed_prompt (daemon_user, system_rdp_port, selected_rdp_port); + + g_signal_handlers_disconnect_by_func (daemon_user, + G_CALLBACK (on_rdp_server_started), + NULL); +} + +static void +on_system_rdp_server_binding (GrdDBusRemoteDesktopRdpServer *system_rdp_server, + int system_rdp_port, + GrdDaemonUser *daemon_user) +{ + GrdContext *context = grd_daemon_get_context (GRD_DAEMON (daemon_user)); + GrdDBusRemoteDesktopRdpServer *user_rdp_server = + grd_context_get_rdp_server_interface (context); + int user_rdp_port = + grd_dbus_remote_desktop_rdp_server_get_port (user_rdp_server); + + if (system_rdp_port != user_rdp_port) + return; + + g_debug ("[DaemonUser] Restarting RDP server due to port conflict with the " + "system daemon"); + + g_signal_connect (daemon_user, "rdp-server-stopped", + G_CALLBACK (on_rdp_server_stopped), NULL); + g_signal_connect (daemon_user, "rdp-server-started", + G_CALLBACK (on_rdp_server_started), NULL); + + grd_daemon_restart_rdp_server_with_delay (GRD_DAEMON (daemon_user)); +} + +static void +on_remote_desktop_rdp_server_proxy_acquired (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + GrdDaemonUser *daemon_user = user_data; + g_autoptr (GError) error = NULL; + + daemon_user->system_rdp_server = + grd_dbus_remote_desktop_rdp_server_proxy_new_for_bus_finish (result, &error); + if (!daemon_user->system_rdp_server) + { + g_warning ("[DaemonUser] Failed to create system RDP server proxy: " + "%s", error->message); + return; + } + + g_signal_connect (daemon_user->system_rdp_server, "binding", + G_CALLBACK (on_system_rdp_server_binding), + daemon_user); +} +#endif /* HAVE_RDP */ + +GrdDaemonUser * +grd_daemon_user_new (GrdRuntimeMode runtime_mode, + GError **error) +{ + GrdContext *context; + GrdDaemonUser *daemon_user; + const char *application_id = NULL; + + context = grd_context_new (runtime_mode, error); + if (!context) + return NULL; + + switch (runtime_mode) + { + case GRD_RUNTIME_MODE_SCREEN_SHARE: + application_id = GRD_DAEMON_USER_APPLICATION_ID; + break; + case GRD_RUNTIME_MODE_HEADLESS: + application_id = GRD_DAEMON_HEADLESS_APPLICATION_ID; + break; + case GRD_RUNTIME_MODE_SYSTEM: + case GRD_RUNTIME_MODE_HANDOVER: + g_assert_not_reached (); + } + + daemon_user = g_object_new (GRD_TYPE_DAEMON_USER, + "application-id", application_id, + "flags", G_APPLICATION_IS_SERVICE, + "context", context, + NULL); + + return daemon_user; +} + +static void +grd_daemon_user_init (GrdDaemonUser *daemon_user) +{ +} + +static void +grd_daemon_user_startup (GApplication *app) +{ + GrdDaemonUser *daemon_user = GRD_DAEMON_USER (app); +#ifdef HAVE_RDP + GCancellable *cancellable = + grd_daemon_get_cancellable (GRD_DAEMON (daemon_user)); +#endif /* HAVE_RDP */ + + grd_daemon_acquire_mutter_dbus_proxies (GRD_DAEMON (daemon_user)); + + g_signal_connect (daemon_user, "mutter-proxy-acquired", + G_CALLBACK (grd_daemon_maybe_enable_services), NULL); + +#ifdef HAVE_RDP + grd_dbus_remote_desktop_rdp_server_proxy_new_for_bus ( + G_BUS_TYPE_SYSTEM, + G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START, + REMOTE_DESKTOP_BUS_NAME, + GRD_RDP_SERVER_OBJECT_PATH, + cancellable, + on_remote_desktop_rdp_server_proxy_acquired, + daemon_user); +#endif /* HAVE_RDP */ + + G_APPLICATION_CLASS (grd_daemon_user_parent_class)->startup (app); +} + +static void +grd_daemon_user_shutdown (GApplication *app) +{ +#ifdef HAVE_RDP + GrdDaemonUser *daemon_user = GRD_DAEMON_USER (app); + + g_clear_object (&daemon_user->system_rdp_server); + if (daemon_user->prompt_cancellable) + { + g_cancellable_cancel (daemon_user->prompt_cancellable); + g_clear_object (&daemon_user->prompt_cancellable); + } + g_clear_object (&daemon_user->prompt); +#endif /* HAVE_RDP */ + + G_APPLICATION_CLASS (grd_daemon_user_parent_class)->shutdown (app); +} + +static gboolean +grd_daemon_user_is_ready (GrdDaemon *daemon) +{ + GrdContext *context = grd_daemon_get_context (daemon); + + if (!grd_context_get_mutter_remote_desktop_proxy (context) || + !grd_context_get_mutter_screen_cast_proxy (context)) + return FALSE; + + return TRUE; +} + +static void +grd_daemon_user_class_init (GrdDaemonUserClass *klass) +{ + GApplicationClass *g_application_class = G_APPLICATION_CLASS (klass); + GrdDaemonClass *daemon_class = GRD_DAEMON_CLASS (klass); + + g_application_class->startup = grd_daemon_user_startup; + g_application_class->shutdown = grd_daemon_user_shutdown; + + daemon_class->is_daemon_ready = grd_daemon_user_is_ready; +} diff --git a/grd-daemon-user.h b/grd-daemon-user.h new file mode 100644 index 0000000..8645414 --- /dev/null +++ b/grd-daemon-user.h @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2022 Red Hat Inc. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + * Written by: + * Jonas Ådahl + */ + +#pragma once + +#include "grd-daemon.h" + +#define GRD_TYPE_DAEMON_USER (grd_daemon_user_get_type ()) +G_DECLARE_FINAL_TYPE (GrdDaemonUser, + grd_daemon_user, + GRD, DAEMON_USER, + GrdDaemon) + +GrdDaemonUser *grd_daemon_user_new (GrdRuntimeMode runtime_mode, + GError **error); diff --git a/grd-daemon-utils.c b/grd-daemon-utils.c new file mode 100644 index 0000000..87c95cd --- /dev/null +++ b/grd-daemon-utils.c @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2023 SUSE Software Solutions Germany GmbH + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-daemon-utils.h" + +#include + +enum +{ + GSM_LOGOUT_MODE_NORMAL = 0, + GSM_LOGOUT_MODE_NO_CONFIRMATION, + GSM_LOGOUT_MODE_FORCE, +}; + +gboolean +grd_get_pid_of_sender_sync (GDBusConnection *connection, + const char *name, + pid_t *out_pid, + GCancellable *cancellable, + GError **error) +{ + g_autoptr (GVariant) result = NULL; + uint32_t pid; + + g_return_val_if_fail (G_IS_DBUS_CONNECTION (connection), FALSE); + g_return_val_if_fail (name != NULL, FALSE); + + g_assert (out_pid); + + result = g_dbus_connection_call_sync (connection, + "org.freedesktop.DBus", + "/org/freedesktop/DBus", + "org.freedesktop.DBus", + "GetConnectionUnixProcessID", + g_variant_new ("(s)", name), + G_VARIANT_TYPE ("(u)"), + G_DBUS_CALL_FLAGS_NONE, + -1, + cancellable, error); + if (!result) + return FALSE; + + g_variant_get (result, "(u)", &pid); + + *out_pid = (pid_t) pid; + + return TRUE; +} + +gboolean +grd_get_uid_of_sender_sync (GDBusConnection *connection, + const char *name, + uid_t *out_uid, + GCancellable *cancellable, + GError **error) +{ + g_autoptr (GVariant) result = NULL; + uint32_t uid; + + g_return_val_if_fail (G_IS_DBUS_CONNECTION (connection), FALSE); + g_return_val_if_fail (name != NULL, FALSE); + + g_assert (out_uid); + + result = g_dbus_connection_call_sync (connection, + "org.freedesktop.DBus", + "/org/freedesktop/DBus", + "org.freedesktop.DBus", + "GetConnectionUnixUser", + g_variant_new ("(s)", name), + G_VARIANT_TYPE ("(u)"), + G_DBUS_CALL_FLAGS_NONE, + -1, + cancellable, error); + if (!result) + return FALSE; + + g_variant_get (result, "(u)", &uid); + + *out_uid = (uid_t) uid; + + return TRUE; +} + +char * +grd_get_session_id_from_pid (pid_t pid) +{ + char *session_id = NULL; + int res; + + res = sd_pid_get_session (pid, &session_id); + if (res < 0 && res != -ENODATA) + { + g_warning ("Failed to retrieve session information for " + "pid %d: %s", (int) pid, strerror (-res)); + } + + return g_steal_pointer (&session_id); +} + +static gboolean +grd_sd_session_is_graphical (const char *session_id) +{ + const char * const graphical_session_types[] = { "wayland", "x11", NULL }; + int res; + g_autofree char *type = NULL; + + res = sd_session_get_type (session_id, &type); + if (res < 0) + return FALSE; + + return g_strv_contains (graphical_session_types, type); +} + +static gboolean +grd_sd_session_is_active (const char *session_id) +{ + const char * const active_states[] = { "active", "online", NULL }; + int res; + g_autofree char *state = NULL; + + res = sd_session_get_state (session_id, &state); + if (res < 0) + return FALSE; + + return g_strv_contains (active_states, state); +} + +char * +grd_get_session_id_from_uid (uid_t uid) +{ + g_auto (GStrv) sessions = NULL; + char *session_id = NULL; + int n_sessions; + int i; + + n_sessions = sd_uid_get_sessions (uid, 0, &sessions); + + for (i = n_sessions - 1; i >= 0; i--) + { + if (!grd_sd_session_is_graphical (sessions[i])) + continue; + + if (!grd_sd_session_is_active (sessions[i])) + continue; + + session_id = sessions[i]; + break; + } + + return g_strdup (session_id); +} + +gboolean +grd_is_remote_login (void) +{ + g_autofree char *session_id = NULL; + g_autofree char *class = NULL; + pid_t pid; + uid_t uid; + int ret; + + pid = getpid (); + session_id = grd_get_session_id_from_pid (pid); + if (!session_id) + { + uid = getuid (); + session_id = grd_get_session_id_from_uid (uid); + } + + if (!session_id) + { + g_warning ("Couldn't get session id"); + return FALSE; + } + + ret = sd_session_get_class (session_id, &class); + if (ret < 0) + { + g_warning ("Couldn't determine session class: %s", g_strerror (-ret)); + return FALSE; + } + + return g_strcmp0 (class, "greeter") == 0 && + sd_session_is_remote (session_id) == 1; +} + +void +grd_session_manager_call_logout_sync (void) +{ + g_autoptr (GDBusConnection) connection = NULL; + g_autoptr (GError) error = NULL; + + connection = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error); + if (!connection) + { + g_warning ("Couldn't get session bus to logout: %s", error->message); + return; + } + + g_dbus_connection_call_sync (connection, + "org.gnome.SessionManager", + "/org/gnome/SessionManager", + "org.gnome.SessionManager", + "Logout", + g_variant_new ("(u)", + GSM_LOGOUT_MODE_NO_CONFIRMATION), + NULL, + G_DBUS_CALL_FLAGS_NONE, + -1, + NULL, &error); + if (error) + g_warning ("Couldn't logout of session: %s", error->message); +} diff --git a/grd-daemon-utils.h b/grd-daemon-utils.h new file mode 100644 index 0000000..174ef30 --- /dev/null +++ b/grd-daemon-utils.h @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2023 SUSE Software Solutions Germany GmbH + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include + +gboolean grd_get_pid_of_sender_sync (GDBusConnection *connection, + const char *name, + pid_t *out_pid, + GCancellable *cancellable, + GError **error); + +gboolean grd_get_uid_of_sender_sync (GDBusConnection *connection, + const char *name, + uid_t *out_uid, + GCancellable *cancellable, + GError **error); + +char *grd_get_session_id_from_pid (pid_t pid); + +char *grd_get_session_id_from_uid (uid_t uid); + +gboolean grd_is_remote_login (void); + +void grd_session_manager_call_logout_sync (void); diff --git a/grd-daemon.c b/grd-daemon.c new file mode 100644 index 0000000..50bcd3d --- /dev/null +++ b/grd-daemon.c @@ -0,0 +1,1248 @@ +/* + * Copyright (C) 2015 Red Hat Inc. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + * Written by: + * Jonas Ådahl + */ + +#include "config.h" + +#include "grd-daemon.h" + +#include +#include +#include +#include +#include +#include + +#include "grd-context.h" +#include "grd-daemon-user.h" +#include "grd-dbus-mutter-remote-desktop.h" +#include "grd-dbus-remote-desktop.h" +#include "grd-private.h" +#include "grd-rdp-server.h" +#include "grd-settings-headless.h" +#include "grd-settings-system.h" +#include "grd-settings-user.h" +#include "grd-types.h" +#include "grd-vnc-server.h" + +#ifdef HAVE_LIBSYSTEMD +#include "grd-daemon-handover.h" +#include "grd-daemon-system.h" +#endif /* HAVE_LIBSYSTEMD */ + +#define RDP_SERVER_RESTART_DELAY_MS 3000 + +#define DEFAULT_MAX_PARALLEL_CONNECTIONS 10 + +enum +{ + PROP_0, + + PROP_CONTEXT, +}; + +enum +{ + MUTTER_PROXY_ACQUIRED, + RDP_SERVER_STARTED, + RDP_SERVER_STOPPED, + + N_SIGNALS, +}; + +static guint signals[N_SIGNALS]; + +typedef struct _GrdDaemonPrivate +{ + GSource *sigint_source; + GSource *sigterm_source; + + GCancellable *cancellable; + guint mutter_remote_desktop_watch_name_id; + guint mutter_screen_cast_watch_name_id; + + GrdContext *context; + + GDBusConnection *connection; + GrdDBusRemoteDesktopOrgGnomeRemoteDesktop *remote_desktop_interface; + +#ifdef HAVE_RDP + GrdRdpServer *rdp_server; + unsigned int restart_rdp_server_source_id; + GrdDBusRemoteDesktopRdpServer *other_rdp_server_iface; + unsigned int other_rdp_server_watch_name_id; +#endif +#ifdef HAVE_VNC + GrdVncServer *vnc_server; +#endif +} GrdDaemonPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (GrdDaemon, grd_daemon, G_TYPE_APPLICATION) + +#define QUOTE1(a) #a +#define QUOTE(a) QUOTE1(a) + +#ifdef HAVE_RDP +static void maybe_start_rdp_server (GrdDaemon *daemon); +#endif + +static const GDBusErrorEntry grd_dbus_error_entries[] = +{ + { GRD_DBUS_ERROR_NO_HANDOVER, "org.gnome.RemoteDesktop.Error.NoHandover" }, +}; + +GQuark +grd_dbus_error_quark (void) +{ + static gsize quark = 0; + + g_dbus_error_register_error_domain ("grd-dbus-error-quark", + &quark, + grd_dbus_error_entries, + G_N_ELEMENTS (grd_dbus_error_entries)); + return (GQuark) quark; +} + +GCancellable * +grd_daemon_get_cancellable (GrdDaemon *daemon) +{ + GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon); + + return priv->cancellable; +} + +GrdContext * +grd_daemon_get_context (GrdDaemon *daemon) +{ + GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon); + + return priv->context; +} + +#ifdef HAVE_RDP +GrdRdpServer * +grd_daemon_get_rdp_server (GrdDaemon *daemon) +{ + GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon); + + return priv->rdp_server; +} +#endif /* HAVE_RDP */ + +static void +export_remote_desktop_interface (GrdDaemon *daemon) +{ + GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon); + GrdRuntimeMode runtime_mode = grd_context_get_runtime_mode (priv->context); + + priv->remote_desktop_interface = + grd_dbus_remote_desktop_org_gnome_remote_desktop_skeleton_new (); + + switch (runtime_mode) + { + case GRD_RUNTIME_MODE_SCREEN_SHARE: + grd_dbus_remote_desktop_org_gnome_remote_desktop_set_runtime_mode ( + priv->remote_desktop_interface, "screen-share"); + break; + case GRD_RUNTIME_MODE_HEADLESS: + grd_dbus_remote_desktop_org_gnome_remote_desktop_set_runtime_mode ( + priv->remote_desktop_interface, "headless"); + break; + case GRD_RUNTIME_MODE_SYSTEM: + grd_dbus_remote_desktop_org_gnome_remote_desktop_set_runtime_mode ( + priv->remote_desktop_interface, "system"); + break; + case GRD_RUNTIME_MODE_HANDOVER: + grd_dbus_remote_desktop_org_gnome_remote_desktop_set_runtime_mode ( + priv->remote_desktop_interface, "handover"); + break; + } + + g_dbus_interface_skeleton_export ( + G_DBUS_INTERFACE_SKELETON (priv->remote_desktop_interface), + priv->connection, + REMOTE_DESKTOP_OBJECT_PATH, + NULL); +} + +static void +unexport_remote_desktop_interface (GrdDaemon *daemon) +{ + GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon); + + g_dbus_interface_skeleton_unexport ( + G_DBUS_INTERFACE_SKELETON (priv->remote_desktop_interface)); + + g_clear_object (&priv->remote_desktop_interface); +} + +#ifdef HAVE_RDP +static void +export_rdp_server_interface (GrdDaemon *daemon) +{ + GrdDBusRemoteDesktopRdpServer *rdp_server_interface; + GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon); + GrdSettings *settings = grd_context_get_settings (priv->context); + + rdp_server_interface = + grd_dbus_remote_desktop_rdp_server_skeleton_new (); + + grd_dbus_remote_desktop_rdp_server_set_enabled (rdp_server_interface, FALSE); + grd_dbus_remote_desktop_rdp_server_set_port (rdp_server_interface, -1); + g_object_bind_property (settings, "rdp-negotiate-port", + rdp_server_interface, "negotiate-port", + G_BINDING_SYNC_CREATE); + g_object_bind_property (settings, "rdp-server-cert-path", + rdp_server_interface, "tls-cert", + G_BINDING_SYNC_CREATE); + g_object_bind_property (settings, "rdp-server-fingerprint", + rdp_server_interface, "tls-fingerprint", + G_BINDING_SYNC_CREATE); + g_object_bind_property (settings, "rdp-server-key-path", + rdp_server_interface, "tls-key", + G_BINDING_SYNC_CREATE); + g_object_bind_property (settings, "rdp-auth-methods", + rdp_server_interface, "auth-methods", + G_BINDING_SYNC_CREATE); + g_object_bind_property (settings, "rdp-kerberos-keytab", + rdp_server_interface, "kerberos-keytab", + G_BINDING_SYNC_CREATE); + g_object_bind_property (settings, "rdp-view-only", + rdp_server_interface, "view-only", + G_BINDING_SYNC_CREATE); + + g_dbus_interface_skeleton_export ( + G_DBUS_INTERFACE_SKELETON (rdp_server_interface), + priv->connection, + GRD_RDP_SERVER_OBJECT_PATH, + NULL); + + grd_context_set_rdp_server_interface (priv->context, rdp_server_interface); +} + +static void +unexport_rdp_server_interface (GrdDaemon *daemon) +{ + GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon); + GrdDBusRemoteDesktopRdpServer *rdp_server_interface = + grd_context_get_rdp_server_interface (priv->context); + + g_dbus_interface_skeleton_unexport ( + G_DBUS_INTERFACE_SKELETON (rdp_server_interface)); + + grd_context_set_rdp_server_interface (priv->context, NULL); +} + +static void +start_rdp_server_when_ready (GrdDaemon *daemon, + gboolean should_start_when_ready) +{ + GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon); + GrdSettings *settings = grd_context_get_settings (priv->context); + + g_signal_handlers_disconnect_by_func (G_OBJECT (settings), + G_CALLBACK (maybe_start_rdp_server), + daemon); + + if (!should_start_when_ready) + return; + + g_signal_connect_object (G_OBJECT (settings), + "notify::rdp-server-cert", + G_CALLBACK (maybe_start_rdp_server), + daemon, G_CONNECT_SWAPPED); + g_signal_connect_object (G_OBJECT (settings), + "notify::rdp-server-key", + G_CALLBACK (maybe_start_rdp_server), + daemon, G_CONNECT_SWAPPED); + g_signal_connect_object (G_OBJECT (settings), + "notify::rdp-auth-methods", + G_CALLBACK (maybe_start_rdp_server), + daemon, G_CONNECT_SWAPPED); + g_signal_connect_object (G_OBJECT (settings), + "notify::rdp-kerberos-keytab", + G_CALLBACK (maybe_start_rdp_server), + daemon, G_CONNECT_SWAPPED); +} + +static void +stop_rdp_server (GrdDaemon *daemon) +{ + GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon); + + start_rdp_server_when_ready (daemon, FALSE); + + if (!priv->rdp_server) + return; + + g_signal_emit (daemon, signals[RDP_SERVER_STOPPED], 0); + grd_rdp_server_stop (priv->rdp_server); + g_clear_object (&priv->other_rdp_server_iface); + g_clear_handle_id (&priv->other_rdp_server_watch_name_id, g_bus_unwatch_name); + g_clear_object (&priv->rdp_server); + g_clear_handle_id (&priv->restart_rdp_server_source_id, g_source_remove); + g_message ("RDP server stopped"); +} + +static void +on_rdp_server_binding_failed (GrdRdpServer *rdp_server, + GrdDaemon *daemon) +{ + stop_rdp_server (daemon); +} + +static void +on_other_rdp_server_new_connection (GrdDBusRemoteDesktopRdpServer *interface, + GrdDaemon *daemon) +{ + GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon); + + g_assert (priv->rdp_server); + + grd_rdp_server_stop_sessions (priv->rdp_server); +} + +static void +on_remote_desktop_rdp_server_proxy_acquired (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + GrdDaemon *daemon = user_data; + GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon); + g_autoptr (GrdDBusRemoteDesktopRdpServer) proxy = NULL; + g_autoptr (GError) error = NULL; + + proxy = grd_dbus_remote_desktop_rdp_server_proxy_new_for_bus_finish (result, + &error); + if (!proxy) + { + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return; + + g_warning ("Failed to create remote desktop rdp server proxy: %s", + error->message); + return; + } + + priv->other_rdp_server_iface = g_steal_pointer (&proxy); + g_signal_connect (priv->other_rdp_server_iface, "new-connection", + G_CALLBACK (on_other_rdp_server_new_connection), daemon); +} + +static void +on_remote_desktop_rdp_server_name_appeared (GDBusConnection *connection, + const char *name, + const char *name_owner, + gpointer user_data) +{ + GrdDaemon *daemon = user_data; + GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon); + + grd_dbus_remote_desktop_rdp_server_proxy_new ( + connection, + G_DBUS_PROXY_FLAGS_NONE, + name, + GRD_RDP_SERVER_OBJECT_PATH, + priv->cancellable, + on_remote_desktop_rdp_server_proxy_acquired, + daemon); +} + +static void +on_remote_desktop_rdp_server_name_vanished (GDBusConnection *connection, + const char *name, + gpointer user_data) +{ + GrdDaemon *daemon = user_data; + GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon); + + g_clear_object (&priv->other_rdp_server_iface); +} + +static void +connect_to_other_rdp_server (GrdDaemon *daemon) +{ + GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon); + GrdRuntimeMode runtime_mode = grd_context_get_runtime_mode (priv->context); + const char *bus_name; + + if (priv->other_rdp_server_watch_name_id != 0) + return; + + if (runtime_mode == GRD_RUNTIME_MODE_HEADLESS) + bus_name = GRD_DAEMON_HANDOVER_APPLICATION_ID; + else if (runtime_mode == GRD_RUNTIME_MODE_HANDOVER) + bus_name = GRD_DAEMON_HEADLESS_APPLICATION_ID; + else + g_assert_not_reached (); + + priv->other_rdp_server_watch_name_id = + g_bus_watch_name (G_BUS_TYPE_SESSION, + bus_name, + G_BUS_NAME_WATCHER_FLAGS_NONE, + on_remote_desktop_rdp_server_name_appeared, + on_remote_desktop_rdp_server_name_vanished, + daemon, NULL); +} + +static void +start_rdp_server (GrdDaemon *daemon) +{ + GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon); + GrdRuntimeMode runtime_mode = grd_context_get_runtime_mode (priv->context); + g_autoptr (GError) error = NULL; + + g_assert (!priv->rdp_server); + + priv->rdp_server = grd_rdp_server_new (priv->context); + g_signal_connect (priv->rdp_server, "binding-failed", + G_CALLBACK (on_rdp_server_binding_failed), daemon); + if (!grd_rdp_server_start (priv->rdp_server, &error)) + { + g_warning ("Failed to start RDP server: %s\n", error->message); + stop_rdp_server (daemon); + } + else + { + g_signal_emit (daemon, signals[RDP_SERVER_STARTED], 0); + g_message ("RDP server started"); + + if (runtime_mode == GRD_RUNTIME_MODE_HANDOVER || + runtime_mode == GRD_RUNTIME_MODE_HEADLESS) + connect_to_other_rdp_server (daemon); + } +} + +static void +maybe_start_rdp_server (GrdDaemon *daemon) +{ + GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon); + GrdSettings *settings = grd_context_get_settings (priv->context); + g_autoptr (GError) error = NULL; + GrdRdpAuthMethods auth_methods = 0; + g_autofree char *kerberos_keytab = NULL; + g_autofree char *certificate = NULL; + g_autofree char *key = NULL; + gboolean rdp_enabled = FALSE; + + if (priv->rdp_server) + return; + + if (!GRD_DAEMON_GET_CLASS (daemon)->is_daemon_ready (daemon)) + { + g_debug ("Daemon not ready, not starting RDP server"); + return; + } + + g_object_get (G_OBJECT (settings), + "rdp-enabled", &rdp_enabled, + "rdp-auth-methods", &auth_methods, + "rdp-kerberos-keytab", &kerberos_keytab, + "rdp-server-cert", &certificate, + "rdp-server-key", &key, + NULL); + + if (!rdp_enabled) + { + g_debug ("RDP not enabled, not starting RDP server"); + return; + } + + if (!auth_methods) + { + g_warning ("No RDP auth methods configured, not enabling server."); + return; + } + + if (auth_methods & GRD_RDP_AUTH_METHOD_KERBEROS && + !kerberos_keytab) + { + g_debug ("Kerberos keytab not set, not starting RDP server"); + return; + } + + if ((certificate && key) || + grd_context_get_runtime_mode (priv->context) == GRD_RUNTIME_MODE_HANDOVER) + { + start_rdp_server (daemon); + } + else + { + g_message ("RDP TLS certificate and key not yet configured properly"); + start_rdp_server_when_ready (daemon, TRUE); + } +} + +static gboolean +restart_rdp_server (gpointer user_data) +{ + GrdDaemon *daemon = user_data; + GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon); + + priv->restart_rdp_server_source_id = 0; + + maybe_start_rdp_server (daemon); + + return G_SOURCE_REMOVE; +} + +void +grd_daemon_restart_rdp_server_with_delay (GrdDaemon *daemon) +{ + GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon); + + if (priv->restart_rdp_server_source_id) + return; + + stop_rdp_server (daemon); + + priv->restart_rdp_server_source_id = + g_timeout_add (RDP_SERVER_RESTART_DELAY_MS, restart_rdp_server, daemon); +} +#endif /* HAVE_RDP */ + +#ifdef HAVE_VNC +static gboolean +set_string_from_auth_method_enum (GBinding *binding, + const GValue *source_value, + GValue *target_value, + gpointer user_data) +{ + GrdVncAuthMethod src_auth_method = g_value_get_enum (source_value); + + switch (src_auth_method) + { + case GRD_VNC_AUTH_METHOD_PROMPT: + g_value_set_string (target_value, "prompt"); + break; + case GRD_VNC_AUTH_METHOD_PASSWORD: + g_value_set_string (target_value, "password"); + break; + } + + return TRUE; +} + +static void +export_vnc_server_interface (GrdDaemon *daemon) +{ + GrdDBusRemoteDesktopVncServer *vnc_server_interface; + GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon); + GrdSettings *settings = grd_context_get_settings (priv->context); + + vnc_server_interface = + grd_dbus_remote_desktop_vnc_server_skeleton_new (); + + grd_dbus_remote_desktop_vnc_server_set_enabled (vnc_server_interface, FALSE); + grd_dbus_remote_desktop_vnc_server_set_port (vnc_server_interface, -1); + g_object_bind_property (settings, "vnc-negotiate-port", + vnc_server_interface, "negotiate-port", + G_BINDING_SYNC_CREATE); + g_object_bind_property (settings, "vnc-view-only", + vnc_server_interface, "view-only", + G_BINDING_SYNC_CREATE); + g_object_bind_property_full (settings, "vnc-auth-method", + vnc_server_interface, "auth-method", + G_BINDING_SYNC_CREATE, + set_string_from_auth_method_enum, + NULL, NULL, NULL); + + g_dbus_interface_skeleton_export ( + G_DBUS_INTERFACE_SKELETON (vnc_server_interface), + priv->connection, + GRD_VNC_SERVER_OBJECT_PATH, + NULL); + + grd_context_set_vnc_server_interface (priv->context, vnc_server_interface); +} + +static void +unexport_vnc_server_interface (GrdDaemon *daemon) +{ + GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon); + GrdDBusRemoteDesktopVncServer *vnc_server_interface = + grd_context_get_vnc_server_interface (priv->context); + + g_dbus_interface_skeleton_unexport ( + G_DBUS_INTERFACE_SKELETON (vnc_server_interface)); + + grd_context_set_vnc_server_interface (priv->context, NULL); +} + +static void +stop_vnc_server (GrdDaemon *daemon) +{ + GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon); + + if (!priv->vnc_server) + return; + + grd_vnc_server_stop (priv->vnc_server); + g_clear_object (&priv->vnc_server); + g_message ("VNC server stopped"); +} + +static void +start_vnc_server (GrdDaemon *daemon) +{ + GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon); + g_autoptr (GError) error = NULL; + + if (priv->vnc_server) + return; + + priv->vnc_server = grd_vnc_server_new (priv->context); + if (!grd_vnc_server_start (priv->vnc_server, &error)) + { + g_warning ("Failed to initialize VNC server: %s\n", error->message); + stop_vnc_server (daemon); + } + else + { + g_message ("VNC server started"); + } +} +#endif /* HAVE_VNC */ + +static void +export_services_status (GrdDaemon *daemon) +{ + export_remote_desktop_interface (daemon); + +#ifdef HAVE_RDP + export_rdp_server_interface (daemon); +#endif +#ifdef HAVE_VNC + if (GRD_IS_DAEMON_USER (daemon)) + export_vnc_server_interface (daemon); +#endif +} + +static void +unexport_services_status (GrdDaemon *daemon) +{ + unexport_remote_desktop_interface (daemon); + +#ifdef HAVE_RDP + unexport_rdp_server_interface (daemon); +#endif +#ifdef HAVE_VNC + if (GRD_IS_DAEMON_USER (daemon)) + unexport_vnc_server_interface (daemon); +#endif +} + +void +grd_daemon_maybe_enable_services (GrdDaemon *daemon) +{ + GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon); + GrdSettings *settings = grd_context_get_settings (priv->context); + gboolean rdp_enabled; + gboolean vnc_enabled; + + if (!GRD_DAEMON_GET_CLASS (daemon)->is_daemon_ready (daemon)) + return; + + grd_context_notify_daemon_ready (priv->context); + + g_object_get (G_OBJECT (settings), + "rdp-enabled", &rdp_enabled, + "vnc-enabled", &vnc_enabled, + NULL); + +#ifdef HAVE_RDP + maybe_start_rdp_server (daemon); +#endif + +#ifdef HAVE_VNC + if (vnc_enabled) + start_vnc_server (daemon); +#endif +} + +void +grd_daemon_disable_services (GrdDaemon *daemon) +{ +#ifdef HAVE_RDP + stop_rdp_server (daemon); +#endif +#ifdef HAVE_VNC + stop_vnc_server (daemon); +#endif +} + +static void +on_mutter_remote_desktop_proxy_acquired (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + GrdDaemon *daemon = user_data; + GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon); + GrdDBusMutterRemoteDesktop *proxy; + g_autoptr (GError) error = NULL; + + proxy = grd_dbus_mutter_remote_desktop_proxy_new_finish (result, &error); + if (!proxy) + { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("Failed to create remote desktop proxy: %s", error->message); + return; + } + + grd_context_set_mutter_remote_desktop_proxy (priv->context, proxy); + + g_signal_emit (daemon, signals[MUTTER_PROXY_ACQUIRED], 0); +} + +static void +on_mutter_screen_cast_proxy_acquired (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + GrdDaemon *daemon = user_data; + GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon); + GrdDBusMutterScreenCast *proxy; + g_autoptr (GError) error = NULL; + + proxy = grd_dbus_mutter_screen_cast_proxy_new_finish (result, &error); + if (!proxy) + { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("Failed to create screen cast proxy: %s", error->message); + return; + } + + grd_context_set_mutter_screen_cast_proxy (priv->context, proxy); + + g_signal_emit (daemon, signals[MUTTER_PROXY_ACQUIRED], 0); +} + +static void +on_mutter_remote_desktop_name_appeared (GDBusConnection *connection, + const char *name, + const char *name_owner, + gpointer user_data) +{ + GrdDaemon *daemon = user_data; + GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon); + + grd_dbus_mutter_remote_desktop_proxy_new (connection, + G_DBUS_PROXY_FLAGS_NONE, + MUTTER_REMOTE_DESKTOP_BUS_NAME, + MUTTER_REMOTE_DESKTOP_OBJECT_PATH, + priv->cancellable, + on_mutter_remote_desktop_proxy_acquired, + daemon); +} + +static void +on_mutter_remote_desktop_name_vanished (GDBusConnection *connection, + const char *name, + gpointer user_data) +{ + GrdDaemon *daemon = user_data; + GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon); + + grd_daemon_disable_services (daemon); + + grd_context_set_mutter_remote_desktop_proxy (priv->context, NULL); +} + +static void +on_mutter_screen_cast_name_appeared (GDBusConnection *connection, + const char *name, + const char *name_owner, + gpointer user_data) +{ + GrdDaemon *daemon = user_data; + GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon); + + grd_dbus_mutter_screen_cast_proxy_new (connection, + G_DBUS_PROXY_FLAGS_NONE, + MUTTER_SCREEN_CAST_BUS_NAME, + MUTTER_SCREEN_CAST_OBJECT_PATH, + priv->cancellable, + on_mutter_screen_cast_proxy_acquired, + daemon); +} + +static void +on_mutter_screen_cast_name_vanished (GDBusConnection *connection, + const char *name, + gpointer user_data) +{ + GrdDaemon *daemon = user_data; + GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon); + + grd_daemon_disable_services (daemon); + + grd_context_set_mutter_screen_cast_proxy (priv->context, NULL); +} + +void +grd_daemon_acquire_mutter_dbus_proxies (GrdDaemon *daemon) +{ + GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon); + + g_clear_handle_id (&priv->mutter_remote_desktop_watch_name_id, g_bus_unwatch_name); + priv->mutter_remote_desktop_watch_name_id = + g_bus_watch_name (G_BUS_TYPE_SESSION, + MUTTER_REMOTE_DESKTOP_BUS_NAME, + G_BUS_NAME_WATCHER_FLAGS_NONE, + on_mutter_remote_desktop_name_appeared, + on_mutter_remote_desktop_name_vanished, + daemon, NULL); + + g_clear_handle_id (&priv->mutter_screen_cast_watch_name_id, g_bus_unwatch_name); + priv->mutter_screen_cast_watch_name_id = + g_bus_watch_name (G_BUS_TYPE_SESSION, + MUTTER_SCREEN_CAST_BUS_NAME, + G_BUS_NAME_WATCHER_FLAGS_NONE, + on_mutter_screen_cast_name_appeared, + on_mutter_screen_cast_name_vanished, + daemon, NULL); +} + +#ifdef HAVE_RDP +static void +on_rdp_enabled_changed (GrdSettings *settings, + GParamSpec *pspec, + GrdDaemon *daemon) +{ + gboolean rdp_enabled; + + if (!GRD_DAEMON_GET_CLASS (daemon)->is_daemon_ready (daemon)) + return; + + g_object_get (G_OBJECT (settings), "rdp-enabled", &rdp_enabled, NULL); + if (rdp_enabled) + maybe_start_rdp_server (daemon); + else + stop_rdp_server (daemon); +} +#endif /* HAVE_RDP */ + +#ifdef HAVE_VNC +static void +on_vnc_enabled_changed (GrdSettings *settings, + GParamSpec *pspec, + GrdDaemon *daemon) +{ + GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon); + gboolean vnc_enabled; + + if (!GRD_DAEMON_GET_CLASS (daemon)->is_daemon_ready (daemon)) + return; + + g_object_get (G_OBJECT (settings), "vnc-enabled", &vnc_enabled, NULL); + if (vnc_enabled) + { + g_return_if_fail (!priv->vnc_server); + start_vnc_server (daemon); + } + else + { + stop_vnc_server (daemon); + } +} +#endif /* HAVE_VNC */ + +static void +grd_daemon_init (GrdDaemon *daemon) +{ + GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon); + + priv->cancellable = g_cancellable_new (); +} + +static GDBusConnection * +get_daemon_dbus_connection (GrdDaemon *daemon) +{ + g_autoptr (GDBusConnection) connection = NULL; + g_autoptr (GError) error = NULL; + +#if defined(HAVE_RDP) && defined(HAVE_LIBSYSTEMD) + if (GRD_IS_DAEMON_SYSTEM (daemon)) + connection = g_bus_get_sync (G_BUS_TYPE_SYSTEM, NULL, &error); + else +#endif /* HAVE_RDP && HAVE_LIBSYSTEMD */ + connection = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error); + + if (!connection) + { + g_warning ("Failing acquiring dbus connection: %s", error->message); + return NULL; + } + + return g_steal_pointer (&connection); +} + +static void +grd_daemon_startup (GApplication *app) +{ + GrdDaemon *daemon = GRD_DAEMON (app); + GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon); + GrdSettings *settings = grd_context_get_settings (priv->context); + + priv->connection = get_daemon_dbus_connection (daemon); + export_services_status (daemon); + +#ifdef HAVE_RDP + if (GRD_IS_SETTINGS_USER (settings) || + GRD_IS_SETTINGS_HEADLESS (settings) || + GRD_IS_SETTINGS_SYSTEM (settings)) + { + g_signal_connect (settings, "notify::rdp-enabled", + G_CALLBACK (on_rdp_enabled_changed), + daemon); + } +#endif +#ifdef HAVE_VNC + if (GRD_IS_SETTINGS_USER (settings) || + GRD_IS_SETTINGS_HEADLESS (settings)) + { + g_signal_connect (settings, "notify::vnc-enabled", + G_CALLBACK (on_vnc_enabled_changed), + daemon); + } +#endif + + /* Run indefinitely, until told to exit. */ + g_application_hold (app); + + G_APPLICATION_CLASS (grd_daemon_parent_class)->startup (app); +} + +static void +grd_daemon_shutdown (GApplication *app) +{ + GrdDaemon *daemon = GRD_DAEMON (app); + GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon); + + g_cancellable_cancel (priv->cancellable); + g_clear_object (&priv->cancellable); + + grd_daemon_disable_services (daemon); + + unexport_services_status (daemon); + g_clear_object (&priv->connection); + + grd_context_set_mutter_remote_desktop_proxy (priv->context, NULL); + g_clear_handle_id (&priv->mutter_remote_desktop_watch_name_id, g_bus_unwatch_name); + + grd_context_set_mutter_screen_cast_proxy (priv->context, NULL); + g_clear_handle_id (&priv->mutter_screen_cast_watch_name_id, g_bus_unwatch_name); + + g_clear_object (&priv->context); + + if (priv->sigterm_source) + { + g_source_destroy (priv->sigterm_source); + g_clear_pointer (&priv->sigterm_source, g_source_unref); + } + if (priv->sigint_source) + { + g_source_destroy (priv->sigint_source); + g_clear_pointer (&priv->sigint_source, g_source_unref); + } + + G_APPLICATION_CLASS (grd_daemon_parent_class)->shutdown (app); +} + +static void +grd_daemon_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GrdDaemon *daemon = GRD_DAEMON (object); + GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon); + + switch (prop_id) + { + case PROP_CONTEXT: + g_value_set_object (value, priv->context); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +grd_daemon_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GrdDaemon *daemon = GRD_DAEMON (object); + GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon); + + switch (prop_id) + { + case PROP_CONTEXT: + priv->context = g_value_get_object (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +grd_daemon_class_init (GrdDaemonClass *klass) +{ + GApplicationClass *g_application_class = G_APPLICATION_CLASS (klass); + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + g_application_class->startup = grd_daemon_startup; + g_application_class->shutdown = grd_daemon_shutdown; + + object_class->get_property = grd_daemon_get_property; + object_class->set_property = grd_daemon_set_property; + + g_object_class_install_property (object_class, + PROP_CONTEXT, + g_param_spec_object ("context", + "GrdContext", + "The GrdContext instance", + GRD_TYPE_CONTEXT, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + + signals[MUTTER_PROXY_ACQUIRED] = g_signal_new ("mutter-proxy-acquired", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, 0); + signals[RDP_SERVER_STARTED] = g_signal_new ("rdp-server-started", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, 0); + signals[RDP_SERVER_STOPPED] = g_signal_new ("rdp-server-stopped", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, 0); + + grd_dbus_error_quark (); +} + +static void +activate_terminate (GAction *action, + GVariant *parameter, + GrdDaemon *daemon) +{ + g_application_release (G_APPLICATION (daemon)); +} + +static void +add_actions (GApplication *app) +{ + g_autoptr(GSimpleAction) action = NULL; + + action = g_simple_action_new ("terminate", NULL); + g_signal_connect (action, "activate", G_CALLBACK (activate_terminate), app); + g_action_map_add_action (G_ACTION_MAP (app), G_ACTION (action)); +} + +static gboolean +sigint_terminate_daemon (gpointer user_data) +{ + GrdDaemon *daemon = user_data; + GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon); + + g_debug ("Received SIGINT signal. Exiting..."); + g_clear_pointer (&priv->sigint_source, g_source_unref); + g_application_release (G_APPLICATION (daemon)); + + return G_SOURCE_REMOVE; +} + +static gboolean +sigterm_terminate_daemon (gpointer user_data) +{ + GrdDaemon *daemon = user_data; + GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon); + + g_debug ("Received SIGTERM signal. Exiting..."); + g_clear_pointer (&priv->sigterm_source, g_source_unref); + g_application_release (G_APPLICATION (daemon)); + + return G_SOURCE_REMOVE; +} + +static void +register_signals (GrdDaemon *daemon) +{ + GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon); + + priv->sigint_source = g_unix_signal_source_new (SIGINT); + g_source_set_callback (priv->sigint_source, sigint_terminate_daemon, + daemon, NULL); + g_source_attach (priv->sigint_source, NULL); + + priv->sigterm_source = g_unix_signal_source_new (SIGTERM); + g_source_set_callback (priv->sigterm_source, sigterm_terminate_daemon, + daemon, NULL); + g_source_attach (priv->sigterm_source, NULL); +} + +static int +count_trues (int n_args, ...) +{ + va_list booleans; + int booleans_count = 0; + + va_start (booleans, n_args); + + for (int i = 0; i < n_args; ++i) + booleans_count += va_arg (booleans, gboolean) ? 1 : 0; + + return booleans_count; +} + +int +main (int argc, char **argv) +{ + GrdContext *context; + GrdSettings *settings; + gboolean print_version = FALSE; + gboolean headless = FALSE; + gboolean system = FALSE; + gboolean handover = FALSE; + int rdp_port = -1; + int vnc_port = -1; + int max_parallel_connections = DEFAULT_MAX_PARALLEL_CONNECTIONS; + + GOptionEntry entries[] = { + { "version", 0, 0, G_OPTION_ARG_NONE, &print_version, + "Print version", NULL }, + { "headless", 0, 0, G_OPTION_ARG_NONE, &headless, + "Run in headless mode", NULL }, +#if defined(HAVE_RDP) && defined(HAVE_LIBSYSTEMD) + { "system", 0, 0, G_OPTION_ARG_NONE, &system, + "Run in headless mode as a system g-r-d service", NULL }, + { "handover", 0, 0, G_OPTION_ARG_NONE, &handover, + "Run in headless mode taking a connection from system g-r-d service", NULL }, +#endif /* HAVE_RDP && HAVE_LIBSYSTEMD */ + { "rdp-port", 0, 0, G_OPTION_ARG_INT, &rdp_port, + "RDP port", NULL }, + { "vnc-port", 0, 0, G_OPTION_ARG_INT, &vnc_port, + "VNC port", NULL }, + { "max-parallel-connections", 0, 0, + G_OPTION_ARG_INT, &max_parallel_connections, + "Max number of parallel connections (0 for unlimited, " + "default: " QUOTE(DEFAULT_MAX_PARALLEL_CONNECTIONS) ")", NULL }, + { NULL } + }; + g_autoptr (GOptionContext) option_context = NULL; + g_autoptr (GrdDaemon) daemon = NULL; + g_autoptr (GError) error = NULL; + GrdRuntimeMode runtime_mode; + + g_set_application_name (_("GNOME Remote Desktop")); + + option_context = g_option_context_new (NULL); + g_option_context_add_main_entries (option_context, entries, GETTEXT_PACKAGE); + if (!g_option_context_parse (option_context, &argc, &argv, &error)) + { + g_printerr ("Invalid option: %s\n", error->message); + return EXIT_FAILURE; + } + + if (print_version) + { + g_print ("GNOME Remote Desktop %s\n", VERSION); + return EXIT_SUCCESS; + } + + if (count_trues (3, headless, system, handover) > 1) + { + g_printerr ("Invalid option: More than one runtime mode specified\n"); + return EXIT_FAILURE; + } + + if (max_parallel_connections == 0) + { + max_parallel_connections = INT_MAX; + } + else if (max_parallel_connections < 0) + { + g_printerr ("Invalid number of max parallel connections: %d\n", + max_parallel_connections); + return EXIT_FAILURE; + } + + if (headless) + runtime_mode = GRD_RUNTIME_MODE_HEADLESS; + else if (system) + runtime_mode = GRD_RUNTIME_MODE_SYSTEM; + else if (handover) + runtime_mode = GRD_RUNTIME_MODE_HANDOVER; + else + runtime_mode = GRD_RUNTIME_MODE_SCREEN_SHARE; + + switch (runtime_mode) + { + case GRD_RUNTIME_MODE_SCREEN_SHARE: + case GRD_RUNTIME_MODE_HEADLESS: + daemon = GRD_DAEMON (grd_daemon_user_new (runtime_mode, &error)); + break; +#ifdef HAVE_LIBSYSTEMD + case GRD_RUNTIME_MODE_SYSTEM: + daemon = GRD_DAEMON (grd_daemon_system_new (&error)); + break; + case GRD_RUNTIME_MODE_HANDOVER: + daemon = GRD_DAEMON (grd_daemon_handover_new (&error)); + break; +#else + case GRD_RUNTIME_MODE_SYSTEM: + case GRD_RUNTIME_MODE_HANDOVER: + g_assert_not_reached (); + break; +#endif /* HAVE_LIBSYSTEMD */ + } + + if (!daemon) + { + g_printerr ("Failed to initialize: %s\n", error->message); + return EXIT_FAILURE; + } + + add_actions (G_APPLICATION (daemon)); + register_signals (daemon); + + context = grd_daemon_get_context (daemon); + settings = grd_context_get_settings (context); + if (rdp_port != -1) + grd_settings_override_rdp_port (settings, rdp_port); + if (vnc_port != -1) + grd_settings_override_vnc_port (settings, vnc_port); + + grd_settings_override_max_parallel_connections (settings, + max_parallel_connections); + + return g_application_run (G_APPLICATION (daemon), argc, argv); +} diff --git a/grd-daemon.h b/grd-daemon.h new file mode 100644 index 0000000..0ff5239 --- /dev/null +++ b/grd-daemon.h @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2015 Red Hat Inc. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + * Written by: + * Jonas Ådahl + */ + +#pragma once + +#include + +#include "grd-context.h" + +typedef struct _GrdDaemon GrdDaemon; + +#define GRD_TYPE_DAEMON (grd_daemon_get_type ()) +G_DECLARE_DERIVABLE_TYPE (GrdDaemon, grd_daemon, GRD, DAEMON, GApplication) + +struct _GrdDaemonClass +{ + GApplicationClass parent_class; + + gboolean (*is_daemon_ready) (GrdDaemon *daemon); +}; + +GCancellable *grd_daemon_get_cancellable (GrdDaemon *daemon); + +GrdContext *grd_daemon_get_context (GrdDaemon *daemon); + +GrdRdpServer *grd_daemon_get_rdp_server (GrdDaemon *daemon); + +void grd_daemon_restart_rdp_server_with_delay (GrdDaemon *daemon); + +void grd_daemon_maybe_enable_services (GrdDaemon *daemon); + +void grd_daemon_disable_services (GrdDaemon *daemon); + +void grd_daemon_acquire_mutter_dbus_proxies (GrdDaemon *daemon); diff --git a/grd-damage-detector-sw.c b/grd-damage-detector-sw.c new file mode 100644 index 0000000..044276a --- /dev/null +++ b/grd-damage-detector-sw.c @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2025 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-damage-detector-sw.h" + +#include "grd-damage-utils.h" +#include "grd-local-buffer.h" +#include "grd-utils.h" + +#define TILE_WIDTH 64 +#define TILE_HEIGHT 64 + +struct _GrdDamageDetectorSw +{ + GObject parent; + + uint32_t surface_width; + uint32_t surface_height; + + uint32_t damage_buffer_width; + uint32_t damage_buffer_height; + + uint32_t *damage_buffer; + uint32_t damage_buffer_length; +}; + +G_DEFINE_TYPE (GrdDamageDetectorSw, grd_damage_detector_sw, + G_TYPE_OBJECT) + +uint32_t * +grd_damage_detector_sw_get_damage_buffer (GrdDamageDetectorSw *damage_detector) +{ + return damage_detector->damage_buffer; +} + +uint32_t +grd_damage_detector_sw_get_damage_buffer_length (GrdDamageDetectorSw *damage_detector) +{ + return damage_detector->damage_buffer_length; +} + +static void +invalidate_surface (GrdDamageDetectorSw *damage_detector) +{ + uint32_t i; + + for (i = 0; i < damage_detector->damage_buffer_length; ++i) + damage_detector->damage_buffer[i] = 1; +} + +static void +compute_frame_damage (GrdDamageDetectorSw *damage_detector, + GrdLocalBuffer *local_buffer_new, + GrdLocalBuffer *local_buffer_old) +{ + uint32_t surface_width = damage_detector->surface_width; + uint32_t surface_height = damage_detector->surface_height; + uint32_t local_buffer_stride; + uint32_t damage_buffer_stride; + uint32_t x, y; + + g_assert (local_buffer_new); + g_assert (local_buffer_old); + + g_assert (grd_local_buffer_get_buffer_stride (local_buffer_new) == + grd_local_buffer_get_buffer_stride (local_buffer_old)); + local_buffer_stride = grd_local_buffer_get_buffer_stride (local_buffer_new); + + damage_buffer_stride = damage_detector->damage_buffer_width; + + for (y = 0; y < damage_detector->damage_buffer_height; ++y) + { + for (x = 0; x < damage_detector->damage_buffer_width; ++x) + { + cairo_rectangle_int_t tile; + uint32_t tile_damaged = 0; + uint32_t buffer_pos; + + tile.x = x * TILE_WIDTH; + tile.y = y * TILE_HEIGHT; + tile.width = surface_width - tile.x < TILE_WIDTH ? + surface_width - tile.x : TILE_WIDTH; + tile.height = surface_height - tile.y < TILE_HEIGHT ? + surface_height - tile.y : TILE_HEIGHT; + + if (grd_is_tile_dirty (&tile, + grd_local_buffer_get_buffer (local_buffer_new), + grd_local_buffer_get_buffer (local_buffer_old), + local_buffer_stride, 4)) + tile_damaged = 1; + + buffer_pos = y * damage_buffer_stride + x; + damage_detector->damage_buffer[buffer_pos] = tile_damaged; + } + } +} + +void +grd_damage_detector_sw_compute_damage (GrdDamageDetectorSw *damage_detector, + GrdLocalBuffer *local_buffer_new, + GrdLocalBuffer *local_buffer_old) +{ + if (!local_buffer_old) + { + invalidate_surface (damage_detector); + return; + } + + compute_frame_damage (damage_detector, local_buffer_new, local_buffer_old); +} + +static void +create_damage_buffer (GrdDamageDetectorSw *damage_detector) +{ + uint32_t surface_width = damage_detector->surface_width; + uint32_t surface_height = damage_detector->surface_height; + uint32_t damage_buffer_width; + uint32_t damage_buffer_height; + uint32_t damage_buffer_length; + + damage_buffer_width = grd_get_aligned_size (surface_width, TILE_WIDTH) / + TILE_WIDTH; + damage_buffer_height = grd_get_aligned_size (surface_height, TILE_HEIGHT) / + TILE_HEIGHT; + damage_buffer_length = damage_buffer_width * damage_buffer_height; + + damage_detector->damage_buffer_width = damage_buffer_width; + damage_detector->damage_buffer_height = damage_buffer_height; + + damage_detector->damage_buffer = g_new0 (uint32_t, damage_buffer_length); + damage_detector->damage_buffer_length = damage_buffer_length; +} + +GrdDamageDetectorSw * +grd_damage_detector_sw_new (uint32_t surface_width, + uint32_t surface_height) +{ + GrdDamageDetectorSw *damage_detector; + + damage_detector = g_object_new (GRD_TYPE_DAMAGE_DETECTOR_SW, NULL); + damage_detector->surface_width = surface_width; + damage_detector->surface_height = surface_height; + + create_damage_buffer (damage_detector); + + return damage_detector; +} + +static void +grd_damage_detector_sw_dispose (GObject *object) +{ + GrdDamageDetectorSw *damage_detector = GRD_DAMAGE_DETECTOR_SW (object); + + g_clear_pointer (&damage_detector->damage_buffer, g_free); + + G_OBJECT_CLASS (grd_damage_detector_sw_parent_class)->dispose (object); +} + +static void +grd_damage_detector_sw_init (GrdDamageDetectorSw *damage_detector) +{ +} + +static void +grd_damage_detector_sw_class_init (GrdDamageDetectorSwClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = grd_damage_detector_sw_dispose; +} diff --git a/grd-damage-detector-sw.h b/grd-damage-detector-sw.h new file mode 100644 index 0000000..74ed557 --- /dev/null +++ b/grd-damage-detector-sw.h @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2025 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include +#include + +#include "grd-types.h" + +#define GRD_TYPE_DAMAGE_DETECTOR_SW (grd_damage_detector_sw_get_type ()) +G_DECLARE_FINAL_TYPE (GrdDamageDetectorSw, grd_damage_detector_sw, + GRD, DAMAGE_DETECTOR_SW, GObject) + +GrdDamageDetectorSw *grd_damage_detector_sw_new (uint32_t surface_width, + uint32_t surface_height); + +uint32_t *grd_damage_detector_sw_get_damage_buffer (GrdDamageDetectorSw *damage_detector); + +uint32_t grd_damage_detector_sw_get_damage_buffer_length (GrdDamageDetectorSw *damage_detector); + +void grd_damage_detector_sw_compute_damage (GrdDamageDetectorSw *damage_detector, + GrdLocalBuffer *local_buffer_new, + GrdLocalBuffer *local_buffer_old); diff --git a/grd-damage-utils.c b/grd-damage-utils.c new file mode 100644 index 0000000..6753ec4 --- /dev/null +++ b/grd-damage-utils.c @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2020 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-damage-utils.h" + +#include + +bool +grd_is_tile_dirty (cairo_rectangle_int_t *tile, + uint8_t *current_data, + uint8_t *prev_data, + uint32_t stride, + uint32_t bytes_per_pixel) +{ + uint32_t y; + + for (y = tile->y; y < tile->y + tile->height; ++y) + { + if (memcmp (prev_data + y * stride + tile->x * bytes_per_pixel, + current_data + y * stride + tile->x * bytes_per_pixel, + tile->width * bytes_per_pixel)) + return true; + } + + return false; +} + +cairo_region_t * +grd_get_damage_region (uint8_t *current_data, + uint8_t *prev_data, + uint32_t surface_width, + uint32_t surface_height, + uint32_t tile_width, + uint32_t tile_height, + uint32_t stride, + uint32_t bytes_per_pixel) +{ + cairo_region_t *damage_region; + cairo_rectangle_int_t tile; + uint32_t cols, rows; + uint32_t x, y; + + damage_region = cairo_region_create (); + if (current_data == NULL || prev_data == NULL) + { + tile.x = tile.y = 0; + tile.width = surface_width; + tile.height = surface_height; + cairo_region_union_rectangle (damage_region, &tile); + + return damage_region; + } + + cols = surface_width / tile_width + (surface_width % tile_width ? 1 : 0); + rows = surface_height / tile_height + (surface_height % tile_height ? 1 : 0); + + for (y = 0; y < rows; ++y) + { + for (x = 0; x < cols; ++x) + { + tile.x = x * tile_width; + tile.y = y * tile_height; + tile.width = surface_width - tile.x < tile_width ? surface_width - tile.x + : tile_width; + tile.height = surface_height - tile.y < tile_height ? surface_height - tile.y + : tile_height; + + if (grd_is_tile_dirty (&tile, current_data, prev_data, stride, bytes_per_pixel)) + cairo_region_union_rectangle (damage_region, &tile); + } + } + + return damage_region; +} diff --git a/grd-damage-utils.h b/grd-damage-utils.h new file mode 100644 index 0000000..88129dd --- /dev/null +++ b/grd-damage-utils.h @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2020 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include +#include +#include + +cairo_region_t *grd_get_damage_region (uint8_t *current_data, + uint8_t *prev_data, + uint32_t surface_width, + uint32_t surface_height, + uint32_t tile_width, + uint32_t tile_height, + uint32_t stride, + uint32_t bytes_per_pixel); + +bool grd_is_tile_dirty (cairo_rectangle_int_t *tile, + uint8_t *current_data, + uint8_t *prev_data, + uint32_t stride, + uint32_t bytes_per_pixel); diff --git a/grd-debug.c b/grd-debug.c new file mode 100644 index 0000000..e8136b6 --- /dev/null +++ b/grd-debug.c @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2015,2022 Red Hat Inc. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + */ + +#include "config.h" + +#include "grd-debug.h" + +#include + +static const GDebugKey grd_debug_keys[] = { + { "vnc", GRD_DEBUG_VNC }, + { "tpm", GRD_DEBUG_TPM }, + { "vk-validation", GRD_DEBUG_VK_VALIDATION }, + { "vk-times", GRD_DEBUG_VK_TIMES }, + { "va-times", GRD_DEBUG_VA_TIMES }, + { "vkva-renderer", GRD_DEBUG_VKVA }, +}; + +static GrdDebugFlags debug_flags; + +static gpointer +init_debug_flags (gpointer user_data) +{ + const char *debug_env; + + debug_env = g_getenv ("GNOME_REMOTE_DESKTOP_DEBUG"); + if (debug_env) + { + debug_flags = g_parse_debug_string (debug_env, + grd_debug_keys, + G_N_ELEMENTS (grd_debug_keys)); + } + + return NULL; +} + +GrdDebugFlags +grd_get_debug_flags (void) +{ + static GOnce debug_flags_once = G_ONCE_INIT; + + g_once (&debug_flags_once, init_debug_flags, NULL); + + return debug_flags; +} diff --git a/grd-debug.h b/grd-debug.h new file mode 100644 index 0000000..87e1999 --- /dev/null +++ b/grd-debug.h @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2015,2022 Red Hat Inc. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + */ + +#pragma once + +typedef enum _GrdDebugFlags +{ + GRD_DEBUG_NONE = 0, + GRD_DEBUG_VNC = 1 << 0, + GRD_DEBUG_TPM = 1 << 1, + GRD_DEBUG_VK_VALIDATION = 1 << 2, + GRD_DEBUG_VK_TIMES = 1 << 3, + GRD_DEBUG_VA_TIMES = 1 << 4, + GRD_DEBUG_VKVA = 1 << 5, +} GrdDebugFlags; + +GrdDebugFlags grd_get_debug_flags (void); diff --git a/grd-decode-session-sw-avc.c b/grd-decode-session-sw-avc.c new file mode 100644 index 0000000..65a8409 --- /dev/null +++ b/grd-decode-session-sw-avc.c @@ -0,0 +1,422 @@ +/* + * Copyright (C) 2025 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-decode-session-sw-avc.h" + +#include +#include +#include +#include +#include + +#include "grd-sample-buffer.h" +#include "grd-utils.h" + +#define MAX_BASE_SIZE (1 << 18) + +typedef struct +{ + uint8_t *data; + + GrdSampleBuffer *sample_buffer; +} BitstreamBuffer; + +struct _GrdDecodeSessionSwAVC +{ + GrdDecodeSession parent; + + H264_CONTEXT *h264_context; + uint32_t surface_width; + uint32_t surface_height; + + GHashTable *bitstream_buffers; + GHashTable *pipewire_buffers; + + GMutex pending_frames_mutex; + GHashTable *pending_frames; +}; + +G_DEFINE_TYPE (GrdDecodeSessionSwAVC, grd_decode_session_sw_avc, + GRD_TYPE_DECODE_SESSION) + +static BitstreamBuffer * +bitstream_buffer_new (size_t size) +{ + BitstreamBuffer *bitstream_buffer; + + bitstream_buffer = g_new0 (BitstreamBuffer, 1); + + bitstream_buffer->data = g_malloc0 (size); + bitstream_buffer->sample_buffer = + grd_sample_buffer_new (bitstream_buffer->data, size); + + return bitstream_buffer; +} + +static void +bitstream_buffer_free (BitstreamBuffer *bitstream_buffer) +{ + g_clear_pointer (&bitstream_buffer->sample_buffer, grd_sample_buffer_free); + g_clear_pointer (&bitstream_buffer->data, g_free); + + g_free (bitstream_buffer); +} + +static void +grd_decode_session_sw_avc_get_drm_format_modifiers (GrdDecodeSession *decode_session, + uint32_t drm_format, + uint32_t *out_n_modifiers, + uint64_t **out_modifiers) +{ + *out_n_modifiers = 0; + *out_modifiers = NULL; +} + +static gboolean +grd_decode_session_sw_avc_reset (GrdDecodeSession *decode_session, + uint32_t surface_width, + uint32_t surface_height, + uint64_t drm_format_modifier, + GError **error) +{ + GrdDecodeSessionSwAVC *decode_session_sw_avc = + GRD_DECODE_SESSION_SW_AVC (decode_session); + + decode_session_sw_avc->surface_width = surface_width; + decode_session_sw_avc->surface_height = surface_height; + + if (!h264_context_reset (decode_session_sw_avc->h264_context, surface_width, surface_height)) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to reset H.264 decode context"); + return FALSE; + } + + return TRUE; +} + +static gboolean +allocate_mem_fd_buffer (GrdDecodeSessionSwAVC *decode_session_sw_avc, + struct pw_buffer *pw_buffer, + GError **error) +{ + uint32_t surface_width = decode_session_sw_avc->surface_width; + uint32_t surface_height = decode_session_sw_avc->surface_height; + struct spa_data *spa_data = &pw_buffer->buffer->datas[0]; + g_autofd int fd = -1; + unsigned int seals; + int ret; + + fd = memfd_create ("grd-decode-session-mem-fd", + MFD_CLOEXEC | MFD_ALLOW_SEALING); + if (fd == -1) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create mem-fd: %s", strerror (errno)); + return FALSE; + } + + spa_data->chunk->stride = surface_width * 4; + spa_data->maxsize = spa_data->chunk->stride * surface_height; + spa_data->mapoffset = 0; + + do + ret = ftruncate (fd, spa_data->maxsize); + while (ret == -1 && errno == EINTR); + if (ret < 0) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to truncate mem-fd: %s", strerror (errno)); + return FALSE; + } + + seals = F_SEAL_GROW | F_SEAL_SHRINK | F_SEAL_SEAL; + if (fcntl (fd, F_ADD_SEALS, seals) < 0) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to add seals to mem-fd: %s", strerror (errno)); + return FALSE; + } + + spa_data->data = mmap (NULL, spa_data->maxsize, + PROT_READ | PROT_WRITE, + MAP_SHARED, + fd, + spa_data->mapoffset); + if (spa_data->data == MAP_FAILED) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to mmap memory: %s", strerror (errno)); + return FALSE; + } + + spa_data->type = SPA_DATA_MemFd; + spa_data->flags = SPA_DATA_FLAG_READABLE | SPA_DATA_FLAG_MAPPABLE; + spa_data->fd = g_steal_fd (&fd); + + return TRUE; +} + +static gboolean +grd_decode_session_sw_avc_register_buffer (GrdDecodeSession *decode_session, + struct pw_buffer *pw_buffer, + GError **error) +{ + GrdDecodeSessionSwAVC *decode_session_sw_avc = + GRD_DECODE_SESSION_SW_AVC (decode_session); + uint32_t surface_width = decode_session_sw_avc->surface_width; + uint32_t surface_height = decode_session_sw_avc->surface_height; + BitstreamBuffer *bitstream_buffer; + size_t width_in_mbs; + size_t height_in_mbs; + size_t size; + + if (!allocate_mem_fd_buffer (decode_session_sw_avc, pw_buffer, error)) + return FALSE; + + width_in_mbs = grd_get_aligned_size (surface_width, 16) / 16; + height_in_mbs = grd_get_aligned_size (surface_height, 16) / 16; + size = MAX_BASE_SIZE + width_in_mbs * height_in_mbs * 400; + + bitstream_buffer = bitstream_buffer_new (size); + + g_hash_table_insert (decode_session_sw_avc->bitstream_buffers, + pw_buffer, bitstream_buffer); + g_hash_table_insert (decode_session_sw_avc->pipewire_buffers, + bitstream_buffer->sample_buffer, pw_buffer); + + return TRUE; +} + +static void +destroy_mem_fd_buffer (GrdDecodeSessionSwAVC *decode_session_sw_avc, + struct pw_buffer *pw_buffer) +{ + struct spa_data *spa_data = &pw_buffer->buffer->datas[0]; + + if (spa_data->type != SPA_DATA_MemFd) + return; + + g_assert (spa_data->data != MAP_FAILED); + g_assert (spa_data->fd > 0); + + munmap (spa_data->data, spa_data->maxsize); + close (spa_data->fd); +} + +static void +grd_decode_session_sw_avc_unregister_buffer (GrdDecodeSession *decode_session, + struct pw_buffer *pw_buffer) +{ + GrdDecodeSessionSwAVC *decode_session_sw_avc = + GRD_DECODE_SESSION_SW_AVC (decode_session); + BitstreamBuffer *bitstream_buffer = NULL; + GrdSampleBuffer *sample_buffer; + + if (!g_hash_table_lookup_extended (decode_session_sw_avc->bitstream_buffers, + pw_buffer, + NULL, (gpointer *) &bitstream_buffer)) + return; + + sample_buffer = bitstream_buffer->sample_buffer; + + g_hash_table_remove (decode_session_sw_avc->bitstream_buffers, pw_buffer); + g_hash_table_remove (decode_session_sw_avc->pipewire_buffers, sample_buffer); + + destroy_mem_fd_buffer (decode_session_sw_avc, pw_buffer); +} + +static GrdSampleBuffer * +grd_decode_session_sw_avc_get_sample_buffer (GrdDecodeSession *decode_session, + struct pw_buffer *pw_buffer) +{ + GrdDecodeSessionSwAVC *decode_session_sw_avc = + GRD_DECODE_SESSION_SW_AVC (decode_session); + BitstreamBuffer *bitstream_buffer = NULL; + + if (!g_hash_table_lookup_extended (decode_session_sw_avc->bitstream_buffers, + pw_buffer, + NULL, (gpointer *) &bitstream_buffer)) + g_assert_not_reached (); + + return bitstream_buffer->sample_buffer; +} + +static gboolean +grd_decode_session_sw_avc_submit_sample (GrdDecodeSession *decode_session, + GrdSampleBuffer *sample_buffer, + GError **error) +{ + GrdDecodeSessionSwAVC *decode_session_sw_avc = + GRD_DECODE_SESSION_SW_AVC (decode_session); + + g_mutex_lock (&decode_session_sw_avc->pending_frames_mutex); + g_assert (!g_hash_table_contains (decode_session_sw_avc->pending_frames, + sample_buffer)); + + g_hash_table_add (decode_session_sw_avc->pending_frames, sample_buffer); + g_mutex_unlock (&decode_session_sw_avc->pending_frames_mutex); + + return TRUE; +} + +static uint32_t +grd_decode_session_sw_avc_get_n_pending_frames (GrdDecodeSession *decode_session) +{ + GrdDecodeSessionSwAVC *decode_session_sw_avc = + GRD_DECODE_SESSION_SW_AVC (decode_session); + g_autoptr (GMutexLocker) locker = NULL; + + locker = g_mutex_locker_new (&decode_session_sw_avc->pending_frames_mutex); + return g_hash_table_size (decode_session_sw_avc->pending_frames); +} + +static gboolean +grd_decode_session_sw_avc_decode_frame (GrdDecodeSession *decode_session, + GrdSampleBuffer *sample_buffer, + GError **error) +{ + GrdDecodeSessionSwAVC *decode_session_sw_avc = + GRD_DECODE_SESSION_SW_AVC (decode_session); + uint32_t surface_width = decode_session_sw_avc->surface_width; + uint32_t surface_height = decode_session_sw_avc->surface_height; + struct pw_buffer *pw_buffer = NULL; + struct spa_data *spa_data; + RECTANGLE_16 rect = {}; + int32_t ret; + + g_mutex_lock (&decode_session_sw_avc->pending_frames_mutex); + if (!g_hash_table_remove (decode_session_sw_avc->pending_frames, + sample_buffer)) + g_assert_not_reached (); + g_mutex_unlock (&decode_session_sw_avc->pending_frames_mutex); + + if (!g_hash_table_lookup_extended (decode_session_sw_avc->pipewire_buffers, + sample_buffer, + NULL, (gpointer *) &pw_buffer)) + g_assert_not_reached (); + + spa_data = &pw_buffer->buffer->datas[0]; + + rect.left = 0; + rect.top = 0; + rect.right = surface_width; + rect.bottom = surface_height; + + ret = avc420_decompress (decode_session_sw_avc->h264_context, + grd_sample_buffer_get_data_pointer (sample_buffer), + grd_sample_buffer_get_sample_size (sample_buffer), + spa_data->data, PIXEL_FORMAT_BGRA32, + spa_data->chunk->stride, surface_width, surface_height, + &rect, 1); + if (ret < 0) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "avc420_decompress failed with status code %i", -ret); + return FALSE; + } + + return TRUE; +} + +GrdDecodeSessionSwAVC * +grd_decode_session_sw_avc_new (GError **error) +{ + g_autoptr (GrdDecodeSessionSwAVC) decode_session_sw_avc = NULL; + + decode_session_sw_avc = g_object_new (GRD_TYPE_DECODE_SESSION_SW_AVC, NULL); + + decode_session_sw_avc->h264_context = h264_context_new (FALSE); + if (!decode_session_sw_avc->h264_context) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create H.264 decode context"); + return NULL; + } + + return g_steal_pointer (&decode_session_sw_avc); +} + +static void +grd_decode_session_sw_avc_dispose (GObject *object) +{ + GrdDecodeSessionSwAVC *decode_session_sw_avc = + GRD_DECODE_SESSION_SW_AVC (object); + + g_clear_pointer (&decode_session_sw_avc->h264_context, h264_context_free); + + g_clear_pointer (&decode_session_sw_avc->pending_frames, g_hash_table_unref); + g_clear_pointer (&decode_session_sw_avc->pipewire_buffers, g_hash_table_unref); + g_clear_pointer (&decode_session_sw_avc->bitstream_buffers, g_hash_table_unref); + + G_OBJECT_CLASS (grd_decode_session_sw_avc_parent_class)->dispose (object); +} + +static void +grd_decode_session_sw_avc_finalize (GObject *object) +{ + GrdDecodeSessionSwAVC *decode_session_sw_avc = + GRD_DECODE_SESSION_SW_AVC (object); + + g_mutex_clear (&decode_session_sw_avc->pending_frames_mutex); + + G_OBJECT_CLASS (grd_decode_session_sw_avc_parent_class)->finalize (object); +} + +static void +grd_decode_session_sw_avc_init (GrdDecodeSessionSwAVC *decode_session_sw_avc) +{ + decode_session_sw_avc->bitstream_buffers = + g_hash_table_new_full (NULL, NULL, + NULL, (GDestroyNotify) bitstream_buffer_free); + decode_session_sw_avc->pipewire_buffers = g_hash_table_new (NULL, NULL); + decode_session_sw_avc->pending_frames = g_hash_table_new (NULL, NULL); + + g_mutex_init (&decode_session_sw_avc->pending_frames_mutex); +} + +static void +grd_decode_session_sw_avc_class_init (GrdDecodeSessionSwAVCClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GrdDecodeSessionClass *decode_session_class = + GRD_DECODE_SESSION_CLASS (klass); + + object_class->dispose = grd_decode_session_sw_avc_dispose; + object_class->finalize = grd_decode_session_sw_avc_finalize; + + decode_session_class->get_drm_format_modifiers = + grd_decode_session_sw_avc_get_drm_format_modifiers; + decode_session_class->reset = + grd_decode_session_sw_avc_reset; + decode_session_class->register_buffer = + grd_decode_session_sw_avc_register_buffer; + decode_session_class->unregister_buffer = + grd_decode_session_sw_avc_unregister_buffer; + decode_session_class->get_sample_buffer = + grd_decode_session_sw_avc_get_sample_buffer; + decode_session_class->submit_sample = + grd_decode_session_sw_avc_submit_sample; + decode_session_class->get_n_pending_frames = + grd_decode_session_sw_avc_get_n_pending_frames; + decode_session_class->decode_frame = + grd_decode_session_sw_avc_decode_frame; +} diff --git a/grd-decode-session-sw-avc.h b/grd-decode-session-sw-avc.h new file mode 100644 index 0000000..cbeced5 --- /dev/null +++ b/grd-decode-session-sw-avc.h @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2025 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include "grd-decode-session.h" + +#define GRD_TYPE_DECODE_SESSION_SW_AVC (grd_decode_session_sw_avc_get_type ()) +G_DECLARE_FINAL_TYPE (GrdDecodeSessionSwAVC, grd_decode_session_sw_avc, + GRD, DECODE_SESSION_SW_AVC, GrdDecodeSession) + +GrdDecodeSessionSwAVC *grd_decode_session_sw_avc_new (GError **error); diff --git a/grd-decode-session.c b/grd-decode-session.c new file mode 100644 index 0000000..50e790d --- /dev/null +++ b/grd-decode-session.c @@ -0,0 +1,267 @@ +/* + * Copyright (C) 2025 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-decode-session.h" + +typedef struct +{ + GrdSampleBuffer *sample_buffer; + + GrdDecodeSessionOnFrameReadyFunc callback; + gpointer callback_user_data; +} GrdDecodeFrameTask; + +typedef struct +{ + gboolean in_shutdown; + + GThread *decode_thread; + GMainContext *decode_context; + + GSource *frame_decode_source; + GAsyncQueue *task_queue; +} GrdDecodeSessionPrivate; + +G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (GrdDecodeSession, grd_decode_session, + G_TYPE_OBJECT) + +void +grd_decode_session_get_drm_format_modifiers (GrdDecodeSession *decode_session, + uint32_t drm_format, + uint32_t *out_n_modifiers, + uint64_t **out_modifiers) +{ + GrdDecodeSessionClass *klass = GRD_DECODE_SESSION_GET_CLASS (decode_session); + + klass->get_drm_format_modifiers (decode_session, drm_format, + out_n_modifiers, out_modifiers); +} + +gboolean +grd_decode_session_reset (GrdDecodeSession *decode_session, + uint32_t surface_width, + uint32_t surface_height, + uint64_t drm_format_modifier, + GError **error) +{ + GrdDecodeSessionClass *klass = GRD_DECODE_SESSION_GET_CLASS (decode_session); + + return klass->reset (decode_session, surface_width, surface_height, + drm_format_modifier, error); +} + +gboolean +grd_decode_session_register_buffer (GrdDecodeSession *decode_session, + struct pw_buffer *pw_buffer, + GError **error) +{ + GrdDecodeSessionClass *klass = GRD_DECODE_SESSION_GET_CLASS (decode_session); + + return klass->register_buffer (decode_session, pw_buffer, error); +} + +void +grd_decode_session_unregister_buffer (GrdDecodeSession *decode_session, + struct pw_buffer *pw_buffer) +{ + GrdDecodeSessionClass *klass = GRD_DECODE_SESSION_GET_CLASS (decode_session); + + return klass->unregister_buffer (decode_session, pw_buffer); +} + +GrdSampleBuffer * +grd_decode_session_get_sample_buffer (GrdDecodeSession *decode_session, + struct pw_buffer *pw_buffer) +{ + GrdDecodeSessionClass *klass = GRD_DECODE_SESSION_GET_CLASS (decode_session); + + return klass->get_sample_buffer (decode_session, pw_buffer); +} + +uint32_t +grd_decode_session_get_n_pending_frames (GrdDecodeSession *decode_session) +{ + GrdDecodeSessionClass *klass = GRD_DECODE_SESSION_GET_CLASS (decode_session); + + return klass->get_n_pending_frames (decode_session); +} + +gboolean +grd_decode_session_decode_frame (GrdDecodeSession *decode_session, + GrdSampleBuffer *sample_buffer, + GrdDecodeSessionOnFrameReadyFunc on_frame_ready, + gpointer user_data, + GError **error) +{ + GrdDecodeSessionClass *klass = GRD_DECODE_SESSION_GET_CLASS (decode_session); + GrdDecodeSessionPrivate *priv = + grd_decode_session_get_instance_private (decode_session); + GrdDecodeFrameTask *task; + + if (!klass->submit_sample (decode_session, sample_buffer, error)) + return FALSE; + + task = g_new0 (GrdDecodeFrameTask, 1); + task->sample_buffer = sample_buffer; + task->callback = on_frame_ready; + task->callback_user_data = user_data; + + g_async_queue_push (priv->task_queue, task); + g_source_set_ready_time (priv->frame_decode_source, 0); + + return TRUE; +} + +static void +stop_decode_thread (GrdDecodeSession *decode_session) +{ + GrdDecodeSessionPrivate *priv = + grd_decode_session_get_instance_private (decode_session); + + g_assert (priv->decode_context); + g_assert (priv->decode_thread); + + priv->in_shutdown = TRUE; + + g_main_context_wakeup (priv->decode_context); + g_clear_pointer (&priv->decode_thread, g_thread_join); +} + +static void +grd_decode_session_dispose (GObject *object) +{ + GrdDecodeSession *decode_session = GRD_DECODE_SESSION (object); + GrdDecodeSessionPrivate *priv = + grd_decode_session_get_instance_private (decode_session); + + g_assert (g_async_queue_try_pop (priv->task_queue) == NULL); + + if (priv->decode_thread) + stop_decode_thread (decode_session); + + if (priv->frame_decode_source) + { + g_source_destroy (priv->frame_decode_source); + g_clear_pointer (&priv->frame_decode_source, g_source_unref); + } + + g_clear_pointer (&priv->decode_context, g_main_context_unref); + + G_OBJECT_CLASS (grd_decode_session_parent_class)->dispose (object); +} + +static void +grd_decode_session_finalize (GObject *object) +{ + GrdDecodeSession *decode_session = GRD_DECODE_SESSION (object); + GrdDecodeSessionPrivate *priv = + grd_decode_session_get_instance_private (decode_session); + + g_clear_pointer (&priv->task_queue, g_async_queue_unref); + + G_OBJECT_CLASS (grd_decode_session_parent_class)->finalize (object); +} + +static gpointer +decode_thread_func (gpointer data) +{ + GrdDecodeSession *decode_session = data; + GrdDecodeSessionPrivate *priv = + grd_decode_session_get_instance_private (decode_session); + + while (!priv->in_shutdown) + g_main_context_iteration (priv->decode_context, TRUE); + + return NULL; +} + +static gboolean +decode_frames (gpointer user_data) +{ + GrdDecodeSession *decode_session = user_data; + GrdDecodeSessionClass *klass = GRD_DECODE_SESSION_GET_CLASS (decode_session); + GrdDecodeSessionPrivate *priv = + grd_decode_session_get_instance_private (decode_session); + GrdDecodeFrameTask *task; + + while ((task = g_async_queue_try_pop (priv->task_queue))) + { + gboolean success; + g_autoptr (GError) error = NULL; + + success = klass->decode_frame (decode_session, task->sample_buffer, + &error); + g_assert (success == !error); + + task->callback (decode_session, task->sample_buffer, + task->callback_user_data, error); + + g_free (task); + } + + return G_SOURCE_CONTINUE; +} + +static gboolean +source_dispatch (GSource *source, + GSourceFunc callback, + gpointer user_data) +{ + g_source_set_ready_time (source, -1); + + return callback (user_data); +} + +static GSourceFuncs source_funcs = +{ + .dispatch = source_dispatch, +}; + +static void +grd_decode_session_init (GrdDecodeSession *decode_session) +{ + GrdDecodeSessionPrivate *priv = + grd_decode_session_get_instance_private (decode_session); + GSource *frame_decode_source; + + priv->task_queue = g_async_queue_new (); + + priv->decode_context = g_main_context_new (); + priv->decode_thread = g_thread_new ("Decode thread", + decode_thread_func, + decode_session); + + frame_decode_source = g_source_new (&source_funcs, sizeof (GSource)); + g_source_set_callback (frame_decode_source, decode_frames, + decode_session, NULL); + g_source_set_ready_time (frame_decode_source, -1); + g_source_attach (frame_decode_source, priv->decode_context); + priv->frame_decode_source = frame_decode_source; +} + +static void +grd_decode_session_class_init (GrdDecodeSessionClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = grd_decode_session_dispose; + object_class->finalize = grd_decode_session_finalize; +} diff --git a/grd-decode-session.h b/grd-decode-session.h new file mode 100644 index 0000000..1a9bef5 --- /dev/null +++ b/grd-decode-session.h @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2025 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include +#include + +#include "grd-types.h" + +#define GRD_TYPE_DECODE_SESSION (grd_decode_session_get_type ()) +G_DECLARE_DERIVABLE_TYPE (GrdDecodeSession, grd_decode_session, + GRD, DECODE_SESSION, GObject) + +typedef void (* GrdDecodeSessionOnFrameReadyFunc) (GrdDecodeSession *decode_session, + GrdSampleBuffer *sample_buffer, + gpointer user_data, + GError *error); + +struct _GrdDecodeSessionClass +{ + GObjectClass parent_class; + + void (* get_drm_format_modifiers) (GrdDecodeSession *decode_session, + uint32_t drm_format, + uint32_t *out_n_modifiers, + uint64_t **out_modifiers); + gboolean (* reset) (GrdDecodeSession *decode_session, + uint32_t surface_width, + uint32_t surface_height, + uint64_t drm_format_modifier, + GError **error); + gboolean (* register_buffer) (GrdDecodeSession *decode_session, + struct pw_buffer *pw_buffer, + GError **error); + void (* unregister_buffer) (GrdDecodeSession *decode_session, + struct pw_buffer *pw_buffer); + GrdSampleBuffer *(* get_sample_buffer) (GrdDecodeSession *decode_session, + struct pw_buffer *pw_buffer); + gboolean (* submit_sample) (GrdDecodeSession *decode_session, + GrdSampleBuffer *sample_buffer, + GError **error); + uint32_t (* get_n_pending_frames) (GrdDecodeSession *decode_session); + gboolean (* decode_frame) (GrdDecodeSession *decode_session, + GrdSampleBuffer *sample_buffer, + GError **error); +}; + +void grd_decode_session_get_drm_format_modifiers (GrdDecodeSession *decode_session, + uint32_t drm_format, + uint32_t *out_n_modifiers, + uint64_t **out_modifiers); + +gboolean grd_decode_session_reset (GrdDecodeSession *decode_session, + uint32_t surface_width, + uint32_t surface_height, + uint64_t drm_format_modifier, + GError **error); + +gboolean grd_decode_session_register_buffer (GrdDecodeSession *decode_session, + struct pw_buffer *pw_buffer, + GError **error); + +void grd_decode_session_unregister_buffer (GrdDecodeSession *decode_session, + struct pw_buffer *pw_buffer); + +GrdSampleBuffer *grd_decode_session_get_sample_buffer (GrdDecodeSession *decode_session, + struct pw_buffer *pw_buffer); + +uint32_t grd_decode_session_get_n_pending_frames (GrdDecodeSession *decode_session); + +gboolean grd_decode_session_decode_frame (GrdDecodeSession *decode_session, + GrdSampleBuffer *sample_buffer, + GrdDecodeSessionOnFrameReadyFunc on_frame_ready, + gpointer user_data, + GError **error); diff --git a/grd-egl-thread.c b/grd-egl-thread.c new file mode 100644 index 0000000..2418d9c --- /dev/null +++ b/grd-egl-thread.c @@ -0,0 +1,1325 @@ +/* + * Copyright (C) 2021 Red Hat Inc. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + */ + +#include "config.h" + +#include "grd-egl-thread.h" + +#include +#include +#include + +#ifndef EGL_DRM_RENDER_NODE_FILE_EXT +#define EGL_DRM_RENDER_NODE_FILE_EXT 0x3377 +#endif + +struct _GrdEglThread +{ + GThread *thread; + GMutex mutex; + GCond cond; + + GAsyncQueue *task_queue; + GHashTable *slot_table; + + GHashTable *modifiers; + + struct + { + gboolean initialized; + GError *error; + + GMainContext *main_context; + GMainLoop *main_loop; + + EGLDisplay egl_display; + EGLContext egl_context; + + const char *drm_render_node; + + GSource *egl_thread_source; + } impl; +}; + +typedef struct _GrdEglTask +{ + GFunc func; + GDestroyNotify destroy; + + GrdEglThreadCallback callback; + gpointer callback_user_data; + GDestroyNotify callback_destroy; +} GrdEglTask; + +typedef struct +{ + GrdEglTask base; + + GMutex task_mutex; + GrdEglTask *task; +} GrdReplaceableTask; + +typedef struct _GrdEglTaskCustom +{ + GrdEglTask base; + + GrdEglThreadCustomFunc custom_func; + gpointer user_data; +} GrdEglTaskCustom; + +typedef struct _GrdEglTaskSync +{ + GrdEglTask base; +} GrdEglTaskSync; + +typedef struct _GrdEglTaskUpload +{ + GrdEglTask base; + + GLuint pbo; + + uint32_t height; + uint32_t stride; + uint8_t *src_data; + + GrdEglThreadAllocBufferFunc allocate_func; + gpointer allocate_user_data; + GDestroyNotify allocate_user_data_destroy; + + GrdEglThreadCustomFunc realize_func; + gpointer realize_user_data; + GDestroyNotify realize_user_data_destroy; +} GrdEglTaskUpload; + +typedef struct _GrdEglTaskDeallocateMemory +{ + GrdEglTask base; + + GLuint pbo; + + GrdEglThreadDeallocBufferFunc deallocate_func; + gpointer deallocate_user_data; +} GrdEglTaskDeallocateMemory; + +typedef struct _GrdEglTaskAllocateMemory +{ + GrdEglTask base; + + uint32_t height; + uint32_t stride; + + GrdEglThreadAllocBufferFunc allocate_func; + gpointer allocate_user_data; +} GrdEglTaskAllocateMemory; + +typedef struct _GrdEglTaskDownload +{ + GrdEglTask base; + + uint8_t *dst_data; + int dst_row_width; + + uint32_t format; + unsigned int width; + unsigned int height; + uint32_t n_planes; + int *fds; + uint32_t *strides; + uint32_t *offsets; + uint64_t *modifiers; +} GrdEglTaskDownload; + +static void +grd_egl_task_free (GrdEglTask *task) +{ + if (task->callback_destroy) + g_clear_pointer (&task->callback_user_data, task->callback_destroy); + g_free (task); +} + +static void +run_replaceable_task_in_impl (gpointer data, + gpointer user_data) +{ + GrdEglThread *egl_thread = data; + GrdReplaceableTask *replaceable_task = user_data; + g_autoptr (GMutexLocker) locker = NULL; + GrdEglTask *task; + + locker = g_mutex_locker_new (&replaceable_task->task_mutex); + task = g_steal_pointer (&replaceable_task->task); + if (!task) + return; + + g_clear_pointer (&locker, g_mutex_locker_free); + + task->func (egl_thread, task); + task->destroy (task); +} + +static void +replaceable_task_dummy_free (GrdEglTask *task_base) +{ +} + +static GrdReplaceableTask * +replaceable_task_new (void) +{ + GrdReplaceableTask *replaceable_task; + + replaceable_task = g_new0 (GrdReplaceableTask, 1); + replaceable_task->base.func = run_replaceable_task_in_impl; + replaceable_task->base.destroy = (GDestroyNotify) replaceable_task_dummy_free; + + g_mutex_init (&replaceable_task->task_mutex); + + return replaceable_task; +} + +static void +replaceable_task_replace_task (GrdReplaceableTask *replaceable_task, + GrdEglTask *new_task) +{ + GrdEglTask *task; + + g_mutex_lock (&replaceable_task->task_mutex); + task = g_steal_pointer (&replaceable_task->task); + + if (task) + { + if (task->callback) + task->callback (FALSE, task->callback_user_data); + + task->destroy (task); + } + + replaceable_task->task = new_task; + g_mutex_unlock (&replaceable_task->task_mutex); +} + +static void +replaceable_task_free (GrdReplaceableTask *replaceable_task) +{ + replaceable_task_replace_task (replaceable_task, NULL); + + g_mutex_clear (&replaceable_task->task_mutex); + + grd_egl_task_free (&replaceable_task->base); +} + +static gboolean +is_hardware_accelerated (void) +{ + const char *renderer; + gboolean is_software; + + renderer = (const char *) glGetString (GL_RENDERER); + g_assert_cmpuint (glGetError (), ==, GL_NO_ERROR); + if (!renderer) + { + g_warning ("OpenGL driver returned NULL as the renderer, " + "something is wrong"); + return FALSE; + } + + is_software = (strstr (renderer, "llvmpipe") || + strstr (renderer, "softpipe") || + strstr (renderer, "software rasterizer") || + strstr (renderer, "Software Rasterizer") || + strstr (renderer, "SWR")); + return !is_software; +} + +static gboolean +query_format_modifiers (GrdEglThread *egl_thread, + EGLDisplay egl_display, + GError **error) +{ + EGLint n_formats = 0; + g_autofree EGLint *formats = NULL; + EGLint i; + + if (!eglQueryDmaBufFormatsEXT (egl_display, 0, NULL, &n_formats)) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to query number of DMA buffer formats"); + return FALSE; + } + + if (n_formats == 0) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "No available DMA buffer formats"); + return FALSE; + } + + formats = g_new0 (EGLint, n_formats); + if (!eglQueryDmaBufFormatsEXT (egl_display, n_formats, formats, &n_formats)) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to query DMA buffer formats"); + return FALSE; + } + + for (i = 0; i < n_formats; i++) + { + EGLint n_modifiers; + GArray *modifiers; + + if (!eglQueryDmaBufModifiersEXT (egl_display, + formats[i], 0, NULL, NULL, + &n_modifiers)) + { + g_warning ("Failed to query number of modifiers for format %d", + formats[0]); + continue; + } + + if (n_modifiers == 0) + { + g_debug ("No modifiers available for format %d", formats[0]); + continue; + } + + modifiers = g_array_sized_new (FALSE, FALSE, + sizeof (EGLuint64KHR), + n_modifiers); + g_array_set_size (modifiers, n_modifiers); + if (!eglQueryDmaBufModifiersEXT (egl_display, + formats[i], n_modifiers, + (EGLuint64KHR *) modifiers->data, + NULL, + &n_modifiers)) + { + g_warning ("Failed to query modifiers for format %d", + formats[0]); + continue; + } + + g_hash_table_insert (egl_thread->modifiers, + GINT_TO_POINTER (formats[i]), + modifiers); + } + + return TRUE; +} + +static gboolean +lookup_drm_render_node (GrdEglThread *egl_thread, + EGLDisplay egl_display, + GError **error) +{ + EGLAttrib egl_attrib = 0; + EGLDeviceEXT egl_device; + const char *egl_extensions; + + if (!eglQueryDisplayAttribEXT (egl_display, EGL_DEVICE_EXT, &egl_attrib)) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, + "Failed to retrieve EGL device"); + return FALSE; + } + g_assert (egl_attrib); + + egl_device = (EGLDeviceEXT) egl_attrib; + + egl_extensions = eglQueryDeviceStringEXT (egl_device, EGL_EXTENSIONS); + if (!egl_extensions) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, + "Failed to retrieve EGL device extensions"); + return FALSE; + } + + if (!strstr (egl_extensions, "EGL_EXT_device_drm_render_node")) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, + "Missing extension 'EGL_EXT_device_drm_render_node'"); + return FALSE; + } + + egl_thread->impl.drm_render_node = + eglQueryDeviceStringEXT (egl_device, EGL_DRM_RENDER_NODE_FILE_EXT); + if (!egl_thread->impl.drm_render_node) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, + "Failed to retrieve EGL device extensions"); + return FALSE; + } + + return TRUE; +} + +static gboolean +initialize_egl_context (GrdEglThread *egl_thread, + EGLenum egl_platform, + GError **error) +{ + EGLDisplay egl_display; + EGLint major, minor; + EGLint *attrs; + EGLContext egl_context; + + g_clear_error (error); + + egl_display = eglGetPlatformDisplayEXT (egl_platform, NULL, NULL); + if (egl_display == EGL_NO_DISPLAY) + { + int code = egl_platform == EGL_PLATFORM_DEVICE_EXT ? G_IO_ERROR_NOT_SUPPORTED + : G_IO_ERROR_FAILED; + g_set_error (error, G_IO_ERROR, code, + "Failed to get EGL display"); + return FALSE; + } + + if (!eglInitialize (egl_display, &major, &minor)) + { + eglTerminate (egl_display); + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to initialize EGL display"); + return FALSE; + } + + if (!eglBindAPI (EGL_OPENGL_ES_API)) + { + eglTerminate (egl_display); + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to bind OpenGL ES API"); + return FALSE; + } + + if (egl_platform == EGL_PLATFORM_WAYLAND_EXT) + { + if (!epoxy_has_egl_extension (egl_display, "EGL_MESA_configless_context")) + { + eglTerminate (egl_display); + g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, + "Missing extension 'EGL_MESA_configless_context'"); + return FALSE; + } + } + + if (!epoxy_has_egl_extension (egl_display, + "EGL_EXT_image_dma_buf_import_modifiers")) + { + eglTerminate (egl_display); + g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, + "Missing extension 'EGL_EXT_image_dma_buf_import_modifiers'"); + return FALSE; + } + + attrs = (EGLint[]) { + EGL_CONTEXT_CLIENT_VERSION, 2, + EGL_NONE + }; + egl_context = eglCreateContext (egl_display, + EGL_NO_CONFIG_KHR, + EGL_NO_CONTEXT, + attrs); + if (egl_context == EGL_NO_CONTEXT) + { + eglTerminate (egl_display); + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create EGL context"); + return FALSE; + } + + eglMakeCurrent (egl_display, + EGL_NO_SURFACE, + EGL_NO_SURFACE, + egl_context); + + if (!query_format_modifiers (egl_thread, egl_display, error)) + { + eglMakeCurrent (egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, + EGL_NO_CONTEXT); + eglDestroyContext (egl_display, egl_context); + eglTerminate (egl_display); + return FALSE; + } + + if (g_hash_table_size (egl_thread->modifiers) == 0) + { + eglMakeCurrent (egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, + EGL_NO_CONTEXT); + eglDestroyContext (egl_display, egl_context); + eglTerminate (egl_display); + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "No DMA buffer modifiers / format pair found"); + return FALSE; + } + + if (!is_hardware_accelerated ()) + { + eglMakeCurrent (egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, + EGL_NO_CONTEXT); + eglDestroyContext (egl_display, egl_context); + eglTerminate (egl_display); + g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, + "EGL context not hardware accelerated"); + return FALSE; + } + + if (!lookup_drm_render_node (egl_thread, egl_display, error)) + { + eglMakeCurrent (egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, + EGL_NO_CONTEXT); + eglDestroyContext (egl_display, egl_context); + eglTerminate (egl_display); + return FALSE; + } + + g_debug ("[EGL] DRM render node path: %s", egl_thread->impl.drm_render_node); + + egl_thread->impl.egl_display = egl_display; + egl_thread->impl.egl_context = egl_context; + + return TRUE; +} + +static gboolean +grd_egl_init_in_impl (GrdEglThread *egl_thread, + GError **error) +{ + gboolean initialized; + + if (!epoxy_has_egl_extension (NULL, "EGL_EXT_platform_base")) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Missing extension 'EGL_EXT_platform_base'"); + return FALSE; + } + + initialized = initialize_egl_context (egl_thread, + EGL_PLATFORM_WAYLAND_EXT, + error); + if (initialized) + { + g_debug ("EGL context initialized with platform " + "EGL_PLATFORM_WAYLAND_EXT"); + } + else + { + initialized = initialize_egl_context (egl_thread, + EGL_PLATFORM_DEVICE_EXT, + error); + if (initialized) + { + g_debug ("EGL context initialized with platform " + "EGL_PLATFORM_DEVICE_EXT"); + } + } + + return initialized; +} + +static void +grd_egl_task_download_free (GrdEglTask *task_base) +{ + GrdEglTaskDownload *task = (GrdEglTaskDownload *) task_base; + + g_free (task->fds); + g_free (task->strides); + g_free (task->offsets); + g_free (task->modifiers); + + grd_egl_task_free (task_base); +} + +static void +grd_egl_task_upload_free (GrdEglTask *task_base) +{ + GrdEglTaskUpload *task = (GrdEglTaskUpload *) task_base; + + task->allocate_user_data_destroy (task->allocate_user_data); + task->realize_user_data_destroy (task->realize_user_data); + + grd_egl_task_free (task_base); +} + +static void +grd_thread_dispatch_in_impl (GrdEglThread *egl_thread) +{ + GrdEglTask *task; + + task = g_async_queue_try_pop (egl_thread->task_queue); + if (!task) + return; + + task->func (egl_thread, task); + task->destroy (task); +} + +typedef struct _EglTaskSource +{ + GSource base; + GrdEglThread *egl_thread; +} EglTaskSource; + +static gboolean +egl_task_source_prepare (GSource *source, + int *timeout) +{ + EglTaskSource *task_source = (EglTaskSource *) source; + GrdEglThread *egl_thread = task_source->egl_thread; + + g_assert (g_source_get_context (source) == egl_thread->impl.main_context); + + *timeout = -1; + + return g_async_queue_length (egl_thread->task_queue) > 0; +} + +static gboolean +egl_task_source_check (GSource *source) +{ + EglTaskSource *task_source = (EglTaskSource *) source; + GrdEglThread *egl_thread = task_source->egl_thread; + + g_assert (g_source_get_context (source) == egl_thread->impl.main_context); + + return g_async_queue_length (egl_thread->task_queue) > 0; +} + +static gboolean +egl_task_source_dispatch (GSource *source, + GSourceFunc callback, + gpointer user_data) +{ + EglTaskSource *task_source = (EglTaskSource *) source; + GrdEglThread *egl_thread = task_source->egl_thread; + + g_assert (g_source_get_context (source) == egl_thread->impl.main_context); + + grd_thread_dispatch_in_impl (egl_thread); + + return G_SOURCE_CONTINUE; +} + +static GSourceFuncs egl_task_source_funcs = { + .prepare = egl_task_source_prepare, + .check = egl_task_source_check, + .dispatch = egl_task_source_dispatch, +}; + +static gpointer +grd_egl_thread_func (gpointer user_data) +{ + GrdEglThread *egl_thread = user_data; + GError *error = NULL; + + egl_thread->impl.main_context = g_main_context_new (); + + if (!grd_egl_init_in_impl (egl_thread, &error)) + { + g_propagate_error (&egl_thread->impl.error, error); + g_mutex_lock (&egl_thread->mutex); + egl_thread->impl.initialized = TRUE; + g_cond_signal (&egl_thread->cond); + g_mutex_unlock (&egl_thread->mutex); + return NULL; + } + + egl_thread->impl.egl_thread_source = + g_source_new (&egl_task_source_funcs, sizeof (EglTaskSource)); + ((EglTaskSource *) egl_thread->impl.egl_thread_source)->egl_thread = + egl_thread; + g_source_set_name (egl_thread->impl.egl_thread_source, "EGL thread source"); + g_source_set_ready_time (egl_thread->impl.egl_thread_source, -1); + g_source_attach (egl_thread->impl.egl_thread_source, + egl_thread->impl.main_context); + egl_thread->task_queue = g_async_queue_new (); + + egl_thread->impl.main_loop = g_main_loop_new (egl_thread->impl.main_context, + FALSE); + g_mutex_lock (&egl_thread->mutex); + egl_thread->impl.initialized = TRUE; + g_cond_signal (&egl_thread->cond); + g_mutex_unlock (&egl_thread->mutex); + + g_main_loop_run (egl_thread->impl.main_loop); + + g_source_destroy (egl_thread->impl.egl_thread_source); + g_source_unref (egl_thread->impl.egl_thread_source); + + g_clear_pointer (&egl_thread->impl.main_loop, g_main_loop_unref); + + while (TRUE) + { + GrdEglTask *task; + + task = g_async_queue_try_pop (egl_thread->task_queue); + if (!task) + break; + + task->destroy (task); + } + g_async_queue_unref (egl_thread->task_queue); + + eglMakeCurrent (egl_thread->impl.egl_display, + EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + eglDestroyContext (egl_thread->impl.egl_display, + egl_thread->impl.egl_context); + eglTerminate (egl_thread->impl.egl_display); + + return NULL; +} + +GrdEglThread * +grd_egl_thread_new (GError **error) +{ + g_autoptr (GrdEglThread) egl_thread = NULL; + + egl_thread = g_new0 (GrdEglThread, 1); + egl_thread->slot_table = + g_hash_table_new_full (NULL, NULL, + NULL, (GDestroyNotify) replaceable_task_free); + egl_thread->modifiers = + g_hash_table_new_full (NULL, NULL, + NULL, (GDestroyNotify) g_array_unref); + + g_mutex_init (&egl_thread->mutex); + g_cond_init (&egl_thread->cond); + + g_mutex_lock (&egl_thread->mutex); + + egl_thread->thread = g_thread_new ("GRD EGL thread", + grd_egl_thread_func, + egl_thread); + + while (!egl_thread->impl.initialized) + g_cond_wait (&egl_thread->cond, &egl_thread->mutex); + g_mutex_unlock (&egl_thread->mutex); + + if (egl_thread->impl.error) + { + g_propagate_error (error, egl_thread->impl.error); + return NULL; + } + + return g_steal_pointer (&egl_thread); +} + +static gboolean +quit_main_loop_in_impl (gpointer user_data) +{ + GrdEglThread *egl_thread = user_data; + + if (egl_thread->impl.main_loop) + g_main_loop_quit (egl_thread->impl.main_loop); + + return G_SOURCE_REMOVE; +} + +void +grd_egl_thread_free (GrdEglThread *egl_thread) +{ + GSource *source; + + source = g_idle_source_new (); + g_source_set_callback (source, quit_main_loop_in_impl, egl_thread, NULL); + g_source_attach (source, egl_thread->impl.main_context); + g_source_unref (source); + + g_thread_join (egl_thread->thread); + g_mutex_clear (&egl_thread->mutex); + g_cond_clear (&egl_thread->cond); + + g_clear_pointer (&egl_thread->modifiers, g_hash_table_unref); + g_clear_pointer (&egl_thread->impl.main_context, g_main_context_unref); + + g_assert (g_hash_table_size (egl_thread->slot_table) == 0); + g_clear_pointer (&egl_thread->slot_table, g_hash_table_unref); + + g_free (egl_thread); +} + +const char * +grd_egl_thread_get_drm_render_node (GrdEglThread *egl_thread) +{ + return egl_thread->impl.drm_render_node; +} + +void * +grd_egl_thread_acquire_slot (GrdEglThread *egl_thread) +{ + GrdReplaceableTask *replaceable_task; + + replaceable_task = replaceable_task_new (); + + g_async_queue_lock (egl_thread->task_queue); + g_hash_table_add (egl_thread->slot_table, replaceable_task); + g_async_queue_unlock (egl_thread->task_queue); + + return replaceable_task; +} + +void +grd_egl_thread_release_slot (GrdEglThread *egl_thread, + GrdEglThreadSlot slot) +{ + g_async_queue_lock (egl_thread->task_queue); + g_hash_table_remove (egl_thread->slot_table, slot); + g_async_queue_unlock (egl_thread->task_queue); +} + +static EGLImageKHR +create_dmabuf_image (GrdEglThread *egl_thread, + unsigned int width, + unsigned int height, + uint32_t format, + uint32_t n_planes, + const int *fds, + const uint32_t *strides, + const uint32_t *offsets, + const uint64_t *modifiers) +{ + EGLint attribs[37]; + int atti = 0; + EGLImageKHR egl_image; + + attribs[atti++] = EGL_WIDTH; + attribs[atti++] = width; + attribs[atti++] = EGL_HEIGHT; + attribs[atti++] = height; + attribs[atti++] = EGL_LINUX_DRM_FOURCC_EXT; + attribs[atti++] = format; + + if (n_planes > 0) + { + attribs[atti++] = EGL_DMA_BUF_PLANE0_FD_EXT; + attribs[atti++] = fds[0]; + attribs[atti++] = EGL_DMA_BUF_PLANE0_OFFSET_EXT; + attribs[atti++] = offsets[0]; + attribs[atti++] = EGL_DMA_BUF_PLANE0_PITCH_EXT; + attribs[atti++] = strides[0]; + if (modifiers) + { + attribs[atti++] = EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT; + attribs[atti++] = modifiers[0] & 0xFFFFFFFF; + attribs[atti++] = EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT; + attribs[atti++] = modifiers[0] >> 32; + } + } + + if (n_planes > 1) + { + attribs[atti++] = EGL_DMA_BUF_PLANE1_FD_EXT; + attribs[atti++] = fds[1]; + attribs[atti++] = EGL_DMA_BUF_PLANE1_OFFSET_EXT; + attribs[atti++] = offsets[1]; + attribs[atti++] = EGL_DMA_BUF_PLANE1_PITCH_EXT; + attribs[atti++] = strides[1]; + if (modifiers) + { + attribs[atti++] = EGL_DMA_BUF_PLANE1_MODIFIER_LO_EXT; + attribs[atti++] = modifiers[1] & 0xFFFFFFFF; + attribs[atti++] = EGL_DMA_BUF_PLANE1_MODIFIER_HI_EXT; + attribs[atti++] = modifiers[1] >> 32; + } + } + + if (n_planes > 2) + { + attribs[atti++] = EGL_DMA_BUF_PLANE2_FD_EXT; + attribs[atti++] = fds[2]; + attribs[atti++] = EGL_DMA_BUF_PLANE2_OFFSET_EXT; + attribs[atti++] = offsets[2]; + attribs[atti++] = EGL_DMA_BUF_PLANE2_PITCH_EXT; + attribs[atti++] = strides[2]; + if (modifiers) + { + attribs[atti++] = EGL_DMA_BUF_PLANE2_MODIFIER_LO_EXT; + attribs[atti++] = modifiers[2] & 0xFFFFFFFF; + attribs[atti++] = EGL_DMA_BUF_PLANE2_MODIFIER_HI_EXT; + attribs[atti++] = modifiers[2] >> 32; + } + } + + attribs[atti++] = EGL_NONE; + g_assert (atti <= G_N_ELEMENTS (attribs)); + + egl_image = eglCreateImageKHR (egl_thread->impl.egl_display, + EGL_NO_CONTEXT, + EGL_LINUX_DMA_BUF_EXT, NULL, + attribs); + if (egl_image == EGL_NO_IMAGE) + { + g_warning ("Failed to import DMA buffer as EGL image: %d", + eglGetError ()); + } + + return egl_image; +} + +static gboolean +bind_egl_image (GrdEglThread *egl_thread, + EGLImageKHR egl_image, + int dst_row_width, + GLuint *tex, + GLuint *fbo) +{ + GLenum error; + + glGenTextures (1, tex); + glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0); + glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glBindTexture (GL_TEXTURE_2D, *tex); + glPixelStorei (GL_PACK_ROW_LENGTH, dst_row_width); + glEGLImageTargetTexture2DOES (GL_TEXTURE_2D, egl_image); + error = glGetError (); + if (error != GL_NO_ERROR) + { + g_warning ("[EGL Thread] Failed to bind DMA buf texture: %u", error); + return FALSE; + } + + if (!fbo) + return TRUE; + + glGenFramebuffers (1, fbo); + glBindFramebuffer (GL_FRAMEBUFFER, *fbo); + glFramebufferTexture2D (GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, + GL_TEXTURE_2D, *tex, 0); + if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != + GL_FRAMEBUFFER_COMPLETE) + { + g_warning ("Failed to bind DMA buf framebuffer"); + return FALSE; + } + + return TRUE; +} + +static void +read_pixels (uint8_t *dst_data, + unsigned int width, + unsigned int height) +{ + glReadPixels (0, 0, width, height, + GL_BGRA, GL_UNSIGNED_BYTE, + dst_data); +} + +static void +download_in_impl (gpointer data, + gpointer user_data) +{ + GrdEglThread *egl_thread = data; + GrdEglTaskDownload *task = user_data; + EGLImageKHR egl_image = EGL_NO_IMAGE; + gboolean success = FALSE; + GLuint tex = 0; + GLuint fbo = 0; + + egl_image = + create_dmabuf_image (egl_thread, + task->width, + task->height, + task->format, + task->n_planes, + task->fds, + task->strides, + task->offsets, + task->modifiers); + if (egl_image == EGL_NO_IMAGE) + goto out; + + if (!bind_egl_image (egl_thread, egl_image, task->dst_row_width, &tex, + task->dst_data ? &fbo : NULL)) + goto out; + + if (task->dst_data) + read_pixels (task->dst_data, task->width, task->height); + + success = TRUE; + +out: + glBindBuffer (GL_PIXEL_PACK_BUFFER, 0); + + if (fbo) + { + glBindFramebuffer (GL_FRAMEBUFFER, 0); + glDeleteFramebuffers (1, &fbo); + } + if (tex) + { + glBindTexture (GL_TEXTURE_2D, 0); + glDeleteTextures (1, &tex); + } + + if (egl_image != EGL_NO_IMAGE) + eglDestroyImageKHR (egl_thread->impl.egl_display, egl_image); + + task->base.callback (success, task->base.callback_user_data); +} + +static void +allocate_in_impl (gpointer data, + gpointer user_data) +{ + GrdEglTaskAllocateMemory *task = user_data; + gboolean success = FALSE; + uint32_t buffer_size; + GLuint pbo = 0; + + buffer_size = task->stride * task->height * sizeof (uint8_t); + + glGenBuffers (1, &pbo); + glBindBuffer (GL_PIXEL_PACK_BUFFER, pbo); + glBufferData (GL_PIXEL_PACK_BUFFER, buffer_size, NULL, GL_DYNAMIC_DRAW); + glBindBuffer (GL_PIXEL_PACK_BUFFER, 0); + + if (task->allocate_func (task->allocate_user_data, pbo)) + success = TRUE; + + if (!success && pbo) + { + glDeleteBuffers (1, &pbo); + pbo = 0; + } + + task->base.callback (success, task->base.callback_user_data); +} + +static void +deallocate_in_impl (gpointer data, + gpointer user_data) +{ + GrdEglTaskDeallocateMemory *task = user_data; + + task->deallocate_func (task->deallocate_user_data); + + if (task->pbo) + glDeleteBuffers (1, &task->pbo); + + if (task->base.callback) + task->base.callback (TRUE, task->base.callback_user_data); +} + +static void +upload_in_impl (gpointer data, + gpointer user_data) +{ + GrdEglTaskUpload *task = user_data; + gboolean success = FALSE; + uint32_t buffer_size; + GLenum error; + + buffer_size = task->stride * task->height * sizeof (uint8_t); + + if (!task->pbo) + { + GLuint pbo = 0; + + glGenBuffers (1, &pbo); + glBindBuffer (GL_PIXEL_PACK_BUFFER, pbo); + glBufferData (GL_PIXEL_PACK_BUFFER, buffer_size, NULL, GL_DYNAMIC_DRAW); + glBindBuffer (GL_PIXEL_PACK_BUFFER, 0); + + if (!task->allocate_func (task->allocate_user_data, pbo)) + { + g_warning ("[EGL Thread] Failed to allocate GL resources"); + glDeleteBuffers (1, &pbo); + goto out; + } + + g_debug ("[EGL Thread] Allocating GL resources was successful"); + task->pbo = pbo; + } + + glBindBuffer (GL_PIXEL_PACK_BUFFER, task->pbo); + glBufferData (GL_PIXEL_PACK_BUFFER, buffer_size, NULL, GL_DYNAMIC_DRAW); + glBufferSubData (GL_PIXEL_PACK_BUFFER, 0, buffer_size, task->src_data); + error = glGetError (); + if (error != GL_NO_ERROR) + { + g_warning ("[EGL Thread] Failed to update buffer data: %u", error); + goto out; + } + + if (!task->realize_func (task->realize_user_data)) + goto out; + + success = TRUE; + +out: + glBindBuffer (GL_PIXEL_PACK_BUFFER, 0); + + task->base.callback (success, task->base.callback_user_data); +} + +static void +sync_in_impl (gpointer data, + gpointer user_data) +{ + GrdEglTaskSync *task = user_data; + + task->base.callback (TRUE, task->base.callback_user_data); +} + +static void +run_custom_task_in_impl (gpointer data, + gpointer user_data) +{ + GrdEglTaskCustom *task = user_data; + gboolean success; + + success = task->custom_func (task->user_data); + + if (task->base.callback) + task->base.callback (success, task->base.callback_user_data); +} + +static void +push_replaceable_task (GrdEglThread *egl_thread, + GrdEglThreadSlot slot, + GrdEglTask *task) +{ + GrdReplaceableTask *replaceable_task = NULL; + + g_async_queue_lock (egl_thread->task_queue); + if (!g_hash_table_lookup_extended (egl_thread->slot_table, slot, + NULL, (gpointer *) &replaceable_task)) + g_assert_not_reached (); + + g_assert (replaceable_task); + + replaceable_task_replace_task (replaceable_task, task); + g_async_queue_push_unlocked (egl_thread->task_queue, replaceable_task); + g_async_queue_unlock (egl_thread->task_queue); +} + +void +grd_egl_thread_download (GrdEglThread *egl_thread, + GrdEglThreadSlot slot, + uint8_t *dst_data, + int dst_row_width, + uint32_t format, + unsigned int width, + unsigned int height, + uint32_t n_planes, + const int *fds, + const uint32_t *strides, + const uint32_t *offsets, + const uint64_t *modifiers, + GrdEglThreadCallback callback, + gpointer user_data, + GDestroyNotify destroy) +{ + GrdEglTaskDownload *task; + + task = g_new0 (GrdEglTaskDownload, 1); + + task->dst_data = dst_data; + task->dst_row_width = dst_row_width; + + task->format = format; + task->width = width; + task->height = height; + task->n_planes = n_planes; + task->fds = g_memdup2 (fds, n_planes * sizeof (int)); + task->strides = g_memdup2 (strides, n_planes * sizeof (uint32_t)); + task->offsets = g_memdup2 (offsets, n_planes * sizeof (uint32_t)); + task->modifiers = g_memdup2 (modifiers, n_planes * sizeof (uint64_t)); + + task->base.func = download_in_impl; + task->base.destroy = (GDestroyNotify) grd_egl_task_download_free; + task->base.callback = callback; + task->base.callback_user_data = user_data; + task->base.callback_destroy = destroy; + + if (slot) + push_replaceable_task (egl_thread, slot, &task->base); + else + g_async_queue_push (egl_thread->task_queue, task); + + g_main_context_wakeup (egl_thread->impl.main_context); +} + +void +grd_egl_thread_allocate (GrdEglThread *egl_thread, + uint32_t height, + uint32_t stride, + GrdEglThreadAllocBufferFunc allocate_func, + gpointer allocate_user_data, + GrdEglThreadCallback callback, + gpointer user_data, + GDestroyNotify destroy) +{ + GrdEglTaskAllocateMemory *task; + + task = g_new0 (GrdEglTaskAllocateMemory, 1); + + task->height = height; + task->stride = stride; + task->allocate_func = allocate_func; + task->allocate_user_data = allocate_user_data; + + task->base.func = allocate_in_impl; + task->base.destroy = (GDestroyNotify) grd_egl_task_free; + task->base.callback = callback; + task->base.callback_user_data = user_data; + task->base.callback_destroy = destroy; + + g_async_queue_push (egl_thread->task_queue, task); + g_main_context_wakeup (egl_thread->impl.main_context); +} + +void +grd_egl_thread_deallocate (GrdEglThread *egl_thread, + uint32_t pbo, + GrdEglThreadDeallocBufferFunc deallocate_func, + gpointer deallocate_user_data, + GrdEglThreadCallback callback, + gpointer user_data, + GDestroyNotify destroy) +{ + GrdEglTaskDeallocateMemory *task; + + task = g_new0 (GrdEglTaskDeallocateMemory, 1); + + task->pbo = pbo; + + task->deallocate_func = deallocate_func; + task->deallocate_user_data = deallocate_user_data; + + task->base.func = deallocate_in_impl; + task->base.destroy = (GDestroyNotify) grd_egl_task_free; + task->base.callback = callback; + task->base.callback_user_data = user_data; + task->base.callback_destroy = destroy; + + g_async_queue_push (egl_thread->task_queue, task); + g_main_context_wakeup (egl_thread->impl.main_context); +} + +void +grd_egl_thread_upload (GrdEglThread *egl_thread, + GrdEglThreadSlot slot, + uint32_t pbo, + uint32_t height, + uint32_t stride, + uint8_t *src_data, + GrdEglThreadAllocBufferFunc allocate_func, + gpointer allocate_user_data, + GDestroyNotify allocate_user_data_destroy, + GrdEglThreadCustomFunc realize_func, + gpointer realize_user_data, + GDestroyNotify realize_user_data_destroy, + GrdEglThreadCallback callback, + gpointer user_data, + GDestroyNotify destroy) +{ + GrdEglTaskUpload *task; + + task = g_new0 (GrdEglTaskUpload, 1); + + task->pbo = pbo; + + task->height = height; + task->stride = stride; + task->src_data = src_data; + + task->allocate_func = allocate_func; + task->allocate_user_data = allocate_user_data; + task->allocate_user_data_destroy = allocate_user_data_destroy; + + task->realize_func = realize_func; + task->realize_user_data = realize_user_data; + task->realize_user_data_destroy = realize_user_data_destroy; + + task->base.func = upload_in_impl; + task->base.destroy = (GDestroyNotify) grd_egl_task_upload_free; + task->base.callback = callback; + task->base.callback_user_data = user_data; + task->base.callback_destroy = destroy; + + if (slot) + push_replaceable_task (egl_thread, slot, &task->base); + else + g_async_queue_push (egl_thread->task_queue, task); + + g_main_context_wakeup (egl_thread->impl.main_context); +} + +void +grd_egl_thread_sync (GrdEglThread *egl_thread, + GrdEglThreadCallback callback, + gpointer user_data, + GDestroyNotify destroy) +{ + GrdEglTaskSync *task; + + task = g_new0 (GrdEglTaskSync, 1); + + task->base.func = sync_in_impl; + task->base.destroy = (GDestroyNotify) grd_egl_task_free; + task->base.callback = callback; + task->base.callback_user_data = user_data; + task->base.callback_destroy = destroy; + + g_async_queue_push (egl_thread->task_queue, task); + g_main_context_wakeup (egl_thread->impl.main_context); +} + +void +grd_egl_thread_run_custom_task (GrdEglThread *egl_thread, + GrdEglThreadCustomFunc custom_func, + gpointer custom_func_data, + GrdEglThreadCallback callback, + gpointer user_data, + GDestroyNotify destroy) +{ + GrdEglTaskCustom *task; + + task = g_new0 (GrdEglTaskCustom, 1); + + task->custom_func = custom_func; + task->user_data = custom_func_data; + + task->base.func = run_custom_task_in_impl; + task->base.destroy = (GDestroyNotify) grd_egl_task_free; + task->base.callback = callback; + task->base.callback_user_data = user_data; + task->base.callback_destroy = destroy; + + g_async_queue_push (egl_thread->task_queue, task); + g_main_context_wakeup (egl_thread->impl.main_context); +} + +gboolean +grd_egl_thread_get_modifiers_for_format (GrdEglThread *egl_thread, + uint32_t format, + int *out_n_modifiers, + uint64_t **out_modifiers) +{ + GArray *modifiers; + + modifiers = g_hash_table_lookup (egl_thread->modifiers, + GINT_TO_POINTER (format)); + if (!modifiers) + return FALSE; + + *out_n_modifiers = modifiers->len; + *out_modifiers = g_memdup2 (modifiers->data, + sizeof (uint64_t) * modifiers->len); + return TRUE; +} diff --git a/grd-egl-thread.h b/grd-egl-thread.h new file mode 100644 index 0000000..4767779 --- /dev/null +++ b/grd-egl-thread.h @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2021 Red Hat Inc. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + */ + +#pragma once + +#include +#include + +#include "grd-types.h" + +typedef gpointer GrdEglThreadSlot; + +typedef void (* GrdEglThreadCallback) (gboolean success, + gpointer user_data); +typedef gboolean (* GrdEglThreadCustomFunc) (gpointer user_data); +typedef gboolean (* GrdEglThreadAllocBufferFunc) (gpointer user_data, + uint32_t pbo); +typedef void (* GrdEglThreadDeallocBufferFunc) (gpointer user_data); + +GrdEglThread * grd_egl_thread_new (GError **error); + +void grd_egl_thread_free (GrdEglThread *egl_thread); + +const char * grd_egl_thread_get_drm_render_node (GrdEglThread *egl_thread); + +void * grd_egl_thread_acquire_slot (GrdEglThread *egl_thread); + +void grd_egl_thread_release_slot (GrdEglThread *egl_thread, + GrdEglThreadSlot slot); + +void grd_egl_thread_download (GrdEglThread *egl_thread, + GrdEglThreadSlot slot, + uint8_t *dst_data, + int dst_row_width, + uint32_t format, + unsigned int width, + unsigned int height, + uint32_t n_planes, + const int *fds, + const uint32_t *strides, + const uint32_t *offsets, + const uint64_t *modifiers, + GrdEglThreadCallback callback, + gpointer user_data, + GDestroyNotify destroy); + +void grd_egl_thread_allocate (GrdEglThread *egl_thread, + uint32_t height, + uint32_t stride, + GrdEglThreadAllocBufferFunc allocate_func, + gpointer allocate_user_data, + GrdEglThreadCallback callback, + gpointer user_data, + GDestroyNotify destroy); + +void grd_egl_thread_deallocate (GrdEglThread *egl_thread, + uint32_t pbo, + GrdEglThreadDeallocBufferFunc deallocate_func, + gpointer deallocate_user_data, + GrdEglThreadCallback callback, + gpointer user_data, + GDestroyNotify destroy); + +void grd_egl_thread_upload (GrdEglThread *egl_thread, + GrdEglThreadSlot slot, + uint32_t pbo, + uint32_t height, + uint32_t stride, + uint8_t *src_data, + GrdEglThreadAllocBufferFunc allocate_func, + gpointer allocate_user_data, + GDestroyNotify allocate_user_data_destroy, + GrdEglThreadCustomFunc realize_func, + gpointer realize_user_data, + GDestroyNotify realize_user_data_destroy, + GrdEglThreadCallback callback, + gpointer user_data, + GDestroyNotify destroy); + +void grd_egl_thread_sync (GrdEglThread *egl_thread, + GrdEglThreadCallback callback, + gpointer user_data, + GDestroyNotify destroy); + +void grd_egl_thread_run_custom_task (GrdEglThread *egl_thread, + GrdEglThreadCustomFunc custom_func, + gpointer custom_func_data, + GrdEglThreadCallback callback, + gpointer user_data, + GDestroyNotify destroy); + +gboolean grd_egl_thread_get_modifiers_for_format (GrdEglThread *egl_thread, + uint32_t format, + int *out_n_modifiers, + uint64_t **out_modifiers); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (GrdEglThread, grd_egl_thread_free) diff --git a/grd-enable-service.c b/grd-enable-service.c new file mode 100644 index 0000000..5c98c71 --- /dev/null +++ b/grd-enable-service.c @@ -0,0 +1,261 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "grd-enums.h" + +#define GRD_SYSTEMD_SERVICE "gnome-remote-desktop.service" +#define GRD_SYSTEMD_HEADLESS_SERVICE "gnome-remote-desktop-headless.service" +#define GRD_POLKIT_ACTION "org.gnome.remotedesktop.configure-system-daemon" + +static gboolean +get_runtime_mode (const char *argv, + GrdRuntimeMode *runtime_mode) +{ + if (g_str_equal (argv, "user")) + *runtime_mode = GRD_RUNTIME_MODE_SCREEN_SHARE; + else if (g_str_equal (argv, "headless")) + *runtime_mode = GRD_RUNTIME_MODE_HEADLESS; + else if (g_str_equal (argv, "system")) + *runtime_mode = GRD_RUNTIME_MODE_SYSTEM; + else + return FALSE; + + return TRUE; +} + +static gboolean +check_polkit_permission (const char *action_id, + pid_t pid, + GError **error) +{ + g_autoptr (GPermission) permission = NULL; + g_autoptr (PolkitSubject) subject = NULL; + + subject = polkit_unix_process_new_for_owner (pid, 0, -1); + permission = polkit_permission_new_sync (action_id, subject, NULL, error); + if (!permission) + return FALSE; + + if (!g_permission_get_allowed (permission)) + { + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_PERMISSION_DENIED, + "This program requires callers to have access to the %s " + "polkit action", action_id); + return FALSE; + } + + return TRUE; +} + +static gboolean +validate_caller (int64_t pid) +{ + g_autoptr (GError) error = NULL; + + if (geteuid () != 0) + { + g_printerr ("Enabling system service is not meant to be done directly by users.\n"); + return FALSE; + } + + if (!check_polkit_permission (GRD_POLKIT_ACTION, (pid_t) pid, &error)) + { + g_printerr ("Authorization failed for " GRD_POLKIT_ACTION ": %s\n", + error->message); + return FALSE; + } + + return TRUE; +} + +static void +get_systemd_unit_info (GrdRuntimeMode runtime_mode, + GBusType *bus_type, + const char **unit) +{ + switch (runtime_mode) + { + case GRD_RUNTIME_MODE_HEADLESS: + *bus_type = G_BUS_TYPE_SESSION; + *unit = GRD_SYSTEMD_HEADLESS_SERVICE; + break; + case GRD_RUNTIME_MODE_SYSTEM: + *bus_type = G_BUS_TYPE_SYSTEM; + *unit = GRD_SYSTEMD_SERVICE; + break; + case GRD_RUNTIME_MODE_SCREEN_SHARE: + *bus_type = G_BUS_TYPE_SESSION; + *unit = GRD_SYSTEMD_SERVICE; + break; + default: + g_assert_not_reached (); + } +} + +static gboolean +enable_systemd_unit (GrdRuntimeMode runtime_mode, + GError **error) +{ + g_autoptr (GDBusConnection) connection = NULL; + g_autoptr (GVariant) reply = NULL; + GBusType bus_type; + const char *unit; + + get_systemd_unit_info (runtime_mode, &bus_type, &unit); + + connection = g_bus_get_sync (bus_type, NULL, error); + if (!connection) + return FALSE; + + reply = g_dbus_connection_call_sync (connection, + "org.freedesktop.systemd1", + "/org/freedesktop/systemd1", + "org.freedesktop.systemd1.Manager", + "StartUnit", + g_variant_new ("(ss)", + unit, "replace"), + NULL, + G_DBUS_CALL_FLAGS_NO_AUTO_START, + -1, NULL, error); + if (!reply) + return FALSE; + + reply = g_dbus_connection_call_sync (connection, + "org.freedesktop.systemd1", + "/org/freedesktop/systemd1", + "org.freedesktop.systemd1.Manager", + "EnableUnitFiles", + g_variant_new ("(^asbb)", + (const char*[]) + { unit, NULL }, + FALSE, FALSE), + (GVariantType *) "(ba(sss))", + G_DBUS_CALL_FLAGS_NONE, + -1, + NULL, + error); + + return reply != NULL; +} + +static gboolean +disable_systemd_unit (GrdRuntimeMode runtime_mode, + GError **error) +{ + g_autoptr (GDBusConnection) connection = NULL; + g_autoptr (GVariant) reply = NULL; + GBusType bus_type; + const char *unit; + + get_systemd_unit_info (runtime_mode, &bus_type, &unit); + + connection = g_bus_get_sync (bus_type, NULL, error); + if (!connection) + return FALSE; + + reply = g_dbus_connection_call_sync (connection, + "org.freedesktop.systemd1", + "/org/freedesktop/systemd1", + "org.freedesktop.systemd1.Manager", + "StopUnit", + g_variant_new ("(ss)", + unit, "replace"), + NULL, + G_DBUS_CALL_FLAGS_NO_AUTO_START, + -1, NULL, error); + if (!reply) + return FALSE; + + reply = g_dbus_connection_call_sync (connection, + "org.freedesktop.systemd1", + "/org/freedesktop/systemd1", + "org.freedesktop.systemd1.Manager", + "DisableUnitFiles", + g_variant_new ("(^asb)", + (const char*[]) + { unit, NULL }, + FALSE), + (GVariantType *) "(a(sss))", + G_DBUS_CALL_FLAGS_NONE, + -1, + NULL, + error); + + return reply != NULL; +} + +static void +print_usage (void) +{ + g_printerr ("Usage: %s pid \n", g_get_prgname ()); +} + +int +main (int argc, + char *argv[]) +{ + g_autoptr (GError) error = NULL; + GrdRuntimeMode runtime_mode; + gboolean success; + gboolean enable; + int64_t pid; + + g_set_prgname (argv[0]); + + if (argc != 4) + { + print_usage (); + return EXIT_FAILURE; + } + + success = g_ascii_string_to_signed (argv[1], 10, 1, INT_MAX, &pid, NULL); + if (!success) + { + print_usage (); + return EXIT_FAILURE; + } + + success = get_runtime_mode (argv[2], &runtime_mode); + if (!success) + { + print_usage (); + return EXIT_FAILURE; + } + + enable = g_strcmp0 (argv[3], "true") == 0; + if (!enable && g_strcmp0 (argv[3], "false") != 0) + { + print_usage (); + return EXIT_FAILURE; + } + + if (runtime_mode == GRD_RUNTIME_MODE_SYSTEM) + { + success = validate_caller (pid); + if (!success) + return EXIT_FAILURE; + } + + if (enable) + success = enable_systemd_unit (runtime_mode, &error); + else + success = disable_systemd_unit (runtime_mode, &error); + if (!success) + { + g_printerr ("Failed to %s service: %s\n", + enable ? "enable" : "disable", error->message); + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} diff --git a/grd-encode-context.c b/grd-encode-context.c new file mode 100644 index 0000000..dcea60d --- /dev/null +++ b/grd-encode-context.c @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2024 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-encode-context.h" + +#include + +struct _GrdEncodeContext +{ + cairo_region_t *damage_region; +}; + +cairo_region_t * +grd_encode_context_get_damage_region (GrdEncodeContext *encode_context) +{ + return encode_context->damage_region; +} + +void +grd_encode_context_set_damage_region (GrdEncodeContext *encode_context, + cairo_region_t *damage_region) +{ + encode_context->damage_region = cairo_region_reference (damage_region); +} + +GrdEncodeContext * +grd_encode_context_new (void) +{ + return g_new0 (GrdEncodeContext, 1); +} + +void +grd_encode_context_free (GrdEncodeContext *encode_context) +{ + g_clear_pointer (&encode_context->damage_region, cairo_region_destroy); + + g_free (encode_context); +} diff --git a/grd-encode-context.h b/grd-encode-context.h new file mode 100644 index 0000000..55e8cb1 --- /dev/null +++ b/grd-encode-context.h @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2024 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include + +#include "grd-types.h" + +GrdEncodeContext *grd_encode_context_new (void); + +void grd_encode_context_free (GrdEncodeContext *encode_context); + +cairo_region_t *grd_encode_context_get_damage_region (GrdEncodeContext *encode_context); + +void grd_encode_context_set_damage_region (GrdEncodeContext *encode_context, + cairo_region_t *damage_region); diff --git a/grd-encode-session-ca-sw.c b/grd-encode-session-ca-sw.c new file mode 100644 index 0000000..b1064c2 --- /dev/null +++ b/grd-encode-session-ca-sw.c @@ -0,0 +1,406 @@ +/* + * Copyright (C) 2024 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-encode-session-ca-sw.h" + +#include + +#include "grd-bitstream.h" +#include "grd-encode-context.h" +#include "grd-image-view-rgb.h" +#include "grd-local-buffer.h" +#include "grd-rdp-sw-encoder-ca.h" + +/* + * One surface is needed, when encoding a frame, + * one is needed, when preparing the view, + * one is needed for the submitted pending frame in the renderer, + * and one is needed to prepare a new pending frame before it is submitted to + * the renderer, where it then replaces the old pending frame + * + * In total, this makes four needed source surfaces + */ +#define N_SRC_SURFACES 4 + +/* + * One encode stream is needed, when submitting a frame, + * one is needed, when encoding a frame + * + * In total, this makes two needed encode streams + */ +#define N_ENCODE_STREAMS 2 + +#define INITIAL_STREAM_SIZE 16384 + +typedef struct +{ + GrdImageView *image_view; +} RGBSurface; + +struct _GrdEncodeSessionCaSw +{ + GrdEncodeSession parent; + + GrdRdpSwEncoderCa *encoder_ca; + + uint32_t surface_width; + uint32_t surface_height; + + GHashTable *surfaces; + + GMutex pending_encodes_mutex; + GHashTable *pending_encodes; + + GMutex acquired_encode_streams_mutex; + wStream *encode_streams[N_ENCODE_STREAMS]; + GHashTable *acquired_encode_streams; + + GMutex bitstreams_mutex; + GHashTable *bitstreams; + + gboolean pending_header; +}; + +G_DEFINE_TYPE (GrdEncodeSessionCaSw, grd_encode_session_ca_sw, + GRD_TYPE_ENCODE_SESSION) + +static RGBSurface * +rgb_surface_new (void) +{ + RGBSurface *rgb_surface = NULL; + GrdImageViewRGB *image_view_rgb; + + rgb_surface = g_new0 (RGBSurface, 1); + + image_view_rgb = grd_image_view_rgb_new (); + rgb_surface->image_view = GRD_IMAGE_VIEW (image_view_rgb); + + return rgb_surface; +} + +static void +rgb_surface_free (RGBSurface *rgb_surface) +{ + g_clear_object (&rgb_surface->image_view); + + g_free (rgb_surface); +} + +static void +grd_encode_session_ca_sw_get_surface_size (GrdEncodeSession *encode_session, + uint32_t *surface_width, + uint32_t *surface_height) +{ + g_assert_not_reached (); +} + +static GList * +grd_encode_session_ca_sw_get_image_views (GrdEncodeSession *encode_session) +{ + GrdEncodeSessionCaSw *encode_session_ca = + GRD_ENCODE_SESSION_CA_SW (encode_session); + + return g_hash_table_get_keys (encode_session_ca->surfaces); +} + +static gboolean +grd_encode_session_ca_sw_has_pending_frames (GrdEncodeSession *encode_session) +{ + GrdEncodeSessionCaSw *encode_session_ca = + GRD_ENCODE_SESSION_CA_SW (encode_session); + + g_mutex_lock (&encode_session_ca->pending_encodes_mutex); + g_assert (g_hash_table_size (encode_session_ca->pending_encodes) == 0); + g_mutex_unlock (&encode_session_ca->pending_encodes_mutex); + + return FALSE; +} + +static gboolean +grd_encode_session_ca_sw_encode_frame (GrdEncodeSession *encode_session, + GrdEncodeContext *encode_context, + GrdImageView *image_view, + GError **error) +{ + GrdEncodeSessionCaSw *encode_session_ca = + GRD_ENCODE_SESSION_CA_SW (encode_session); + g_autoptr (GMutexLocker) locker = NULL; + + locker = g_mutex_locker_new (&encode_session_ca->pending_encodes_mutex); + g_assert (!g_hash_table_contains (encode_session_ca->pending_encodes, + image_view)); + + g_hash_table_insert (encode_session_ca->pending_encodes, + image_view, encode_context); + + return TRUE; +} + +static wStream * +acquire_encode_stream (GrdEncodeSessionCaSw *encode_session_ca) +{ + g_autoptr (GMutexLocker) locker = NULL; + uint32_t i; + + locker = g_mutex_locker_new (&encode_session_ca->acquired_encode_streams_mutex); + for (i = 0; i < N_ENCODE_STREAMS; ++i) + { + wStream *encode_stream = encode_session_ca->encode_streams[i]; + + if (g_hash_table_contains (encode_session_ca->acquired_encode_streams, + encode_stream)) + continue; + + g_hash_table_add (encode_session_ca->acquired_encode_streams, + encode_stream); + return encode_stream; + } + + g_assert_not_reached (); + return NULL; +} + +static void +release_encode_stream (GrdEncodeSessionCaSw *encode_session_ca, + wStream *encode_stream) +{ + g_autoptr (GMutexLocker) locker = NULL; + + locker = g_mutex_locker_new (&encode_session_ca->acquired_encode_streams_mutex); + if (!g_hash_table_remove (encode_session_ca->acquired_encode_streams, + encode_stream)) + g_assert_not_reached (); +} + +static GrdBitstream * +grd_encode_session_ca_sw_lock_bitstream (GrdEncodeSession *encode_session, + GrdImageView *image_view, + GError **error) +{ + GrdEncodeSessionCaSw *encode_session_ca = + GRD_ENCODE_SESSION_CA_SW (encode_session); + GrdImageViewRGB *image_view_rgb = GRD_IMAGE_VIEW_RGB (image_view); + GrdEncodeContext *encode_context = NULL; + g_autoptr (GMutexLocker) locker = NULL; + GrdLocalBuffer *local_buffer; + uint8_t *buffer; + uint32_t buffer_stride; + cairo_region_t *damage_region; + wStream *encode_stream; + GrdBitstream *bitstream; + + locker = g_mutex_locker_new (&encode_session_ca->pending_encodes_mutex); + if (!g_hash_table_lookup_extended (encode_session_ca->pending_encodes, + image_view, + NULL, (gpointer *) &encode_context)) + g_assert_not_reached (); + + g_clear_pointer (&locker, g_mutex_locker_free); + g_assert (encode_context); + + local_buffer = grd_image_view_rgb_get_local_buffer (image_view_rgb); + buffer = grd_local_buffer_get_buffer (local_buffer); + buffer_stride = grd_local_buffer_get_buffer_stride (local_buffer); + damage_region = grd_encode_context_get_damage_region (encode_context); + encode_stream = acquire_encode_stream (encode_session_ca); + + grd_rdp_sw_encoder_ca_encode_progressive_frame (encode_session_ca->encoder_ca, + encode_session_ca->surface_width, + encode_session_ca->surface_height, + buffer, + buffer_stride, + damage_region, + encode_stream, + encode_session_ca->pending_header); + + encode_session_ca->pending_header = FALSE; + + bitstream = grd_bitstream_new (Stream_Buffer (encode_stream), + Stream_Length (encode_stream)); + + g_mutex_lock (&encode_session_ca->bitstreams_mutex); + g_hash_table_insert (encode_session_ca->bitstreams, bitstream, encode_stream); + g_mutex_unlock (&encode_session_ca->bitstreams_mutex); + + locker = g_mutex_locker_new (&encode_session_ca->pending_encodes_mutex); + if (!g_hash_table_remove (encode_session_ca->pending_encodes, image_view)) + g_assert_not_reached (); + + return bitstream; +} + +static gboolean +grd_encode_session_ca_sw_unlock_bitstream (GrdEncodeSession *encode_session, + GrdBitstream *bitstream, + GError **error) +{ + GrdEncodeSessionCaSw *encode_session_ca = + GRD_ENCODE_SESSION_CA_SW (encode_session); + wStream *encode_stream = NULL; + g_autoptr (GMutexLocker) locker = NULL; + + locker = g_mutex_locker_new (&encode_session_ca->bitstreams_mutex); + if (!g_hash_table_steal_extended (encode_session_ca->bitstreams, + bitstream, + NULL, (gpointer *) &encode_stream)) + g_assert_not_reached (); + + g_clear_pointer (&locker, g_mutex_locker_free); + g_assert (encode_stream); + + grd_bitstream_free (bitstream); + release_encode_stream (encode_session_ca, encode_stream); + + return TRUE; +} + +static gboolean +init_surfaces_and_streams (GrdEncodeSessionCaSw *encode_session_ca, + GError **error) +{ + uint32_t i; + + for (i = 0; i < N_SRC_SURFACES; ++i) + { + RGBSurface *surface; + + surface = rgb_surface_new (); + + g_hash_table_insert (encode_session_ca->surfaces, + surface->image_view, surface); + } + + for (i = 0; i < N_ENCODE_STREAMS; ++i) + { + wStream *encode_stream; + + encode_stream = Stream_New (NULL, INITIAL_STREAM_SIZE); + if (!encode_stream) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create encode stream"); + return FALSE; + } + encode_session_ca->encode_streams[i] = encode_stream; + } + + return TRUE; +} + +GrdEncodeSessionCaSw * +grd_encode_session_ca_sw_new (GrdRdpSwEncoderCa *encoder_ca, + uint32_t source_width, + uint32_t source_height, + GError **error) +{ + g_autoptr (GrdEncodeSessionCaSw) encode_session_ca = NULL; + + encode_session_ca = g_object_new (GRD_TYPE_ENCODE_SESSION_CA_SW, NULL); + encode_session_ca->encoder_ca = encoder_ca; + encode_session_ca->surface_width = source_width; + encode_session_ca->surface_height = source_height; + + if (!init_surfaces_and_streams (encode_session_ca, error)) + return NULL; + + return g_steal_pointer (&encode_session_ca); +} + +static void +encode_stream_free (wStream *encode_stream) +{ + Stream_Free (encode_stream, TRUE); +} + +static void +grd_encode_session_ca_sw_dispose (GObject *object) +{ + GrdEncodeSessionCaSw *encode_session_ca = GRD_ENCODE_SESSION_CA_SW (object); + uint32_t i; + + g_assert (g_hash_table_size (encode_session_ca->pending_encodes) == 0); + g_assert (g_hash_table_size (encode_session_ca->acquired_encode_streams) == 0); + g_assert (g_hash_table_size (encode_session_ca->bitstreams) == 0); + + for (i = 0; i < N_ENCODE_STREAMS; ++i) + g_clear_pointer (&encode_session_ca->encode_streams[i], encode_stream_free); + + g_clear_pointer (&encode_session_ca->surfaces, g_hash_table_unref); + + G_OBJECT_CLASS (grd_encode_session_ca_sw_parent_class)->dispose (object); +} + +static void +grd_encode_session_ca_sw_finalize (GObject *object) +{ + GrdEncodeSessionCaSw *encode_session_ca = GRD_ENCODE_SESSION_CA_SW (object); + + g_mutex_clear (&encode_session_ca->bitstreams_mutex); + g_mutex_clear (&encode_session_ca->acquired_encode_streams_mutex); + g_mutex_clear (&encode_session_ca->pending_encodes_mutex); + + g_clear_pointer (&encode_session_ca->bitstreams, g_hash_table_unref); + g_clear_pointer (&encode_session_ca->acquired_encode_streams, g_hash_table_unref); + g_clear_pointer (&encode_session_ca->pending_encodes, g_hash_table_unref); + + G_OBJECT_CLASS (grd_encode_session_ca_sw_parent_class)->finalize (object); +} + +static void +grd_encode_session_ca_sw_init (GrdEncodeSessionCaSw *encode_session_ca) +{ + encode_session_ca->pending_header = TRUE; + + encode_session_ca->surfaces = + g_hash_table_new_full (NULL, NULL, + NULL, (GDestroyNotify) rgb_surface_free); + encode_session_ca->pending_encodes = g_hash_table_new (NULL, NULL); + encode_session_ca->acquired_encode_streams = g_hash_table_new (NULL, NULL); + encode_session_ca->bitstreams = g_hash_table_new (NULL, NULL); + + g_mutex_init (&encode_session_ca->pending_encodes_mutex); + g_mutex_init (&encode_session_ca->acquired_encode_streams_mutex); + g_mutex_init (&encode_session_ca->bitstreams_mutex); +} + +static void +grd_encode_session_ca_sw_class_init (GrdEncodeSessionCaSwClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GrdEncodeSessionClass *encode_session_class = + GRD_ENCODE_SESSION_CLASS (klass); + + object_class->dispose = grd_encode_session_ca_sw_dispose; + object_class->finalize = grd_encode_session_ca_sw_finalize; + + encode_session_class->get_surface_size = + grd_encode_session_ca_sw_get_surface_size; + encode_session_class->get_image_views = + grd_encode_session_ca_sw_get_image_views; + encode_session_class->has_pending_frames = + grd_encode_session_ca_sw_has_pending_frames; + encode_session_class->encode_frame = + grd_encode_session_ca_sw_encode_frame; + encode_session_class->lock_bitstream = + grd_encode_session_ca_sw_lock_bitstream; + encode_session_class->unlock_bitstream = + grd_encode_session_ca_sw_unlock_bitstream; +} diff --git a/grd-encode-session-ca-sw.h b/grd-encode-session-ca-sw.h new file mode 100644 index 0000000..5fef0e4 --- /dev/null +++ b/grd-encode-session-ca-sw.h @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2024 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include "grd-encode-session.h" +#include "grd-types.h" + +#define GRD_TYPE_ENCODE_SESSION_CA_SW (grd_encode_session_ca_sw_get_type ()) +G_DECLARE_FINAL_TYPE (GrdEncodeSessionCaSw, grd_encode_session_ca_sw, + GRD, ENCODE_SESSION_CA_SW, GrdEncodeSession) + +GrdEncodeSessionCaSw *grd_encode_session_ca_sw_new (GrdRdpSwEncoderCa *encoder_ca, + uint32_t source_width, + uint32_t source_height, + GError **error); diff --git a/grd-encode-session-vaapi.c b/grd-encode-session-vaapi.c new file mode 100644 index 0000000..0f4d4f3 --- /dev/null +++ b/grd-encode-session-vaapi.c @@ -0,0 +1,1790 @@ +/* + * Copyright (C) 2024 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-encode-session-vaapi.h" + +#include +#include +#include + +#include "grd-avc-frame-info.h" +#include "grd-bitstream.h" +#include "grd-debug.h" +#include "grd-image-view-nv12.h" +#include "grd-nal-writer.h" +#include "grd-utils.h" +#include "grd-vk-image.h" +#include "grd-vk-utils.h" + +/* + * See also Table 7-6 – Name association to slice_type + * (Rec. ITU-T H.264 (08/2021)) + */ +#define H264_SLICE_TYPE_P 0 +#define H264_SLICE_TYPE_I 2 + +/* See also E.2.1 VUI parameters semantics (Rec. ITU-T H.264 (08/2021)) */ +#define H264_Extended_SAR 255 + +#define LOG2_MAX_FRAME_NUM 8 + +/* + * One surface is needed, when encoding a frame, + * one is needed, when preparing the view, + * one is needed for the submitted pending frame in the renderer, + * and one is needed to prepare a new pending frame before it is submitted to + * the renderer, where it then replaces the old pending frame + * + * In total, this makes four needed source surfaces per view + */ +#define N_SRC_SURFACES_PER_VIEW 4 +#define N_SRC_SURFACES (N_SRC_SURFACES_PER_VIEW * 2) + +typedef struct +{ + GrdEncodeSessionVaapi *encode_session_vaapi; + + VASurfaceID va_surface; + + int32_t frame_num; + gboolean is_idr; +} VAAPIPicture; + +typedef struct +{ + GrdEncodeSessionVaapi *encode_session_vaapi; + + VAAPIPicture *reconstructed_picture; + + VABufferID sequence_buffer; + VABufferID picture_buffer; + VABufferID slice_buffer; + + GList *packed_header_buffers; + GList *misc_buffers; + + uint32_t aud_header_length; + uint32_t sequence_header_length; + uint32_t picture_header_length; + uint32_t slice_header_length; + + VASurfaceID src_surface; + VABufferID bitstream_buffer; + + GrdAVCFrameInfo *avc_frame_info; + + int64_t timestamps[3]; +} H264Frame; + +typedef struct +{ + GrdEncodeSessionVaapi *encode_session_vaapi; + + VASurfaceID va_surface; + GrdImageView *image_view; + + int fds[2]; +} NV12VkVASurface; + +struct _GrdEncodeSessionVaapi +{ + GrdEncodeSession parent; + + GrdVkDevice *vk_device; + VADisplay va_display; + + gboolean supports_quality_level; + gboolean debug_va_times; + + VAConfigID va_config; + VAContextID va_context; + + uint32_t surface_width; + uint32_t surface_height; + uint32_t refresh_rate; + uint8_t level_idc; + + gboolean pending_idr_frame; + + GrdNalWriter *nal_writer; + GHashTable *surfaces; + + VAAPIPicture *reference_picture; + uint16_t frame_num; + + VAEncPictureParameterBufferH264 picture_param; + VAEncSequenceParameterBufferH264 sequence_param; + + GMutex pending_frames_mutex; + GHashTable *pending_frames; + + GMutex bitstreams_mutex; + GHashTable *bitstreams; +}; + +G_DEFINE_TYPE (GrdEncodeSessionVaapi, grd_encode_session_vaapi, + GRD_TYPE_ENCODE_SESSION) + +static void +vaapi_picture_free (VAAPIPicture *picture); + +static void +h264_frame_free (H264Frame *h264_frame); + +static void +nv12_vkva_surface_free (NV12VkVASurface *vkva_surface); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (VAAPIPicture, vaapi_picture_free) +G_DEFINE_AUTOPTR_CLEANUP_FUNC (H264Frame, h264_frame_free) +G_DEFINE_AUTOPTR_CLEANUP_FUNC (NV12VkVASurface, nv12_vkva_surface_free) + +static VAAPIPicture * +vaapi_picture_new (GrdEncodeSessionVaapi *encode_session_vaapi, + uint32_t surface_width, + uint32_t surface_height, + int32_t frame_num, + gboolean is_idr, + GError **error) +{ + g_autoptr (VAAPIPicture) picture = NULL; + VAStatus va_status; + + picture = g_new0 (VAAPIPicture, 1); + picture->encode_session_vaapi = encode_session_vaapi; + picture->va_surface = VA_INVALID_SURFACE; + picture->frame_num = frame_num; + picture->is_idr = is_idr; + + va_status = vaCreateSurfaces (encode_session_vaapi->va_display, + VA_RT_FORMAT_YUV420, + surface_width, surface_height, + &picture->va_surface, 1, + NULL, 0); + if (va_status != VA_STATUS_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create surface for reconstructed picture: %s", + vaErrorStr (va_status)); + return NULL; + } + g_assert (picture->va_surface != VA_INVALID_SURFACE); + + return g_steal_pointer (&picture); +} + +static void +vaapi_picture_free (VAAPIPicture *picture) +{ + GrdEncodeSessionVaapi *encode_session_vaapi = picture->encode_session_vaapi; + VADisplay va_display = encode_session_vaapi->va_display; + + if (picture->va_surface != VA_INVALID_SURFACE) + vaDestroySurfaces (va_display, &picture->va_surface, 1); + + g_free (picture); +} + +static H264Frame * +h264_frame_new (GrdEncodeSessionVaapi *encode_session_vaapi, + GError **error) +{ + g_autoptr (H264Frame) h264_frame = NULL; + uint32_t surface_width = encode_session_vaapi->surface_width; + uint32_t surface_height = encode_session_vaapi->surface_height; + + g_assert (surface_width % 16 == 0); + g_assert (surface_height % 16 == 0); + g_assert (surface_width >= 16); + g_assert (surface_height >= 16); + + h264_frame = g_new0 (H264Frame, 1); + h264_frame->encode_session_vaapi = encode_session_vaapi; + h264_frame->sequence_buffer = VA_INVALID_ID; + h264_frame->picture_buffer = VA_INVALID_ID; + h264_frame->slice_buffer = VA_INVALID_ID; + h264_frame->bitstream_buffer = VA_INVALID_ID; + + h264_frame->reconstructed_picture = + vaapi_picture_new (encode_session_vaapi, + surface_width, surface_height, + encode_session_vaapi->frame_num, + encode_session_vaapi->pending_idr_frame, + error); + if (!h264_frame->reconstructed_picture) + return NULL; + + return g_steal_pointer (&h264_frame); +} + +static void +h264_frame_free (H264Frame *h264_frame) +{ + GrdEncodeSessionVaapi *encode_session_vaapi = + h264_frame->encode_session_vaapi; + VADisplay va_display = encode_session_vaapi->va_display; + GList *l; + + for (l = h264_frame->misc_buffers; l; l = l->next) + vaDestroyBuffer (va_display, GPOINTER_TO_UINT (l->data)); + g_clear_pointer (&h264_frame->misc_buffers, g_list_free); + + for (l = h264_frame->packed_header_buffers; l; l = l->next) + vaDestroyBuffer (va_display, GPOINTER_TO_UINT (l->data)); + g_clear_pointer (&h264_frame->packed_header_buffers, g_list_free); + + if (h264_frame->bitstream_buffer != VA_INVALID_ID) + vaDestroyBuffer (va_display, h264_frame->bitstream_buffer); + if (h264_frame->slice_buffer != VA_INVALID_ID) + vaDestroyBuffer (va_display, h264_frame->slice_buffer); + if (h264_frame->picture_buffer != VA_INVALID_ID) + vaDestroyBuffer (va_display, h264_frame->picture_buffer); + if (h264_frame->sequence_buffer != VA_INVALID_ID) + vaDestroyBuffer (va_display, h264_frame->sequence_buffer); + + g_clear_pointer (&h264_frame->avc_frame_info, g_free); + g_clear_pointer (&h264_frame->reconstructed_picture, vaapi_picture_free); + + g_free (h264_frame); +} + +static VABufferID * +h264_frame_get_buffers (H264Frame *h264_frame, + uint32_t *n_buffers) +{ + VABufferID *va_buffers; + uint32_t i = 0; + GList *l; + + *n_buffers = 0; + + if (h264_frame->sequence_buffer != VA_INVALID_ID) + *n_buffers += 1; + + g_assert (h264_frame->picture_buffer != VA_INVALID_ID); + g_assert (h264_frame->slice_buffer != VA_INVALID_ID); + *n_buffers += 2; + + *n_buffers += g_list_length (h264_frame->packed_header_buffers); + *n_buffers += g_list_length (h264_frame->misc_buffers); + + g_assert (*n_buffers > 0); + va_buffers = g_new0 (VABufferID, *n_buffers); + + if (h264_frame->sequence_buffer != VA_INVALID_ID) + va_buffers[i++] = h264_frame->sequence_buffer; + + va_buffers[i++] = h264_frame->picture_buffer; + + for (l = h264_frame->packed_header_buffers; l; l = l->next) + { + g_assert (i < *n_buffers); + + va_buffers[i++] = GPOINTER_TO_UINT (l->data); + } + for (l = h264_frame->misc_buffers; l; l = l->next) + { + g_assert (i < *n_buffers); + + va_buffers[i++] = GPOINTER_TO_UINT (l->data); + } + va_buffers[i++] = h264_frame->slice_buffer; + g_assert (i == *n_buffers); + + return va_buffers; +} + +static NV12VkVASurface * +nv12_vkva_surface_new (GrdEncodeSessionVaapi *encode_session_vaapi, + uint32_t surface_width, + uint32_t surface_height, + GError **error) +{ + g_autoptr (NV12VkVASurface) vkva_surface = NULL; + VASurfaceAttrib surface_attributes[2] = {}; + VADRMPRIMESurfaceDescriptor prime_descriptor = {}; + g_autoptr (GrdVkImage) vk_y_layer = NULL; + g_autoptr (GrdVkImage) vk_uv_layer = NULL; + GrdImageViewNV12 *image_view_nv12; + uint32_t object_idx; + int fd; + uint64_t drm_format_modifier; + VAStatus va_status; + + g_assert (surface_width % 16 == 0); + g_assert (surface_height % 16 == 0); + g_assert (surface_width >= 16); + g_assert (surface_height >= 16); + + vkva_surface = g_new0 (NV12VkVASurface, 1); + vkva_surface->encode_session_vaapi = encode_session_vaapi; + vkva_surface->va_surface = VA_INVALID_SURFACE; + vkva_surface->fds[0] = -1; + vkva_surface->fds[1] = -1; + + surface_attributes[0].type = VASurfaceAttribPixelFormat; + surface_attributes[0].flags = VA_SURFACE_ATTRIB_SETTABLE; + surface_attributes[0].value.type = VAGenericValueTypeInteger; + surface_attributes[0].value.value.i = VA_FOURCC_NV12; + + surface_attributes[1].type = VASurfaceAttribUsageHint; + surface_attributes[1].flags = VA_SURFACE_ATTRIB_SETTABLE; + surface_attributes[1].value.type = VAGenericValueTypeInteger; + surface_attributes[1].value.value.i = VA_SURFACE_ATTRIB_USAGE_HINT_EXPORT; + + va_status = vaCreateSurfaces (encode_session_vaapi->va_display, + VA_RT_FORMAT_YUV420, + surface_width, surface_height, + &vkva_surface->va_surface, 1, + surface_attributes, + G_N_ELEMENTS (surface_attributes)); + if (va_status != VA_STATUS_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create NV12 surface: %s", vaErrorStr (va_status)); + return NULL; + } + g_assert (vkva_surface->va_surface != VA_INVALID_SURFACE); + + va_status = vaExportSurfaceHandle (encode_session_vaapi->va_display, + vkva_surface->va_surface, + VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2, + VA_EXPORT_SURFACE_WRITE_ONLY | + VA_EXPORT_SURFACE_SEPARATE_LAYERS, + &prime_descriptor); + if (va_status != VA_STATUS_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to export surface handle: %s", + vaErrorStr (va_status)); + return NULL; + } + g_assert (prime_descriptor.num_layers == 2); + g_assert (prime_descriptor.layers[0].num_planes == 1); + g_assert (prime_descriptor.layers[1].num_planes == 1); + + g_assert (prime_descriptor.num_objects > 0); + g_assert (prime_descriptor.num_objects <= 2); + g_assert (prime_descriptor.objects[0].fd != -1); + if (prime_descriptor.num_objects > 1) + g_assert (prime_descriptor.objects[1].fd != -1); + + vkva_surface->fds[0] = prime_descriptor.objects[0].fd; + if (prime_descriptor.num_objects > 1) + vkva_surface->fds[1] = prime_descriptor.objects[1].fd; + + object_idx = prime_descriptor.layers[0].object_index[0]; + fd = prime_descriptor.objects[object_idx].fd; + drm_format_modifier = + prime_descriptor.objects[object_idx].drm_format_modifier; + + vk_y_layer = + grd_vk_dma_buf_image_new (encode_session_vaapi->vk_device, + VK_FORMAT_R8_UNORM, + surface_width, surface_height, + VK_IMAGE_USAGE_STORAGE_BIT, fd, + prime_descriptor.layers[0].offset[0], + prime_descriptor.layers[0].pitch[0], + drm_format_modifier, error); + if (!vk_y_layer) + return NULL; + + object_idx = prime_descriptor.layers[1].object_index[0]; + fd = prime_descriptor.objects[object_idx].fd; + drm_format_modifier = + prime_descriptor.objects[object_idx].drm_format_modifier; + + vk_uv_layer = + grd_vk_dma_buf_image_new (encode_session_vaapi->vk_device, + VK_FORMAT_R8G8_UNORM, + surface_width / 2, surface_height / 2, + VK_IMAGE_USAGE_STORAGE_BIT, fd, + prime_descriptor.layers[1].offset[0], + prime_descriptor.layers[1].pitch[0], + drm_format_modifier, error); + if (!vk_uv_layer) + return NULL; + + image_view_nv12 = + grd_image_view_nv12_new (g_steal_pointer (&vk_y_layer), + g_steal_pointer (&vk_uv_layer)); + vkva_surface->image_view = GRD_IMAGE_VIEW (image_view_nv12); + + g_clear_fd (&vkva_surface->fds[1], NULL); + g_clear_fd (&vkva_surface->fds[0], NULL); + + return g_steal_pointer (&vkva_surface); +} + +static void +nv12_vkva_surface_free (NV12VkVASurface *vkva_surface) +{ + GrdEncodeSessionVaapi *encode_session_vaapi = + vkva_surface->encode_session_vaapi; + VADisplay va_display = encode_session_vaapi->va_display; + + g_clear_object (&vkva_surface->image_view); + + g_clear_fd (&vkva_surface->fds[1], NULL); + g_clear_fd (&vkva_surface->fds[0], NULL); + + if (vkva_surface->va_surface != VA_INVALID_SURFACE) + vaDestroySurfaces (va_display, &vkva_surface->va_surface, 1); + + g_free (vkva_surface); +} + +static void +grd_encode_session_vaapi_get_surface_size (GrdEncodeSession *encode_session, + uint32_t *surface_width, + uint32_t *surface_height) +{ + GrdEncodeSessionVaapi *encode_session_vaapi = + GRD_ENCODE_SESSION_VAAPI (encode_session); + + g_assert (encode_session_vaapi->surface_width % 16 == 0); + g_assert (encode_session_vaapi->surface_height % 16 == 0); + g_assert (encode_session_vaapi->surface_width >= 16); + g_assert (encode_session_vaapi->surface_height >= 16); + + *surface_width = encode_session_vaapi->surface_width; + *surface_height = encode_session_vaapi->surface_height; +} + +static GList * +grd_encode_session_vaapi_get_image_views (GrdEncodeSession *encode_session) +{ + GrdEncodeSessionVaapi *encode_session_vaapi = + GRD_ENCODE_SESSION_VAAPI (encode_session); + + return g_hash_table_get_keys (encode_session_vaapi->surfaces); +} + +static gboolean +grd_encode_session_vaapi_has_pending_frames (GrdEncodeSession *encode_session) +{ + GrdEncodeSessionVaapi *encode_session_vaapi = + GRD_ENCODE_SESSION_VAAPI (encode_session); + g_autoptr (GMutexLocker) locker = NULL; + + locker = g_mutex_locker_new (&encode_session_vaapi->pending_frames_mutex); + return g_hash_table_size (encode_session_vaapi->pending_frames) > 0; +} + +static gboolean +create_packed_header_buffers (GrdEncodeSessionVaapi *encode_session_vaapi, + H264Frame *h264_frame, + VAEncPackedHeaderType header_type, + uint8_t *header_data, + uint32_t header_length, + GError **error) +{ + VAEncPackedHeaderParameterBuffer packed_header_param = {}; + VABufferID packed_header_buffer = VA_INVALID_ID; + VABufferID packed_header_data_buffer = VA_INVALID_ID; + VAStatus va_status; + + packed_header_param.type = header_type; + packed_header_param.bit_length = header_length; + packed_header_param.has_emulation_bytes = 0; + + va_status = vaCreateBuffer (encode_session_vaapi->va_display, + encode_session_vaapi->va_context, + VAEncPackedHeaderParameterBufferType, + sizeof (VAEncPackedHeaderParameterBuffer), 1, + &packed_header_param, + &packed_header_buffer); + if (va_status != VA_STATUS_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create packed header parameter buffer: %s", + vaErrorStr (va_status)); + return FALSE; + } + g_assert (packed_header_buffer != VA_INVALID_ID); + + h264_frame->packed_header_buffers = + g_list_append (h264_frame->packed_header_buffers, + GUINT_TO_POINTER (packed_header_buffer)); + + va_status = vaCreateBuffer (encode_session_vaapi->va_display, + encode_session_vaapi->va_context, + VAEncPackedHeaderDataBufferType, + (header_length + 7) / 8, 1, + header_data, + &packed_header_data_buffer); + if (va_status != VA_STATUS_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create packed header data buffer: %s", + vaErrorStr (va_status)); + return FALSE; + } + g_assert (packed_header_data_buffer != VA_INVALID_ID); + + h264_frame->packed_header_buffers = + g_list_append (h264_frame->packed_header_buffers, + GUINT_TO_POINTER (packed_header_data_buffer)); + + return TRUE; +} + +static gboolean +ensure_access_unit_delimiter (GrdEncodeSessionVaapi *encode_session_vaapi, + H264Frame *h264_frame, + GError **error) +{ + GrdNalWriter *nal_writer = encode_session_vaapi->nal_writer; + g_autofree uint8_t *header_data = NULL; + uint32_t header_length = 0; + + header_data = grd_nal_writer_get_aud_bitstream (nal_writer, &header_length); + if (!create_packed_header_buffers (encode_session_vaapi, h264_frame, + VAEncPackedHeaderRawData, + header_data, header_length, error)) + return FALSE; + + h264_frame->aud_header_length = header_length; + + return TRUE; +} + +static void +fill_avc_sequence (GrdEncodeSessionVaapi *encode_session_vaapi, + VAEncSequenceParameterBufferH264 *sequence_param) +{ + uint32_t surface_width = encode_session_vaapi->surface_width; + uint32_t surface_height = encode_session_vaapi->surface_height; + + g_assert (surface_width % 16 == 0); + g_assert (surface_height % 16 == 0); + g_assert (surface_width >= 16); + g_assert (surface_height >= 16); + + sequence_param->seq_parameter_set_id = 0; + sequence_param->level_idc = encode_session_vaapi->level_idc; + sequence_param->intra_period = 0; + sequence_param->intra_idr_period = sequence_param->intra_period; + sequence_param->ip_period = 1; /* 1 + B-Frames */ + + sequence_param->bits_per_second = 0; + sequence_param->max_num_ref_frames = 1; + sequence_param->picture_width_in_mbs = surface_width / 16; + sequence_param->picture_height_in_mbs = surface_height / 16; + + sequence_param->seq_fields.value = 0; + sequence_param->seq_fields.bits.chroma_format_idc = 1; + sequence_param->seq_fields.bits.frame_mbs_only_flag = 1; + sequence_param->seq_fields.bits.direct_8x8_inference_flag = 1; + sequence_param->seq_fields.bits.log2_max_frame_num_minus4 = + LOG2_MAX_FRAME_NUM - 4; + sequence_param->seq_fields.bits.pic_order_cnt_type = 2; + + sequence_param->vui_parameters_present_flag = 1; + sequence_param->vui_fields.value = 0; + sequence_param->vui_fields.bits.aspect_ratio_info_present_flag = 1; + sequence_param->vui_fields.bits.timing_info_present_flag = 1; + sequence_param->vui_fields.bits.bitstream_restriction_flag = 1; + sequence_param->vui_fields.bits.log2_max_mv_length_horizontal = 15; + sequence_param->vui_fields.bits.log2_max_mv_length_vertical = 15; + sequence_param->aspect_ratio_idc = H264_Extended_SAR; + sequence_param->sar_width = 1; + sequence_param->sar_height = 1; + sequence_param->num_units_in_tick = 1 * 1000; + sequence_param->time_scale = 2 * encode_session_vaapi->refresh_rate * 1000; +} + +static gboolean +create_sequence_buffer (GrdEncodeSessionVaapi *encode_session_vaapi, + H264Frame *h264_frame, + GError **error) +{ + VAEncSequenceParameterBufferH264 *sequence_param = + &encode_session_vaapi->sequence_param; + VAStatus va_status; + + va_status = vaCreateBuffer (encode_session_vaapi->va_display, + encode_session_vaapi->va_context, + VAEncSequenceParameterBufferType, + sizeof (VAEncSequenceParameterBufferH264), 1, + sequence_param, + &h264_frame->sequence_buffer); + if (va_status != VA_STATUS_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create sequence parameter buffer: %s", + vaErrorStr (va_status)); + return FALSE; + } + g_assert (h264_frame->sequence_buffer != VA_INVALID_ID); + + return TRUE; +} + +static gboolean +maybe_ensure_sequence (GrdEncodeSessionVaapi *encode_session_vaapi, + H264Frame *h264_frame, + GError **error) +{ + GrdNalWriter *nal_writer = encode_session_vaapi->nal_writer; + VAAPIPicture *picture = h264_frame->reconstructed_picture; + VAEncSequenceParameterBufferH264 *sequence_param = + &encode_session_vaapi->sequence_param; + g_autofree uint8_t *header_data = NULL; + uint32_t header_length = 0; + + if (!picture->is_idr) + return TRUE; + + *sequence_param = (VAEncSequenceParameterBufferH264) {}; + fill_avc_sequence (encode_session_vaapi, sequence_param); + + if (!create_sequence_buffer (encode_session_vaapi, h264_frame, error)) + return FALSE; + + header_data = grd_nal_writer_get_sps_bitstream (nal_writer, sequence_param, + &header_length); + if (!create_packed_header_buffers (encode_session_vaapi, h264_frame, + VAEncPackedHeaderSequence, + header_data, header_length, error)) + return FALSE; + + h264_frame->sequence_header_length = header_length; + + return TRUE; +} + +static gboolean +ensure_misc_param_quality_level (GrdEncodeSessionVaapi *encode_session_vaapi, + H264Frame *h264_frame, + GError **error) +{ + VAEncMiscParameterBuffer *misc_param = NULL; + VAEncMiscParameterBufferQualityLevel *misc_param_quality_level; + VABufferID misc_param_buffer = VA_INVALID_ID; + uint32_t total_size; + VAStatus va_status; + + total_size = sizeof (VAEncMiscParameterBuffer) + + sizeof (VAEncMiscParameterBufferQualityLevel); + + va_status = vaCreateBuffer (encode_session_vaapi->va_display, + encode_session_vaapi->va_context, + VAEncMiscParameterBufferType, + total_size, 1, + NULL, + &misc_param_buffer); + if (va_status != VA_STATUS_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create misc parameter buffer: %s", + vaErrorStr (va_status)); + return FALSE; + } + g_assert (misc_param_buffer != VA_INVALID_ID); + + va_status = vaMapBuffer (encode_session_vaapi->va_display, + misc_param_buffer, + (void **) &misc_param); + if (va_status != VA_STATUS_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to map misc parameter buffer: %s", + vaErrorStr (va_status)); + return FALSE; + } + g_assert (misc_param); + + misc_param->type = VAEncMiscParameterTypeQualityLevel; + + misc_param_quality_level = + (VAEncMiscParameterBufferQualityLevel *) misc_param->data; + *misc_param_quality_level = (VAEncMiscParameterBufferQualityLevel) {}; + + misc_param_quality_level->quality_level = 0; + + va_status = vaUnmapBuffer (encode_session_vaapi->va_display, + misc_param_buffer); + if (va_status != VA_STATUS_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to unmap misc parameter buffer: %s", + vaErrorStr (va_status)); + return FALSE; + } + + h264_frame->misc_buffers = + g_list_append (h264_frame->misc_buffers, + GUINT_TO_POINTER (misc_param_buffer)); + + return TRUE; +} + +static gboolean +ensure_misc_param_sub_mb_part_pel_h264 (GrdEncodeSessionVaapi *encode_session_vaapi, + H264Frame *h264_frame, + GError **error) +{ + VAEncMiscParameterBuffer *misc_param = NULL; + VAEncMiscParameterSubMbPartPelH264 *misc_param_sub_mb_part_pel_h264; + VABufferID misc_param_buffer = VA_INVALID_ID; + uint32_t total_size; + VAStatus va_status; + + total_size = sizeof (VAEncMiscParameterBuffer) + + sizeof (VAEncMiscParameterSubMbPartPelH264); + + va_status = vaCreateBuffer (encode_session_vaapi->va_display, + encode_session_vaapi->va_context, + VAEncMiscParameterBufferType, + total_size, 1, + NULL, + &misc_param_buffer); + if (va_status != VA_STATUS_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create misc parameter buffer: %s", + vaErrorStr (va_status)); + return FALSE; + } + g_assert (misc_param_buffer != VA_INVALID_ID); + + va_status = vaMapBuffer (encode_session_vaapi->va_display, + misc_param_buffer, + (void **) &misc_param); + if (va_status != VA_STATUS_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to map misc parameter buffer: %s", + vaErrorStr (va_status)); + return FALSE; + } + g_assert (misc_param); + + misc_param->type = VAEncMiscParameterTypeSubMbPartPel; + + misc_param_sub_mb_part_pel_h264 = + (VAEncMiscParameterSubMbPartPelH264 *) misc_param->data; + *misc_param_sub_mb_part_pel_h264 = (VAEncMiscParameterSubMbPartPelH264) {}; + + misc_param_sub_mb_part_pel_h264->enable_sub_pel_mode = 1; + misc_param_sub_mb_part_pel_h264->sub_pel_mode = 3; + + va_status = vaUnmapBuffer (encode_session_vaapi->va_display, + misc_param_buffer); + if (va_status != VA_STATUS_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to unmap misc parameter buffer: %s", + vaErrorStr (va_status)); + return FALSE; + } + + h264_frame->misc_buffers = + g_list_append (h264_frame->misc_buffers, + GUINT_TO_POINTER (misc_param_buffer)); + + return TRUE; +} + +static gboolean +ensure_misc_params (GrdEncodeSessionVaapi *encode_session_vaapi, + H264Frame *h264_frame, + GError **error) +{ + VAAPIPicture *picture = h264_frame->reconstructed_picture; + + if (picture->is_idr && + encode_session_vaapi->supports_quality_level && + !ensure_misc_param_quality_level (encode_session_vaapi, h264_frame, error)) + return FALSE; + if (!ensure_misc_param_sub_mb_part_pel_h264 (encode_session_vaapi, h264_frame, + error)) + return FALSE; + + return TRUE; +} + +static void +fill_avc_picture (GrdEncodeSessionVaapi *encode_session_vaapi, + VAEncPictureParameterBufferH264 *picture_param, + H264Frame *h264_frame) +{ + VAAPIPicture *picture = h264_frame->reconstructed_picture; + uint8_t i = 0; + + picture_param->CurrPic.picture_id = picture->va_surface; + picture_param->CurrPic.frame_idx = picture->frame_num; + picture_param->CurrPic.TopFieldOrderCnt = picture->frame_num * 2; + picture_param->CurrPic.BottomFieldOrderCnt = picture->frame_num * 2; + + if (!picture->is_idr) + { + VAPictureH264 *h264_picture = &picture_param->ReferenceFrames[0]; + VAAPIPicture *reference = encode_session_vaapi->reference_picture; + + g_assert (reference); + + h264_picture->picture_id = reference->va_surface; + h264_picture->frame_idx = reference->frame_num; + h264_picture->flags = VA_PICTURE_H264_SHORT_TERM_REFERENCE; + h264_picture->TopFieldOrderCnt = reference->frame_num * 2; + h264_picture->BottomFieldOrderCnt = reference->frame_num * 2; + ++i; + } + for (; i < G_N_ELEMENTS (picture_param->ReferenceFrames); ++i) + { + picture_param->ReferenceFrames[i].picture_id = VA_INVALID_ID; + picture_param->ReferenceFrames[i].flags = VA_PICTURE_H264_INVALID; + } + + picture_param->pic_parameter_set_id = 0; + picture_param->seq_parameter_set_id = 0; + + picture_param->last_picture = 0; + + picture_param->frame_num = picture->frame_num; + picture_param->pic_init_qp = 22; + picture_param->num_ref_idx_l0_active_minus1 = 0; + picture_param->num_ref_idx_l1_active_minus1 = 0; + + picture_param->chroma_qp_index_offset = 0; + picture_param->second_chroma_qp_index_offset = 0; + + picture_param->pic_fields.value = 0; + + if (picture->is_idr) + picture_param->pic_fields.bits.idr_pic_flag = 1; + picture_param->pic_fields.bits.reference_pic_flag = 1; /* No B-Frames are present */ + picture_param->pic_fields.bits.entropy_coding_mode_flag = 1; /* CABAC */ + picture_param->pic_fields.bits.transform_8x8_mode_flag = 1; +} + +static gboolean +prepare_picture (GrdEncodeSessionVaapi *encode_session_vaapi, + H264Frame *h264_frame, + GError **error) +{ + GrdNalWriter *nal_writer = encode_session_vaapi->nal_writer; + VAAPIPicture *picture = h264_frame->reconstructed_picture; + VAEncPictureParameterBufferH264 *picture_param = + &encode_session_vaapi->picture_param; + g_autofree uint8_t *header_data = NULL; + uint32_t header_length = 0; + + *picture_param = (VAEncPictureParameterBufferH264) {}; + fill_avc_picture (encode_session_vaapi, picture_param, h264_frame); + + if (!picture->is_idr) + return TRUE; + + header_data = grd_nal_writer_get_pps_bitstream (nal_writer, picture_param, + &header_length); + if (!create_packed_header_buffers (encode_session_vaapi, h264_frame, + VAEncPackedHeaderPicture, + header_data, header_length, error)) + return FALSE; + + h264_frame->picture_header_length = header_length; + + return TRUE; +} + +static void +fill_avc_slice (GrdEncodeSessionVaapi *encode_session_vaapi, + VAEncSliceParameterBufferH264 *slice_param, + H264Frame *h264_frame) +{ + VAAPIPicture *picture = h264_frame->reconstructed_picture; + uint32_t surface_width = encode_session_vaapi->surface_width; + uint32_t surface_height = encode_session_vaapi->surface_height; + uint32_t width_in_mbs; + uint32_t height_in_mbs; + uint32_t n_macroblocks; + uint8_t i = 0; + + g_assert (G_N_ELEMENTS (slice_param->RefPicList0) < 255); + g_assert (G_N_ELEMENTS (slice_param->RefPicList1) < 255); + + g_assert (surface_width % 16 == 0); + g_assert (surface_height % 16 == 0); + g_assert (surface_width >= 16); + g_assert (surface_height >= 16); + + width_in_mbs = surface_width / 16; + height_in_mbs = surface_height / 16; + n_macroblocks = width_in_mbs * height_in_mbs; + + slice_param->macroblock_address = 0; + slice_param->num_macroblocks = n_macroblocks; + slice_param->macroblock_info = VA_INVALID_ID; + slice_param->slice_type = picture->is_idr ? H264_SLICE_TYPE_I + : H264_SLICE_TYPE_P; + slice_param->pic_parameter_set_id = 0; + slice_param->idr_pic_id = 0; + + slice_param->pic_order_cnt_lsb = picture->frame_num * 2; + + if (!picture->is_idr) + { + VAAPIPicture *reference = encode_session_vaapi->reference_picture; + + g_assert (reference); + + slice_param->RefPicList0[0].picture_id = reference->va_surface; + slice_param->RefPicList0[0].frame_idx = reference->frame_num; + slice_param->RefPicList0[0].TopFieldOrderCnt = reference->frame_num * 2; + slice_param->RefPicList0[0].BottomFieldOrderCnt = reference->frame_num * 2; + slice_param->RefPicList0[0].flags = VA_PICTURE_H264_SHORT_TERM_REFERENCE; + ++i; + } + for (; i < G_N_ELEMENTS (slice_param->RefPicList0); ++i) + { + slice_param->RefPicList0[i].picture_id = VA_INVALID_SURFACE; + slice_param->RefPicList0[i].flags = VA_PICTURE_H264_INVALID; + } + /* B slices are not used here */ + for (i = 0; i < G_N_ELEMENTS (slice_param->RefPicList1); ++i) + { + slice_param->RefPicList1[i].picture_id = VA_INVALID_SURFACE; + slice_param->RefPicList1[i].flags = VA_PICTURE_H264_INVALID; + } + + slice_param->slice_qp_delta = 0; +} + +static gboolean +create_slice_buffer (GrdEncodeSessionVaapi *encode_session_vaapi, + H264Frame *h264_frame, + VAEncSliceParameterBufferH264 *slice_param, + GError **error) +{ + VAStatus va_status; + + va_status = vaCreateBuffer (encode_session_vaapi->va_display, + encode_session_vaapi->va_context, + VAEncSliceParameterBufferType, + sizeof (VAEncSliceParameterBufferH264), 1, + slice_param, + &h264_frame->slice_buffer); + if (va_status != VA_STATUS_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create slice parameter buffer: %s", + vaErrorStr (va_status)); + return FALSE; + } + g_assert (h264_frame->slice_buffer != VA_INVALID_ID); + + return TRUE; +} + +static gboolean +ensure_slice (GrdEncodeSessionVaapi *encode_session_vaapi, + H264Frame *h264_frame, + GError **error) +{ + GrdNalWriter *nal_writer = encode_session_vaapi->nal_writer; + VAEncSliceParameterBufferH264 slice_param = {}; + g_autofree uint8_t *header_data = NULL; + uint32_t header_length = 0; + + fill_avc_slice (encode_session_vaapi, &slice_param, h264_frame); + if (!create_slice_buffer (encode_session_vaapi, h264_frame, &slice_param, + error)) + return FALSE; + + header_data = + grd_nal_writer_get_slice_header_bitstream (nal_writer, &slice_param, + &encode_session_vaapi->sequence_param, + &encode_session_vaapi->picture_param, + &header_length); + if (!create_packed_header_buffers (encode_session_vaapi, h264_frame, + VAEncPackedHeaderSlice, + header_data, header_length, error)) + return FALSE; + + h264_frame->slice_header_length = header_length; + + return TRUE; +} + +static gboolean +ensure_bitstream_buffer (GrdEncodeSessionVaapi *encode_session_vaapi, + H264Frame *h264_frame, + GError **error) +{ + VAEncPictureParameterBufferH264 *picture_param = + &encode_session_vaapi->picture_param; + uint32_t surface_width = encode_session_vaapi->surface_width; + uint32_t surface_height = encode_session_vaapi->surface_height; + uint32_t bitstream_buffer_size; + uint32_t header_lengths = 0; + uint32_t width_in_mbs; + uint32_t height_in_mbs; + VAStatus va_status; + + g_assert (surface_width % 16 == 0); + g_assert (surface_height % 16 == 0); + g_assert (surface_width >= 16); + g_assert (surface_height >= 16); + + width_in_mbs = surface_width / 16; + height_in_mbs = surface_height / 16; + + bitstream_buffer_size = width_in_mbs * height_in_mbs * 400; + + header_lengths += h264_frame->aud_header_length; + header_lengths += h264_frame->sequence_header_length; + header_lengths += h264_frame->picture_header_length; + header_lengths += h264_frame->slice_header_length; + bitstream_buffer_size += (header_lengths + 7) / 8; + + va_status = vaCreateBuffer (encode_session_vaapi->va_display, + encode_session_vaapi->va_context, + VAEncCodedBufferType, + bitstream_buffer_size, 1, + NULL, + &h264_frame->bitstream_buffer); + if (va_status != VA_STATUS_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create bitstream buffer: %s", + vaErrorStr (va_status)); + return FALSE; + } + g_assert (h264_frame->bitstream_buffer != VA_INVALID_ID); + + picture_param->coded_buf = h264_frame->bitstream_buffer; + + return TRUE; +} + +static gboolean +create_picture_buffer (GrdEncodeSessionVaapi *encode_session_vaapi, + H264Frame *h264_frame, + GError **error) +{ + VAEncPictureParameterBufferH264 *picture_param = + &encode_session_vaapi->picture_param; + VAStatus va_status; + + va_status = vaCreateBuffer (encode_session_vaapi->va_display, + encode_session_vaapi->va_context, + VAEncPictureParameterBufferType, + sizeof (VAEncPictureParameterBufferH264), 1, + picture_param, + &h264_frame->picture_buffer); + if (va_status != VA_STATUS_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create picture parameter buffer: %s", + vaErrorStr (va_status)); + return FALSE; + } + g_assert (h264_frame->picture_buffer != VA_INVALID_ID); + + return TRUE; +} + +static gboolean +render_picture (GrdEncodeSessionVaapi *encode_session_vaapi, + VAContextID va_context, + VASurfaceID va_render_target, + VABufferID *va_buffers, + uint32_t n_buffers, + GError **error) +{ + VAStatus va_status; + + va_status = vaBeginPicture (encode_session_vaapi->va_display, va_context, + va_render_target); + if (va_status != VA_STATUS_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to begin picture: %s", vaErrorStr (va_status)); + return FALSE; + } + + va_status = vaRenderPicture (encode_session_vaapi->va_display, va_context, + va_buffers, n_buffers); + if (va_status != VA_STATUS_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to submit buffers: %s", vaErrorStr (va_status)); + return FALSE; + } + + va_status = vaEndPicture (encode_session_vaapi->va_display, va_context); + if (va_status != VA_STATUS_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to end picture: %s", vaErrorStr (va_status)); + return FALSE; + } + + return TRUE; +} + +static void +prepare_avc_frame_info (GrdEncodeSessionVaapi *encode_session_vaapi, + H264Frame *h264_frame) +{ + VAAPIPicture *picture = h264_frame->reconstructed_picture; + VAEncPictureParameterBufferH264 *picture_param = + &encode_session_vaapi->picture_param; + + h264_frame->avc_frame_info = + grd_avc_frame_info_new (picture->is_idr ? GRD_AVC_FRAME_TYPE_I + : GRD_AVC_FRAME_TYPE_P, + picture_param->pic_init_qp, + 100); +} + +static H264Frame * +encode_frame (GrdEncodeSessionVaapi *encode_session_vaapi, + VASurfaceID src_surface, + GError **error) +{ + g_autoptr (H264Frame) h264_frame = NULL; + g_autofree VABufferID *va_buffers = NULL; + uint32_t n_buffers = 0; + + h264_frame = h264_frame_new (encode_session_vaapi, error); + if (!h264_frame) + return NULL; + + if (!ensure_access_unit_delimiter (encode_session_vaapi, h264_frame, error)) + return NULL; + if (!maybe_ensure_sequence (encode_session_vaapi, h264_frame, error)) + return NULL; + if (!ensure_misc_params (encode_session_vaapi, h264_frame, error)) + return NULL; + if (!prepare_picture (encode_session_vaapi, h264_frame, error)) + return NULL; + if (!ensure_slice (encode_session_vaapi, h264_frame, error)) + return NULL; + if (!ensure_bitstream_buffer (encode_session_vaapi, h264_frame, error)) + return NULL; + if (!create_picture_buffer (encode_session_vaapi, h264_frame, error)) + return NULL; + + if (encode_session_vaapi->debug_va_times) + h264_frame->timestamps[0] = g_get_monotonic_time (); + + va_buffers = h264_frame_get_buffers (h264_frame, &n_buffers); + if (!render_picture (encode_session_vaapi, encode_session_vaapi->va_context, + src_surface, va_buffers, n_buffers, error)) + return NULL; + + h264_frame->src_surface = src_surface; + prepare_avc_frame_info (encode_session_vaapi, h264_frame); + + if (encode_session_vaapi->debug_va_times) + h264_frame->timestamps[1] = g_get_monotonic_time (); + + return g_steal_pointer (&h264_frame); +} + +static void +prepare_next_frame (GrdEncodeSessionVaapi *encode_session_vaapi, + H264Frame *current_h264_frame) +{ + uint16_t max_frame_num; + + g_clear_pointer (&encode_session_vaapi->reference_picture, vaapi_picture_free); + encode_session_vaapi->reference_picture = + g_steal_pointer (¤t_h264_frame->reconstructed_picture); + + max_frame_num = 1 << LOG2_MAX_FRAME_NUM; + + ++encode_session_vaapi->frame_num; + encode_session_vaapi->frame_num %= max_frame_num + 1; + + encode_session_vaapi->pending_idr_frame = FALSE; +} + +static gboolean +grd_encode_session_vaapi_encode_frame (GrdEncodeSession *encode_session, + GrdEncodeContext *encode_context, + GrdImageView *image_view, + GError **error) +{ + GrdEncodeSessionVaapi *encode_session_vaapi = + GRD_ENCODE_SESSION_VAAPI (encode_session); + NV12VkVASurface *vkva_surface = NULL; + H264Frame *h264_frame = NULL; + + g_mutex_lock (&encode_session_vaapi->pending_frames_mutex); + g_assert (!g_hash_table_contains (encode_session_vaapi->pending_frames, + image_view)); + g_mutex_unlock (&encode_session_vaapi->pending_frames_mutex); + + if (!g_hash_table_lookup_extended (encode_session_vaapi->surfaces, + image_view, + NULL, (gpointer *) &vkva_surface)) + g_assert_not_reached (); + + g_assert (vkva_surface); + + h264_frame = encode_frame (encode_session_vaapi, vkva_surface->va_surface, + error); + if (!h264_frame) + return FALSE; + + g_mutex_lock (&encode_session_vaapi->pending_frames_mutex); + g_hash_table_insert (encode_session_vaapi->pending_frames, + image_view, h264_frame); + g_mutex_unlock (&encode_session_vaapi->pending_frames_mutex); + + prepare_next_frame (encode_session_vaapi, h264_frame); + + return TRUE; +} + +static gboolean +get_bitstream (GrdEncodeSessionVaapi *encode_session_vaapi, + VABufferID output_buf, + GrdBitstream **bitstream, + GError **error) +{ + VACodedBufferSegment *va_segment = NULL; + VAStatus va_status; + + va_status = vaMapBuffer (encode_session_vaapi->va_display, + output_buf, + (void **) &va_segment); + if (va_status != VA_STATUS_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to map output buffer: %s", vaErrorStr (va_status)); + return FALSE; + } + g_assert (va_segment); + + *bitstream = grd_bitstream_new (va_segment->buf, va_segment->size); + + return TRUE; +} + +static gboolean +finish_frame_encoding (GrdEncodeSessionVaapi *encode_session_vaapi, + H264Frame *h264_frame, + GrdBitstream **bitstream, + GError **error) +{ + int64_t *timestamps = h264_frame->timestamps; + VAStatus va_status; + + va_status = vaSyncSurface (encode_session_vaapi->va_display, + h264_frame->src_surface); + if (va_status != VA_STATUS_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to sync surface: %s", vaErrorStr (va_status)); + return FALSE; + } + + if (encode_session_vaapi->debug_va_times) + timestamps[2] = g_get_monotonic_time (); + + if (!get_bitstream (encode_session_vaapi, h264_frame->bitstream_buffer, + bitstream, error)) + return FALSE; + + grd_bitstream_set_avc_frame_info (*bitstream, + g_steal_pointer (&h264_frame->avc_frame_info)); + + if (encode_session_vaapi->debug_va_times) + { + g_debug ("[HWAccel.VAAPI] EncodeFrame[Times]: submission: %liµs, " + "waitForBitstream: %liµs, total: %liµs", + timestamps[1] - timestamps[0], + timestamps[2] - timestamps[1], + timestamps[2] - timestamps[0]); + } + + return TRUE; +} + +static GrdBitstream * +grd_encode_session_vaapi_lock_bitstream (GrdEncodeSession *encode_session, + GrdImageView *image_view, + GError **error) +{ + GrdEncodeSessionVaapi *encode_session_vaapi = + GRD_ENCODE_SESSION_VAAPI (encode_session); + g_autoptr (H264Frame) h264_frame = NULL; + g_autoptr (GMutexLocker) locker = NULL; + GrdBitstream *bitstream = NULL; + + g_mutex_lock (&encode_session_vaapi->pending_frames_mutex); + if (!g_hash_table_steal_extended (encode_session_vaapi->pending_frames, + image_view, + NULL, (gpointer *) &h264_frame)) + g_assert_not_reached (); + + g_mutex_unlock (&encode_session_vaapi->pending_frames_mutex); + g_assert (h264_frame); + + if (!finish_frame_encoding (encode_session_vaapi, h264_frame, &bitstream, + error)) + return NULL; + + locker = g_mutex_locker_new (&encode_session_vaapi->bitstreams_mutex); + g_hash_table_insert (encode_session_vaapi->bitstreams, + bitstream, g_steal_pointer (&h264_frame)); + + return bitstream; +} + +static gboolean +grd_encode_session_vaapi_unlock_bitstream (GrdEncodeSession *encode_session, + GrdBitstream *bitstream, + GError **error) +{ + GrdEncodeSessionVaapi *encode_session_vaapi = + GRD_ENCODE_SESSION_VAAPI (encode_session); + g_autoptr (H264Frame) h264_frame = NULL; + g_autoptr (GMutexLocker) locker = NULL; + VAStatus va_status; + + locker = g_mutex_locker_new (&encode_session_vaapi->bitstreams_mutex); + if (!g_hash_table_steal_extended (encode_session_vaapi->bitstreams, + bitstream, + NULL, (gpointer *) &h264_frame)) + g_assert_not_reached (); + + g_clear_pointer (&locker, g_mutex_locker_free); + g_assert (h264_frame); + + va_status = vaUnmapBuffer (encode_session_vaapi->va_display, + h264_frame->bitstream_buffer); + if (va_status != VA_STATUS_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to unmap output buffer: %s", vaErrorStr (va_status)); + return FALSE; + } + + grd_bitstream_free (bitstream); + + return TRUE; +} + +static gboolean +get_avc_level_idc_for_mbps (uint32_t mbps, + uint8_t *level_idc, + GError **error) +{ + *level_idc = 0; + + /* See also Table A-1 - Level limits (Rec. ITU-T H.264 (08/2021)) */ + if (mbps <= 1485) + *level_idc = 10; + else if (mbps <= 3000) + *level_idc = 11; + else if (mbps <= 6000) + *level_idc = 12; + else if (mbps <= 11880) + *level_idc = 13; + else if (mbps <= 19800) + *level_idc = 21; + else if (mbps <= 20250) + *level_idc = 22; + else if (mbps <= 40500) + *level_idc = 30; + else if (mbps <= 108000) + *level_idc = 31; + else if (mbps <= 216000) + *level_idc = 32; + else if (mbps <= 245760) + *level_idc = 40; + else if (mbps <= 522240) + *level_idc = 42; + else if (mbps <= 589824) + *level_idc = 50; + else if (mbps <= 983040) + *level_idc = 51; + else if (mbps <= 2073600) + *level_idc = 52; + else if (mbps <= 4177920) + *level_idc = 60; + else if (mbps <= 8355840) + *level_idc = 61; + else if (mbps <= 16711680) + *level_idc = 62; + + if (*level_idc == 0) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Unable to determine level_idc"); + return FALSE; + } + + return TRUE; +} + +static gboolean +determine_avc_level_idc (GrdEncodeSessionVaapi *encode_session_vaapi, + GError **error) +{ + uint32_t surface_width = encode_session_vaapi->surface_width; + uint32_t surface_height = encode_session_vaapi->surface_height; + uint32_t width_in_mbs; + uint32_t height_in_mbs; + uint32_t mbs_per_sec; + + g_assert (surface_width % 16 == 0); + g_assert (surface_height % 16 == 0); + g_assert (surface_width >= 16); + g_assert (surface_height >= 16); + + width_in_mbs = surface_width / 16; + height_in_mbs = surface_height / 16; + + mbs_per_sec = width_in_mbs * height_in_mbs * + encode_session_vaapi->refresh_rate; + if (!get_avc_level_idc_for_mbps (mbs_per_sec, + &encode_session_vaapi->level_idc, + error)) + return FALSE; + + return TRUE; +} + +static gboolean +get_surface_constraints (GrdEncodeSessionVaapi *encode_session_vaapi, + gboolean *supports_nv12, + int32_t *min_width, + int32_t *max_width, + int32_t *min_height, + int32_t *max_height, + GError **error) +{ + g_autofree VASurfaceAttrib *surface_attributes = NULL; + unsigned int n_attributes = 0; + gboolean found_min_width = FALSE; + gboolean found_max_width = FALSE; + gboolean found_min_height = FALSE; + gboolean found_max_height = FALSE; + VAStatus va_status; + unsigned int i; + + *supports_nv12 = FALSE; + + va_status = vaQuerySurfaceAttributes (encode_session_vaapi->va_display, + encode_session_vaapi->va_config, + NULL, &n_attributes); + if (va_status != VA_STATUS_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to query num surface attributes: %s", + vaErrorStr (va_status)); + return FALSE; + } + + if (n_attributes == 0) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Invalid max num surface attributes"); + return FALSE; + } + surface_attributes = g_new0 (VASurfaceAttrib, n_attributes); + + va_status = vaQuerySurfaceAttributes (encode_session_vaapi->va_display, + encode_session_vaapi->va_config, + surface_attributes, &n_attributes); + if (va_status != VA_STATUS_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to query surface attributes: %s", + vaErrorStr (va_status)); + return FALSE; + } + + for (i = 0; i < n_attributes; ++i) + { + if (!(surface_attributes[i].flags & VA_SURFACE_ATTRIB_GETTABLE) || + surface_attributes[i].value.type != VAGenericValueTypeInteger) + continue; + + if (surface_attributes[i].type == VASurfaceAttribPixelFormat && + surface_attributes[i].value.value.i == VA_FOURCC_NV12) + *supports_nv12 = TRUE; + + if (surface_attributes[i].type == VASurfaceAttribMinWidth) + { + *min_width = surface_attributes[i].value.value.i; + found_min_width = TRUE; + } + if (surface_attributes[i].type == VASurfaceAttribMaxWidth) + { + *max_width = surface_attributes[i].value.value.i; + found_max_width = TRUE; + } + if (surface_attributes[i].type == VASurfaceAttribMinHeight) + { + *min_height = surface_attributes[i].value.value.i; + found_min_height = TRUE; + } + if (surface_attributes[i].type == VASurfaceAttribMaxHeight) + { + *max_height = surface_attributes[i].value.value.i; + found_max_height = TRUE; + } + } + + if (found_min_width == FALSE || found_max_width == FALSE || + found_min_height == FALSE || found_max_height == FALSE) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Unable to query surface size constraints"); + return FALSE; + } + + return TRUE; +} + +static gboolean +create_avc420_encode_session (GrdEncodeSessionVaapi *encode_session_vaapi, + GError **error) +{ + uint32_t surface_width = encode_session_vaapi->surface_width; + uint32_t surface_height = encode_session_vaapi->surface_height; + VAConfigAttrib config_attributes[3] = {}; + gboolean supports_nv12 = FALSE; + int32_t min_surface_width = 0; + int32_t max_surface_width = 0; + int32_t min_surface_height = 0; + int32_t max_surface_height = 0; + VAStatus va_status; + uint32_t i; + + g_assert (surface_width % 16 == 0); + g_assert (surface_height % 16 == 0); + g_assert (surface_width >= 16); + g_assert (surface_height >= 16); + + if (!determine_avc_level_idc (encode_session_vaapi, error)) + return FALSE; + + config_attributes[0].type = VAConfigAttribRTFormat; + config_attributes[0].value = VA_RT_FORMAT_YUV420; + + config_attributes[1].type = VAConfigAttribRateControl; + config_attributes[1].value = VA_RC_CQP; + + config_attributes[2].type = VAConfigAttribEncPackedHeaders; + config_attributes[2].value = VA_ENC_PACKED_HEADER_SEQUENCE | + VA_ENC_PACKED_HEADER_PICTURE | + VA_ENC_PACKED_HEADER_SLICE | + VA_ENC_PACKED_HEADER_RAW_DATA; + + va_status = vaCreateConfig (encode_session_vaapi->va_display, + VAProfileH264High, VAEntrypointEncSlice, + config_attributes, + G_N_ELEMENTS (config_attributes), + &encode_session_vaapi->va_config); + if (va_status != VA_STATUS_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create AVC420 encode session config: %s", + vaErrorStr (va_status)); + return FALSE; + } + g_assert (encode_session_vaapi->va_config != VA_INVALID_ID); + + if (!get_surface_constraints (encode_session_vaapi, &supports_nv12, + &min_surface_width, &max_surface_width, + &min_surface_height, &max_surface_height, + error)) + return FALSE; + + if (!supports_nv12) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Surface does not support NV12 as pixel format"); + return FALSE; + } + + if (surface_width < min_surface_width || surface_width > max_surface_width || + surface_height < min_surface_height || surface_height > max_surface_height) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, + "Surface size does not meet encoder constraints. Requested " + "size: [%i, %i], Size constraints: [[%i, %i], [%i, %i]]", + surface_width, surface_height, + min_surface_width, max_surface_width, + min_surface_height, max_surface_height); + return FALSE; + } + + g_debug ("[HWAccel.VAAPI] Requested surface size: [%i, %i], Size " + "constraints: [[%i, %i], [%i, %i]]", + surface_width, surface_height, + min_surface_width, max_surface_width, + min_surface_height, max_surface_height); + + va_status = vaCreateContext (encode_session_vaapi->va_display, + encode_session_vaapi->va_config, + surface_width, surface_height, VA_PROGRESSIVE, + NULL, 0, + &encode_session_vaapi->va_context); + if (va_status != VA_STATUS_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create AVC420 encode session context: %s", + vaErrorStr (va_status)); + return FALSE; + } + g_assert (encode_session_vaapi->va_context != VA_INVALID_ID); + + for (i = 0; i < N_SRC_SURFACES; ++i) + { + NV12VkVASurface *surface; + + surface = nv12_vkva_surface_new (encode_session_vaapi, + surface_width, surface_height, + error); + if (!surface) + return FALSE; + + g_hash_table_insert (encode_session_vaapi->surfaces, + surface->image_view, surface); + } + + encode_session_vaapi->nal_writer = grd_nal_writer_new (); + + return TRUE; +} + +GrdEncodeSessionVaapi * +grd_encode_session_vaapi_new (GrdVkDevice *vk_device, + VADisplay va_display, + gboolean supports_quality_level, + uint32_t source_width, + uint32_t source_height, + uint32_t refresh_rate, + GError **error) +{ + g_autoptr (GrdEncodeSessionVaapi) encode_session_vaapi = NULL; + uint8_t level_idc; + + encode_session_vaapi = g_object_new (GRD_TYPE_ENCODE_SESSION_VAAPI, NULL); + encode_session_vaapi->vk_device = vk_device; + encode_session_vaapi->va_display = va_display; + encode_session_vaapi->supports_quality_level = supports_quality_level; + + encode_session_vaapi->surface_width = grd_get_aligned_size (source_width, 32); + encode_session_vaapi->surface_height = grd_get_aligned_size (source_height, 16); + encode_session_vaapi->refresh_rate = refresh_rate; + + if (!create_avc420_encode_session (encode_session_vaapi, error)) + return NULL; + + level_idc = encode_session_vaapi->level_idc; + g_debug ("[HWAccel.VAAPI] Using level_idc %u.%u for AVC encode session", + level_idc / 10, level_idc - (level_idc / 10) * 10); + + return g_steal_pointer (&encode_session_vaapi); +} + +static void +grd_encode_session_vaapi_dispose (GObject *object) +{ + GrdEncodeSessionVaapi *encode_session_vaapi = + GRD_ENCODE_SESSION_VAAPI (object); + VADisplay va_display = encode_session_vaapi->va_display; + + g_assert (g_hash_table_size (encode_session_vaapi->bitstreams) == 0); + g_assert (g_hash_table_size (encode_session_vaapi->pending_frames) == 0); + + g_clear_pointer (&encode_session_vaapi->reference_picture, vaapi_picture_free); + g_clear_pointer (&encode_session_vaapi->surfaces, g_hash_table_unref); + + g_clear_object (&encode_session_vaapi->nal_writer); + + if (encode_session_vaapi->va_context != VA_INVALID_ID) + vaDestroyContext (va_display, encode_session_vaapi->va_context); + if (encode_session_vaapi->va_config != VA_INVALID_ID) + vaDestroyConfig (va_display, encode_session_vaapi->va_config); + + G_OBJECT_CLASS (grd_encode_session_vaapi_parent_class)->dispose (object); +} + +static void +grd_encode_session_vaapi_finalize (GObject *object) +{ + GrdEncodeSessionVaapi *encode_session_vaapi = + GRD_ENCODE_SESSION_VAAPI (object); + + g_mutex_clear (&encode_session_vaapi->bitstreams_mutex); + g_mutex_clear (&encode_session_vaapi->pending_frames_mutex); + + g_clear_pointer (&encode_session_vaapi->bitstreams, g_hash_table_unref); + g_clear_pointer (&encode_session_vaapi->pending_frames, g_hash_table_unref); + + G_OBJECT_CLASS (grd_encode_session_vaapi_parent_class)->finalize (object); +} + +static void +grd_encode_session_vaapi_init (GrdEncodeSessionVaapi *encode_session_vaapi) +{ + if (grd_get_debug_flags () & GRD_DEBUG_VA_TIMES) + encode_session_vaapi->debug_va_times = TRUE; + + encode_session_vaapi->va_config = VA_INVALID_ID; + encode_session_vaapi->va_context = VA_INVALID_ID; + encode_session_vaapi->pending_idr_frame = TRUE; + + encode_session_vaapi->surfaces = + g_hash_table_new_full (NULL, NULL, + NULL, (GDestroyNotify) nv12_vkva_surface_free); + encode_session_vaapi->pending_frames = g_hash_table_new (NULL, NULL); + encode_session_vaapi->bitstreams = g_hash_table_new (NULL, NULL); + + g_mutex_init (&encode_session_vaapi->pending_frames_mutex); + g_mutex_init (&encode_session_vaapi->bitstreams_mutex); +} + +static void +grd_encode_session_vaapi_class_init (GrdEncodeSessionVaapiClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GrdEncodeSessionClass *encode_session_class = + GRD_ENCODE_SESSION_CLASS (klass); + + object_class->dispose = grd_encode_session_vaapi_dispose; + object_class->finalize = grd_encode_session_vaapi_finalize; + + encode_session_class->get_surface_size = + grd_encode_session_vaapi_get_surface_size; + encode_session_class->get_image_views = + grd_encode_session_vaapi_get_image_views; + encode_session_class->has_pending_frames = + grd_encode_session_vaapi_has_pending_frames; + encode_session_class->encode_frame = + grd_encode_session_vaapi_encode_frame; + encode_session_class->lock_bitstream = + grd_encode_session_vaapi_lock_bitstream; + encode_session_class->unlock_bitstream = + grd_encode_session_vaapi_unlock_bitstream; +} diff --git a/grd-encode-session-vaapi.h b/grd-encode-session-vaapi.h new file mode 100644 index 0000000..642a5e9 --- /dev/null +++ b/grd-encode-session-vaapi.h @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2024 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include + +#include "grd-encode-session.h" + +#define GRD_TYPE_ENCODE_SESSION_VAAPI (grd_encode_session_vaapi_get_type ()) +G_DECLARE_FINAL_TYPE (GrdEncodeSessionVaapi, grd_encode_session_vaapi, + GRD, ENCODE_SESSION_VAAPI, GrdEncodeSession) + +GrdEncodeSessionVaapi *grd_encode_session_vaapi_new (GrdVkDevice *vk_device, + VADisplay va_display, + gboolean supports_quality_level, + uint32_t source_width, + uint32_t source_height, + uint32_t refresh_rate, + GError **error); diff --git a/grd-encode-session.c b/grd-encode-session.c new file mode 100644 index 0000000..99b3ce0 --- /dev/null +++ b/grd-encode-session.c @@ -0,0 +1,244 @@ +/* + * Copyright (C) 2024 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-encode-session.h" + +typedef struct +{ + GrdImageView *image_view; + + GrdEncodeSessionOnBitstreamLockedFunc callback; + gpointer callback_user_data; +} GrdLockBitstreamTask; + +typedef struct +{ + gboolean in_shutdown; + + GThread *encode_thread; + GMainContext *encode_context; + + GSource *bitstream_lock_source; + GAsyncQueue *task_queue; +} GrdEncodeSessionPrivate; + +G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (GrdEncodeSession, grd_encode_session, + G_TYPE_OBJECT) + +void +grd_encode_session_get_surface_size (GrdEncodeSession *encode_session, + uint32_t *surface_width, + uint32_t *surface_height) +{ + GrdEncodeSessionClass *klass = GRD_ENCODE_SESSION_GET_CLASS (encode_session); + + return klass->get_surface_size (encode_session, surface_width, surface_height); +} + +GList * +grd_encode_session_get_image_views (GrdEncodeSession *encode_session) +{ + GrdEncodeSessionClass *klass = GRD_ENCODE_SESSION_GET_CLASS (encode_session); + + return klass->get_image_views (encode_session); +} + +gboolean +grd_encode_session_has_pending_frames (GrdEncodeSession *encode_session) +{ + GrdEncodeSessionClass *klass = GRD_ENCODE_SESSION_GET_CLASS (encode_session); + + return klass->has_pending_frames (encode_session); +} + +gboolean +grd_encode_session_encode_frame (GrdEncodeSession *encode_session, + GrdEncodeContext *encode_context, + GrdImageView *image_view, + GError **error) +{ + GrdEncodeSessionClass *klass = GRD_ENCODE_SESSION_GET_CLASS (encode_session); + + return klass->encode_frame (encode_session, encode_context, image_view, error); +} + +void +grd_encode_session_lock_bitstream (GrdEncodeSession *encode_session, + GrdImageView *image_view, + GrdEncodeSessionOnBitstreamLockedFunc on_bitstream_locked, + gpointer user_data) +{ + GrdEncodeSessionPrivate *priv = + grd_encode_session_get_instance_private (encode_session); + GrdLockBitstreamTask *task; + + task = g_new0 (GrdLockBitstreamTask, 1); + task->image_view = image_view; + task->callback = on_bitstream_locked; + task->callback_user_data = user_data; + + g_async_queue_push (priv->task_queue, task); + g_source_set_ready_time (priv->bitstream_lock_source, 0); +} + +gboolean +grd_encode_session_unlock_bitstream (GrdEncodeSession *encode_session, + GrdBitstream *bitstream, + GError **error) +{ + GrdEncodeSessionClass *klass = GRD_ENCODE_SESSION_GET_CLASS (encode_session); + + return klass->unlock_bitstream (encode_session, bitstream, error); +} + +static void +stop_encode_thread (GrdEncodeSession *encode_session) +{ + GrdEncodeSessionPrivate *priv = + grd_encode_session_get_instance_private (encode_session); + + g_assert (priv->encode_context); + g_assert (priv->encode_thread); + + priv->in_shutdown = TRUE; + + g_main_context_wakeup (priv->encode_context); + g_clear_pointer (&priv->encode_thread, g_thread_join); +} + +static void +grd_encode_session_dispose (GObject *object) +{ + GrdEncodeSession *encode_session = GRD_ENCODE_SESSION (object); + GrdEncodeSessionPrivate *priv = + grd_encode_session_get_instance_private (encode_session); + + g_assert (g_async_queue_try_pop (priv->task_queue) == NULL); + + if (priv->encode_thread) + stop_encode_thread (encode_session); + + if (priv->bitstream_lock_source) + { + g_source_destroy (priv->bitstream_lock_source); + g_clear_pointer (&priv->bitstream_lock_source, g_source_unref); + } + + g_clear_pointer (&priv->encode_context, g_main_context_unref); + + G_OBJECT_CLASS (grd_encode_session_parent_class)->dispose (object); +} + +static void +grd_encode_session_finalize (GObject *object) +{ + GrdEncodeSession *encode_session = GRD_ENCODE_SESSION (object); + GrdEncodeSessionPrivate *priv = + grd_encode_session_get_instance_private (encode_session); + + g_clear_pointer (&priv->task_queue, g_async_queue_unref); + + G_OBJECT_CLASS (grd_encode_session_parent_class)->finalize (object); +} + +static gpointer +encode_thread_func (gpointer data) +{ + GrdEncodeSession *encode_session = data; + GrdEncodeSessionPrivate *priv = + grd_encode_session_get_instance_private (encode_session); + + while (!priv->in_shutdown) + g_main_context_iteration (priv->encode_context, TRUE); + + return NULL; +} + +static gboolean +lock_bitstreams (gpointer user_data) +{ + GrdEncodeSession *encode_session = user_data; + GrdEncodeSessionClass *klass = GRD_ENCODE_SESSION_GET_CLASS (encode_session); + GrdEncodeSessionPrivate *priv = + grd_encode_session_get_instance_private (encode_session); + GrdLockBitstreamTask *task; + + while ((task = g_async_queue_try_pop (priv->task_queue))) + { + GrdBitstream *bitstream; + g_autoptr (GError) error = NULL; + + bitstream = klass->lock_bitstream (encode_session, task->image_view, + &error); + task->callback (encode_session, bitstream, task->callback_user_data, + error); + + g_free (task); + } + + return G_SOURCE_CONTINUE; +} + +static gboolean +source_dispatch (GSource *source, + GSourceFunc callback, + gpointer user_data) +{ + g_source_set_ready_time (source, -1); + + return callback (user_data); +} + +static GSourceFuncs source_funcs = +{ + .dispatch = source_dispatch, +}; + +static void +grd_encode_session_init (GrdEncodeSession *encode_session) +{ + GrdEncodeSessionPrivate *priv = + grd_encode_session_get_instance_private (encode_session); + GSource *bitstream_lock_source; + + priv->task_queue = g_async_queue_new (); + + priv->encode_context = g_main_context_new (); + priv->encode_thread = g_thread_new ("Encode thread", + encode_thread_func, + encode_session); + + bitstream_lock_source = g_source_new (&source_funcs, sizeof (GSource)); + g_source_set_callback (bitstream_lock_source, lock_bitstreams, + encode_session, NULL); + g_source_set_ready_time (bitstream_lock_source, -1); + g_source_attach (bitstream_lock_source, priv->encode_context); + priv->bitstream_lock_source = bitstream_lock_source; +} + +static void +grd_encode_session_class_init (GrdEncodeSessionClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = grd_encode_session_dispose; + object_class->finalize = grd_encode_session_finalize; +} diff --git a/grd-encode-session.h b/grd-encode-session.h new file mode 100644 index 0000000..bc54352 --- /dev/null +++ b/grd-encode-session.h @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2024 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include +#include + +#include "grd-types.h" + +#define GRD_TYPE_ENCODE_SESSION (grd_encode_session_get_type ()) +G_DECLARE_DERIVABLE_TYPE (GrdEncodeSession, grd_encode_session, + GRD, ENCODE_SESSION, GObject) + +typedef void (* GrdEncodeSessionOnBitstreamLockedFunc) (GrdEncodeSession *encode_session, + GrdBitstream *bitstream, + gpointer user_data, + GError *error); + +struct _GrdEncodeSessionClass +{ + GObjectClass parent_class; + + void (* get_surface_size) (GrdEncodeSession *encode_session, + uint32_t *surface_width, + uint32_t *surface_height); + GList *(* get_image_views) (GrdEncodeSession *encode_session); + gboolean (* has_pending_frames) (GrdEncodeSession *encode_session); + gboolean (* encode_frame) (GrdEncodeSession *encode_session, + GrdEncodeContext *encode_context, + GrdImageView *image_view, + GError **error); + GrdBitstream *(* lock_bitstream) (GrdEncodeSession *encode_session, + GrdImageView *image_view, + GError **error); + gboolean (* unlock_bitstream) (GrdEncodeSession *encode_session, + GrdBitstream *bitstream, + GError **error); +}; + +void grd_encode_session_get_surface_size (GrdEncodeSession *encode_session, + uint32_t *surface_width, + uint32_t *surface_height); + +GList *grd_encode_session_get_image_views (GrdEncodeSession *encode_session); + +gboolean grd_encode_session_has_pending_frames (GrdEncodeSession *encode_session); + +gboolean grd_encode_session_encode_frame (GrdEncodeSession *encode_session, + GrdEncodeContext *encode_context, + GrdImageView *image_view, + GError **error); + +void grd_encode_session_lock_bitstream (GrdEncodeSession *encode_session, + GrdImageView *image_view, + GrdEncodeSessionOnBitstreamLockedFunc on_bitstream_locked, + gpointer user_data); + +gboolean grd_encode_session_unlock_bitstream (GrdEncodeSession *encode_session, + GrdBitstream *bitstream, + GError **error); diff --git a/grd-enum-types.c.in b/grd-enum-types.c.in new file mode 100644 index 0000000..9e2cf0e --- /dev/null +++ b/grd-enum-types.c.in @@ -0,0 +1,41 @@ +/*** BEGIN file-header ***/ + +#include + +/*** END file-header ***/ + +/*** BEGIN file-production ***/ + +/* enumerations from "@filename@" */ +#include "@filename@" + +/*** END file-production ***/ + +/*** BEGIN value-header ***/ +GType +@enum_name@_get_type (void) +{ + static size_t g_enum_type_id = 0; + + if (g_once_init_enter (&g_enum_type_id)) + { + static const G@Type@Value values[] = { +/*** END value-header ***/ + +/*** BEGIN value-production ***/ + { @VALUENAME@, "@VALUENAME@", "@valuenick@" }, +/*** END value-production ***/ + +/*** BEGIN value-tail ***/ + { 0, NULL, NULL } + }; + GType id; + + id = g_@type@_register_static (g_intern_static_string ("@EnumName@"), values); + + g_once_init_leave (&g_enum_type_id, id); + } + + return g_enum_type_id; +} +/*** END value-tail ***/ diff --git a/grd-enum-types.h.in b/grd-enum-types.h.in new file mode 100644 index 0000000..bc21c14 --- /dev/null +++ b/grd-enum-types.h.in @@ -0,0 +1,23 @@ +/*** BEGIN file-header ***/ +#pragma once + +#include + +G_BEGIN_DECLS + +/*** END file-header ***/ + +/*** BEGIN file-production ***/ +/* enumerations from "@basename@" */ +/*** END file-production ***/ + +/*** BEGIN file-tail ***/ +G_END_DECLS + +/*** END file-tail ***/ + +/*** BEGIN value-header ***/ +GType @enum_name@_get_type (void) G_GNUC_CONST; +#define @ENUMPREFIX@_TYPE_@ENUMSHORT@ (@enum_name@_get_type ()) + +/*** END value-header ***/ diff --git a/grd-enums.h b/grd-enums.h new file mode 100644 index 0000000..5221fcf --- /dev/null +++ b/grd-enums.h @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2018 Red Hat Inc. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + */ + +#pragma once + +typedef enum +{ + GRD_RDP_SCREEN_SHARE_MODE_MIRROR_PRIMARY, + GRD_RDP_SCREEN_SHARE_MODE_EXTEND, +} GrdRdpScreenShareMode; + +typedef enum +{ + GRD_VNC_AUTH_METHOD_PROMPT, + GRD_VNC_AUTH_METHOD_PASSWORD +} GrdVncAuthMethod; + +typedef enum +{ + GRD_VNC_SCREEN_SHARE_MODE_MIRROR_PRIMARY, + GRD_VNC_SCREEN_SHARE_MODE_EXTEND, +} GrdVncScreenShareMode; + +typedef enum +{ + GRD_RUNTIME_MODE_SCREEN_SHARE, + GRD_RUNTIME_MODE_HEADLESS, + GRD_RUNTIME_MODE_SYSTEM, + GRD_RUNTIME_MODE_HANDOVER, +} GrdRuntimeMode; + +typedef enum +{ + GRD_RDP_AUTH_METHOD_CREDENTIALS = 1 << 0, + GRD_RDP_AUTH_METHOD_KERBEROS = 1 << 1, +} GrdRdpAuthMethods; diff --git a/grd-frame-clock.c b/grd-frame-clock.c new file mode 100644 index 0000000..10bd34a --- /dev/null +++ b/grd-frame-clock.c @@ -0,0 +1,229 @@ +/* + * Copyright (C) 2025 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-frame-clock.h" + +#include +#include +#include + +#include "grd-utils.h" + +struct _GrdFrameClock +{ + GObject parent; + + GrdFrameClockCallback on_trigger; + gpointer on_trigger_user_data; + + GSource *timer_source; + int timer_fd; + + uint64_t start_time_ns; + uint64_t interval_ns; + gboolean armed; +}; + +G_DEFINE_TYPE (GrdFrameClock, grd_frame_clock, + G_TYPE_OBJECT) + +gboolean +grd_frame_clock_is_armed (GrdFrameClock *frame_clock) +{ + return frame_clock->armed; +} + +static inline uint64_t +timespec_to_ns (struct timespec *spec) +{ + return s2ns (spec->tv_sec) + spec->tv_nsec; +} + +static uint64_t +extrapolate_next_interval_boundary (uint64_t boundary_ns, + uint64_t reference_ns, + uint64_t interval_ns) +{ + uint64_t num_intervals; + + num_intervals = MAX ((reference_ns - boundary_ns + interval_ns - 1) / + interval_ns, 0); + return boundary_ns + num_intervals * interval_ns; +} + +static void +schedule_next_frame (GrdFrameClock *frame_clock) +{ + struct itimerspec timer_spec = {}; + struct timespec now = {}; + uint64_t next_dispatch_time_ns; + + clock_gettime (CLOCK_MONOTONIC, &now); + + next_dispatch_time_ns = + extrapolate_next_interval_boundary (frame_clock->start_time_ns, + timespec_to_ns (&now), + frame_clock->interval_ns); + + timer_spec.it_value.tv_sec = next_dispatch_time_ns / s2ns (1); + timer_spec.it_value.tv_nsec = next_dispatch_time_ns % s2ns (1); + + timerfd_settime (frame_clock->timer_fd, TFD_TIMER_ABSTIME, &timer_spec, NULL); +} + +void +grd_frame_clock_arm_timer (GrdFrameClock *frame_clock, + uint64_t clock_rate_num, + uint64_t clock_rate_denom) +{ + struct timespec start_time = {}; + + clock_gettime (CLOCK_MONOTONIC, &start_time); + frame_clock->start_time_ns = timespec_to_ns (&start_time); + frame_clock->interval_ns = s2ns (clock_rate_denom) / clock_rate_num; + + schedule_next_frame (frame_clock); + frame_clock->armed = TRUE; +} + +void +grd_frame_clock_disarm_timer (GrdFrameClock *frame_clock) +{ + struct itimerspec timer_spec = {}; + + timerfd_settime (frame_clock->timer_fd, 0, &timer_spec, NULL); + frame_clock->armed = FALSE; +} + +static gboolean +timer_source_prepare (GSource *source, + int *timeout) +{ + *timeout = -1; + + return FALSE; +} + +static gboolean +timer_source_dispatch (GSource *source, + GSourceFunc callback, + gpointer user_data) +{ + g_source_set_ready_time (source, -1); + + return callback (user_data); +} + +static GSourceFuncs timer_source_funcs = +{ + .prepare = timer_source_prepare, + .dispatch = timer_source_dispatch, +}; + +static gboolean +handle_triggered_timer (gpointer user_data) +{ + GrdFrameClock *frame_clock = user_data; + uint64_t expirations; + ssize_t ret; + + ret = read (frame_clock->timer_fd, &expirations, sizeof (uint64_t)); + if (ret == -1) + { + g_warning ("Failed to read from timerfd: %s", g_strerror (errno)); + return G_SOURCE_CONTINUE; + } + else if (ret != sizeof (expirations)) + { + g_warning ("Failed to read from timerfd: unexpected size %zi", ret); + return G_SOURCE_CONTINUE; + } + + frame_clock->on_trigger (frame_clock->on_trigger_user_data); + + g_assert (frame_clock->armed); + schedule_next_frame (frame_clock); + + return G_SOURCE_CONTINUE; +} + +GrdFrameClock * +grd_frame_clock_new (GMainContext *main_context, + GrdFrameClockCallback on_trigger, + gpointer on_trigger_user_data, + GError **error) +{ + g_autoptr (GrdFrameClock) frame_clock = NULL; + + frame_clock = g_object_new (GRD_TYPE_FRAME_CLOCK, NULL); + frame_clock->on_trigger = on_trigger; + frame_clock->on_trigger_user_data = on_trigger_user_data; + + frame_clock->timer_fd = timerfd_create (CLOCK_MONOTONIC, + TFD_NONBLOCK | TFD_CLOEXEC); + if (frame_clock->timer_fd == -1) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create timerfd: %s", g_strerror (errno)); + return NULL; + } + + frame_clock->timer_source = g_source_new (&timer_source_funcs, + sizeof (GSource)); + g_source_set_callback (frame_clock->timer_source, handle_triggered_timer, + frame_clock, NULL); + g_source_set_ready_time (frame_clock->timer_source, -1); + g_source_add_unix_fd (frame_clock->timer_source, + frame_clock->timer_fd, G_IO_IN); + g_source_attach (frame_clock->timer_source, main_context); + + return g_steal_pointer (&frame_clock); +} + +static void +grd_frame_clock_dispose (GObject *object) +{ + GrdFrameClock *frame_clock = GRD_FRAME_CLOCK (object); + + if (frame_clock->timer_source) + { + g_source_destroy (frame_clock->timer_source); + g_clear_pointer (&frame_clock->timer_source, g_source_unref); + } + + g_clear_fd (&frame_clock->timer_fd, NULL); + + G_OBJECT_CLASS (grd_frame_clock_parent_class)->dispose (object); +} + +static void +grd_frame_clock_init (GrdFrameClock *frame_clock) +{ + frame_clock->timer_fd = -1; +} + +static void +grd_frame_clock_class_init (GrdFrameClockClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = grd_frame_clock_dispose; +} diff --git a/grd-frame-clock.h b/grd-frame-clock.h new file mode 100644 index 0000000..9b60c01 --- /dev/null +++ b/grd-frame-clock.h @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2025 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include +#include + +#define GRD_TYPE_FRAME_CLOCK (grd_frame_clock_get_type ()) +G_DECLARE_FINAL_TYPE (GrdFrameClock, grd_frame_clock, + GRD, FRAME_CLOCK, GObject) + +typedef void (* GrdFrameClockCallback) (gpointer user_data); + +GrdFrameClock *grd_frame_clock_new (GMainContext *main_context, + GrdFrameClockCallback on_trigger, + gpointer on_trigger_user_data, + GError **error); + +gboolean grd_frame_clock_is_armed (GrdFrameClock *frame_clock); + +void grd_frame_clock_arm_timer (GrdFrameClock *frame_clock, + uint64_t clock_rate_num, + uint64_t clock_rate_denom); + +void grd_frame_clock_disarm_timer (GrdFrameClock *frame_clock); diff --git a/grd-hwaccel-nvidia.c b/grd-hwaccel-nvidia.c new file mode 100644 index 0000000..b4640e0 --- /dev/null +++ b/grd-hwaccel-nvidia.c @@ -0,0 +1,1017 @@ +/* + * Copyright (C) 2021 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-hwaccel-nvidia.h" + +#include + +#include "grd-egl-thread.h" +#include "grd-utils.h" + +#define MAX_CUDA_DEVICES_FOR_RETRIEVAL 32 + +typedef CUresult CUDAAPI tcuGraphicsGLRegisterBuffer (CUgraphicsResource *resource, + GLuint buffer, + unsigned int flags); +typedef CUresult CUDAAPI tcuGraphicsResourceGetMappedPointer_v2 (CUdeviceptr *dev_ptr, + size_t *size, + CUgraphicsResource resource); + +typedef struct _ExtraCudaFunctions +{ + tcuGraphicsGLRegisterBuffer *cuGraphicsGLRegisterBuffer; + tcuGraphicsResourceGetMappedPointer_v2 *cuGraphicsResourceGetMappedPointer; +} ExtraCudaFunctions; + +typedef struct _DevRetrievalData +{ + GrdHwAccelNvidia *hwaccel_nvidia; + GrdSyncPoint sync_point; + + unsigned int n_devices; + CUdevice *devices; +} DevRetrievalData; + +typedef struct _NvEncEncodeSession +{ + GrdHwAccelNvidia *hwaccel_nvidia; + + void *encoder; + + uint32_t encode_width; + uint32_t encode_height; + + NV_ENC_OUTPUT_PTR buffer_out; + + NV_ENC_REGISTERED_PTR registered_resource; + NV_ENC_INPUT_PTR mapped_resource; +} NvEncEncodeSession; + +struct _GrdHwAccelNvidia +{ + GObject parent; + + GrdEglThread *egl_thread; + + CudaFunctions *cuda_funcs; + NvencFunctions *nvenc_funcs; + NV_ENCODE_API_FUNCTION_LIST nvenc_api; + + void *cuda_lib; + ExtraCudaFunctions *extra_cuda_funcs; + + CUdevice cu_device; + CUcontext cu_context; + gboolean initialized; + + CUmodule cu_module_dmg_utils; + CUfunction cu_chk_dmg_pxl; + CUfunction cu_cmb_dmg_arr_cols; + CUfunction cu_cmb_dmg_arr_rows; + CUfunction cu_simplify_dmg_arr; + + CUmodule cu_module_avc_utils; + CUfunction cu_bgrx_to_yuv420; + + GHashTable *encode_sessions; + + uint32_t next_encode_session_id; +}; + +G_DEFINE_TYPE (GrdHwAccelNvidia, grd_hwaccel_nvidia, G_TYPE_OBJECT) + +static void +nvenc_encode_session_free (NvEncEncodeSession *encode_session); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (NvEncEncodeSession, nvenc_encode_session_free) + +static NvEncEncodeSession * +nvenc_encode_session_new (GrdHwAccelNvidia *hwaccel_nvidia, + uint32_t encode_width, + uint32_t encode_height) +{ + NvEncEncodeSession *encode_session; + + encode_session = g_new0 (NvEncEncodeSession, 1); + encode_session->hwaccel_nvidia = hwaccel_nvidia; + encode_session->encode_width = encode_width; + encode_session->encode_height = encode_height; + + return encode_session; +} + +static void +nvenc_encode_session_free (NvEncEncodeSession *encode_session) +{ + GrdHwAccelNvidia *hwaccel_nvidia = encode_session->hwaccel_nvidia; + NV_ENCODE_API_FUNCTION_LIST *nvenc_api = &hwaccel_nvidia->nvenc_api; + + if (encode_session->mapped_resource) + { + nvenc_api->nvEncUnmapInputResource (encode_session->encoder, + encode_session->mapped_resource); + } + if (encode_session->registered_resource) + { + nvenc_api->nvEncUnregisterResource (encode_session->encoder, + encode_session->registered_resource); + } + + if (encode_session->buffer_out) + { + nvenc_api->nvEncDestroyBitstreamBuffer (encode_session->encoder, + encode_session->buffer_out); + } + + if (encode_session->encoder) + { + NV_ENC_PIC_PARAMS pic_params = {}; + + pic_params.encodePicFlags = NV_ENC_PIC_FLAG_EOS; + nvenc_api->nvEncEncodePicture (encode_session->encoder, &pic_params); + + nvenc_api->nvEncDestroyEncoder (encode_session->encoder); + } + + g_free (encode_session); +} + +void +grd_hwaccel_nvidia_get_cuda_functions (GrdHwAccelNvidia *hwaccel_nvidia, + gpointer *cuda_funcs) +{ + *cuda_funcs = hwaccel_nvidia->cuda_funcs; +} + +void +grd_hwaccel_nvidia_get_cuda_damage_kernels (GrdHwAccelNvidia *hwaccel_nvidia, + CUfunction *cu_chk_dmg_pxl, + CUfunction *cu_cmb_dmg_arr_cols, + CUfunction *cu_cmb_dmg_arr_rows, + CUfunction *cu_simplify_dmg_arr) +{ + *cu_chk_dmg_pxl = hwaccel_nvidia->cu_chk_dmg_pxl; + *cu_cmb_dmg_arr_cols = hwaccel_nvidia->cu_cmb_dmg_arr_cols; + *cu_cmb_dmg_arr_rows = hwaccel_nvidia->cu_cmb_dmg_arr_rows; + *cu_simplify_dmg_arr = hwaccel_nvidia->cu_simplify_dmg_arr; +} + +static const char * +get_cuda_error_string (GrdHwAccelNvidia *hwaccel_nvidia, + CUresult cu_result) +{ + CudaFunctions *cuda_funcs = hwaccel_nvidia->cuda_funcs; + const char *error_string = NULL; + CUresult local_cu_result; + + g_assert (cuda_funcs); + g_assert (cuda_funcs->cuGetErrorString); + + local_cu_result = cuda_funcs->cuGetErrorString (cu_result, &error_string); + if (G_UNLIKELY (local_cu_result != CUDA_SUCCESS)) + g_warning ("[HWAccel.CUDA] cuGetErrorString() failed: %i", local_cu_result); + + return error_string; +} + +void +grd_hwaccel_nvidia_push_cuda_context (GrdHwAccelNvidia *hwaccel_nvidia) +{ + CudaFunctions *cuda_funcs = hwaccel_nvidia->cuda_funcs; + CUresult cu_result; + + cu_result = cuda_funcs->cuCtxPushCurrent (hwaccel_nvidia->cu_context); + if (cu_result != CUDA_SUCCESS) + { + g_error ("[HWAccel.CUDA] Failed to push CUDA context: %s", + get_cuda_error_string (hwaccel_nvidia, cu_result)); + } +} + +void +grd_hwaccel_nvidia_pop_cuda_context (GrdHwAccelNvidia *hwaccel_nvidia) +{ + CUcontext cu_context; + + hwaccel_nvidia->cuda_funcs->cuCtxPopCurrent (&cu_context); +} + +gboolean +grd_hwaccel_nvidia_register_read_only_gl_buffer (GrdHwAccelNvidia *hwaccel_nvidia, + CUgraphicsResource *cuda_resource, + uint32_t buffer) +{ + ExtraCudaFunctions *extra_cuda_funcs = hwaccel_nvidia->extra_cuda_funcs; + CUresult cu_result; + + cu_result = + extra_cuda_funcs->cuGraphicsGLRegisterBuffer (cuda_resource, buffer, + CU_GRAPHICS_REGISTER_FLAGS_READ_ONLY); + if (cu_result != CUDA_SUCCESS) + { + g_warning ("[HWAccel.CUDA] Failed to register GL buffer: %s", + get_cuda_error_string (hwaccel_nvidia, cu_result)); + return FALSE; + } + + return TRUE; +} + +void +grd_hwaccel_nvidia_unregister_cuda_resource (GrdHwAccelNvidia *hwaccel_nvidia, + CUgraphicsResource cuda_resource, + CUstream cuda_stream) +{ + hwaccel_nvidia->cuda_funcs->cuStreamSynchronize (cuda_stream); + hwaccel_nvidia->cuda_funcs->cuGraphicsUnregisterResource (cuda_resource); +} + +gboolean +grd_hwaccel_nvidia_map_cuda_resource (GrdHwAccelNvidia *hwaccel_nvidia, + CUgraphicsResource cuda_resource, + CUdeviceptr *dev_ptr, + size_t *size, + CUstream cuda_stream) +{ + CudaFunctions *cuda_funcs = hwaccel_nvidia->cuda_funcs; + ExtraCudaFunctions *extra_cuda_funcs = hwaccel_nvidia->extra_cuda_funcs; + CUresult cu_result; + + cu_result = cuda_funcs->cuGraphicsMapResources (1, &cuda_resource, + cuda_stream); + if (cu_result != CUDA_SUCCESS) + { + g_warning ("[HWAccel.CUDA] Failed to map resources: %s", + get_cuda_error_string (hwaccel_nvidia, cu_result)); + return FALSE; + } + cu_result = extra_cuda_funcs->cuGraphicsResourceGetMappedPointer (dev_ptr, size, + cuda_resource); + if (cu_result != CUDA_SUCCESS) + { + g_warning ("[HWAccel.CUDA] Failed to get mapped pointer: %s", + get_cuda_error_string (hwaccel_nvidia, cu_result)); + cuda_funcs->cuGraphicsUnmapResources (1, &cuda_resource, cuda_stream); + return FALSE; + } + + return TRUE; +} + +void +grd_hwaccel_nvidia_unmap_cuda_resource (GrdHwAccelNvidia *hwaccel_nvidia, + CUgraphicsResource cuda_resource, + CUstream cuda_stream) +{ + hwaccel_nvidia->cuda_funcs->cuGraphicsUnmapResources (1, &cuda_resource, + cuda_stream); +} + +gboolean +grd_hwaccel_nvidia_create_cuda_stream (GrdHwAccelNvidia *hwaccel_nvidia, + CUstream *cuda_stream) +{ + CudaFunctions *cuda_funcs = hwaccel_nvidia->cuda_funcs; + CUresult cu_result; + + cu_result = cuda_funcs->cuStreamCreate (cuda_stream, CU_STREAM_NON_BLOCKING); + if (cu_result != CUDA_SUCCESS) + { + g_warning ("[HWAccel.CUDA] Failed to create stream: %s", + get_cuda_error_string (hwaccel_nvidia, cu_result)); + return FALSE; + } + + return TRUE; +} + +void +grd_hwaccel_nvidia_destroy_cuda_stream (GrdHwAccelNvidia *hwaccel_nvidia, + CUstream cuda_stream) +{ + hwaccel_nvidia->cuda_funcs->cuStreamSynchronize (cuda_stream); + hwaccel_nvidia->cuda_funcs->cuStreamDestroy (cuda_stream); +} + +gboolean +grd_hwaccel_nvidia_alloc_mem (GrdHwAccelNvidia *hwaccel_nvidia, + CUdeviceptr *device_ptr, + size_t size) +{ + CudaFunctions *cuda_funcs = hwaccel_nvidia->cuda_funcs; + CUresult cu_result; + + cu_result = cuda_funcs->cuMemAlloc (device_ptr, size); + if (cu_result != CUDA_SUCCESS) + { + g_warning ("[HWAccel.CUDA] Failed to allocate memory: %s", + get_cuda_error_string (hwaccel_nvidia, cu_result)); + return FALSE; + } + + return TRUE; +} + +void +grd_hwaccel_nvidia_clear_mem_ptr (GrdHwAccelNvidia *hwaccel_nvidia, + CUdeviceptr *device_ptr) +{ + if (!(*device_ptr)) + return; + + hwaccel_nvidia->cuda_funcs->cuMemFree (*device_ptr); + *device_ptr = 0; +} + +static uint32_t +get_next_free_encode_session_id (GrdHwAccelNvidia *hwaccel_nvidia) +{ + uint32_t encode_session_id = hwaccel_nvidia->next_encode_session_id; + + while (g_hash_table_contains (hwaccel_nvidia->encode_sessions, + GUINT_TO_POINTER (encode_session_id))) + ++encode_session_id; + + hwaccel_nvidia->next_encode_session_id = encode_session_id + 1; + + return encode_session_id; +} + +static const char * +get_last_nvenc_error_string (GrdHwAccelNvidia *hwaccel_nvidia, + void *encoder) +{ + NV_ENCODE_API_FUNCTION_LIST *nvenc_api = &hwaccel_nvidia->nvenc_api; + + g_assert (nvenc_api); + g_assert (nvenc_api->nvEncGetLastErrorString); + + return nvenc_api->nvEncGetLastErrorString (encoder); +} + +gboolean +grd_hwaccel_nvidia_create_nvenc_session (GrdHwAccelNvidia *hwaccel_nvidia, + uint32_t *encode_session_id, + uint16_t surface_width, + uint16_t surface_height, + uint16_t *aligned_width, + uint16_t *aligned_height, + uint16_t refresh_rate) +{ + g_autoptr (NvEncEncodeSession) encode_session = NULL; + NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS open_params = {0}; + NV_ENC_INITIALIZE_PARAMS init_params = {0}; + NV_ENC_CONFIG encode_config = {0}; + NV_ENC_CREATE_BITSTREAM_BUFFER create_bitstream_buffer = {0}; + + *aligned_width = grd_get_aligned_size (surface_width, 16); + *aligned_height = grd_get_aligned_size (surface_height, 64); + + *encode_session_id = get_next_free_encode_session_id (hwaccel_nvidia); + encode_session = nvenc_encode_session_new (hwaccel_nvidia, + *aligned_width, *aligned_height); + + open_params.version = NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS_VER; + open_params.deviceType = NV_ENC_DEVICE_TYPE_CUDA; + open_params.device = hwaccel_nvidia->cu_context; + open_params.apiVersion = NVENCAPI_VERSION; + + if (hwaccel_nvidia->nvenc_api.nvEncOpenEncodeSessionEx ( + &open_params, &encode_session->encoder) != NV_ENC_SUCCESS) + { + g_debug ("[HWAccel.NVENC] Failed to open encode session: %s", + get_last_nvenc_error_string (hwaccel_nvidia, encode_session->encoder)); + return FALSE; + } + + encode_config.version = NV_ENC_CONFIG_VER; + encode_config.profileGUID = NV_ENC_H264_PROFILE_PROGRESSIVE_HIGH_GUID; + encode_config.gopLength = NVENC_INFINITE_GOPLENGTH; + encode_config.frameIntervalP = 1; + encode_config.frameFieldMode = NV_ENC_PARAMS_FRAME_FIELD_MODE_FRAME; + encode_config.mvPrecision = NV_ENC_MV_PRECISION_QUARTER_PEL; + encode_config.rcParams.version = NV_ENC_RC_PARAMS_VER; + encode_config.rcParams.rateControlMode = NV_ENC_PARAMS_RC_VBR; + encode_config.rcParams.averageBitRate = 0; + encode_config.rcParams.maxBitRate = 0; + encode_config.rcParams.targetQuality = 22; + encode_config.encodeCodecConfig.h264Config.idrPeriod = NVENC_INFINITE_GOPLENGTH; + encode_config.encodeCodecConfig.h264Config.chromaFormatIDC = 1; + + init_params.version = NV_ENC_INITIALIZE_PARAMS_VER; + init_params.encodeGUID = NV_ENC_CODEC_H264_GUID; + init_params.encodeWidth = *aligned_width; + init_params.encodeHeight = *aligned_height; + init_params.darWidth = surface_width; + init_params.darHeight = surface_height; + init_params.frameRateNum = refresh_rate; + init_params.frameRateDen = 1; + init_params.enablePTD = 1; + init_params.encodeConfig = &encode_config; + if (hwaccel_nvidia->nvenc_api.nvEncInitializeEncoder ( + encode_session->encoder, &init_params) != NV_ENC_SUCCESS) + { + g_warning ("[HWAccel.NVENC] Failed to initialize encoder: %s", + get_last_nvenc_error_string (hwaccel_nvidia, encode_session->encoder)); + return FALSE; + } + + create_bitstream_buffer.version = NV_ENC_CREATE_BITSTREAM_BUFFER_VER; + if (hwaccel_nvidia->nvenc_api.nvEncCreateBitstreamBuffer ( + encode_session->encoder, &create_bitstream_buffer) != NV_ENC_SUCCESS) + { + g_warning ("[HWAccel.NVENC] Failed to create bitstream buffer: %s", + get_last_nvenc_error_string (hwaccel_nvidia, encode_session->encoder)); + return FALSE; + } + encode_session->buffer_out = create_bitstream_buffer.bitstreamBuffer; + + g_hash_table_insert (hwaccel_nvidia->encode_sessions, + GUINT_TO_POINTER (*encode_session_id), + g_steal_pointer (&encode_session)); + + return TRUE; +} + +void +grd_hwaccel_nvidia_free_nvenc_session (GrdHwAccelNvidia *hwaccel_nvidia, + uint32_t encode_session_id) +{ + NvEncEncodeSession *encode_session = NULL; + + if (!g_hash_table_steal_extended (hwaccel_nvidia->encode_sessions, + GUINT_TO_POINTER (encode_session_id), + NULL, (gpointer *) &encode_session)) + g_assert_not_reached (); + + g_assert (encode_session); + nvenc_encode_session_free (encode_session); +} + +gboolean +grd_hwaccel_nvidia_avc420_encode_bgrx_frame (GrdHwAccelNvidia *hwaccel_nvidia, + uint32_t encode_session_id, + CUdeviceptr src_data, + CUdeviceptr *main_view_nv12, + uint16_t src_width, + uint16_t src_height, + uint16_t aligned_width, + uint16_t aligned_height, + CUstream cuda_stream) +{ + CudaFunctions *cuda_funcs = hwaccel_nvidia->cuda_funcs; + NvEncEncodeSession *encode_session; + NV_ENC_REGISTER_RESOURCE register_res = {0}; + NV_ENC_MAP_INPUT_RESOURCE map_input_res = {0}; + NV_ENC_PIC_PARAMS pic_params = {0}; + unsigned int grid_dim_x, grid_dim_y, grid_dim_z; + unsigned int block_dim_x, block_dim_y, block_dim_z; + void *args[7]; + CUresult cu_result; + + if (!g_hash_table_lookup_extended (hwaccel_nvidia->encode_sessions, + GUINT_TO_POINTER (encode_session_id), + NULL, (gpointer *) &encode_session)) + return FALSE; + + g_assert (encode_session->encode_width == aligned_width); + g_assert (encode_session->encode_height == aligned_height); + + g_assert (encode_session->mapped_resource == NULL); + g_assert (encode_session->registered_resource == NULL); + + if (!(*main_view_nv12) && + !grd_hwaccel_nvidia_alloc_mem (hwaccel_nvidia, main_view_nv12, + aligned_width * (aligned_height + aligned_height / 2))) + return FALSE; + + /* Threads per blocks */ + block_dim_x = 16; + block_dim_y = 16; + block_dim_z = 1; + /* Amount of blocks per grid */ + grid_dim_x = aligned_width / 2 / block_dim_x + + (aligned_width / 2 % block_dim_x ? 1 : 0); + grid_dim_y = aligned_height / 2 / block_dim_y + + (aligned_height / 2 % block_dim_y ? 1 : 0); + grid_dim_z = 1; + + args[0] = main_view_nv12; + args[1] = &src_data; + args[2] = &src_width; + args[3] = &src_height; + args[4] = &aligned_width; + args[5] = &aligned_height; + args[6] = &aligned_width; + + cu_result = cuda_funcs->cuLaunchKernel (hwaccel_nvidia->cu_bgrx_to_yuv420, + grid_dim_x, grid_dim_y, grid_dim_z, + block_dim_x, block_dim_y, block_dim_z, + 0, cuda_stream, args, NULL); + if (cu_result != CUDA_SUCCESS) + { + g_warning ("[HWAccel.CUDA] Failed to launch BGRX_TO_YUV420 kernel: %s", + get_cuda_error_string (hwaccel_nvidia, cu_result)); + return FALSE; + } + + cu_result = cuda_funcs->cuStreamSynchronize (cuda_stream); + if (cu_result != CUDA_SUCCESS) + { + g_warning ("[HWAccel.CUDA] Failed to synchronize stream: %s", + get_cuda_error_string (hwaccel_nvidia, cu_result)); + return FALSE; + } + + register_res.version = NV_ENC_REGISTER_RESOURCE_VER; + register_res.resourceType = NV_ENC_INPUT_RESOURCE_TYPE_CUDADEVICEPTR; + register_res.width = aligned_width; + register_res.height = aligned_height; + register_res.pitch = aligned_width; + register_res.resourceToRegister = (void *) *main_view_nv12; + register_res.bufferFormat = NV_ENC_BUFFER_FORMAT_NV12; + register_res.bufferUsage = NV_ENC_INPUT_IMAGE; + + if (hwaccel_nvidia->nvenc_api.nvEncRegisterResource ( + encode_session->encoder, ®ister_res) != NV_ENC_SUCCESS) + { + g_warning ("[HWAccel.NVENC] Failed to register resource: %s", + get_last_nvenc_error_string (hwaccel_nvidia, encode_session->encoder)); + return FALSE; + } + + map_input_res.version = NV_ENC_MAP_INPUT_RESOURCE_VER; + map_input_res.registeredResource = register_res.registeredResource; + + if (hwaccel_nvidia->nvenc_api.nvEncMapInputResource ( + encode_session->encoder, &map_input_res) != NV_ENC_SUCCESS) + { + g_warning ("[HWAccel.NVENC] Failed to map input resource: %s", + get_last_nvenc_error_string (hwaccel_nvidia, encode_session->encoder)); + hwaccel_nvidia->nvenc_api.nvEncUnregisterResource (encode_session->encoder, + register_res.registeredResource); + return FALSE; + } + + pic_params.version = NV_ENC_PIC_PARAMS_VER; + pic_params.inputWidth = aligned_width; + pic_params.inputHeight = aligned_height; + pic_params.inputPitch = aligned_width; + pic_params.inputBuffer = map_input_res.mappedResource; + pic_params.outputBitstream = encode_session->buffer_out; + pic_params.bufferFmt = map_input_res.mappedBufferFmt; + pic_params.pictureStruct = NV_ENC_PIC_STRUCT_FRAME; + + if (hwaccel_nvidia->nvenc_api.nvEncEncodePicture ( + encode_session->encoder, &pic_params) != NV_ENC_SUCCESS) + { + g_warning ("[HWAccel.NVENC] Failed to encode frame: %s", + get_last_nvenc_error_string (hwaccel_nvidia, encode_session->encoder)); + hwaccel_nvidia->nvenc_api.nvEncUnmapInputResource (encode_session->encoder, + map_input_res.mappedResource); + hwaccel_nvidia->nvenc_api.nvEncUnregisterResource (encode_session->encoder, + register_res.registeredResource); + return FALSE; + } + + encode_session->mapped_resource = map_input_res.mappedResource; + encode_session->registered_resource = register_res.registeredResource; + + return TRUE; +} + +gboolean +grd_hwaccel_nvidia_avc420_retrieve_bitstream (GrdHwAccelNvidia *hwaccel_nvidia, + uint32_t encode_session_id, + uint8_t **bitstream, + uint32_t *bitstream_size) +{ + NV_ENCODE_API_FUNCTION_LIST *nvenc_api = &hwaccel_nvidia->nvenc_api; + NvEncEncodeSession *encode_session; + NV_ENC_LOCK_BITSTREAM lock_bitstream = {0}; + gboolean success = FALSE; + + if (!g_hash_table_lookup_extended (hwaccel_nvidia->encode_sessions, + GUINT_TO_POINTER (encode_session_id), + NULL, (gpointer *) &encode_session)) + g_assert_not_reached (); + + g_assert (encode_session->mapped_resource != NULL); + g_assert (encode_session->registered_resource != NULL); + + lock_bitstream.version = NV_ENC_LOCK_BITSTREAM_VER; + lock_bitstream.outputBitstream = encode_session->buffer_out; + + if (nvenc_api->nvEncLockBitstream (encode_session->encoder, + &lock_bitstream) != NV_ENC_SUCCESS) + { + g_warning ("[HWAccel.NVENC] Failed to lock bitstream: %s", + get_last_nvenc_error_string (hwaccel_nvidia, encode_session->encoder)); + goto out; + } + + if (bitstream_size) + *bitstream_size = lock_bitstream.bitstreamSizeInBytes; + if (bitstream) + *bitstream = g_memdup2 (lock_bitstream.bitstreamBufferPtr, *bitstream_size); + + nvenc_api->nvEncUnlockBitstream (encode_session->encoder, + lock_bitstream.outputBitstream); + + success = TRUE; + +out: + nvenc_api->nvEncUnmapInputResource (encode_session->encoder, + encode_session->mapped_resource); + nvenc_api->nvEncUnregisterResource (encode_session->encoder, + encode_session->registered_resource); + encode_session->mapped_resource = NULL; + encode_session->registered_resource = NULL; + + return success; +} + +static gboolean +load_extra_cuda_functions (GrdHwAccelNvidia *hwaccel_nvidia) +{ + ExtraCudaFunctions *extra_cuda_funcs; + + hwaccel_nvidia->cuda_lib = dlopen ("libcuda.so.1", RTLD_LAZY); + if (!hwaccel_nvidia->cuda_lib) + return FALSE; + + hwaccel_nvidia->extra_cuda_funcs = g_malloc0 (sizeof (ExtraCudaFunctions)); + + extra_cuda_funcs = hwaccel_nvidia->extra_cuda_funcs; + extra_cuda_funcs->cuGraphicsGLRegisterBuffer = + dlsym (hwaccel_nvidia->cuda_lib, "cuGraphicsGLRegisterBuffer"); + if (!extra_cuda_funcs->cuGraphicsGLRegisterBuffer) + return FALSE; + + extra_cuda_funcs->cuGraphicsResourceGetMappedPointer = + dlsym (hwaccel_nvidia->cuda_lib, "cuGraphicsResourceGetMappedPointer_v2"); + if (!extra_cuda_funcs->cuGraphicsGLRegisterBuffer) + return FALSE; + + return TRUE; +} + +static gboolean +get_cuda_devices_in_impl (gpointer user_data) +{ + DevRetrievalData *data = user_data; + GrdHwAccelNvidia *hwaccel_nvidia = data->hwaccel_nvidia; + CudaFunctions *cuda_funcs = hwaccel_nvidia->cuda_funcs; + + return cuda_funcs->cuGLGetDevices (&data->n_devices, data->devices, + MAX_CUDA_DEVICES_FOR_RETRIEVAL, + CU_GL_DEVICE_LIST_ALL) == CUDA_SUCCESS; +} + +static void +compute_devices_ready (gboolean success, + gpointer user_data) +{ + GrdSyncPoint *sync_point = user_data; + + grd_sync_point_complete (sync_point, success); +} + +static gboolean +get_cuda_devices_from_gl_context (GrdHwAccelNvidia *hwaccel_nvidia, + GrdEglThread *egl_thread, + unsigned int *n_returned_devices, + CUdevice *device_array) +{ + DevRetrievalData data = {}; + gboolean success; + + grd_sync_point_init (&data.sync_point); + data.hwaccel_nvidia = hwaccel_nvidia; + data.devices = device_array; + + grd_egl_thread_run_custom_task (egl_thread, + get_cuda_devices_in_impl, + &data, + compute_devices_ready, + &data.sync_point, + NULL); + + success = grd_sync_point_wait_for_completion (&data.sync_point); + grd_sync_point_clear (&data.sync_point); + + *n_returned_devices = data.n_devices; + + return success; +} + +static gboolean +push_cuda_context_in_egl_thread (gpointer user_data) +{ + GrdHwAccelNvidia *hwaccel_nvidia = user_data; + + grd_hwaccel_nvidia_push_cuda_context (hwaccel_nvidia); + + return TRUE; +} + +static gboolean +pop_cuda_context_in_egl_thread (gpointer user_data) +{ + GrdHwAccelNvidia *hwaccel_nvidia = user_data; + + grd_hwaccel_nvidia_pop_cuda_context (hwaccel_nvidia); + + return TRUE; +} + +static void +complete_sync (gboolean success, + gpointer user_data) +{ + GrdSyncPoint *sync_point = user_data; + + grd_sync_point_complete (sync_point, success); +} + +static void +run_function_in_egl_thread (GrdHwAccelNvidia *hwaccel_nvidia, + GrdEglThreadCustomFunc function) +{ + GrdSyncPoint sync_point = {}; + + grd_sync_point_init (&sync_point); + + grd_egl_thread_run_custom_task (hwaccel_nvidia->egl_thread, + function, + hwaccel_nvidia, + complete_sync, + &sync_point, + NULL); + + grd_sync_point_wait_for_completion (&sync_point); + grd_sync_point_clear (&sync_point); +} + +static gboolean +load_cuda_module (GrdHwAccelNvidia *hwaccel_nvidia, + CUmodule *module, + const char *name, + const char *ptx_instructions) +{ + CudaFunctions *cuda_funcs = hwaccel_nvidia->cuda_funcs; + CUresult cu_result; + + cu_result = cuda_funcs->cuModuleLoadData (module, ptx_instructions); + if (cu_result != CUDA_SUCCESS) + { + g_warning ("[HWAccel.CUDA] Failed to load %s module: %s", + name, get_cuda_error_string (hwaccel_nvidia, cu_result)); + return FALSE; + } + + return TRUE; +} + +static gboolean +load_cuda_function (GrdHwAccelNvidia *hwaccel_nvidia, + CUfunction *function, + CUmodule module, + const char *name) +{ + CudaFunctions *cuda_funcs = hwaccel_nvidia->cuda_funcs; + CUresult cu_result; + + cu_result = cuda_funcs->cuModuleGetFunction (function, module, name); + if (cu_result != CUDA_SUCCESS) + { + g_warning ("[HWAccel.CUDA] Failed to get kernel %s: %s", + name, get_cuda_error_string (hwaccel_nvidia, cu_result)); + return FALSE; + } + + return TRUE; +} + +GrdHwAccelNvidia * +grd_hwaccel_nvidia_new (GrdEglThread *egl_thread) +{ + g_autoptr (GrdHwAccelNvidia) hwaccel_nvidia = NULL; + gboolean cuda_device_found = FALSE; + CUdevice cu_devices[MAX_CUDA_DEVICES_FOR_RETRIEVAL] = {}; + CUdevice cu_device = 0; + unsigned int cu_device_count = 0; + CudaFunctions *cuda_funcs; + NvencFunctions *nvenc_funcs; + g_autofree char *dmg_ptx_path = NULL; + g_autofree char *dmg_ptx_instructions = NULL; + g_autofree char *avc_ptx_path = NULL; + g_autofree char *avc_ptx_instructions = NULL; + g_autoptr (GError) error = NULL; + CUresult cu_result; + unsigned int i; + + hwaccel_nvidia = g_object_new (GRD_TYPE_HWACCEL_NVIDIA, NULL); + hwaccel_nvidia->egl_thread = egl_thread; + + cuda_load_functions (&hwaccel_nvidia->cuda_funcs, NULL); + nvenc_load_functions (&hwaccel_nvidia->nvenc_funcs, NULL); + + if (!hwaccel_nvidia->cuda_funcs || !hwaccel_nvidia->nvenc_funcs) + { + g_debug ("[HWAccel.CUDA] Failed to load CUDA or NVENC library"); + return NULL; + } + if (!load_extra_cuda_functions (hwaccel_nvidia)) + { + g_warning ("[HWAccel.CUDA] Failed to load extra CUDA functions"); + return NULL; + } + + cuda_funcs = hwaccel_nvidia->cuda_funcs; + nvenc_funcs = hwaccel_nvidia->nvenc_funcs; + + cu_result = cuda_funcs->cuInit (0); + if (cu_result != CUDA_SUCCESS) + { + g_debug ("[HWAccel.CUDA] Failed to initialize CUDA: %s", + get_cuda_error_string (hwaccel_nvidia, cu_result)); + return NULL; + } + if (!get_cuda_devices_from_gl_context (hwaccel_nvidia, egl_thread, + &cu_device_count, cu_devices)) + { + g_message ("[HWAccel.CUDA] Unable to retrieve CUDA devices"); + return NULL; + } + + g_debug ("[HWAccel.CUDA] Retrieved %u CUDA device(s)", cu_device_count); + for (i = 0; i < cu_device_count; ++i) + { + int cc_major = 0, cc_minor = 0; + + cu_device = cu_devices[i]; + cu_result = + cuda_funcs->cuDeviceGetAttribute (&cc_major, + CU_DEVICE_ATTRIBUTE_COMPUTE_CAPABILITY_MAJOR, + cu_device); + if (cu_result != CUDA_SUCCESS) + { + g_warning ("[HWAccel.CUDA] Failed to get device attribute: %s", + get_cuda_error_string (hwaccel_nvidia, cu_result)); + continue; + } + cu_result = + cuda_funcs->cuDeviceGetAttribute (&cc_minor, + CU_DEVICE_ATTRIBUTE_COMPUTE_CAPABILITY_MINOR, + cu_device); + if (cu_result != CUDA_SUCCESS) + { + g_warning ("[HWAccel.CUDA] Failed to get device attribute: %s", + get_cuda_error_string (hwaccel_nvidia, cu_result)); + continue; + } + + g_debug ("[HWAccel.CUDA] Device %u compute capability: [%i, %i]", + i, cc_major, cc_minor); + if (cc_major >= 3) + { + g_debug ("[HWAccel.CUDA] Choosing CUDA device with id %u", i); + cuda_device_found = TRUE; + break; + } + } + + if (!cu_device_count || !cuda_device_found) + { + g_debug ("[HWAccel.CUDA] No appropriate CUDA capable gpu found"); + return NULL; + } + + hwaccel_nvidia->cu_device = cu_device; + cu_result = cuda_funcs->cuDevicePrimaryCtxRetain (&hwaccel_nvidia->cu_context, + hwaccel_nvidia->cu_device); + if (cu_result != CUDA_SUCCESS) + { + g_warning ("[HWAccel.CUDA] Failed to retain CUDA context: %s", + get_cuda_error_string (hwaccel_nvidia, cu_result)); + return NULL; + } + + hwaccel_nvidia->nvenc_api.version = NV_ENCODE_API_FUNCTION_LIST_VER; + if (nvenc_funcs->NvEncodeAPICreateInstance (&hwaccel_nvidia->nvenc_api) != NV_ENC_SUCCESS) + { + g_warning ("[HWAccel.NVENC] Could not create NVENC API instance"); + cuda_funcs->cuDevicePrimaryCtxRelease (hwaccel_nvidia->cu_device); + return NULL; + } + + cu_result = cuda_funcs->cuCtxPushCurrent (hwaccel_nvidia->cu_context); + if (cu_result != CUDA_SUCCESS) + { + g_warning ("[HWAccel.CUDA] Failed to push CUDA context: %s", + get_cuda_error_string (hwaccel_nvidia, cu_result)); + cuda_funcs->cuDevicePrimaryCtxRelease (hwaccel_nvidia->cu_device); + return NULL; + } + + run_function_in_egl_thread (hwaccel_nvidia, push_cuda_context_in_egl_thread); + + hwaccel_nvidia->initialized = TRUE; + + dmg_ptx_path = g_strdup_printf ("%s/grd-cuda-damage-utils_30.ptx", GRD_DATA_DIR); + avc_ptx_path = g_strdup_printf ("%s/grd-cuda-avc-utils_30.ptx", GRD_DATA_DIR); + + if (!g_file_get_contents (dmg_ptx_path, &dmg_ptx_instructions, NULL, &error) || + !g_file_get_contents (avc_ptx_path, &avc_ptx_instructions, NULL, &error)) + g_error ("[HWAccel.CUDA] Failed to read PTX instructions: %s", error->message); + + if (!load_cuda_module (hwaccel_nvidia, &hwaccel_nvidia->cu_module_dmg_utils, + "damage utils", dmg_ptx_instructions)) + return NULL; + + if (!load_cuda_function (hwaccel_nvidia, &hwaccel_nvidia->cu_chk_dmg_pxl, + hwaccel_nvidia->cu_module_dmg_utils, "check_damaged_pixel") || + !load_cuda_function (hwaccel_nvidia, &hwaccel_nvidia->cu_cmb_dmg_arr_cols, + hwaccel_nvidia->cu_module_dmg_utils, "combine_damage_array_cols") || + !load_cuda_function (hwaccel_nvidia, &hwaccel_nvidia->cu_cmb_dmg_arr_rows, + hwaccel_nvidia->cu_module_dmg_utils, "combine_damage_array_rows") || + !load_cuda_function (hwaccel_nvidia, &hwaccel_nvidia->cu_simplify_dmg_arr, + hwaccel_nvidia->cu_module_dmg_utils, "simplify_damage_array")) + return NULL; + + if (!load_cuda_module (hwaccel_nvidia, &hwaccel_nvidia->cu_module_avc_utils, + "AVC utils", avc_ptx_instructions)) + return NULL; + + if (!load_cuda_function (hwaccel_nvidia, &hwaccel_nvidia->cu_bgrx_to_yuv420, + hwaccel_nvidia->cu_module_avc_utils, "convert_2x2_bgrx_area_to_yuv420_nv12")) + return NULL; + + return g_steal_pointer (&hwaccel_nvidia); +} + +static void +grd_hwaccel_nvidia_dispose (GObject *object) +{ + GrdHwAccelNvidia *hwaccel_nvidia = GRD_HWACCEL_NVIDIA (object); + + g_clear_pointer (&hwaccel_nvidia->cu_module_avc_utils, + hwaccel_nvidia->cuda_funcs->cuModuleUnload); + g_clear_pointer (&hwaccel_nvidia->cu_module_dmg_utils, + hwaccel_nvidia->cuda_funcs->cuModuleUnload); + + if (hwaccel_nvidia->initialized) + { + run_function_in_egl_thread (hwaccel_nvidia, pop_cuda_context_in_egl_thread); + + hwaccel_nvidia->cuda_funcs->cuCtxPopCurrent (&hwaccel_nvidia->cu_context); + hwaccel_nvidia->cuda_funcs->cuDevicePrimaryCtxRelease (hwaccel_nvidia->cu_device); + + hwaccel_nvidia->initialized = FALSE; + } + + g_clear_pointer (&hwaccel_nvidia->cuda_lib, dlclose); + g_clear_pointer (&hwaccel_nvidia->extra_cuda_funcs, g_free); + + nvenc_free_functions (&hwaccel_nvidia->nvenc_funcs); + cuda_free_functions (&hwaccel_nvidia->cuda_funcs); + + g_assert (!hwaccel_nvidia->encode_sessions || + g_hash_table_size (hwaccel_nvidia->encode_sessions) == 0); + g_clear_pointer (&hwaccel_nvidia->encode_sessions, g_hash_table_destroy); + + G_OBJECT_CLASS (grd_hwaccel_nvidia_parent_class)->dispose (object); +} + +static void +grd_hwaccel_nvidia_init (GrdHwAccelNvidia *hwaccel_nvidia) +{ + hwaccel_nvidia->encode_sessions = g_hash_table_new (NULL, NULL); +} + +static void +grd_hwaccel_nvidia_class_init (GrdHwAccelNvidiaClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = grd_hwaccel_nvidia_dispose; +} diff --git a/grd-hwaccel-nvidia.h b/grd-hwaccel-nvidia.h new file mode 100644 index 0000000..1241b10 --- /dev/null +++ b/grd-hwaccel-nvidia.h @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2021 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include +#include +#include + +#include "grd-types.h" + +#define GRD_TYPE_HWACCEL_NVIDIA (grd_hwaccel_nvidia_get_type ()) +G_DECLARE_FINAL_TYPE (GrdHwAccelNvidia, grd_hwaccel_nvidia, + GRD, HWACCEL_NVIDIA, GObject) + +GrdHwAccelNvidia *grd_hwaccel_nvidia_new (GrdEglThread *egl_thread); + +void grd_hwaccel_nvidia_get_cuda_functions (GrdHwAccelNvidia *hwaccel_nvidia, + gpointer *cuda_funcs); + +void grd_hwaccel_nvidia_get_cuda_damage_kernels (GrdHwAccelNvidia *hwaccel_nvidia, + CUfunction *cu_chk_dmg_pxl, + CUfunction *cu_cmb_dmg_arr_cols, + CUfunction *cu_cmb_dmg_arr_rows, + CUfunction *cu_simplify_dmg_arr); + +void grd_hwaccel_nvidia_push_cuda_context (GrdHwAccelNvidia *hwaccel_nvidia); + +void grd_hwaccel_nvidia_pop_cuda_context (GrdHwAccelNvidia *hwaccel_nvidia); + +gboolean grd_hwaccel_nvidia_register_read_only_gl_buffer (GrdHwAccelNvidia *hwaccel_nvidia, + CUgraphicsResource *cuda_resource, + uint32_t buffer); + +void grd_hwaccel_nvidia_unregister_cuda_resource (GrdHwAccelNvidia *hwaccel_nvidia, + CUgraphicsResource cuda_resource, + CUstream cuda_stream); + +gboolean grd_hwaccel_nvidia_map_cuda_resource (GrdHwAccelNvidia *hwaccel_nvidia, + CUgraphicsResource cuda_resource, + CUdeviceptr *dev_ptr, + size_t *size, + CUstream cuda_stream); + +void grd_hwaccel_nvidia_unmap_cuda_resource (GrdHwAccelNvidia *hwaccel_nvidia, + CUgraphicsResource cuda_resource, + CUstream cuda_stream); + +gboolean grd_hwaccel_nvidia_create_cuda_stream (GrdHwAccelNvidia *hwaccel_nvidia, + CUstream *cuda_stream); + +void grd_hwaccel_nvidia_destroy_cuda_stream (GrdHwAccelNvidia *hwaccel_nvidia, + CUstream cuda_stream); + +gboolean grd_hwaccel_nvidia_alloc_mem (GrdHwAccelNvidia *hwaccel_nvidia, + CUdeviceptr *device_ptr, + size_t size); + +void grd_hwaccel_nvidia_clear_mem_ptr (GrdHwAccelNvidia *hwaccel_nvidia, + CUdeviceptr *device_ptr); + +gboolean grd_hwaccel_nvidia_create_nvenc_session (GrdHwAccelNvidia *hwaccel_nvidia, + uint32_t *encode_session_id, + uint16_t surface_width, + uint16_t surface_height, + uint16_t *aligned_width, + uint16_t *aligned_height, + uint16_t refresh_rate); + +void grd_hwaccel_nvidia_free_nvenc_session (GrdHwAccelNvidia *hwaccel_nvidia, + uint32_t encode_session_id); + +gboolean grd_hwaccel_nvidia_avc420_encode_bgrx_frame (GrdHwAccelNvidia *hwaccel_nvidia, + uint32_t encode_session_id, + CUdeviceptr src_data, + CUdeviceptr *main_view_nv12, + uint16_t src_width, + uint16_t src_height, + uint16_t aligned_width, + uint16_t aligned_height, + CUstream cuda_stream); + +gboolean grd_hwaccel_nvidia_avc420_retrieve_bitstream (GrdHwAccelNvidia *hwaccel_nvidia, + uint32_t encode_session_id, + uint8_t **bitstream, + uint32_t *bitstream_size); diff --git a/grd-hwaccel-vaapi.c b/grd-hwaccel-vaapi.c new file mode 100644 index 0000000..161b7b6 --- /dev/null +++ b/grd-hwaccel-vaapi.c @@ -0,0 +1,323 @@ +/* + * Copyright (C) 2022 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-hwaccel-vaapi.h" + +#include +#include +#include +#include +#include +#include + +#include "grd-encode-session-vaapi.h" +#include "grd-vk-device.h" + +struct _GrdHwAccelVaapi +{ + GObject parent; + + GrdVkDevice *vk_device; + + int drm_fd; + VADisplay va_display; + + gboolean supports_quality_level; +}; + +G_DEFINE_TYPE (GrdHwAccelVaapi, grd_hwaccel_vaapi, G_TYPE_OBJECT) + +GrdEncodeSession * +grd_hwaccel_vaapi_create_encode_session (GrdHwAccelVaapi *hwaccel_vaapi, + uint32_t source_width, + uint32_t source_height, + uint32_t refresh_rate, + GError **error) +{ + return (GrdEncodeSession *) grd_encode_session_vaapi_new (hwaccel_vaapi->vk_device, + hwaccel_vaapi->va_display, + hwaccel_vaapi->supports_quality_level, + source_width, source_height, + refresh_rate, error); +} + +static gboolean +initialize_vaapi (GrdHwAccelVaapi *hwaccel_vaapi, + int *version_major, + int *version_minor, + int64_t render_minor, + GError **error) +{ + g_autofree char *render_node = NULL; + VAStatus va_status; + + render_node = g_strdup_printf ("/dev/dri/renderD%" PRIi64, render_minor); + + hwaccel_vaapi->drm_fd = g_open (render_node, O_RDWR | O_CLOEXEC, 600); + if (hwaccel_vaapi->drm_fd == -1) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to open render node: %s", g_strerror (errno)); + return FALSE; + } + + hwaccel_vaapi->va_display = vaGetDisplayDRM (hwaccel_vaapi->drm_fd); + if (!hwaccel_vaapi->va_display) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to get VA display from DRM fd"); + return FALSE; + } + + va_status = vaInitialize (hwaccel_vaapi->va_display, + version_major, version_minor); + if (va_status != VA_STATUS_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to initialize VA display: %s", vaErrorStr (va_status)); + return FALSE; + } + + return TRUE; +} + +static gboolean +has_device_avc_profile (VAProfile *profiles, + int n_profiles) +{ + int i; + + for (i = 0; i < n_profiles; ++i) + { + if (profiles[i] == VAProfileH264High) + return TRUE; + } + + return FALSE; +} + +static gboolean +has_device_avc_enc_entrypoint (VAEntrypoint *entrypoints, + int n_entrypoints) +{ + int i; + + for (i = 0; i < n_entrypoints; ++i) + { + if (entrypoints[i] == VAEntrypointEncSlice) + return TRUE; + } + + return FALSE; +} + +static gboolean +check_device_capabilities (GrdHwAccelVaapi *hwaccel_vaapi, + GError **error) +{ + g_autofree VAProfile *profiles = NULL; + g_autofree VAEntrypoint *entrypoints = NULL; + VAConfigAttrib attributes[] = + { + { .type = VAConfigAttribRTFormat }, + { .type = VAConfigAttribRateControl }, + { .type = VAConfigAttribEncMaxRefFrames }, + { .type = VAConfigAttribEncPackedHeaders }, + { .type = VAConfigAttribEncQualityRange }, + }; + int max_profiles; + int max_num_entrypoints; + int n_profiles = 0; + int n_entrypoints = 0; + uint32_t max_ref_l0; + VAStatus va_status; + + max_profiles = vaMaxNumProfiles (hwaccel_vaapi->va_display); + if (max_profiles < 1) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Invalid max num profiles: %i", max_profiles); + return FALSE; + } + profiles = g_new0 (VAProfile, max_profiles); + + va_status = vaQueryConfigProfiles (hwaccel_vaapi->va_display, + profiles, &n_profiles); + if (va_status != VA_STATUS_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to query config profiles: %s", + vaErrorStr (va_status)); + return FALSE; + } + + if (!has_device_avc_profile (profiles, n_profiles)) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, + "Unsuitable device, missing required AVC profile"); + return FALSE; + } + + max_num_entrypoints = vaMaxNumEntrypoints (hwaccel_vaapi->va_display); + if (max_num_entrypoints < 1) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Invalid max num entrypoints: %i", max_num_entrypoints); + return FALSE; + } + entrypoints = g_new0 (VAEntrypoint, max_num_entrypoints); + + va_status = vaQueryConfigEntrypoints (hwaccel_vaapi->va_display, + VAProfileH264High, + entrypoints, &n_entrypoints); + if (va_status != VA_STATUS_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to query config entrypoints: %s", + vaErrorStr (va_status)); + return FALSE; + } + + if (!has_device_avc_enc_entrypoint (entrypoints, n_entrypoints)) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Unsuitable device, missing required AVC encoding " + "entrypoint"); + return FALSE; + } + + va_status = vaGetConfigAttributes (hwaccel_vaapi->va_display, + VAProfileH264High, VAEntrypointEncSlice, + attributes, G_N_ELEMENTS (attributes)); + if (va_status != VA_STATUS_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to get config attributes: %s", + vaErrorStr (va_status)); + return FALSE; + } + + g_assert (attributes[0].type == VAConfigAttribRTFormat); + if (attributes[0].value == VA_ATTRIB_NOT_SUPPORTED || + !(attributes[0].value & VA_RT_FORMAT_YUV420)) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, + "Unsuitable device, device does not support YUV420 format"); + return FALSE; + } + + g_assert (attributes[1].type == VAConfigAttribRateControl); + if (attributes[1].value == VA_ATTRIB_NOT_SUPPORTED || + !(attributes[1].value & VA_RC_CQP)) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, + "Unsuitable device, device does not support CQP"); + return FALSE; + } + + g_assert (attributes[2].type == VAConfigAttribEncMaxRefFrames); + if (attributes[2].value == VA_ATTRIB_NOT_SUPPORTED) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, + "Unsuitable device, invalid max ref frames attribute: %u", + attributes[2].value); + return FALSE; + } + + max_ref_l0 = attributes[2].value & 0xFFFF; + if (max_ref_l0 < 1) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, + "Unsuitable device, device cannot handle reference frames"); + return FALSE; + } + + g_assert (attributes[3].type == VAConfigAttribEncPackedHeaders); + if (attributes[3].value == VA_ATTRIB_NOT_SUPPORTED || + !(attributes[3].value & VA_ENC_PACKED_HEADER_SEQUENCE) || + !(attributes[3].value & VA_ENC_PACKED_HEADER_PICTURE) || + !(attributes[3].value & VA_ENC_PACKED_HEADER_SLICE) || + !(attributes[3].value & VA_ENC_PACKED_HEADER_RAW_DATA)) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, + "Unsuitable device, device does not support required packed " + "headers (only supports 0x%08X)", attributes[3].value); + return FALSE; + } + + g_assert (attributes[4].type == VAConfigAttribEncQualityRange); + hwaccel_vaapi->supports_quality_level = attributes[4].value != + VA_ATTRIB_NOT_SUPPORTED; + + return TRUE; +} + +GrdHwAccelVaapi * +grd_hwaccel_vaapi_new (GrdVkDevice *vk_device, + GError **error) +{ + g_autoptr (GrdHwAccelVaapi) hwaccel_vaapi = NULL; + int version_major = 0; + int version_minor = 0; + + g_assert (vk_device); + + hwaccel_vaapi = g_object_new (GRD_TYPE_HWACCEL_VAAPI, NULL); + hwaccel_vaapi->vk_device = vk_device; + + if (!initialize_vaapi (hwaccel_vaapi, &version_major, &version_minor, + grd_vk_device_get_drm_render_node (vk_device), + error)) + return NULL; + + if (!check_device_capabilities (hwaccel_vaapi, error)) + return NULL; + + g_message ("[HWAccel.VAAPI] Successfully initialized VAAPI %i.%i with " + "vendor: %s", version_major, version_minor, + vaQueryVendorString (hwaccel_vaapi->va_display)); + + return g_steal_pointer (&hwaccel_vaapi); +} + +static void +grd_hwaccel_vaapi_dispose (GObject *object) +{ + GrdHwAccelVaapi *hwaccel_vaapi = GRD_HWACCEL_VAAPI (object); + + g_clear_pointer (&hwaccel_vaapi->va_display, vaTerminate); + g_clear_fd (&hwaccel_vaapi->drm_fd, NULL); + + G_OBJECT_CLASS (grd_hwaccel_vaapi_parent_class)->dispose (object); +} + +static void +grd_hwaccel_vaapi_init (GrdHwAccelVaapi *hwaccel_vaapi) +{ +} + +static void +grd_hwaccel_vaapi_class_init (GrdHwAccelVaapiClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = grd_hwaccel_vaapi_dispose; +} diff --git a/grd-hwaccel-vaapi.h b/grd-hwaccel-vaapi.h new file mode 100644 index 0000000..fdddfa7 --- /dev/null +++ b/grd-hwaccel-vaapi.h @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2022 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include +#include + +#include "grd-types.h" + +#define GRD_TYPE_HWACCEL_VAAPI (grd_hwaccel_vaapi_get_type ()) +G_DECLARE_FINAL_TYPE (GrdHwAccelVaapi, grd_hwaccel_vaapi, + GRD, HWACCEL_VAAPI, GObject) + +GrdHwAccelVaapi *grd_hwaccel_vaapi_new (GrdVkDevice *vk_device, + GError **error); + +GrdEncodeSession *grd_hwaccel_vaapi_create_encode_session (GrdHwAccelVaapi *hwaccel_vaapi, + uint32_t source_width, + uint32_t source_height, + uint32_t refresh_rate, + GError **error); diff --git a/grd-hwaccel-vulkan.c b/grd-hwaccel-vulkan.c new file mode 100644 index 0000000..5d5ec89 --- /dev/null +++ b/grd-hwaccel-vulkan.c @@ -0,0 +1,940 @@ +/* + * Copyright (C) 2022 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-hwaccel-vulkan.h" + +#include +#include +#include +#include +#include + +#include "grd-debug.h" +#include "grd-egl-thread.h" +#include "grd-vk-device.h" +#include "grd-vk-physical-device.h" +#include "grd-vk-utils.h" + +#define VULKAN_API_VERSION VK_API_VERSION_1_2 + +struct _GrdHwAccelVulkan +{ + GObject parent; + + GrdEglThread *egl_thread; + + VkInstance vk_instance; + + /* VK_EXT_debug_utils */ + PFN_vkCreateDebugUtilsMessengerEXT vkCreateDebugUtilsMessengerEXT; + PFN_vkDestroyDebugUtilsMessengerEXT vkDestroyDebugUtilsMessengerEXT; + + VkDebugUtilsMessengerEXT vk_debug_messenger; + + GrdVkSPIRVSources spirv_sources; +}; + +G_DEFINE_TYPE (GrdHwAccelVulkan, grd_hwaccel_vulkan, G_TYPE_OBJECT) + +static gboolean +list_contains_drm_format_modifier (uint64_t *modifiers, + int n_modifiers, + uint64_t modifier) +{ + int i; + + for (i = 0; i < n_modifiers; ++i) + { + if (modifiers[i] == modifier) + return TRUE; + } + + return FALSE; +} + +static GArray * +get_egl_vulkan_format_modifier_intersection (GrdHwAccelVulkan *hwaccel_vulkan, + VkPhysicalDevice vk_physical_device, + uint32_t drm_format, + uint32_t *required_n_planes, + VkFormatFeatureFlags2 *required_features, + GError **error) +{ + GrdEglThread *egl_thread = hwaccel_vulkan->egl_thread; + VkFormatProperties2 format_properties_2 = {}; + VkDrmFormatModifierPropertiesList2EXT modifier_properties_list2 = {}; + g_autofree VkDrmFormatModifierProperties2EXT *modifier_properties_2 = NULL; + VkFormat vk_format = VK_FORMAT_UNDEFINED; + g_autofree uint64_t *egl_modifiers = NULL; + int n_egl_modifiers; + GArray *vk_modifiers; + uint32_t i; + + if (!grd_vk_get_vk_format_from_drm_format (drm_format, &vk_format, error)) + return NULL; + + format_properties_2.sType = VK_STRUCTURE_TYPE_FORMAT_PROPERTIES_2; + modifier_properties_list2.sType = + VK_STRUCTURE_TYPE_DRM_FORMAT_MODIFIER_PROPERTIES_LIST_2_EXT; + + grd_vk_append_to_chain (&format_properties_2, &modifier_properties_list2); + + vkGetPhysicalDeviceFormatProperties2 (vk_physical_device, vk_format, + &format_properties_2); + + if (modifier_properties_list2.drmFormatModifierCount == 0) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, + "No DRM format modifiers available for DRM format %u", + drm_format); + return NULL; + } + + modifier_properties_2 = + g_new0 (VkDrmFormatModifierProperties2EXT, + modifier_properties_list2.drmFormatModifierCount); + + modifier_properties_list2.pDrmFormatModifierProperties = modifier_properties_2; + vkGetPhysicalDeviceFormatProperties2 (vk_physical_device, vk_format, + &format_properties_2); + + if (!grd_egl_thread_get_modifiers_for_format (egl_thread, drm_format, + &n_egl_modifiers, + &egl_modifiers)) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, + "No DRM format modifiers available for DRM format %u from " + "EGL", drm_format); + return NULL; + } + + vk_modifiers = g_array_new (FALSE, FALSE, sizeof (uint64_t)); + + for (i = 0; i < modifier_properties_list2.drmFormatModifierCount; i++) + { + uint64_t modifier = modifier_properties_2[i].drmFormatModifier; + uint32_t n_planes = modifier_properties_2[i].drmFormatModifierPlaneCount; + VkFormatFeatureFlags2 tiling_features = + modifier_properties_2[i].drmFormatModifierTilingFeatures; + + if (required_n_planes && + n_planes != *required_n_planes) + continue; + if (required_features && + (tiling_features & *required_features) != *required_features) + continue; + if (!list_contains_drm_format_modifier (egl_modifiers, n_egl_modifiers, + modifier)) + continue; + + g_array_append_vals (vk_modifiers, &modifier, 1); + } + + if (vk_modifiers->len == 0) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, + "No common DRM format modifiers available for DRM format %u " + "from EGL and Vulkan", drm_format); + g_array_free (vk_modifiers, TRUE); + return NULL; + } + + return vk_modifiers; +} + +gboolean +grd_hwaccel_vulkan_get_modifiers_for_format (GrdHwAccelVulkan *hwaccel_vulkan, + GrdVkPhysicalDevice *physical_device, + uint32_t drm_format, + int *out_n_modifiers, + uint64_t **out_modifiers) +{ + VkPhysicalDevice vk_physical_device = + grd_vk_physical_device_get_physical_device (physical_device); + GArray *modifiers; + g_autoptr (GError) error = NULL; + + modifiers = + get_egl_vulkan_format_modifier_intersection (hwaccel_vulkan, + vk_physical_device, + drm_format, + NULL, NULL, &error); + if (!modifiers) + return FALSE; + + *out_n_modifiers = modifiers->len; + *out_modifiers = (uint64_t *) g_array_free (modifiers, FALSE); + + return TRUE; +} + +static gboolean +has_vk_extension (VkExtensionProperties *properties, + uint32_t n_properties, + const char *extension) +{ + uint32_t i; + + for (i = 0; i < n_properties; ++i) + { + if (strcmp (properties[i].extensionName, extension) == 0) + return TRUE; + } + + return FALSE; +} + +static gboolean +check_device_extension (VkExtensionProperties *properties, + uint32_t n_properties, + const char *extension) +{ + if (!has_vk_extension (properties, n_properties, extension)) + { + g_debug ("[HWAccel.Vulkan] Skipping device. Missing extension '%s'", + extension); + return FALSE; + } + + return TRUE; +} + +static gboolean +check_device_extensions (VkPhysicalDevice vk_physical_device) +{ + g_autofree VkExtensionProperties *properties = NULL; + uint32_t n_properties = 0; + VkResult vk_result; + + vk_result = vkEnumerateDeviceExtensionProperties (vk_physical_device, NULL, + &n_properties, NULL); + if (vk_result != VK_SUCCESS) + { + g_warning ("[HWAccel.Vulkan] Failed to enumerate device extension " + "properties: %i", vk_result); + return FALSE; + } + if (n_properties == 0) + return TRUE; + + properties = g_new0 (VkExtensionProperties, n_properties); + vk_result = vkEnumerateDeviceExtensionProperties (vk_physical_device, NULL, + &n_properties, properties); + if (vk_result != VK_SUCCESS) + { + g_warning ("[HWAccel.Vulkan] Failed to enumerate device extension " + "properties: %i", vk_result); + return FALSE; + } + + if (!check_device_extension (properties, n_properties, + VK_KHR_SYNCHRONIZATION_2_EXTENSION_NAME)) + return FALSE; + if (!check_device_extension (properties, n_properties, + VK_KHR_EXTERNAL_MEMORY_FD_EXTENSION_NAME)) + return FALSE; + if (!check_device_extension (properties, n_properties, + VK_EXT_EXTERNAL_MEMORY_DMA_BUF_EXTENSION_NAME)) + return FALSE; + if (!check_device_extension (properties, n_properties, + VK_EXT_IMAGE_DRM_FORMAT_MODIFIER_EXTENSION_NAME)) + return FALSE; + if (!check_device_extension (properties, n_properties, + VK_KHR_FORMAT_FEATURE_FLAGS_2_EXTENSION_NAME)) + return FALSE; + if (!check_device_extension (properties, n_properties, + VK_KHR_ZERO_INITIALIZE_WORKGROUP_MEMORY_EXTENSION_NAME)) + return FALSE; + if (!check_device_extension (properties, n_properties, + VK_EXT_PHYSICAL_DEVICE_DRM_EXTENSION_NAME)) + return FALSE; + + return TRUE; +} + +static gboolean +get_queue_family_properties (VkPhysicalDevice vk_physical_device, + VkQueueFamilyProperties2 **properties_2, + uint32_t *n_properties_2) +{ + uint32_t i; + + *properties_2 = NULL; + *n_properties_2 = 0; + + vkGetPhysicalDeviceQueueFamilyProperties2 (vk_physical_device, + n_properties_2, NULL); + if (*n_properties_2 == 0) + return FALSE; + + *properties_2 = g_new0 (VkQueueFamilyProperties2, *n_properties_2); + for (i = 0; i < *n_properties_2; ++i) + (*properties_2)[i].sType = VK_STRUCTURE_TYPE_QUEUE_FAMILY_PROPERTIES_2; + + vkGetPhysicalDeviceQueueFamilyProperties2 (vk_physical_device, + n_properties_2, *properties_2); + + return TRUE; +} + +static gboolean +has_queue_family_with_bitmask (VkPhysicalDevice vk_physical_device, + VkQueueFlags bitmask) +{ + g_autofree VkQueueFamilyProperties2 *properties_2 = NULL; + uint32_t n_properties_2 = 0; + uint32_t i; + + if (!get_queue_family_properties (vk_physical_device, &properties_2, + &n_properties_2)) + return FALSE; + + for (i = 0; i < n_properties_2; ++i) + { + if (properties_2[i].queueFamilyProperties.queueFlags & bitmask) + return TRUE; + } + + return FALSE; +} + +static gboolean +supports_bgrx_dma_buf_images (GrdHwAccelVulkan *hwaccel_vulkan, + VkPhysicalDevice vk_physical_device, + GError **error) +{ + uint32_t drm_format; + uint32_t required_n_planes; + VkFormatFeatureFlags2 required_features; + GArray *modifier_array; + uint64_t *modifiers; + uint32_t i; + + drm_format = DRM_FORMAT_XRGB8888; + required_n_planes = 1; + required_features = VK_FORMAT_FEATURE_2_SAMPLED_IMAGE_BIT; + + modifier_array = + get_egl_vulkan_format_modifier_intersection (hwaccel_vulkan, + vk_physical_device, + drm_format, + &required_n_planes, + &required_features, + error); + if (!modifier_array) + return FALSE; + + modifiers = (uint64_t *) modifier_array->data; + for (i = 0; i < modifier_array->len; ++i) + { + g_debug ("[HWAccel.Vulkan] Found DRM format modifier %lu for DRM " + "format %u", modifiers[i], drm_format); + } + g_array_free (modifier_array, TRUE); + + return TRUE; +} + +static gboolean +check_physical_device (GrdHwAccelVulkan *hwaccel_vulkan, + VkPhysicalDevice vk_physical_device, + int64_t render_major_egl, + int64_t render_minor_egl, + GrdVkDeviceFeatures *device_features) +{ + VkPhysicalDeviceProperties properties = {}; + VkPhysicalDeviceProperties2 properties_2 = {}; + VkPhysicalDeviceDrmPropertiesEXT drm_properties = {}; + VkPhysicalDeviceFeatures2 features_2 = {}; + VkPhysicalDeviceVulkan12Features vulkan12_features = {}; + VkPhysicalDeviceZeroInitializeWorkgroupMemoryFeatures zero_init_features = {}; + g_autoptr (GError) error = NULL; + + *device_features = 0; + + vkGetPhysicalDeviceProperties (vk_physical_device, &properties); + if (properties.apiVersion < VULKAN_API_VERSION) + { + g_debug ("[HWAccel.Vulkan] Skipping device. API version too old " + "(have %u, need %u)", properties.apiVersion, VULKAN_API_VERSION); + return FALSE; + } + if (!properties.limits.timestampComputeAndGraphics) + { + g_debug ("[HWAccel.Vulkan] Skipping device. Support for " + "'timestampComputeAndGraphics' is missing"); + return FALSE; + } + + if (!check_device_extensions (vk_physical_device)) + return FALSE; + + properties_2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2; + drm_properties.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRM_PROPERTIES_EXT; + + grd_vk_append_to_chain (&properties_2, &drm_properties); + + vkGetPhysicalDeviceProperties2 (vk_physical_device, &properties_2); + + if (drm_properties.hasRender != VK_TRUE) + { + g_debug ("[HWAccel.Vulkan] Skipping device. Device has no " + "DRM render node"); + return FALSE; + } + if (drm_properties.renderMajor != render_major_egl || + drm_properties.renderMinor != render_minor_egl) + { + g_debug ("[HWAccel.Vulkan] Skipping device. Vulkan and EGL device DRM " + "render nodes don't match"); + return FALSE; + } + + features_2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2; + vulkan12_features.sType = + VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES; + zero_init_features.sType = + VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ZERO_INITIALIZE_WORKGROUP_MEMORY_FEATURES; + + grd_vk_append_to_chain (&features_2, &vulkan12_features); + grd_vk_append_to_chain (&features_2, &zero_init_features); + + vkGetPhysicalDeviceFeatures2 (vk_physical_device, &features_2); + + if (zero_init_features.shaderZeroInitializeWorkgroupMemory != VK_TRUE) + { + g_debug ("[HWAccel.Vulkan] Skipping device. Support for " + "'shaderZeroInitializeWorkgroupMemory' is missing"); + return FALSE; + } + + if (vulkan12_features.descriptorBindingSampledImageUpdateAfterBind != VK_FALSE) + *device_features |= GRD_VK_DEVICE_FEATURE_UPDATE_AFTER_BIND_SAMPLED_IMAGE; + if (vulkan12_features.descriptorBindingStorageImageUpdateAfterBind != VK_FALSE) + *device_features |= GRD_VK_DEVICE_FEATURE_UPDATE_AFTER_BIND_STORAGE_IMAGE; + + if (!has_queue_family_with_bitmask (vk_physical_device, + VK_QUEUE_COMPUTE_BIT | + VK_QUEUE_TRANSFER_BIT)) + { + g_debug ("[HWAccel.Vulkan] Skipping device. Missing device queue family " + "with (VK_QUEUE_COMPUTE_BIT | VK_QUEUE_TRANSFER_BIT) bitmask"); + return FALSE; + } + + if (!supports_bgrx_dma_buf_images (hwaccel_vulkan, vk_physical_device, + &error)) + { + g_debug ("[HWAccel.Vulkan] Skipping device: %s", error->message); + return FALSE; + } + + return TRUE; +} + +static GrdVkPhysicalDevice * +find_and_create_physical_device (GrdHwAccelVulkan *hwaccel_vulkan, + const VkPhysicalDevice *physical_devices, + uint32_t n_physical_devices, + int64_t render_major, + int64_t render_minor, + GError **error) +{ + uint32_t i; + + for (i = 0; i < n_physical_devices; ++i) + { + VkPhysicalDevice vk_physical_device = physical_devices[i]; + GrdVkDeviceFeatures device_features = 0; + + g_debug ("[HWAccel.Vulkan] Checking physical device %u/%u", + i + 1, n_physical_devices); + if (check_physical_device (hwaccel_vulkan, vk_physical_device, + render_major, render_minor, &device_features)) + return grd_vk_physical_device_new (vk_physical_device, device_features); + } + + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Could not find proper device"); + + return NULL; +} + +GrdVkPhysicalDevice * +grd_hwaccel_vulkan_acquire_physical_device (GrdHwAccelVulkan *hwaccel_vulkan, + GError **error) +{ + GrdEglThread *egl_thread = hwaccel_vulkan->egl_thread; + g_autofree VkPhysicalDevice *physical_devices = NULL; + uint32_t n_physical_devices = 0; + const char *drm_render_node; + struct stat stat_buf = {}; + VkResult vk_result; + int ret; + + g_assert (egl_thread); + + vk_result = vkEnumeratePhysicalDevices (hwaccel_vulkan->vk_instance, + &n_physical_devices, NULL); + if (vk_result != VK_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to enumerate physical devices: %i", vk_result); + return NULL; + } + if (n_physical_devices == 0) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "No physical devices found: %i", vk_result); + return NULL; + } + + physical_devices = g_new0 (VkPhysicalDevice, n_physical_devices); + vk_result = vkEnumeratePhysicalDevices (hwaccel_vulkan->vk_instance, + &n_physical_devices, + physical_devices); + if (vk_result != VK_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to enumerate physical devices: %i", vk_result); + return NULL; + } + + drm_render_node = grd_egl_thread_get_drm_render_node (egl_thread); + + ret = stat (drm_render_node, &stat_buf); + if (ret < 0) + { + g_set_error (error, G_IO_ERROR, g_io_error_from_errno (-ret), + "Failed to check status of DRM render node: %s", + strerror (-ret)); + return NULL; + } + if (!S_ISCHR (stat_buf.st_mode)) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Invalid file mode for DRM render node"); + return NULL; + } + + return find_and_create_physical_device (hwaccel_vulkan, + physical_devices, n_physical_devices, + major (stat_buf.st_rdev), + minor (stat_buf.st_rdev), + error); +} + +GrdVkDevice * +grd_hwaccel_vulkan_acquire_device (GrdHwAccelVulkan *hwaccel_vulkan, + GrdVkPhysicalDevice *physical_device, + GError **error) +{ + return grd_vk_device_new (physical_device, + &hwaccel_vulkan->spirv_sources, + error); +} + +static gboolean +has_vk_layer (VkLayerProperties *properties, + uint32_t n_properties, + const char *extension) +{ + uint32_t i; + + for (i = 0; i < n_properties; ++i) + { + if (strcmp (properties[i].layerName, extension) == 0) + return TRUE; + } + + return FALSE; +} + +static gboolean +check_instance_layers (GrdHwAccelVulkan *hwaccel_vulkan, + gboolean *has_validation_layer, + GError **error) +{ + g_autofree VkLayerProperties *properties = NULL; + uint32_t n_properties = 0; + VkResult vk_result; + + *has_validation_layer = FALSE; + + vk_result = vkEnumerateInstanceLayerProperties (&n_properties, NULL); + if (vk_result != VK_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to enumerate instance layer properties: %i", + vk_result); + return FALSE; + } + if (n_properties == 0) + return TRUE; + + properties = g_new0 (VkLayerProperties, n_properties); + vk_result = vkEnumerateInstanceLayerProperties (&n_properties, properties); + if (vk_result == VK_INCOMPLETE) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Instance layer properties changed during count fetch"); + return FALSE; + } + if (vk_result != VK_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to enumerate instance layer properties: %i", + vk_result); + return FALSE; + } + + *has_validation_layer = has_vk_layer (properties, n_properties, + "VK_LAYER_KHRONOS_validation"); + + return TRUE; +} + +static gboolean +check_instance_extensions (GrdHwAccelVulkan *hwaccel_vulkan, + gboolean *supports_debug_utils, + GError **error) +{ + g_autofree VkExtensionProperties *properties = NULL; + uint32_t n_properties = 0; + VkResult vk_result; + + *supports_debug_utils = FALSE; + + vk_result = vkEnumerateInstanceExtensionProperties (NULL, + &n_properties, + NULL); + if (vk_result != VK_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to enumerate instance extension properties: %i", + vk_result); + return FALSE; + } + if (n_properties == 0) + return TRUE; + + properties = g_new0 (VkExtensionProperties, n_properties); + vk_result = vkEnumerateInstanceExtensionProperties (NULL, + &n_properties, + properties); + if (vk_result == VK_INCOMPLETE) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Instance extension properties changed during count fetch"); + return FALSE; + } + if (vk_result != VK_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to enumerate instance extension properties: %i", + vk_result); + return FALSE; + } + + *supports_debug_utils = has_vk_extension (properties, n_properties, + VK_EXT_DEBUG_UTILS_EXTENSION_NAME); + + return TRUE; +} + +static gboolean +load_instance_debug_funcs (GrdHwAccelVulkan *hwaccel_vulkan, + GError **error) +{ + VkInstance vk_instance = hwaccel_vulkan->vk_instance; + + hwaccel_vulkan->vkCreateDebugUtilsMessengerEXT = (PFN_vkCreateDebugUtilsMessengerEXT) + vkGetInstanceProcAddr (vk_instance, "vkCreateDebugUtilsMessengerEXT"); + if (!hwaccel_vulkan->vkCreateDebugUtilsMessengerEXT) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to get instance function address for function " + "\"vkCreateDebugUtilsMessengerEXT\""); + return FALSE; + } + + hwaccel_vulkan->vkDestroyDebugUtilsMessengerEXT = (PFN_vkDestroyDebugUtilsMessengerEXT) + vkGetInstanceProcAddr (vk_instance, "vkDestroyDebugUtilsMessengerEXT"); + if (!hwaccel_vulkan->vkDestroyDebugUtilsMessengerEXT) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to get instance function address for function " + "\"vkDestroyDebugUtilsMessengerEXT\""); + return FALSE; + } + + return TRUE; +} + +static const char * +message_severity_to_string (VkDebugUtilsMessageSeverityFlagBitsEXT message_severity) +{ + switch (message_severity) + { + case VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT: + return "Verbose"; + case VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT: + return "Info"; + case VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT: + return "Warning"; + case VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT: + return "Error"; + default: + g_assert_not_reached (); + return NULL; + } +} + +static VkBool32 +debug_messenger (VkDebugUtilsMessageSeverityFlagBitsEXT message_severity, + VkDebugUtilsMessageTypeFlagsEXT message_type_flags, + const VkDebugUtilsMessengerCallbackDataEXT *callback_data, + void* user_data) +{ + g_debug ("[HWAccel.Vulkan] Debug[%s]: Types: 0x%08X: Id: [%i: %s]: %s", + message_severity_to_string (message_severity), message_type_flags, + callback_data->messageIdNumber, callback_data->pMessageIdName, + callback_data->pMessage); + + return VK_FALSE; +} + +static gboolean +create_debug_messenger (GrdHwAccelVulkan *hwaccel_vulkan, + GError **error) +{ + VkInstance vk_instance = hwaccel_vulkan->vk_instance; + VkDebugUtilsMessengerCreateInfoEXT create_info = {}; + VkDebugUtilsMessengerEXT vk_debug_messenger = VK_NULL_HANDLE; + VkResult vk_result; + + g_assert (vk_instance != VK_NULL_HANDLE); + g_assert (hwaccel_vulkan->vkCreateDebugUtilsMessengerEXT); + + create_info.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT; + create_info.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT | + VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT | + VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | + VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT; + create_info.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | + VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT | + VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT; + create_info.pfnUserCallback = debug_messenger; + create_info.pUserData = NULL; + + vk_result = + hwaccel_vulkan->vkCreateDebugUtilsMessengerEXT (vk_instance, + &create_info, NULL, + &vk_debug_messenger); + if (vk_result != VK_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create debug messenger: %i", vk_result); + return FALSE; + } + g_assert (vk_debug_messenger != VK_NULL_HANDLE); + + hwaccel_vulkan->vk_debug_messenger = vk_debug_messenger; + + return TRUE; +} + +static gboolean +create_vk_instance (GrdHwAccelVulkan *hwaccel_vulkan, + GError **error) +{ + VkInstanceCreateInfo instance_create_info = {}; + VkApplicationInfo app_info = {}; + gboolean has_validation_layer = FALSE; + gboolean supports_debug_utils = FALSE; + gboolean vulkan_debug = FALSE; + const char *layers[1] = {}; + uint32_t n_layers = 0; + const char *extensions[1] = {}; + uint32_t n_extensions = 0; + VkResult vk_result; + + if (grd_get_debug_flags () & GRD_DEBUG_VK_VALIDATION) + vulkan_debug = TRUE; + + if (!check_instance_layers (hwaccel_vulkan, &has_validation_layer, error)) + return FALSE; + if (!check_instance_extensions (hwaccel_vulkan, &supports_debug_utils, error)) + return FALSE; + + app_info.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO; + app_info.apiVersion = VULKAN_API_VERSION; + + if (vulkan_debug) + { + if (has_validation_layer) + layers[n_layers++] = "VK_LAYER_KHRONOS_validation"; + else + g_warning ("[HWAccel.Vulkan] VK_LAYER_KHRONOS_validation is unavailable"); + + if (supports_debug_utils) + extensions[n_extensions++] = VK_EXT_DEBUG_UTILS_EXTENSION_NAME; + else + g_warning ("[HWAccel.Vulkan] VK_EXT_debug_utils is unavailable"); + } + + instance_create_info.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; + instance_create_info.pApplicationInfo = &app_info; + instance_create_info.enabledLayerCount = n_layers; + instance_create_info.ppEnabledLayerNames = layers; + instance_create_info.enabledExtensionCount = n_extensions; + instance_create_info.ppEnabledExtensionNames = extensions; + + vk_result = vkCreateInstance (&instance_create_info, NULL, + &hwaccel_vulkan->vk_instance); + if (vk_result != VK_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create instance: %i", vk_result); + return FALSE; + } + g_assert (hwaccel_vulkan->vk_instance != VK_NULL_HANDLE); + + if (!vulkan_debug || !supports_debug_utils) + return TRUE; + + if (!load_instance_debug_funcs (hwaccel_vulkan, error)) + return FALSE; + if (!create_debug_messenger (hwaccel_vulkan, error)) + return FALSE; + + return TRUE; +} + +GrdHwAccelVulkan * +grd_hwaccel_vulkan_new (GrdEglThread *egl_thread, + GError **error) +{ + g_autoptr (GrdHwAccelVulkan) hwaccel_vulkan = NULL; + + hwaccel_vulkan = g_object_new (GRD_TYPE_HWACCEL_VULKAN, NULL); + hwaccel_vulkan->egl_thread = egl_thread; + + if (!create_vk_instance (hwaccel_vulkan, error)) + return NULL; + + return g_steal_pointer (&hwaccel_vulkan); +} + +static void +spirv_source_free (GrdVkSPIRVSource *spirv_source) +{ + g_free (spirv_source->data); + g_free (spirv_source); +} + +static void +free_spirv_sources (GrdHwAccelVulkan *hwaccel_vulkan) +{ + GrdVkSPIRVSources *spirv_sources = &hwaccel_vulkan->spirv_sources; + + g_clear_pointer (&spirv_sources->avc_dual_view, spirv_source_free); +} + +static void +grd_hwaccel_vulkan_dispose (GObject *object) +{ + GrdHwAccelVulkan *hwaccel_vulkan = GRD_HWACCEL_VULKAN (object); + + free_spirv_sources (hwaccel_vulkan); + + if (hwaccel_vulkan->vk_debug_messenger != VK_NULL_HANDLE) + { + VkInstance vk_instance = hwaccel_vulkan->vk_instance; + VkDebugUtilsMessengerEXT vk_debug_messenger = + hwaccel_vulkan->vk_debug_messenger; + + g_assert (vk_instance != VK_NULL_HANDLE); + g_assert (hwaccel_vulkan->vkDestroyDebugUtilsMessengerEXT); + + hwaccel_vulkan->vkDestroyDebugUtilsMessengerEXT (vk_instance, + vk_debug_messenger, + NULL); + hwaccel_vulkan->vk_debug_messenger = VK_NULL_HANDLE; + } + + if (hwaccel_vulkan->vk_instance != VK_NULL_HANDLE) + { + vkDestroyInstance (hwaccel_vulkan->vk_instance, NULL); + hwaccel_vulkan->vk_instance = VK_NULL_HANDLE; + } + + G_OBJECT_CLASS (grd_hwaccel_vulkan_parent_class)->dispose (object); +} + +static gboolean +load_spirv_source (const char *path, + GrdVkSPIRVSource **spirv_source, + GError **error) +{ + char *data = NULL; + size_t size = 0; + + if (!g_file_get_contents (path, &data, &size, error)) + return FALSE; + + /* SPIR-V sources are always aligned to 32 bits */ + g_assert (size % 4 == 0); + + *spirv_source = g_new0 (GrdVkSPIRVSource, 1); + (*spirv_source)->data = data; + (*spirv_source)->size = size; + + return TRUE; +} + +static void +load_spirv_sources (GrdHwAccelVulkan *hwaccel_vulkan) +{ + GrdVkSPIRVSources *spirv_sources = &hwaccel_vulkan->spirv_sources; + g_autofree char *avc_dual_view_path = NULL; + g_autoptr (GError) error = NULL; + + avc_dual_view_path = g_strdup_printf ("%s/grd-avc-dual-view_opt.spv", + GRD_SHADER_DIR); + if (!load_spirv_source (avc_dual_view_path, &spirv_sources->avc_dual_view, + &error)) + g_error ("[HWAccel.Vulkan] Failed to load shader: %s", error->message); +} + +static void +grd_hwaccel_vulkan_init (GrdHwAccelVulkan *hwaccel_vulkan) +{ + load_spirv_sources (hwaccel_vulkan); +} + +static void +grd_hwaccel_vulkan_class_init (GrdHwAccelVulkanClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = grd_hwaccel_vulkan_dispose; +} diff --git a/grd-hwaccel-vulkan.h b/grd-hwaccel-vulkan.h new file mode 100644 index 0000000..317e210 --- /dev/null +++ b/grd-hwaccel-vulkan.h @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2022 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include +#include + +#include "grd-types.h" + +#define GRD_TYPE_HWACCEL_VULKAN (grd_hwaccel_vulkan_get_type ()) +G_DECLARE_FINAL_TYPE (GrdHwAccelVulkan, grd_hwaccel_vulkan, + GRD, HWACCEL_VULKAN, GObject) + +struct _GrdVkSPIRVSource +{ + char *data; + size_t size; +}; + +struct _GrdVkSPIRVSources +{ + GrdVkSPIRVSource *avc_dual_view; +}; + +GrdHwAccelVulkan *grd_hwaccel_vulkan_new (GrdEglThread *egl_thread, + GError **error); + +gboolean grd_hwaccel_vulkan_get_modifiers_for_format (GrdHwAccelVulkan *hwaccel_vulkan, + GrdVkPhysicalDevice *physical_device, + uint32_t drm_format, + int *out_n_modifiers, + uint64_t **out_modifiers); + +GrdVkPhysicalDevice *grd_hwaccel_vulkan_acquire_physical_device (GrdHwAccelVulkan *hwaccel_vulkan, + GError **error); + +GrdVkDevice *grd_hwaccel_vulkan_acquire_device (GrdHwAccelVulkan *hwaccel_vulkan, + GrdVkPhysicalDevice *physical_device, + GError **error); diff --git a/grd-image-view-nv12.c b/grd-image-view-nv12.c new file mode 100644 index 0000000..daa1976 --- /dev/null +++ b/grd-image-view-nv12.c @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2024 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-image-view-nv12.h" + +#include "grd-vk-image.h" + +struct _GrdImageViewNV12 +{ + GrdImageView parent; + + GrdVkImage *vk_y_layer; + GrdVkImage *vk_uv_layer; +}; + +G_DEFINE_TYPE (GrdImageViewNV12, grd_image_view_nv12, + GRD_TYPE_IMAGE_VIEW) + +VkImageView +grd_image_view_nv12_get_y_layer (GrdImageViewNV12 *image_view_nv12) +{ + return grd_vk_image_get_image_view (image_view_nv12->vk_y_layer); +} + +VkImageView +grd_image_view_nv12_get_uv_layer (GrdImageViewNV12 *image_view_nv12) +{ + return grd_vk_image_get_image_view (image_view_nv12->vk_uv_layer); +} + +GList * +grd_image_view_nv12_get_images (GrdImageViewNV12 *image_view_nv12) +{ + GList *images = NULL; + + if (image_view_nv12->vk_y_layer) + images = g_list_append (images, image_view_nv12->vk_y_layer); + if (image_view_nv12->vk_uv_layer) + images = g_list_append (images, image_view_nv12->vk_uv_layer); + + return images; +} + +VkImageLayout +grd_image_view_nv12_get_image_layout (GrdImageViewNV12 *image_view_nv12) +{ + g_assert (grd_vk_image_get_image_layout (image_view_nv12->vk_y_layer) == + grd_vk_image_get_image_layout (image_view_nv12->vk_uv_layer)); + + return grd_vk_image_get_image_layout (image_view_nv12->vk_y_layer); +} + +void +grd_image_view_nv12_set_image_layout (GrdImageViewNV12 *image_view_nv12, + VkImageLayout vk_image_layout) +{ + grd_vk_image_set_image_layout (image_view_nv12->vk_y_layer, vk_image_layout); + grd_vk_image_set_image_layout (image_view_nv12->vk_uv_layer, vk_image_layout); +} + +void +grd_image_view_nv12_notify_image_view_release (GrdImageView *image_view) +{ +} + +GrdImageViewNV12 * +grd_image_view_nv12_new (GrdVkImage *vk_y_layer, + GrdVkImage *vk_uv_layer) +{ + GrdImageViewNV12 *image_view_nv12; + + image_view_nv12 = g_object_new (GRD_TYPE_IMAGE_VIEW_NV12, NULL); + image_view_nv12->vk_y_layer = vk_y_layer; + image_view_nv12->vk_uv_layer = vk_uv_layer; + + return image_view_nv12; +} + +static void +grd_image_view_nv12_dispose (GObject *object) +{ + GrdImageViewNV12 *image_view_nv12 = GRD_IMAGE_VIEW_NV12 (object); + + g_clear_object (&image_view_nv12->vk_uv_layer); + g_clear_object (&image_view_nv12->vk_y_layer); + + G_OBJECT_CLASS (grd_image_view_nv12_parent_class)->dispose (object); +} + +static void +grd_image_view_nv12_init (GrdImageViewNV12 *image_view_nv12) +{ +} + +static void +grd_image_view_nv12_class_init (GrdImageViewNV12Class *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GrdImageViewClass *image_view_class = GRD_IMAGE_VIEW_CLASS (klass); + + object_class->dispose = grd_image_view_nv12_dispose; + + image_view_class->notify_image_view_release = + grd_image_view_nv12_notify_image_view_release; +} diff --git a/grd-image-view-nv12.h b/grd-image-view-nv12.h new file mode 100644 index 0000000..607d303 --- /dev/null +++ b/grd-image-view-nv12.h @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2024 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include + +#include "grd-image-view.h" +#include "grd-types.h" + +#define GRD_TYPE_IMAGE_VIEW_NV12 (grd_image_view_nv12_get_type ()) +G_DECLARE_FINAL_TYPE (GrdImageViewNV12, grd_image_view_nv12, + GRD, IMAGE_VIEW_NV12, GrdImageView) + +GrdImageViewNV12 *grd_image_view_nv12_new (GrdVkImage *vk_y_layer, + GrdVkImage *vk_uv_layer); + +VkImageView grd_image_view_nv12_get_y_layer (GrdImageViewNV12 *image_view_nv12); + +VkImageView grd_image_view_nv12_get_uv_layer (GrdImageViewNV12 *image_view_nv12); + +GList *grd_image_view_nv12_get_images (GrdImageViewNV12 *image_view_nv12); + +VkImageLayout grd_image_view_nv12_get_image_layout (GrdImageViewNV12 *image_view_nv12); + +void grd_image_view_nv12_set_image_layout (GrdImageViewNV12 *image_view_nv12, + VkImageLayout vk_image_layout); diff --git a/grd-image-view-rgb.c b/grd-image-view-rgb.c new file mode 100644 index 0000000..6adfe5d --- /dev/null +++ b/grd-image-view-rgb.c @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2024 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-image-view-rgb.h" + +struct _GrdImageViewRGB +{ + GrdImageView parent; + + GrdLocalBuffer *local_buffer; + GrdImageViewRGBReleaseCallback on_release; + gpointer on_release_user_data; +}; + +G_DEFINE_TYPE (GrdImageViewRGB, grd_image_view_rgb, + GRD_TYPE_IMAGE_VIEW) + +GrdLocalBuffer * +grd_image_view_rgb_get_local_buffer (GrdImageViewRGB *image_view_rgb) +{ + return image_view_rgb->local_buffer; +} + +void +grd_image_view_rgb_attach_local_buffer (GrdImageViewRGB *image_view_rgb, + GrdLocalBuffer *local_buffer, + GrdImageViewRGBReleaseCallback callback, + gpointer user_data) +{ + g_assert (!image_view_rgb->local_buffer); + + image_view_rgb->local_buffer = local_buffer; + image_view_rgb->on_release = callback; + image_view_rgb->on_release_user_data = user_data; +} + +static void +grd_image_view_rgb_notify_image_view_release (GrdImageView *image_view) +{ + GrdImageViewRGB *image_view_rgb = GRD_IMAGE_VIEW_RGB (image_view); + + if (!image_view_rgb->on_release) + return; + + image_view_rgb->on_release (image_view_rgb->on_release_user_data, + image_view_rgb->local_buffer); + + image_view_rgb->local_buffer = NULL; + image_view_rgb->on_release = NULL; + image_view_rgb->on_release_user_data = NULL; +} + +GrdImageViewRGB * +grd_image_view_rgb_new (void) +{ + return g_object_new (GRD_TYPE_IMAGE_VIEW_RGB, NULL); +} + +static void +grd_image_view_rgb_init (GrdImageViewRGB *image_view_rgb) +{ +} + +static void +grd_image_view_rgb_class_init (GrdImageViewRGBClass *klass) +{ + GrdImageViewClass *image_view_class = GRD_IMAGE_VIEW_CLASS (klass); + + image_view_class->notify_image_view_release = + grd_image_view_rgb_notify_image_view_release; +} diff --git a/grd-image-view-rgb.h b/grd-image-view-rgb.h new file mode 100644 index 0000000..1dbd307 --- /dev/null +++ b/grd-image-view-rgb.h @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2024 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include "grd-image-view.h" +#include "grd-types.h" + +#define GRD_TYPE_IMAGE_VIEW_RGB (grd_image_view_rgb_get_type ()) +G_DECLARE_FINAL_TYPE (GrdImageViewRGB, grd_image_view_rgb, + GRD, IMAGE_VIEW_RGB, GrdImageView) + +typedef void (* GrdImageViewRGBReleaseCallback) (gpointer user_data, + GrdLocalBuffer *local_buffer); + +GrdImageViewRGB *grd_image_view_rgb_new (void); + +GrdLocalBuffer *grd_image_view_rgb_get_local_buffer (GrdImageViewRGB *image_view_rgb); + +void grd_image_view_rgb_attach_local_buffer (GrdImageViewRGB *image_view_rgb, + GrdLocalBuffer *local_buffer, + GrdImageViewRGBReleaseCallback callback, + gpointer user_data); diff --git a/grd-image-view.c b/grd-image-view.c new file mode 100644 index 0000000..ae9dcd4 --- /dev/null +++ b/grd-image-view.c @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2024 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-image-view.h" + +G_DEFINE_ABSTRACT_TYPE (GrdImageView, grd_image_view, + G_TYPE_OBJECT) + +void +grd_image_view_notify_image_view_release (GrdImageView *image_view) +{ + GrdImageViewClass *klass = GRD_IMAGE_VIEW_GET_CLASS (image_view); + + klass->notify_image_view_release (image_view); +} + +static void +grd_image_view_init (GrdImageView *image_view) +{ +} + +static void +grd_image_view_class_init (GrdImageViewClass *klass) +{ +} diff --git a/grd-image-view.h b/grd-image-view.h new file mode 100644 index 0000000..a7b651e --- /dev/null +++ b/grd-image-view.h @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2024 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include + +#define GRD_TYPE_IMAGE_VIEW (grd_image_view_get_type ()) +G_DECLARE_DERIVABLE_TYPE (GrdImageView, grd_image_view, + GRD, IMAGE_VIEW, GObject) + +struct _GrdImageViewClass +{ + GObjectClass parent_class; + + void (* notify_image_view_release) (GrdImageView *image_view); +}; + +void grd_image_view_notify_image_view_release (GrdImageView *image_view); diff --git a/grd-local-buffer-copy.c b/grd-local-buffer-copy.c new file mode 100644 index 0000000..588f917 --- /dev/null +++ b/grd-local-buffer-copy.c @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2025 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-local-buffer-copy.h" + +struct _GrdLocalBufferCopy +{ + GrdLocalBuffer parent; + + uint8_t *buffer; + uint32_t buffer_stride; +}; + +G_DEFINE_TYPE (GrdLocalBufferCopy, grd_local_buffer_copy, + GRD_TYPE_LOCAL_BUFFER) + +static uint8_t * +grd_local_buffer_copy_get_buffer (GrdLocalBuffer *local_buffer) +{ + GrdLocalBufferCopy *local_buffer_copy = GRD_LOCAL_BUFFER_COPY (local_buffer); + + return local_buffer_copy->buffer; +} + +static uint32_t +grd_local_buffer_copy_get_buffer_stride (GrdLocalBuffer *local_buffer) +{ + GrdLocalBufferCopy *local_buffer_copy = GRD_LOCAL_BUFFER_COPY (local_buffer); + + return local_buffer_copy->buffer_stride; +} + +GrdLocalBufferCopy * +grd_local_buffer_copy_new (uint32_t buffer_width, + uint32_t buffer_height) +{ + GrdLocalBufferCopy *local_buffer_copy; + uint32_t buffer_stride; + + g_assert (buffer_width > 0); + g_assert (buffer_height > 0); + + local_buffer_copy = g_object_new (GRD_TYPE_LOCAL_BUFFER_COPY, NULL); + + buffer_stride = buffer_width * 4; + + local_buffer_copy->buffer = g_new0 (uint8_t, buffer_stride * buffer_height); + local_buffer_copy->buffer_stride = buffer_stride; + + return local_buffer_copy; +} + +static void +grd_local_buffer_copy_dispose (GObject *object) +{ + GrdLocalBufferCopy *local_buffer_copy = GRD_LOCAL_BUFFER_COPY (object); + + g_clear_pointer (&local_buffer_copy->buffer, g_free); + + G_OBJECT_CLASS (grd_local_buffer_copy_parent_class)->dispose (object); +} + +static void +grd_local_buffer_copy_init (GrdLocalBufferCopy *local_buffer) +{ +} + +static void +grd_local_buffer_copy_class_init (GrdLocalBufferCopyClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GrdLocalBufferClass *local_buffer_class = GRD_LOCAL_BUFFER_CLASS (klass); + + object_class->dispose = grd_local_buffer_copy_dispose; + + local_buffer_class->get_buffer = grd_local_buffer_copy_get_buffer; + local_buffer_class->get_buffer_stride = + grd_local_buffer_copy_get_buffer_stride; +} diff --git a/grd-local-buffer-copy.h b/grd-local-buffer-copy.h new file mode 100644 index 0000000..854bbf9 --- /dev/null +++ b/grd-local-buffer-copy.h @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2025 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include "grd-local-buffer.h" + +#define GRD_TYPE_LOCAL_BUFFER_COPY (grd_local_buffer_copy_get_type ()) +G_DECLARE_FINAL_TYPE (GrdLocalBufferCopy, grd_local_buffer_copy, + GRD, LOCAL_BUFFER_COPY, GrdLocalBuffer) + +GrdLocalBufferCopy *grd_local_buffer_copy_new (uint32_t buffer_width, + uint32_t buffer_height); diff --git a/grd-local-buffer-wrapper-rdp.c b/grd-local-buffer-wrapper-rdp.c new file mode 100644 index 0000000..1e084f7 --- /dev/null +++ b/grd-local-buffer-wrapper-rdp.c @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2025 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-local-buffer-wrapper-rdp.h" + +#include "grd-rdp-buffer.h" +#include "grd-rdp-pw-buffer.h" + +struct _GrdLocalBufferWrapperRdp +{ + GrdLocalBuffer parent; + + uint8_t *buffer; + int32_t buffer_stride; +}; + +G_DEFINE_TYPE (GrdLocalBufferWrapperRdp, grd_local_buffer_wrapper_rdp, + GRD_TYPE_LOCAL_BUFFER) + +void +grd_local_buffer_wrapper_rdp_attach_rdp_buffer (GrdLocalBufferWrapperRdp *buffer_wrapper, + GrdRdpBuffer *rdp_buffer) +{ + GrdRdpPwBuffer *rdp_pw_buffer = grd_rdp_buffer_get_rdp_pw_buffer (rdp_buffer); + + buffer_wrapper->buffer = + grd_rdp_pw_buffer_get_mapped_data (rdp_pw_buffer, + &buffer_wrapper->buffer_stride); +} + +static uint8_t * +grd_local_buffer_wrapper_rdp_get_buffer (GrdLocalBuffer *local_buffer) +{ + GrdLocalBufferWrapperRdp *buffer_wrapper = + GRD_LOCAL_BUFFER_WRAPPER_RDP (local_buffer); + + return buffer_wrapper->buffer; +} + +static uint32_t +grd_local_buffer_wrapper_rdp_get_buffer_stride (GrdLocalBuffer *local_buffer) +{ + GrdLocalBufferWrapperRdp *buffer_wrapper = + GRD_LOCAL_BUFFER_WRAPPER_RDP (local_buffer); + + return buffer_wrapper->buffer_stride; +} + +GrdLocalBufferWrapperRdp * +grd_local_buffer_wrapper_rdp_new (void) +{ + return g_object_new (GRD_TYPE_LOCAL_BUFFER_WRAPPER_RDP, NULL); +} + +static void +grd_local_buffer_wrapper_rdp_init (GrdLocalBufferWrapperRdp *local_buffer) +{ +} + +static void +grd_local_buffer_wrapper_rdp_class_init (GrdLocalBufferWrapperRdpClass *klass) +{ + GrdLocalBufferClass *local_buffer_class = GRD_LOCAL_BUFFER_CLASS (klass); + + local_buffer_class->get_buffer = grd_local_buffer_wrapper_rdp_get_buffer; + local_buffer_class->get_buffer_stride = + grd_local_buffer_wrapper_rdp_get_buffer_stride; +} diff --git a/grd-local-buffer-wrapper-rdp.h b/grd-local-buffer-wrapper-rdp.h new file mode 100644 index 0000000..2781719 --- /dev/null +++ b/grd-local-buffer-wrapper-rdp.h @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2025 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include "grd-local-buffer.h" +#include "grd-types.h" + +#define GRD_TYPE_LOCAL_BUFFER_WRAPPER_RDP (grd_local_buffer_wrapper_rdp_get_type ()) +G_DECLARE_FINAL_TYPE (GrdLocalBufferWrapperRdp, grd_local_buffer_wrapper_rdp, + GRD, LOCAL_BUFFER_WRAPPER_RDP, GrdLocalBuffer) + +GrdLocalBufferWrapperRdp *grd_local_buffer_wrapper_rdp_new (void); + +void grd_local_buffer_wrapper_rdp_attach_rdp_buffer (GrdLocalBufferWrapperRdp *buffer_wrapper, + GrdRdpBuffer *rdp_buffer); diff --git a/grd-local-buffer.c b/grd-local-buffer.c new file mode 100644 index 0000000..4fc9273 --- /dev/null +++ b/grd-local-buffer.c @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2024 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-local-buffer.h" + +G_DEFINE_ABSTRACT_TYPE (GrdLocalBuffer, grd_local_buffer, + G_TYPE_OBJECT) + +uint8_t * +grd_local_buffer_get_buffer (GrdLocalBuffer *local_buffer) +{ + GrdLocalBufferClass *klass = GRD_LOCAL_BUFFER_GET_CLASS (local_buffer); + + return klass->get_buffer (local_buffer); +} + +uint32_t +grd_local_buffer_get_buffer_stride (GrdLocalBuffer *local_buffer) +{ + GrdLocalBufferClass *klass = GRD_LOCAL_BUFFER_GET_CLASS (local_buffer); + + return klass->get_buffer_stride (local_buffer); +} + +static void +grd_local_buffer_init (GrdLocalBuffer *local_buffer) +{ +} + +static void +grd_local_buffer_class_init (GrdLocalBufferClass *klass) +{ +} diff --git a/grd-local-buffer.h b/grd-local-buffer.h new file mode 100644 index 0000000..2efffdb --- /dev/null +++ b/grd-local-buffer.h @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2024 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include +#include + +#define GRD_TYPE_LOCAL_BUFFER (grd_local_buffer_get_type ()) +G_DECLARE_DERIVABLE_TYPE (GrdLocalBuffer, grd_local_buffer, + GRD, LOCAL_BUFFER, GObject) + +struct _GrdLocalBufferClass +{ + GObjectClass parent_class; + + uint8_t *(* get_buffer) (GrdLocalBuffer *local_buffer); + uint32_t (* get_buffer_stride) (GrdLocalBuffer *local_buffer); +}; + +uint8_t *grd_local_buffer_get_buffer (GrdLocalBuffer *local_buffer); + +uint32_t grd_local_buffer_get_buffer_stride (GrdLocalBuffer *local_buffer); diff --git a/grd-mime-type.c b/grd-mime-type.c new file mode 100644 index 0000000..7da7e7d --- /dev/null +++ b/grd-mime-type.c @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2020 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-mime-type.h" + +#include + +const char * +grd_mime_type_to_string (GrdMimeType mime_type) +{ + switch (mime_type) + { + case GRD_MIME_TYPE_TEXT_PLAIN: + return "text/plain"; + case GRD_MIME_TYPE_TEXT_PLAIN_UTF8: + return "text/plain;charset=utf-8"; + case GRD_MIME_TYPE_TEXT_UTF8_STRING: + return "UTF8_STRING"; + case GRD_MIME_TYPE_TEXT_HTML: + return "text/html"; + case GRD_MIME_TYPE_IMAGE_BMP: + return "image/bmp"; + case GRD_MIME_TYPE_IMAGE_TIFF: + return "image/tiff"; + case GRD_MIME_TYPE_IMAGE_GIF: + return "image/gif"; + case GRD_MIME_TYPE_IMAGE_JPEG: + return "image/jpeg"; + case GRD_MIME_TYPE_IMAGE_PNG: + return "image/png"; + case GRD_MIME_TYPE_TEXT_URILIST: + return "text/uri-list"; + case GRD_MIME_TYPE_XS_GNOME_COPIED_FILES: + return "x-special/gnome-copied-files"; + default: + return NULL; + } + + g_assert_not_reached (); +} + +GrdMimeType +grd_mime_type_from_string (const char *mime_type_string) +{ + if (strcmp (mime_type_string, "text/plain") == 0) + return GRD_MIME_TYPE_TEXT_PLAIN; + else if (strcmp (mime_type_string, "text/plain;charset=utf-8") == 0) + return GRD_MIME_TYPE_TEXT_PLAIN_UTF8; + else if (strcmp (mime_type_string, "UTF8_STRING") == 0) + return GRD_MIME_TYPE_TEXT_UTF8_STRING; + else if (strcmp (mime_type_string, "text/html") == 0) + return GRD_MIME_TYPE_TEXT_HTML; + else if (strcmp (mime_type_string, "image/bmp") == 0) + return GRD_MIME_TYPE_IMAGE_BMP; + else if (strcmp (mime_type_string, "image/tiff") == 0) + return GRD_MIME_TYPE_IMAGE_TIFF; + else if (strcmp (mime_type_string, "image/gif") == 0) + return GRD_MIME_TYPE_IMAGE_GIF; + else if (strcmp (mime_type_string, "image/jpeg") == 0) + return GRD_MIME_TYPE_IMAGE_JPEG; + else if (strcmp (mime_type_string, "image/png") == 0) + return GRD_MIME_TYPE_IMAGE_PNG; + else if (strcmp (mime_type_string, "text/uri-list") == 0) + return GRD_MIME_TYPE_TEXT_URILIST; + else if (strcmp (mime_type_string, "x-special/gnome-copied-files") == 0) + return GRD_MIME_TYPE_XS_GNOME_COPIED_FILES; + + return GRD_MIME_TYPE_NONE; +} diff --git a/grd-mime-type.h b/grd-mime-type.h new file mode 100644 index 0000000..ed97cdc --- /dev/null +++ b/grd-mime-type.h @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2020 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include + +typedef enum _GrdMimeType +{ + GRD_MIME_TYPE_NONE, + GRD_MIME_TYPE_TEXT_PLAIN, /* text/plain */ + GRD_MIME_TYPE_TEXT_PLAIN_UTF8, /* text/plain;charset=utf-8 */ + GRD_MIME_TYPE_TEXT_UTF8_STRING, /* UTF8_STRING */ + GRD_MIME_TYPE_TEXT_HTML, /* text/html */ + GRD_MIME_TYPE_IMAGE_BMP, /* image/bmp */ + GRD_MIME_TYPE_IMAGE_TIFF, /* image/tiff */ + GRD_MIME_TYPE_IMAGE_GIF, /* image/gif */ + GRD_MIME_TYPE_IMAGE_JPEG, /* image/jpeg */ + GRD_MIME_TYPE_IMAGE_PNG, /* image/png */ + GRD_MIME_TYPE_TEXT_URILIST, /* text/uri-list */ + GRD_MIME_TYPE_XS_GNOME_COPIED_FILES, /* x-special/gnome-copied-files */ +} GrdMimeType; + +typedef struct _GrdMimeTypeTable +{ + GrdMimeType mime_type; + + struct + { + uint32_t format_id; + } rdp; +} GrdMimeTypeTable; + +const char *grd_mime_type_to_string (GrdMimeType mime_type); + +GrdMimeType grd_mime_type_from_string (const char *mime_type_string); diff --git a/grd-nal-writer.c b/grd-nal-writer.c new file mode 100644 index 0000000..697418c --- /dev/null +++ b/grd-nal-writer.c @@ -0,0 +1,886 @@ +/* + * Copyright (C) 2022 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-nal-writer.h" + +#define H264_PROFILE_HIGH 100 + +/* See also E.2.1 VUI parameters semantics (Rec. ITU-T H.264 (08/2021)) */ +#define H264_Extended_SAR 255 + +#define H264_NAL_REF_IDC_ZERO 0 +#define H264_NAL_REF_IDC_MEDIUM 2 +#define H264_NAL_REF_IDC_HIGH 3 + +/* + * See also Table 7-1 – NAL unit type codes, syntax element categories, + * and NAL unit type classes (Rec. ITU-T H.264 (08/2021)) + */ +#define H264_NAL_UNIT_TYPE_SLICE_NON_IDR 1 +#define H264_NAL_UNIT_TYPE_SLICE_IDR 5 +#define H264_NAL_UNIT_TYPE_SPS 7 +#define H264_NAL_UNIT_TYPE_PPS 8 +#define H264_NAL_UNIT_TYPE_AUD 9 + +/* + * See also Table 7-6 – Name association to slice_type + * (Rec. ITU-T H.264 (08/2021)) + */ +#define H264_SLICE_TYPE_P 0 +#define H264_SLICE_TYPE_B 1 +#define H264_SLICE_TYPE_I 2 + +#define BITSTREAM_ALLOCATION_STEP 4096 + +typedef struct +{ + uint32_t *buffer; + uint32_t bit_offset; + uint32_t capacity; +} NalBitstream; + +struct _GrdNalWriter +{ + GObject parent; + + NalBitstream *nal_bitstream; +}; + +G_DEFINE_TYPE (GrdNalWriter, grd_nal_writer, G_TYPE_OBJECT) + +static void +start_bitstream (GrdNalWriter *nal_writer) +{ + g_assert (!nal_writer->nal_bitstream); + + nal_writer->nal_bitstream = g_new0 (NalBitstream, 1); +} + +static uint32_t +swap_byte_order (uint32_t value) +{ + uint8_t *ptr = (uint8_t *) &value; + + return ptr[0] << 24 | ptr[1] << 16 | ptr[2] << 8 | ptr[3]; +} + +static uint8_t * +end_bitstream (GrdNalWriter *nal_writer, + uint32_t *bitstream_length) +{ + NalBitstream *nal_bitstream = nal_writer->nal_bitstream; + uint32_t *buffer = nal_bitstream->buffer; + uint32_t offset_in_dword; + uint32_t byte_pos; + uint32_t bits_left; + + byte_pos = nal_bitstream->bit_offset >> 5; + offset_in_dword = nal_bitstream->bit_offset & 0x1F; + bits_left = 32 - offset_in_dword; + + if (bits_left) + buffer[byte_pos] = swap_byte_order (buffer[byte_pos] << bits_left); + + *bitstream_length = nal_bitstream->bit_offset; + g_clear_pointer (&nal_writer->nal_bitstream, g_free); + + return (uint8_t *) buffer; +} + +static void +ensure_remaining_capacity (NalBitstream *nal_bitstream, + uint32_t required_capacity_in_bits) +{ + uint32_t new_capacity; + + if (required_capacity_in_bits <= nal_bitstream->capacity * 32) + return; + + new_capacity = nal_bitstream->capacity + BITSTREAM_ALLOCATION_STEP; + g_assert (required_capacity_in_bits <= new_capacity * 32); + + nal_bitstream->buffer = g_realloc (nal_bitstream->buffer, + new_capacity * sizeof (uint32_t)); + nal_bitstream->capacity = new_capacity; +} + +static void +write_u (GrdNalWriter *nal_writer, + uint32_t value, + uint32_t n_bits) +{ + NalBitstream *nal_bitstream = nal_writer->nal_bitstream; + uint32_t offset_in_dword; + uint32_t byte_pos; + uint32_t bits_left; + + if (n_bits == 0) + return; + + ensure_remaining_capacity (nal_bitstream, nal_bitstream->bit_offset + n_bits); + + byte_pos = nal_bitstream->bit_offset >> 5; + offset_in_dword = nal_bitstream->bit_offset & 0x1F; + bits_left = 32 - offset_in_dword; + + if (!offset_in_dword) + nal_bitstream->buffer[byte_pos] = 0; + + nal_bitstream->bit_offset += n_bits; + if (bits_left > n_bits) + { + nal_bitstream->buffer[byte_pos] <<= n_bits; + nal_bitstream->buffer[byte_pos] |= value; + return; + } + + n_bits -= bits_left; + nal_bitstream->buffer[byte_pos] <<= bits_left; + nal_bitstream->buffer[byte_pos] |= value >> n_bits; + nal_bitstream->buffer[byte_pos] = + swap_byte_order (nal_bitstream->buffer[byte_pos]); + + nal_bitstream->buffer[++byte_pos] = value; +} + +/* Exponential Golomb coding (unsigned) */ +static void +write_ue (GrdNalWriter *nal_writer, + uint32_t value) +{ + uint32_t value_to_write; + uint32_t n_bits = 0; + uint32_t tmp; + + /* + * Write down value + 1, but before that write down n - 1 zeros, + * where n represents the bits to be written for value + 1 + */ + value_to_write = value + 1; + tmp = value_to_write; + while (tmp) + { + ++n_bits; + tmp >>= 1; + } + + if (n_bits > 1) + write_u (nal_writer, 0, n_bits - 1); + write_u (nal_writer, value_to_write, n_bits); +} + +/* Exponential Golomb coding (signed) */ +static void +write_se (GrdNalWriter *nal_writer, + int32_t value) +{ + /* + * If the value is <= 0, map the value to -2 * value (even integer value), + * otherwise map it to 2 * value - 1 (odd integer value) + */ + if (value <= 0) + write_ue (nal_writer, -value << 1); + else + write_ue (nal_writer, (value << 1) - 1); +} + +static void +write_nal_start_code_prefix (GrdNalWriter *nal_writer) +{ + write_u (nal_writer, 0x00000001, 32); +} + +static void +write_nal_header (GrdNalWriter *nal_writer, + uint8_t nal_ref_idc, + uint8_t nal_unit_type) +{ + /* See also 7.3.1 NAL unit syntax (Rec. ITU-T H.264 (08/2021)) */ + /* forbidden_zero_bit */ + write_u (nal_writer, 0, 1); + /* nal_ref_idc */ + write_u (nal_writer, nal_ref_idc, 2); + /* nal_unit_type */ + write_u (nal_writer, nal_unit_type, 5); +} + +static void +byte_align_bitstream (GrdNalWriter *nal_writer) +{ + NalBitstream *nal_bitstream = nal_writer->nal_bitstream; + uint32_t offset_in_byte; + uint32_t bits_left; + + offset_in_byte = nal_bitstream->bit_offset & 0x7; + bits_left = 8 - offset_in_byte; + + if (!bits_left) + return; + + /* rbsp_alignment_zero_bit */ + write_u (nal_writer, 0, bits_left); +} + +static void +write_trailing_bits (GrdNalWriter *nal_writer) +{ + /* rbsp_stop_one_bit */ + write_u (nal_writer, 1, 1); + byte_align_bitstream (nal_writer); +} + +static void +write_access_unit_delimiter (GrdNalWriter *nal_writer) +{ + uint32_t primary_pic_type; + + primary_pic_type = 1; + + write_u (nal_writer, primary_pic_type, 3); +} + +uint8_t * +grd_nal_writer_get_aud_bitstream (GrdNalWriter *nal_writer, + uint32_t *bitstream_length) +{ + uint8_t *bitstream; + + start_bitstream (nal_writer); + write_nal_start_code_prefix (nal_writer); + write_nal_header (nal_writer, H264_NAL_REF_IDC_ZERO, H264_NAL_UNIT_TYPE_AUD); + write_access_unit_delimiter (nal_writer); + write_trailing_bits (nal_writer); + + bitstream = end_bitstream (nal_writer, bitstream_length); + g_assert (*bitstream_length % 8 == 0); + + return bitstream; +} + +static void +write_vui_parameters (GrdNalWriter *nal_writer, + const VAEncSequenceParameterBufferH264 *sequence_param) +{ + uint32_t aspect_ratio_info_present_flag; + uint32_t overscan_info_present_flag; + uint32_t video_signal_type_present_flag; + uint32_t chroma_loc_info_present_flag; + uint32_t timing_info_present_flag; + uint32_t nal_hrd_parameters_present_flag; + uint32_t vcl_hrd_parameters_present_flag; + uint32_t pic_struct_present_flag; + uint32_t bitstream_restriction_flag; + + g_assert (sequence_param->vui_parameters_present_flag); + + aspect_ratio_info_present_flag = + sequence_param->vui_fields.bits.aspect_ratio_info_present_flag; + timing_info_present_flag = + sequence_param->vui_fields.bits.timing_info_present_flag; + bitstream_restriction_flag = + sequence_param->vui_fields.bits.bitstream_restriction_flag; + + overscan_info_present_flag = 0; + video_signal_type_present_flag = 0; + chroma_loc_info_present_flag = 0; + nal_hrd_parameters_present_flag = 0; + vcl_hrd_parameters_present_flag = 0; + pic_struct_present_flag = 0; + + /* + * See also E.1.1 VUI parameters syntax (Rec. ITU-T H.264 (08/2021)) + * + * Not all paths are covered, only the ones relevant for GNOME Remote Desktop. + * Unhandled branches are preceded with an assertion. + */ + + /* aspect_ratio_info_present_flag */ + write_u (nal_writer, aspect_ratio_info_present_flag, 1); + if (aspect_ratio_info_present_flag) + { + /* aspect_ratio_idc */ + write_u (nal_writer, sequence_param->aspect_ratio_idc, 8); + if (sequence_param->aspect_ratio_idc == H264_Extended_SAR) + { + /* sar_width */ + write_u (nal_writer, sequence_param->sar_width, 16); + /* sar_height */ + write_u (nal_writer, sequence_param->sar_height, 16); + } + } + /* overscan_info_present_flag */ + write_u (nal_writer, overscan_info_present_flag, 1); + g_assert (!overscan_info_present_flag); + + /* video_signal_type_present_flag */ + write_u (nal_writer, video_signal_type_present_flag, 1); + g_assert (!video_signal_type_present_flag); + + /* chroma_loc_info_present_flag */ + write_u (nal_writer, chroma_loc_info_present_flag, 1); + g_assert (!chroma_loc_info_present_flag); + + /* timing_info_present_flag */ + write_u (nal_writer, timing_info_present_flag, 1); + if (timing_info_present_flag) + { + uint32_t fixed_frame_rate_flag = + sequence_param->vui_fields.bits.fixed_frame_rate_flag; + + /* num_units_in_tick */ + write_u (nal_writer, sequence_param->num_units_in_tick, 32); + /* time_scale */ + write_u (nal_writer, sequence_param->time_scale, 32); + /* fixed_frame_rate_flag */ + write_u (nal_writer, fixed_frame_rate_flag, 1); + } + + /* nal_hrd_parameters_present_flag */ + write_u (nal_writer, nal_hrd_parameters_present_flag, 1); + g_assert (!nal_hrd_parameters_present_flag); + + /* vcl_hrd_parameters_present_flag */ + write_u (nal_writer, vcl_hrd_parameters_present_flag, 1); + g_assert (!vcl_hrd_parameters_present_flag); + + g_assert (!nal_hrd_parameters_present_flag && + !vcl_hrd_parameters_present_flag); + + /* pic_struct_present_flag */ + write_u (nal_writer, pic_struct_present_flag, 1); + /* bitstream_restriction_flag */ + write_u (nal_writer, bitstream_restriction_flag, 1); + if (bitstream_restriction_flag) + { + uint32_t motion_vectors_over_pic_boundaries_flag; + uint32_t max_bytes_per_pic_denom; + uint32_t max_bits_per_mb_denom; + uint32_t log2_max_mv_length_horizontal; + uint32_t log2_max_mv_length_vertical; + uint32_t max_num_reorder_frames; + uint32_t max_dec_frame_buffering; + + log2_max_mv_length_horizontal = + sequence_param->vui_fields.bits.log2_max_mv_length_horizontal; + log2_max_mv_length_vertical = + sequence_param->vui_fields.bits.log2_max_mv_length_vertical; + + motion_vectors_over_pic_boundaries_flag = 1; + max_bytes_per_pic_denom = 0; + max_bits_per_mb_denom = 0; + max_num_reorder_frames = 0; + max_dec_frame_buffering = 1; + + /* motion_vectors_over_pic_boundaries_flag */ + write_u (nal_writer, motion_vectors_over_pic_boundaries_flag, 1); + /* max_bytes_per_pic_denom */ + write_ue (nal_writer, max_bytes_per_pic_denom); + /* max_bits_per_mb_denom */ + write_ue (nal_writer, max_bits_per_mb_denom); + /* log2_max_mv_length_horizontal */ + write_ue (nal_writer, log2_max_mv_length_horizontal); + /* log2_max_mv_length_vertical */ + write_ue (nal_writer, log2_max_mv_length_vertical); + /* max_num_reorder_frames */ + write_ue (nal_writer, max_num_reorder_frames); + /* max_dec_frame_buffering */ + write_ue (nal_writer, max_dec_frame_buffering); + } +} + +static void +write_sps_data (GrdNalWriter *nal_writer, + const VAEncSequenceParameterBufferH264 *sequence_param) +{ + uint32_t profile_idc; + uint32_t constraint_set0_flag; + uint32_t constraint_set1_flag; + uint32_t constraint_set2_flag; + uint32_t constraint_set3_flag; + uint32_t constraint_set4_flag; + uint32_t constraint_set5_flag; + uint32_t chroma_format_idc; + uint32_t qpprime_y_zero_transform_bypass_flag; + uint32_t seq_scaling_matrix_present_flag; + uint32_t log2_max_frame_num_minus4; + uint32_t pic_order_cnt_type; + uint32_t gaps_in_frame_num_value_allowed_flag; + uint32_t pic_height_in_map_units; + uint32_t frame_mbs_only_flag; + uint32_t direct_8x8_inference_flag; + + g_assert (sequence_param->picture_width_in_mbs > 0); + + frame_mbs_only_flag = sequence_param->seq_fields.bits.frame_mbs_only_flag; + + /* + * See also 7.4.2.1.1 Sequence parameter set data semantics + * (Rec. ITU-T H.264 (08/2021)) + */ + profile_idc = H264_PROFILE_HIGH; + constraint_set0_flag = 0; + constraint_set1_flag = 0; + constraint_set2_flag = 0; + constraint_set3_flag = 0; + + g_assert (profile_idc == H264_PROFILE_HIGH); + g_assert (frame_mbs_only_flag == 1); + /* frame_mbs_only_flag is equal to 1 */ + constraint_set4_flag = 1; + + g_assert (profile_idc == H264_PROFILE_HIGH); + /* No B-slices are present in the coded video sequence */ + constraint_set5_flag = 1; + + chroma_format_idc = sequence_param->seq_fields.bits.chroma_format_idc; + seq_scaling_matrix_present_flag = + sequence_param->seq_fields.bits.seq_scaling_matrix_present_flag; + log2_max_frame_num_minus4 = + sequence_param->seq_fields.bits.log2_max_frame_num_minus4; + pic_order_cnt_type = sequence_param->seq_fields.bits.pic_order_cnt_type; + direct_8x8_inference_flag = + sequence_param->seq_fields.bits.direct_8x8_inference_flag; + + qpprime_y_zero_transform_bypass_flag = 0; + gaps_in_frame_num_value_allowed_flag = 0; + + g_assert (frame_mbs_only_flag); + pic_height_in_map_units = sequence_param->picture_height_in_mbs; + g_assert (pic_height_in_map_units > 0); + + /* + * See also 7.3.2.1.1 Sequence parameter set data syntax + * (Rec. ITU-T H.264 (08/2021)) + * + * Not all paths are covered, only the ones relevant for GNOME Remote Desktop. + * Unhandled branches are preceded with an assertion. + */ + /* profile_idc */ + write_u (nal_writer, profile_idc, 8); + /* constraint_set0_flag */ + write_u (nal_writer, constraint_set0_flag, 1); + /* constraint_set1_flag */ + write_u (nal_writer, constraint_set1_flag, 1); + /* constraint_set2_flag */ + write_u (nal_writer, constraint_set2_flag, 1); + /* constraint_set3_flag */ + write_u (nal_writer, constraint_set3_flag, 1); + /* constraint_set4_flag */ + write_u (nal_writer, constraint_set4_flag, 1); + /* constraint_set5_flag */ + write_u (nal_writer, constraint_set5_flag, 1); + /* reserved_zero_2bits */ + write_u (nal_writer, 0, 2); + /* level_idc */ + write_u (nal_writer, sequence_param->level_idc, 8); + /* seq_parameter_set_id */ + write_ue (nal_writer, sequence_param->seq_parameter_set_id); + + g_assert (profile_idc == H264_PROFILE_HIGH); + /* chroma_format_idc */ + write_ue (nal_writer, chroma_format_idc); + g_assert (chroma_format_idc != 3); + + /* bit_depth_luma_minus8 */ + write_ue (nal_writer, sequence_param->bit_depth_luma_minus8); + /* bit_depth_chroma_minus8 */ + write_ue (nal_writer, sequence_param->bit_depth_chroma_minus8); + /* qpprime_y_zero_transform_bypass_flag */ + write_u (nal_writer, qpprime_y_zero_transform_bypass_flag, 1); + /* seq_scaling_matrix_present_flag */ + write_u (nal_writer, seq_scaling_matrix_present_flag, 1); + g_assert (!seq_scaling_matrix_present_flag); + + /* log2_max_frame_num_minus4 */ + write_ue (nal_writer, log2_max_frame_num_minus4); + /* pic_order_cnt_type */ + write_ue (nal_writer, pic_order_cnt_type); + if (pic_order_cnt_type == 0) + g_assert_not_reached (); + else if (pic_order_cnt_type == 1) + g_assert_not_reached (); + + /* max_num_ref_frames */ + write_ue (nal_writer, sequence_param->max_num_ref_frames); + /* gaps_in_frame_num_value_allowed_flag */ + write_u (nal_writer, gaps_in_frame_num_value_allowed_flag, 1); + /* pic_width_in_mbs_minus1 */ + write_ue (nal_writer, sequence_param->picture_width_in_mbs - 1); + /* pic_height_in_map_units_minus1 */ + write_ue (nal_writer, pic_height_in_map_units - 1); + /* frame_mbs_only_flag */ + write_u (nal_writer, frame_mbs_only_flag, 1); + g_assert (frame_mbs_only_flag); + + /* direct_8x8_inference_flag */ + write_u (nal_writer, direct_8x8_inference_flag, 1); + /* frame_cropping_flag */ + write_u (nal_writer, sequence_param->frame_cropping_flag, 1); + g_assert (!sequence_param->frame_cropping_flag); + + /* vui_parameters_present_flag */ + write_u (nal_writer, sequence_param->vui_parameters_present_flag, 1); + if (sequence_param->vui_parameters_present_flag) + write_vui_parameters (nal_writer, sequence_param); +} + +uint8_t * +grd_nal_writer_get_sps_bitstream (GrdNalWriter *nal_writer, + const VAEncSequenceParameterBufferH264 *sequence_param, + uint32_t *bitstream_length) +{ + uint8_t *bitstream; + + start_bitstream (nal_writer); + write_nal_start_code_prefix (nal_writer); + write_nal_header (nal_writer, H264_NAL_REF_IDC_HIGH, H264_NAL_UNIT_TYPE_SPS); + write_sps_data (nal_writer, sequence_param); + write_trailing_bits (nal_writer); + + bitstream = end_bitstream (nal_writer, bitstream_length); + g_assert (*bitstream_length % 8 == 0); + + return bitstream; +} + +static void +write_pps_data (GrdNalWriter *nal_writer, + const VAEncPictureParameterBufferH264 *picture_param) +{ + uint32_t entropy_coding_mode_flag; + uint32_t bottom_field_pic_order_in_frame_present_flag; + uint32_t num_slice_groups_minus1; + uint32_t weighted_pred_flag; + uint32_t weighted_bipred_idc; + uint32_t pic_init_qs_minus26; + uint32_t deblocking_filter_control_present_flag; + uint32_t constrained_intra_pred_flag; + uint32_t redundant_pic_cnt_present_flag; + uint32_t transform_8x8_mode_flag; + uint32_t pic_scaling_matrix_present_flag; + + entropy_coding_mode_flag = + picture_param->pic_fields.bits.entropy_coding_mode_flag; + bottom_field_pic_order_in_frame_present_flag = + picture_param->pic_fields.bits.pic_order_present_flag; + weighted_pred_flag = picture_param->pic_fields.bits.weighted_pred_flag; + weighted_bipred_idc = picture_param->pic_fields.bits.weighted_bipred_idc; + deblocking_filter_control_present_flag = + picture_param->pic_fields.bits.deblocking_filter_control_present_flag; + constrained_intra_pred_flag = + picture_param->pic_fields.bits.constrained_intra_pred_flag; + redundant_pic_cnt_present_flag = + picture_param->pic_fields.bits.redundant_pic_cnt_present_flag; + transform_8x8_mode_flag = + picture_param->pic_fields.bits.transform_8x8_mode_flag; + pic_scaling_matrix_present_flag = + picture_param->pic_fields.bits.pic_scaling_matrix_present_flag; + + num_slice_groups_minus1 = 0; + pic_init_qs_minus26 = 0; + + /* + * See also 7.3.2.2 Picture parameter set RBSP syntax + * (Rec. ITU-T H.264 (08/2021)) + * + * Not all paths are covered, only the ones relevant for GNOME Remote Desktop. + * Unhandled branches are preceded with an assertion. + */ + /* pic_parameter_set_id */ + write_ue (nal_writer, picture_param->pic_parameter_set_id); + /* seq_parameter_set_id */ + write_ue (nal_writer, picture_param->seq_parameter_set_id); + /* entropy_coding_mode_flag */ + write_u (nal_writer, entropy_coding_mode_flag, 1); + /* bottom_field_pic_order_in_frame_present_flag */ + write_u (nal_writer, bottom_field_pic_order_in_frame_present_flag, 1); + /* num_slice_groups_minus1 */ + write_ue (nal_writer, num_slice_groups_minus1); + g_assert (num_slice_groups_minus1 == 0); + + /* num_ref_idx_l0_default_active_minus1 */ + write_ue (nal_writer, picture_param->num_ref_idx_l0_active_minus1); + /* num_ref_idx_l1_default_active_minus1 */ + write_ue (nal_writer, picture_param->num_ref_idx_l1_active_minus1); + /* weighted_pred_flag */ + write_u (nal_writer, weighted_pred_flag, 1); + /* weighted_bipred_idc */ + write_u (nal_writer, weighted_bipred_idc, 2); + /* pic_init_qp_minus26 */ + write_se (nal_writer, picture_param->pic_init_qp - 26); + /* pic_init_qs_minus26 */ + write_se (nal_writer, pic_init_qs_minus26); + /* chroma_qp_index_offset */ + write_se (nal_writer, picture_param->chroma_qp_index_offset); + /* deblocking_filter_control_present_flag */ + write_u (nal_writer, deblocking_filter_control_present_flag, 1); + /* constrained_intra_pred_flag */ + write_u (nal_writer, constrained_intra_pred_flag, 1); + /* redundant_pic_cnt_present_flag */ + write_u (nal_writer, redundant_pic_cnt_present_flag, 1); + + /* more_rbsp_data */ + /* transform_8x8_mode_flag */ + write_u (nal_writer, transform_8x8_mode_flag, 1); + /* pic_scaling_matrix_present_flag */ + write_u (nal_writer, pic_scaling_matrix_present_flag, 1); + g_assert (!pic_scaling_matrix_present_flag); + + /* second_chroma_qp_index_offset */ + write_se (nal_writer, picture_param->second_chroma_qp_index_offset); +} + +uint8_t * +grd_nal_writer_get_pps_bitstream (GrdNalWriter *nal_writer, + const VAEncPictureParameterBufferH264 *picture_param, + uint32_t *bitstream_length) +{ + uint8_t *bitstream; + + start_bitstream (nal_writer); + write_nal_start_code_prefix (nal_writer); + write_nal_header (nal_writer, H264_NAL_REF_IDC_HIGH, H264_NAL_UNIT_TYPE_PPS); + write_pps_data (nal_writer, picture_param); + write_trailing_bits (nal_writer); + + bitstream = end_bitstream (nal_writer, bitstream_length); + g_assert (*bitstream_length % 8 == 0); + + return bitstream; +} + +static void +write_ref_pic_list_modification (GrdNalWriter *nal_writer, + const VAEncSliceParameterBufferH264 *slice_param) +{ + if (slice_param->slice_type != H264_SLICE_TYPE_I) + { + uint32_t ref_pic_list_modification_flag_l0; + + ref_pic_list_modification_flag_l0 = 0; + + /* ref_pic_list_modification_flag_l0 */ + write_u (nal_writer, ref_pic_list_modification_flag_l0, 1); + g_assert (!ref_pic_list_modification_flag_l0); + } + g_assert (slice_param->slice_type != H264_SLICE_TYPE_B); +} + +static void +write_dec_ref_pic_marking (GrdNalWriter *nal_writer, + const VAEncPictureParameterBufferH264 *picture_param) +{ + if (picture_param->pic_fields.bits.idr_pic_flag) + { + uint32_t no_output_of_prior_pics_flag; + uint32_t long_term_reference_flag; + + no_output_of_prior_pics_flag = 0; + long_term_reference_flag = 0; + + /* no_output_of_prior_pics_flag */ + write_u (nal_writer, no_output_of_prior_pics_flag, 1); + /* long_term_reference_flag */ + write_u (nal_writer, long_term_reference_flag, 1); + } + else + { + uint32_t adaptive_ref_pic_marking_mode_flag; + + adaptive_ref_pic_marking_mode_flag = 0; + + /* adaptive_ref_pic_marking_mode_flag */ + write_u (nal_writer, adaptive_ref_pic_marking_mode_flag, 1); + g_assert (!adaptive_ref_pic_marking_mode_flag); + } +} + +static void +write_slice_header (GrdNalWriter *nal_writer, + const VAEncSliceParameterBufferH264 *slice_param, + const VAEncSequenceParameterBufferH264 *sequence_param, + const VAEncPictureParameterBufferH264 *picture_param, + const uint8_t nal_ref_idc) +{ + uint32_t separate_colour_plane_flag; + uint32_t log2_max_frame_num; + uint16_t frame_num; + uint32_t frame_mbs_only_flag; + uint32_t pic_order_cnt_type; + uint32_t redundant_pic_cnt_present_flag; + uint32_t weighted_pred_flag; + uint32_t weighted_bipred_idc; + uint32_t entropy_coding_mode_flag; + uint32_t deblocking_filter_control_present_flag; + uint32_t num_slice_groups_minus1; + + frame_num = picture_param->frame_num; + log2_max_frame_num = + sequence_param->seq_fields.bits.log2_max_frame_num_minus4 + 4; + frame_mbs_only_flag = sequence_param->seq_fields.bits.frame_mbs_only_flag; + pic_order_cnt_type = sequence_param->seq_fields.bits.pic_order_cnt_type; + redundant_pic_cnt_present_flag = + picture_param->pic_fields.bits.redundant_pic_cnt_present_flag; + weighted_pred_flag = picture_param->pic_fields.bits.weighted_pred_flag; + weighted_bipred_idc = picture_param->pic_fields.bits.weighted_bipred_idc; + entropy_coding_mode_flag = + picture_param->pic_fields.bits.entropy_coding_mode_flag; + deblocking_filter_control_present_flag = + picture_param->pic_fields.bits.deblocking_filter_control_present_flag; + + separate_colour_plane_flag = 0; + num_slice_groups_minus1 = 0; + + /* + * See also 7.3.3 Slice header syntax (Rec. ITU-T H.264 (08/2021)) + * + * Not all paths are covered, only the ones relevant for GNOME Remote Desktop. + * Unhandled branches are preceded with an assertion. + */ + /* first_mb_in_slice */ + write_ue (nal_writer, slice_param->macroblock_address); + /* slice_type */ + write_ue (nal_writer, slice_param->slice_type); + /* pic_parameter_set_id */ + write_ue (nal_writer, slice_param->pic_parameter_set_id); + g_assert (!separate_colour_plane_flag); + + /* frame_num */ + write_u (nal_writer, frame_num, log2_max_frame_num); + g_assert (frame_mbs_only_flag); + + if (picture_param->pic_fields.bits.idr_pic_flag) + { + /* idr_pic_id */ + write_ue (nal_writer, slice_param->idr_pic_id); + } + if (pic_order_cnt_type == 0) + g_assert_not_reached (); + if (pic_order_cnt_type == 1) + g_assert_not_reached (); + + g_assert (!redundant_pic_cnt_present_flag); + g_assert (slice_param->slice_type != H264_SLICE_TYPE_B); + + if (slice_param->slice_type == H264_SLICE_TYPE_P) + { + /* num_ref_idx_active_override_flag */ + write_u (nal_writer, slice_param->num_ref_idx_active_override_flag, 1); + g_assert (!slice_param->num_ref_idx_active_override_flag); + } + + write_ref_pic_list_modification (nal_writer, slice_param); + + g_assert (!weighted_pred_flag && !weighted_bipred_idc); + + if (nal_ref_idc) + write_dec_ref_pic_marking (nal_writer, picture_param); + + if (entropy_coding_mode_flag && slice_param->slice_type != H264_SLICE_TYPE_I) + { + /* cabac_init_idc */ + write_ue (nal_writer, slice_param->cabac_init_idc); + } + + /* slice_qp_delta */ + write_se (nal_writer, slice_param->slice_qp_delta); + + g_assert (slice_param->slice_type == H264_SLICE_TYPE_I || + slice_param->slice_type == H264_SLICE_TYPE_P); + + g_assert (!deblocking_filter_control_present_flag); + g_assert (num_slice_groups_minus1 == 0); +} + +static void +get_nal_header_parameters (const VAEncSliceParameterBufferH264 *slice_param, + const VAEncPictureParameterBufferH264 *picture_param, + uint8_t *nal_ref_idc, + uint8_t *nal_unit_type) +{ + switch (slice_param->slice_type) + { + case H264_SLICE_TYPE_I: + *nal_ref_idc = H264_NAL_REF_IDC_HIGH; + if (picture_param->pic_fields.bits.idr_pic_flag) + *nal_unit_type = H264_NAL_UNIT_TYPE_SLICE_IDR; + else + *nal_unit_type = H264_NAL_UNIT_TYPE_SLICE_NON_IDR; + break; + case H264_SLICE_TYPE_P: + *nal_ref_idc = H264_NAL_REF_IDC_MEDIUM; + *nal_unit_type = H264_NAL_UNIT_TYPE_SLICE_NON_IDR; + break; + default: + g_assert_not_reached (); + } +} + +uint8_t * +grd_nal_writer_get_slice_header_bitstream (GrdNalWriter *nal_writer, + const VAEncSliceParameterBufferH264 *slice_param, + const VAEncSequenceParameterBufferH264 *sequence_param, + const VAEncPictureParameterBufferH264 *picture_param, + uint32_t *bitstream_length) +{ + uint8_t nal_ref_idc; + uint8_t nal_unit_type; + + get_nal_header_parameters (slice_param, picture_param, + &nal_ref_idc, &nal_unit_type); + + start_bitstream (nal_writer); + write_nal_start_code_prefix (nal_writer); + write_nal_header (nal_writer, nal_ref_idc, nal_unit_type); + write_slice_header (nal_writer, slice_param, sequence_param, picture_param, + nal_ref_idc); + + return end_bitstream (nal_writer, bitstream_length); +} + +GrdNalWriter * +grd_nal_writer_new (void) +{ + return g_object_new (GRD_TYPE_NAL_WRITER, NULL); +} + +static void +grd_nal_writer_dispose (GObject *object) +{ + GrdNalWriter *nal_writer = GRD_NAL_WRITER (object); + + g_assert (!nal_writer->nal_bitstream); + + G_OBJECT_CLASS (grd_nal_writer_parent_class)->dispose (object); +} + +static void +grd_nal_writer_init (GrdNalWriter *nal_writer) +{ +} + +static void +grd_nal_writer_class_init (GrdNalWriterClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = grd_nal_writer_dispose; +} diff --git a/grd-nal-writer.h b/grd-nal-writer.h new file mode 100644 index 0000000..9206fa2 --- /dev/null +++ b/grd-nal-writer.h @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2022 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include +#include + +#define GRD_TYPE_NAL_WRITER (grd_nal_writer_get_type ()) +G_DECLARE_FINAL_TYPE (GrdNalWriter, grd_nal_writer, + GRD, NAL_WRITER, GObject) + +GrdNalWriter *grd_nal_writer_new (void); + +uint8_t *grd_nal_writer_get_aud_bitstream (GrdNalWriter *nal_writer, + uint32_t *bitstream_length); + +uint8_t *grd_nal_writer_get_sps_bitstream (GrdNalWriter *nal_writer, + const VAEncSequenceParameterBufferH264 *sequence_param, + uint32_t *bitstream_length); + +uint8_t *grd_nal_writer_get_pps_bitstream (GrdNalWriter *nal_writer, + const VAEncPictureParameterBufferH264 *picture_param, + uint32_t *bitstream_length); + +uint8_t *grd_nal_writer_get_slice_header_bitstream (GrdNalWriter *nal_writer, + const VAEncSliceParameterBufferH264 *slice_param, + const VAEncSequenceParameterBufferH264 *sequence_param, + const VAEncPictureParameterBufferH264 *picture_param, + uint32_t *bitstream_length); diff --git a/grd-pipewire-utils.c b/grd-pipewire-utils.c new file mode 100644 index 0000000..71655c9 --- /dev/null +++ b/grd-pipewire-utils.c @@ -0,0 +1,207 @@ +/* + * Copyright (C) 2015 Red Hat Inc. + * Copyright (C) 2020 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-pipewire-utils.h" + +#include +#include +#include + +static void +pipewire_source_unref (GrdPipeWireSource *pipewire_source); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (GrdPipeWireSource, pipewire_source_unref) + +static gboolean +pipewire_loop_source_prepare (GSource *base, + int *timeout) +{ + *timeout = -1; + + return FALSE; +} + +static gboolean +pipewire_loop_source_dispatch (GSource *source, + GSourceFunc callback, + gpointer user_data) +{ + GrdPipeWireSource *pipewire_source = (GrdPipeWireSource *) source; + int result; + + result = pw_loop_iterate (pipewire_source->pipewire_loop, 0); + if (result < 0) + { + g_warning ("%s pw_loop_iterate() failed: %s", pipewire_source->message_tag, + spa_strerror (result)); + } + + return G_SOURCE_CONTINUE; +} + +static void +pipewire_loop_source_finalize (GSource *source) +{ + GrdPipeWireSource *pipewire_source = (GrdPipeWireSource *) source; + + if (pipewire_source->pipewire_loop) + { + pw_loop_leave (pipewire_source->pipewire_loop); + pw_loop_destroy (pipewire_source->pipewire_loop); + } + g_clear_pointer (&pipewire_source->message_tag, g_free); +} + +static GSourceFuncs pipewire_source_funcs = +{ + pipewire_loop_source_prepare, + NULL, + pipewire_loop_source_dispatch, + pipewire_loop_source_finalize, +}; + +GrdPipeWireSource * +grd_pipewire_source_new (const char *message_tag, + GError **error) +{ + g_autoptr (GrdPipeWireSource) pipewire_source = NULL; + + g_assert (strlen (message_tag) > 0); + + pipewire_source = + (GrdPipeWireSource *) g_source_new (&pipewire_source_funcs, + sizeof (GrdPipeWireSource)); + pipewire_source->message_tag = g_strdup_printf ("[%s]", message_tag); + + pipewire_source->pipewire_loop = pw_loop_new (NULL); + if (!pipewire_source->pipewire_loop) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create PipeWire loop"); + return NULL; + } + + g_source_add_unix_fd (&pipewire_source->base, + pw_loop_get_fd (pipewire_source->pipewire_loop), + G_IO_IN | G_IO_ERR); + + pw_loop_enter (pipewire_source->pipewire_loop); + + return g_steal_pointer (&pipewire_source); +} + +GrdPipeWireSource * +grd_attached_pipewire_source_new (const char *message_tag, + GError **error) +{ + GrdPipeWireSource *pipewire_source; + + pipewire_source = grd_pipewire_source_new (message_tag, error); + if (!pipewire_source) + return NULL; + + g_source_attach (&pipewire_source->base, NULL); + + return pipewire_source; +} + +static void +pipewire_source_unref (GrdPipeWireSource *pipewire_source) +{ + g_source_unref (&pipewire_source->base); +} + +gboolean +grd_pipewire_buffer_has_pointer_bitmap (struct pw_buffer *buffer) +{ + struct spa_meta_cursor *spa_meta_cursor; + + spa_meta_cursor = spa_buffer_find_meta_data (buffer->buffer, SPA_META_Cursor, + sizeof *spa_meta_cursor); + if (spa_meta_cursor && spa_meta_cursor_is_valid (spa_meta_cursor)) + { + struct spa_meta_bitmap *spa_meta_bitmap = NULL; + + if (spa_meta_cursor->bitmap_offset) + { + spa_meta_bitmap = SPA_MEMBER (spa_meta_cursor, + spa_meta_cursor->bitmap_offset, + struct spa_meta_bitmap); + } + if (spa_meta_bitmap) + return TRUE; + } + + return FALSE; +} + +gboolean +grd_pipewire_buffer_has_frame_data (struct pw_buffer *buffer) +{ + return buffer->buffer->datas[0].chunk->size > 0; +} + +gboolean +grd_spa_pixel_format_to_grd_pixel_format (uint32_t spa_format, + GrdPixelFormat *out_format) +{ + if (spa_format == SPA_VIDEO_FORMAT_RGBA) + *out_format = GRD_PIXEL_FORMAT_RGBA8888; + else + return FALSE; + + return TRUE; +} + +static struct +{ + enum spa_video_format spa_format; + uint32_t drm_format; + int bpp; +} format_table[] = { + { SPA_VIDEO_FORMAT_ARGB, DRM_FORMAT_BGRA8888, 4 }, + { SPA_VIDEO_FORMAT_BGRA, DRM_FORMAT_ARGB8888, 4 }, + { SPA_VIDEO_FORMAT_xRGB, DRM_FORMAT_BGRX8888, 4 }, + { SPA_VIDEO_FORMAT_BGRx, DRM_FORMAT_XRGB8888, 4 }, +}; + +void +grd_get_spa_format_details (enum spa_video_format spa_format, + uint32_t *drm_format, + int *bpp) +{ + int i; + + for (i = 0; i < G_N_ELEMENTS (format_table); i++) + { + if (format_table[i].spa_format == spa_format) + { + if (drm_format) + *drm_format = format_table[i].drm_format; + if (bpp) + *bpp = format_table[i].bpp; + return; + } + } + + g_assert_not_reached (); +} diff --git a/grd-pipewire-utils.h b/grd-pipewire-utils.h new file mode 100644 index 0000000..1ff84d3 --- /dev/null +++ b/grd-pipewire-utils.h @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2020 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include +#include +#include +#include + +#include "grd-types.h" + +#define CURSOR_META_SIZE(width, height) \ + (sizeof(struct spa_meta_cursor) + \ + sizeof(struct spa_meta_bitmap) + width * height * 4) + +typedef struct _GrdPipeWireSource +{ + GSource base; + + char *message_tag; + struct pw_loop *pipewire_loop; +} GrdPipeWireSource; + +GrdPipeWireSource *grd_pipewire_source_new (const char *message_tag, + GError **error); + +GrdPipeWireSource *grd_attached_pipewire_source_new (const char *message_tag, + GError **error); + +gboolean grd_pipewire_buffer_has_pointer_bitmap (struct pw_buffer *buffer); + +gboolean grd_pipewire_buffer_has_frame_data (struct pw_buffer *buffer); + +gboolean grd_spa_pixel_format_to_grd_pixel_format (uint32_t spa_format, + GrdPixelFormat *out_format); + +void grd_get_spa_format_details (enum spa_video_format spa_format, + uint32_t *drm_format, + int *bpp); diff --git a/grd-private.h b/grd-private.h new file mode 100644 index 0000000..70b236b --- /dev/null +++ b/grd-private.h @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2015 Red Hat Inc. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + * Written by: + * Jonas Ådahl + */ + +#pragma once + +#define GRD_DAEMON_USER_APPLICATION_ID "org.gnome.RemoteDesktop.User" +#define GRD_DAEMON_HEADLESS_APPLICATION_ID "org.gnome.RemoteDesktop.Headless" +#define GRD_DAEMON_SYSTEM_APPLICATION_ID "org.gnome.RemoteDesktop" +#define GRD_DAEMON_HANDOVER_APPLICATION_ID "org.gnome.RemoteDesktop.Handover" +#define REMOTE_DESKTOP_BUS_NAME "org.gnome.RemoteDesktop" +#define REMOTE_DESKTOP_OBJECT_PATH "/org/gnome/RemoteDesktop" +#define REMOTE_DESKTOP_CONFIGURATION_BUS_NAME "org.gnome.RemoteDesktop.Configuration" +#define REMOTE_DESKTOP_CONFIGURATION_OBJECT_PATH "/org/gnome/RemoteDesktop/Configuration" +#define REMOTE_DESKTOP_CLIENT_OBJECT_PATH "/org/gnome/RemoteDesktop/Client" +#define REMOTE_DESKTOP_DISPATCHER_OBJECT_PATH "/org/gnome/RemoteDesktop/Rdp/Dispatcher" +#define REMOTE_DESKTOP_HANDOVERS_OBJECT_PATH "/org/gnome/RemoteDesktop/Rdp/Handovers" +#define GRD_RDP_SERVER_OBJECT_PATH "/org/gnome/RemoteDesktop/Rdp/Server" +#define GRD_VNC_SERVER_OBJECT_PATH "/org/gnome/RemoteDesktop/Vnc/Server" +#define MUTTER_REMOTE_DESKTOP_BUS_NAME "org.gnome.Mutter.RemoteDesktop" +#define MUTTER_REMOTE_DESKTOP_OBJECT_PATH "/org/gnome/Mutter/RemoteDesktop" +#define MUTTER_SCREEN_CAST_BUS_NAME "org.gnome.Mutter.ScreenCast" +#define MUTTER_SCREEN_CAST_OBJECT_PATH "/org/gnome/Mutter/ScreenCast" +#define GDM_BUS_NAME "org.gnome.DisplayManager" +#define GDM_REMOTE_DISPLAY_FACTORY_OBJECT_PATH "/org/gnome/DisplayManager/RemoteDisplayFactory" +#define GDM_OBJECT_MANAGER_OBJECT_PATH "/org/gnome/DisplayManager/Displays" diff --git a/grd-prompt.c b/grd-prompt.c new file mode 100644 index 0000000..3fde741 --- /dev/null +++ b/grd-prompt.c @@ -0,0 +1,207 @@ +/* + * Copyright (C) 2018 Red Hat Inc. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + */ + +#include "config.h" + +#include + +#include "grd-prompt.h" + +typedef struct _GrdPromptResult +{ + GrdPromptResponse response; +} GrdPromptResult; + +struct _GrdPrompt +{ + GObject parent; +}; + +G_DEFINE_TYPE (GrdPrompt, grd_prompt, G_TYPE_OBJECT) + +static void +handle_notification_response (NotifyNotification *notification, + char *response, + gpointer user_data) +{ + GTask *task = G_TASK (user_data); + + if (g_strcmp0 (response, "accept") == 0) + { + g_task_return_int (task, GRD_PROMPT_RESPONSE_ACCEPT); + } + else if (g_strcmp0 (response, "cancel") == 0 || + g_strcmp0 (response, "closed") == 0) + { + g_task_return_int (task, GRD_PROMPT_RESPONSE_CANCEL); + } + else + { + g_warning ("Unknown prompt response '%s'", response); + g_task_return_int (task, GRD_PROMPT_RESPONSE_CANCEL); + } +} + +static void +on_notification_closed (NotifyNotification *notification, + gpointer user_data) +{ + handle_notification_response (notification, "closed", user_data); +} + +static gboolean +cancelled_idle_callback (gpointer user_data) +{ + GTask *task = G_TASK (user_data); + + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_CANCELLED, + "Prompt was cancelled"); + return G_SOURCE_REMOVE; +} + +static void +on_cancellable_cancelled (GCancellable *cancellable, + GTask *task) +{ + NotifyNotification *notification = + NOTIFY_NOTIFICATION (g_task_get_task_data (task)); + + g_signal_handlers_disconnect_by_func (notification, + G_CALLBACK (on_notification_closed), + task); + notify_notification_close (notification, NULL); + + g_idle_add (cancelled_idle_callback, task); +} + +static gboolean +show_notification_idle_callback (gpointer user_data) +{ + GTask *task = G_TASK (user_data); + GCancellable *cancellable = g_task_get_cancellable (task); + NotifyNotification *notification; + GError *error = NULL; + + if (g_cancellable_is_cancelled (cancellable)) + return G_SOURCE_REMOVE; + + notification = g_task_get_task_data (task); + + if (!notify_notification_show (notification, &error)) + g_task_return_error (task, error); + + return G_SOURCE_REMOVE; +} + +void +grd_prompt_query_async (GrdPrompt *prompt, + GrdPromptDefinition *prompt_definition, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + NotifyNotification *notification; + GTask *task; + + task = g_task_new (G_OBJECT (prompt), cancellable, callback, user_data); + + g_assert (prompt_definition); + g_assert (prompt_definition->summary || + prompt_definition->body); + + notification = notify_notification_new (prompt_definition->summary, + prompt_definition->body, + "preferences-desktop-remote-desktop"); + + notify_notification_set_urgency (notification, NOTIFY_URGENCY_CRITICAL); + + if (prompt_definition->cancel_label) + { + notify_notification_add_action (notification, + "cancel", + prompt_definition->cancel_label, + handle_notification_response, + task, NULL); + } + + if (prompt_definition->accept_label) + { + notify_notification_add_action (notification, + "accept", + prompt_definition->accept_label, + handle_notification_response, + task, NULL); + } + + g_task_set_task_data (task, notification, g_object_unref); + + g_signal_connect (notification, "closed", + G_CALLBACK (on_notification_closed), task); + + g_cancellable_connect (cancellable, + G_CALLBACK (on_cancellable_cancelled), + task, NULL); + + g_idle_add (show_notification_idle_callback, task); +} + +gboolean +grd_prompt_query_finish (GrdPrompt *prompt, + GAsyncResult *result, + GrdPromptResponse *out_response, + GError **error) +{ + g_autoptr(GTask) task = G_TASK (result); + GCancellable *cancellable; + GrdPromptResponse response; + + cancellable = g_task_get_cancellable (task); + g_signal_handlers_disconnect_by_func (cancellable, + G_CALLBACK (on_cancellable_cancelled), + task); + + response = g_task_propagate_int (task, error); + if (response == -1) + return FALSE; + + *out_response = response; + return TRUE; +} + +void +grd_prompt_definition_free (GrdPromptDefinition *prompt_definition) +{ + g_clear_pointer (&prompt_definition->summary, g_free); + g_clear_pointer (&prompt_definition->body, g_free); + g_clear_pointer (&prompt_definition->accept_label, g_free); + g_clear_pointer (&prompt_definition->cancel_label, g_free); + g_free (prompt_definition); +} + +static void +grd_prompt_init (GrdPrompt *prompt) +{ +} + +static void +grd_prompt_class_init (GrdPromptClass *klass) +{ + notify_init (g_get_application_name ()); +} diff --git a/grd-prompt.h b/grd-prompt.h new file mode 100644 index 0000000..5a56e93 --- /dev/null +++ b/grd-prompt.h @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2018 Red Hat Inc. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + */ + +#pragma once + +typedef enum _GrdPromptResponse +{ + GRD_PROMPT_RESPONSE_ACCEPT, + GRD_PROMPT_RESPONSE_CANCEL +} GrdPromptResponse; + +typedef struct _GrdPromptDefinition +{ + char *summary; + char *body; + char *accept_label; + char *cancel_label; +} GrdPromptDefinition; + +#define GRD_TYPE_PROMPT (grd_prompt_get_type ()) +G_DECLARE_FINAL_TYPE (GrdPrompt, grd_prompt, GRD, PROMPT, GObject) + +void grd_prompt_query_async (GrdPrompt *prompt, + GrdPromptDefinition *prompt_definition, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); + +gboolean grd_prompt_query_finish (GrdPrompt *prompt, + GAsyncResult *result, + GrdPromptResponse *response, + GError **error); + +void grd_prompt_definition_free (GrdPromptDefinition *prompt_definition); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (GrdPromptDefinition, grd_prompt_definition_free) diff --git a/grd-rdp-audio-output-stream.c b/grd-rdp-audio-output-stream.c new file mode 100644 index 0000000..d0f8285 --- /dev/null +++ b/grd-rdp-audio-output-stream.c @@ -0,0 +1,387 @@ +/* + * Copyright (C) 2022 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-rdp-audio-output-stream.h" + +#include +#include +#include + +#include "grd-rdp-dvc-audio-playback.h" + +struct _GrdRdpAudioOutputStream +{ + GObject parent; + + uint32_t target_node_id; + + GrdRdpDvcAudioPlayback *audio_playback; + + struct pw_node *pipewire_node; + struct spa_hook pipewire_node_listener; + + struct pw_stream *pipewire_stream; + struct spa_hook pipewire_stream_listener; + + GMutex volume_mutex; + gboolean audio_muted; + float volumes[SPA_AUDIO_MAX_CHANNELS]; + uint32_t n_volumes; +}; + +G_DEFINE_TYPE (GrdRdpAudioOutputStream, grd_rdp_audio_output_stream, G_TYPE_OBJECT) + +void +grd_rdp_audio_output_stream_set_active (GrdRdpAudioOutputStream *audio_output_stream, + gboolean active) +{ + pw_stream_set_active (audio_output_stream->pipewire_stream, active); +} + +void +grd_rdp_audio_output_stream_get_volume_data (GrdRdpAudioOutputStream *audio_output_stream, + GrdRdpAudioVolumeData *volume_data) +{ + uint32_t i; + + g_mutex_lock (&audio_output_stream->volume_mutex); + volume_data->audio_muted = audio_output_stream->audio_muted; + volume_data->n_volumes = audio_output_stream->n_volumes; + + for (i = 0; i < audio_output_stream->n_volumes; ++i) + volume_data->volumes[i] = audio_output_stream->volumes[i]; + g_mutex_unlock (&audio_output_stream->volume_mutex); +} + +static void +pipewire_node_info (void *user_data, + const struct pw_node_info *info) +{ + GrdRdpAudioOutputStream *audio_output_stream = user_data; + uint32_t i; + + if (!(info->change_mask & PW_NODE_CHANGE_MASK_PARAMS)) + return; + + for (i = 0; i < info->n_params; ++i) + { + if (info->params[i].id == SPA_PARAM_Props && + info->params[i].flags & SPA_PARAM_INFO_READ) + { + pw_node_enum_params (audio_output_stream->pipewire_node, + 0, SPA_PARAM_Props, 0, -1, NULL); + } + } +} + +static void +pipewire_node_param (void *user_data, + int seq, + uint32_t id, + uint32_t index, + uint32_t next, + const struct spa_pod *param) +{ + GrdRdpAudioOutputStream *audio_output_stream = user_data; + const struct spa_pod_object *object = (const struct spa_pod_object *) param; + struct spa_pod_prop *prop; + + if (id != SPA_PARAM_Props) + return; + + SPA_POD_OBJECT_FOREACH (object, prop) + { + if (prop->key == SPA_PROP_mute) + { + bool mute; + + if (spa_pod_get_bool (&prop->value, &mute) < 0) + continue; + + g_mutex_lock (&audio_output_stream->volume_mutex); + audio_output_stream->audio_muted = !!mute; + g_mutex_unlock (&audio_output_stream->volume_mutex); + } + else if (prop->key == SPA_PROP_channelVolumes) + { + g_mutex_lock (&audio_output_stream->volume_mutex); + audio_output_stream->n_volumes = + spa_pod_copy_array (&prop->value, SPA_TYPE_Float, + audio_output_stream->volumes, + SPA_AUDIO_MAX_CHANNELS); + g_mutex_unlock (&audio_output_stream->volume_mutex); + } + } +} + +static const struct pw_node_events pipewire_node_events = +{ + .version = PW_VERSION_NODE_EVENTS, + .info = pipewire_node_info, + .param = pipewire_node_param, +}; + +static void +pipewire_stream_state_changed (void *user_data, + enum pw_stream_state old, + enum pw_stream_state state, + const char *error) +{ + GrdRdpAudioOutputStream *audio_output_stream = user_data; + + g_debug ("[RDP.AUDIO_PLAYBACK] Node %u: PipeWire stream state changed from " + "%s to %s", audio_output_stream->target_node_id, + pw_stream_state_as_string (old), + pw_stream_state_as_string (state)); + + switch (state) + { + case PW_STREAM_STATE_ERROR: + g_warning ("[RDP.AUDIO_PLAYBACK] Node %u: PipeWire stream error: %s", + audio_output_stream->target_node_id, error); + break; + case PW_STREAM_STATE_PAUSED: + case PW_STREAM_STATE_STREAMING: + case PW_STREAM_STATE_UNCONNECTED: + case PW_STREAM_STATE_CONNECTING: + break; + } +} + +static void +pipewire_stream_param_changed (void *user_data, + uint32_t id, + const struct spa_pod *param) +{ + GrdRdpAudioOutputStream *audio_output_stream = user_data; + struct spa_pod_builder pod_builder; + const struct spa_pod *params[1]; + uint8_t params_buffer[1024]; + + if (!param || id != SPA_PARAM_Format) + return; + + pod_builder = SPA_POD_BUILDER_INIT (params_buffer, sizeof (params_buffer)); + + params[0] = spa_pod_builder_add_object ( + &pod_builder, + SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers, + SPA_PARAM_BUFFERS_dataType, SPA_POD_Int (1 << SPA_DATA_MemPtr)); + + pw_stream_update_params (audio_output_stream->pipewire_stream, params, 1); +} + +static void +pipewire_stream_process (void *user_data) +{ + GrdRdpAudioOutputStream *audio_output_stream = user_data; + struct pw_buffer *next_buffer; + struct pw_buffer *buffer = NULL; + int16_t *data; + uint32_t offset; + uint32_t size; + + next_buffer = pw_stream_dequeue_buffer (audio_output_stream->pipewire_stream); + while (next_buffer) + { + buffer = next_buffer; + next_buffer = pw_stream_dequeue_buffer (audio_output_stream->pipewire_stream); + + if (next_buffer) + pw_stream_queue_buffer (audio_output_stream->pipewire_stream, buffer); + } + if (!buffer) + return; + + offset = buffer->buffer->datas[0].chunk->offset; + size = buffer->buffer->datas[0].chunk->size; + data = buffer->buffer->datas[0].data + offset; + + if (size > 0) + { + GrdRdpDvcAudioPlayback *audio_playback = + audio_output_stream->audio_playback; + uint32_t node_id = audio_output_stream->target_node_id; + GrdRdpAudioVolumeData volume_data = {}; + + grd_rdp_audio_output_stream_get_volume_data (audio_output_stream, + &volume_data); + grd_rdp_dvc_audio_playback_maybe_submit_samples (audio_playback, node_id, + &volume_data, data, size); + } + + pw_stream_queue_buffer (audio_output_stream->pipewire_stream, buffer); +} + +static const struct pw_stream_events pipewire_stream_events = +{ + .version = PW_VERSION_STREAM_EVENTS, + .state_changed = pipewire_stream_state_changed, + .param_changed = pipewire_stream_param_changed, + .process = pipewire_stream_process, +}; + +static gboolean +connect_to_stream (GrdRdpAudioOutputStream *audio_output_stream, + struct pw_core *pipewire_core, + uint32_t target_node_id, + uint32_t n_samples_per_sec, + uint32_t n_channels, + uint32_t *position, + GError **error) +{ + struct spa_pod_builder pod_builder; + const struct spa_pod *params[1]; + uint8_t params_buffer[1024]; + int result; + + pod_builder = SPA_POD_BUILDER_INIT (params_buffer, sizeof (params_buffer)); + params[0] = spa_pod_builder_add_object ( + &pod_builder, + SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, + SPA_FORMAT_mediaType, SPA_POD_Id (SPA_MEDIA_TYPE_audio), + SPA_FORMAT_mediaSubtype, SPA_POD_Id (SPA_MEDIA_SUBTYPE_raw), + SPA_FORMAT_AUDIO_format, SPA_POD_Id (SPA_AUDIO_FORMAT_S16), + SPA_FORMAT_AUDIO_rate, SPA_POD_Int (n_samples_per_sec), + SPA_FORMAT_AUDIO_channels, SPA_POD_Int (n_channels), + SPA_FORMAT_AUDIO_position, SPA_POD_Array (sizeof (uint32_t), SPA_TYPE_Id, + n_channels, position), + 0); + + audio_output_stream->pipewire_stream = + pw_stream_new (pipewire_core, + "GRD::RDP::AUDIO_PLAYBACK", + pw_properties_new (PW_KEY_MEDIA_TYPE, "Audio", + PW_KEY_MEDIA_CATEGORY, "Capture", + PW_KEY_NODE_FORCE_QUANTUM, "256", + NULL)); + if (!audio_output_stream->pipewire_stream) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create PipeWire stream"); + return FALSE; + } + + pw_stream_add_listener (audio_output_stream->pipewire_stream, + &audio_output_stream->pipewire_stream_listener, + &pipewire_stream_events, audio_output_stream); + + result = pw_stream_connect (audio_output_stream->pipewire_stream, + PW_DIRECTION_INPUT, target_node_id, + PW_STREAM_FLAG_RT_PROCESS | + PW_STREAM_FLAG_AUTOCONNECT | + PW_STREAM_FLAG_INACTIVE | + PW_STREAM_FLAG_MAP_BUFFERS, + params, 1); + if (result < 0) + { + g_set_error_literal (error, G_IO_ERROR, g_io_error_from_errno (-result), + strerror (-result)); + return FALSE; + } + + return TRUE; +} + +GrdRdpAudioOutputStream * +grd_rdp_audio_output_stream_new (GrdRdpDvcAudioPlayback *audio_playback, + struct pw_core *pipewire_core, + struct pw_registry *pipewire_registry, + uint32_t target_node_id, + uint32_t n_samples_per_sec, + uint32_t n_channels, + uint32_t *position, + GError **error) +{ + g_autoptr (GrdRdpAudioOutputStream) audio_output_stream = NULL; + + audio_output_stream = g_object_new (GRD_TYPE_RDP_AUDIO_OUTPUT_STREAM, NULL); + audio_output_stream->audio_playback = audio_playback; + audio_output_stream->target_node_id = target_node_id; + + audio_output_stream->pipewire_node = pw_registry_bind (pipewire_registry, + target_node_id, + PW_TYPE_INTERFACE_Node, + PW_VERSION_NODE, 0); + if (!audio_output_stream->pipewire_node) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to bind PipeWire node"); + return NULL; + } + + pw_node_add_listener (audio_output_stream->pipewire_node, + &audio_output_stream->pipewire_node_listener, + &pipewire_node_events, audio_output_stream); + + if (!connect_to_stream (audio_output_stream, pipewire_core, target_node_id, + n_samples_per_sec, n_channels, position, error)) + return NULL; + + return g_steal_pointer (&audio_output_stream); +} + +static void +grd_rdp_audio_output_stream_dispose (GObject *object) +{ + GrdRdpAudioOutputStream *audio_output_stream = + GRD_RDP_AUDIO_OUTPUT_STREAM (object); + + if (audio_output_stream->pipewire_stream) + { + pw_stream_destroy (audio_output_stream->pipewire_stream); + audio_output_stream->pipewire_stream = NULL; + } + if (audio_output_stream->pipewire_node) + { + spa_hook_remove (&audio_output_stream->pipewire_node_listener); + pw_proxy_destroy ((struct pw_proxy *) audio_output_stream->pipewire_node); + audio_output_stream->pipewire_node = NULL; + } + + G_OBJECT_CLASS (grd_rdp_audio_output_stream_parent_class)->dispose (object); +} + +static void +grd_rdp_audio_output_stream_finalize (GObject *object) +{ + GrdRdpAudioOutputStream *audio_output_stream = + GRD_RDP_AUDIO_OUTPUT_STREAM (object); + + g_mutex_clear (&audio_output_stream->volume_mutex); + + G_OBJECT_CLASS (grd_rdp_audio_output_stream_parent_class)->finalize (object); +} + +static void +grd_rdp_audio_output_stream_init (GrdRdpAudioOutputStream *audio_output_stream) +{ + g_mutex_init (&audio_output_stream->volume_mutex); +} + +static void +grd_rdp_audio_output_stream_class_init (GrdRdpAudioOutputStreamClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = grd_rdp_audio_output_stream_dispose; + object_class->finalize = grd_rdp_audio_output_stream_finalize; +} diff --git a/grd-rdp-audio-output-stream.h b/grd-rdp-audio-output-stream.h new file mode 100644 index 0000000..8bb9473 --- /dev/null +++ b/grd-rdp-audio-output-stream.h @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2022 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include +#include +#include + +#include "grd-types.h" + +#define GRD_TYPE_RDP_AUDIO_OUTPUT_STREAM (grd_rdp_audio_output_stream_get_type ()) +G_DECLARE_FINAL_TYPE (GrdRdpAudioOutputStream, grd_rdp_audio_output_stream, + GRD, RDP_AUDIO_OUTPUT_STREAM, GObject) + +struct _GrdRdpAudioVolumeData +{ + gboolean audio_muted; + + float volumes[SPA_AUDIO_MAX_CHANNELS]; + uint32_t n_volumes; +}; + +GrdRdpAudioOutputStream *grd_rdp_audio_output_stream_new (GrdRdpDvcAudioPlayback *audio_playback, + struct pw_core *pipewire_core, + struct pw_registry *pipewire_registry, + uint32_t target_node_id, + uint32_t n_samples_per_sec, + uint32_t n_channels, + uint32_t *position, + GError **error); + +void grd_rdp_audio_output_stream_set_active (GrdRdpAudioOutputStream *audio_output_stream, + gboolean active); + +void grd_rdp_audio_output_stream_get_volume_data (GrdRdpAudioOutputStream *audio_output_stream, + GrdRdpAudioVolumeData *volume_data); diff --git a/grd-rdp-buffer-info.h b/grd-rdp-buffer-info.h new file mode 100644 index 0000000..a38cd31 --- /dev/null +++ b/grd-rdp-buffer-info.h @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2024 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include + +typedef enum +{ + GRD_RDP_BUFFER_TYPE_NONE, + GRD_RDP_BUFFER_TYPE_DMA_BUF, + GRD_RDP_BUFFER_TYPE_MEM_FD, +} GrdRdpBufferType; + +struct _GrdRdpBufferInfo +{ + GrdRdpBufferType buffer_type; + + uint32_t drm_format; + uint64_t drm_format_modifier; + gboolean has_vk_image; +}; diff --git a/grd-rdp-buffer-pool.c b/grd-rdp-buffer-pool.c new file mode 100644 index 0000000..9fad71a --- /dev/null +++ b/grd-rdp-buffer-pool.c @@ -0,0 +1,394 @@ +/* + * Copyright (C) 2022 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-rdp-buffer-pool.h" + +#include "grd-context.h" +#include "grd-egl-thread.h" +#include "grd-rdp-legacy-buffer.h" +#include "grd-rdp-renderer.h" +#include "grd-rdp-server.h" +#include "grd-rdp-surface.h" +#include "grd-session-rdp.h" +#include "grd-utils.h" + +typedef struct _BufferInfo +{ + gboolean buffer_taken; +} BufferInfo; + +struct _GrdRdpBufferPool +{ + GObject parent; + + GrdRdpSurface *rdp_surface; + + uint32_t buffer_height; + uint32_t buffer_stride; + + GSource *resize_pool_source; + uint32_t minimum_pool_size; + + GSource *unmap_source; + + GMutex pool_mutex; + GHashTable *buffer_table; + uint32_t buffers_taken; +}; + +G_DEFINE_TYPE (GrdRdpBufferPool, grd_rdp_buffer_pool, G_TYPE_OBJECT) + +GrdRdpSurface * +grd_rdp_buffer_pool_get_surface (GrdRdpBufferPool *buffer_pool) +{ + return buffer_pool->rdp_surface; +} + +static gboolean +add_buffer_to_pool (GrdRdpBufferPool *buffer_pool, + gboolean preallocate_on_gpu) +{ + GrdRdpLegacyBuffer *buffer; + BufferInfo *buffer_info; + + buffer = grd_rdp_legacy_buffer_new (buffer_pool, + buffer_pool->buffer_height, + buffer_pool->buffer_stride, + preallocate_on_gpu); + if (!buffer) + return FALSE; + + buffer_info = g_new0 (BufferInfo, 1); + + g_hash_table_insert (buffer_pool->buffer_table, buffer, buffer_info); + + return TRUE; +} + +static gboolean +fill_buffer_pool (GrdRdpBufferPool *buffer_pool) +{ + uint32_t minimum_size = buffer_pool->minimum_pool_size; + + while (g_hash_table_size (buffer_pool->buffer_table) < minimum_size) + { + if (!add_buffer_to_pool (buffer_pool, TRUE)) + return FALSE; + } + + return TRUE; +} + +gboolean +grd_rdp_buffer_pool_resize_buffers (GrdRdpBufferPool *buffer_pool, + uint32_t buffer_height, + uint32_t buffer_stride) +{ + g_autoptr (GMutexLocker) locker = NULL; + + locker = g_mutex_locker_new (&buffer_pool->pool_mutex); + g_assert (buffer_pool->buffers_taken == 0); + + buffer_pool->buffer_height = buffer_height; + buffer_pool->buffer_stride = buffer_stride; + + g_hash_table_remove_all (buffer_pool->buffer_table); + if (!fill_buffer_pool (buffer_pool)) + return FALSE; + + return TRUE; +} + +static gboolean +buffer_has_mapped_data (GrdRdpLegacyBuffer *buffer) +{ + if (grd_rdp_legacy_buffer_get_mapped_cuda_pointer (buffer)) + return TRUE; + + return FALSE; +} + +GrdRdpLegacyBuffer * +grd_rdp_buffer_pool_acquire (GrdRdpBufferPool *buffer_pool) +{ + g_autoptr (GMutexLocker) locker = NULL; + GHashTableIter iter; + GrdRdpLegacyBuffer *buffer; + BufferInfo *buffer_info; + gboolean buffer_found = FALSE; + + g_assert (buffer_pool->buffer_height > 0); + g_assert (buffer_pool->buffer_stride > 0); + + locker = g_mutex_locker_new (&buffer_pool->pool_mutex); + if (g_hash_table_size (buffer_pool->buffer_table) <= buffer_pool->buffers_taken && + !add_buffer_to_pool (buffer_pool, FALSE)) + return NULL; + + g_hash_table_iter_init (&iter, buffer_pool->buffer_table); + while (g_hash_table_iter_next (&iter, (gpointer *) &buffer, + (gpointer *) &buffer_info)) + { + if (!buffer_info->buffer_taken && !buffer_has_mapped_data (buffer)) + { + buffer_found = TRUE; + break; + } + } + + if (!buffer_found) + { + g_hash_table_iter_init (&iter, buffer_pool->buffer_table); + while (g_hash_table_iter_next (&iter, (gpointer *) &buffer, + (gpointer *) &buffer_info)) + { + if (!buffer_info->buffer_taken) + break; + } + } + g_assert (buffer); + + buffer_info->buffer_taken = TRUE; + ++buffer_pool->buffers_taken; + + return buffer; +} + +static gboolean +should_resize_buffer_pool (GrdRdpBufferPool *buffer_pool) +{ + uint32_t buffers_taken = buffer_pool->buffers_taken; + uint32_t minimum_size = buffer_pool->minimum_pool_size; + uint32_t current_pool_size; + + current_pool_size = g_hash_table_size (buffer_pool->buffer_table); + + if (current_pool_size > minimum_size && + current_pool_size > buffers_taken) + return TRUE; + + return FALSE; +} + +void +grd_rdp_buffer_pool_release_buffer (GrdRdpBufferPool *buffer_pool, + GrdRdpLegacyBuffer *buffer) +{ + BufferInfo *buffer_info; + gboolean queue_pool_resize; + gboolean queue_unmap; + + g_mutex_lock (&buffer_pool->pool_mutex); + if (!g_hash_table_lookup_extended (buffer_pool->buffer_table, buffer, + NULL, (gpointer *) &buffer_info)) + g_assert_not_reached (); + + g_assert (buffer_info->buffer_taken); + + buffer_info->buffer_taken = FALSE; + --buffer_pool->buffers_taken; + + queue_pool_resize = should_resize_buffer_pool (buffer_pool); + queue_unmap = buffer_has_mapped_data (buffer); + g_mutex_unlock (&buffer_pool->pool_mutex); + + if (queue_pool_resize) + g_source_set_ready_time (buffer_pool->resize_pool_source, 0); + if (queue_unmap) + g_source_set_ready_time (buffer_pool->unmap_source, 0); +} + +static gboolean +resize_buffer_pool (gpointer user_data) +{ + GrdRdpBufferPool *buffer_pool = user_data; + GHashTableIter iter; + BufferInfo *buffer_info; + + g_mutex_lock (&buffer_pool->pool_mutex); + g_hash_table_iter_init (&iter, buffer_pool->buffer_table); + while (should_resize_buffer_pool (buffer_pool) && + g_hash_table_iter_next (&iter, NULL, (gpointer *) &buffer_info)) + { + if (!buffer_info->buffer_taken) + g_hash_table_iter_remove (&iter); + } + g_mutex_unlock (&buffer_pool->pool_mutex); + + return G_SOURCE_CONTINUE; +} + +static gboolean +unmap_untaken_buffers (gpointer user_data) +{ + GrdRdpBufferPool *buffer_pool = user_data; + GHashTableIter iter; + GrdRdpLegacyBuffer *buffer; + BufferInfo *buffer_info; + + g_mutex_lock (&buffer_pool->pool_mutex); + g_hash_table_iter_init (&iter, buffer_pool->buffer_table); + while (g_hash_table_iter_next (&iter, (gpointer *) &buffer, + (gpointer *) &buffer_info)) + { + if (!buffer_info->buffer_taken) + grd_rdp_legacy_buffer_queue_resource_unmap (buffer); + } + g_mutex_unlock (&buffer_pool->pool_mutex); + + return G_SOURCE_CONTINUE; +} + +static gboolean +buffer_pool_source_dispatch (GSource *source, + GSourceFunc callback, + gpointer user_data) +{ + g_source_set_ready_time (source, -1); + + return callback (user_data); +} + +static GSourceFuncs buffer_pool_source_funcs = +{ + .dispatch = buffer_pool_source_dispatch, +}; + +GrdRdpBufferPool * +grd_rdp_buffer_pool_new (GrdRdpSurface *rdp_surface, + uint32_t minimum_size) +{ + g_autoptr (GrdRdpBufferPool) buffer_pool = NULL; + + buffer_pool = g_object_new (GRD_TYPE_RDP_BUFFER_POOL, NULL); + buffer_pool->rdp_surface = rdp_surface; + buffer_pool->minimum_pool_size = minimum_size; + + buffer_pool->resize_pool_source = g_source_new (&buffer_pool_source_funcs, + sizeof (GSource)); + g_source_set_callback (buffer_pool->resize_pool_source, + resize_buffer_pool, buffer_pool, NULL); + g_source_set_ready_time (buffer_pool->resize_pool_source, -1); + g_source_attach (buffer_pool->resize_pool_source, NULL); + + buffer_pool->unmap_source = g_source_new (&buffer_pool_source_funcs, + sizeof (GSource)); + g_source_set_callback (buffer_pool->unmap_source, + unmap_untaken_buffers, buffer_pool, NULL); + g_source_set_priority (buffer_pool->unmap_source, G_PRIORITY_LOW); + g_source_set_ready_time (buffer_pool->unmap_source, -1); + g_source_attach (buffer_pool->unmap_source, NULL); + + return g_steal_pointer (&buffer_pool); +} + +static void +on_sync_complete (gboolean success, + gpointer user_data) +{ + GrdSyncPoint *sync_point = user_data; + + grd_sync_point_complete (sync_point, success); +} + +static void +sync_egl_thread (GrdRdpBufferPool *buffer_pool) +{ + GrdRdpRenderer *renderer = buffer_pool->rdp_surface->renderer; + GrdSessionRdp *session_rdp = grd_rdp_renderer_get_session (renderer); + GrdRdpServer *rdp_server = grd_session_rdp_get_server (session_rdp); + GrdContext *context = grd_rdp_server_get_context (rdp_server); + GrdEglThread *egl_thread = grd_context_get_egl_thread (context); + GrdSyncPoint sync_point = {}; + + grd_sync_point_init (&sync_point); + grd_egl_thread_sync (egl_thread, on_sync_complete, + &sync_point, NULL); + + grd_sync_point_wait_for_completion (&sync_point); + grd_sync_point_clear (&sync_point); +} + +static void +grd_rdp_buffer_pool_dispose (GObject *object) +{ + GrdRdpBufferPool *buffer_pool = GRD_RDP_BUFFER_POOL (object); + + if (buffer_pool->unmap_source) + { + g_source_destroy (buffer_pool->unmap_source); + g_clear_pointer (&buffer_pool->unmap_source, g_source_unref); + } + if (buffer_pool->resize_pool_source) + { + g_source_destroy (buffer_pool->resize_pool_source); + g_clear_pointer (&buffer_pool->resize_pool_source, g_source_unref); + } + + g_assert (buffer_pool->buffers_taken == 0); + + G_OBJECT_CLASS (grd_rdp_buffer_pool_parent_class)->dispose (object); +} + +static void +grd_rdp_buffer_pool_finalize (GObject *object) +{ + GrdRdpBufferPool *buffer_pool = GRD_RDP_BUFFER_POOL (object); + GrdRdpRenderer *renderer = buffer_pool->rdp_surface->renderer; + GrdSessionRdp *session_rdp = grd_rdp_renderer_get_session (renderer); + GrdRdpServer *rdp_server = grd_session_rdp_get_server (session_rdp); + GrdContext *context = grd_rdp_server_get_context (rdp_server); + GrdEglThread *egl_thread = grd_context_get_egl_thread (context); + + g_mutex_clear (&buffer_pool->pool_mutex); + + g_clear_pointer (&buffer_pool->buffer_table, g_hash_table_unref); + + /* + * All buffers need to be destroyed, before the pool is freed to avoid use + * after free by the EGL thread, when the RDP server is shut down and with it + * the GrdHwAccelNvidia instance + */ + if (egl_thread) + sync_egl_thread (buffer_pool); + + G_OBJECT_CLASS (grd_rdp_buffer_pool_parent_class)->finalize (object); +} + +static void +grd_rdp_buffer_pool_init (GrdRdpBufferPool *buffer_pool) +{ + buffer_pool->buffer_table = + g_hash_table_new_full (NULL, NULL, + (GDestroyNotify) grd_rdp_legacy_buffer_free, + g_free); + + g_mutex_init (&buffer_pool->pool_mutex); +} + +static void +grd_rdp_buffer_pool_class_init (GrdRdpBufferPoolClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = grd_rdp_buffer_pool_dispose; + object_class->finalize = grd_rdp_buffer_pool_finalize; +} diff --git a/grd-rdp-buffer-pool.h b/grd-rdp-buffer-pool.h new file mode 100644 index 0000000..5c20d8f --- /dev/null +++ b/grd-rdp-buffer-pool.h @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2022 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include +#include +#include + +#include "grd-types.h" + +#define GRD_TYPE_RDP_BUFFER_POOL (grd_rdp_buffer_pool_get_type ()) +G_DECLARE_FINAL_TYPE (GrdRdpBufferPool, grd_rdp_buffer_pool, + GRD, RDP_BUFFER_POOL, GObject) + +GrdRdpBufferPool *grd_rdp_buffer_pool_new (GrdRdpSurface *rdp_surface, + uint32_t minimum_size); + +GrdRdpSurface *grd_rdp_buffer_pool_get_surface (GrdRdpBufferPool *buffer_pool); + +gboolean grd_rdp_buffer_pool_resize_buffers (GrdRdpBufferPool *buffer_pool, + uint32_t buffer_height, + uint32_t buffer_stride); + +GrdRdpLegacyBuffer *grd_rdp_buffer_pool_acquire (GrdRdpBufferPool *buffer_pool); + +void grd_rdp_buffer_pool_release_buffer (GrdRdpBufferPool *buffer_pool, + GrdRdpLegacyBuffer *buffer); diff --git a/grd-rdp-buffer.c b/grd-rdp-buffer.c new file mode 100644 index 0000000..8789e95 --- /dev/null +++ b/grd-rdp-buffer.c @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2024 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-rdp-buffer.h" + +#include + +#include "grd-rdp-buffer-info.h" +#include "grd-rdp-pw-buffer.h" +#include "grd-rdp-surface.h" +#include "grd-vk-utils.h" + +struct _GrdRdpBuffer +{ + GObject parent; + + GrdRdpPwBuffer *rdp_pw_buffer; + GrdRdpBufferInfo *rdp_buffer_info; + + GrdVkImage *dma_buf_image; + + gboolean marked_for_removal; +}; + +G_DEFINE_TYPE (GrdRdpBuffer, grd_rdp_buffer, G_TYPE_OBJECT) + +GrdRdpPwBuffer * +grd_rdp_buffer_get_rdp_pw_buffer (GrdRdpBuffer *rdp_buffer) +{ + return rdp_buffer->rdp_pw_buffer; +} + +const GrdRdpBufferInfo * +grd_rdp_buffer_get_rdp_buffer_info (GrdRdpBuffer *rdp_buffer) +{ + return rdp_buffer->rdp_buffer_info; +} + +GrdVkImage * +grd_rdp_buffer_get_dma_buf_image (GrdRdpBuffer *rdp_buffer) +{ + return rdp_buffer->dma_buf_image; +} + +gboolean +grd_rdp_buffer_is_marked_for_removal (GrdRdpBuffer *rdp_buffer) +{ + return rdp_buffer->marked_for_removal; +} + +void +grd_rdp_buffer_mark_for_removal (GrdRdpBuffer *rdp_buffer) +{ + rdp_buffer->marked_for_removal = TRUE; +} + +static gboolean +import_dma_buf_image (GrdRdpBuffer *rdp_buffer, + GrdRdpPwBuffer *rdp_pw_buffer, + GrdRdpBufferInfo *rdp_buffer_info, + GrdRdpSurface *rdp_surface, + GrdVkDevice *vk_device, + GError **error) +{ + uint32_t surface_width = grd_rdp_surface_get_width (rdp_surface); + uint32_t surface_height = grd_rdp_surface_get_height (rdp_surface); + const GrdRdpPwBufferDmaBufInfo *dma_buf_info = + grd_rdp_pw_buffer_get_dma_buf_info (rdp_pw_buffer); + VkFormat vk_format = VK_FORMAT_UNDEFINED; + + if (!grd_vk_get_vk_format_from_drm_format (rdp_buffer_info->drm_format, + &vk_format, error)) + return FALSE; + + rdp_buffer->dma_buf_image = + grd_vk_dma_buf_image_new (vk_device, vk_format, + surface_width, surface_height, + VK_IMAGE_USAGE_SAMPLED_BIT, + dma_buf_info->fd, + dma_buf_info->offset, + dma_buf_info->stride, + rdp_buffer_info->drm_format_modifier, + error); + if (!rdp_buffer->dma_buf_image) + return FALSE; + + rdp_buffer_info->has_vk_image = TRUE; + + return TRUE; +} + +GrdRdpBuffer * +grd_rdp_buffer_new (GrdRdpPwBuffer *rdp_pw_buffer, + GrdRdpBufferInfo *rdp_buffer_info, + GrdRdpSurface *rdp_surface, + GrdVkDevice *vk_device, + GError **error) +{ + g_autoptr (GrdRdpBuffer) rdp_buffer = NULL; + GrdRdpBufferType buffer_type; + + rdp_buffer = g_object_new (GRD_TYPE_RDP_BUFFER, NULL); + rdp_buffer->rdp_pw_buffer = rdp_pw_buffer; + + buffer_type = grd_rdp_pw_buffer_get_buffer_type (rdp_pw_buffer); + if (buffer_type == GRD_RDP_BUFFER_TYPE_DMA_BUF && + vk_device && + rdp_buffer_info->drm_format_modifier != DRM_FORMAT_MOD_INVALID) + { + g_autoptr (GError) local_error = NULL; + + if (!import_dma_buf_image (rdp_buffer, rdp_pw_buffer, rdp_buffer_info, + rdp_surface, vk_device, &local_error)) + { + if (rdp_buffer_info->has_vk_image) + { + g_propagate_error (error, g_steal_pointer (&local_error)); + return NULL; + } + + g_debug ("[RDP] Could not import dma-buf image: %s", + local_error->message); + } + } + + rdp_buffer->rdp_buffer_info = + g_memdup2 (rdp_buffer_info, sizeof (GrdRdpBufferInfo)); + + return g_steal_pointer (&rdp_buffer); +} + +static void +grd_rdp_buffer_dispose (GObject *object) +{ + GrdRdpBuffer *rdp_buffer = GRD_RDP_BUFFER (object); + + g_clear_object (&rdp_buffer->dma_buf_image); + g_clear_pointer (&rdp_buffer->rdp_buffer_info, g_free); + + G_OBJECT_CLASS (grd_rdp_buffer_parent_class)->dispose (object); +} + +static void +grd_rdp_buffer_init (GrdRdpBuffer *rdp_buffer) +{ +} + +static void +grd_rdp_buffer_class_init (GrdRdpBufferClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = grd_rdp_buffer_dispose; +} diff --git a/grd-rdp-buffer.h b/grd-rdp-buffer.h new file mode 100644 index 0000000..b6818dd --- /dev/null +++ b/grd-rdp-buffer.h @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2024 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include + +#include "grd-types.h" + +#define GRD_TYPE_RDP_BUFFER (grd_rdp_buffer_get_type ()) +G_DECLARE_FINAL_TYPE (GrdRdpBuffer, grd_rdp_buffer, + GRD, RDP_BUFFER, GObject) + +GrdRdpBuffer *grd_rdp_buffer_new (GrdRdpPwBuffer *rdp_pw_buffer, + GrdRdpBufferInfo *rdp_buffer_info, + GrdRdpSurface *rdp_surface, + GrdVkDevice *vk_device, + GError **error); + +GrdRdpPwBuffer *grd_rdp_buffer_get_rdp_pw_buffer (GrdRdpBuffer *rdp_buffer); + +const GrdRdpBufferInfo *grd_rdp_buffer_get_rdp_buffer_info (GrdRdpBuffer *rdp_buffer); + +GrdVkImage *grd_rdp_buffer_get_dma_buf_image (GrdRdpBuffer *rdp_buffer); + +gboolean grd_rdp_buffer_is_marked_for_removal (GrdRdpBuffer *rdp_buffer); + +void grd_rdp_buffer_mark_for_removal (GrdRdpBuffer *rdp_buffer); diff --git a/grd-rdp-camera-stream.c b/grd-rdp-camera-stream.c new file mode 100644 index 0000000..6912270 --- /dev/null +++ b/grd-rdp-camera-stream.c @@ -0,0 +1,1079 @@ +/* + * Copyright (C) 2025 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-rdp-camera-stream.h" + +#include + +#include "grd-decode-session-sw-avc.h" +#include "grd-frame-clock.h" +#include "grd-pipewire-utils.h" +#include "grd-rdp-dvc-camera-device.h" + +#define MAX_N_PENDING_FRAMES 2 + +#define PARAMS_BUFFER_SIZE 1024 + +typedef struct +{ + GMutex buffer_mutex; +} BufferContext; + +struct _GrdRdpCameraStream +{ + GObject parent; + + GrdRdpDvcCameraDevice *device; + const char *device_name; + uint8_t stream_index; + GList *media_type_descriptions; + + GrdFrameClock *frame_clock; + + struct pw_stream *pipewire_stream; + struct spa_hook pipewire_stream_listener; + + GHashTable *buffer_contexts; + + struct spa_video_info_raw video_format; + CAM_MEDIA_TYPE_DESCRIPTION *current_media_type_description; + gboolean pending_decode_session_reset; + + gboolean is_enabled; + + uint32_t current_run_sequence; + uint32_t last_acked_run_sequence; + + gboolean camera_loop_inhibited; + + GrdDecodeSession *sw_decode_session; + GrdDecodeSession *decode_session_in_use; + + GMutex dequeued_buffers_mutex; + GHashTable *dequeued_buffers; + GHashTable *buffers_in_decoding; + + uint64_t buffer_sequence; +}; + +G_DEFINE_TYPE (GrdRdpCameraStream, grd_rdp_camera_stream, + G_TYPE_OBJECT) + +static BufferContext * +buffer_context_new (void) +{ + BufferContext *buffer_context; + + buffer_context = g_new0 (BufferContext, 1); + g_mutex_init (&buffer_context->buffer_mutex); + + return buffer_context; +} + +static void +buffer_context_free (BufferContext *buffer_context) +{ + g_mutex_clear (&buffer_context->buffer_mutex); + + g_free (buffer_context); +} + +static void +acquire_pipewire_buffer_lock (GrdRdpCameraStream *camera_stream, + struct pw_buffer *pw_buffer) +{ + BufferContext *buffer_context = NULL; + + if (!g_hash_table_lookup_extended (camera_stream->buffer_contexts, pw_buffer, + NULL, (gpointer *) &buffer_context)) + g_assert_not_reached (); + + g_mutex_lock (&buffer_context->buffer_mutex); +} + +static void +release_pipewire_buffer_lock (GrdRdpCameraStream *camera_stream, + struct pw_buffer *pw_buffer) +{ + BufferContext *buffer_context = NULL; + + if (!g_hash_table_lookup_extended (camera_stream->buffer_contexts, pw_buffer, + NULL, (gpointer *) &buffer_context)) + g_assert_not_reached (); + + g_mutex_unlock (&buffer_context->buffer_mutex); +} + +static void +ensure_unlocked_pipewire_buffer (GrdRdpCameraStream *camera_stream, + struct pw_buffer *pw_buffer) +{ + BufferContext *buffer_context = NULL; + + if (!g_hash_table_lookup_extended (camera_stream->buffer_contexts, pw_buffer, + NULL, (gpointer *) &buffer_context)) + g_assert_not_reached (); + + g_mutex_lock (&buffer_context->buffer_mutex); + g_mutex_unlock (&buffer_context->buffer_mutex); +} + +uint8_t +grd_rdp_camera_stream_get_stream_index (GrdRdpCameraStream *camera_stream) +{ + return camera_stream->stream_index; +} + +static gboolean +can_serve_frames (GrdRdpCameraStream *camera_stream) +{ + return camera_stream->is_enabled && + camera_stream->current_run_sequence == camera_stream->last_acked_run_sequence && + !camera_stream->camera_loop_inhibited; +} + +static void +maybe_start_camera_loop (GrdRdpCameraStream *camera_stream) +{ + const char *device_name = camera_stream->device_name; + uint8_t stream_index = camera_stream->stream_index; + struct spa_video_info_raw *video_format = &camera_stream->video_format; + + if (!can_serve_frames (camera_stream)) + return; + + if (grd_frame_clock_is_armed (camera_stream->frame_clock)) + return; + + g_debug ("[RDP.CAM_DEVICE] Device \"%s\", stream %u: Starting camera stream", + device_name, stream_index); + + grd_frame_clock_arm_timer (camera_stream->frame_clock, + video_format->framerate.num, + video_format->framerate.denom); +} + +gboolean +grd_rdp_camera_stream_notify_stream_started (GrdRdpCameraStream *camera_stream, + uint32_t run_sequence) +{ + if (run_sequence != camera_stream->current_run_sequence) + return FALSE; + + if (run_sequence == camera_stream->last_acked_run_sequence) + return TRUE; + + camera_stream->last_acked_run_sequence = run_sequence; + + maybe_start_camera_loop (camera_stream); + + return TRUE; +} + +static void +stop_camera_loop (GrdRdpCameraStream *camera_stream) +{ + grd_frame_clock_disarm_timer (camera_stream->frame_clock); +} + +void +grd_rdp_camera_stream_inhibit_camera_loop (GrdRdpCameraStream *camera_stream) +{ + camera_stream->camera_loop_inhibited = TRUE; + + stop_camera_loop (camera_stream); +} + +void +grd_rdp_camera_stream_uninhibit_camera_loop (GrdRdpCameraStream *camera_stream) +{ + camera_stream->camera_loop_inhibited = FALSE; + + maybe_start_camera_loop (camera_stream); +} + +gboolean +grd_rdp_camera_stream_announce_new_sample (GrdRdpCameraStream *camera_stream, + GrdSampleBuffer *sample_buffer) +{ + g_autoptr (GMutexLocker) locker = NULL; + struct pw_buffer *pw_buffer = NULL; + g_autoptr (GError) error = NULL; + + locker = g_mutex_locker_new (&camera_stream->dequeued_buffers_mutex); + if (!g_hash_table_lookup_extended (camera_stream->dequeued_buffers, + sample_buffer, + NULL, (gpointer *) &pw_buffer)) + return FALSE; + + g_hash_table_insert (camera_stream->buffers_in_decoding, + sample_buffer, pw_buffer); + + return TRUE; +} + +static void +queue_corrupted_pw_buffer (GrdRdpCameraStream *camera_stream, + struct pw_buffer *pw_buffer) +{ + struct spa_buffer *spa_buffer; + struct spa_data *spa_data; + struct spa_meta_header *spa_header; + + spa_buffer = pw_buffer->buffer; + spa_data = &spa_buffer->datas[0]; + + spa_data->chunk->size = 0; + spa_data->chunk->flags = SPA_CHUNK_FLAG_CORRUPTED; + + spa_header = spa_buffer_find_meta_data (spa_buffer, SPA_META_Header, + sizeof (struct spa_meta_header)); + if (spa_header) + { + spa_header->flags = 0; + spa_header->pts = pw_stream_get_nsec (camera_stream->pipewire_stream); + spa_header->dts_offset = 0; + spa_header->seq = ++camera_stream->buffer_sequence; + } + + pw_stream_queue_buffer (camera_stream->pipewire_stream, pw_buffer); + release_pipewire_buffer_lock (camera_stream, pw_buffer); +} + +static void +discard_failed_sample_request (GrdRdpCameraStream *camera_stream, + GrdSampleBuffer *sample_buffer, + struct pw_buffer *pw_buffer) +{ + g_mutex_lock (&camera_stream->dequeued_buffers_mutex); + g_hash_table_remove (camera_stream->buffers_in_decoding, sample_buffer); + g_hash_table_remove (camera_stream->dequeued_buffers, sample_buffer); + g_mutex_unlock (&camera_stream->dequeued_buffers_mutex); + + queue_corrupted_pw_buffer (camera_stream, pw_buffer); +} + +static void +on_frame_ready (GrdDecodeSession *decode_session, + GrdSampleBuffer *sample_buffer, + gpointer user_data, + GError *error) +{ + GrdRdpCameraStream *camera_stream = user_data; + const char *device_name = camera_stream->device_name; + uint8_t stream_index = camera_stream->stream_index; + struct pw_buffer *pw_buffer = NULL; + struct spa_buffer *spa_buffer; + struct spa_data *spa_data; + struct spa_meta_header *spa_header; + + g_mutex_lock (&camera_stream->dequeued_buffers_mutex); + if (!g_hash_table_steal_extended (camera_stream->dequeued_buffers, + sample_buffer, + NULL, (gpointer *) &pw_buffer)) + g_assert_not_reached (); + + g_hash_table_remove (camera_stream->buffers_in_decoding, sample_buffer); + g_mutex_unlock (&camera_stream->dequeued_buffers_mutex); + + spa_buffer = pw_buffer->buffer; + spa_data = &spa_buffer->datas[0]; + + if (error) + { + g_warning ("[RDP.CAM_DEVICE] Device \"%s\", stream %u: Failed to decode " + "frame: %s", device_name, stream_index, error->message); + spa_data->chunk->size = 0; + spa_data->chunk->flags = SPA_CHUNK_FLAG_CORRUPTED; + } + else + { + spa_data->chunk->offset = 0; + spa_data->chunk->size = spa_data->maxsize; + spa_data->chunk->flags = SPA_CHUNK_FLAG_NONE; + } + + spa_header = spa_buffer_find_meta_data (spa_buffer, SPA_META_Header, + sizeof (struct spa_meta_header)); + if (spa_header) + { + spa_header->flags = 0; + spa_header->pts = pw_stream_get_nsec (camera_stream->pipewire_stream); + spa_header->dts_offset = 0; + spa_header->seq = ++camera_stream->buffer_sequence; + } + + pw_stream_queue_buffer (camera_stream->pipewire_stream, pw_buffer); + release_pipewire_buffer_lock (camera_stream, pw_buffer); +} + +void +grd_rdp_camera_stream_submit_sample (GrdRdpCameraStream *camera_stream, + GrdSampleBuffer *sample_buffer, + gboolean success) +{ + GrdDecodeSession *decode_session = camera_stream->decode_session_in_use; + const char *device_name = camera_stream->device_name; + uint8_t stream_index = camera_stream->stream_index; + g_autoptr (GMutexLocker) locker = NULL; + struct pw_buffer *pw_buffer = NULL; + g_autoptr (GError) error = NULL; + + locker = g_mutex_locker_new (&camera_stream->dequeued_buffers_mutex); + if (!g_hash_table_lookup_extended (camera_stream->dequeued_buffers, + sample_buffer, + NULL, (gpointer *) &pw_buffer)) + return; + + g_hash_table_insert (camera_stream->buffers_in_decoding, + sample_buffer, pw_buffer); + g_clear_pointer (&locker, g_mutex_locker_free); + + if (!success || !can_serve_frames (camera_stream)) + { + discard_failed_sample_request (camera_stream, sample_buffer, pw_buffer); + return; + } + + if (grd_decode_session_decode_frame (decode_session, sample_buffer, + on_frame_ready, camera_stream, + &error)) + return; + + g_warning ("[RDP.CAM_DEVICE] Device \"%s\", stream %u: Failed to start " + "decoding frame: %s", device_name, stream_index, error->message); + + discard_failed_sample_request (camera_stream, sample_buffer, pw_buffer); +} + +static void +on_frame_clock_trigger (gpointer user_data) +{ + GrdRdpCameraStream *camera_stream = user_data; + GrdDecodeSession *decode_session = camera_stream->decode_session_in_use; + struct pw_buffer *pw_buffer; + GrdSampleBuffer *sample_buffer; + uint32_t n_pending_frames; + + if (!can_serve_frames (camera_stream)) + return; + + n_pending_frames = grd_decode_session_get_n_pending_frames (decode_session); + if (n_pending_frames > MAX_N_PENDING_FRAMES) + return; + + /* + * If there is no PipeWire buffer available, then the consumer side is too + * slow. Don't emit a warning for this, as this can spam up the whole log. + */ + pw_buffer = pw_stream_dequeue_buffer (camera_stream->pipewire_stream); + if (!pw_buffer) + return; + + acquire_pipewire_buffer_lock (camera_stream, pw_buffer); + + sample_buffer = grd_decode_session_get_sample_buffer (decode_session, + pw_buffer); + + g_mutex_lock (&camera_stream->dequeued_buffers_mutex); + g_hash_table_insert (camera_stream->dequeued_buffers, + sample_buffer, pw_buffer); + + g_assert (!g_hash_table_contains (camera_stream->buffers_in_decoding, + sample_buffer)); + g_mutex_unlock (&camera_stream->dequeued_buffers_mutex); + + grd_rdp_dvc_camera_device_request_sample (camera_stream->device, + camera_stream, + sample_buffer); +} + +static void +append_pod_offset (GArray *pod_offsets, + struct spa_pod_builder *pod_builder) +{ + g_array_append_val (pod_offsets, pod_builder->state.offset); +} + +static void +push_format_object (GArray *pod_offsets, + struct spa_pod_builder *pod_builder, + enum spa_video_format format, + uint64_t *modifiers, + uint32_t n_modifiers, + gboolean fixate_modifier, + ...) +{ + struct spa_pod_frame pod_frame; + va_list args; + + append_pod_offset (pod_offsets, pod_builder); + + spa_pod_builder_push_object (pod_builder, + &pod_frame, + SPA_TYPE_OBJECT_Format, + SPA_PARAM_EnumFormat); + spa_pod_builder_add (pod_builder, + SPA_FORMAT_mediaType, + SPA_POD_Id (SPA_MEDIA_TYPE_video), + 0); + spa_pod_builder_add (pod_builder, + SPA_FORMAT_mediaSubtype, + SPA_POD_Id (SPA_MEDIA_SUBTYPE_raw), + 0); + spa_pod_builder_add (pod_builder, + SPA_FORMAT_VIDEO_format, + SPA_POD_Id (format), + 0); + if (n_modifiers > 0) + { + if (fixate_modifier) + { + spa_pod_builder_prop (pod_builder, + SPA_FORMAT_VIDEO_modifier, + SPA_POD_PROP_FLAG_MANDATORY); + spa_pod_builder_long (pod_builder, modifiers[0]); + } + else + { + struct spa_pod_frame pod_frame_mods; + uint32_t i; + + spa_pod_builder_prop (pod_builder, + SPA_FORMAT_VIDEO_modifier, + (SPA_POD_PROP_FLAG_MANDATORY | + SPA_POD_PROP_FLAG_DONT_FIXATE)); + spa_pod_builder_push_choice (pod_builder, + &pod_frame_mods, + SPA_CHOICE_Enum, + 0); + spa_pod_builder_long (pod_builder, modifiers[0]); + for (i = 0; i < n_modifiers; i++) + spa_pod_builder_long (pod_builder, modifiers[i]); + spa_pod_builder_pop (pod_builder, &pod_frame_mods); + } + } + + va_start (args, fixate_modifier); + spa_pod_builder_addv (pod_builder, args); + va_end (args); + + spa_pod_builder_pop (pod_builder, &pod_frame); +} + +static void +push_format_objects (GrdRdpCameraStream *camera_stream, + GrdDecodeSession *decode_session, + enum spa_video_format format, + struct spa_pod_builder *pod_builder, + GArray *pod_offsets) +{ + g_autofree uint64_t *modifiers = NULL; + uint32_t n_modifiers = 0; + uint32_t drm_format; + GList *l; + + grd_get_spa_format_details (format, &drm_format, NULL); + + grd_decode_session_get_drm_format_modifiers (decode_session, drm_format, + &n_modifiers, &modifiers); + + for (l = camera_stream->media_type_descriptions; l; l = l->next) + { + CAM_MEDIA_TYPE_DESCRIPTION *description = l->data; + + /* Only add sanitized formats */ + if (description->FrameRateNumerator == 0 || + description->FrameRateDenominator == 0 || + description->PixelAspectRatioDenominator == 0) + continue; + + push_format_object ( + pod_offsets, pod_builder, + format, modifiers, n_modifiers, FALSE, + SPA_FORMAT_VIDEO_size, + SPA_POD_Rectangle (&SPA_RECTANGLE (description->Width, + description->Height)), + SPA_FORMAT_VIDEO_framerate, + SPA_POD_Fraction (&SPA_FRACTION (description->FrameRateNumerator, + description->FrameRateDenominator)), + SPA_FORMAT_VIDEO_pixelAspectRatio, + SPA_POD_Rectangle (&SPA_RECTANGLE (description->PixelAspectRatioNumerator, + description->PixelAspectRatioDenominator)), + 0); + } +} + +static void +build_format_params (GrdRdpCameraStream *camera_stream, + struct spa_pod_builder *pod_builder, + GArray *pod_offsets) +{ + GrdDecodeSession *sw_decode_session = camera_stream->sw_decode_session; + + if (sw_decode_session) + { + push_format_objects (camera_stream, sw_decode_session, + SPA_VIDEO_FORMAT_BGRA, pod_builder, pod_offsets); + } +} + +static GPtrArray * +finish_format_params (struct spa_pod_builder *pod_builder, + GArray *pod_offsets) +{ + GPtrArray *params = NULL; + size_t i; + + params = g_ptr_array_new (); + + for (i = 0; i < pod_offsets->len; ++i) + { + uint32_t pod_offset = g_array_index (pod_offsets, uint32_t, i); + + g_ptr_array_add (params, spa_pod_builder_deref (pod_builder, pod_offset)); + } + + return params; +} + +static void +stop_camera_stream (GrdRdpCameraStream *camera_stream, + gboolean was_connecting) +{ + const char *device_name = camera_stream->device_name; + uint8_t stream_index = camera_stream->stream_index; + + camera_stream->is_enabled = FALSE; + stop_camera_loop (camera_stream); + + if (was_connecting) + return; + + g_debug ("[RDP.CAM_DEVICE] Device \"%s\", stream %u: Stopping camera stream", + device_name, stream_index); + + grd_rdp_dvc_camera_device_stop_stream (camera_stream->device, camera_stream); +} + +static void +start_camera_stream (GrdRdpCameraStream *camera_stream) +{ + CAM_MEDIA_TYPE_DESCRIPTION *media_type_description = + camera_stream->current_media_type_description; + + while (G_UNLIKELY (camera_stream->current_run_sequence + 1 == + camera_stream->last_acked_run_sequence)) + ++camera_stream->current_run_sequence; + + grd_rdp_dvc_camera_device_start_stream (camera_stream->device, camera_stream, + media_type_description, + ++camera_stream->current_run_sequence); + camera_stream->is_enabled = TRUE; +} + +static void +on_stream_state_changed (void *user_data, + enum pw_stream_state old, + enum pw_stream_state state, + const char *error) +{ + GrdRdpCameraStream *camera_stream = user_data; + const char *device_name = camera_stream->device_name; + uint8_t stream_index = camera_stream->stream_index; + + g_debug ("[RDP.CAM_DEVICE] Device \"%s\", stream %u: PipeWire stream state " + "changed from %s to %s", device_name, stream_index, + pw_stream_state_as_string (old), + pw_stream_state_as_string (state)); + + switch (state) + { + case PW_STREAM_STATE_ERROR: + g_warning ("[RDP.CAM_DEVICE] Device \"%s\", stream %u: PipeWire stream " + "error: %s", device_name, stream_index, error); + stop_camera_stream (camera_stream, old == PW_STREAM_STATE_CONNECTING); + break; + case PW_STREAM_STATE_PAUSED: + stop_camera_stream (camera_stream, old == PW_STREAM_STATE_CONNECTING); + break; + case PW_STREAM_STATE_STREAMING: + start_camera_stream (camera_stream); + break; + case PW_STREAM_STATE_UNCONNECTED: + case PW_STREAM_STATE_CONNECTING: + break; + } +} + +static uint32_t +gcd (uint32_t num, + uint32_t denom) +{ + g_assert (num > 0); + g_assert (denom > 0); + + while (denom != 0) + { + uint32_t remainder; + + remainder = num % denom; + num = denom; + denom = remainder; + } + + return num; +} + +static CAM_MEDIA_TYPE_DESCRIPTION * +get_media_type_description_for_video_format (GrdRdpCameraStream *camera_stream, + GError **error) +{ + struct spa_video_info_raw *video_format = &camera_stream->video_format; + GList *l; + + for (l = camera_stream->media_type_descriptions; l; l = l->next) + { + CAM_MEDIA_TYPE_DESCRIPTION *description = l->data; + uint32_t sanitized_ecam_num; + uint32_t sanitized_ecam_denom; + uint32_t gcd_pw; + uint32_t gcd_ecam; + + /* Only check added formats */ + if (description->FrameRateNumerator == 0 || + description->FrameRateDenominator == 0 || + description->PixelAspectRatioDenominator == 0) + continue; + + if (video_format->size.width != description->Width || + video_format->size.height != description->Height) + continue; + + gcd_pw = gcd (video_format->framerate.num, + video_format->framerate.denom); + gcd_ecam = gcd (description->FrameRateNumerator, + description->FrameRateDenominator); + + sanitized_ecam_num = description->FrameRateNumerator / gcd_ecam; + sanitized_ecam_denom = description->FrameRateDenominator / gcd_ecam; + + if (video_format->framerate.num / gcd_pw != sanitized_ecam_num || + video_format->framerate.denom / gcd_pw != sanitized_ecam_denom) + continue; + + return description; + } + + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to find associated media type description for PipeWire " + "video format"); + + return NULL; +} + +static void +pod_builder_add_object (struct spa_pod_builder *pod_builder, + GArray *pod_offsets, + uint32_t type, + uint32_t id, + ...) +{ + struct spa_pod_frame pod_frame; + va_list args; + + append_pod_offset (pod_offsets, pod_builder); + + spa_pod_builder_push_object (pod_builder, &pod_frame, type, id); + + va_start (args, id); + spa_pod_builder_addv (pod_builder, args); + va_end (args); + + spa_pod_builder_pop (pod_builder, &pod_frame); +} + +static void +on_stream_param_changed (void *user_data, + uint32_t id, + const struct spa_pod *param) +{ + GrdRdpCameraStream *camera_stream = user_data; + const char *device_name = camera_stream->device_name; + uint8_t stream_index = camera_stream->stream_index; + struct spa_video_info_raw *video_format = &camera_stream->video_format; + g_autoptr (GArray) pod_offsets = NULL; + g_autoptr (GPtrArray) params = NULL; + g_autoptr (GError) error = NULL; + struct spa_pod_dynamic_builder pod_builder; + uint32_t buffer_type; + + if (!param || id != SPA_PARAM_Format) + return; + + g_assert (!camera_stream->is_enabled); + + spa_format_video_raw_parse (param, video_format); + g_debug ("[RDP.CAM_DEVICE] Device \"%s\", stream %u: New format: " + "Size: %ux%u, framerate: %u/%u", device_name, stream_index, + video_format->size.width, video_format->size.height, + video_format->framerate.num, video_format->framerate.denom); + + camera_stream->current_media_type_description = + get_media_type_description_for_video_format (camera_stream, + &error); + if (!camera_stream->current_media_type_description) + { + pw_stream_set_error (camera_stream->pipewire_stream, -EINVAL, + "%s", error->message); + return; + } + camera_stream->pending_decode_session_reset = TRUE; + + pod_offsets = g_array_new (FALSE, FALSE, sizeof (uint32_t)); + spa_pod_dynamic_builder_init (&pod_builder, NULL, 0, PARAMS_BUFFER_SIZE); + + buffer_type = 1 << SPA_DATA_MemFd; + + pod_builder_add_object ( + &pod_builder.b, pod_offsets, + SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers, + SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int (8, 2, 8), + SPA_PARAM_BUFFERS_blocks, SPA_POD_Int (1), + SPA_PARAM_BUFFERS_align, SPA_POD_Int (16), + SPA_PARAM_BUFFERS_dataType, SPA_POD_CHOICE_FLAGS_Int (buffer_type)); + + pod_builder_add_object ( + &pod_builder.b, pod_offsets, + SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta, + SPA_PARAM_META_type, SPA_POD_Id (SPA_META_Header), + SPA_PARAM_META_size, SPA_POD_Int (sizeof (struct spa_meta_header))); + + params = finish_format_params (&pod_builder.b, pod_offsets); + + pw_stream_update_params (camera_stream->pipewire_stream, + (const struct spa_pod **) params->pdata, + params->len); + spa_pod_dynamic_builder_clean (&pod_builder); +} + +static gboolean +has_negotiated_dma_bufs (GrdRdpCameraStream *camera_stream, + struct pw_buffer *pw_buffer) +{ + struct spa_data *spa_data = &pw_buffer->buffer->datas[0]; + + if (spa_data->type & 1 << SPA_DATA_DmaBuf) + return TRUE; + + return FALSE; +} + +static gboolean +maybe_reset_decode_session (GrdRdpCameraStream *camera_stream, + gboolean using_dma_bufs, + GError **error) +{ + struct spa_video_info_raw *video_format = &camera_stream->video_format; + const char *device_name = camera_stream->device_name; + uint8_t stream_index = camera_stream->stream_index; + + if (!camera_stream->pending_decode_session_reset) + return TRUE; + + g_debug ("[RDP.CAM_DEVICE] Device \"%s\", stream %u: Resetting decode " + "session", device_name, stream_index); + + g_assert (!using_dma_bufs); + camera_stream->decode_session_in_use = camera_stream->sw_decode_session; + + if (!grd_decode_session_reset (camera_stream->decode_session_in_use, + video_format->size.width, + video_format->size.height, + video_format->modifier, + error)) + return FALSE; + + camera_stream->pending_decode_session_reset = FALSE; + + return TRUE; +} + +static void +maybe_init_buffer_data (struct pw_buffer *pw_buffer) +{ + struct spa_data *spa_data = &pw_buffer->buffer->datas[0]; + + if (spa_data->type != SPA_DATA_MemFd) + return; + + /* + * Some applications like Firefox or Chromium don't support BGRX images, but + * only BGRA images. The alpha bit is however untouched by FreeRDPs H.264 + * decoder. So initialize the memory with 0xFF to ensure that the opacity is + * always 100%. + */ + memset (spa_data->data, 0xFF, spa_data->maxsize); +} + +static void +on_stream_add_buffer (void *user_data, + struct pw_buffer *pw_buffer) +{ + GrdRdpCameraStream *camera_stream = user_data; + const char *device_name = camera_stream->device_name; + uint8_t stream_index = camera_stream->stream_index; + struct spa_data *spa_data = &pw_buffer->buffer->datas[0]; + g_autoptr (GError) error = NULL; + gboolean using_dma_bufs; + const char *buffer_type; + + g_hash_table_insert (camera_stream->buffer_contexts, + pw_buffer, buffer_context_new ()); + + if (!(spa_data->type & 1 << SPA_DATA_MemFd)) + { + pw_stream_set_error (camera_stream->pipewire_stream, -EINVAL, + "No supported PipeWire stream buffer data type " + "could be negotiated"); + return; + } + + using_dma_bufs = has_negotiated_dma_bufs (camera_stream, pw_buffer); + + buffer_type = using_dma_bufs ? "dma-buf" : "mem-fd"; + g_debug ("[RDP.CAM_DEVICE] Device \"%s\", stream %u: Allocating %s buffer " + "for PipeWire stream", device_name, stream_index, buffer_type); + + if (!maybe_reset_decode_session (camera_stream, using_dma_bufs, &error) || + !grd_decode_session_register_buffer (camera_stream->decode_session_in_use, + pw_buffer, &error)) + { + pw_stream_set_error (camera_stream->pipewire_stream, -EINVAL, + "%s", error->message); + return; + } + + maybe_init_buffer_data (pw_buffer); +} + +static void +discard_unanswered_sample_requests (GrdRdpCameraStream *camera_stream) +{ + GrdSampleBuffer *sample_buffer = NULL; + struct pw_buffer *pw_buffer = NULL; + GHashTableIter iter; + + g_mutex_lock (&camera_stream->dequeued_buffers_mutex); + g_hash_table_iter_init (&iter, camera_stream->dequeued_buffers); + while (g_hash_table_iter_next (&iter, + (gpointer *) &sample_buffer, + (gpointer *) &pw_buffer)) + { + if (g_hash_table_contains (camera_stream->buffers_in_decoding, + sample_buffer)) + continue; + + queue_corrupted_pw_buffer (camera_stream, pw_buffer); + g_hash_table_iter_remove (&iter); + } + g_mutex_unlock (&camera_stream->dequeued_buffers_mutex); +} + +static void +on_stream_remove_buffer (void *user_data, + struct pw_buffer *pw_buffer) +{ + GrdRdpCameraStream *camera_stream = user_data; + const char *device_name = camera_stream->device_name; + uint8_t stream_index = camera_stream->stream_index; + + discard_unanswered_sample_requests (camera_stream); + ensure_unlocked_pipewire_buffer (camera_stream, pw_buffer); + + g_debug ("[RDP.CAM_DEVICE] Device \"%s\", stream %u: Removing buffer for " + "PipeWire stream", device_name, stream_index); + + grd_decode_session_unregister_buffer (camera_stream->decode_session_in_use, + pw_buffer); + g_hash_table_remove (camera_stream->buffer_contexts, pw_buffer); +} + +static const struct pw_stream_events stream_events = +{ + .version = PW_VERSION_STREAM_EVENTS, + .state_changed = on_stream_state_changed, + .param_changed = on_stream_param_changed, + .add_buffer = on_stream_add_buffer, + .remove_buffer = on_stream_remove_buffer, +}; + +static gboolean +set_up_video_source (GrdRdpCameraStream *camera_stream, + struct pw_core *pipewire_core, + GError **error) +{ + const char *device_name = camera_stream->device_name; + g_autofree char *stream_name = NULL; + g_autoptr (GArray) pod_offsets = NULL; + g_autoptr (GPtrArray) params = NULL; + struct spa_pod_dynamic_builder pod_builder; + int result; + + stream_name = + g_strdup_printf ("%s::%u", device_name, camera_stream->stream_index); + + camera_stream->pipewire_stream = + pw_stream_new (pipewire_core, stream_name, + pw_properties_new (PW_KEY_MEDIA_TYPE, "Video", + PW_KEY_MEDIA_CATEGORY, "Capture", + PW_KEY_MEDIA_ROLE, "Camera", + PW_KEY_MEDIA_CLASS, "Video/Source", + PW_KEY_NODE_NAME, "grd_remote_video_source", + PW_KEY_NODE_NICK, device_name, + PW_KEY_NODE_DESCRIPTION, device_name, + NULL)); + if (!camera_stream->pipewire_stream) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create PipeWire stream"); + return FALSE; + } + + pw_stream_add_listener (camera_stream->pipewire_stream, + &camera_stream->pipewire_stream_listener, + &stream_events, camera_stream); + + pod_offsets = g_array_new (FALSE, FALSE, sizeof (uint32_t)); + spa_pod_dynamic_builder_init (&pod_builder, NULL, 0, PARAMS_BUFFER_SIZE); + + build_format_params (camera_stream, &pod_builder.b, pod_offsets); + params = finish_format_params (&pod_builder.b, pod_offsets); + + result = pw_stream_connect (camera_stream->pipewire_stream, + PW_DIRECTION_OUTPUT, PW_ID_ANY, + PW_STREAM_FLAG_DRIVER | + PW_STREAM_FLAG_ALLOC_BUFFERS, + (const struct spa_pod **) params->pdata, + params->len); + spa_pod_dynamic_builder_clean (&pod_builder); + + if (result < 0) + { + g_set_error_literal (error, G_IO_ERROR, g_io_error_from_errno (-result), + strerror (-result)); + return FALSE; + } + + return TRUE; +} + +GrdRdpCameraStream * +grd_rdp_camera_stream_new (GrdRdpDvcCameraDevice *device, + GMainContext *camera_context, + struct pw_core *pipewire_core, + const char *device_name, + uint8_t stream_index, + GList *media_type_descriptions, + GError **error) +{ + g_autoptr (GrdRdpCameraStream) camera_stream = NULL; + + camera_stream = g_object_new (GRD_TYPE_RDP_CAMERA_STREAM, NULL); + camera_stream->device = device; + camera_stream->device_name = device_name; + camera_stream->stream_index = stream_index; + camera_stream->media_type_descriptions = media_type_descriptions; + + camera_stream->sw_decode_session = + (GrdDecodeSession *) grd_decode_session_sw_avc_new (error); + if (!camera_stream->sw_decode_session) + return NULL; + + camera_stream->frame_clock = grd_frame_clock_new (camera_context, + on_frame_clock_trigger, + camera_stream, + error); + if (!camera_stream->frame_clock) + return NULL; + + if (!set_up_video_source (camera_stream, pipewire_core, error)) + return NULL; + + return g_steal_pointer (&camera_stream); +} + +static void +grd_rdp_camera_stream_dispose (GObject *object) +{ + GrdRdpCameraStream *camera_stream = GRD_RDP_CAMERA_STREAM (object); + + if (camera_stream->pipewire_stream) + { + pw_stream_destroy (camera_stream->pipewire_stream); + camera_stream->pipewire_stream = NULL; + } + + g_mutex_lock (&camera_stream->dequeued_buffers_mutex); + g_assert (g_hash_table_size (camera_stream->dequeued_buffers) == 0); + g_assert (g_hash_table_size (camera_stream->buffers_in_decoding) == 0); + g_mutex_unlock (&camera_stream->dequeued_buffers_mutex); + + g_clear_object (&camera_stream->frame_clock); + g_clear_object (&camera_stream->sw_decode_session); + + G_OBJECT_CLASS (grd_rdp_camera_stream_parent_class)->dispose (object); +} + +static void +grd_rdp_camera_stream_finalize (GObject *object) +{ + GrdRdpCameraStream *camera_stream = GRD_RDP_CAMERA_STREAM (object); + + g_mutex_clear (&camera_stream->dequeued_buffers_mutex); + + g_clear_pointer (&camera_stream->dequeued_buffers, g_hash_table_unref); + g_clear_pointer (&camera_stream->buffers_in_decoding, g_hash_table_unref); + g_clear_pointer (&camera_stream->buffer_contexts, g_hash_table_unref); + + G_OBJECT_CLASS (grd_rdp_camera_stream_parent_class)->finalize (object); +} + +static void +grd_rdp_camera_stream_init (GrdRdpCameraStream *camera_stream) +{ + camera_stream->buffer_contexts = + g_hash_table_new_full (NULL, NULL, + NULL, (GDestroyNotify) buffer_context_free); + camera_stream->dequeued_buffers = g_hash_table_new (NULL, NULL); + camera_stream->buffers_in_decoding = g_hash_table_new (NULL, NULL); + + g_mutex_init (&camera_stream->dequeued_buffers_mutex); +} + +static void +grd_rdp_camera_stream_class_init (GrdRdpCameraStreamClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = grd_rdp_camera_stream_dispose; + object_class->finalize = grd_rdp_camera_stream_finalize; +} diff --git a/grd-rdp-camera-stream.h b/grd-rdp-camera-stream.h new file mode 100644 index 0000000..3cf40d2 --- /dev/null +++ b/grd-rdp-camera-stream.h @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2025 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include +#include + +#include "grd-types.h" + +#define GRD_TYPE_RDP_CAMERA_STREAM (grd_rdp_camera_stream_get_type ()) +G_DECLARE_FINAL_TYPE (GrdRdpCameraStream, grd_rdp_camera_stream, + GRD, RDP_CAMERA_STREAM, GObject) + +GrdRdpCameraStream *grd_rdp_camera_stream_new (GrdRdpDvcCameraDevice *device, + GMainContext *camera_context, + struct pw_core *pipewire_core, + const char *device_name, + uint8_t stream_index, + GList *media_type_descriptions, + GError **error); + +uint8_t grd_rdp_camera_stream_get_stream_index (GrdRdpCameraStream *camera_stream); + +gboolean grd_rdp_camera_stream_notify_stream_started (GrdRdpCameraStream *camera_stream, + uint32_t run_sequence); + +void grd_rdp_camera_stream_inhibit_camera_loop (GrdRdpCameraStream *camera_stream); + +void grd_rdp_camera_stream_uninhibit_camera_loop (GrdRdpCameraStream *camera_stream); + +gboolean grd_rdp_camera_stream_announce_new_sample (GrdRdpCameraStream *camera_stream, + GrdSampleBuffer *sample_buffer); + +void grd_rdp_camera_stream_submit_sample (GrdRdpCameraStream *camera_stream, + GrdSampleBuffer *sample_buffer, + gboolean success); diff --git a/grd-rdp-connect-time-autodetection.c b/grd-rdp-connect-time-autodetection.c new file mode 100644 index 0000000..2e624cd --- /dev/null +++ b/grd-rdp-connect-time-autodetection.c @@ -0,0 +1,643 @@ +/* + * Copyright (C) 2023 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-rdp-connect-time-autodetection.h" + +#define CT_BW_MAX_TOLERABLE_RESPONSE_LATENCY_US (400 * 1000) +#define CT_BW_MAX_TOLERABLE_TIME_DELTA_MS 100 + +#define CT_N_PINGS 10 +#define CT_PING_INTERVAL_MS 10 + +#define BW_MEASURE_SEQUENCE_NUMBER 0 +#define CT_NON_RTT_SEQUENCE_NUMBER 0 + +typedef enum +{ + CT_AUTODETECT_STATE_NONE, + CT_AUTODETECT_STATE_MEASURE_BW_1, + CT_AUTODETECT_STATE_MEASURE_BW_2, + CT_AUTODETECT_STATE_MEASURE_BW_3, + CT_AUTODETECT_STATE_AWAIT_BW_RESULT_1, + CT_AUTODETECT_STATE_AWAIT_BW_RESULT_2, + CT_AUTODETECT_STATE_AWAIT_BW_RESULT_3, + CT_AUTODETECT_STATE_START_RTT_DETECTION, + CT_AUTODETECT_STATE_AWAIT_LAST_RTT_RESPONSE, + CT_AUTODETECT_STATE_SEND_NET_CHAR_RESULT, + CT_AUTODETECT_STATE_COMPLETE, +} CtAutodetectState; + +struct _GrdRdpConnectTimeAutodetection +{ + GObject parent; + + GrdRdpNetworkAutodetection *network_autodetection; + rdpAutoDetect *rdp_autodetect; + gboolean protocol_stopped; + + GMutex ct_autodetection_mutex; + CtAutodetectState state; + + GSource *sync_source; + GCond net_char_sync_cond; + GMutex net_char_sync_mutex; + gboolean pending_net_char_sync; + + GSource *ping_source; + uint32_t pending_pings; + gboolean pending_last_sequence_number; + uint16_t last_sequence_number; + + int64_t base_round_trip_time_us; + int64_t avg_round_trip_time_us; + + int64_t bw_measure_time_us; + + uint32_t last_time_delta_ms; + uint32_t last_byte_count; +}; + +G_DEFINE_TYPE (GrdRdpConnectTimeAutodetection, grd_rdp_connect_time_autodetection, + G_TYPE_OBJECT) + +gboolean +grd_rdp_connect_time_autodetection_is_complete (GrdRdpConnectTimeAutodetection *ct_autodetection) +{ + g_autoptr (GMutexLocker) locker = NULL; + + locker = g_mutex_locker_new (&ct_autodetection->ct_autodetection_mutex); + return ct_autodetection->state == CT_AUTODETECT_STATE_COMPLETE; +} + +static void +determine_bw_measure_payloads (GrdRdpConnectTimeAutodetection *ct_autodetection, + uint16_t *payload_lengths, + uint32_t *n_payloads) +{ + switch (ct_autodetection->state) + { + case CT_AUTODETECT_STATE_MEASURE_BW_1: + *payload_lengths = 15 * 1024 + 512 + 256 + 128 + 64; + *n_payloads = 1; + break; + case CT_AUTODETECT_STATE_MEASURE_BW_2: + *payload_lengths = 15 * 1024 + 512 + 256 + 128 + 64; + *n_payloads = 4; + break; + case CT_AUTODETECT_STATE_MEASURE_BW_3: + *payload_lengths = 15 * 1024 + 512 + 256 + 128 + 64; + *n_payloads = 16; + break; + default: + g_assert_not_reached (); + } +} + +static CtAutodetectState +get_next_bw_measure_state (GrdRdpConnectTimeAutodetection *ct_autodetection) +{ + switch (ct_autodetection->state) + { + case CT_AUTODETECT_STATE_MEASURE_BW_1: + return CT_AUTODETECT_STATE_AWAIT_BW_RESULT_1; + case CT_AUTODETECT_STATE_MEASURE_BW_2: + return CT_AUTODETECT_STATE_AWAIT_BW_RESULT_2; + case CT_AUTODETECT_STATE_MEASURE_BW_3: + return CT_AUTODETECT_STATE_AWAIT_BW_RESULT_3; + case CT_AUTODETECT_STATE_AWAIT_BW_RESULT_1: + return CT_AUTODETECT_STATE_MEASURE_BW_2; + case CT_AUTODETECT_STATE_AWAIT_BW_RESULT_2: + return CT_AUTODETECT_STATE_MEASURE_BW_3; + default: + g_assert_not_reached (); + } + + return CT_AUTODETECT_STATE_NONE; +} + +static void +measure_connect_time_bandwidth (GrdRdpConnectTimeAutodetection *ct_autodetection, + uint16_t payload_lengths, + uint32_t n_payloads) +{ + rdpAutoDetect *rdp_autodetect = ct_autodetection->rdp_autodetect; + uint32_t i; + + g_assert (payload_lengths > 0); + g_assert (n_payloads >= 1); + + rdp_autodetect->BandwidthMeasureStart (rdp_autodetect, RDP_TRANSPORT_TCP, + BW_MEASURE_SEQUENCE_NUMBER); + for (i = 0; i < n_payloads - 1; ++i) + { + rdp_autodetect->BandwidthMeasurePayload (rdp_autodetect, + RDP_TRANSPORT_TCP, + BW_MEASURE_SEQUENCE_NUMBER, + payload_lengths); + } + rdp_autodetect->BandwidthMeasureStop (rdp_autodetect, RDP_TRANSPORT_TCP, + BW_MEASURE_SEQUENCE_NUMBER, + payload_lengths); +} + +static const char * +ct_autodetect_state_to_string (CtAutodetectState state) +{ + switch (state) + { + case CT_AUTODETECT_STATE_NONE: + return "NONE"; + case CT_AUTODETECT_STATE_MEASURE_BW_1: + return "MEASURE_BW_1"; + case CT_AUTODETECT_STATE_MEASURE_BW_2: + return "MEASURE_BW_2"; + case CT_AUTODETECT_STATE_MEASURE_BW_3: + return "MEASURE_BW_3"; + case CT_AUTODETECT_STATE_AWAIT_BW_RESULT_1: + return "AWAIT_BW_RESULT_1"; + case CT_AUTODETECT_STATE_AWAIT_BW_RESULT_2: + return "AWAIT_BW_RESULT_2"; + case CT_AUTODETECT_STATE_AWAIT_BW_RESULT_3: + return "AWAIT_BW_RESULT_3"; + case CT_AUTODETECT_STATE_START_RTT_DETECTION: + return "START_RTT_DETECTION"; + case CT_AUTODETECT_STATE_AWAIT_LAST_RTT_RESPONSE: + return "AWAIT_LAST_RTT_RESPONSE"; + case CT_AUTODETECT_STATE_SEND_NET_CHAR_RESULT: + return "SEND_NET_CHAR_RESULT"; + case CT_AUTODETECT_STATE_COMPLETE: + return "COMPLETE"; + } + + g_assert_not_reached (); +} + +static void +transition_to_state (GrdRdpConnectTimeAutodetection *ct_autodetection, + CtAutodetectState state) +{ + g_assert (ct_autodetection->state != state); + + g_debug ("[RDP] Connect-Time Autodetect: Updating state from %s to %s", + ct_autodetect_state_to_string (ct_autodetection->state), + ct_autodetect_state_to_string (state)); + + ct_autodetection->state = state; +} + +static void +last_sequence_number_ready (gpointer user_data, + uint16_t sequence_number) +{ + GrdRdpConnectTimeAutodetection *ct_autodetection = user_data; + + g_mutex_lock (&ct_autodetection->ct_autodetection_mutex); + ct_autodetection->last_sequence_number = sequence_number; + ct_autodetection->pending_last_sequence_number = FALSE; + g_mutex_unlock (&ct_autodetection->ct_autodetection_mutex); +} + +static gboolean +emit_ping (gpointer user_data) +{ + GrdRdpConnectTimeAutodetection *ct_autodetection = user_data; + GrdRdpNetworkAutodetection *network_autodetection = + ct_autodetection->network_autodetection; + GrdRdpNwAutodetectSequenceNumberReadyCallback sequence_number_ready = NULL; + + g_assert (!ct_autodetection->protocol_stopped); + + g_assert (ct_autodetection->pending_pings > 0); + --ct_autodetection->pending_pings; + + if (ct_autodetection->pending_pings == 0) + { + g_mutex_lock (&ct_autodetection->ct_autodetection_mutex); + g_clear_pointer (&ct_autodetection->ping_source, g_source_unref); + g_mutex_unlock (&ct_autodetection->ct_autodetection_mutex); + + sequence_number_ready = last_sequence_number_ready; + } + + grd_rdp_network_autodetection_emit_ping (network_autodetection, + sequence_number_ready, + ct_autodetection); + + if (ct_autodetection->pending_pings == 0) + return G_SOURCE_REMOVE; + + return G_SOURCE_CONTINUE; +} + +static void +start_ping_emission (GrdRdpConnectTimeAutodetection *ct_autodetection) +{ + emit_ping (ct_autodetection); + + g_mutex_lock (&ct_autodetection->ct_autodetection_mutex); + ct_autodetection->ping_source = g_timeout_source_new (CT_PING_INTERVAL_MS); + + g_source_set_callback (ct_autodetection->ping_source, emit_ping, + ct_autodetection, NULL); + g_source_attach (ct_autodetection->ping_source, NULL); + g_mutex_unlock (&ct_autodetection->ct_autodetection_mutex); +} + +static void +notify_network_characteristics_result (GrdRdpConnectTimeAutodetection *ct_autodetection) +{ + rdpAutoDetect *rdp_autodetect = ct_autodetection->rdp_autodetect; + rdpNetworkCharacteristicsResult result = {}; + uint64_t bit_count; + uint32_t bandwidth_kbits; + + bit_count = ((uint64_t) ct_autodetection->last_byte_count) * UINT64_C (8); + bandwidth_kbits = bit_count / MAX (ct_autodetection->last_time_delta_ms, 1); + + result.type = RDP_NETCHAR_RESULT_TYPE_BASE_RTT_BW_AVG_RTT; + result.baseRTT = ct_autodetection->base_round_trip_time_us / 1000; + result.averageRTT = ct_autodetection->avg_round_trip_time_us / 1000; + result.bandwidth = bandwidth_kbits; + + rdp_autodetect->NetworkCharacteristicsResult (rdp_autodetect, + RDP_TRANSPORT_TCP, + CT_NON_RTT_SEQUENCE_NUMBER, + &result); +} + +static FREERDP_AUTODETECT_STATE +detect_network_characteristics (GrdRdpConnectTimeAutodetection *ct_autodetection) +{ + CtAutodetectState new_state = CT_AUTODETECT_STATE_NONE; + g_autoptr (GMutexLocker) locker = NULL; + gboolean pending_bw_request = FALSE; + gboolean pending_ping_emission = FALSE; + gboolean pending_net_char_result = FALSE; + uint16_t payload_lengths = 0; + uint32_t n_payloads = 1; + + locker = g_mutex_locker_new (&ct_autodetection->ct_autodetection_mutex); + switch (ct_autodetection->state) + { + case CT_AUTODETECT_STATE_NONE: + g_assert_not_reached (); + case CT_AUTODETECT_STATE_MEASURE_BW_1: + case CT_AUTODETECT_STATE_MEASURE_BW_2: + case CT_AUTODETECT_STATE_MEASURE_BW_3: + determine_bw_measure_payloads (ct_autodetection, + &payload_lengths, &n_payloads); + ct_autodetection->bw_measure_time_us = g_get_monotonic_time (); + new_state = get_next_bw_measure_state (ct_autodetection); + pending_bw_request = TRUE; + break; + case CT_AUTODETECT_STATE_AWAIT_BW_RESULT_1: + case CT_AUTODETECT_STATE_AWAIT_BW_RESULT_2: + case CT_AUTODETECT_STATE_AWAIT_BW_RESULT_3: + return FREERDP_AUTODETECT_STATE_REQUEST; + case CT_AUTODETECT_STATE_START_RTT_DETECTION: + g_assert (!ct_autodetection->ping_source); + g_assert (ct_autodetection->pending_pings >= 2); + + new_state = CT_AUTODETECT_STATE_AWAIT_LAST_RTT_RESPONSE; + pending_ping_emission = TRUE; + break; + case CT_AUTODETECT_STATE_AWAIT_LAST_RTT_RESPONSE: + return FREERDP_AUTODETECT_STATE_REQUEST; + case CT_AUTODETECT_STATE_SEND_NET_CHAR_RESULT: + new_state = CT_AUTODETECT_STATE_COMPLETE; + pending_net_char_result = TRUE; + break; + case CT_AUTODETECT_STATE_COMPLETE: + return FREERDP_AUTODETECT_STATE_COMPLETE; + } + + g_assert (new_state != CT_AUTODETECT_STATE_NONE); + transition_to_state (ct_autodetection, new_state); + g_clear_pointer (&locker, g_mutex_locker_free); + + if (pending_bw_request) + { + measure_connect_time_bandwidth (ct_autodetection, payload_lengths, + n_payloads); + } + + if (pending_ping_emission) + start_ping_emission (ct_autodetection); + + if (pending_net_char_result) + { + notify_network_characteristics_result (ct_autodetection); + return FREERDP_AUTODETECT_STATE_COMPLETE; + } + + return FREERDP_AUTODETECT_STATE_REQUEST; +} + +void +grd_rdp_connect_time_autodetection_start_detection (GrdRdpConnectTimeAutodetection *ct_autodetection) +{ + g_mutex_lock (&ct_autodetection->ct_autodetection_mutex); + g_assert (ct_autodetection->state == CT_AUTODETECT_STATE_NONE); + + transition_to_state (ct_autodetection, CT_AUTODETECT_STATE_MEASURE_BW_1); + g_mutex_unlock (&ct_autodetection->ct_autodetection_mutex); + + detect_network_characteristics (ct_autodetection); +} + +void +grd_rdp_connect_time_autodetection_invoke_shutdown (GrdRdpConnectTimeAutodetection *ct_autodetection) +{ + g_mutex_lock (&ct_autodetection->net_char_sync_mutex); + ct_autodetection->protocol_stopped = TRUE; + g_cond_signal (&ct_autodetection->net_char_sync_cond); + g_mutex_unlock (&ct_autodetection->net_char_sync_mutex); +} + +static uint32_t +get_bw_measure_phase (GrdRdpConnectTimeAutodetection *ct_autodetection) +{ + switch (ct_autodetection->state) + { + case CT_AUTODETECT_STATE_AWAIT_BW_RESULT_1: + return 1; + case CT_AUTODETECT_STATE_AWAIT_BW_RESULT_2: + return 2; + case CT_AUTODETECT_STATE_AWAIT_BW_RESULT_3: + return 3; + default: + g_assert_not_reached (); + } + + return 0; +} + +BOOL +grd_rdp_connect_time_autodetection_notify_bw_measure_results (GrdRdpConnectTimeAutodetection *ct_autodetection, + uint32_t time_delta_ms, + uint32_t byte_count) +{ + CtAutodetectState new_state = CT_AUTODETECT_STATE_NONE; + g_autoptr (GMutexLocker) locker = NULL; + int64_t current_time_us; + int64_t response_latency_us; + + locker = g_mutex_locker_new (&ct_autodetection->ct_autodetection_mutex); + switch (ct_autodetection->state) + { + case CT_AUTODETECT_STATE_AWAIT_BW_RESULT_1: + case CT_AUTODETECT_STATE_AWAIT_BW_RESULT_2: + current_time_us = g_get_monotonic_time (); + response_latency_us = current_time_us - ct_autodetection->bw_measure_time_us; + + if (response_latency_us < CT_BW_MAX_TOLERABLE_RESPONSE_LATENCY_US && + time_delta_ms < CT_BW_MAX_TOLERABLE_TIME_DELTA_MS) + new_state = get_next_bw_measure_state (ct_autodetection); + else + new_state = CT_AUTODETECT_STATE_START_RTT_DETECTION; + break; + case CT_AUTODETECT_STATE_AWAIT_BW_RESULT_3: + new_state = CT_AUTODETECT_STATE_START_RTT_DETECTION; + break; + default: + g_warning ("[RDP] Connect-Time Autodetect: Received stray Bandwidth " + "Measure Results PDU (current state: %s)", + ct_autodetect_state_to_string (ct_autodetection->state)); + return FALSE; + } + + g_debug ("[RDP] Connect-Time Autodetect: BW Measure Phase %u: " + "time delta: %ums, byte count: %u => %uKB/s", + get_bw_measure_phase (ct_autodetection), time_delta_ms, byte_count, + byte_count / MAX (time_delta_ms, 1)); + + ct_autodetection->last_time_delta_ms = time_delta_ms; + ct_autodetection->last_byte_count = byte_count; + + g_assert (new_state != CT_AUTODETECT_STATE_NONE); + transition_to_state (ct_autodetection, new_state); + + return TRUE; +} + +void +grd_rdp_connect_time_autodetection_notify_rtt_measure_response (GrdRdpConnectTimeAutodetection *ct_autodetection, + uint16_t sequence_number, + int64_t base_round_trip_time_us, + int64_t avg_round_trip_time_us) +{ + g_autoptr (GMutexLocker) locker = NULL; + + locker = g_mutex_locker_new (&ct_autodetection->ct_autodetection_mutex); + if (ct_autodetection->pending_last_sequence_number || + ct_autodetection->last_sequence_number != sequence_number) + return; + + g_assert (ct_autodetection->state == CT_AUTODETECT_STATE_AWAIT_LAST_RTT_RESPONSE); + + g_debug ("[RDP] Connect-Time Autodetect: base RTT: %lims, average RTT: %lims", + base_round_trip_time_us / 1000, avg_round_trip_time_us / 1000); + + ct_autodetection->base_round_trip_time_us = base_round_trip_time_us; + ct_autodetection->avg_round_trip_time_us = avg_round_trip_time_us; + + transition_to_state (ct_autodetection, CT_AUTODETECT_STATE_SEND_NET_CHAR_RESULT); +} + +static gboolean +sync_with_main_context (gpointer user_data) +{ + GrdRdpConnectTimeAutodetection *ct_autodetection = user_data; + + g_mutex_lock (&ct_autodetection->ct_autodetection_mutex); + if (ct_autodetection->ping_source) + { + g_source_destroy (ct_autodetection->ping_source); + g_clear_pointer (&ct_autodetection->ping_source, g_source_unref); + } + g_mutex_unlock (&ct_autodetection->ct_autodetection_mutex); + + g_mutex_lock (&ct_autodetection->net_char_sync_mutex); + g_clear_pointer (&ct_autodetection->sync_source, g_source_unref); + + ct_autodetection->pending_net_char_sync = FALSE; + g_cond_signal (&ct_autodetection->net_char_sync_cond); + g_mutex_unlock (&ct_autodetection->net_char_sync_mutex); + + return G_SOURCE_REMOVE; +} + +static void +sync_ct_autodetect_handling (GrdRdpConnectTimeAutodetection *ct_autodetection) +{ + g_mutex_lock (&ct_autodetection->net_char_sync_mutex); + ct_autodetection->pending_net_char_sync = TRUE; + + ct_autodetection->sync_source = g_idle_source_new (); + g_source_set_callback (ct_autodetection->sync_source, sync_with_main_context, + ct_autodetection, NULL); + g_source_attach (ct_autodetection->sync_source, NULL); + + while (!ct_autodetection->protocol_stopped && + ct_autodetection->pending_net_char_sync) + { + g_cond_wait (&ct_autodetection->net_char_sync_cond, + &ct_autodetection->net_char_sync_mutex); + } + g_mutex_unlock (&ct_autodetection->net_char_sync_mutex); +} + +static BOOL +autodetect_network_characteristics_sync (rdpAutoDetect *rdp_autodetect, + RDP_TRANSPORT_TYPE transport_type, + uint16_t sequenceNumber, + uint32_t bandwidth, + uint32_t rtt) +{ + GrdRdpNetworkAutodetection *network_autodetection = rdp_autodetect->custom; + GrdRdpConnectTimeAutodetection *ct_autodetection = + grd_rdp_network_autodetection_get_ct_handler (network_autodetection); + g_autoptr (GMutexLocker) locker = NULL; + gboolean pending_sync = FALSE; + + locker = g_mutex_locker_new (&ct_autodetection->ct_autodetection_mutex); + switch (ct_autodetection->state) + { + case CT_AUTODETECT_STATE_NONE: + case CT_AUTODETECT_STATE_MEASURE_BW_1: + case CT_AUTODETECT_STATE_MEASURE_BW_2: + case CT_AUTODETECT_STATE_MEASURE_BW_3: + g_assert_not_reached (); + break; + case CT_AUTODETECT_STATE_AWAIT_BW_RESULT_1: + case CT_AUTODETECT_STATE_AWAIT_BW_RESULT_2: + case CT_AUTODETECT_STATE_AWAIT_BW_RESULT_3: + g_assert (!pending_sync); + break; + case CT_AUTODETECT_STATE_START_RTT_DETECTION: + g_assert_not_reached (); + break; + case CT_AUTODETECT_STATE_AWAIT_LAST_RTT_RESPONSE: + pending_sync = TRUE; + break; + case CT_AUTODETECT_STATE_SEND_NET_CHAR_RESULT: + g_assert_not_reached (); + break; + case CT_AUTODETECT_STATE_COMPLETE: + g_warning ("[RDP] Connect-Time Autodetect: Received stray Network " + "Characteristics Sync PDU (current state: %s). Ignoring PDU...", + ct_autodetect_state_to_string (ct_autodetection->state)); + return TRUE; + } + g_clear_pointer (&locker, g_mutex_locker_free); + + if (pending_sync) + sync_ct_autodetect_handling (ct_autodetection); + + g_debug ("[RDP] Connect-Time Autodetect: Received Sync PDU: " + "RTT: %ums, bandwidth: %uKBit/s", rtt, bandwidth); + + locker = g_mutex_locker_new (&ct_autodetection->ct_autodetection_mutex); + transition_to_state (ct_autodetection, CT_AUTODETECT_STATE_COMPLETE); + + return TRUE; +} + +static FREERDP_AUTODETECT_STATE +autodetect_on_connect_time_autodetect_progress (rdpAutoDetect *rdp_autodetect) +{ + GrdRdpNetworkAutodetection *network_autodetection = rdp_autodetect->custom; + GrdRdpConnectTimeAutodetection *ct_autodetection = + grd_rdp_network_autodetection_get_ct_handler (network_autodetection); + + return detect_network_characteristics (ct_autodetection); +} + +GrdRdpConnectTimeAutodetection * +grd_rdp_connect_time_autodetection_new (GrdRdpNetworkAutodetection *network_autodetection, + rdpAutoDetect *rdp_autodetect) +{ + GrdRdpConnectTimeAutodetection *ct_autodetection; + + ct_autodetection = g_object_new (GRD_TYPE_RDP_CONNECT_TIME_AUTODETECTION, NULL); + ct_autodetection->network_autodetection = network_autodetection; + ct_autodetection->rdp_autodetect = rdp_autodetect; + + rdp_autodetect->NetworkCharacteristicsSync = autodetect_network_characteristics_sync; + rdp_autodetect->OnConnectTimeAutoDetectProgress = autodetect_on_connect_time_autodetect_progress; + + return ct_autodetection; +} + +static void +grd_rdp_connect_time_autodetection_dispose (GObject *object) +{ + GrdRdpConnectTimeAutodetection *ct_autodetection = GRD_RDP_CONNECT_TIME_AUTODETECTION (object); + + g_assert (ct_autodetection->protocol_stopped); + + if (ct_autodetection->sync_source) + { + g_source_destroy (ct_autodetection->sync_source); + g_clear_pointer (&ct_autodetection->sync_source, g_source_unref); + } + if (ct_autodetection->ping_source) + { + g_source_destroy (ct_autodetection->ping_source); + g_clear_pointer (&ct_autodetection->ping_source, g_source_unref); + } + + G_OBJECT_CLASS (grd_rdp_connect_time_autodetection_parent_class)->dispose (object); +} + +static void +grd_rdp_connect_time_autodetection_finalize (GObject *object) +{ + GrdRdpConnectTimeAutodetection *ct_autodetection = GRD_RDP_CONNECT_TIME_AUTODETECTION (object); + + g_cond_clear (&ct_autodetection->net_char_sync_cond); + g_mutex_clear (&ct_autodetection->net_char_sync_mutex); + g_mutex_clear (&ct_autodetection->ct_autodetection_mutex); + + G_OBJECT_CLASS (grd_rdp_connect_time_autodetection_parent_class)->finalize (object); +} + +static void +grd_rdp_connect_time_autodetection_init (GrdRdpConnectTimeAutodetection *ct_autodetection) +{ + ct_autodetection->state = CT_AUTODETECT_STATE_NONE; + + ct_autodetection->pending_pings = CT_N_PINGS; + ct_autodetection->pending_last_sequence_number = TRUE; + + g_mutex_init (&ct_autodetection->ct_autodetection_mutex); + g_mutex_init (&ct_autodetection->net_char_sync_mutex); + g_cond_init (&ct_autodetection->net_char_sync_cond); +} + +static void +grd_rdp_connect_time_autodetection_class_init (GrdRdpConnectTimeAutodetectionClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = grd_rdp_connect_time_autodetection_dispose; + object_class->finalize = grd_rdp_connect_time_autodetection_finalize; +} diff --git a/grd-rdp-connect-time-autodetection.h b/grd-rdp-connect-time-autodetection.h new file mode 100644 index 0000000..24455c0 --- /dev/null +++ b/grd-rdp-connect-time-autodetection.h @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2023 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include + +#include "grd-rdp-network-autodetection.h" + +#define GRD_TYPE_RDP_CONNECT_TIME_AUTODETECTION (grd_rdp_connect_time_autodetection_get_type ()) +G_DECLARE_FINAL_TYPE (GrdRdpConnectTimeAutodetection, grd_rdp_connect_time_autodetection, + GRD, RDP_CONNECT_TIME_AUTODETECTION, GObject) + +GrdRdpConnectTimeAutodetection *grd_rdp_connect_time_autodetection_new (GrdRdpNetworkAutodetection *network_autodetection, + rdpAutoDetect *rdp_autodetect); + +gboolean grd_rdp_connect_time_autodetection_is_complete (GrdRdpConnectTimeAutodetection *ct_autodetection); + +void grd_rdp_connect_time_autodetection_start_detection (GrdRdpConnectTimeAutodetection *ct_autodetection); + +void grd_rdp_connect_time_autodetection_invoke_shutdown (GrdRdpConnectTimeAutodetection *ct_autodetection); + +BOOL grd_rdp_connect_time_autodetection_notify_bw_measure_results (GrdRdpConnectTimeAutodetection *ct_autodetection, + uint32_t time_delta_ms, + uint32_t byte_count); + +void grd_rdp_connect_time_autodetection_notify_rtt_measure_response (GrdRdpConnectTimeAutodetection *ct_autodetection, + uint16_t sequence_number, + int64_t base_round_trip_time_us, + int64_t avg_round_trip_time_us); diff --git a/grd-rdp-cursor-renderer.c b/grd-rdp-cursor-renderer.c new file mode 100644 index 0000000..bbfb02c --- /dev/null +++ b/grd-rdp-cursor-renderer.c @@ -0,0 +1,572 @@ +/* + * Copyright (C) 2023 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-rdp-cursor-renderer.h" + +#include "grd-damage-utils.h" +#include "grd-rdp-renderer.h" + +typedef struct +{ + GrdRdpCursorUpdate *cursor_update; + + int64_t last_used; +} GrdRdpCursor; + +struct _GrdRdpCursorRenderer +{ + GObject parent; + + gboolean session_ready; + + rdpContext *rdp_context; + + GSource *render_source; + /* Current cursor pointer does not own cursor data */ + GrdRdpCursor *current_cursor; + + GrdRdpCursor current_system_cursor; + /* Current cached cursor pointer does not own cursor data */ + GrdRdpCursor *current_cached_cursor; + + uint32_t pointer_cache_size; + GrdRdpCursor *pointer_cache; + + GMutex update_mutex; + GrdRdpCursorUpdate *pending_cursor_update; +}; + +G_DEFINE_TYPE (GrdRdpCursorRenderer, grd_rdp_cursor_renderer, G_TYPE_OBJECT) + +static void +grd_rdp_cursor_update_free (GrdRdpCursorUpdate *cursor_update) +{ + g_free (cursor_update->bitmap); + g_free (cursor_update); +} + +void +grd_rdp_cursor_renderer_notify_session_ready (GrdRdpCursorRenderer *cursor_renderer) +{ + cursor_renderer->session_ready = TRUE; + + g_source_set_ready_time (cursor_renderer->render_source, 0); +} + +void +grd_rdp_cursor_renderer_submit_cursor_update (GrdRdpCursorRenderer *cursor_renderer, + GrdRdpCursorUpdate *cursor_update) +{ + g_mutex_lock (&cursor_renderer->update_mutex); + g_clear_pointer (&cursor_renderer->pending_cursor_update, + grd_rdp_cursor_update_free); + + cursor_renderer->pending_cursor_update = cursor_update; + g_mutex_unlock (&cursor_renderer->update_mutex); + + g_source_set_ready_time (cursor_renderer->render_source, 0); +} + +static void +update_to_system_cursor (GrdRdpCursorRenderer *cursor_renderer, + GrdRdpCursorUpdate *cursor_update) +{ + GrdRdpCursor *current_system_cursor = &cursor_renderer->current_system_cursor; + + g_clear_pointer (¤t_system_cursor->cursor_update, + grd_rdp_cursor_update_free); + current_system_cursor->cursor_update = cursor_update; + + cursor_renderer->current_cached_cursor = NULL; + cursor_renderer->current_cursor = current_system_cursor; +} + +static void +reset_cursor_shape_fastpath (GrdRdpCursorRenderer *cursor_renderer) +{ + rdpContext *rdp_context = cursor_renderer->rdp_context; + rdpUpdate *rdp_update = rdp_context->update; + POINTER_SYSTEM_UPDATE pointer_system = {}; + + pointer_system.type = SYSPTR_DEFAULT; + + rdp_update->pointer->PointerSystem (rdp_context, &pointer_system); +} + +static gboolean +maybe_reset_cursor_shape (GrdRdpCursorRenderer *cursor_renderer, + GrdRdpCursorUpdate *cursor_update) +{ + GrdRdpCursor *current_cursor = cursor_renderer->current_cursor; + GrdRdpCursorUpdate *current_cursor_update = NULL; + + if (current_cursor) + current_cursor_update = current_cursor->cursor_update; + + if (current_cursor_update && + current_cursor_update->update_type == GRD_RDP_CURSOR_UPDATE_TYPE_DEFAULT) + return FALSE; + + update_to_system_cursor (cursor_renderer, cursor_update); + + reset_cursor_shape_fastpath (cursor_renderer); + + return TRUE; +} + +static void +hide_cursor_fastpath (GrdRdpCursorRenderer *cursor_renderer) +{ + rdpContext *rdp_context = cursor_renderer->rdp_context; + rdpUpdate *rdp_update = rdp_context->update; + POINTER_SYSTEM_UPDATE pointer_system = {}; + + pointer_system.type = SYSPTR_NULL; + + rdp_update->pointer->PointerSystem (rdp_context, &pointer_system); +} + +static gboolean +maybe_hide_cursor (GrdRdpCursorRenderer *cursor_renderer, + GrdRdpCursorUpdate *cursor_update) +{ + GrdRdpCursor *current_cursor = cursor_renderer->current_cursor; + GrdRdpCursorUpdate *current_cursor_update = NULL; + + if (current_cursor) + current_cursor_update = current_cursor->cursor_update; + + if (current_cursor_update && + current_cursor_update->update_type == GRD_RDP_CURSOR_UPDATE_TYPE_HIDDEN) + return FALSE; + + update_to_system_cursor (cursor_renderer, cursor_update); + + hide_cursor_fastpath (cursor_renderer); + + return TRUE; +} + +static gboolean +is_cursor_hidden (GrdRdpCursorUpdate *cursor_update) +{ + uint32_t cursor_size; + uint8_t *src_data; + uint32_t i; + + cursor_size = ((uint32_t) cursor_update->width) * cursor_update->height; + for (i = 0, src_data = cursor_update->bitmap; i < cursor_size; ++i, ++src_data) + { + src_data += 3; + if (*src_data) + return FALSE; + } + + return TRUE; +} + +static gboolean +are_cursor_bitmaps_equal (GrdRdpCursorUpdate *first, + GrdRdpCursorUpdate *second) +{ + cairo_rectangle_int_t cairo_rect = {}; + + g_assert (first); + g_assert (second); + g_assert (first->update_type == GRD_RDP_CURSOR_UPDATE_TYPE_NORMAL); + g_assert (second->update_type == GRD_RDP_CURSOR_UPDATE_TYPE_NORMAL); + + if (first->hotspot_x != second->hotspot_x || + first->hotspot_y != second->hotspot_y || + first->width != second->width || + first->height != second->height) + return FALSE; + + cairo_rect.x = cairo_rect.y = 0; + cairo_rect.width = first->width; + cairo_rect.height = first->height; + + return !grd_is_tile_dirty (&cairo_rect, first->bitmap, second->bitmap, + first->width * 4u, 4u); +} + +static void +use_cached_cursor_fastpath (GrdRdpCursorRenderer *cursor_renderer, + uint16_t cache_index) +{ + rdpContext *rdp_context = cursor_renderer->rdp_context; + rdpUpdate *rdp_update = rdp_context->update; + POINTER_CACHED_UPDATE pointer_cached = {}; + + pointer_cached.cacheIndex = cache_index; + + rdp_update->pointer->PointerCached (rdp_context, &pointer_cached); +} + +static void +use_cached_cursor (GrdRdpCursorRenderer *cursor_renderer, + uint16_t cache_index) +{ + use_cached_cursor_fastpath (cursor_renderer, cache_index); +} + +GrdRdpCursor * +get_cursor_to_replace (GrdRdpCursorRenderer *cursor_renderer, + uint32_t *cache_index) +{ + GrdRdpCursor *lru_cursor = NULL; + uint32_t i; + + g_assert (cache_index); + + for (i = 0; i < cursor_renderer->pointer_cache_size; ++i) + { + GrdRdpCursor *cached_cursor = &cursor_renderer->pointer_cache[i]; + + /* Least recently used cursor */ + if (!lru_cursor || lru_cursor->last_used > cached_cursor->last_used) + { + lru_cursor = cached_cursor; + *cache_index = i; + } + } + g_assert (lru_cursor); + + return lru_cursor; +} + +static void +submit_cursor_fastpath (GrdRdpCursorRenderer *cursor_renderer, + GrdRdpCursorUpdate *cursor_update, + uint8_t *xor_mask, + uint32_t xor_mask_length, + uint32_t cache_index) +{ + rdpContext *rdp_context = cursor_renderer->rdp_context; + rdpUpdate *rdp_update = rdp_context->update; + POINTER_NEW_UPDATE pointer_new = {}; + POINTER_LARGE_UPDATE pointer_large = {}; + POINTER_COLOR_UPDATE *pointer_color; + + if (cursor_update->width <= 96 && cursor_update->height <= 96) + { + pointer_new.xorBpp = 32; + pointer_color = &pointer_new.colorPtrAttr; + pointer_color->cacheIndex = cache_index; + pointer_color->hotSpotX = cursor_update->hotspot_x; + pointer_color->hotSpotY = cursor_update->hotspot_y; + pointer_color->width = cursor_update->width; + pointer_color->height = cursor_update->height; + pointer_color->lengthAndMask = 0; + pointer_color->lengthXorMask = xor_mask_length; + pointer_color->xorMaskData = xor_mask; + pointer_color->andMaskData = NULL; + + rdp_update->pointer->PointerNew (rdp_context, &pointer_new); + } + else + { + pointer_large.xorBpp = 32; + pointer_large.cacheIndex = cache_index; + pointer_large.hotSpotX = cursor_update->hotspot_x; + pointer_large.hotSpotY = cursor_update->hotspot_y; + pointer_large.width = cursor_update->width; + pointer_large.height = cursor_update->height; + pointer_large.lengthAndMask = 0; + pointer_large.lengthXorMask = xor_mask_length; + pointer_large.xorMaskData = xor_mask; + pointer_large.andMaskData = NULL; + + rdp_update->pointer->PointerLarge (rdp_context, &pointer_large); + } +} + +static void +submit_cursor (GrdRdpCursorRenderer *cursor_renderer, + GrdRdpCursorUpdate *cursor_update, + uint32_t cache_index) +{ + uint32_t width = cursor_update->width; + uint32_t height = cursor_update->height; + g_autofree uint8_t *xor_mask = NULL; + uint32_t xor_mask_length; + uint32_t stride; + uint8_t *src_data, *dst_data; + uint8_t r, g, b, a; + uint32_t x, y; + + stride = width * 4u; + + xor_mask_length = height * stride * sizeof (uint8_t); + xor_mask = g_malloc0 (xor_mask_length); + + for (y = 0; y < height; ++y) + { + src_data = &cursor_update->bitmap[stride * (height - 1 - y)]; + dst_data = &xor_mask[stride * y]; + + for (x = 0; x < width; ++x) + { + r = *src_data++; + g = *src_data++; + b = *src_data++; + a = *src_data++; + + *dst_data++ = b; + *dst_data++ = g; + *dst_data++ = r; + *dst_data++ = a; + } + } + + submit_cursor_fastpath (cursor_renderer, cursor_update, + xor_mask, xor_mask_length, cache_index); +} + +static gboolean +maybe_update_cursor (GrdRdpCursorRenderer *cursor_renderer, + GrdRdpCursorUpdate *cursor_update) +{ + GrdRdpCursor *current_cursor = cursor_renderer->current_cursor; + GrdRdpCursorUpdate *current_cursor_update = NULL; + GrdRdpCursor *cursor_slot = NULL; + uint32_t cursor_slot_index = 0; + uint32_t i; + + g_assert (cursor_update->update_type == GRD_RDP_CURSOR_UPDATE_TYPE_NORMAL); + g_assert (cursor_update->bitmap); + + if (current_cursor) + current_cursor_update = current_cursor->cursor_update; + + if (is_cursor_hidden (cursor_update)) + { + cursor_update->update_type = GRD_RDP_CURSOR_UPDATE_TYPE_HIDDEN; + g_clear_pointer (&cursor_update->bitmap, g_free); + + return maybe_hide_cursor (cursor_renderer, cursor_update); + } + + /* RDP only handles pointer bitmaps up to 384x384 pixels */ + if (cursor_update->width > 384 || cursor_update->height > 384) + { + cursor_update->update_type = GRD_RDP_CURSOR_UPDATE_TYPE_DEFAULT; + g_clear_pointer (&cursor_update->bitmap, g_free); + + return maybe_reset_cursor_shape (cursor_renderer, cursor_update); + } + + if (current_cursor_update && + current_cursor_update->update_type == GRD_RDP_CURSOR_UPDATE_TYPE_NORMAL && + are_cursor_bitmaps_equal (cursor_update, current_cursor_update)) + { + current_cursor->last_used = g_get_monotonic_time (); + return FALSE; + } + + for (i = 0; i < cursor_renderer->pointer_cache_size; ++i) + { + GrdRdpCursor *cached_cursor = &cursor_renderer->pointer_cache[i]; + GrdRdpCursorUpdate *cached_cursor_update = cached_cursor->cursor_update; + GrdRdpCursorUpdateType update_type; + + if (!cached_cursor_update) + { + g_assert (!cursor_slot); + cursor_slot = cached_cursor; + cursor_slot_index = i; + break; + } + + update_type = cached_cursor_update->update_type; + g_assert (update_type == GRD_RDP_CURSOR_UPDATE_TYPE_NORMAL); + + g_assert (cached_cursor); + if (current_cursor == cached_cursor) + continue; + + if (!are_cursor_bitmaps_equal (cursor_update, cached_cursor_update)) + continue; + + cursor_renderer->current_cached_cursor = cached_cursor; + cursor_renderer->current_cursor = cursor_renderer->current_cached_cursor; + + cached_cursor->last_used = g_get_monotonic_time (); + use_cached_cursor (cursor_renderer, i); + + return FALSE; + } + + if (!cursor_slot) + cursor_slot = get_cursor_to_replace (cursor_renderer, &cursor_slot_index); + + g_clear_pointer (&cursor_slot->cursor_update, grd_rdp_cursor_update_free); + cursor_slot->cursor_update = cursor_update; + + cursor_renderer->current_cached_cursor = cursor_slot; + cursor_renderer->current_cursor = cursor_renderer->current_cached_cursor; + + cursor_slot->last_used = g_get_monotonic_time (); + submit_cursor (cursor_renderer, cursor_update, cursor_slot_index); + use_cached_cursor (cursor_renderer, cursor_slot_index); + + return TRUE; +} + +static gboolean +maybe_render_cursor (gpointer user_data) +{ + GrdRdpCursorRenderer *cursor_renderer = user_data; + GrdRdpCursorUpdate *cursor_update; + gboolean update_taken = FALSE; + + if (!cursor_renderer->session_ready) + return G_SOURCE_CONTINUE; + + g_mutex_lock (&cursor_renderer->update_mutex); + cursor_update = g_steal_pointer (&cursor_renderer->pending_cursor_update); + g_mutex_unlock (&cursor_renderer->update_mutex); + + if (!cursor_update) + return G_SOURCE_CONTINUE; + + switch (cursor_update->update_type) + { + case GRD_RDP_CURSOR_UPDATE_TYPE_DEFAULT: + update_taken = maybe_reset_cursor_shape (cursor_renderer, cursor_update); + break; + case GRD_RDP_CURSOR_UPDATE_TYPE_HIDDEN: + update_taken = maybe_hide_cursor (cursor_renderer, cursor_update); + break; + case GRD_RDP_CURSOR_UPDATE_TYPE_NORMAL: + update_taken = maybe_update_cursor (cursor_renderer, cursor_update); + break; + } + + if (!update_taken) + grd_rdp_cursor_update_free (cursor_update); + + return G_SOURCE_CONTINUE; +} + +static gboolean +render_source_dispatch (GSource *source, + GSourceFunc callback, + gpointer user_data) +{ + g_source_set_ready_time (source, -1); + + return callback (user_data); +} + +static GSourceFuncs render_source_funcs = +{ + .dispatch = render_source_dispatch, +}; + +GrdRdpCursorRenderer * +grd_rdp_cursor_renderer_new (GrdRdpRenderer *renderer, + rdpContext *rdp_context) +{ + GMainContext *render_context = grd_rdp_renderer_get_graphics_context (renderer); + rdpSettings *rdp_settings = rdp_context->settings; + GrdRdpCursorRenderer *cursor_renderer; + GSource *render_source; + + cursor_renderer = g_object_new (GRD_TYPE_RDP_CURSOR_RENDERER, NULL); + cursor_renderer->rdp_context = rdp_context; + + cursor_renderer->pointer_cache_size = + freerdp_settings_get_uint32 (rdp_settings, FreeRDP_PointerCacheSize); + g_assert (cursor_renderer->pointer_cache_size > 0); + g_assert (cursor_renderer->pointer_cache_size <= UINT16_MAX); + + g_debug ("[RDP] Cursor renderer: Cache has %u slots", + cursor_renderer->pointer_cache_size); + + cursor_renderer->pointer_cache = g_new0 (GrdRdpCursor, + cursor_renderer->pointer_cache_size); + + render_source = g_source_new (&render_source_funcs, sizeof (GSource)); + g_source_set_callback (render_source, maybe_render_cursor, + cursor_renderer, NULL); + g_source_set_ready_time (render_source, -1); + g_source_attach (render_source, render_context); + cursor_renderer->render_source = render_source; + + return cursor_renderer; +} + +static void +grd_rdp_cursor_renderer_dispose (GObject *object) +{ + GrdRdpCursorRenderer *cursor_renderer = GRD_RDP_CURSOR_RENDERER (object); + + if (cursor_renderer->render_source) + { + g_source_destroy (cursor_renderer->render_source); + g_clear_pointer (&cursor_renderer->render_source, g_source_unref); + } + + if (cursor_renderer->pointer_cache) + { + uint32_t i; + + for (i = 0; i < cursor_renderer->pointer_cache_size; ++i) + { + g_clear_pointer (&cursor_renderer->pointer_cache[i].cursor_update, + grd_rdp_cursor_update_free); + } + g_clear_pointer (&cursor_renderer->pointer_cache, g_free); + } + + g_clear_pointer (&cursor_renderer->current_system_cursor.cursor_update, + grd_rdp_cursor_update_free); + g_clear_pointer (&cursor_renderer->pending_cursor_update, + grd_rdp_cursor_update_free); + + G_OBJECT_CLASS (grd_rdp_cursor_renderer_parent_class)->dispose (object); +} + +static void +grd_rdp_cursor_renderer_finalize (GObject *object) +{ + GrdRdpCursorRenderer *cursor_renderer = GRD_RDP_CURSOR_RENDERER (object); + + g_mutex_clear (&cursor_renderer->update_mutex); + + G_OBJECT_CLASS (grd_rdp_cursor_renderer_parent_class)->finalize (object); +} + +static void +grd_rdp_cursor_renderer_init (GrdRdpCursorRenderer *cursor_renderer) +{ + g_mutex_init (&cursor_renderer->update_mutex); +} + +static void +grd_rdp_cursor_renderer_class_init (GrdRdpCursorRendererClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = grd_rdp_cursor_renderer_dispose; + object_class->finalize = grd_rdp_cursor_renderer_finalize; +} diff --git a/grd-rdp-cursor-renderer.h b/grd-rdp-cursor-renderer.h new file mode 100644 index 0000000..fac9198 --- /dev/null +++ b/grd-rdp-cursor-renderer.h @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2023 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include +#include + +#include "grd-types.h" + +#define GRD_TYPE_RDP_CURSOR_RENDERER (grd_rdp_cursor_renderer_get_type ()) +G_DECLARE_FINAL_TYPE (GrdRdpCursorRenderer, grd_rdp_cursor_renderer, + GRD, RDP_CURSOR_RENDERER, GObject) + +typedef enum +{ + GRD_RDP_CURSOR_UPDATE_TYPE_DEFAULT = 0, + GRD_RDP_CURSOR_UPDATE_TYPE_HIDDEN, + GRD_RDP_CURSOR_UPDATE_TYPE_NORMAL, +} GrdRdpCursorUpdateType; + +typedef struct +{ + GrdRdpCursorUpdateType update_type; + + uint16_t hotspot_x; + uint16_t hotspot_y; + uint16_t width; + uint16_t height; + uint8_t *bitmap; +} GrdRdpCursorUpdate; + +GrdRdpCursorRenderer *grd_rdp_cursor_renderer_new (GrdRdpRenderer *renderer, + rdpContext *rdp_context); + +void grd_rdp_cursor_renderer_notify_session_ready (GrdRdpCursorRenderer *cursor_renderer); + +void grd_rdp_cursor_renderer_submit_cursor_update (GrdRdpCursorRenderer *cursor_renderer, + GrdRdpCursorUpdate *cursor_update); diff --git a/grd-rdp-damage-detector-cuda.c b/grd-rdp-damage-detector-cuda.c new file mode 100644 index 0000000..15b971f --- /dev/null +++ b/grd-rdp-damage-detector-cuda.c @@ -0,0 +1,461 @@ +/* + * Copyright (C) 2022 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-rdp-damage-detector-cuda.h" + +#include "grd-hwaccel-nvidia.h" +#include "grd-rdp-legacy-buffer.h" + +#define TILE_WIDTH 64 +#define TILE_HEIGHT 64 + +typedef struct _GrdRdpDamageDetectorCuda +{ + GrdRdpDamageDetector parent; + + CudaFunctions *cuda_funcs; + CUstream cuda_stream; + + CUfunction cu_chk_dmg_pxl; + CUfunction cu_cmb_dmg_arr_cols; + CUfunction cu_cmb_dmg_arr_rows; + CUfunction cu_simplify_dmg_arr; + + uint32_t surface_width; + uint32_t surface_height; + + uint32_t cols; + uint32_t rows; + + GrdRdpLegacyBuffer *last_framebuffer; + + CUdeviceptr region_is_damaged; + CUdeviceptr damage_array; + CUdeviceptr simplified_damage_array; +} GrdRdpDamageDetectorCuda; + +G_DEFINE_TYPE (GrdRdpDamageDetectorCuda, + grd_rdp_damage_detector_cuda, + GRD_TYPE_RDP_DAMAGE_DETECTOR) + +static gboolean +invalidate_surface (GrdRdpDamageDetector *detector) +{ + GrdRdpDamageDetectorCuda *detector_cuda = + GRD_RDP_DAMAGE_DETECTOR_CUDA (detector); + CudaFunctions *cuda_funcs = detector_cuda->cuda_funcs; + uint32_t surface_width = detector_cuda->surface_width; + uint32_t surface_height = detector_cuda->surface_height; + + g_clear_pointer (&detector_cuda->last_framebuffer, + grd_rdp_legacy_buffer_release); + + if (!detector_cuda->damage_array) + return TRUE; + + if (cuda_funcs->cuMemsetD8Async (detector_cuda->damage_array, + 1, surface_width * surface_height, + detector_cuda->cuda_stream) != CUDA_SUCCESS || + cuda_funcs->cuMemsetD8Async (detector_cuda->region_is_damaged, 1, 1, + detector_cuda->cuda_stream) != CUDA_SUCCESS) + { + g_warning ("[HWAccel.CUDA] Failed to set memory"); + return FALSE; + } + + return TRUE; +} + +static void +clear_cuda_pointer (GrdRdpDamageDetectorCuda *detector_cuda, + CUdeviceptr *device_ptr) +{ + if (!(*device_ptr)) + return; + + detector_cuda->cuda_funcs->cuMemFree (*device_ptr); + *device_ptr = 0; +} + +static gboolean +resize_surface (GrdRdpDamageDetector *detector, + uint32_t width, + uint32_t height) +{ + GrdRdpDamageDetectorCuda *detector_cuda = + GRD_RDP_DAMAGE_DETECTOR_CUDA (detector); + CudaFunctions *cuda_funcs = detector_cuda->cuda_funcs; + uint32_t cols; + uint32_t rows; + + g_clear_pointer (&detector_cuda->last_framebuffer, + grd_rdp_legacy_buffer_release); + + clear_cuda_pointer (detector_cuda, &detector_cuda->simplified_damage_array); + clear_cuda_pointer (detector_cuda, &detector_cuda->damage_array); + + detector_cuda->surface_width = width; + detector_cuda->surface_height = height; + + cols = width / TILE_WIDTH + (width % TILE_WIDTH ? 1 : 0); + rows = height / TILE_HEIGHT + (height % TILE_HEIGHT ? 1 : 0); + detector_cuda->cols = cols; + detector_cuda->rows = rows; + + if (cuda_funcs->cuMemAlloc (&detector_cuda->damage_array, + width * height) != CUDA_SUCCESS) + { + g_warning ("[HWAccel.CUDA] Failed to allocate damage array"); + return FALSE; + } + if (cuda_funcs->cuMemAlloc (&detector_cuda->simplified_damage_array, + cols * rows) != CUDA_SUCCESS) + { + g_warning ("[HWAccel.CUDA] Failed to allocate simplified damage array"); + return FALSE; + } + if (cuda_funcs->cuMemsetD8Async (detector_cuda->damage_array, 1, width * height, + detector_cuda->cuda_stream) != CUDA_SUCCESS || + cuda_funcs->cuMemsetD8Async (detector_cuda->region_is_damaged, 1, 1, + detector_cuda->cuda_stream) != CUDA_SUCCESS) + { + g_warning ("[HWAccel.CUDA] Failed to set memory"); + return FALSE; + } + + return TRUE; +} + +static gboolean +submit_new_framebuffer (GrdRdpDamageDetector *detector, + GrdRdpLegacyBuffer *buffer) +{ + GrdRdpDamageDetectorCuda *detector_cuda = + GRD_RDP_DAMAGE_DETECTOR_CUDA (detector); + CudaFunctions *cuda_funcs = detector_cuda->cuda_funcs; + uint32_t surface_width = detector_cuda->surface_width; + uint32_t surface_height = detector_cuda->surface_height; + CUdeviceptr current_data; + CUdeviceptr previous_data; + unsigned int grid_dim_x, grid_dim_y, grid_dim_z; + unsigned int block_dim_x, block_dim_y, block_dim_z; + void *args[8]; + + g_assert (detector_cuda->damage_array); + + if (!detector_cuda->last_framebuffer) + { + if (cuda_funcs->cuMemsetD8Async (detector_cuda->damage_array, + 1, surface_width * surface_height, + detector_cuda->cuda_stream) != CUDA_SUCCESS || + cuda_funcs->cuMemsetD8Async (detector_cuda->region_is_damaged, 1, 1, + detector_cuda->cuda_stream) != CUDA_SUCCESS) + { + g_warning ("[HWAccel.CUDA] Failed to set memory"); + return FALSE; + } + + detector_cuda->last_framebuffer = buffer; + + return TRUE; + } + + if (cuda_funcs->cuMemsetD8Async (detector_cuda->region_is_damaged, 0, 1, + detector_cuda->cuda_stream) != CUDA_SUCCESS) + { + g_warning ("[HWAccel.CUDA] Failed to set memory"); + return FALSE; + } + + current_data = grd_rdp_legacy_buffer_get_mapped_cuda_pointer (buffer); + previous_data = + grd_rdp_legacy_buffer_get_mapped_cuda_pointer (detector_cuda->last_framebuffer); + + /* Threads per blocks */ + block_dim_x = 32; + block_dim_y = 16; + block_dim_z = 1; + /* Amount of blocks per grid */ + grid_dim_x = surface_width / block_dim_x + + (surface_width % block_dim_x ? 1 : 0); + grid_dim_y = surface_height / block_dim_y + + (surface_height % block_dim_y ? 1 : 0); + grid_dim_z = 1; + + args[0] = &detector_cuda->damage_array; + args[1] = &detector_cuda->region_is_damaged; + args[2] = ¤t_data; + args[3] = &previous_data; + args[4] = &surface_width; + args[5] = &surface_width; + args[6] = &surface_height; + args[7] = &surface_width; + if (cuda_funcs->cuLaunchKernel (detector_cuda->cu_chk_dmg_pxl, + grid_dim_x, grid_dim_y, grid_dim_z, + block_dim_x, block_dim_y, block_dim_z, + 0, detector_cuda->cuda_stream, + args, NULL) != CUDA_SUCCESS) + { + g_warning ("[HWAccel.CUDA] Failed to launch CHK_DMG_PXL kernel"); + return FALSE; + } + + g_clear_pointer (&detector_cuda->last_framebuffer, + grd_rdp_legacy_buffer_release); + detector_cuda->last_framebuffer = buffer; + + return TRUE; +} + +static gboolean +is_region_damaged (GrdRdpDamageDetector *detector) +{ + GrdRdpDamageDetectorCuda *detector_cuda = + GRD_RDP_DAMAGE_DETECTOR_CUDA (detector); + CudaFunctions *cuda_funcs = detector_cuda->cuda_funcs; + uint8_t is_damaged; + + g_assert (detector_cuda->damage_array); + g_assert (detector_cuda->last_framebuffer); + + cuda_funcs->cuMemcpyDtoHAsync (&is_damaged, detector_cuda->region_is_damaged, + 1, detector_cuda->cuda_stream); + cuda_funcs->cuStreamSynchronize (detector_cuda->cuda_stream); + + return !!is_damaged; +} + +static cairo_region_t * +get_cairo_region (GrdRdpDamageDetectorCuda *detector_cuda, + uint8_t *simplified_damage_array) +{ + uint32_t surface_width = detector_cuda->surface_width; + uint32_t surface_height = detector_cuda->surface_height; + cairo_region_t *damage_region; + cairo_rectangle_int_t tile; + uint32_t x, y; + + damage_region = cairo_region_create (); + for (y = 0; y < detector_cuda->rows; ++y) + { + for (x = 0; x < detector_cuda->cols; ++x) + { + if (simplified_damage_array[y * detector_cuda->cols + x]) + { + tile.x = x * TILE_WIDTH; + tile.y = y * TILE_HEIGHT; + tile.width = surface_width - tile.x < TILE_WIDTH ? surface_width - tile.x + : TILE_WIDTH; + tile.height = surface_height - tile.y < TILE_HEIGHT ? surface_height - tile.y + : TILE_HEIGHT; + + cairo_region_union_rectangle (damage_region, &tile); + } + } + } + + return damage_region; +} + +static cairo_region_t * +get_damage_region (GrdRdpDamageDetector *detector) +{ + GrdRdpDamageDetectorCuda *detector_cuda = + GRD_RDP_DAMAGE_DETECTOR_CUDA (detector); + CudaFunctions *cuda_funcs = detector_cuda->cuda_funcs; + g_autofree uint8_t *simplified_damage_array_host = NULL; + unsigned int grid_dim_x, grid_dim_y, grid_dim_z; + unsigned int block_dim_x, block_dim_y, block_dim_z; + uint32_t combine_shift[6]; + void *args_cols[5]; + void *args_rows[5]; + void *args_simplify[6]; + uint32_t i; + + g_assert (detector_cuda->damage_array); + g_assert (detector_cuda->last_framebuffer); + + /* Threads per blocks */ + block_dim_x = 32; + block_dim_y = 16; + block_dim_z = 1; + + args_cols[0] = args_rows[0] = &detector_cuda->damage_array; + args_cols[1] = args_rows[1] = &detector_cuda->surface_width; + args_cols[2] = args_rows[2] = &detector_cuda->surface_height; + args_cols[3] = args_rows[3] = &detector_cuda->surface_width; + + for (i = 0; i < 6; ++i) + { + uint32_t full_blocks; + + combine_shift[i] = i; + args_cols[4] = &combine_shift[i]; + + full_blocks = detector_cuda->surface_width >> (i + 1); + + /* Amount of blocks per grid */ + grid_dim_x = full_blocks / block_dim_x + 1; + grid_dim_y = detector_cuda->surface_height / block_dim_y + + (detector_cuda->surface_height % block_dim_y ? 1 : 0); + grid_dim_z = 1; + + if (cuda_funcs->cuLaunchKernel (detector_cuda->cu_cmb_dmg_arr_cols, + grid_dim_x, grid_dim_y, grid_dim_z, + block_dim_x, block_dim_y, block_dim_z, + 0, detector_cuda->cuda_stream, + args_cols, NULL) != CUDA_SUCCESS) + { + g_warning ("[HWAccel.CUDA] Failed to launch CMB_DMG_ARR_COLS kernel"); + return NULL; + } + } + + for (i = 0; i < 6; ++i) + { + uint32_t full_blocks; + + args_rows[4] = &combine_shift[i]; + + full_blocks = detector_cuda->surface_height >> (i + 1); + + /* Amount of blocks per grid */ + grid_dim_x = detector_cuda->surface_width / block_dim_x + + (detector_cuda->surface_width % block_dim_x ? 1 : 0); + grid_dim_y = full_blocks / block_dim_y + 1; + grid_dim_z = 1; + + if (cuda_funcs->cuLaunchKernel (detector_cuda->cu_cmb_dmg_arr_rows, + grid_dim_x, grid_dim_y, grid_dim_z, + block_dim_x, block_dim_y, block_dim_z, + 0, detector_cuda->cuda_stream, + args_rows, NULL) != CUDA_SUCCESS) + { + g_warning ("[HWAccel.CUDA] Failed to launch CMB_DMG_ARR_ROWS kernel"); + return NULL; + } + } + + /* Amount of blocks per grid */ + grid_dim_x = detector_cuda->surface_width / block_dim_x + + (detector_cuda->surface_width % block_dim_x ? 1 : 0); + grid_dim_y = detector_cuda->surface_height / block_dim_y + + (detector_cuda->surface_height % block_dim_y ? 1 : 0); + grid_dim_z = 1; + + args_simplify[0] = &detector_cuda->simplified_damage_array; + args_simplify[1] = &detector_cuda->damage_array; + args_simplify[2] = &detector_cuda->cols; + args_simplify[3] = &detector_cuda->surface_width; + args_simplify[4] = &detector_cuda->surface_height; + args_simplify[5] = &detector_cuda->surface_width; + + if (cuda_funcs->cuLaunchKernel (detector_cuda->cu_simplify_dmg_arr, + grid_dim_x, grid_dim_y, grid_dim_z, + block_dim_x, block_dim_y, block_dim_z, + 0, detector_cuda->cuda_stream, + args_simplify, NULL) != CUDA_SUCCESS) + { + g_warning ("[HWAccel.CUDA] Failed to launch SIMPLIFY_DMG_ARR kernel"); + return NULL; + } + + simplified_damage_array_host = g_malloc0 (detector_cuda->cols * + detector_cuda->rows * + sizeof (uint8_t)); + + if (cuda_funcs->cuMemcpyDtoHAsync (simplified_damage_array_host, + detector_cuda->simplified_damage_array, + detector_cuda->cols * detector_cuda->rows, + detector_cuda->cuda_stream) != CUDA_SUCCESS) + { + g_warning ("[HWAccel.CUDA] Failed to transfer simplified damage array"); + return NULL; + } + if (cuda_funcs->cuStreamSynchronize (detector_cuda->cuda_stream) != CUDA_SUCCESS) + { + g_warning ("[HWAccel.CUDA] Failed to synchronize stream"); + return NULL; + } + + return get_cairo_region (detector_cuda, simplified_damage_array_host); +} + +GrdRdpDamageDetectorCuda * +grd_rdp_damage_detector_cuda_new (GrdHwAccelNvidia *hwaccel_nvidia, + CUstream cuda_stream) +{ + g_autoptr (GrdRdpDamageDetectorCuda) detector_cuda = NULL; + CudaFunctions *cuda_funcs; + + detector_cuda = g_object_new (GRD_TYPE_RDP_DAMAGE_DETECTOR_CUDA, NULL); + detector_cuda->cuda_stream = cuda_stream; + + grd_hwaccel_nvidia_get_cuda_functions (hwaccel_nvidia, + (gpointer *) &detector_cuda->cuda_funcs); + grd_hwaccel_nvidia_get_cuda_damage_kernels (hwaccel_nvidia, + &detector_cuda->cu_chk_dmg_pxl, + &detector_cuda->cu_cmb_dmg_arr_cols, + &detector_cuda->cu_cmb_dmg_arr_rows, + &detector_cuda->cu_simplify_dmg_arr); + + cuda_funcs = detector_cuda->cuda_funcs; + if (cuda_funcs->cuMemAlloc (&detector_cuda->region_is_damaged, 1) != CUDA_SUCCESS) + return NULL; + + return g_steal_pointer (&detector_cuda); +} + +static void +grd_rdp_damage_detector_cuda_dispose (GObject *object) +{ + GrdRdpDamageDetectorCuda *detector_cuda = + GRD_RDP_DAMAGE_DETECTOR_CUDA (object); + + g_assert (!detector_cuda->last_framebuffer); + + clear_cuda_pointer (detector_cuda, &detector_cuda->simplified_damage_array); + clear_cuda_pointer (detector_cuda, &detector_cuda->damage_array); + clear_cuda_pointer (detector_cuda, &detector_cuda->region_is_damaged); + + G_OBJECT_CLASS (grd_rdp_damage_detector_cuda_parent_class)->dispose (object); +} + +static void +grd_rdp_damage_detector_cuda_init (GrdRdpDamageDetectorCuda *detector_cuda) +{ +} + +static void +grd_rdp_damage_detector_cuda_class_init (GrdRdpDamageDetectorCudaClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GrdRdpDamageDetectorClass *detector_class = + GRD_RDP_DAMAGE_DETECTOR_CLASS (klass); + + object_class->dispose = grd_rdp_damage_detector_cuda_dispose; + + detector_class->invalidate_surface = invalidate_surface; + detector_class->resize_surface = resize_surface; + detector_class->submit_new_framebuffer = submit_new_framebuffer; + detector_class->is_region_damaged = is_region_damaged; + detector_class->get_damage_region = get_damage_region; +} diff --git a/grd-rdp-damage-detector-cuda.h b/grd-rdp-damage-detector-cuda.h new file mode 100644 index 0000000..8e0bac1 --- /dev/null +++ b/grd-rdp-damage-detector-cuda.h @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2022 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include + +#include "grd-rdp-damage-detector.h" + +#define GRD_TYPE_RDP_DAMAGE_DETECTOR_CUDA (grd_rdp_damage_detector_cuda_get_type ()) +G_DECLARE_FINAL_TYPE (GrdRdpDamageDetectorCuda, grd_rdp_damage_detector_cuda, + GRD, RDP_DAMAGE_DETECTOR_CUDA, GrdRdpDamageDetector) + +GrdRdpDamageDetectorCuda *grd_rdp_damage_detector_cuda_new (GrdHwAccelNvidia *hwaccel_nvidia, + CUstream cuda_stream); diff --git a/grd-rdp-damage-detector-memcmp.c b/grd-rdp-damage-detector-memcmp.c new file mode 100644 index 0000000..9576b08 --- /dev/null +++ b/grd-rdp-damage-detector-memcmp.c @@ -0,0 +1,249 @@ +/* + * Copyright (C) 2022 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-rdp-damage-detector-memcmp.h" + +#include "grd-damage-utils.h" +#include "grd-rdp-legacy-buffer.h" + +#define TILE_WIDTH 64 +#define TILE_HEIGHT 64 + +typedef struct _GrdRdpDamageDetectorMemcmp +{ + GrdRdpDamageDetector parent; + + uint32_t surface_width; + uint32_t surface_height; + + uint32_t cols; + uint32_t rows; + + GrdRdpLegacyBuffer *last_framebuffer; + + gboolean region_is_damaged; + uint8_t *damage_array; +} GrdRdpDamageDetectorMemcmp; + +G_DEFINE_TYPE (GrdRdpDamageDetectorMemcmp, + grd_rdp_damage_detector_memcmp, + GRD_TYPE_RDP_DAMAGE_DETECTOR) + +static gboolean +invalidate_surface (GrdRdpDamageDetector *detector) +{ + GrdRdpDamageDetectorMemcmp *detector_memcmp = + GRD_RDP_DAMAGE_DETECTOR_MEMCMP (detector); + uint32_t cols = detector_memcmp->cols; + uint32_t rows = detector_memcmp->rows; + + g_clear_pointer (&detector_memcmp->last_framebuffer, + grd_rdp_legacy_buffer_release); + + if (!detector_memcmp->damage_array) + return TRUE; + + memset (detector_memcmp->damage_array, 1, cols * rows); + detector_memcmp->region_is_damaged = TRUE; + + return TRUE; +} + +static gboolean +resize_surface (GrdRdpDamageDetector *detector, + uint32_t width, + uint32_t height) +{ + GrdRdpDamageDetectorMemcmp *detector_memcmp = + GRD_RDP_DAMAGE_DETECTOR_MEMCMP (detector); + uint32_t cols; + uint32_t rows; + + g_clear_pointer (&detector_memcmp->last_framebuffer, + grd_rdp_legacy_buffer_release); + g_clear_pointer (&detector_memcmp->damage_array, g_free); + + detector_memcmp->surface_width = width; + detector_memcmp->surface_height = height; + + cols = width / TILE_WIDTH + (width % TILE_WIDTH ? 1 : 0); + rows = height / TILE_HEIGHT + (height % TILE_HEIGHT ? 1 : 0); + detector_memcmp->damage_array = g_malloc0 (cols * rows * sizeof (uint8_t)); + + memset (detector_memcmp->damage_array, 1, cols * rows); + detector_memcmp->region_is_damaged = TRUE; + + detector_memcmp->cols = cols; + detector_memcmp->rows = rows; + + return TRUE; +} + +static gboolean +submit_new_framebuffer (GrdRdpDamageDetector *detector, + GrdRdpLegacyBuffer *buffer) +{ + GrdRdpDamageDetectorMemcmp *detector_memcmp = + GRD_RDP_DAMAGE_DETECTOR_MEMCMP (detector); + uint32_t surface_width = detector_memcmp->surface_width; + uint32_t surface_height = detector_memcmp->surface_height; + uint32_t cols = detector_memcmp->cols; + uint32_t rows = detector_memcmp->rows; + gboolean region_is_damaged = FALSE; + uint32_t x, y; + + g_assert (detector_memcmp->damage_array); + + if (!detector_memcmp->last_framebuffer) + { + detector_memcmp->last_framebuffer = buffer; + + memset (detector_memcmp->damage_array, 1, cols * rows); + detector_memcmp->region_is_damaged = TRUE; + return TRUE; + } + + for (y = 0; y < detector_memcmp->rows; ++y) + { + for (x = 0; x < detector_memcmp->cols; ++x) + { + GrdRdpLegacyBuffer *last_framebuffer = detector_memcmp->last_framebuffer; + cairo_rectangle_int_t tile; + uint8_t tile_damaged = 0; + + tile.x = x * TILE_WIDTH; + tile.y = y * TILE_HEIGHT; + tile.width = surface_width - tile.x < TILE_WIDTH ? surface_width - tile.x + : TILE_WIDTH; + tile.height = surface_height - tile.y < TILE_HEIGHT ? surface_height - tile.y + : TILE_HEIGHT; + + if (grd_is_tile_dirty (&tile, grd_rdp_legacy_buffer_get_local_data (buffer), + grd_rdp_legacy_buffer_get_local_data (last_framebuffer), + surface_width * 4, 4)) + { + tile_damaged = 1; + region_is_damaged = TRUE; + } + + detector_memcmp->damage_array[y * detector_memcmp->cols + x] = tile_damaged; + } + } + + g_clear_pointer (&detector_memcmp->last_framebuffer, + grd_rdp_legacy_buffer_release); + detector_memcmp->last_framebuffer = buffer; + detector_memcmp->region_is_damaged = region_is_damaged; + + return TRUE; +} + +static gboolean +is_region_damaged (GrdRdpDamageDetector *detector) +{ + GrdRdpDamageDetectorMemcmp *detector_memcmp = + GRD_RDP_DAMAGE_DETECTOR_MEMCMP (detector); + + g_assert (detector_memcmp->damage_array); + g_assert (detector_memcmp->last_framebuffer); + + return detector_memcmp->region_is_damaged; +} + +static cairo_region_t * +get_damage_region (GrdRdpDamageDetector *detector) +{ + GrdRdpDamageDetectorMemcmp *detector_memcmp = + GRD_RDP_DAMAGE_DETECTOR_MEMCMP (detector); + uint32_t surface_width = detector_memcmp->surface_width; + uint32_t surface_height = detector_memcmp->surface_height; + cairo_region_t *damage_region; + cairo_rectangle_int_t tile; + uint32_t x, y; + + g_assert (detector_memcmp->damage_array); + g_assert (detector_memcmp->last_framebuffer); + + damage_region = cairo_region_create (); + for (y = 0; y < detector_memcmp->rows; ++y) + { + for (x = 0; x < detector_memcmp->cols; ++x) + { + if (detector_memcmp->damage_array[y * detector_memcmp->cols + x]) + { + tile.x = x * TILE_WIDTH; + tile.y = y * TILE_HEIGHT; + tile.width = surface_width - tile.x < TILE_WIDTH ? surface_width - tile.x + : TILE_WIDTH; + tile.height = surface_height - tile.y < TILE_HEIGHT ? surface_height - tile.y + : TILE_HEIGHT; + + cairo_region_union_rectangle (damage_region, &tile); + } + } + } + + return damage_region; +} + +GrdRdpDamageDetectorMemcmp * +grd_rdp_damage_detector_memcmp_new (void) +{ + GrdRdpDamageDetectorMemcmp *detector_memcmp; + + detector_memcmp = g_object_new (GRD_TYPE_RDP_DAMAGE_DETECTOR_MEMCMP, NULL); + + return detector_memcmp; +} + +static void +grd_rdp_damage_detector_memcmp_dispose (GObject *object) +{ + GrdRdpDamageDetectorMemcmp *detector_memcmp = + GRD_RDP_DAMAGE_DETECTOR_MEMCMP (object); + + g_assert (!detector_memcmp->last_framebuffer); + + g_clear_pointer (&detector_memcmp->damage_array, g_free); + + G_OBJECT_CLASS (grd_rdp_damage_detector_memcmp_parent_class)->dispose (object); +} + +static void +grd_rdp_damage_detector_memcmp_init (GrdRdpDamageDetectorMemcmp *detector_memcmp) +{ +} + +static void +grd_rdp_damage_detector_memcmp_class_init (GrdRdpDamageDetectorMemcmpClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GrdRdpDamageDetectorClass *detector_class = + GRD_RDP_DAMAGE_DETECTOR_CLASS (klass); + + object_class->dispose = grd_rdp_damage_detector_memcmp_dispose; + + detector_class->invalidate_surface = invalidate_surface; + detector_class->resize_surface = resize_surface; + detector_class->submit_new_framebuffer = submit_new_framebuffer; + detector_class->is_region_damaged = is_region_damaged; + detector_class->get_damage_region = get_damage_region; +} diff --git a/grd-rdp-damage-detector-memcmp.h b/grd-rdp-damage-detector-memcmp.h new file mode 100644 index 0000000..959caa6 --- /dev/null +++ b/grd-rdp-damage-detector-memcmp.h @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2022 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include "grd-rdp-damage-detector.h" + +#define GRD_TYPE_RDP_DAMAGE_DETECTOR_MEMCMP (grd_rdp_damage_detector_memcmp_get_type ()) +G_DECLARE_FINAL_TYPE (GrdRdpDamageDetectorMemcmp, grd_rdp_damage_detector_memcmp, + GRD, RDP_DAMAGE_DETECTOR_MEMCMP, GrdRdpDamageDetector) + +GrdRdpDamageDetectorMemcmp *grd_rdp_damage_detector_memcmp_new (void); diff --git a/grd-rdp-damage-detector.c b/grd-rdp-damage-detector.c new file mode 100644 index 0000000..61933ad --- /dev/null +++ b/grd-rdp-damage-detector.c @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2022 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-rdp-damage-detector.h" + +G_DEFINE_TYPE (GrdRdpDamageDetector, grd_rdp_damage_detector, G_TYPE_OBJECT); + +gboolean +grd_rdp_damage_detector_invalidate_surface (GrdRdpDamageDetector *detector) +{ + GrdRdpDamageDetectorClass *klass = + GRD_RDP_DAMAGE_DETECTOR_GET_CLASS (detector); + + return klass->invalidate_surface (detector); +} + +gboolean +grd_rdp_damage_detector_resize_surface (GrdRdpDamageDetector *detector, + uint32_t width, + uint32_t height) +{ + GrdRdpDamageDetectorClass *klass = + GRD_RDP_DAMAGE_DETECTOR_GET_CLASS (detector); + + return klass->resize_surface (detector, width, height); +} + +gboolean +grd_rdp_damage_detector_submit_new_framebuffer (GrdRdpDamageDetector *detector, + GrdRdpLegacyBuffer *buffer) +{ + GrdRdpDamageDetectorClass *klass = + GRD_RDP_DAMAGE_DETECTOR_GET_CLASS (detector); + + return klass->submit_new_framebuffer (detector, buffer); +} + +gboolean +grd_rdp_damage_detector_is_region_damaged (GrdRdpDamageDetector *detector) +{ + GrdRdpDamageDetectorClass *klass = + GRD_RDP_DAMAGE_DETECTOR_GET_CLASS (detector); + + return klass->is_region_damaged (detector); +} + +cairo_region_t * +grd_rdp_damage_detector_get_damage_region (GrdRdpDamageDetector *detector) +{ + GrdRdpDamageDetectorClass *klass = + GRD_RDP_DAMAGE_DETECTOR_GET_CLASS (detector); + + return klass->get_damage_region (detector); +} + +static void +grd_rdp_damage_detector_init (GrdRdpDamageDetector *detector) +{ +} + +static void +grd_rdp_damage_detector_class_init (GrdRdpDamageDetectorClass *klass) +{ +} diff --git a/grd-rdp-damage-detector.h b/grd-rdp-damage-detector.h new file mode 100644 index 0000000..5d83b6d --- /dev/null +++ b/grd-rdp-damage-detector.h @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2022 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include +#include +#include + +#include "grd-types.h" + +#define GRD_TYPE_RDP_DAMAGE_DETECTOR (grd_rdp_damage_detector_get_type ()) +G_DECLARE_DERIVABLE_TYPE (GrdRdpDamageDetector, grd_rdp_damage_detector, + GRD, RDP_DAMAGE_DETECTOR, GObject) + +struct _GrdRdpDamageDetectorClass +{ + GObjectClass parent_class; + + gboolean (* invalidate_surface) (GrdRdpDamageDetector *detector); + gboolean (* resize_surface) (GrdRdpDamageDetector *detector, + uint32_t width, + uint32_t height); + gboolean (* submit_new_framebuffer) (GrdRdpDamageDetector *detector, + GrdRdpLegacyBuffer *buffer); + gboolean (* is_region_damaged) (GrdRdpDamageDetector *detector); + cairo_region_t *(* get_damage_region) (GrdRdpDamageDetector *detector); +}; + +gboolean grd_rdp_damage_detector_invalidate_surface (GrdRdpDamageDetector *detector); + +gboolean grd_rdp_damage_detector_resize_surface (GrdRdpDamageDetector *detector, + uint32_t width, + uint32_t height); + +gboolean grd_rdp_damage_detector_submit_new_framebuffer (GrdRdpDamageDetector *detector, + GrdRdpLegacyBuffer *buffer); + +gboolean grd_rdp_damage_detector_is_region_damaged (GrdRdpDamageDetector *detector); + +cairo_region_t *grd_rdp_damage_detector_get_damage_region (GrdRdpDamageDetector *detector); diff --git a/grd-rdp-dsp.c b/grd-rdp-dsp.c new file mode 100644 index 0000000..f83d5c5 --- /dev/null +++ b/grd-rdp-dsp.c @@ -0,0 +1,499 @@ +/* + * Copyright (C) 2022 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-rdp-dsp.h" + +#include +#include +#include + +#define G711_QUANT_MASK 0xF +#define G711_SEG_MASK 0x70 +#define G711_SEG_SHIFT 4 + +struct _GrdRdpDsp +{ + GObject parent; + + GrdRdpDspCreateFlag create_flags; + uint32_t n_channels; + + HANDLE_AACENCODER aac_encoder; + uint32_t aac_frame_length; + + OpusEncoder *opus_encoder; + uint32_t opus_frame_length; +}; + +G_DEFINE_TYPE (GrdRdpDsp, grd_rdp_dsp, G_TYPE_OBJECT) + +const char * +grd_rdp_dsp_codec_to_string (GrdRdpDspCodec dsp_codec) +{ + switch (dsp_codec) + { + case GRD_RDP_DSP_CODEC_NONE: + return "PCM"; + case GRD_RDP_DSP_CODEC_AAC: + return "AAC"; + case GRD_RDP_DSP_CODEC_ALAW: + return "A-law"; + case GRD_RDP_DSP_CODEC_OPUS: + return "Opus"; + } + + g_assert_not_reached (); +} + +uint32_t +grd_rdp_dsp_get_frames_per_packet (GrdRdpDsp *rdp_dsp, + GrdRdpDspCodec codec) +{ + g_assert (rdp_dsp->create_flags & GRD_RDP_DSP_CREATE_FLAG_ENCODER); + + switch (codec) + { + case GRD_RDP_DSP_CODEC_NONE: + g_assert_not_reached (); + case GRD_RDP_DSP_CODEC_AAC: + return rdp_dsp->aac_frame_length; + case GRD_RDP_DSP_CODEC_ALAW: + g_assert_not_reached (); + case GRD_RDP_DSP_CODEC_OPUS: + return rdp_dsp->opus_frame_length; + } + + g_assert_not_reached (); +} + +static gboolean +encode_aac (GrdRdpDsp *rdp_dsp, + int16_t *input_data, + int input_size, + int input_elem_size, + uint8_t **output_data, + uint32_t *output_size) +{ + AACENC_BufferIdentifier ident_in; + AACENC_BufferIdentifier ident_out; + AACENC_BufDesc buffer_descriptor_in = {}; + AACENC_BufDesc buffer_descriptor_out = {}; + AACENC_InArgs args_in = {}; + AACENC_OutArgs args_out = {}; + int output_allocation_size; + int output_elem_size; + AACENC_ERROR aac_error; + + *output_size = 0; + + ident_in = IN_AUDIO_DATA; + + buffer_descriptor_in.numBufs = 1; + buffer_descriptor_in.bufs = (void **) &input_data; + buffer_descriptor_in.bufferIdentifiers = (int *) &ident_in; + buffer_descriptor_in.bufSizes = &input_size; + buffer_descriptor_in.bufElSizes = &input_elem_size; + + ident_out = OUT_BITSTREAM_DATA; + output_elem_size = sizeof (uint8_t); + + output_allocation_size = input_size; + *output_data = g_malloc0 (output_allocation_size); + + buffer_descriptor_out.numBufs = 1; + buffer_descriptor_out.bufs = (void **) output_data; + buffer_descriptor_out.bufferIdentifiers = (int *) &ident_out; + buffer_descriptor_out.bufSizes = &output_allocation_size; + buffer_descriptor_out.bufElSizes = &output_elem_size; + + args_in.numInSamples = input_size / input_elem_size; + + aac_error = aacEncEncode (rdp_dsp->aac_encoder, &buffer_descriptor_in, + &buffer_descriptor_out, &args_in, &args_out); + if (aac_error != AACENC_OK) + { + g_warning ("[RDP.DSP] Failed to AAC encode samples"); + g_clear_pointer (output_data, g_free); + return FALSE; + } + + *output_size = args_out.numOutBytes; + + return TRUE; +} + +static gboolean +encode_opus (GrdRdpDsp *rdp_dsp, + int16_t *input_data, + uint32_t input_size, + uint32_t input_elem_size, + uint8_t **output_data, + uint32_t *output_size) +{ + uint32_t frames; + int32_t length; + int output_allocation_size; + + frames = input_size / rdp_dsp->n_channels / input_elem_size; + g_assert (frames > 0); + + output_allocation_size = input_size; + *output_data = g_malloc0 (output_allocation_size); + + length = opus_encode (rdp_dsp->opus_encoder, input_data, frames, + *output_data, output_allocation_size); + if (length < 0) + { + g_warning ("[RDP.DSP] Failed to Opus encode samples: %s", + opus_strerror (length)); + g_clear_pointer (output_data, g_free); + return FALSE; + } + *output_size = length; + + return TRUE; +} + +gboolean +grd_rdp_dsp_encode (GrdRdpDsp *rdp_dsp, + GrdRdpDspCodec codec, + int16_t *input_data, + uint32_t input_size, + uint32_t input_elem_size, + uint8_t **output_data, + uint32_t *output_size) +{ + g_assert (rdp_dsp->create_flags & GRD_RDP_DSP_CREATE_FLAG_ENCODER); + + switch (codec) + { + case GRD_RDP_DSP_CODEC_NONE: + g_assert_not_reached (); + return FALSE; + case GRD_RDP_DSP_CODEC_AAC: + return encode_aac (rdp_dsp, input_data, input_size, input_elem_size, + output_data, output_size); + case GRD_RDP_DSP_CODEC_ALAW: + g_assert_not_reached (); + return FALSE; + case GRD_RDP_DSP_CODEC_OPUS: + return encode_opus (rdp_dsp, input_data, input_size, input_elem_size, + output_data, output_size); + } + + g_assert_not_reached (); + + return FALSE; +} + +static int16_t +alaw_to_s16 (uint8_t alaw_value) +{ + int16_t segment; + int16_t temp; + + alaw_value ^= 0x55; + + temp = (alaw_value & G711_QUANT_MASK) << 4; + temp += 8; + + segment = (alaw_value & G711_SEG_MASK) >> G711_SEG_SHIFT; + + if (segment > 0) + temp += 0x100; + if (segment > 1) + temp <<= segment - 1; + + return alaw_value > 127 ? temp : -temp; +} + +static gboolean +decode_alaw (GrdRdpDsp *rdp_dsp, + uint8_t *input_data, + uint32_t input_size, + int16_t **output_data, + uint32_t *output_size) +{ + uint32_t i; + + g_assert (output_data); + g_assert (output_size); + + g_assert (input_data); + g_assert (input_size > 0); + + *output_size = input_size / sizeof (uint8_t) * sizeof (int16_t); + *output_data = g_malloc0 (*output_size); + + for (i = 0; i < input_size; ++i) + (*output_data)[i] = alaw_to_s16 (input_data[i]); + + return TRUE; +} + +gboolean +grd_rdp_dsp_decode (GrdRdpDsp *rdp_dsp, + GrdRdpDspCodec codec, + uint8_t *input_data, + uint32_t input_size, + int16_t **output_data, + uint32_t *output_size) +{ + g_assert (rdp_dsp->create_flags & GRD_RDP_DSP_CREATE_FLAG_DECODER); + + switch (codec) + { + case GRD_RDP_DSP_CODEC_NONE: + case GRD_RDP_DSP_CODEC_AAC: + g_assert_not_reached (); + return FALSE; + case GRD_RDP_DSP_CODEC_ALAW: + return decode_alaw (rdp_dsp, input_data, input_size, + output_data, output_size); + case GRD_RDP_DSP_CODEC_OPUS: + g_assert_not_reached (); + return FALSE; + } + + g_assert_not_reached (); + + return FALSE; +} + +static gboolean +create_aac_encoder (GrdRdpDsp *rdp_dsp, + uint32_t n_samples_per_sec, + uint32_t n_channels, + uint32_t bitrate, + GError **error) +{ + AACENC_InfoStruct info = {}; + AACENC_ERROR aac_error; + + /* + * Assert n_channels == 2 due to (currently) hardcoded + * AACENC_CHANNELMODE setting + */ + g_assert (n_channels == 2); + + aac_error = aacEncOpen (&rdp_dsp->aac_encoder, 0, n_channels); + if (aac_error != AACENC_OK) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create AAC encoder. AAC error %i", aac_error); + return FALSE; + } + + aac_error = aacEncoder_SetParam (rdp_dsp->aac_encoder, AACENC_AOT, + AOT_AAC_LC); + if (aac_error != AACENC_OK) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to set AAC encoder param AACENC_AOT. " + "AAC error %i", aac_error); + return FALSE; + } + + aac_error = aacEncoder_SetParam (rdp_dsp->aac_encoder, AACENC_BITRATE, + bitrate); + if (aac_error != AACENC_OK) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to set AAC encoder param AACENC_BITRATE. " + "AAC error %i", aac_error); + return FALSE; + } + + aac_error = aacEncoder_SetParam (rdp_dsp->aac_encoder, AACENC_SAMPLERATE, + n_samples_per_sec); + if (aac_error != AACENC_OK) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to set AAC encoder param AACENC_SAMPLERATE. " + "AAC error %i", aac_error); + return FALSE; + } + + aac_error = aacEncoder_SetParam (rdp_dsp->aac_encoder, AACENC_CHANNELMODE, + MODE_2); + if (aac_error != AACENC_OK) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to set AAC encoder param AACENC_CHANNELMODE. " + "AAC error %i", aac_error); + return FALSE; + } + + aac_error = aacEncoder_SetParam (rdp_dsp->aac_encoder, AACENC_CHANNELORDER, + 1); + if (aac_error != AACENC_OK) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to set AAC encoder param AACENC_CHANNELORDER. " + "AAC error %i", aac_error); + return FALSE; + } + + aac_error = aacEncoder_SetParam (rdp_dsp->aac_encoder, AACENC_TRANSMUX, + TT_MP4_RAW); + if (aac_error != AACENC_OK) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to set AAC encoder param AACENC_TRANSMUX. " + "AAC error %i", aac_error); + return FALSE; + } + + aac_error = aacEncoder_SetParam (rdp_dsp->aac_encoder, AACENC_AFTERBURNER, 1); + if (aac_error != AACENC_OK) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to set AAC encoder param AACENC_AFTERBURNER. " + "AAC error %i", aac_error); + return FALSE; + } + + aac_error = aacEncEncode (rdp_dsp->aac_encoder, NULL, NULL, NULL, NULL); + if (aac_error != AACENC_OK) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to initialize AAC encoder. AAC error %i", aac_error); + return FALSE; + } + + aac_error = aacEncInfo (rdp_dsp->aac_encoder, &info); + if (aac_error != AACENC_OK) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to acquire AAC encoder info. AAC error %i", + aac_error); + return FALSE; + } + rdp_dsp->aac_frame_length = info.frameLength; + + g_debug ("[RDP.DSP] AAC encoder: maxOutBufBytes: %u, maxAncBytes: %u, " + "inBufFillLevel: %u, inputChannels: %u, frameLength: %u, " + "nDelay: %u, nDelayCore: %u", + info.maxOutBufBytes, info.maxAncBytes, info.inBufFillLevel, + info.inputChannels, info.frameLength, info.nDelay, info.nDelayCore); + + return TRUE; +} + +static gboolean +create_opus_encoder (GrdRdpDsp *rdp_dsp, + uint32_t n_samples_per_sec, + uint32_t n_channels, + uint32_t bitrate, + GError **error) +{ + int opus_error = OPUS_OK; + + rdp_dsp->opus_encoder = opus_encoder_create (n_samples_per_sec, n_channels, + OPUS_APPLICATION_AUDIO, + &opus_error); + if (opus_error != OPUS_OK) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create Opus encoder: %s", + opus_strerror (opus_error)); + return FALSE; + } + g_assert (rdp_dsp->opus_encoder); + + opus_error = opus_encoder_ctl (rdp_dsp->opus_encoder, + OPUS_SET_BITRATE(bitrate)); + if (opus_error != OPUS_OK) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to set Opus encoder bitrate: %s", + opus_strerror (opus_error)); + return FALSE; + } + + rdp_dsp->opus_frame_length = n_samples_per_sec / 50; + + return TRUE; +} + +static gboolean +create_encoders (GrdRdpDsp *rdp_dsp, + const GrdRdpDspDescriptor *dsp_descriptor, + GError **error) +{ + rdp_dsp->n_channels = dsp_descriptor->n_channels; + + if (!create_aac_encoder (rdp_dsp, + dsp_descriptor->n_samples_per_sec_aac, + dsp_descriptor->n_channels, + dsp_descriptor->bitrate_aac, + error)) + return FALSE; + if (!create_opus_encoder (rdp_dsp, + dsp_descriptor->n_samples_per_sec_opus, + dsp_descriptor->n_channels, + dsp_descriptor->bitrate_opus, + error)) + return FALSE; + + return TRUE; +} + +GrdRdpDsp * +grd_rdp_dsp_new (const GrdRdpDspDescriptor *dsp_descriptor, + GError **error) +{ + g_autoptr (GrdRdpDsp) rdp_dsp = NULL; + + rdp_dsp = g_object_new (GRD_TYPE_RDP_DSP, NULL); + rdp_dsp->create_flags = dsp_descriptor->create_flags; + + if (dsp_descriptor->create_flags & GRD_RDP_DSP_CREATE_FLAG_ENCODER && + !create_encoders (rdp_dsp, dsp_descriptor, error)) + return NULL; + + return g_steal_pointer (&rdp_dsp); +} + +static void +grd_rdp_dsp_dispose (GObject *object) +{ + GrdRdpDsp *rdp_dsp = GRD_RDP_DSP (object); + + g_clear_pointer (&rdp_dsp->opus_encoder, opus_encoder_destroy); + + aacEncClose (&rdp_dsp->aac_encoder); + + G_OBJECT_CLASS (grd_rdp_dsp_parent_class)->dispose (object); +} + +static void +grd_rdp_dsp_init (GrdRdpDsp *rdp_dsp) +{ +} + +static void +grd_rdp_dsp_class_init (GrdRdpDspClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = grd_rdp_dsp_dispose; +} diff --git a/grd-rdp-dsp.h b/grd-rdp-dsp.h new file mode 100644 index 0000000..3449900 --- /dev/null +++ b/grd-rdp-dsp.h @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2022 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include +#include + +#define GRD_TYPE_RDP_DSP (grd_rdp_dsp_get_type ()) +G_DECLARE_FINAL_TYPE (GrdRdpDsp, grd_rdp_dsp, + GRD, RDP_DSP, GObject) + +typedef enum _GrdRdpDspCodec +{ + GRD_RDP_DSP_CODEC_NONE, + GRD_RDP_DSP_CODEC_AAC, + GRD_RDP_DSP_CODEC_ALAW, + GRD_RDP_DSP_CODEC_OPUS, +} GrdRdpDspCodec; + +typedef enum +{ + GRD_RDP_DSP_CREATE_FLAG_NONE = 0, + GRD_RDP_DSP_CREATE_FLAG_ENCODER = 1 << 0, + GRD_RDP_DSP_CREATE_FLAG_DECODER = 1 << 1, +} GrdRdpDspCreateFlag; + +typedef struct +{ + GrdRdpDspCreateFlag create_flags; + + /* Encoder */ + uint32_t n_samples_per_sec_aac; + uint32_t n_samples_per_sec_opus; + uint32_t n_channels; + uint32_t bitrate_aac; + uint32_t bitrate_opus; +} GrdRdpDspDescriptor; + +GrdRdpDsp *grd_rdp_dsp_new (const GrdRdpDspDescriptor *dsp_descriptor, + GError **error); + +const char *grd_rdp_dsp_codec_to_string (GrdRdpDspCodec dsp_codec); + +uint32_t grd_rdp_dsp_get_frames_per_packet (GrdRdpDsp *rdp_dsp, + GrdRdpDspCodec codec); + +gboolean grd_rdp_dsp_encode (GrdRdpDsp *rdp_dsp, + GrdRdpDspCodec codec, + int16_t *input_data, + uint32_t input_size, + uint32_t input_elem_size, + uint8_t **output_data, + uint32_t *output_size); + +gboolean grd_rdp_dsp_decode (GrdRdpDsp *rdp_dsp, + GrdRdpDspCodec codec, + uint8_t *input_data, + uint32_t input_size, + int16_t **output_data, + uint32_t *output_size); diff --git a/grd-rdp-dvc-audio-input.c b/grd-rdp-dvc-audio-input.c new file mode 100644 index 0000000..46e852d --- /dev/null +++ b/grd-rdp-dvc-audio-input.c @@ -0,0 +1,1096 @@ +/* + * Copyright (C) 2022 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-rdp-dvc-audio-input.h" + +#include +#include + +#include "grd-pipewire-utils.h" +#include "grd-rdp-dsp.h" + +#define PROTOCOL_TIMEOUT_MS (10 * 1000) + +#define MAX_LOCAL_FRAMES_LIFETIME_US (200 * 1000) + +#define N_CHANNELS 2 +#define N_SAMPLES_PER_SEC 44100 +#define N_BYTES_PER_SAMPLE_PCM sizeof (int16_t) +#define N_BYTES_PER_SAMPLE_ALAW sizeof (uint8_t) +#define N_BLOCK_ALIGN_PCM (N_CHANNELS * N_BYTES_PER_SAMPLE_PCM) +#define N_BLOCK_ALIGN_ALAW (N_CHANNELS * N_BYTES_PER_SAMPLE_ALAW) + +typedef enum +{ + NEGOTIATION_STATE_AWAIT_VERSION, + NEGOTIATION_STATE_AWAIT_INCOMING_DATA, + NEGOTIATION_STATE_AWAIT_FORMATS, + NEGOTIATION_STATE_AWAIT_FORMAT_CHANGE, + NEGOTIATION_STATE_AWAIT_OPEN_REPLY, + NEGOTIATION_STATE_COMPLETE, +} NegotiationState; + +typedef enum +{ + RT_STATE_AWAIT_INCOMING_DATA, + RT_STATE_AWAIT_DATA, +} RuntimeState; + +typedef struct +{ + int16_t *data; + uint32_t n_frames; + uint32_t sample_offset; + int64_t timestamp_us; +} AudioData; + +struct _GrdRdpDvcAudioInput +{ + GrdRdpDvc parent; + + audin_server_context *audin_context; + gboolean channel_opened; + + GMutex prevent_dvc_init_mutex; + gboolean prevent_dvc_initialization; + + GMutex protocol_timeout_mutex; + GSource *protocol_timeout_source; + + NegotiationState negotiation_state; + RuntimeState runtime_state; + + int64_t incoming_data_time_us; + + int64_t requested_format_idx; + int64_t alaw_client_format_idx; + int64_t pcm_client_format_idx; + GrdRdpDspCodec codec; + + GrdRdpDsp *rdp_dsp; + + GSource *pipewire_source; + + struct pw_context *pipewire_context; + struct pw_core *pipewire_core; + struct spa_hook pipewire_core_listener; + + struct pw_stream *pipewire_stream; + struct spa_hook pipewire_stream_listener; + + GMutex pending_frames_mutex; + GQueue *pending_frames; +}; + +G_DEFINE_TYPE (GrdRdpDvcAudioInput, grd_rdp_dvc_audio_input, + GRD_TYPE_RDP_DVC) + +static void +audio_data_free (gpointer data); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (AudioData, audio_data_free) + +static void +audio_data_free (gpointer data) +{ + AudioData *audio_data = data; + + g_free (audio_data->data); + g_free (audio_data); +} + +static gboolean +initiate_channel_teardown (gpointer user_data) +{ + GrdRdpDvcAudioInput *audio_input = user_data; + + g_warning ("[RDP.AUDIO_INPUT] Client did not respond to protocol initiation. " + "Terminating protocol"); + + g_mutex_lock (&audio_input->protocol_timeout_mutex); + g_clear_pointer (&audio_input->protocol_timeout_source, g_source_unref); + g_mutex_unlock (&audio_input->protocol_timeout_mutex); + + grd_rdp_dvc_queue_channel_tear_down (GRD_RDP_DVC (audio_input)); + + return G_SOURCE_REMOVE; +} + +static void +grd_rdp_dvc_audio_input_maybe_init (GrdRdpDvc *dvc) +{ + GrdRdpDvcAudioInput *audio_input = GRD_RDP_DVC_AUDIO_INPUT (dvc); + audin_server_context *audin_context; + g_autoptr (GMutexLocker) locker = NULL; + + if (audio_input->channel_opened) + return; + + locker = g_mutex_locker_new (&audio_input->prevent_dvc_init_mutex); + if (audio_input->prevent_dvc_initialization) + return; + + g_debug ("[RDP.AUDIO_INPUT] Trying to open DVC"); + + audin_context = audio_input->audin_context; + if (!audin_context->Open (audin_context)) + { + g_warning ("[RDP.AUDIO_INPUT] Failed to open channel. " + "Terminating protocol"); + grd_rdp_dvc_queue_channel_tear_down (GRD_RDP_DVC (audio_input)); + return; + } + audio_input->channel_opened = TRUE; + + g_assert (!audio_input->protocol_timeout_source); + + audio_input->protocol_timeout_source = + g_timeout_source_new (PROTOCOL_TIMEOUT_MS); + g_source_set_callback (audio_input->protocol_timeout_source, + initiate_channel_teardown, audio_input, NULL); + g_source_attach (audio_input->protocol_timeout_source, NULL); +} + +static void +dvc_creation_status (gpointer user_data, + int32_t creation_status) +{ + GrdRdpDvcAudioInput *audio_input = user_data; + + if (creation_status < 0) + { + g_warning ("[RDP.AUDIO_INPUT] Failed to open channel " + "(CreationStatus %i). Terminating protocol", creation_status); + grd_rdp_dvc_queue_channel_tear_down (GRD_RDP_DVC (audio_input)); + } +} + +static BOOL +audin_channel_id_assigned (audin_server_context *audin_context, + uint32_t channel_id) +{ + GrdRdpDvcAudioInput *audio_input = audin_context->userdata; + GrdRdpDvc *dvc = GRD_RDP_DVC (audio_input); + + g_debug ("[RDP.AUDIO_INPUT] DVC channel id assigned to id %u", channel_id); + + grd_rdp_dvc_subscribe_creation_status (dvc, channel_id, + dvc_creation_status, + audio_input); + + return TRUE; +} + +static const char * +negotiation_state_to_string (NegotiationState state) +{ + switch (state) + { + case NEGOTIATION_STATE_AWAIT_VERSION: + return "AWAIT_VERSION"; + case NEGOTIATION_STATE_AWAIT_INCOMING_DATA: + return "AWAIT_INCOMING_DATA"; + case NEGOTIATION_STATE_AWAIT_FORMATS: + return "AWAIT_FORMATS"; + case NEGOTIATION_STATE_AWAIT_FORMAT_CHANGE: + return "AWAIT_FORMAT_CHANGE"; + case NEGOTIATION_STATE_AWAIT_OPEN_REPLY: + return "AWAIT_OPEN_REPLY"; + case NEGOTIATION_STATE_COMPLETE: + return "COMPLETE"; + } + + g_assert_not_reached (); +} + +static const AUDIO_FORMAT audio_format_alaw = +{ + .wFormatTag = WAVE_FORMAT_ALAW, + .nChannels = N_CHANNELS, + .nSamplesPerSec = N_SAMPLES_PER_SEC, + .nAvgBytesPerSec = N_SAMPLES_PER_SEC * N_BLOCK_ALIGN_ALAW, + .nBlockAlign = N_BLOCK_ALIGN_ALAW, + .wBitsPerSample = N_BYTES_PER_SAMPLE_ALAW * 8, + .cbSize = 0, +}; + +static const AUDIO_FORMAT audio_format_pcm = +{ + .wFormatTag = WAVE_FORMAT_PCM, + .nChannels = N_CHANNELS, + .nSamplesPerSec = N_SAMPLES_PER_SEC, + .nAvgBytesPerSec = N_SAMPLES_PER_SEC * N_BLOCK_ALIGN_PCM, + .nBlockAlign = N_BLOCK_ALIGN_PCM, + .wBitsPerSample = N_BYTES_PER_SAMPLE_PCM * 8, + .cbSize = 0, +}; + +static AUDIO_FORMAT server_formats[] = +{ + audio_format_alaw, + audio_format_pcm, +}; + +static uint32_t +audin_receive_version (audin_server_context *audin_context, + const SNDIN_VERSION *version) +{ + GrdRdpDvcAudioInput *audio_input = audin_context->userdata; + SNDIN_FORMATS formats = {}; + + if (audio_input->negotiation_state != NEGOTIATION_STATE_AWAIT_VERSION) + { + g_warning ("[RDP.AUDIO_INPUT] Protocol violation: Received stray Version " + "PDU (Negotiation state: %s). Terminating protocol", + negotiation_state_to_string (audio_input->negotiation_state)); + grd_rdp_dvc_queue_channel_tear_down (GRD_RDP_DVC (audio_input)); + + return CHANNEL_RC_INITIALIZATION_ERROR; + } + + if (version->Version < SNDIN_VERSION_Version_1) + { + g_warning ("[RDP.AUDIO_INPUT] Protocol violation: Received invalid " + "Version PDU. Terminating protocol"); + grd_rdp_dvc_queue_channel_tear_down (GRD_RDP_DVC (audio_input)); + + return CHANNEL_RC_INITIALIZATION_ERROR; + } + + g_debug ("[RDP.AUDIO_INPUT] Client supports version %u", version->Version); + + audio_input->negotiation_state = NEGOTIATION_STATE_AWAIT_INCOMING_DATA; + + formats.NumFormats = G_N_ELEMENTS (server_formats); + formats.SoundFormats = server_formats; + + return audin_context->SendFormats (audin_context, &formats); +} + +static gboolean +is_audio_format_data_equal (const AUDIO_FORMAT *audio_format_1, + const AUDIO_FORMAT *audio_format_2) +{ + uint32_t i; + + if (audio_format_1->cbSize != audio_format_2->cbSize) + return FALSE; + + for (i = 0; i < audio_format_1->cbSize; ++i) + { + if (audio_format_1->data[i] != audio_format_2->data[i]) + return FALSE; + } + + return TRUE; +} + +static gboolean +are_audio_formats_equal (const AUDIO_FORMAT *audio_format_1, + const AUDIO_FORMAT *audio_format_2) +{ + if (audio_format_1->wFormatTag == audio_format_2->wFormatTag && + audio_format_1->nChannels == audio_format_2->nChannels && + audio_format_1->nSamplesPerSec == audio_format_2->nSamplesPerSec && + audio_format_1->nAvgBytesPerSec == audio_format_2->nAvgBytesPerSec && + audio_format_1->nBlockAlign == audio_format_2->nBlockAlign && + audio_format_1->wBitsPerSample == audio_format_2->wBitsPerSample && + is_audio_format_data_equal (audio_format_1, audio_format_2)) + return TRUE; + + return FALSE; +} + +/* KSDATAFORMAT_SUBTYPE_PCM ({00000001-0000-0010-8000-00aa00389b71}) */ +static GUID KSDATAFORMAT_SUBTYPE_PCM = +{ + .Data1 = 0x00000001, + .Data2 = 0x0000, + .Data3 = 0x0010, + .Data4[0] = 0x80, + .Data4[1] = 0x00, + .Data4[2] = 0x00, + .Data4[3] = 0xaa, + .Data4[4] = 0x00, + .Data4[5] = 0x38, + .Data4[6] = 0x9b, + .Data4[7] = 0x71, +}; + +static uint32_t +audin_receive_formats (audin_server_context *audin_context, + const SNDIN_FORMATS *formats) +{ + GrdRdpDvcAudioInput *audio_input = audin_context->userdata; + int64_t current_time_us; + int64_t time_delta_us; + size_t byte_count; + SNDIN_OPEN open = {}; + WAVEFORMAT_EXTENSIBLE waveformat_extensible = {}; + uint32_t i; + + current_time_us = g_get_monotonic_time (); + + if (audio_input->negotiation_state != NEGOTIATION_STATE_AWAIT_FORMATS) + { + g_warning ("[RDP.AUDIO_INPUT] Protocol violation: Received stray Sound " + "Formats PDU (Negotiation state: %s). Terminating protocol", + negotiation_state_to_string (audio_input->negotiation_state)); + grd_rdp_dvc_queue_channel_tear_down (GRD_RDP_DVC (audio_input)); + + return CHANNEL_RC_INITIALIZATION_ERROR; + } + + for (i = 0; i < formats->NumFormats; ++i) + { + AUDIO_FORMAT *audio_format = &formats->SoundFormats[i]; + + if (audio_input->alaw_client_format_idx < 0 && + are_audio_formats_equal (audio_format, &audio_format_alaw)) + audio_input->alaw_client_format_idx = i; + if (audio_input->pcm_client_format_idx < 0 && + are_audio_formats_equal (audio_format, &audio_format_pcm)) + audio_input->pcm_client_format_idx = i; + } + + if (audio_input->alaw_client_format_idx < 0 && + audio_input->pcm_client_format_idx < 0) + { + g_warning ("[RDP.AUDIO_INPUT] Audio Format negotiation with client " + "failed. Terminating protocol"); + grd_rdp_dvc_queue_channel_tear_down (GRD_RDP_DVC (audio_input)); + + return CHANNEL_RC_INITIALIZATION_ERROR; + } + + g_debug ("[RDP.AUDIO_INPUT] Client Formats: [A-law: %s, PCM: %s]", + audio_input->alaw_client_format_idx >= 0 ? "true" : "false", + audio_input->pcm_client_format_idx >= 0 ? "true" : "false"); + + if (audio_input->alaw_client_format_idx >= 0) + audio_input->requested_format_idx = audio_input->alaw_client_format_idx; + else if (audio_input->pcm_client_format_idx >= 0) + audio_input->requested_format_idx = audio_input->pcm_client_format_idx; + else + g_assert_not_reached (); + + time_delta_us = current_time_us - audio_input->incoming_data_time_us; + byte_count = formats->cbSizeFormatsPacket + formats->ExtraDataSize; + g_debug ("[RDP.AUDIO_INPUT] Measured clients uplink: >= %zuKB/s " + "(cbSizeFormatsPacket: %u, ExtraDataSize, %zu)", + byte_count * 1000 / MAX (time_delta_us, 1), + formats->cbSizeFormatsPacket, formats->ExtraDataSize); + + audio_input->negotiation_state = NEGOTIATION_STATE_AWAIT_FORMAT_CHANGE; + + waveformat_extensible.Samples.wValidBitsPerSample = N_BYTES_PER_SAMPLE_PCM * 8; + waveformat_extensible.dwChannelMask = SPEAKER_FRONT_LEFT | + SPEAKER_FRONT_RIGHT; + waveformat_extensible.SubFormat = KSDATAFORMAT_SUBTYPE_PCM; + + open.FramesPerPacket = N_SAMPLES_PER_SEC / 100; + open.initialFormat = audio_input->requested_format_idx; + open.captureFormat.wFormatTag = WAVE_FORMAT_EXTENSIBLE; + open.captureFormat.nChannels = N_CHANNELS; + open.captureFormat.nSamplesPerSec = N_SAMPLES_PER_SEC; + open.captureFormat.nAvgBytesPerSec = N_SAMPLES_PER_SEC * N_BLOCK_ALIGN_PCM; + open.captureFormat.nBlockAlign = N_BLOCK_ALIGN_PCM; + open.captureFormat.wBitsPerSample = N_BYTES_PER_SAMPLE_PCM * 8; + open.ExtraFormatData = &waveformat_extensible; + + return audin_context->SendOpen (audin_context, &open); +} + +static uint32_t +audin_open_reply (audin_server_context *audin_context, + const SNDIN_OPEN_REPLY *open_reply) +{ + GrdRdpDvcAudioInput *audio_input = audin_context->userdata; + int32_t signed_result = open_reply->Result; + + if (audio_input->negotiation_state != NEGOTIATION_STATE_AWAIT_OPEN_REPLY) + { + g_warning ("[RDP.AUDIO_INPUT] Protocol violation: Received stray Open " + "Reply PDU (Negotiation state: %s). Terminating protocol", + negotiation_state_to_string (audio_input->negotiation_state)); + grd_rdp_dvc_queue_channel_tear_down (GRD_RDP_DVC (audio_input)); + + return CHANNEL_RC_INITIALIZATION_ERROR; + } + + g_mutex_lock (&audio_input->protocol_timeout_mutex); + if (audio_input->protocol_timeout_source) + { + g_source_destroy (audio_input->protocol_timeout_source); + g_clear_pointer (&audio_input->protocol_timeout_source, g_source_unref); + } + g_mutex_unlock (&audio_input->protocol_timeout_mutex); + + if (signed_result < 0) + { + g_warning ("[RDP.AUDIO_INPUT] Failed to open audio capture device. " + "Terminating protocol"); + grd_rdp_dvc_queue_channel_tear_down (GRD_RDP_DVC (audio_input)); + + return CHANNEL_RC_INITIALIZATION_ERROR; + } + + g_debug ("[RDP.AUDIO_INPUT] Received Open Reply PDU, HRESULT: %i", + signed_result); + + audio_input->negotiation_state = NEGOTIATION_STATE_COMPLETE; + audio_input->runtime_state = RT_STATE_AWAIT_INCOMING_DATA; + + return CHANNEL_RC_OK; +} + +static uint32_t +audin_incoming_data (audin_server_context *audin_context, + const SNDIN_DATA_INCOMING *data_incoming) +{ + GrdRdpDvcAudioInput *audio_input = audin_context->userdata; + + audio_input->incoming_data_time_us = g_get_monotonic_time (); + + if (audio_input->negotiation_state < NEGOTIATION_STATE_COMPLETE && + audio_input->negotiation_state != NEGOTIATION_STATE_AWAIT_INCOMING_DATA) + { + g_warning ("[RDP.AUDIO_INPUT] Protocol violation: Received stray " + "Incoming Data PDU (Negotiation state: %s)", + negotiation_state_to_string (audio_input->negotiation_state)); + return CHANNEL_RC_OK; + } + + if (audio_input->negotiation_state == NEGOTIATION_STATE_COMPLETE && + audio_input->runtime_state != RT_STATE_AWAIT_INCOMING_DATA) + { + g_warning ("[RDP.AUDIO_INPUT] Protocol violation: Received duplicated " + "Incoming Data PDU"); + } + + if (audio_input->negotiation_state == NEGOTIATION_STATE_AWAIT_INCOMING_DATA) + audio_input->negotiation_state = NEGOTIATION_STATE_AWAIT_FORMATS; + else if (audio_input->negotiation_state == NEGOTIATION_STATE_COMPLETE) + audio_input->runtime_state = RT_STATE_AWAIT_DATA; + else + g_assert_not_reached (); + + return CHANNEL_RC_OK; +} + +static const char * +runtime_state_to_string (RuntimeState state) +{ + switch (state) + { + case RT_STATE_AWAIT_INCOMING_DATA: + return "AWAIT_INCOMING_DATA"; + case RT_STATE_AWAIT_DATA: + return "AWAIT_DATA"; + } + + g_assert_not_reached (); +} + +static uint32_t +audin_data (audin_server_context *audin_context, + const SNDIN_DATA *data) +{ + GrdRdpDvcAudioInput *audio_input = audin_context->userdata; + g_autoptr (AudioData) audio_data = NULL; + gboolean success = FALSE; + uint32_t dst_size = 0; + uint8_t *src_data; + uint32_t src_size; + + if (audio_input->negotiation_state < NEGOTIATION_STATE_COMPLETE || + audio_input->runtime_state != RT_STATE_AWAIT_DATA) + { + g_warning ("[RDP.AUDIO_INPUT] Protocol violation: Received stray Data " + "PDU (Negotiation state: %s, Runtime state: %s). " + "Terminating protocol", + negotiation_state_to_string (audio_input->negotiation_state), + runtime_state_to_string (audio_input->runtime_state)); + grd_rdp_dvc_queue_channel_tear_down (GRD_RDP_DVC (audio_input)); + + return CHANNEL_RC_UNSUPPORTED_VERSION; + } + + audio_input->runtime_state = RT_STATE_AWAIT_INCOMING_DATA; + + src_data = Stream_Pointer (data->Data); + src_size = Stream_Length (data->Data); + + if (!src_data || src_size == 0) + return CHANNEL_RC_OK; + + audio_data = g_new0 (AudioData, 1); + audio_data->timestamp_us = g_get_monotonic_time (); + + switch (audio_input->codec) + { + case GRD_RDP_DSP_CODEC_NONE: + audio_data->n_frames = src_size / N_BLOCK_ALIGN_PCM; + audio_data->data = g_memdup2 (src_data, audio_data->n_frames * + N_BLOCK_ALIGN_PCM); + success = TRUE; + break; + case GRD_RDP_DSP_CODEC_AAC: + case GRD_RDP_DSP_CODEC_ALAW: + case GRD_RDP_DSP_CODEC_OPUS: + success = grd_rdp_dsp_decode (audio_input->rdp_dsp, audio_input->codec, + src_data, src_size, + &audio_data->data, &dst_size); + audio_data->n_frames = dst_size / N_BLOCK_ALIGN_PCM; + break; + } + if (!success || audio_data->n_frames == 0) + return CHANNEL_RC_OK; + + g_mutex_lock (&audio_input->pending_frames_mutex); + g_queue_push_tail (audio_input->pending_frames, g_steal_pointer (&audio_data)); + g_mutex_unlock (&audio_input->pending_frames_mutex); + + return CHANNEL_RC_OK; +} + +static uint32_t +audin_receive_format_change (audin_server_context *audin_context, + const SNDIN_FORMATCHANGE *format_change) +{ + GrdRdpDvcAudioInput *audio_input = audin_context->userdata; + + if (audio_input->negotiation_state < NEGOTIATION_STATE_COMPLETE && + audio_input->negotiation_state != NEGOTIATION_STATE_AWAIT_FORMAT_CHANGE) + { + g_warning ("[RDP.AUDIO_INPUT] Protocol violation: Received stray Format " + "Change PDU (Negotiation state: %s). Terminating protocol", + negotiation_state_to_string (audio_input->negotiation_state)); + grd_rdp_dvc_queue_channel_tear_down (GRD_RDP_DVC (audio_input)); + + return CHANNEL_RC_INITIALIZATION_ERROR; + } + + if (audio_input->negotiation_state == NEGOTIATION_STATE_COMPLETE) + { + g_warning ("[RDP.AUDIO_INPUT] Protocol violation: Received stray Format " + "Change PDU (Runtime state: %s). Terminating protocol", + runtime_state_to_string (audio_input->runtime_state)); + grd_rdp_dvc_queue_channel_tear_down (GRD_RDP_DVC (audio_input)); + + return CHANNEL_RC_UNSUPPORTED_VERSION; + } + + if (format_change->NewFormat != audio_input->requested_format_idx) + { + g_warning ("[RDP.AUDIO_INPUT] Protocol violation: Received Format Change " + "PDU with invalid new format (%u), expected %li. " + "Terminating protocol", + format_change->NewFormat, audio_input->requested_format_idx); + grd_rdp_dvc_queue_channel_tear_down (GRD_RDP_DVC (audio_input)); + + return CHANNEL_RC_INITIALIZATION_ERROR; + } + + if (audio_input->requested_format_idx == audio_input->alaw_client_format_idx) + audio_input->codec = GRD_RDP_DSP_CODEC_ALAW; + else if (audio_input->requested_format_idx == audio_input->pcm_client_format_idx) + audio_input->codec = GRD_RDP_DSP_CODEC_NONE; + else + g_assert_not_reached (); + + g_debug ("[RDP.AUDIO_INPUT] Format Change: New Format: %u, Codec: %s", + format_change->NewFormat, + grd_rdp_dsp_codec_to_string (audio_input->codec)); + + if (audio_input->negotiation_state < NEGOTIATION_STATE_COMPLETE) + audio_input->negotiation_state = NEGOTIATION_STATE_AWAIT_OPEN_REPLY; + else + audio_input->runtime_state = RT_STATE_AWAIT_INCOMING_DATA; + + return CHANNEL_RC_OK; +} + +static void +pipewire_core_error (void *user_data, + uint32_t id, + int seq, + int res, + const char *message) +{ + GrdRdpDvcAudioInput *audio_input = user_data; + + g_warning ("[RDP.AUDIO_INPUT] PipeWire core error: " + "id: %u, seq: %i, res: %i, %s", id, seq, res, message); + + if (id == PW_ID_CORE && res == -EPIPE) + grd_rdp_dvc_queue_channel_tear_down (GRD_RDP_DVC (audio_input)); +} + +static const struct pw_core_events pipewire_core_events = +{ + .version = PW_VERSION_CORE_EVENTS, + .error = pipewire_core_error, +}; + +static gboolean +set_up_pipewire (GrdRdpDvcAudioInput *audio_input, + GError **error) +{ + GrdPipeWireSource *pipewire_source; + + pipewire_source = grd_attached_pipewire_source_new ("RDP.AUDIO_INPUT", error); + if (!pipewire_source) + return FALSE; + + audio_input->pipewire_source = (GSource *) pipewire_source; + + audio_input->pipewire_context = + pw_context_new (pipewire_source->pipewire_loop, NULL, 0); + if (!audio_input->pipewire_context) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create PipeWire context"); + return FALSE; + } + + audio_input->pipewire_core = + pw_context_connect (audio_input->pipewire_context, NULL, 0); + if (!audio_input->pipewire_core) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create PipeWire core"); + return FALSE; + } + + pw_core_add_listener (audio_input->pipewire_core, + &audio_input->pipewire_core_listener, + &pipewire_core_events, audio_input); + + return TRUE; +} + +static void +ensure_dvc_is_closed (GrdRdpDvcAudioInput *audio_input) +{ + GrdRdpDvc *dvc = GRD_RDP_DVC (audio_input); + + g_mutex_lock (&audio_input->prevent_dvc_init_mutex); + audio_input->prevent_dvc_initialization = TRUE; + g_mutex_unlock (&audio_input->prevent_dvc_init_mutex); + + if (audio_input->channel_opened) + { + g_debug ("[RDP.AUDIO_INPUT] Closing DVC"); + + audio_input->audin_context->Close (audio_input->audin_context); + audio_input->channel_opened = FALSE; + } + grd_rdp_dvc_maybe_unsubscribe_creation_status (dvc); + + if (audio_input->protocol_timeout_source) + { + g_source_destroy (audio_input->protocol_timeout_source); + g_clear_pointer (&audio_input->protocol_timeout_source, g_source_unref); + } + + g_mutex_lock (&audio_input->pending_frames_mutex); + g_queue_clear_full (audio_input->pending_frames, audio_data_free); + g_mutex_unlock (&audio_input->pending_frames_mutex); + + audio_input->negotiation_state = NEGOTIATION_STATE_AWAIT_VERSION; + audio_input->runtime_state = RT_STATE_AWAIT_INCOMING_DATA; + + audio_input->requested_format_idx = -1; + audio_input->alaw_client_format_idx = -1; + audio_input->pcm_client_format_idx = -1; + audio_input->codec = GRD_RDP_DSP_CODEC_NONE; +} + +static void +ensure_dvc_can_be_opened (GrdRdpDvcAudioInput *audio_input) +{ + g_assert (!audio_input->protocol_timeout_source); + + /* No need to lock prevent_dvc_init_mutex */ + audio_input->prevent_dvc_initialization = FALSE; +} + +static void +pipewire_stream_state_changed (void *user_data, + enum pw_stream_state old, + enum pw_stream_state state, + const char *error) +{ + GrdRdpDvcAudioInput *audio_input = user_data; + + g_debug ("[RDP.AUDIO_INPUT] PipeWire stream state changed from %s to %s", + pw_stream_state_as_string (old), + pw_stream_state_as_string (state)); + + switch (state) + { + case PW_STREAM_STATE_ERROR: + g_warning ("[RDP.AUDIO_INPUT] PipeWire stream error: %s", error); + grd_rdp_dvc_queue_channel_tear_down (GRD_RDP_DVC (audio_input)); + break; + case PW_STREAM_STATE_PAUSED: + ensure_dvc_is_closed (audio_input); + break; + case PW_STREAM_STATE_STREAMING: + ensure_dvc_can_be_opened (audio_input); + break; + case PW_STREAM_STATE_UNCONNECTED: + case PW_STREAM_STATE_CONNECTING: + break; + } +} + +static void +pipewire_stream_param_changed (void *user_data, + uint32_t id, + const struct spa_pod *param) +{ + GrdRdpDvcAudioInput *audio_input = user_data; + struct spa_pod_builder pod_builder; + const struct spa_pod *params[1]; + uint8_t params_buffer[1024]; + + if (!param || id != SPA_PARAM_Format) + return; + + pod_builder = SPA_POD_BUILDER_INIT (params_buffer, sizeof (params_buffer)); + + params[0] = spa_pod_builder_add_object ( + &pod_builder, + SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers, + SPA_PARAM_BUFFERS_dataType, SPA_POD_Int (1 << SPA_DATA_MemPtr)); + + pw_stream_update_params (audio_input->pipewire_stream, params, 1); +} + +static void +clear_old_frames (GrdRdpDvcAudioInput *audio_input) +{ + int64_t current_time_us; + AudioData *audio_data; + + current_time_us = g_get_monotonic_time (); + + audio_data = g_queue_peek_head (audio_input->pending_frames); + if (!audio_data) + return; + + if (current_time_us - audio_data->timestamp_us <= MAX_LOCAL_FRAMES_LIFETIME_US) + return; + + audio_data = g_queue_pop_tail (audio_input->pending_frames); + g_assert (audio_data); + + g_queue_clear_full (audio_input->pending_frames, audio_data_free); + g_queue_push_tail (audio_input->pending_frames, audio_data); +} + +static void +pipewire_stream_process (void *user_data) +{ + GrdRdpDvcAudioInput *audio_input = user_data; + g_autoptr (GMutexLocker) locker = NULL; + struct pw_buffer *buffer = NULL; + uint32_t n_frames; + uint8_t *data; + uint32_t *size; + + locker = g_mutex_locker_new (&audio_input->pending_frames_mutex); + clear_old_frames (audio_input); + + if (g_queue_get_length (audio_input->pending_frames) == 0) + return; + + buffer = pw_stream_dequeue_buffer (audio_input->pipewire_stream); + if (!buffer) + { + g_warning ("[RDP.AUDIO_INPUT] Failed to dequeue PipeWire buffer"); + return; + } + + n_frames = buffer->buffer->datas[0].maxsize / N_BLOCK_ALIGN_PCM; + if (buffer->requested > 0 && buffer->requested < n_frames) + n_frames = buffer->requested; + + buffer->buffer->datas[0].chunk->offset = 0; + buffer->buffer->datas[0].chunk->stride = N_BLOCK_ALIGN_PCM; + buffer->buffer->datas[0].chunk->size = 0; + + data = buffer->buffer->datas[0].data; + g_assert (data); + + size = &buffer->buffer->datas[0].chunk->size; + while (*size < n_frames * N_BLOCK_ALIGN_PCM) + { + AudioData *audio_data; + uint32_t frames_taken; + + audio_data = g_queue_pop_head (audio_input->pending_frames); + if (!audio_data) + break; + + frames_taken = MIN (n_frames - *size / N_BLOCK_ALIGN_PCM, + audio_data->n_frames); + memcpy (data + *size, audio_data->data + audio_data->sample_offset, + frames_taken * N_BLOCK_ALIGN_PCM); + *size += frames_taken * N_BLOCK_ALIGN_PCM; + + audio_data->n_frames -= frames_taken; + audio_data->sample_offset += frames_taken * N_CHANNELS; + if (audio_data->n_frames == 0) + audio_data_free (audio_data); + else + g_queue_push_head (audio_input->pending_frames, audio_data); + } + g_clear_pointer (&locker, g_mutex_locker_free); + + pw_stream_queue_buffer (audio_input->pipewire_stream, buffer); +} + +static const struct pw_stream_events pipewire_stream_events = +{ + .version = PW_VERSION_STREAM_EVENTS, + .state_changed = pipewire_stream_state_changed, + .param_changed = pipewire_stream_param_changed, + .process = pipewire_stream_process, +}; + +static gboolean +set_up_audio_source (GrdRdpDvcAudioInput *audio_input, + GError **error) +{ + struct spa_pod_builder pod_builder; + const struct spa_pod *params[1]; + uint8_t params_buffer[1024]; + uint32_t position[N_CHANNELS] = {}; + int result; + + g_message ("[RDP.AUDIO_INPUT] Setting up Audio Source"); + + position[0] = SPA_AUDIO_CHANNEL_FL; + position[1] = SPA_AUDIO_CHANNEL_FR; + + pod_builder = SPA_POD_BUILDER_INIT (params_buffer, sizeof (params_buffer)); + params[0] = spa_pod_builder_add_object ( + &pod_builder, + SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, + SPA_FORMAT_mediaType, SPA_POD_Id (SPA_MEDIA_TYPE_audio), + SPA_FORMAT_mediaSubtype, SPA_POD_Id (SPA_MEDIA_SUBTYPE_raw), + SPA_FORMAT_AUDIO_format, SPA_POD_Id (SPA_AUDIO_FORMAT_S16), + SPA_FORMAT_AUDIO_rate, SPA_POD_Int (N_SAMPLES_PER_SEC), + SPA_FORMAT_AUDIO_channels, SPA_POD_Int (N_CHANNELS), + SPA_FORMAT_AUDIO_position, SPA_POD_Array (sizeof (uint32_t), SPA_TYPE_Id, + N_CHANNELS, position), + 0); + + audio_input->pipewire_stream = + pw_stream_new (audio_input->pipewire_core, + "GRD::RDP::AUDIO_INPUT", + pw_properties_new (PW_KEY_MEDIA_TYPE, "Audio", + PW_KEY_MEDIA_CATEGORY, "Playback", + PW_KEY_MEDIA_CLASS, "Audio/Source", + PW_KEY_NODE_FORCE_QUANTUM, "256", + PW_KEY_NODE_SUSPEND_ON_IDLE, "true", + PW_KEY_PRIORITY_DRIVER, "24009", + PW_KEY_PRIORITY_SESSION, "24009", + PW_KEY_NODE_NAME, "grd_remote_audio_source", + PW_KEY_NODE_NICK, "GNOME Remote Desktop Audio Input", + PW_KEY_NODE_DESCRIPTION, "Remoteaudio Source", + NULL)); + if (!audio_input->pipewire_stream) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create PipeWire stream"); + return FALSE; + } + + pw_stream_add_listener (audio_input->pipewire_stream, + &audio_input->pipewire_stream_listener, + &pipewire_stream_events, audio_input); + + result = pw_stream_connect (audio_input->pipewire_stream, + PW_DIRECTION_OUTPUT, PW_ID_ANY, + PW_STREAM_FLAG_RT_PROCESS | + PW_STREAM_FLAG_AUTOCONNECT | + PW_STREAM_FLAG_MAP_BUFFERS, + params, 1); + if (result < 0) + { + g_set_error_literal (error, G_IO_ERROR, g_io_error_from_errno (-result), + strerror (-result)); + return FALSE; + } + + return TRUE; +} + +GrdRdpDvcAudioInput * +grd_rdp_dvc_audio_input_new (GrdSessionRdp *session_rdp, + GrdRdpDvcHandler *dvc_handler, + HANDLE vcm, + rdpContext *rdp_context) +{ + g_autoptr (GrdRdpDvcAudioInput) audio_input = NULL; + audin_server_context *audin_context; + GrdRdpDspDescriptor dsp_descriptor = {}; + g_autoptr (GError) error = NULL; + + audio_input = g_object_new (GRD_TYPE_RDP_DVC_AUDIO_INPUT, NULL); + audin_context = audin_server_context_new (vcm); + if (!audin_context) + g_error ("[RDP.AUDIO_INPUT] Failed to create server context (OOM)"); + + audio_input->audin_context = audin_context; + + grd_rdp_dvc_initialize_base (GRD_RDP_DVC (audio_input), + dvc_handler, session_rdp, + GRD_RDP_CHANNEL_AUDIO_INPUT); + + audin_context->serverVersion = SNDIN_VERSION_Version_2; + + audin_context->ChannelIdAssigned = audin_channel_id_assigned; + audin_context->ReceiveVersion = audin_receive_version; + audin_context->ReceiveFormats = audin_receive_formats; + audin_context->OpenReply = audin_open_reply; + audin_context->IncomingData = audin_incoming_data; + audin_context->Data = audin_data; + audin_context->ReceiveFormatChange = audin_receive_format_change; + audin_context->rdpcontext = rdp_context; + audin_context->userdata = audio_input; + + dsp_descriptor.create_flags = GRD_RDP_DSP_CREATE_FLAG_DECODER; + + audio_input->rdp_dsp = grd_rdp_dsp_new (&dsp_descriptor, &error); + if (!audio_input->rdp_dsp) + { + g_warning ("[RDP.AUDIO_INPUT] Failed to create DSP instance: %s", + error->message); + return NULL; + } + + if (!set_up_pipewire (audio_input, &error)) + { + g_warning ("[RDP.AUDIO_INPUT] Failed to set up PipeWire: %s", + error->message); + return NULL; + } + if (!set_up_audio_source (audio_input, &error)) + { + g_warning ("[RDP.AUDIO_INPUT] Failed to create audio source: %s", + error->message); + return NULL; + } + + return g_steal_pointer (&audio_input); +} + +static void +grd_rdp_dvc_audio_input_dispose (GObject *object) +{ + GrdRdpDvcAudioInput *audio_input = GRD_RDP_DVC_AUDIO_INPUT (object); + + ensure_dvc_is_closed (audio_input); + g_assert (!audio_input->protocol_timeout_source); + + if (audio_input->pipewire_stream) + { + spa_hook_remove (&audio_input->pipewire_stream_listener); + g_clear_pointer (&audio_input->pipewire_stream, pw_stream_destroy); + } + + if (audio_input->pipewire_core) + { + spa_hook_remove (&audio_input->pipewire_core_listener); + g_clear_pointer (&audio_input->pipewire_core, pw_core_disconnect); + } + g_clear_pointer (&audio_input->pipewire_context, pw_context_destroy); + + if (audio_input->pipewire_source) + { + g_source_destroy (audio_input->pipewire_source); + g_clear_pointer (&audio_input->pipewire_source, g_source_unref); + } + + g_clear_object (&audio_input->rdp_dsp); + + if (audio_input->pending_frames) + { + g_queue_free_full (audio_input->pending_frames, audio_data_free); + audio_input->pending_frames = NULL; + } + + g_clear_pointer (&audio_input->audin_context, audin_server_context_free); + + G_OBJECT_CLASS (grd_rdp_dvc_audio_input_parent_class)->dispose (object); +} + +static void +grd_rdp_dvc_audio_input_finalize (GObject *object) +{ + GrdRdpDvcAudioInput *audio_input = GRD_RDP_DVC_AUDIO_INPUT (object); + + pw_deinit (); + + g_mutex_clear (&audio_input->pending_frames_mutex); + g_mutex_clear (&audio_input->protocol_timeout_mutex); + g_mutex_clear (&audio_input->prevent_dvc_init_mutex); + + G_OBJECT_CLASS (grd_rdp_dvc_audio_input_parent_class)->finalize (object); +} + +static void +grd_rdp_dvc_audio_input_init (GrdRdpDvcAudioInput *audio_input) +{ + audio_input->prevent_dvc_initialization = TRUE; + audio_input->negotiation_state = NEGOTIATION_STATE_AWAIT_VERSION; + audio_input->runtime_state = RT_STATE_AWAIT_INCOMING_DATA; + + audio_input->requested_format_idx = -1; + audio_input->alaw_client_format_idx = -1; + audio_input->pcm_client_format_idx = -1; + audio_input->codec = GRD_RDP_DSP_CODEC_NONE; + + audio_input->pending_frames = g_queue_new (); + + g_mutex_init (&audio_input->prevent_dvc_init_mutex); + g_mutex_init (&audio_input->protocol_timeout_mutex); + g_mutex_init (&audio_input->pending_frames_mutex); + + pw_init (NULL, NULL); +} + +static void +grd_rdp_dvc_audio_input_class_init (GrdRdpDvcAudioInputClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GrdRdpDvcClass *dvc_class = GRD_RDP_DVC_CLASS (klass); + + object_class->dispose = grd_rdp_dvc_audio_input_dispose; + object_class->finalize = grd_rdp_dvc_audio_input_finalize; + + dvc_class->maybe_init = grd_rdp_dvc_audio_input_maybe_init; +} diff --git a/grd-rdp-dvc-audio-input.h b/grd-rdp-dvc-audio-input.h new file mode 100644 index 0000000..6815e13 --- /dev/null +++ b/grd-rdp-dvc-audio-input.h @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2022 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include "grd-rdp-dvc.h" + +#define GRD_TYPE_RDP_DVC_AUDIO_INPUT (grd_rdp_dvc_audio_input_get_type ()) +G_DECLARE_FINAL_TYPE (GrdRdpDvcAudioInput, grd_rdp_dvc_audio_input, + GRD, RDP_DVC_AUDIO_INPUT, GrdRdpDvc) + +GrdRdpDvcAudioInput *grd_rdp_dvc_audio_input_new (GrdSessionRdp *session_rdp, + GrdRdpDvcHandler *dvc_handler, + HANDLE vcm, + rdpContext *rdp_context); diff --git a/grd-rdp-dvc-audio-playback.c b/grd-rdp-dvc-audio-playback.c new file mode 100644 index 0000000..59410d6 --- /dev/null +++ b/grd-rdp-dvc-audio-playback.c @@ -0,0 +1,1442 @@ +/* + * Copyright (C) 2021 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-rdp-dvc-audio-playback.h" + +#include + +#include "grd-pipewire-utils.h" +#include "grd-rdp-audio-output-stream.h" +#include "grd-rdp-dsp.h" + +#define PROTOCOL_TIMEOUT_MS (10 * 1000) + +#define PCM_FRAMES_PER_PACKET 1024 +#define MAX_IDLING_TIME_US_DEFAULT (5 * G_USEC_PER_SEC) +#define MAX_IDLING_TIME_US_OPUS (10 * G_USEC_PER_SEC) +#define MAX_LOCAL_FRAMES_LIFETIME_US (50 * 1000) +#define MAX_RENDER_LATENCY_MS 300 + +#define N_CHANNELS 2 +#define N_SAMPLES_PER_SEC_DEFAULT 44100 +#define N_SAMPLES_PER_SEC_OPUS 48000 +#define N_BYTES_PER_SAMPLE_PCM sizeof (int16_t) +#define N_BLOCK_ALIGN_PCM (N_CHANNELS * N_BYTES_PER_SAMPLE_PCM) + +#define TRAINING_PACKSIZE 1024 +#define TRAINING_TIMESTAMP 0 + +typedef struct _AudioData +{ + int16_t *data; + uint32_t size; + int64_t timestamp_us; +} AudioData; + +typedef struct _BlockInfo +{ + uint16_t render_latency_ms; + int64_t render_latency_set_us; +} BlockInfo; + +struct _GrdRdpDvcAudioPlayback +{ + GrdRdpDvc parent; + + RdpsndServerContext *rdpsnd_context; + gboolean channel_opened; + + gboolean prevent_dvc_initialization; + GSource *svc_setup_source; + + GMutex protocol_timeout_mutex; + GSource *protocol_timeout_source; + + gboolean pending_client_formats; + gboolean pending_training_confirm; + gboolean protocol_stopped; + + int32_t aac_client_format_idx; + int32_t opus_client_format_idx; + int32_t pcm_client_format_idx; + int32_t client_format_idx; + uint32_t n_samples_per_sec; + GrdRdpDspCodec codec; + + GThread *encode_thread; + GMainContext *encode_context; + GSource *encode_source; + + GrdRdpDsp *rdp_dsp; + uint32_t sample_buffer_size; + + GMutex block_mutex; + BlockInfo block_infos[256]; + + GSource *pipewire_setup_source; + GSource *pipewire_source; + + struct pw_context *pipewire_context; + struct pw_core *pipewire_core; + struct spa_hook pipewire_core_listener; + + struct pw_registry *pipewire_registry; + struct spa_hook pipewire_registry_listener; + + GMutex streams_mutex; + GHashTable *audio_streams; + + GMutex stream_lock_mutex; + uint32_t locked_node_id; + gboolean has_stream_lock; + + int64_t first_empty_audio_data_us; + gboolean has_empty_audio_timestamp; + + GMutex pending_frames_mutex; + GQueue *pending_frames; +}; + +G_DEFINE_TYPE (GrdRdpDvcAudioPlayback, grd_rdp_dvc_audio_playback, + GRD_TYPE_RDP_DVC) + +static gboolean +initiate_channel_teardown (gpointer user_data) +{ + GrdRdpDvcAudioPlayback *audio_playback = user_data; + + g_warning ("[RDP.AUDIO_PLAYBACK] Client did not respond to protocol " + "initiation. Terminating protocol"); + + g_mutex_lock (&audio_playback->protocol_timeout_mutex); + g_clear_pointer (&audio_playback->protocol_timeout_source, g_source_unref); + g_mutex_unlock (&audio_playback->protocol_timeout_mutex); + + grd_rdp_dvc_queue_channel_tear_down (GRD_RDP_DVC (audio_playback)); + + return G_SOURCE_REMOVE; +} + +static void +grd_rdp_dvc_audio_playback_maybe_init (GrdRdpDvc *dvc) +{ + GrdRdpDvcAudioPlayback *audio_playback = GRD_RDP_DVC_AUDIO_PLAYBACK (dvc); + RdpsndServerContext *rdpsnd_context; + + if (audio_playback->channel_opened) + return; + + if (audio_playback->prevent_dvc_initialization) + return; + + rdpsnd_context = audio_playback->rdpsnd_context; + if (rdpsnd_context->Start (rdpsnd_context)) + { + g_warning ("[RDP.AUDIO_PLAYBACK] Failed to open AUDIO_PLAYBACK_DVC " + "channel. Terminating protocol"); + grd_rdp_dvc_queue_channel_tear_down (GRD_RDP_DVC (audio_playback)); + return; + } + audio_playback->channel_opened = TRUE; + + g_assert (!audio_playback->protocol_timeout_source); + + audio_playback->protocol_timeout_source = + g_timeout_source_new (PROTOCOL_TIMEOUT_MS); + g_source_set_callback (audio_playback->protocol_timeout_source, + initiate_channel_teardown, audio_playback, NULL); + g_source_attach (audio_playback->protocol_timeout_source, NULL); +} + +static void +prepare_volume_data (GrdRdpAudioVolumeData *volume_data) +{ + uint32_t i; + + g_assert (N_CHANNELS <= SPA_AUDIO_MAX_CHANNELS); + + if (volume_data->audio_muted) + { + for (i = 0; i < volume_data->n_volumes; ++i) + volume_data->volumes[i] = 0.0f; + } + else if (volume_data->n_volumes == 0) + { + for (i = 0; i < N_CHANNELS; ++i) + volume_data->volumes[i] = 0.0f; + } + else if (N_CHANNELS > volume_data->n_volumes) + { + g_assert (N_CHANNELS == 2); + g_assert (volume_data->n_volumes == 1); + + for (i = 1; i < N_CHANNELS; ++i) + volume_data->volumes[i] = volume_data->volumes[0]; + } +} + +static gboolean +does_volume_mute_audio (const GrdRdpAudioVolumeData *volume_data) +{ + uint32_t i; + + if (volume_data->audio_muted) + return TRUE; + + for (i = 0; i < N_CHANNELS; ++i) + { + if (!G_APPROX_VALUE (volume_data->volumes[i], 0.0f, FLT_EPSILON)) + return FALSE; + } + + return TRUE; +} + +static gboolean +is_audio_data_empty (int16_t *data, + uint32_t size) +{ + uint32_t i; + + for (i = 0; i < size / sizeof (int16_t); ++i) + { + if (data[i] != 0) + return FALSE; + } + + return TRUE; +} + +static void +set_other_streams_inactive (GrdRdpDvcAudioPlayback *audio_playback) +{ + GrdRdpAudioOutputStream *audio_output_stream; + gpointer key; + GHashTableIter iter; + + g_assert (audio_playback->has_stream_lock); + g_mutex_unlock (&audio_playback->stream_lock_mutex); + + g_mutex_lock (&audio_playback->streams_mutex); + g_hash_table_iter_init (&iter, audio_playback->audio_streams); + while (g_hash_table_iter_next (&iter, &key, (gpointer *) &audio_output_stream)) + { + uint32_t node_id; + + node_id = GPOINTER_TO_UINT (key); + if (audio_playback->locked_node_id != node_id) + grd_rdp_audio_output_stream_set_active (audio_output_stream, FALSE); + } + g_mutex_unlock (&audio_playback->streams_mutex); + + g_mutex_lock (&audio_playback->stream_lock_mutex); +} + +static void +acquire_stream_lock (GrdRdpDvcAudioPlayback *audio_playback, + uint32_t node_id) +{ + g_assert (!audio_playback->has_stream_lock); + + g_debug ("[RDP.AUDIO_PLAYBACK] Locking audio stream to node id %u", node_id); + + audio_playback->locked_node_id = node_id; + audio_playback->has_stream_lock = TRUE; + + set_other_streams_inactive (audio_playback); +} + +static void +audio_data_free (gpointer data) +{ + AudioData *audio_data = data; + + g_free (audio_data->data); + g_free (audio_data); +} + +static void +set_all_streams_active (GrdRdpDvcAudioPlayback *audio_playback) +{ + GrdRdpAudioOutputStream *audio_output_stream; + GHashTableIter iter; + + g_assert (!audio_playback->has_stream_lock); + + g_mutex_lock (&audio_playback->streams_mutex); + g_hash_table_iter_init (&iter, audio_playback->audio_streams); + while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &audio_output_stream)) + grd_rdp_audio_output_stream_set_active (audio_output_stream, TRUE); + g_mutex_unlock (&audio_playback->streams_mutex); +} + +static void +release_stream_lock (GrdRdpDvcAudioPlayback *audio_playback) +{ + uint16_t i; + + g_assert (audio_playback->has_stream_lock); + + g_debug ("[RDP.AUDIO_PLAYBACK] Unlocking audio stream (was locked to " + "node id %u)", audio_playback->locked_node_id); + + g_mutex_lock (&audio_playback->pending_frames_mutex); + g_queue_clear_full (audio_playback->pending_frames, audio_data_free); + g_mutex_unlock (&audio_playback->pending_frames_mutex); + + g_mutex_lock (&audio_playback->block_mutex); + for (i = 0; i < 256; ++i) + { + BlockInfo *block_info = &audio_playback->block_infos[i]; + + block_info->render_latency_ms = 0; + } + g_mutex_unlock (&audio_playback->block_mutex); + + audio_playback->has_stream_lock = FALSE; + + set_all_streams_active (audio_playback); +} + +static gboolean +is_locked_stream_idling_too_long (GrdRdpDvcAudioPlayback *audio_playback) +{ + int64_t first_empty_audio_data_us = audio_playback->first_empty_audio_data_us; + int64_t max_idling_time_us; + int64_t current_time_us; + + if (!audio_playback->has_empty_audio_timestamp) + return FALSE; + + if (audio_playback->codec == GRD_RDP_DSP_CODEC_OPUS) + max_idling_time_us = MAX_IDLING_TIME_US_OPUS; + else + max_idling_time_us = MAX_IDLING_TIME_US_DEFAULT; + + current_time_us = g_get_monotonic_time (); + if (current_time_us - first_empty_audio_data_us > max_idling_time_us) + return TRUE; + + return FALSE; +} + +static uint32_t +get_pending_audio_data_size (GrdRdpDvcAudioPlayback *audio_playback) +{ + AudioData *audio_data; + uint32_t pending_size = 0; + GQueue *tmp; + + tmp = g_queue_copy (audio_playback->pending_frames); + while ((audio_data = g_queue_pop_head (tmp))) + pending_size += audio_data->size; + + g_queue_free (tmp); + + return pending_size; +} + +void +grd_rdp_dvc_audio_playback_maybe_submit_samples (GrdRdpDvcAudioPlayback *audio_playback, + uint32_t node_id, + GrdRdpAudioVolumeData *volume_data, + int16_t *data, + uint32_t size) +{ + g_autoptr (GMutexLocker) locker = NULL; + gboolean audio_muted; + gboolean data_is_empty; + uint32_t pending_size = 0; + gboolean pending_encode = FALSE; + + g_assert (N_BLOCK_ALIGN_PCM > 0); + + prepare_volume_data (volume_data); + audio_muted = does_volume_mute_audio (volume_data); + + data_is_empty = audio_muted || is_audio_data_empty (data, size); + + locker = g_mutex_locker_new (&audio_playback->stream_lock_mutex); + if (audio_playback->has_stream_lock && + audio_playback->locked_node_id != node_id) + return; + + if (!audio_playback->has_stream_lock && data_is_empty) + return; + + if (!audio_playback->has_stream_lock) + acquire_stream_lock (audio_playback, node_id); + + if (size < N_BLOCK_ALIGN_PCM || + size % N_BLOCK_ALIGN_PCM != 0) + { + release_stream_lock (audio_playback); + return; + } + + if (data_is_empty && is_locked_stream_idling_too_long (audio_playback)) + { + release_stream_lock (audio_playback); + return; + } + else if (data_is_empty && !audio_playback->has_empty_audio_timestamp) + { + audio_playback->first_empty_audio_data_us = g_get_monotonic_time (); + audio_playback->has_empty_audio_timestamp = TRUE; + } + + if (!data_is_empty) + audio_playback->has_empty_audio_timestamp = FALSE; + + g_mutex_lock (&audio_playback->pending_frames_mutex); + pending_size = get_pending_audio_data_size (audio_playback); + + if (!data_is_empty || + (pending_size > 0 && pending_size < audio_playback->sample_buffer_size)) + { + AudioData *audio_data; + + audio_data = g_new0 (AudioData, 1); + audio_data->data = g_memdup2 (data, size); + audio_data->size = size; + audio_data->timestamp_us = g_get_monotonic_time (); + + g_queue_push_tail (audio_playback->pending_frames, audio_data); + + if (pending_size + size >= audio_playback->sample_buffer_size) + pending_encode = TRUE; + } + g_mutex_unlock (&audio_playback->pending_frames_mutex); + + if (pending_encode) + g_source_set_ready_time (audio_playback->encode_source, 0); +} + +static void +dvc_creation_status (gpointer user_data, + int32_t creation_status) +{ + GrdRdpDvcAudioPlayback *audio_playback = user_data; + + if (creation_status < 0) + { + g_message ("[RDP.AUDIO_PLAYBACK] Failed to open AUDIO_PLAYBACK_DVC " + "channel (CreationStatus %i). Trying SVC fallback", + creation_status); + g_source_set_ready_time (audio_playback->svc_setup_source, 0); + } +} + +static BOOL +rdpsnd_channel_id_assigned (RdpsndServerContext *rdpsnd_context, + uint32_t channel_id) +{ + GrdRdpDvcAudioPlayback *audio_playback = rdpsnd_context->data; + GrdRdpDvc *dvc = GRD_RDP_DVC (audio_playback); + + g_debug ("[RDP.AUDIO_PLAYBACK] DVC channel id assigned to id %u", channel_id); + + grd_rdp_dvc_subscribe_creation_status (dvc, channel_id, + dvc_creation_status, + audio_playback); + + return TRUE; +} + +static gboolean +is_audio_format_data_equal (const AUDIO_FORMAT *audio_format_1, + const AUDIO_FORMAT *audio_format_2) +{ + uint32_t i; + + if (audio_format_1->cbSize != audio_format_2->cbSize) + return FALSE; + + for (i = 0; i < audio_format_1->cbSize; ++i) + { + if (audio_format_1->data[i] != audio_format_2->data[i]) + return FALSE; + } + + return TRUE; +} + +static gboolean +are_audio_formats_equal (const AUDIO_FORMAT *audio_format_1, + const AUDIO_FORMAT *audio_format_2) +{ + if (audio_format_1->wFormatTag == audio_format_2->wFormatTag && + audio_format_1->nChannels == audio_format_2->nChannels && + audio_format_1->nSamplesPerSec == audio_format_2->nSamplesPerSec && + audio_format_1->nAvgBytesPerSec == audio_format_2->nAvgBytesPerSec && + audio_format_1->nBlockAlign == audio_format_2->nBlockAlign && + audio_format_1->wBitsPerSample == audio_format_2->wBitsPerSample && + is_audio_format_data_equal (audio_format_1, audio_format_2)) + return TRUE; + + return FALSE; +} + +static const AUDIO_FORMAT audio_format_aac = +{ + .wFormatTag = WAVE_FORMAT_AAC_MS, + .nChannels = N_CHANNELS, + .nSamplesPerSec = N_SAMPLES_PER_SEC_DEFAULT, + .nAvgBytesPerSec = 12000, + .nBlockAlign = 4, + .wBitsPerSample = 16, + .cbSize = 0, +}; + +static const AUDIO_FORMAT audio_format_opus = +{ + .wFormatTag = WAVE_FORMAT_OPUS, + .nChannels = N_CHANNELS, + .nSamplesPerSec = N_SAMPLES_PER_SEC_OPUS, + .nAvgBytesPerSec = 12000, + .nBlockAlign = 4, + .wBitsPerSample = 16, + .cbSize = 0, +}; + +static const AUDIO_FORMAT audio_format_pcm = +{ + .wFormatTag = WAVE_FORMAT_PCM, + .nChannels = N_CHANNELS, + .nSamplesPerSec = N_SAMPLES_PER_SEC_DEFAULT, + .nAvgBytesPerSec = N_SAMPLES_PER_SEC_DEFAULT * N_BLOCK_ALIGN_PCM, + .nBlockAlign = N_BLOCK_ALIGN_PCM, + .wBitsPerSample = N_BYTES_PER_SAMPLE_PCM * 8, + .cbSize = 0, +}; + +static AUDIO_FORMAT server_formats[] = +{ + audio_format_aac, + audio_format_opus, + audio_format_pcm, +}; + +static uint32_t +get_sample_rate (GrdRdpDvcAudioPlayback *audio_playback) +{ + switch (audio_playback->codec) + { + case GRD_RDP_DSP_CODEC_NONE: + case GRD_RDP_DSP_CODEC_AAC: + case GRD_RDP_DSP_CODEC_ALAW: + return N_SAMPLES_PER_SEC_DEFAULT; + case GRD_RDP_DSP_CODEC_OPUS: + return N_SAMPLES_PER_SEC_OPUS; + } + + g_assert_not_reached (); +} + +static void +prepare_client_format (GrdRdpDvcAudioPlayback *audio_playback) +{ + uint32_t frames_per_packet; + + if (audio_playback->aac_client_format_idx >= 0) + { + audio_playback->client_format_idx = audio_playback->aac_client_format_idx; + audio_playback->codec = GRD_RDP_DSP_CODEC_AAC; + } + else if (audio_playback->opus_client_format_idx >= 0) + { + audio_playback->client_format_idx = audio_playback->opus_client_format_idx; + audio_playback->codec = GRD_RDP_DSP_CODEC_OPUS; + } + else if (audio_playback->pcm_client_format_idx >= 0) + { + audio_playback->client_format_idx = audio_playback->pcm_client_format_idx; + audio_playback->codec = GRD_RDP_DSP_CODEC_NONE; + } + else + { + g_assert_not_reached (); + } + + audio_playback->n_samples_per_sec = get_sample_rate (audio_playback); + + switch (audio_playback->codec) + { + case GRD_RDP_DSP_CODEC_NONE: + frames_per_packet = PCM_FRAMES_PER_PACKET; + break; + case GRD_RDP_DSP_CODEC_AAC: + case GRD_RDP_DSP_CODEC_ALAW: + case GRD_RDP_DSP_CODEC_OPUS: + frames_per_packet = + grd_rdp_dsp_get_frames_per_packet (audio_playback->rdp_dsp, + audio_playback->codec); + break; + } + g_assert (frames_per_packet > 0); + + audio_playback->sample_buffer_size = N_BLOCK_ALIGN_PCM * frames_per_packet; + + g_debug ("[RDP.AUDIO_PLAYBACK] Using codec %s with sample buffer size %u", + grd_rdp_dsp_codec_to_string (audio_playback->codec), + audio_playback->sample_buffer_size); +} + +static void +rdpsnd_activated (RdpsndServerContext *rdpsnd_context) +{ + GrdRdpDvcAudioPlayback *audio_playback = rdpsnd_context->data; + uint8_t training_data[TRAINING_PACKSIZE] = {}; + uint16_t i; + + if (!audio_playback->pending_client_formats) + { + g_warning ("[RDP.AUDIO_PLAYBACK] Protocol violation: Received stray " + "Client Audio Formats or Quality Mode PDU. " + "Terminating protocol"); + grd_rdp_dvc_queue_channel_tear_down (GRD_RDP_DVC (audio_playback)); + return; + } + + if (rdpsnd_context->clientVersion < 8) + { + g_message ("[RDP.AUDIO_PLAYBACK] Client protocol version (%u) is too " + "old. Terminating protocol", rdpsnd_context->clientVersion); + grd_rdp_dvc_queue_channel_tear_down (GRD_RDP_DVC (audio_playback)); + return; + } + + for (i = 0; i < rdpsnd_context->num_client_formats; ++i) + { + AUDIO_FORMAT *audio_format = &rdpsnd_context->client_formats[i]; + + if (audio_playback->aac_client_format_idx < 0 && + are_audio_formats_equal (audio_format, &audio_format_aac)) + audio_playback->aac_client_format_idx = i; + if (audio_playback->opus_client_format_idx < 0 && + are_audio_formats_equal (audio_format, &audio_format_opus)) + audio_playback->opus_client_format_idx = i; + if (audio_playback->pcm_client_format_idx < 0 && + are_audio_formats_equal (audio_format, &audio_format_pcm)) + audio_playback->pcm_client_format_idx = i; + } + + if (audio_playback->aac_client_format_idx < 0 && + audio_playback->opus_client_format_idx < 0 && + audio_playback->pcm_client_format_idx < 0) + { + g_warning ("[RDP.AUDIO_PLAYBACK] Audio Format negotiation with client " + "failed. Terminating protocol"); + grd_rdp_dvc_queue_channel_tear_down (GRD_RDP_DVC (audio_playback)); + return; + } + audio_playback->pending_client_formats = FALSE; + + g_message ("[RDP.AUDIO_PLAYBACK] Client Formats: [AAC: %s, Opus: %s, PCM: %s]", + audio_playback->aac_client_format_idx >= 0 ? "true" : "false", + audio_playback->opus_client_format_idx >= 0 ? "true" : "false", + audio_playback->pcm_client_format_idx >= 0 ? "true" : "false"); + + prepare_client_format (audio_playback); + + rdpsnd_context->Training (rdpsnd_context, TRAINING_TIMESTAMP, + TRAINING_PACKSIZE, training_data); +} + +static uint32_t +rdpsnd_training_confirm (RdpsndServerContext *rdpsnd_context, + uint16_t timestamp, + uint16_t packsize) +{ + GrdRdpDvcAudioPlayback *audio_playback = rdpsnd_context->data; + + if (audio_playback->pending_client_formats || + !audio_playback->pending_training_confirm) + { + g_warning ("[RDP.AUDIO_PLAYBACK] Protocol violation: Received unexpected " + "Training Confirm PDU. Terminating protocol"); + grd_rdp_dvc_queue_channel_tear_down (GRD_RDP_DVC (audio_playback)); + return CHANNEL_RC_OK; + } + + if (timestamp != TRAINING_TIMESTAMP || packsize != TRAINING_PACKSIZE) + { + g_warning ("[RDP.AUDIO_PLAYBACK] Received invalid Training Confirm PDU. " + "Ignoring..."); + return CHANNEL_RC_OK; + } + + g_debug ("[RDP.AUDIO_PLAYBACK] Received Training Confirm PDU"); + + g_mutex_lock (&audio_playback->protocol_timeout_mutex); + if (audio_playback->protocol_timeout_source) + { + g_source_destroy (audio_playback->protocol_timeout_source); + g_clear_pointer (&audio_playback->protocol_timeout_source, g_source_unref); + } + g_mutex_unlock (&audio_playback->protocol_timeout_mutex); + + if (audio_playback->svc_setup_source) + { + g_source_destroy (audio_playback->svc_setup_source); + g_clear_pointer (&audio_playback->svc_setup_source, g_source_unref); + } + + audio_playback->pending_training_confirm = FALSE; + + g_source_set_ready_time (audio_playback->pipewire_setup_source, 0); + + return CHANNEL_RC_OK; +} + +static uint32_t +rdpsnd_confirm_block (RdpsndServerContext *rdpsnd_context, + uint8_t confirm_block_num, + uint16_t wtimestamp) +{ + GrdRdpDvcAudioPlayback *audio_playback = rdpsnd_context->data; + BlockInfo *block_info; + + g_mutex_lock (&audio_playback->block_mutex); + block_info = &audio_playback->block_infos[confirm_block_num]; + + if (wtimestamp > 0) + { + block_info->render_latency_ms = wtimestamp; + block_info->render_latency_set_us = g_get_monotonic_time (); + } + g_mutex_unlock (&audio_playback->block_mutex); + + return CHANNEL_RC_OK; +} + +static gpointer +encode_thread_func (gpointer data) +{ + GrdRdpDvcAudioPlayback *audio_playback = data; + + while (!audio_playback->protocol_stopped) + g_main_context_iteration (audio_playback->encode_context, TRUE); + + return NULL; +} + +GrdRdpDvcAudioPlayback * +grd_rdp_dvc_audio_playback_new (GrdSessionRdp *session_rdp, + GrdRdpDvcHandler *dvc_handler, + HANDLE vcm, + rdpContext *rdp_context) +{ + g_autoptr (GrdRdpDvcAudioPlayback) audio_playback = NULL; + RdpsndServerContext *rdpsnd_context; + GrdRdpDspDescriptor dsp_descriptor = {}; + g_autoptr (GError) error = NULL; + + audio_playback = g_object_new (GRD_TYPE_RDP_DVC_AUDIO_PLAYBACK, NULL); + rdpsnd_context = rdpsnd_server_context_new (vcm); + if (!rdpsnd_context) + g_error ("[RDP.AUDIO_PLAYBACK] Failed to create server context"); + + audio_playback->rdpsnd_context = rdpsnd_context; + + grd_rdp_dvc_initialize_base (GRD_RDP_DVC (audio_playback), + dvc_handler, session_rdp, + GRD_RDP_CHANNEL_AUDIO_PLAYBACK); + + rdpsnd_context->use_dynamic_virtual_channel = TRUE; + rdpsnd_context->server_formats = server_formats; + rdpsnd_context->num_server_formats = G_N_ELEMENTS (server_formats); + + rdpsnd_context->ChannelIdAssigned = rdpsnd_channel_id_assigned; + rdpsnd_context->Activated = rdpsnd_activated; + rdpsnd_context->TrainingConfirm = rdpsnd_training_confirm; + rdpsnd_context->ConfirmBlock = rdpsnd_confirm_block; + rdpsnd_context->rdpcontext = rdp_context; + rdpsnd_context->data = audio_playback; + + pw_init (NULL, NULL); + + dsp_descriptor.create_flags = GRD_RDP_DSP_CREATE_FLAG_ENCODER; + dsp_descriptor.n_samples_per_sec_aac = N_SAMPLES_PER_SEC_DEFAULT; + dsp_descriptor.n_samples_per_sec_opus = N_SAMPLES_PER_SEC_OPUS; + dsp_descriptor.n_channels = N_CHANNELS; + dsp_descriptor.bitrate_aac = audio_format_aac.nAvgBytesPerSec * 8; + dsp_descriptor.bitrate_opus = audio_format_opus.nAvgBytesPerSec * 8; + + audio_playback->rdp_dsp = grd_rdp_dsp_new (&dsp_descriptor, &error); + if (!audio_playback->rdp_dsp) + { + g_warning ("[RDP.AUDIO_PLAYBACK] Failed to create DSP instance: %s", + error->message); + return NULL; + } + + audio_playback->encode_thread = g_thread_new ("RDP audio encode thread", + encode_thread_func, + audio_playback); + + return g_steal_pointer (&audio_playback); +} + +static void +grd_rdp_dvc_audio_playback_dispose (GObject *object) +{ + GrdRdpDvcAudioPlayback *audio_playback = GRD_RDP_DVC_AUDIO_PLAYBACK (object); + GrdRdpDvc *dvc = GRD_RDP_DVC (audio_playback); + + audio_playback->protocol_stopped = TRUE; + if (audio_playback->encode_thread) + { + g_main_context_wakeup (audio_playback->encode_context); + g_clear_pointer (&audio_playback->encode_thread, g_thread_join); + } + + if (audio_playback->channel_opened) + { + audio_playback->rdpsnd_context->Stop (audio_playback->rdpsnd_context); + audio_playback->channel_opened = FALSE; + } + grd_rdp_dvc_maybe_unsubscribe_creation_status (dvc); + + if (audio_playback->rdpsnd_context) + audio_playback->rdpsnd_context->server_formats = NULL; + + g_mutex_lock (&audio_playback->streams_mutex); + g_hash_table_remove_all (audio_playback->audio_streams); + g_mutex_unlock (&audio_playback->streams_mutex); + + if (audio_playback->pipewire_registry) + { + spa_hook_remove (&audio_playback->pipewire_registry_listener); + pw_proxy_destroy ((struct pw_proxy *) audio_playback->pipewire_registry); + audio_playback->pipewire_registry = NULL; + } + if (audio_playback->pipewire_core) + { + spa_hook_remove (&audio_playback->pipewire_core_listener); + g_clear_pointer (&audio_playback->pipewire_core, pw_core_disconnect); + } + g_clear_pointer (&audio_playback->pipewire_context, pw_context_destroy); + + if (audio_playback->pipewire_source) + { + g_source_destroy (audio_playback->pipewire_source); + g_clear_pointer (&audio_playback->pipewire_source, g_source_unref); + } + + g_clear_object (&audio_playback->rdp_dsp); + + if (audio_playback->protocol_timeout_source) + { + g_source_destroy (audio_playback->protocol_timeout_source); + g_clear_pointer (&audio_playback->protocol_timeout_source, g_source_unref); + } + if (audio_playback->pipewire_setup_source) + { + g_source_destroy (audio_playback->pipewire_setup_source); + g_clear_pointer (&audio_playback->pipewire_setup_source, g_source_unref); + } + if (audio_playback->encode_source) + { + g_source_destroy (audio_playback->encode_source); + g_clear_pointer (&audio_playback->encode_source, g_source_unref); + } + if (audio_playback->svc_setup_source) + { + g_source_destroy (audio_playback->svc_setup_source); + g_clear_pointer (&audio_playback->svc_setup_source, g_source_unref); + } + + g_clear_pointer (&audio_playback->encode_context, g_main_context_unref); + + if (audio_playback->pending_frames) + { + g_queue_free_full (audio_playback->pending_frames, audio_data_free); + audio_playback->pending_frames = NULL; + } + + g_clear_pointer (&audio_playback->rdpsnd_context, rdpsnd_server_context_free); + + G_OBJECT_CLASS (grd_rdp_dvc_audio_playback_parent_class)->dispose (object); +} + +static void +grd_rdp_dvc_audio_playback_finalize (GObject *object) +{ + GrdRdpDvcAudioPlayback *audio_playback = GRD_RDP_DVC_AUDIO_PLAYBACK (object); + + g_mutex_clear (&audio_playback->pending_frames_mutex); + g_mutex_clear (&audio_playback->stream_lock_mutex); + g_mutex_clear (&audio_playback->streams_mutex); + g_mutex_clear (&audio_playback->block_mutex); + g_mutex_clear (&audio_playback->protocol_timeout_mutex); + + g_assert (g_hash_table_size (audio_playback->audio_streams) == 0); + g_clear_pointer (&audio_playback->audio_streams, g_hash_table_unref); + + pw_deinit (); + + G_OBJECT_CLASS (grd_rdp_dvc_audio_playback_parent_class)->finalize (object); +} + +static gboolean +set_up_static_virtual_channel (gpointer user_data) +{ + GrdRdpDvcAudioPlayback *audio_playback = user_data; + RdpsndServerContext *rdpsnd_context = audio_playback->rdpsnd_context; + GrdRdpDvc *dvc = GRD_RDP_DVC (audio_playback); + + g_clear_pointer (&audio_playback->svc_setup_source, g_source_unref); + + g_assert (audio_playback->channel_opened); + audio_playback->prevent_dvc_initialization = TRUE; + + rdpsnd_context->Stop (rdpsnd_context); + audio_playback->channel_opened = FALSE; + + grd_rdp_dvc_maybe_unsubscribe_creation_status (dvc); + + rdpsnd_context->use_dynamic_virtual_channel = FALSE; + + if (rdpsnd_context->Start (rdpsnd_context)) + { + g_warning ("[RDP.AUDIO_PLAYBACK] Failed to open RDPSND channel. " + "Terminating protocol"); + grd_rdp_dvc_queue_channel_tear_down (GRD_RDP_DVC (audio_playback)); + return G_SOURCE_REMOVE; + } + audio_playback->channel_opened = TRUE; + + return G_SOURCE_REMOVE; +} + +static void +clear_old_frames (GrdRdpDvcAudioPlayback *audio_playback) +{ + int64_t current_time_us; + AudioData *audio_data; + GQueue *tmp; + + current_time_us = g_get_monotonic_time (); + tmp = g_queue_copy (audio_playback->pending_frames); + + while ((audio_data = g_queue_pop_head (tmp))) + { + if (current_time_us - audio_data->timestamp_us > MAX_LOCAL_FRAMES_LIFETIME_US) + audio_data_free (g_queue_pop_head (audio_playback->pending_frames)); + else + break; + } + + g_queue_free (tmp); +} + +static uint32_t +get_current_render_latency (GrdRdpDvcAudioPlayback *audio_playback) +{ + uint32_t accumulated_render_latency_ms = 0; + uint32_t n_latencies = 0; + int64_t current_time_us; + uint16_t i; + + current_time_us = g_get_monotonic_time (); + + g_mutex_lock (&audio_playback->block_mutex); + for (i = 0; i < 256; ++i) + { + BlockInfo *block_info = &audio_playback->block_infos[i]; + + if (block_info->render_latency_ms > 0 && + current_time_us - block_info->render_latency_set_us < G_USEC_PER_SEC) + { + accumulated_render_latency_ms += block_info->render_latency_ms; + ++n_latencies; + } + } + g_mutex_unlock (&audio_playback->block_mutex); + + if (n_latencies == 0) + return 0; + + return accumulated_render_latency_ms / n_latencies; +} + +static void +maybe_drop_pending_frames (GrdRdpDvcAudioPlayback *audio_playback) +{ + uint32_t render_latency_ms; + uint16_t i; + + render_latency_ms = get_current_render_latency (audio_playback); + if (render_latency_ms <= MAX_RENDER_LATENCY_MS) + return; + + g_queue_clear_full (audio_playback->pending_frames, audio_data_free); + + g_mutex_lock (&audio_playback->block_mutex); + for (i = 0; i < 256; ++i) + { + BlockInfo *block_info = &audio_playback->block_infos[i]; + + block_info->render_latency_ms = 0; + } + g_mutex_unlock (&audio_playback->block_mutex); +} + +static void +apply_volume_to_audio_data (GrdRdpDvcAudioPlayback *audio_playback, + const GrdRdpAudioVolumeData *volume_data, + int16_t *data, + uint32_t frames) +{ + uint32_t i, j; + + g_assert (N_CHANNELS <= SPA_AUDIO_MAX_CHANNELS); + + for (i = 0; i < frames; ++i) + { + for (j = 0; j < N_CHANNELS; ++j) + { + float pcm; + + pcm = volume_data->volumes[j] * data[i * N_CHANNELS + j]; + data[i * N_CHANNELS + j] = pcm; + } + } +} + +static void +maybe_send_frames (GrdRdpDvcAudioPlayback *audio_playback, + const GrdRdpAudioVolumeData *volume_data) +{ + RdpsndServerContext *rdpsnd_context = audio_playback->rdpsnd_context; + int32_t client_format_idx = audio_playback->client_format_idx; + GrdRdpDspCodec codec = audio_playback->codec; + g_autofree uint8_t *raw_data = NULL; + uint32_t raw_data_size = 0; + uint32_t copied_data = 0; + g_autofree uint8_t *out_data = NULL; + uint32_t out_size = 0; + gboolean success = FALSE; + BlockInfo *block_info; + + g_assert (N_BLOCK_ALIGN_PCM > 0); + g_assert (audio_playback->sample_buffer_size > 0); + g_assert (audio_playback->sample_buffer_size % N_BLOCK_ALIGN_PCM == 0); + g_assert (!audio_playback->pending_training_confirm); + + raw_data_size = get_pending_audio_data_size (audio_playback); + if (raw_data_size < audio_playback->sample_buffer_size) + return; + + g_assert (raw_data_size > 0); + raw_data = g_malloc0 (audio_playback->sample_buffer_size); + + while (copied_data < audio_playback->sample_buffer_size) + { + AudioData *audio_data; + uint32_t sample_size; + uint32_t frames_taken; + + audio_data = g_queue_pop_head (audio_playback->pending_frames); + + g_assert (audio_data); + g_assert (audio_data->size % N_BLOCK_ALIGN_PCM == 0); + + sample_size = N_BLOCK_ALIGN_PCM; + frames_taken = audio_data->size / sample_size; + + g_assert (audio_data->size >= N_BLOCK_ALIGN_PCM); + + if (copied_data + audio_data->size > audio_playback->sample_buffer_size) + { + AudioData *remaining_audio_data; + uint32_t processed_size; + uint32_t remaining_size; + + processed_size = audio_playback->sample_buffer_size - copied_data; + g_assert (processed_size % N_BLOCK_ALIGN_PCM == 0); + + frames_taken = processed_size / sample_size; + + remaining_size = audio_data->size - processed_size; + if (remaining_size >= N_BLOCK_ALIGN_PCM) + { + remaining_audio_data = g_new0 (AudioData, 1); + remaining_audio_data->data = + g_memdup2 (((uint8_t *) audio_data->data) + processed_size, + remaining_size); + remaining_audio_data->size = remaining_size; + remaining_audio_data->timestamp_us = audio_data->timestamp_us; + + g_queue_push_head (audio_playback->pending_frames, remaining_audio_data); + } + } + + apply_volume_to_audio_data (audio_playback, volume_data, + (int16_t *) audio_data->data, + frames_taken); + + memcpy (raw_data + copied_data, audio_data->data, + frames_taken * sample_size); + copied_data += frames_taken * sample_size; + + audio_data_free (audio_data); + } + + switch (codec) + { + case GRD_RDP_DSP_CODEC_NONE: + out_data = g_steal_pointer (&raw_data); + out_size = copied_data; + success = TRUE; + break; + case GRD_RDP_DSP_CODEC_AAC: + case GRD_RDP_DSP_CODEC_ALAW: + case GRD_RDP_DSP_CODEC_OPUS: + success = grd_rdp_dsp_encode (audio_playback->rdp_dsp, codec, + (int16_t *) raw_data, copied_data, + sizeof (int16_t), &out_data, &out_size); + break; + } + + if (!success) + return; + + g_mutex_lock (&audio_playback->block_mutex); + block_info = &audio_playback->block_infos[rdpsnd_context->block_no]; + + block_info->render_latency_ms = 0; + g_mutex_unlock (&audio_playback->block_mutex); + + rdpsnd_context->SendSamples2 (rdpsnd_context, client_format_idx, + out_data, out_size, 0, 0); +} + +static gboolean +maybe_encode_frames (gpointer user_data) +{ + GrdRdpDvcAudioPlayback *audio_playback = user_data; + g_autoptr (GMutexLocker) streams_locker = NULL; + g_autoptr (GMutexLocker) stream_lock_locker = NULL; + GrdRdpAudioOutputStream *audio_output_stream; + GrdRdpAudioVolumeData volume_data = {}; + uint32_t node_id; + + if (audio_playback->pending_training_confirm) + return G_SOURCE_CONTINUE; + + stream_lock_locker = g_mutex_locker_new (&audio_playback->stream_lock_mutex); + if (!audio_playback->has_stream_lock) + return G_SOURCE_CONTINUE; + + node_id = audio_playback->locked_node_id; + + streams_locker = g_mutex_locker_new (&audio_playback->streams_mutex); + if (!g_hash_table_lookup_extended (audio_playback->audio_streams, + GUINT_TO_POINTER (node_id), + NULL, (gpointer *) &audio_output_stream)) + return G_SOURCE_CONTINUE; + + grd_rdp_audio_output_stream_get_volume_data (audio_output_stream, + &volume_data); + g_clear_pointer (&streams_locker, g_mutex_locker_free); + g_clear_pointer (&stream_lock_locker, g_mutex_locker_free); + + g_return_val_if_fail (volume_data.n_volumes <= SPA_AUDIO_MAX_CHANNELS, + G_SOURCE_CONTINUE); + + prepare_volume_data (&volume_data); + + g_mutex_lock (&audio_playback->pending_frames_mutex); + clear_old_frames (audio_playback); + maybe_drop_pending_frames (audio_playback); + + if (g_queue_get_length (audio_playback->pending_frames) > 0) + maybe_send_frames (audio_playback, &volume_data); + g_mutex_unlock (&audio_playback->pending_frames_mutex); + + return G_SOURCE_CONTINUE; +} + +static void +pipewire_core_error (void *user_data, + uint32_t id, + int seq, + int res, + const char *message) +{ + GrdRdpDvcAudioPlayback *audio_playback = user_data; + + g_warning ("[RDP.AUDIO_PLAYBACK] PipeWire core error: " + "id: %u, seq: %i, res: %i, %s", id, seq, res, message); + + if (id == PW_ID_CORE && res == -EPIPE) + grd_rdp_dvc_queue_channel_tear_down (GRD_RDP_DVC (audio_playback)); +} + +static const struct pw_core_events pipewire_core_events = +{ + .version = PW_VERSION_CORE_EVENTS, + .error = pipewire_core_error, +}; + +static void +registry_event_global (void *user_data, + uint32_t id, + uint32_t permissions, + const char *type, + uint32_t version, + const struct spa_dict *props) +{ + GrdRdpDvcAudioPlayback *audio_playback = user_data; + GrdRdpAudioOutputStream *audio_output_stream; + g_autoptr (GMutexLocker) locker = NULL; + const struct spa_dict_item *item; + gboolean found_audio_sink = FALSE; + uint32_t position[SPA_AUDIO_MAX_CHANNELS] = {}; + g_autofree char *node_name = NULL; + g_autoptr (GError) error = NULL; + + if (strcmp (type, PW_TYPE_INTERFACE_Node) != 0) + return; + + spa_dict_for_each (item, props) + { + if (strcmp (item->key, "node.name") == 0) + { + g_clear_pointer (&node_name, g_free); + node_name = g_strdup (item->value); + } + if (strcmp (item->key, "media.class") == 0 && + strcmp (item->value, "Audio/Sink") == 0) + found_audio_sink = TRUE; + } + + if (!found_audio_sink) + return; + + g_debug ("[RDP.AUDIO_PLAYBACK] Found audio sink: id: %u, type: %s/%u, " + "name: %s", id, type, version, node_name); + + locker = g_mutex_locker_new (&audio_playback->streams_mutex); + if (g_hash_table_contains (audio_playback->audio_streams, + GUINT_TO_POINTER (id))) + return; + + g_clear_pointer (&locker, g_mutex_locker_free); + + g_assert (N_CHANNELS == 2); + position[0] = SPA_AUDIO_CHANNEL_FL; + position[1] = SPA_AUDIO_CHANNEL_FR; + + audio_output_stream = + grd_rdp_audio_output_stream_new (audio_playback, + audio_playback->pipewire_core, + audio_playback->pipewire_registry, + id, audio_playback->n_samples_per_sec, + N_CHANNELS, position, &error); + if (!audio_output_stream) + { + g_warning ("[RDP.AUDIO_PLAYBACK] Failed to establish PipeWire stream: " + "%s", error->message); + return; + } + + locker = g_mutex_locker_new (&audio_playback->streams_mutex); + g_hash_table_insert (audio_playback->audio_streams, + GUINT_TO_POINTER (id), audio_output_stream); + g_clear_pointer (&locker, g_mutex_locker_free); + + g_mutex_lock (&audio_playback->stream_lock_mutex); + if (!audio_playback->has_stream_lock) + grd_rdp_audio_output_stream_set_active (audio_output_stream, TRUE); + g_mutex_unlock (&audio_playback->stream_lock_mutex); +} + +static void +registry_event_global_remove (void *user_data, + uint32_t id) +{ + GrdRdpDvcAudioPlayback *audio_playback = user_data; + + g_mutex_lock (&audio_playback->streams_mutex); + g_hash_table_remove (audio_playback->audio_streams, GUINT_TO_POINTER (id)); + g_mutex_unlock (&audio_playback->streams_mutex); + + g_mutex_lock (&audio_playback->stream_lock_mutex); + if (audio_playback->has_stream_lock && + audio_playback->locked_node_id == id) + release_stream_lock (audio_playback); + g_mutex_unlock (&audio_playback->stream_lock_mutex); +} + +static const struct pw_registry_events registry_events = +{ + .version = PW_VERSION_REGISTRY_EVENTS, + .global = registry_event_global, + .global_remove = registry_event_global_remove, +}; + +static gboolean +set_up_registry_listener (GrdRdpDvcAudioPlayback *audio_playback, + GError **error) +{ + GrdPipeWireSource *pipewire_source; + + pipewire_source = grd_attached_pipewire_source_new ("RDP.AUDIO_PLAYBACK", + error); + if (!pipewire_source) + return FALSE; + + audio_playback->pipewire_source = (GSource *) pipewire_source; + + audio_playback->pipewire_context = + pw_context_new (pipewire_source->pipewire_loop, NULL, 0); + if (!audio_playback->pipewire_context) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create PipeWire context"); + return FALSE; + } + + audio_playback->pipewire_core = + pw_context_connect (audio_playback->pipewire_context, NULL, 0); + if (!audio_playback->pipewire_core) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create PipeWire core"); + return FALSE; + } + + pw_core_add_listener (audio_playback->pipewire_core, + &audio_playback->pipewire_core_listener, + &pipewire_core_events, audio_playback); + + audio_playback->pipewire_registry = + pw_core_get_registry (audio_playback->pipewire_core, + PW_VERSION_REGISTRY, 0); + if (!audio_playback->pipewire_registry) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to retrieve PipeWire registry"); + return FALSE; + } + + pw_registry_add_listener (audio_playback->pipewire_registry, + &audio_playback->pipewire_registry_listener, + ®istry_events, audio_playback); + + return TRUE; +} + +static gboolean +set_up_pipewire (gpointer user_data) +{ + GrdRdpDvcAudioPlayback *audio_playback = user_data; + g_autoptr (GError) error = NULL; + + g_clear_pointer (&audio_playback->pipewire_setup_source, g_source_unref); + + if (!set_up_registry_listener (audio_playback, &error)) + { + g_warning ("[RDP.AUDIO_PLAYBACK] Failed to setup registry listener: %s", + error->message); + grd_rdp_dvc_queue_channel_tear_down (GRD_RDP_DVC (audio_playback)); + } + + return G_SOURCE_REMOVE; +} + +static gboolean +source_dispatch (GSource *source, + GSourceFunc callback, + gpointer user_data) +{ + g_source_set_ready_time (source, -1); + + return callback (user_data); +} + +static GSourceFuncs source_funcs = +{ + .dispatch = source_dispatch, +}; + +static void +grd_rdp_dvc_audio_playback_init (GrdRdpDvcAudioPlayback *audio_playback) +{ + GSource *svc_setup_source; + GSource *encode_source; + GSource *pipewire_setup_source; + + audio_playback->pending_client_formats = TRUE; + audio_playback->pending_training_confirm = TRUE; + + audio_playback->aac_client_format_idx = -1; + audio_playback->opus_client_format_idx = -1; + audio_playback->pcm_client_format_idx = -1; + + audio_playback->audio_streams = g_hash_table_new_full (NULL, NULL, + NULL, g_object_unref); + audio_playback->pending_frames = g_queue_new (); + + g_mutex_init (&audio_playback->protocol_timeout_mutex); + g_mutex_init (&audio_playback->block_mutex); + g_mutex_init (&audio_playback->streams_mutex); + g_mutex_init (&audio_playback->stream_lock_mutex); + g_mutex_init (&audio_playback->pending_frames_mutex); + + audio_playback->encode_context = g_main_context_new (); + + svc_setup_source = g_source_new (&source_funcs, sizeof (GSource)); + g_source_set_callback (svc_setup_source, set_up_static_virtual_channel, + audio_playback, NULL); + g_source_set_ready_time (svc_setup_source, -1); + g_source_attach (svc_setup_source, NULL); + audio_playback->svc_setup_source = svc_setup_source; + + encode_source = g_source_new (&source_funcs, sizeof (GSource)); + g_source_set_callback (encode_source, maybe_encode_frames, + audio_playback, NULL); + g_source_set_ready_time (encode_source, -1); + g_source_attach (encode_source, audio_playback->encode_context); + audio_playback->encode_source = encode_source; + + pipewire_setup_source = g_source_new (&source_funcs, sizeof (GSource)); + g_source_set_callback (pipewire_setup_source, set_up_pipewire, + audio_playback, NULL); + g_source_set_ready_time (pipewire_setup_source, -1); + g_source_attach (pipewire_setup_source, NULL); + audio_playback->pipewire_setup_source = pipewire_setup_source; +} + +static void +grd_rdp_dvc_audio_playback_class_init (GrdRdpDvcAudioPlaybackClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GrdRdpDvcClass *dvc_class = GRD_RDP_DVC_CLASS (klass); + + object_class->dispose = grd_rdp_dvc_audio_playback_dispose; + object_class->finalize = grd_rdp_dvc_audio_playback_finalize; + + dvc_class->maybe_init = grd_rdp_dvc_audio_playback_maybe_init; +} diff --git a/grd-rdp-dvc-audio-playback.h b/grd-rdp-dvc-audio-playback.h new file mode 100644 index 0000000..05919dc --- /dev/null +++ b/grd-rdp-dvc-audio-playback.h @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2021 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include "grd-rdp-dvc.h" + +#define GRD_TYPE_RDP_DVC_AUDIO_PLAYBACK (grd_rdp_dvc_audio_playback_get_type ()) +G_DECLARE_FINAL_TYPE (GrdRdpDvcAudioPlayback, grd_rdp_dvc_audio_playback, + GRD, RDP_DVC_AUDIO_PLAYBACK, GrdRdpDvc) + +GrdRdpDvcAudioPlayback *grd_rdp_dvc_audio_playback_new (GrdSessionRdp *session_rdp, + GrdRdpDvcHandler *dvc_handler, + HANDLE vcm, + rdpContext *rdp_context); + +void grd_rdp_dvc_audio_playback_maybe_submit_samples (GrdRdpDvcAudioPlayback *audio_playback, + uint32_t node_id, + GrdRdpAudioVolumeData *volume_data, + int16_t *data, + uint32_t size); diff --git a/grd-rdp-dvc-camera-device.c b/grd-rdp-dvc-camera-device.c new file mode 100644 index 0000000..9bc39ac --- /dev/null +++ b/grd-rdp-dvc-camera-device.c @@ -0,0 +1,1783 @@ +/* + * Copyright (C) 2025 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-rdp-dvc-camera-device.h" + +#include "grd-pipewire-utils.h" +#include "grd-rdp-camera-stream.h" +#include "grd-sample-buffer.h" +#include "grd-utils.h" + +#define SAMPLE_TIMEOUT_MS (2 * 1000) + +enum +{ + ERROR, + + N_SIGNALS +}; + +static guint signals[N_SIGNALS]; + +typedef enum +{ + DEVICE_STATE_FATAL_ERROR, + DEVICE_STATE_PENDING_ACTIVATION, + DEVICE_STATE_PENDING_ACTIVATION_RESPONSE, + DEVICE_STATE_PENDING_STREAM_LIST_RESPONSE, + DEVICE_STATE_PENDING_MEDIA_TYPE_LIST_RESPONSE, + DEVICE_STATE_PENDING_PROPERTY_LIST_RESPONSE, + DEVICE_STATE_PENDING_PROPERTY_VALUE_RESPONSE, + DEVICE_STATE_PENDING_STREAM_PREPARATION, + DEVICE_STATE_INITIALIZATION_DONE, + DEVICE_STATE_IN_SHUTDOWN, +} DeviceState; + +typedef enum +{ + DEVICE_EVENT_ACK_STARTED_STREAMS, + DEVICE_EVENT_STOP_STREAMS, + DEVICE_EVENT_ACK_STOPPED_STREAMS, +} DeviceEvent; + +typedef enum +{ + CLIENT_REQUEST_TYPE_NONE, + CLIENT_REQUEST_TYPE_START_STREAMS, + CLIENT_REQUEST_TYPE_STOP_STREAMS, +} ClientRequestType; + +typedef struct +{ + CAM_MEDIA_TYPE_DESCRIPTION *media_type_description; + uint32_t run_sequence; +} StreamRunContext; + +typedef struct +{ + GrdRdpDvcCameraDevice *device; + + GList *media_type_descriptions; + GrdRdpCameraStream *camera_stream; + + GAsyncQueue *pending_samples; +} StreamContext; + +struct _GrdRdpDvcCameraDevice +{ + GrdRdpDvc parent; + + CameraDeviceServerContext *device_context; + gboolean channel_opened; + + char *dvc_name; + char *device_name; + + GThread *camera_thread; + GMainContext *camera_context; + gboolean in_shutdown; + + GrdSyncPoint sync_point; + + GSource *initialization_source; + GMutex state_mutex; + DeviceState state; + GQueue *pending_media_type_lists; + GQueue *pending_property_values; + + GHashTable *stream_contexts; + + GSource *pipewire_source; + + struct pw_context *pipewire_context; + struct pw_core *pipewire_core; + struct spa_hook pipewire_core_listener; + + GHashTable *queued_stream_starts; + GHashTable *pending_stream_starts; + GHashTable *running_streams; + + GSource *event_source; + + GMutex event_mutex; + GHashTable *pending_events; + + GMutex client_request_mutex; + gboolean pending_client_request; + ClientRequestType pending_client_request_type; + + GMutex sample_request_mutex; + GHashTable *pending_samples; + + GSource *sample_timeout_source; +}; + +G_DEFINE_TYPE (GrdRdpDvcCameraDevice, grd_rdp_dvc_camera_device, + GRD_TYPE_RDP_DVC) + +static StreamRunContext * +stream_run_context_new (CAM_MEDIA_TYPE_DESCRIPTION *media_type_description, + uint32_t run_sequence) +{ + StreamRunContext *stream_run_context; + + stream_run_context = g_new0 (StreamRunContext, 1); + stream_run_context->media_type_description = media_type_description; + stream_run_context->run_sequence = run_sequence; + + return stream_run_context; +} + +static void +stream_run_context_free (StreamRunContext *stream_run_context) +{ + g_free (stream_run_context); +} + +static StreamContext * +stream_context_new (GrdRdpDvcCameraDevice *device) +{ + StreamContext *stream_context; + + stream_context = g_new0 (StreamContext, 1); + stream_context->device = device; + + stream_context->pending_samples = g_async_queue_new (); + + return stream_context; +} + +static void +discard_pending_sample_requests (StreamContext *stream_context) +{ + GrdRdpDvcCameraDevice *device = stream_context->device; + g_autoptr (GMutexLocker) locker = NULL; + GrdSampleBuffer *sample_buffer; + + locker = g_mutex_locker_new (&device->state_mutex); + while ((sample_buffer = g_async_queue_try_pop (stream_context->pending_samples))) + { + g_mutex_lock (&device->sample_request_mutex); + g_hash_table_remove (device->pending_samples, sample_buffer); + g_mutex_unlock (&device->sample_request_mutex); + + grd_rdp_camera_stream_submit_sample (stream_context->camera_stream, + sample_buffer, FALSE); + } +} + +static void +stream_context_free (StreamContext *stream_context) +{ + if (stream_context->camera_stream) + { + GrdRdpDvcCameraDevice *device = stream_context->device; + uint8_t stream_index = + grd_rdp_camera_stream_get_stream_index (stream_context->camera_stream); + + discard_pending_sample_requests (stream_context); + + g_hash_table_remove (device->queued_stream_starts, + GUINT_TO_POINTER (stream_index)); + g_hash_table_remove (device->pending_stream_starts, + GUINT_TO_POINTER (stream_index)); + g_hash_table_remove (device->running_streams, + GUINT_TO_POINTER (stream_index)); + } + + g_clear_object (&stream_context->camera_stream); + g_clear_list (&stream_context->media_type_descriptions, g_free); + + g_clear_pointer (&stream_context->pending_samples, g_async_queue_unref); + + g_free (stream_context); +} + +const char * +grd_rdp_dvc_camera_device_get_dvc_name (GrdRdpDvcCameraDevice *device) +{ + return device->dvc_name; +} + +void +grd_rdp_dvc_camera_device_start_stream (GrdRdpDvcCameraDevice *device, + GrdRdpCameraStream *camera_stream, + CAM_MEDIA_TYPE_DESCRIPTION *media_type_description, + uint32_t run_sequence) +{ + uint8_t stream_index = grd_rdp_camera_stream_get_stream_index (camera_stream); + + g_hash_table_insert (device->queued_stream_starts, + GUINT_TO_POINTER (stream_index), + stream_run_context_new (media_type_description, + run_sequence)); + + g_source_set_ready_time (device->event_source, 0); +} + +static gboolean +queue_stream_restart (gpointer key, + gpointer value, + gpointer user_data) +{ + GrdRdpDvcCameraDevice *device = user_data; + uint8_t stream_index = GPOINTER_TO_UINT (key); + StreamRunContext *stream_run_context = value; + StreamContext *stream_context = NULL; + + if (!g_hash_table_lookup_extended (device->stream_contexts, + GUINT_TO_POINTER (stream_index), + NULL, (gpointer *) &stream_context)) + g_assert_not_reached (); + + grd_rdp_camera_stream_inhibit_camera_loop (stream_context->camera_stream); + + if (!g_hash_table_contains (device->queued_stream_starts, + GUINT_TO_POINTER (stream_index))) + { + g_hash_table_insert (device->queued_stream_starts, + GUINT_TO_POINTER (stream_index), + g_memdup2 (stream_run_context, + sizeof (StreamRunContext))); + } + + return TRUE; +} + +void +grd_rdp_dvc_camera_device_stop_stream (GrdRdpDvcCameraDevice *device, + GrdRdpCameraStream *camera_stream) +{ + uint8_t stream_index = grd_rdp_camera_stream_get_stream_index (camera_stream); + + g_hash_table_remove (device->running_streams, + GUINT_TO_POINTER (stream_index)); + g_hash_table_remove (device->queued_stream_starts, + GUINT_TO_POINTER (stream_index)); + + /* + * Stopping a stream can mean one of the two things: + * 1) The stream is stopped, because there is no consumer left + * 2) The stream format was changed + * + * For case 2, all streams need to be stopped, because the [MS-RDPECAM] + * protocol only supports stopping all streams and not stopping one individual + * stream. + */ + g_hash_table_foreach_remove (device->running_streams, + queue_stream_restart, device); + + g_mutex_lock (&device->event_mutex); + g_hash_table_add (device->pending_events, + GUINT_TO_POINTER (DEVICE_EVENT_STOP_STREAMS)); + g_mutex_unlock (&device->event_mutex); + + g_source_set_ready_time (device->event_source, 0); +} + +void +grd_rdp_dvc_camera_device_request_sample (GrdRdpDvcCameraDevice *device, + GrdRdpCameraStream *camera_stream, + GrdSampleBuffer *sample_buffer) +{ + CameraDeviceServerContext *device_context = device->device_context; + uint8_t stream_index = grd_rdp_camera_stream_get_stream_index (camera_stream); + StreamContext *stream_context = NULL; + CAM_SAMPLE_REQUEST sample_request = {}; + + g_assert (device->pending_client_request_type != + CLIENT_REQUEST_TYPE_STOP_STREAMS); + + if (!g_hash_table_lookup_extended (device->stream_contexts, + GUINT_TO_POINTER (stream_index), + NULL, (gpointer *) &stream_context)) + g_assert_not_reached (); + + g_mutex_lock (&device->sample_request_mutex); + g_hash_table_add (device->pending_samples, sample_buffer); + g_mutex_unlock (&device->sample_request_mutex); + + g_async_queue_push (stream_context->pending_samples, sample_buffer); + + sample_request.StreamIndex = stream_index; + + device_context->SampleRequest (device_context, &sample_request); +} + +static void +dvc_creation_status_cb (gpointer user_data, + int32_t creation_status) +{ + GrdRdpDvcCameraDevice *device = user_data; + + if (creation_status < 0) + { + g_warning ("[RDP.CAM_DEVICE] Failed to open %s channel " + "(CreationStatus %i, device name: \"%s\"). " + "Terminating protocol", + device->dvc_name, creation_status, device->device_name); + g_signal_emit (device, signals[ERROR], 0); + return; + } + + g_message ("[RDP.CAM_DEVICE] Successfully opened %s channel " + "(CreationStatus %i, device name: \"%s\"). ", + device->dvc_name, creation_status, device->device_name); + + g_source_set_ready_time (device->initialization_source, 0); +} + +static BOOL +device_channel_id_assigned (CameraDeviceServerContext *device_context, + uint32_t channel_id) +{ + GrdRdpDvcCameraDevice *device = device_context->userdata; + GrdRdpDvc *dvc = GRD_RDP_DVC (device); + + g_debug ("[RDP.CAM_DEVICE] DVC channel id for channel %s assigned to id %u", + device->dvc_name, channel_id); + + grd_rdp_dvc_subscribe_creation_status (dvc, channel_id, + dvc_creation_status_cb, + device); + + return TRUE; +} + +static const char * +device_state_to_string (DeviceState state) +{ + switch (state) + { + case DEVICE_STATE_FATAL_ERROR: + return "FATAL_ERROR"; + case DEVICE_STATE_PENDING_ACTIVATION: + return "PENDING_ACTIVATION"; + case DEVICE_STATE_PENDING_ACTIVATION_RESPONSE: + return "PENDING_ACTIVATION_RESPONSE"; + case DEVICE_STATE_PENDING_STREAM_LIST_RESPONSE: + return "PENDING_STREAM_LIST_RESPONSE"; + case DEVICE_STATE_PENDING_MEDIA_TYPE_LIST_RESPONSE: + return "PENDING_MEDIA_TYPE_LIST_RESPONSE"; + case DEVICE_STATE_PENDING_PROPERTY_LIST_RESPONSE: + return "PENDING_PROPERTY_LIST_RESPONSE"; + case DEVICE_STATE_PENDING_PROPERTY_VALUE_RESPONSE: + return "PENDING_PROPERTY_VALUE_RESPONSE"; + case DEVICE_STATE_PENDING_STREAM_PREPARATION: + return "PENDING_STREAM_PREPARATION"; + case DEVICE_STATE_INITIALIZATION_DONE: + return "INITIALIZATION_DONE"; + case DEVICE_STATE_IN_SHUTDOWN: + return "IN_SHUTDOWN"; + } + + g_assert_not_reached (); +} + +static void +transition_into_fatal_error_state (GrdRdpDvcCameraDevice *device) +{ + device->state = DEVICE_STATE_FATAL_ERROR; + g_signal_emit (device, signals[ERROR], 0); +} + +static void +request_stream_list (GrdRdpDvcCameraDevice *device) +{ + CameraDeviceServerContext *device_context = device->device_context; + CAM_STREAM_LIST_REQUEST stream_list_request = {}; + + device->state = DEVICE_STATE_PENDING_STREAM_LIST_RESPONSE; + device_context->StreamListRequest (device_context, &stream_list_request); +} + +static gboolean +try_handle_runtime_success_response (GrdRdpDvcCameraDevice *device, + GError **error) +{ + g_autoptr (GMutexLocker) locker = NULL; + + locker = g_mutex_locker_new (&device->client_request_mutex); + if (!device->pending_client_request) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "Protocol violation: " + "Received stray success response in state %s for device " + "\"%s\". There was no runtime request. Removing device...", + device_state_to_string (device->state), device->device_name); + return FALSE; + } + + g_mutex_lock (&device->event_mutex); + switch (device->pending_client_request_type) + { + case CLIENT_REQUEST_TYPE_NONE: + g_assert_not_reached (); + break; + case CLIENT_REQUEST_TYPE_START_STREAMS: + g_hash_table_add (device->pending_events, + GUINT_TO_POINTER (DEVICE_EVENT_ACK_STARTED_STREAMS)); + break; + case CLIENT_REQUEST_TYPE_STOP_STREAMS: + g_hash_table_add (device->pending_events, + GUINT_TO_POINTER (DEVICE_EVENT_ACK_STOPPED_STREAMS)); + break; + } + g_mutex_unlock (&device->event_mutex); + + g_source_set_ready_time (device->event_source, 0); + + return TRUE; +} + +/* + * Actions for success: + * + * - Activate Device Request + * - Deactivate Device Request + * - Start Streams Request + * - Stop Streams Request + * - Set Property Value Request + */ +static uint32_t +device_success_response (CameraDeviceServerContext *device_context, + const CAM_SUCCESS_RESPONSE *success_response) +{ + GrdRdpDvcCameraDevice *device = device_context->userdata; + g_autoptr (GMutexLocker) locker = NULL; + g_autoptr (GError) error = NULL; + + locker = g_mutex_locker_new (&device->state_mutex); + switch (device->state) + { + case DEVICE_STATE_FATAL_ERROR: + case DEVICE_STATE_IN_SHUTDOWN: + break; + case DEVICE_STATE_PENDING_ACTIVATION: + case DEVICE_STATE_PENDING_STREAM_LIST_RESPONSE: + case DEVICE_STATE_PENDING_MEDIA_TYPE_LIST_RESPONSE: + case DEVICE_STATE_PENDING_PROPERTY_LIST_RESPONSE: + case DEVICE_STATE_PENDING_PROPERTY_VALUE_RESPONSE: + case DEVICE_STATE_PENDING_STREAM_PREPARATION: + g_warning ("[RDP.CAM_DEVICE] Protocol violation: Received stray success " + "response in state %s for device \"%s\". Removing device...", + device_state_to_string (device->state), device->device_name); + transition_into_fatal_error_state (device); + break; + case DEVICE_STATE_PENDING_ACTIVATION_RESPONSE: + g_debug ("[RDP.CAM_DEVICE] Activated device \"%s\"", device->device_name); + request_stream_list (device); + break; + case DEVICE_STATE_INITIALIZATION_DONE: + if (!try_handle_runtime_success_response (device, &error)) + { + g_warning ("[RDP.CAM_DEVICE] %s", error->message); + transition_into_fatal_error_state (device); + } + break; + } + + return CHANNEL_RC_OK; +} + +static const char * +error_code_to_string (CAM_ERROR_CODE error_code) +{ + switch (error_code) + { + case CAM_ERROR_CODE_UnexpectedError: + return "Unexpected Error"; + case CAM_ERROR_CODE_InvalidMessage: + return "Invalid Message"; + case CAM_ERROR_CODE_NotInitialized: + return "Not Initialized"; + case CAM_ERROR_CODE_InvalidRequest: + return "Invalid Request"; + case CAM_ERROR_CODE_InvalidStreamNumber: + return "Invalid Stream Number"; + case CAM_ERROR_CODE_InvalidMediaType: + return "Invalid MediaType"; + case CAM_ERROR_CODE_OutOfMemory: + return "Out Of Memory"; + case CAM_ERROR_CODE_ItemNotFound: + return "Item Not Found"; + case CAM_ERROR_CODE_SetNotFound: + return "Set Not Found"; + case CAM_ERROR_CODE_OperationNotSupported: + return "Operation Not Supported"; + default: + return "Unknown Error"; + } + + g_assert_not_reached (); +} + +static const char * +device_error_to_string (const CAM_ERROR_RESPONSE *error_response) +{ + return error_code_to_string (error_response->ErrorCode); +} + +static const char * +sample_error_to_string (const CAM_SAMPLE_ERROR_RESPONSE *sample_error_response) +{ + return error_code_to_string (sample_error_response->ErrorCode); +} + +static void +fetch_runtime_error_response (GrdRdpDvcCameraDevice *device, + const CAM_ERROR_RESPONSE *error_response, + GError **error) +{ + g_autoptr (GMutexLocker) locker = NULL; + + locker = g_mutex_locker_new (&device->client_request_mutex); + if (!device->pending_client_request) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "Protocol violation: " + "Received stray error response \"%s\" in state %s for " + "device \"%s\". There was no runtime request. Removing " + "device...", device_error_to_string (error_response), + device_state_to_string (device->state), device->device_name); + return; + } + + switch (device->pending_client_request_type) + { + case CLIENT_REQUEST_TYPE_NONE: + g_assert_not_reached (); + break; + case CLIENT_REQUEST_TYPE_START_STREAMS: + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "Failed to start " + "streams on device \"%s\": %s. Removing device...", + device->device_name, device_error_to_string (error_response)); + break; + case CLIENT_REQUEST_TYPE_STOP_STREAMS: + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "Failed to stop " + "streams on device \"%s\": %s. Removing device...", + device->device_name, device_error_to_string (error_response)); + break; + } +} + +/* + * Actions for failure: + * + * - Activate Device Request + * - Deactivate Device Request + * - Start Streams Request + * - Stop Streams Request + * - Set Property Value Request + * + * - Stream List Request + * - Media Type List Request + * - Current Media Type Request + * - Property List Request + * - Property Value Request + */ +static uint32_t +device_error_response (CameraDeviceServerContext *device_context, + const CAM_ERROR_RESPONSE *error_response) +{ + GrdRdpDvcCameraDevice *device = device_context->userdata; + g_autoptr (GError) error = NULL; + + g_autoptr (GMutexLocker) locker = NULL; + + locker = g_mutex_locker_new (&device->state_mutex); + switch (device->state) + { + case DEVICE_STATE_FATAL_ERROR: + case DEVICE_STATE_IN_SHUTDOWN: + break; + case DEVICE_STATE_PENDING_ACTIVATION: + case DEVICE_STATE_PENDING_STREAM_PREPARATION: + g_warning ("[RDP.CAM_DEVICE] Protocol violation: Received stray error " + "response \"%s\" in state %s for device \"%s\". " + "Removing device...", device_error_to_string (error_response), + device_state_to_string (device->state), device->device_name); + break; + case DEVICE_STATE_PENDING_ACTIVATION_RESPONSE: + g_warning ("[RDP.CAM_DEVICE] Failed to activate device \"%s\": %s." + "Removing device...", + device->device_name, device_error_to_string (error_response)); + break; + case DEVICE_STATE_PENDING_STREAM_LIST_RESPONSE: + g_warning ("[RDP.CAM_DEVICE] Failed to fetch stream list from device " + "\"%s\": %s. Removing device...", + device->device_name, device_error_to_string (error_response)); + break; + case DEVICE_STATE_PENDING_MEDIA_TYPE_LIST_RESPONSE: + g_warning ("[RDP.CAM_DEVICE] Failed to fetch media type list from device " + "\"%s\": %s. Removing device...", + device->device_name, device_error_to_string (error_response)); + break; + case DEVICE_STATE_PENDING_PROPERTY_LIST_RESPONSE: + g_warning ("[RDP.CAM_DEVICE] Failed to fetch property list from device " + "\"%s\": %s. Removing device...", + device->device_name, device_error_to_string (error_response)); + break; + case DEVICE_STATE_PENDING_PROPERTY_VALUE_RESPONSE: + g_warning ("[RDP.CAM_DEVICE] Failed to fetch property value from device " + "\"%s\": %s. Removing device...", + device->device_name, device_error_to_string (error_response)); + break; + case DEVICE_STATE_INITIALIZATION_DONE: + fetch_runtime_error_response (device, error_response, &error); + g_warning ("[RDP.CAM_DEVICE] %s", error->message); + break; + } + + transition_into_fatal_error_state (device); + + return CHANNEL_RC_OK; +} + +static void +request_next_media_type_list (GrdRdpDvcCameraDevice *device) +{ + CameraDeviceServerContext *device_context = device->device_context; + CAM_MEDIA_TYPE_LIST_REQUEST media_type_list_request = {}; + + g_assert (g_queue_get_length (device->pending_media_type_lists) > 0); + + media_type_list_request.StreamIndex = + GPOINTER_TO_UINT (g_queue_peek_head (device->pending_media_type_lists)); + + device->state = DEVICE_STATE_PENDING_MEDIA_TYPE_LIST_RESPONSE; + device_context->MediaTypeListRequest (device_context, + &media_type_list_request); +} + +static uint32_t +device_stream_list_response (CameraDeviceServerContext *device_context, + const CAM_STREAM_LIST_RESPONSE *stream_list_response) +{ + GrdRdpDvcCameraDevice *device = device_context->userdata; + g_autoptr (GMutexLocker) locker = NULL; + uint16_t i; + + locker = g_mutex_locker_new (&device->state_mutex); + if (device->state == DEVICE_STATE_FATAL_ERROR || + device->state == DEVICE_STATE_IN_SHUTDOWN) + return CHANNEL_RC_OK; + + if (device->state != DEVICE_STATE_PENDING_STREAM_LIST_RESPONSE) + { + g_warning ("[RDP.CAM_DEVICE] Protocol violation: Received stray stream " + "list response in state %s for device \"%s\". " + "Removing device...", + device_state_to_string (device->state), device->device_name); + transition_into_fatal_error_state (device); + return CHANNEL_RC_OK; + } + if (stream_list_response->N_Descriptions < 1) + { + g_warning ("[RDP.CAM_DEVICE] Device \"%s\": Received invalid stream list " + "response. Removing device...", device->device_name); + transition_into_fatal_error_state (device); + return CHANNEL_RC_OK; + } + + for (i = 0; i < stream_list_response->N_Descriptions; ++i) + { + g_debug ("[RDP.CAM_DEVICE] Device \"%s\": Stream %u: FrameSourceTypes: %u, " + "StreamCategory, %u, Selected: %u, CanBeShared: %u", + device->device_name, i, + stream_list_response->StreamDescriptions[i].FrameSourceTypes, + stream_list_response->StreamDescriptions[i].StreamCategory, + stream_list_response->StreamDescriptions[i].Selected, + stream_list_response->StreamDescriptions[i].CanBeShared); + + g_hash_table_insert (device->stream_contexts, + GUINT_TO_POINTER (i), stream_context_new (device)); + g_queue_push_tail (device->pending_media_type_lists, + GUINT_TO_POINTER (i)); + } + + request_next_media_type_list (device); + + return CHANNEL_RC_OK; +} + +static void +request_property_list (GrdRdpDvcCameraDevice *device) +{ + CameraDeviceServerContext *device_context = device->device_context; + CAM_PROPERTY_LIST_REQUEST property_list_request = {}; + + device->state = DEVICE_STATE_PENDING_PROPERTY_LIST_RESPONSE; + device_context->PropertyListRequest (device_context, &property_list_request); +} + +static void +start_preparing_streams (GrdRdpDvcCameraDevice *device) +{ + device->state = DEVICE_STATE_PENDING_STREAM_PREPARATION; + g_source_set_ready_time (device->initialization_source, 0); +} + +static uint32_t +device_media_type_list_response (CameraDeviceServerContext *device_context, + const CAM_MEDIA_TYPE_LIST_RESPONSE *media_type_list_response) +{ + GrdRdpDvcCameraDevice *device = device_context->userdata; + g_autoptr (GMutexLocker) locker = NULL; + StreamContext *stream_context = NULL; + uint8_t stream_index; + size_t i; + + locker = g_mutex_locker_new (&device->state_mutex); + if (device->state == DEVICE_STATE_FATAL_ERROR || + device->state == DEVICE_STATE_IN_SHUTDOWN) + return CHANNEL_RC_OK; + + if (device->state != DEVICE_STATE_PENDING_MEDIA_TYPE_LIST_RESPONSE) + { + g_warning ("[RDP.CAM_DEVICE] Protocol violation: Received stray media " + "type list response in state %s for device \"%s\". " + "Removing device...", + device_state_to_string (device->state), device->device_name); + transition_into_fatal_error_state (device); + return CHANNEL_RC_OK; + } + if (media_type_list_response->N_Descriptions < 1) + { + g_warning ("[RDP.CAM_DEVICE] Device \"%s\": Received invalid media type " + "list response. Removing device...", device->device_name); + transition_into_fatal_error_state (device); + return CHANNEL_RC_OK; + } + + stream_index = + GPOINTER_TO_UINT (g_queue_pop_head (device->pending_media_type_lists)); + if (!g_hash_table_lookup_extended (device->stream_contexts, + GUINT_TO_POINTER (stream_index), + NULL, (gpointer *) &stream_context)) + g_assert_not_reached (); + + for (i = 0; i < media_type_list_response->N_Descriptions; ++i) + { + CAM_MEDIA_TYPE_DESCRIPTION *media_type_description = + &media_type_list_response->MediaTypeDescriptions[i]; + + g_debug ("[RDP.CAM_DEVICE] Device \"%s\": Stream %u: MediaTypeList %zu: " + "Format: %u, Width: %u, Height: %u, FrameRateNumerator: %u, " + "FrameRateDenominator: %u, PixelAspectRatioNumerator: %u, " + "PixelAspectRatioDenominator: %u, Flags: 0x%08X", + device->device_name, stream_index, i, + media_type_description->Format, + media_type_description->Width, + media_type_description->Height, + media_type_description->FrameRateNumerator, + media_type_description->FrameRateDenominator, + media_type_description->PixelAspectRatioNumerator, + media_type_description->PixelAspectRatioDenominator, + media_type_description->Flags); + + stream_context->media_type_descriptions = + g_list_append (stream_context->media_type_descriptions, + g_memdup2 (media_type_description, + sizeof (CAM_MEDIA_TYPE_DESCRIPTION))); + } + + if (g_queue_get_length (device->pending_media_type_lists) > 0) + request_next_media_type_list (device); + else if (device_context->protocolVersion >= 2) + request_property_list (device); + else + start_preparing_streams (device); + + return CHANNEL_RC_OK; +} + +static uint32_t +device_current_media_type_response (CameraDeviceServerContext *device_context, + const CAM_CURRENT_MEDIA_TYPE_RESPONSE *current_media_type_response) +{ + return CHANNEL_RC_OK; +} + +static gboolean +has_pending_event_unlocked (GrdRdpDvcCameraDevice *device, + DeviceEvent event) +{ + return g_hash_table_contains (device->pending_events, + GUINT_TO_POINTER (event)); +} + +static gboolean +has_pending_event (GrdRdpDvcCameraDevice *device, + DeviceEvent event) +{ + g_autoptr (GMutexLocker) locker = NULL; + + locker = g_mutex_locker_new (&device->event_mutex); + return has_pending_event_unlocked (device, event); +} + +static uint32_t +device_sample_response (CameraDeviceServerContext *device_context, + const CAM_SAMPLE_RESPONSE *sample_response) +{ + GrdRdpDvcCameraDevice *device = device_context->userdata; + uint8_t stream_index = sample_response->StreamIndex; + g_autoptr (GMutexLocker) locker = NULL; + StreamContext *stream_context = NULL; + g_autoptr (GError) error = NULL; + GrdSampleBuffer *sample_buffer; + uint32_t n_pending_samples; + gboolean success = FALSE; + + locker = g_mutex_locker_new (&device->state_mutex); + if (device->state == DEVICE_STATE_FATAL_ERROR || + device->state == DEVICE_STATE_IN_SHUTDOWN) + return CHANNEL_RC_OK; + + if (!g_hash_table_lookup_extended (device->stream_contexts, + GUINT_TO_POINTER (stream_index), + NULL, (gpointer *) &stream_context)) + { + g_warning ("[RDP.CAM_DEVICE] Protocol violation: Received sample " + "response for unknown stream in state %s for device \"%s\". " + "Removing device...", + device_state_to_string (device->state), device->device_name); + transition_into_fatal_error_state (device); + return CHANNEL_RC_OK; + } + + sample_buffer = g_async_queue_try_pop (stream_context->pending_samples); + if (!sample_buffer) + { + g_warning ("[RDP.CAM_DEVICE] Protocol violation: Received stray sample " + "response for stream %u on device \"%s\". Ignoring...", + stream_index, device->device_name); + return CHANNEL_RC_OK; + } + + g_mutex_lock (&device->sample_request_mutex); + g_hash_table_remove (device->pending_samples, sample_buffer); + + n_pending_samples = g_hash_table_size (device->pending_samples); + g_mutex_unlock (&device->sample_request_mutex); + + if (grd_rdp_camera_stream_announce_new_sample (stream_context->camera_stream, + sample_buffer)) + { + success = grd_sample_buffer_load_sample (sample_buffer, + sample_response->Sample, + sample_response->SampleSize, + &error); + if (!success) + { + g_warning ("[RDP.CAM_DEVICE] Device \"%s\", stream %u: Failed to " + "load sample: %s ", device->device_name, stream_index, + error->message); + } + } + + grd_rdp_camera_stream_submit_sample (stream_context->camera_stream, + sample_buffer, success); + + if (n_pending_samples == 0 && + has_pending_event (device, DEVICE_EVENT_STOP_STREAMS)) + g_source_set_ready_time (device->event_source, 0); + + return CHANNEL_RC_OK; +} + +static uint32_t +device_sample_error_response (CameraDeviceServerContext *device_context, + const CAM_SAMPLE_ERROR_RESPONSE *sample_error_response) +{ + GrdRdpDvcCameraDevice *device = device_context->userdata; + uint8_t stream_index = sample_error_response->StreamIndex; + g_autoptr (GMutexLocker) locker = NULL; + StreamContext *stream_context = NULL; + GrdSampleBuffer *sample_buffer; + uint32_t n_pending_samples; + + locker = g_mutex_locker_new (&device->state_mutex); + if (device->state == DEVICE_STATE_FATAL_ERROR || + device->state == DEVICE_STATE_IN_SHUTDOWN) + return CHANNEL_RC_OK; + + if (!g_hash_table_lookup_extended (device->stream_contexts, + GUINT_TO_POINTER (stream_index), + NULL, (gpointer *) &stream_context)) + { + g_warning ("[RDP.CAM_DEVICE] Protocol violation: Received sample error " + "response \"%s\" for unknown stream in state %s for device " + "\"%s\". Removing device...", + sample_error_to_string (sample_error_response), + device_state_to_string (device->state), device->device_name); + transition_into_fatal_error_state (device); + return CHANNEL_RC_OK; + } + + sample_buffer = g_async_queue_try_pop (stream_context->pending_samples); + if (!sample_buffer) + { + g_warning ("[RDP.CAM_DEVICE] Protocol violation: Received stray sample " + "error response \"%s\" for stream %u on device \"%s\". " + "Ignoring...", sample_error_to_string (sample_error_response), + stream_index, device->device_name); + return CHANNEL_RC_OK; + } + + g_mutex_lock (&device->sample_request_mutex); + g_hash_table_remove (device->pending_samples, sample_buffer); + + n_pending_samples = g_hash_table_size (device->pending_samples); + g_mutex_unlock (&device->sample_request_mutex); + + g_warning ("[RDP.CAM_DEVICE] Device \"%s\", stream %u: Failed to retrieve " + "sample from client: %s ", device->device_name, stream_index, + sample_error_to_string (sample_error_response)); + + grd_rdp_camera_stream_submit_sample (stream_context->camera_stream, + sample_buffer, FALSE); + + if (n_pending_samples == 0 && + has_pending_event (device, DEVICE_EVENT_STOP_STREAMS)) + g_source_set_ready_time (device->event_source, 0); + + return CHANNEL_RC_OK; +} + +static void +request_next_property_value (GrdRdpDvcCameraDevice *device) +{ + CameraDeviceServerContext *device_context = device->device_context; + CAM_PROPERTY_VALUE_REQUEST *property_value_request; + + g_assert (g_queue_get_length (device->pending_property_values) > 0); + + property_value_request = g_queue_peek_head (device->pending_property_values); + + device->state = DEVICE_STATE_PENDING_PROPERTY_VALUE_RESPONSE; + device_context->PropertyValueRequest (device_context, property_value_request); +} + +static uint32_t +device_property_list_response (CameraDeviceServerContext *device_context, + const CAM_PROPERTY_LIST_RESPONSE *property_list_response) +{ + GrdRdpDvcCameraDevice *device = device_context->userdata; + g_autoptr (GMutexLocker) locker = NULL; + size_t i; + + locker = g_mutex_locker_new (&device->state_mutex); + if (device->state == DEVICE_STATE_FATAL_ERROR || + device->state == DEVICE_STATE_IN_SHUTDOWN) + return CHANNEL_RC_OK; + + if (device->state != DEVICE_STATE_PENDING_PROPERTY_LIST_RESPONSE) + { + g_warning ("[RDP.CAM_DEVICE] Protocol violation: Received stray " + "property list response in state %s for device \"%s\". " + "Removing device...", + device_state_to_string (device->state), device->device_name); + transition_into_fatal_error_state (device); + return CHANNEL_RC_OK; + } + + for (i = 0; i < property_list_response->N_Properties; ++i) + { + CAM_PROPERTY_DESCRIPTION *property = + &property_list_response->Properties[i]; + CAM_PROPERTY_VALUE_REQUEST *property_value_request; + + g_debug ("[RDP.CAM_DEVICE] Device \"%s\": Property %zu: " + "PropertySet: 0x%02X, PropertyId: 0x%02X, Capabilities: 0x%02X, " + "MinValue: %i, MaxValue: %i, Step: %i, DefaultValue: %i", + device->device_name, i, + property->PropertySet, property->PropertyId, + property->Capabilities, property->MinValue, property->MaxValue, + property->Step, property->DefaultValue); + + property_value_request = g_new0 (CAM_PROPERTY_VALUE_REQUEST, 1); + property_value_request->PropertySet = property->PropertySet; + property_value_request->PropertyId = property->PropertyId; + + g_queue_push_tail (device->pending_property_values, + property_value_request); + } + + if (g_queue_get_length (device->pending_property_values) > 0) + request_next_property_value (device); + else + start_preparing_streams (device); + + return CHANNEL_RC_OK; +} + +static uint32_t +device_property_value_response (CameraDeviceServerContext *device_context, + const CAM_PROPERTY_VALUE_RESPONSE *property_value_response) +{ + GrdRdpDvcCameraDevice *device = device_context->userdata; + const CAM_PROPERTY_VALUE *property_value = + &property_value_response->PropertyValue; + g_autoptr (GMutexLocker) locker = NULL; + g_autofree CAM_PROPERTY_VALUE_REQUEST *property_value_request = NULL; + + locker = g_mutex_locker_new (&device->state_mutex); + if (device->state == DEVICE_STATE_FATAL_ERROR || + device->state == DEVICE_STATE_IN_SHUTDOWN) + return CHANNEL_RC_OK; + + if (device->state != DEVICE_STATE_PENDING_PROPERTY_VALUE_RESPONSE) + { + g_warning ("[RDP.CAM_DEVICE] Protocol violation: Received stray " + "property value response in state %s for device \"%s\". " + "Removing device...", + device_state_to_string (device->state), device->device_name); + transition_into_fatal_error_state (device); + return CHANNEL_RC_OK; + } + + property_value_request = g_queue_pop_head (device->pending_property_values); + g_debug ("[RDP.CAM_DEVICE] Device \"%s\": PropertySet: 0x%02X, " + "PropertyId: 0x%02X, Mode: 0x%02X, Value: %i", + device->device_name, property_value_request->PropertySet, + property_value_request->PropertyId, + property_value->Mode, property_value->Value); + + if (g_queue_get_length (device->pending_property_values) > 0) + request_next_property_value (device); + else + start_preparing_streams (device); + + return CHANNEL_RC_OK; +} + +GrdRdpDvcCameraDevice * +grd_rdp_dvc_camera_device_new (GrdRdpDvcHandler *dvc_handler, + HANDLE vcm, + rdpContext *rdp_context, + uint8_t protocol_version, + const char *dvc_name, + const char *device_name) +{ + g_autoptr (GrdRdpDvcCameraDevice) device = NULL; + CameraDeviceServerContext *device_context; + + device = g_object_new (GRD_TYPE_RDP_DVC_CAMERA_DEVICE, NULL); + device_context = camera_device_server_context_new (vcm); + if (!device_context) + g_error ("[RDP.CAM_DEVICE] Failed to allocate server context (OOM)"); + + device->device_context = device_context; + device->dvc_name = g_strdup (dvc_name); + device->device_name = g_strdup (device_name); + + grd_rdp_dvc_initialize_base (GRD_RDP_DVC (device), + dvc_handler, NULL, + GRD_RDP_CHANNEL_CAMERA); + + device_context->virtualChannelName = g_strdup (dvc_name); + device_context->protocolVersion = protocol_version; + + device_context->ChannelIdAssigned = device_channel_id_assigned; + device_context->SuccessResponse = device_success_response; + device_context->ErrorResponse = device_error_response; + device_context->StreamListResponse = device_stream_list_response; + device_context->MediaTypeListResponse = device_media_type_list_response; + device_context->CurrentMediaTypeResponse = device_current_media_type_response; + device_context->SampleResponse = device_sample_response; + device_context->SampleErrorResponse = device_sample_error_response; + device_context->PropertyListResponse = device_property_list_response; + device_context->PropertyValueResponse = device_property_value_response; + device_context->rdpcontext = rdp_context; + device_context->userdata = device; + + /* Check whether camera thread was able to successfully initialize PipeWire */ + if (!grd_sync_point_wait_for_completion (&device->sync_point)) + return NULL; + + /* + * The DRDYNVC virtual channel is already initialized here, + * as the enumerator channel also uses it + */ + if (device_context->Open (device_context)) + { + g_warning ("[RDP.CAM_DEVICE] Device \"%s\": Failed to open the %s " + "channel. Terminating protocol", device_name, dvc_name); + return NULL; + } + device->channel_opened = TRUE; + + return g_steal_pointer (&device); +} + +static gboolean +tear_down_streams (gpointer user_data) +{ + GrdRdpDvcCameraDevice *device = user_data; + + g_hash_table_remove_all (device->stream_contexts); + if (device->sample_timeout_source) + { + g_source_destroy (device->sample_timeout_source); + g_clear_pointer (&device->sample_timeout_source, g_source_unref); + } + + grd_sync_point_complete (&device->sync_point, TRUE); + + return G_SOURCE_REMOVE; +} + +static gboolean +source_dispatch (GSource *source, + GSourceFunc callback, + gpointer user_data) +{ + g_source_set_ready_time (source, -1); + + return callback (user_data); +} + +static GSourceFuncs source_funcs = +{ + .dispatch = source_dispatch, +}; + +static void +destroy_camera_streams_in_camera_thread (GrdRdpDvcCameraDevice *device) +{ + GSource *stream_teardown_source; + + g_mutex_lock (&device->state_mutex); + device->state = DEVICE_STATE_IN_SHUTDOWN; + g_mutex_unlock (&device->state_mutex); + + grd_sync_point_reset (&device->sync_point); + + stream_teardown_source = g_source_new (&source_funcs, sizeof (GSource)); + g_source_set_callback (stream_teardown_source, tear_down_streams, + device, NULL); + g_source_set_ready_time (stream_teardown_source, 0); + g_source_attach (stream_teardown_source, device->camera_context); + g_source_unref (stream_teardown_source); + + grd_sync_point_wait_for_completion (&device->sync_point); +} + +static void +stop_camera_thread (GrdRdpDvcCameraDevice *device) +{ + destroy_camera_streams_in_camera_thread (device); + + device->in_shutdown = TRUE; + + g_main_context_wakeup (device->camera_context); + g_clear_pointer (&device->camera_thread, g_thread_join); +} + +static void +grd_rdp_dvc_camera_device_dispose (GObject *object) +{ + GrdRdpDvcCameraDevice *device = GRD_RDP_DVC_CAMERA_DEVICE (object); + GrdRdpDvc *dvc = GRD_RDP_DVC (device); + + if (device->camera_thread) + stop_camera_thread (device); + + g_assert (g_hash_table_size (device->stream_contexts) == 0); + g_assert (!device->sample_timeout_source); + + if (device->channel_opened) + { + device->device_context->Close (device->device_context); + device->channel_opened = FALSE; + } + grd_rdp_dvc_maybe_unsubscribe_creation_status (dvc); + + g_assert (!device->pipewire_core); + g_assert (!device->pipewire_context); + g_assert (!device->pipewire_source); + + if (device->event_source) + { + g_source_destroy (device->event_source); + g_clear_pointer (&device->event_source, g_source_unref); + } + if (device->initialization_source) + { + g_source_destroy (device->initialization_source); + g_clear_pointer (&device->initialization_source, g_source_unref); + } + + g_clear_pointer (&device->camera_context, g_main_context_unref); + + g_clear_pointer (&device->pending_samples, g_hash_table_unref); + g_clear_pointer (&device->pending_events, g_hash_table_unref); + g_clear_pointer (&device->running_streams, g_hash_table_unref); + g_clear_pointer (&device->pending_stream_starts, g_hash_table_unref); + g_clear_pointer (&device->queued_stream_starts, g_hash_table_unref); + + if (device->pending_property_values) + { + g_queue_free_full (device->pending_property_values, g_free); + device->pending_property_values = NULL; + } + g_clear_pointer (&device->pending_media_type_lists, g_queue_free); + + g_clear_pointer (&device->device_name, g_free); + g_clear_pointer (&device->dvc_name, g_free); + + g_clear_pointer (&device->device_context, camera_device_server_context_free); + + G_OBJECT_CLASS (grd_rdp_dvc_camera_device_parent_class)->dispose (object); +} + +static void +grd_rdp_dvc_camera_device_finalize (GObject *object) +{ + GrdRdpDvcCameraDevice *device = GRD_RDP_DVC_CAMERA_DEVICE (object); + + grd_sync_point_clear (&device->sync_point); + + g_mutex_clear (&device->sample_request_mutex); + g_mutex_clear (&device->client_request_mutex); + g_mutex_clear (&device->event_mutex); + g_mutex_clear (&device->state_mutex); + + g_assert (g_hash_table_size (device->stream_contexts) == 0); + g_clear_pointer (&device->stream_contexts, g_hash_table_unref); + + G_OBJECT_CLASS (grd_rdp_dvc_camera_device_parent_class)->finalize (object); +} + +static void +pipewire_core_error (void *user_data, + uint32_t id, + int seq, + int res, + const char *message) +{ + GrdRdpDvcCameraDevice *device = user_data; + + g_warning ("[RDP.CAM_DEVICE] Device \"%s\": PipeWire core error: " + "id: %u, seq: %i, res: %i, %s", + device->device_name, id, seq, res, message); + + if (id == PW_ID_CORE && res == -EPIPE) + g_signal_emit (device, signals[ERROR], 0); +} + +static const struct pw_core_events pipewire_core_events = +{ + .version = PW_VERSION_CORE_EVENTS, + .error = pipewire_core_error, +}; + +static gboolean +set_up_pipewire (GrdRdpDvcCameraDevice *device, + GError **error) +{ + GrdPipeWireSource *pipewire_source; + + pipewire_source = grd_pipewire_source_new ("RDP.CAM_DEVICE", error); + if (!pipewire_source) + return FALSE; + + device->pipewire_source = (GSource *) pipewire_source; + + device->pipewire_context = + pw_context_new (pipewire_source->pipewire_loop, NULL, 0); + if (!device->pipewire_context) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create PipeWire context"); + return FALSE; + } + + device->pipewire_core = + pw_context_connect (device->pipewire_context, NULL, 0); + if (!device->pipewire_core) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create PipeWire core"); + return FALSE; + } + + pw_core_add_listener (device->pipewire_core, + &device->pipewire_core_listener, + &pipewire_core_events, device); + + g_source_attach (device->pipewire_source, device->camera_context); + + return TRUE; +} + +static void +stop_pipewire (GrdRdpDvcCameraDevice *device) +{ + if (device->pipewire_core) + { + spa_hook_remove (&device->pipewire_core_listener); + g_clear_pointer (&device->pipewire_core, pw_core_disconnect); + } + g_clear_pointer (&device->pipewire_context, pw_context_destroy); + + if (device->pipewire_source) + { + g_source_destroy (device->pipewire_source); + g_clear_pointer (&device->pipewire_source, g_source_unref); + } +} + +static gpointer +camera_thread_func (gpointer data) +{ + GrdRdpDvcCameraDevice *device = data; + g_autoptr (GError) error = NULL; + gboolean success; + + pw_init (NULL, NULL); + + success = set_up_pipewire (device, &error); + if (!success) + { + g_warning ("[RDP.CAM_DEVICE] Device \"%s\": Failed to set up PipeWire: " + "%s", device->device_name, error->message); + } + grd_sync_point_complete (&device->sync_point, success); + + while (!device->in_shutdown) + g_main_context_iteration (device->camera_context, TRUE); + + stop_pipewire (device); + + pw_deinit (); + + return NULL; +} + +static void +activate_device (GrdRdpDvcCameraDevice *device) +{ + CameraDeviceServerContext *device_context = device->device_context; + CAM_ACTIVATE_DEVICE_REQUEST activate_device_request = {}; + + device->state = DEVICE_STATE_PENDING_ACTIVATION_RESPONSE; + device_context->ActivateDeviceRequest (device_context, + &activate_device_request); +} + +static gboolean +has_supported_media_type (GList *media_type_descriptions) +{ + GList *l; + + for (l = media_type_descriptions; l; l = l->next) + { + CAM_MEDIA_TYPE_DESCRIPTION *media_type_description = l->data; + + /* Sanitize format */ + if (media_type_description->FrameRateNumerator == 0 || + media_type_description->FrameRateDenominator == 0 || + media_type_description->PixelAspectRatioDenominator == 0) + continue; + + if (media_type_description->Format == CAM_MEDIA_FORMAT_H264) + return TRUE; + } + + return FALSE; +} + +static void +create_streams (GrdRdpDvcCameraDevice *device) +{ + StreamContext *stream_context = NULL; + gpointer key = NULL; + GHashTableIter iter; + + g_hash_table_iter_init (&iter, device->stream_contexts); + while (g_hash_table_iter_next (&iter, &key, (gpointer *) &stream_context)) + { + uint8_t stream_index = GPOINTER_TO_UINT (key); + g_autoptr (GError) error = NULL; + + if (!has_supported_media_type (stream_context->media_type_descriptions)) + continue; + + stream_context->camera_stream = + grd_rdp_camera_stream_new (device, + device->camera_context, + device->pipewire_core, + device->device_name, + stream_index, + stream_context->media_type_descriptions, + &error); + if (!stream_context->camera_stream) + { + g_warning ("[RDP.CAM_DEVICE] Device \"%s\": Failed to create camera " + "stream for stream %u: %s ", device->device_name, + stream_index, error->message); + } + } + + device->state = DEVICE_STATE_INITIALIZATION_DONE; +} + +static gboolean +initialize_device (gpointer user_data) +{ + GrdRdpDvcCameraDevice *device = user_data; + g_autoptr (GMutexLocker) locker = NULL; + + locker = g_mutex_locker_new (&device->state_mutex); + switch (device->state) + { + case DEVICE_STATE_FATAL_ERROR: + case DEVICE_STATE_PENDING_ACTIVATION_RESPONSE: + case DEVICE_STATE_PENDING_STREAM_LIST_RESPONSE: + case DEVICE_STATE_PENDING_MEDIA_TYPE_LIST_RESPONSE: + case DEVICE_STATE_PENDING_PROPERTY_LIST_RESPONSE: + case DEVICE_STATE_PENDING_PROPERTY_VALUE_RESPONSE: + case DEVICE_STATE_INITIALIZATION_DONE: + case DEVICE_STATE_IN_SHUTDOWN: + break; + case DEVICE_STATE_PENDING_ACTIVATION: + activate_device (device); + break; + case DEVICE_STATE_PENDING_STREAM_PREPARATION: + create_streams (device); + break; + } + + return G_SOURCE_CONTINUE; +} + +static gboolean +has_pending_ack_event (GrdRdpDvcCameraDevice *device) +{ + g_autoptr (GMutexLocker) locker = NULL; + + locker = g_mutex_locker_new (&device->event_mutex); + return has_pending_event_unlocked (device, DEVICE_EVENT_ACK_STARTED_STREAMS) || + has_pending_event_unlocked (device, DEVICE_EVENT_ACK_STOPPED_STREAMS); +} + +static gboolean +has_pending_client_success_response (GrdRdpDvcCameraDevice *device) +{ + return device->pending_client_request && !has_pending_ack_event (device); +} + +static void +set_pending_client_request (GrdRdpDvcCameraDevice *device, + ClientRequestType request_type) +{ + g_autoptr (GMutexLocker) locker = NULL; + + locker = g_mutex_locker_new (&device->client_request_mutex); + g_assert (!device->pending_client_request); + g_assert (device->pending_client_request_type == CLIENT_REQUEST_TYPE_NONE); + + device->pending_client_request_type = request_type; + device->pending_client_request = TRUE; +} + +static void +reset_pending_client_request (GrdRdpDvcCameraDevice *device) +{ + g_autoptr (GMutexLocker) locker = NULL; + + locker = g_mutex_locker_new (&device->client_request_mutex); + device->pending_client_request = FALSE; + device->pending_client_request_type = CLIENT_REQUEST_TYPE_NONE; +} + +static gboolean +try_pop_event (GrdRdpDvcCameraDevice *device, + DeviceEvent event) +{ + g_autoptr (GMutexLocker) locker = NULL; + + locker = g_mutex_locker_new (&device->event_mutex); + return g_hash_table_remove (device->pending_events, GUINT_TO_POINTER (event)); +} + +static void +ack_started_streams (GrdRdpDvcCameraDevice *device) +{ + StreamRunContext *stream_run_context = NULL; + gpointer key = NULL; + GHashTableIter iter; + + g_hash_table_iter_init (&iter, device->pending_stream_starts); + while (g_hash_table_iter_next (&iter, &key, (gpointer *) &stream_run_context)) + { + uint8_t stream_index = GPOINTER_TO_UINT (key); + StreamContext *stream_context = NULL; + GrdRdpCameraStream *camera_stream; + uint32_t run_sequence; + + if (!g_hash_table_lookup_extended (device->stream_contexts, + GUINT_TO_POINTER (stream_index), + NULL, (gpointer *) &stream_context)) + g_assert_not_reached (); + + camera_stream = stream_context->camera_stream; + run_sequence = stream_run_context->run_sequence; + + if (grd_rdp_camera_stream_notify_stream_started (camera_stream, + run_sequence)) + { + grd_rdp_camera_stream_uninhibit_camera_loop (camera_stream); + + g_hash_table_insert (device->running_streams, + GUINT_TO_POINTER (stream_index), + stream_run_context); + g_hash_table_iter_steal (&iter); + } + else + { + g_hash_table_iter_remove (&iter); + } + } + + g_debug ("[RDP.CAM_DEVICE] Device \"%s\": Started streams", + device->device_name); +} + +static void +ack_stopped_streams (GrdRdpDvcCameraDevice *device) +{ + g_debug ("[RDP.CAM_DEVICE] Device \"%s\": Stopped streams", + device->device_name); +} + +static void +maybe_handle_client_success_response (GrdRdpDvcCameraDevice *device) +{ + if (!has_pending_ack_event (device)) + return; + + reset_pending_client_request (device); + + if (try_pop_event (device, DEVICE_EVENT_ACK_STARTED_STREAMS)) + ack_started_streams (device); + if (try_pop_event (device, DEVICE_EVENT_ACK_STOPPED_STREAMS)) + ack_stopped_streams (device); +} + +static gboolean +has_pending_sample_requests (GrdRdpDvcCameraDevice *device) +{ + g_autoptr (GMutexLocker) locker = NULL; + + locker = g_mutex_locker_new (&device->sample_request_mutex); + return g_hash_table_size (device->pending_samples) > 0; +} + +static gboolean +discard_sample_requests (gpointer user_data) +{ + GrdRdpDvcCameraDevice *device = user_data; + StreamContext *stream_context = NULL; + GHashTableIter iter; + + g_warning ("[RDP.CAM_DEVICE] Device \"%s\": Protocol violation: Got no " + "sample response after timeout. Discarding requests...", + device->device_name); + + g_clear_pointer (&device->sample_timeout_source, g_source_unref); + + g_hash_table_iter_init (&iter, device->stream_contexts); + while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &stream_context)) + discard_pending_sample_requests (stream_context); + + g_source_set_ready_time (device->event_source, 0); + + return G_SOURCE_REMOVE; +} + +static void +maybe_set_sample_timeout (GrdRdpDvcCameraDevice *device) +{ + if (device->sample_timeout_source) + return; + + g_debug ("[RDP.CAM_DEVICE] Device \"%s\": Waiting for remaining samples " + "before sending stop-streams-request", device->device_name); + + device->sample_timeout_source = g_timeout_source_new (SAMPLE_TIMEOUT_MS); + g_source_set_callback (device->sample_timeout_source, discard_sample_requests, + device, NULL); + g_source_attach (device->sample_timeout_source, device->camera_context); +} + +static void +stop_camera_streams (GrdRdpDvcCameraDevice *device) +{ + CameraDeviceServerContext *device_context = device->device_context; + CAM_STOP_STREAMS_REQUEST stop_streams_request = {}; + + if (device->sample_timeout_source) + { + g_source_destroy (device->sample_timeout_source); + g_clear_pointer (&device->sample_timeout_source, g_source_unref); + } + + g_debug ("[RDP.CAM_DEVICE] Device \"%s\": Sending stop-streams-request", + device->device_name); + + set_pending_client_request (device, CLIENT_REQUEST_TYPE_STOP_STREAMS); + + device_context->StopStreamsRequest (device_context, &stop_streams_request); +} + +static void +start_queued_camera_streams (GrdRdpDvcCameraDevice *device) +{ + CameraDeviceServerContext *device_context = device->device_context; + CAM_START_STREAMS_REQUEST start_streams_request = {}; + StreamRunContext *stream_run_context = NULL; + gpointer key = NULL; + GHashTableIter iter; + uint16_t i = 0; + + g_assert (g_hash_table_size (device->pending_stream_starts) == 0); + g_assert (g_hash_table_size (device->queued_stream_starts) > 0); + + start_streams_request.N_Infos = + g_hash_table_size (device->queued_stream_starts); + + g_hash_table_iter_init (&iter, device->queued_stream_starts); + while (g_hash_table_iter_next (&iter, &key, (gpointer *) &stream_run_context)) + { + CAM_START_STREAM_INFO *start_stream_info; + + start_stream_info = &start_streams_request.StartStreamsInfo[i++]; + start_stream_info->StreamIndex = GPOINTER_TO_UINT (key); + start_stream_info->MediaTypeDescription = + *stream_run_context->media_type_description; + + g_hash_table_insert (device->pending_stream_starts, + key, stream_run_context); + g_hash_table_iter_steal (&iter); + } + g_assert (start_streams_request.N_Infos == i); + + g_debug ("[RDP.CAM_DEVICE] Device \"%s\": Sending start-streams-request", + device->device_name); + + set_pending_client_request (device, CLIENT_REQUEST_TYPE_START_STREAMS); + + device_context->StartStreamsRequest (device_context, &start_streams_request); +} + +static gboolean +handle_device_events (gpointer user_data) +{ + GrdRdpDvcCameraDevice *device = user_data; + + if (has_pending_client_success_response (device)) + return G_SOURCE_CONTINUE; + + maybe_handle_client_success_response (device); + + if (has_pending_sample_requests (device) && + has_pending_event (device, DEVICE_EVENT_STOP_STREAMS)) + maybe_set_sample_timeout (device); + + if (!has_pending_sample_requests (device) && + try_pop_event (device, DEVICE_EVENT_STOP_STREAMS)) + stop_camera_streams (device); + else if (g_hash_table_size (device->queued_stream_starts) > 0 && + !has_pending_event (device, DEVICE_EVENT_STOP_STREAMS)) + start_queued_camera_streams (device); + + return G_SOURCE_CONTINUE; +} + +static void +grd_rdp_dvc_camera_device_init (GrdRdpDvcCameraDevice *device) +{ + GSource *initialization_source; + GSource *event_source; + + device->state = DEVICE_STATE_PENDING_ACTIVATION; + + device->stream_contexts = + g_hash_table_new_full (NULL, NULL, + NULL, (GDestroyNotify) stream_context_free); + device->queued_stream_starts = + g_hash_table_new_full (NULL, NULL, + NULL, (GDestroyNotify) stream_run_context_free); + device->pending_stream_starts = + g_hash_table_new_full (NULL, NULL, + NULL, (GDestroyNotify) stream_run_context_free); + device->running_streams = + g_hash_table_new_full (NULL, NULL, + NULL, (GDestroyNotify) stream_run_context_free); + device->pending_events = g_hash_table_new (NULL, NULL); + device->pending_samples = g_hash_table_new (NULL, NULL); + + device->pending_media_type_lists = g_queue_new (); + device->pending_property_values = g_queue_new (); + + g_mutex_init (&device->state_mutex); + g_mutex_init (&device->event_mutex); + g_mutex_init (&device->client_request_mutex); + g_mutex_init (&device->sample_request_mutex); + + grd_sync_point_init (&device->sync_point); + + device->camera_context = g_main_context_new (); + device->camera_thread = g_thread_new ("RDP camera device thread", + camera_thread_func, + device); + + initialization_source = g_source_new (&source_funcs, sizeof (GSource)); + g_source_set_callback (initialization_source, initialize_device, + device, NULL); + g_source_set_ready_time (initialization_source, -1); + g_source_attach (initialization_source, device->camera_context); + device->initialization_source = initialization_source; + + event_source = g_source_new (&source_funcs, sizeof (GSource)); + g_source_set_callback (event_source, handle_device_events, device, NULL); + g_source_set_ready_time (event_source, -1); + g_source_attach (event_source, device->camera_context); + device->event_source = event_source; +} + +static void +grd_rdp_dvc_camera_device_class_init (GrdRdpDvcCameraDeviceClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = grd_rdp_dvc_camera_device_dispose; + object_class->finalize = grd_rdp_dvc_camera_device_finalize; + + signals[ERROR] = g_signal_new ("error", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, 0); +} diff --git a/grd-rdp-dvc-camera-device.h b/grd-rdp-dvc-camera-device.h new file mode 100644 index 0000000..f99701f --- /dev/null +++ b/grd-rdp-dvc-camera-device.h @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2025 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include + +#include "grd-rdp-dvc.h" +#include "grd-types.h" + +#define GRD_TYPE_RDP_DVC_CAMERA_DEVICE (grd_rdp_dvc_camera_device_get_type ()) +G_DECLARE_FINAL_TYPE (GrdRdpDvcCameraDevice, grd_rdp_dvc_camera_device, + GRD, RDP_DVC_CAMERA_DEVICE, GrdRdpDvc) + +GrdRdpDvcCameraDevice *grd_rdp_dvc_camera_device_new (GrdRdpDvcHandler *dvc_handler, + HANDLE vcm, + rdpContext *rdp_context, + uint8_t protocol_version, + const char *dvc_name, + const char *device_name); + +const char *grd_rdp_dvc_camera_device_get_dvc_name (GrdRdpDvcCameraDevice *device); + +void grd_rdp_dvc_camera_device_start_stream (GrdRdpDvcCameraDevice *device, + GrdRdpCameraStream *camera_stream, + CAM_MEDIA_TYPE_DESCRIPTION *media_type_description, + uint32_t run_sequence); + +void grd_rdp_dvc_camera_device_stop_stream (GrdRdpDvcCameraDevice *device, + GrdRdpCameraStream *camera_stream); + +void grd_rdp_dvc_camera_device_request_sample (GrdRdpDvcCameraDevice *device, + GrdRdpCameraStream *camera_stream, + GrdSampleBuffer *sample_buffer); diff --git a/grd-rdp-dvc-camera-enumerator.c b/grd-rdp-dvc-camera-enumerator.c new file mode 100644 index 0000000..7897a20 --- /dev/null +++ b/grd-rdp-dvc-camera-enumerator.c @@ -0,0 +1,517 @@ +/* + * Copyright (C) 2022 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-rdp-dvc-camera-enumerator.h" + +#include + +#include "grd-rdp-dvc-camera-device.h" + +#define SERVER_VERSION 2 +#define MAX_DVC_NAME_LEN 256 + +typedef struct _DeviceInfo +{ + char *device_name; + char *dvc_name; +} DeviceInfo; + +struct _GrdRdpDvcCameraEnumerator +{ + GrdRdpDvc parent; + + CamDevEnumServerContext *enumerator_context; + gboolean channel_opened; + + GrdRdpDvcHandler *dvc_handler; + + uint8_t protocol_version; + gboolean initialized; + + GHashTable *device_table; + + GSource *device_source; + GMutex device_queue_mutex; + GHashTable *devices_to_add; + GHashTable *devices_to_remove; +}; + +G_DEFINE_TYPE (GrdRdpDvcCameraEnumerator, grd_rdp_dvc_camera_enumerator, + GRD_TYPE_RDP_DVC) + +static void +grd_rdp_dvc_camera_enumerator_maybe_init (GrdRdpDvc *dvc) +{ + GrdRdpDvcCameraEnumerator *enumerator = GRD_RDP_DVC_CAMERA_ENUMERATOR (dvc); + CamDevEnumServerContext *enumerator_context; + + if (enumerator->channel_opened) + return; + + enumerator_context = enumerator->enumerator_context; + if (enumerator_context->Open (enumerator_context)) + { + g_warning ("[RDP.CAM_ENUMERATOR] Failed to open channel. " + "Terminating protocol"); + grd_rdp_dvc_queue_channel_tear_down (GRD_RDP_DVC (enumerator)); + return; + } + enumerator->channel_opened = TRUE; +} + +static void +dvc_creation_status_cb (gpointer user_data, + int32_t creation_status) +{ + GrdRdpDvcCameraEnumerator *enumerator = user_data; + + if (creation_status < 0) + { + g_debug ("[RDP.CAM_ENUMERATOR] Failed to open channel " + "(CreationStatus %i). Terminating protocol", creation_status); + grd_rdp_dvc_queue_channel_tear_down (GRD_RDP_DVC (enumerator)); + } +} + +static BOOL +enumerator_channel_id_assigned (CamDevEnumServerContext *enumerator_context, + uint32_t channel_id) +{ + GrdRdpDvcCameraEnumerator *enumerator = enumerator_context->userdata; + GrdRdpDvc *dvc = GRD_RDP_DVC (enumerator); + + g_debug ("[RDP.CAM_ENUMERATOR] DVC channel id assigned to id %u", channel_id); + + grd_rdp_dvc_subscribe_creation_status (dvc, channel_id, + dvc_creation_status_cb, + enumerator); + + return TRUE; +} + +static uint32_t +enumerator_select_version_request (CamDevEnumServerContext *enumerator_context, + const CAM_SELECT_VERSION_REQUEST *select_version_request) +{ + GrdRdpDvcCameraEnumerator *enumerator = enumerator_context->userdata; + CAM_SELECT_VERSION_RESPONSE select_version_response = {}; + + if (enumerator->initialized) + { + g_warning ("[RDP.CAM_ENUMERATOR] Protocol violation: Received version " + "request, but protocol is already initialized"); + grd_rdp_dvc_queue_channel_tear_down (GRD_RDP_DVC (enumerator)); + return CHANNEL_RC_INITIALIZATION_ERROR; + } + + g_debug ("[RDP.CAM_ENUMERATOR] Client supports protocol version %u", + select_version_request->Header.Version); + enumerator->protocol_version = MIN (SERVER_VERSION, + select_version_request->Header.Version); + enumerator->initialized = TRUE; + + select_version_response.Header.Version = enumerator->protocol_version; + select_version_response.Header.MessageId = CAM_MSG_ID_SelectVersionResponse; + + return enumerator_context->SelectVersionResponse (enumerator_context, + &select_version_response); +} + +static const char *reserved_svc_names[] = +{ + "encomsp", /* [MS-RDPEMC] */ + "CLIPRDR", /* [MS-RDPECLIP] */ + "DRDYNVC", /* [MS-RDPEDYC] */ + "RDPDR", /* [MS-RDPEFS] */ + "RDPSND", /* [MS-RDPEA] */ + "RAIL", /* [MS-RDPERP] */ +}; + +static const char *reserved_dvc_names[] = +{ + "AUDIO_INPUT", /* [MS-RDPEAI] */ + "AUDIO_PLAYBACK_DVC", /* [MS-RDPEA] */ + "AUDIO_PLAYBACK_LOSSY_DVC", /* [MS-RDPEA] */ + "dwmprox", /* [MS-RDPCR2] */ + "ECHO", /* [MS-RDPEECO] */ + "FileRedirectorChannel", /* [MS-RDPEPNP] */ + "Microsoft::Windows::RDS::AuthRedirection", /* [MS-RDPEAR] */ + "Microsoft::Windows::RDS::CoreInput", /* [MS-RDPECI] */ + "Microsoft::Windows::RDS::DisplayControl", /* [MS-RDPEDISP] */ + "Microsoft::Windows::RDS::Geometry::v08.01", /* [MS-RDPEGT] */ + "Microsoft::Windows::RDS::Graphics", /* [MS-RDPEGFX] */ + "Microsoft::Windows::RDS::Input", /* [MS-RDPEI] */ + "Microsoft::Windows::RDS::Location", /* [MS-RDPEL] */ + "Microsoft::Windows::RDS::MouseCursor", /* [MS-RDPEMSC] */ + "Microsoft::Windows::RDS::Telemetry", /* [MS-RDPET] */ + "Microsoft::Windows::RDS::Video::Control::v08.01", /* [MS-RDPEVOR] */ + "Microsoft::Windows::RDS::Video::Data::v08.01", /* [MS-RDPEVOR] */ + "PNPDR", /* [MS-RDPEPNP] */ + "RDCamera_Device_Enumerator", /* [MS-RDPECAM] */ + "TextInput_ServerToClientDVC", /* [MS-RDPETXT] */ + "TextInput_ClientToServerDVC", /* [MS-RDPETXT] */ + "TSMF", /* [MS-RDPEV] */ + "TSVCTKT", /* [MS-RDPEXPS] */ + "URBDRC", /* [MS-RDPEUSB] */ + "WebAuthN_Channel", /* [MS-RDPEWA] */ + "WMSAud", /* [MS-RDPADRV] */ + "WMSDL", /* [MS-RDPADRV] */ + "XPSRD", /* [MS-RDPEXPS] */ +}; + +static gboolean +is_vc_name_reserved (const char *dvc_name) +{ + size_t i; + + for (i = 0; i < G_N_ELEMENTS (reserved_svc_names); ++i) + { + if (strcmp (dvc_name, reserved_svc_names[i]) == 0) + return TRUE; + } + for (i = 0; i < G_N_ELEMENTS (reserved_dvc_names); ++i) + { + if (strcmp (dvc_name, reserved_dvc_names[i]) == 0) + return TRUE; + } + + return FALSE; +} + +static uint32_t +enumerator_device_added_notification (CamDevEnumServerContext *enumerator_context, + const CAM_DEVICE_ADDED_NOTIFICATION *device_added_notification) +{ + GrdRdpDvcCameraEnumerator *enumerator = enumerator_context->userdata; + g_autofree char *device_name = NULL; + DeviceInfo *info; + + if (!enumerator->initialized) + { + g_warning ("[RDP.CAM_ENUMERATOR] Protocol violation: Received device " + "added notification, but no protocol version was negotiated"); + grd_rdp_dvc_queue_channel_tear_down (GRD_RDP_DVC (enumerator)); + return CHANNEL_RC_INITIALIZATION_ERROR; + } + + /* + * The name of a device channel MUST be a null-terminated ANSI encoded + * character string containing a maximum of 256 characters. + */ + if (strlen (device_added_notification->VirtualChannelName) > MAX_DVC_NAME_LEN) + { + g_warning ("[RDP.CAM_ENUMERATOR] Protocol violation: Client tried to use " + "too long DVC name. Ignoring announced camera device"); + return CHANNEL_RC_OK; + } + if (is_vc_name_reserved (device_added_notification->VirtualChannelName)) + { + g_warning ("[RDP.CAM_ENUMERATOR] Client tried to use reserved DVC name " + "\"%s\". Ignoring announced camera device", + device_added_notification->VirtualChannelName); + return CHANNEL_RC_OK; + } + + device_name = ConvertWCharToUtf8Alloc (device_added_notification->DeviceName, + NULL); + if (!device_name) + { + g_warning ("[RDP.CAM_ENUMERATOR] Failed to convert device name. " + "Ignoring announced camera device"); + return CHANNEL_RC_OK; + } + + g_debug ("[RDP.CAM_ENUMERATOR] Client announced camera device: " + "DeviceName: \"%s\", DVC name: \"%s\"", + device_name, device_added_notification->VirtualChannelName); + + info = g_new0 (DeviceInfo, 1); + info->device_name = g_strdup (device_name); + info->dvc_name = g_strdup (device_added_notification->VirtualChannelName); + + g_mutex_lock (&enumerator->device_queue_mutex); + g_hash_table_remove (enumerator->devices_to_remove, info->dvc_name); + g_hash_table_insert (enumerator->devices_to_add, info->dvc_name, info); + g_mutex_unlock (&enumerator->device_queue_mutex); + + g_source_set_ready_time (enumerator->device_source, 0); + + return CHANNEL_RC_OK; +} + +static uint32_t +enumerator_device_removed_notification (CamDevEnumServerContext *enumerator_context, + const CAM_DEVICE_REMOVED_NOTIFICATION *device_removed_notification) +{ + GrdRdpDvcCameraEnumerator *enumerator = enumerator_context->userdata; + DeviceInfo *info; + + if (!enumerator->initialized) + { + g_warning ("[RDP.CAM_ENUMERATOR] Protocol violation: Received device " + "removed notification, but no protocol version was negotiated"); + grd_rdp_dvc_queue_channel_tear_down (GRD_RDP_DVC (enumerator)); + return CHANNEL_RC_INITIALIZATION_ERROR; + } + + /* + * The name of a device channel MUST be a null-terminated ANSI encoded + * character string containing a maximum of 256 characters. + */ + if (strlen (device_removed_notification->VirtualChannelName) > MAX_DVC_NAME_LEN) + { + g_warning ("[RDP.CAM_ENUMERATOR] Protocol violation: Client tried to use " + "too long DVC name. Ignoring announced camera device"); + return CHANNEL_RC_OK; + } + + g_debug ("[RDP.CAM_ENUMERATOR] Client removed camera device " + "(channel name: \"%s\")", + device_removed_notification->VirtualChannelName); + + info = g_new0 (DeviceInfo, 1); + info->dvc_name = g_strdup (device_removed_notification->VirtualChannelName); + + g_mutex_lock (&enumerator->device_queue_mutex); + g_hash_table_remove (enumerator->devices_to_add, info->dvc_name); + g_hash_table_insert (enumerator->devices_to_remove, info->dvc_name, info); + g_mutex_unlock (&enumerator->device_queue_mutex); + + g_source_set_ready_time (enumerator->device_source, 0); + + return CHANNEL_RC_OK; +} + +GrdRdpDvcCameraEnumerator * +grd_rdp_dvc_camera_enumerator_new (GrdSessionRdp *session_rdp, + GrdRdpDvcHandler *dvc_handler, + HANDLE vcm, + rdpContext *rdp_context) +{ + GrdRdpDvcCameraEnumerator *enumerator; + CamDevEnumServerContext *enumerator_context; + + enumerator = g_object_new (GRD_TYPE_RDP_DVC_CAMERA_ENUMERATOR, NULL); + enumerator_context = cam_dev_enum_server_context_new (vcm); + if (!enumerator_context) + g_error ("[RDP.CAM_ENUMERATOR] Failed to allocate server context (OOM)"); + + enumerator->enumerator_context = enumerator_context; + enumerator->dvc_handler = dvc_handler; + + grd_rdp_dvc_initialize_base (GRD_RDP_DVC (enumerator), + dvc_handler, session_rdp, + GRD_RDP_CHANNEL_CAMERA_ENUMERATOR); + + enumerator_context->ChannelIdAssigned = enumerator_channel_id_assigned; + enumerator_context->SelectVersionRequest = enumerator_select_version_request; + enumerator_context->DeviceAddedNotification = enumerator_device_added_notification; + enumerator_context->DeviceRemovedNotification = enumerator_device_removed_notification; + enumerator_context->rdpcontext = rdp_context; + enumerator_context->userdata = enumerator; + + return enumerator; +} + +static void +grd_rdp_dvc_camera_enumerator_dispose (GObject *object) +{ + GrdRdpDvcCameraEnumerator *enumerator = + GRD_RDP_DVC_CAMERA_ENUMERATOR (object); + GrdRdpDvc *dvc = GRD_RDP_DVC (enumerator); + + g_hash_table_remove_all (enumerator->device_table); + + if (enumerator->channel_opened) + { + enumerator->enumerator_context->Close (enumerator->enumerator_context); + enumerator->channel_opened = FALSE; + } + grd_rdp_dvc_maybe_unsubscribe_creation_status (dvc); + + if (enumerator->device_source) + { + g_source_destroy (enumerator->device_source); + g_clear_pointer (&enumerator->device_source, g_source_unref); + } + + g_clear_pointer (&enumerator->enumerator_context, + cam_dev_enum_server_context_free); + + G_OBJECT_CLASS (grd_rdp_dvc_camera_enumerator_parent_class)->dispose (object); +} + +static void +grd_rdp_dvc_camera_enumerator_finalize (GObject *object) +{ + GrdRdpDvcCameraEnumerator *enumerator = + GRD_RDP_DVC_CAMERA_ENUMERATOR (object); + + g_mutex_clear (&enumerator->device_queue_mutex); + + g_clear_pointer (&enumerator->devices_to_remove, g_hash_table_unref); + g_clear_pointer (&enumerator->devices_to_add, g_hash_table_unref); + + g_assert (g_hash_table_size (enumerator->device_table) == 0); + g_clear_pointer (&enumerator->device_table, g_hash_table_unref); + + G_OBJECT_CLASS (grd_rdp_dvc_camera_enumerator_parent_class)->finalize (object); +} + +static void +device_info_free (gpointer data) +{ + DeviceInfo *info = data; + + g_clear_pointer (&info->dvc_name, g_free); + g_clear_pointer (&info->device_name, g_free); + + g_free (info); +} + +static gboolean +remove_camera_device (gpointer key, + gpointer value, + gpointer user_data) +{ + GrdRdpDvcCameraEnumerator *enumerator = user_data; + DeviceInfo *info = value; + + g_hash_table_remove (enumerator->device_table, info->dvc_name); + + return TRUE; +} + +static void +on_device_error (GrdRdpDvcCameraDevice *device, + GrdRdpDvcCameraEnumerator *enumerator) +{ + DeviceInfo *info; + + /* Fake a Device Removed Notification on error */ + info = g_new0 (DeviceInfo, 1); + info->dvc_name = g_strdup (grd_rdp_dvc_camera_device_get_dvc_name (device)); + + g_mutex_lock (&enumerator->device_queue_mutex); + g_hash_table_remove (enumerator->devices_to_add, info->dvc_name); + g_hash_table_insert (enumerator->devices_to_remove, info->dvc_name, info); + g_mutex_unlock (&enumerator->device_queue_mutex); + + g_source_set_ready_time (enumerator->device_source, 0); +} + +static gboolean +add_camera_device (gpointer key, + gpointer value, + gpointer user_data) +{ + GrdRdpDvcCameraEnumerator *enumerator = user_data; + CamDevEnumServerContext *enumerator_context = enumerator->enumerator_context; + DeviceInfo *info = value; + GrdRdpDvcCameraDevice *device; + + /* Camera may be re-added by client */ + g_hash_table_remove (enumerator->device_table, info->dvc_name); + + device = grd_rdp_dvc_camera_device_new (enumerator->dvc_handler, + enumerator_context->vcm, + enumerator_context->rdpcontext, + enumerator->protocol_version, + info->dvc_name, + info->device_name); + if (!device) + return TRUE; + + g_signal_connect (device, "error", + G_CALLBACK (on_device_error), + enumerator); + + g_hash_table_insert (enumerator->device_table, + g_steal_pointer (&info->dvc_name), device); + + return TRUE; +} + +static gboolean +manage_devices (gpointer user_data) +{ + GrdRdpDvcCameraEnumerator *enumerator = user_data; + + g_mutex_lock (&enumerator->device_queue_mutex); + g_hash_table_foreach_remove (enumerator->devices_to_remove, + remove_camera_device, enumerator); + g_hash_table_foreach_remove (enumerator->devices_to_add, + add_camera_device, enumerator); + g_mutex_unlock (&enumerator->device_queue_mutex); + + return G_SOURCE_CONTINUE; +} + +static gboolean +device_source_dispatch (GSource *source, + GSourceFunc callback, + gpointer user_data) +{ + g_source_set_ready_time (source, -1); + + return callback (user_data); +} + +static GSourceFuncs device_source_funcs = +{ + .dispatch = device_source_dispatch, +}; + +static void +grd_rdp_dvc_camera_enumerator_init (GrdRdpDvcCameraEnumerator *enumerator) +{ + GSource *device_source; + + enumerator->device_table = + g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, g_object_unref); + enumerator->devices_to_add = + g_hash_table_new_full (g_str_hash, g_str_equal, + NULL, device_info_free); + enumerator->devices_to_remove = + g_hash_table_new_full (g_str_hash, g_str_equal, + NULL, device_info_free); + + g_mutex_init (&enumerator->device_queue_mutex); + + device_source = g_source_new (&device_source_funcs, sizeof (GSource)); + g_source_set_callback (device_source, manage_devices, enumerator, NULL); + g_source_set_ready_time (device_source, -1); + g_source_attach (device_source, NULL); + enumerator->device_source = device_source; +} + +static void +grd_rdp_dvc_camera_enumerator_class_init (GrdRdpDvcCameraEnumeratorClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GrdRdpDvcClass *dvc_class = GRD_RDP_DVC_CLASS (klass); + + object_class->dispose = grd_rdp_dvc_camera_enumerator_dispose; + object_class->finalize = grd_rdp_dvc_camera_enumerator_finalize; + + dvc_class->maybe_init = grd_rdp_dvc_camera_enumerator_maybe_init; +} diff --git a/grd-rdp-dvc-camera-enumerator.h b/grd-rdp-dvc-camera-enumerator.h new file mode 100644 index 0000000..e05f1d4 --- /dev/null +++ b/grd-rdp-dvc-camera-enumerator.h @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2022 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include "grd-rdp-dvc.h" + +#define GRD_TYPE_RDP_DVC_CAMERA_ENUMERATOR (grd_rdp_dvc_camera_enumerator_get_type ()) +G_DECLARE_FINAL_TYPE (GrdRdpDvcCameraEnumerator, grd_rdp_dvc_camera_enumerator, + GRD, RDP_DVC_CAMERA_ENUMERATOR, GrdRdpDvc) + +GrdRdpDvcCameraEnumerator *grd_rdp_dvc_camera_enumerator_new (GrdSessionRdp *session_rdp, + GrdRdpDvcHandler *dvc_handler, + HANDLE vcm, + rdpContext *rdp_context); diff --git a/grd-rdp-dvc-display-control.c b/grd-rdp-dvc-display-control.c new file mode 100644 index 0000000..6af9ac1 --- /dev/null +++ b/grd-rdp-dvc-display-control.c @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2021-2023 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-rdp-dvc-display-control.h" + +#include + +#include "grd-rdp-layout-manager.h" +#include "grd-rdp-monitor-config.h" +#include "grd-session-rdp.h" + +struct _GrdRdpDvcDisplayControl +{ + GrdRdpDvc parent; + + DispServerContext *disp_context; + gboolean channel_opened; + gboolean pending_caps; + + GrdRdpLayoutManager *layout_manager; + GrdSessionRdp *session_rdp; +}; + +G_DEFINE_TYPE (GrdRdpDvcDisplayControl, grd_rdp_dvc_display_control, + GRD_TYPE_RDP_DVC) + +static void +grd_rdp_dvc_display_control_maybe_init (GrdRdpDvc *dvc) +{ + GrdRdpDvcDisplayControl *display_control = GRD_RDP_DVC_DISPLAY_CONTROL (dvc); + DispServerContext *disp_context; + + if (display_control->channel_opened) + return; + + disp_context = display_control->disp_context; + if (disp_context->Open (disp_context)) + { + g_warning ("[RDP.DISP] Failed to open channel. Terminating protocol"); + grd_rdp_dvc_queue_channel_tear_down (GRD_RDP_DVC (display_control)); + return; + } + display_control->channel_opened = TRUE; +} + +static void +dvc_creation_status (gpointer user_data, + int32_t creation_status) +{ + GrdRdpDvcDisplayControl *display_control = user_data; + DispServerContext *disp_context = display_control->disp_context; + + if (creation_status < 0) + { + g_debug ("[RDP.DISP] Failed to open channel (CreationStatus %i). " + "Terminating protocol", creation_status); + grd_rdp_dvc_queue_channel_tear_down (GRD_RDP_DVC (display_control)); + return; + } + + g_debug ("[RDP.DISP] Channel opened successfully. " + "Ready for receiving new monitor layouts"); + display_control->pending_caps = FALSE; + disp_context->DisplayControlCaps (disp_context); +} + +static BOOL +disp_channel_id_assigned (DispServerContext *disp_context, + uint32_t channel_id) +{ + GrdRdpDvcDisplayControl *display_control = disp_context->custom; + GrdRdpDvc *dvc = GRD_RDP_DVC (display_control); + + g_debug ("[RDP.DISP] DVC channel id assigned to id %u", channel_id); + + grd_rdp_dvc_subscribe_creation_status (dvc, channel_id, + dvc_creation_status, + display_control); + + return TRUE; +} + +static uint32_t +disp_monitor_layout (DispServerContext *disp_context, + const DISPLAY_CONTROL_MONITOR_LAYOUT_PDU *monitor_layout_pdu) +{ + GrdRdpDvcDisplayControl *display_control = disp_context->custom; + GrdSessionRdp *session_rdp = display_control->session_rdp; + GrdRdpLayoutManager *layout_manager = display_control->layout_manager; + GrdRdpMonitorConfig *monitor_config; + g_autoptr (GError) error = NULL; + + if (display_control->pending_caps) + { + g_warning ("[RDP.DISP] Protocol violation: Received monitor layout PDU " + "before being able to send capabilities. Terminating session"); + grd_session_rdp_notify_error (session_rdp, + GRD_SESSION_RDP_ERROR_BAD_MONITOR_DATA); + return CHANNEL_RC_OK; + } + + if (monitor_layout_pdu->NumMonitors > disp_context->MaxNumMonitors) + { + g_warning ("[RDP.DISP] Protocol violation: Invalid monitor layout: " + "(exceeded maximum monitor count). Terminating session"); + grd_session_rdp_notify_error (session_rdp, + GRD_SESSION_RDP_ERROR_BAD_MONITOR_DATA); + return CHANNEL_RC_OK; + } + + monitor_config = + grd_rdp_monitor_config_new_from_disp_monitor_layout (monitor_layout_pdu, + &error); + if (!monitor_config) + { + g_warning ("[RDP.DISP] Received invalid monitor layout from client: %s. " + "Terminating session", error->message); + grd_session_rdp_notify_error (session_rdp, + GRD_SESSION_RDP_ERROR_BAD_MONITOR_DATA); + return CHANNEL_RC_OK; + } + + g_debug ("[RDP.DISP] Received new monitor layout. PDU contains %u monitor%s", + monitor_layout_pdu->NumMonitors, + monitor_layout_pdu->NumMonitors == 1 ? "" : "s"); + + grd_rdp_layout_manager_submit_new_monitor_config (layout_manager, + monitor_config); + + return CHANNEL_RC_OK; +} + +GrdRdpDvcDisplayControl * +grd_rdp_dvc_display_control_new (GrdRdpLayoutManager *layout_manager, + GrdSessionRdp *session_rdp, + GrdRdpDvcHandler *dvc_handler, + HANDLE vcm, + uint32_t max_monitor_count) +{ + GrdRdpDvcDisplayControl *display_control; + DispServerContext *disp_context; + + display_control = g_object_new (GRD_TYPE_RDP_DVC_DISPLAY_CONTROL, NULL); + disp_context = disp_server_context_new (vcm); + if (!disp_context) + g_error ("[RDP.DISP] Failed to create server context"); + + display_control->disp_context = disp_context; + display_control->layout_manager = layout_manager; + display_control->session_rdp = session_rdp; + + grd_rdp_dvc_initialize_base (GRD_RDP_DVC (display_control), + dvc_handler, session_rdp, + GRD_RDP_CHANNEL_DISPLAY_CONTROL); + + disp_context->MaxNumMonitors = max_monitor_count; + disp_context->MaxMonitorAreaFactorA = 8192; + disp_context->MaxMonitorAreaFactorB = 8192; + + disp_context->ChannelIdAssigned = disp_channel_id_assigned; + disp_context->DispMonitorLayout = disp_monitor_layout; + disp_context->custom = display_control; + + return display_control; +} + +static void +grd_rdp_dvc_display_control_dispose (GObject *object) +{ + GrdRdpDvcDisplayControl *display_control = + GRD_RDP_DVC_DISPLAY_CONTROL (object); + GrdRdpDvc *dvc = GRD_RDP_DVC (display_control); + + if (display_control->channel_opened) + { + display_control->disp_context->Close (display_control->disp_context); + display_control->channel_opened = FALSE; + } + grd_rdp_dvc_maybe_unsubscribe_creation_status (dvc); + + g_clear_pointer (&display_control->disp_context, disp_server_context_free); + + G_OBJECT_CLASS (grd_rdp_dvc_display_control_parent_class)->dispose (object); +} + +static void +grd_rdp_dvc_display_control_init (GrdRdpDvcDisplayControl *display_control) +{ + display_control->pending_caps = TRUE; +} + +static void +grd_rdp_dvc_display_control_class_init (GrdRdpDvcDisplayControlClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GrdRdpDvcClass *dvc_class = GRD_RDP_DVC_CLASS (klass); + + object_class->dispose = grd_rdp_dvc_display_control_dispose; + + dvc_class->maybe_init = grd_rdp_dvc_display_control_maybe_init; +} diff --git a/grd-rdp-dvc-display-control.h b/grd-rdp-dvc-display-control.h new file mode 100644 index 0000000..41e3f94 --- /dev/null +++ b/grd-rdp-dvc-display-control.h @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2021 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include "grd-rdp-dvc.h" + +#define GRD_TYPE_RDP_DVC_DISPLAY_CONTROL (grd_rdp_dvc_display_control_get_type ()) +G_DECLARE_FINAL_TYPE (GrdRdpDvcDisplayControl, grd_rdp_dvc_display_control, + GRD, RDP_DVC_DISPLAY_CONTROL, GrdRdpDvc) + +GrdRdpDvcDisplayControl *grd_rdp_dvc_display_control_new (GrdRdpLayoutManager *layout_manager, + GrdSessionRdp *session_rdp, + GrdRdpDvcHandler *dvc_handler, + HANDLE vcm, + uint32_t max_monitor_count); diff --git a/grd-rdp-dvc-graphics-pipeline.c b/grd-rdp-dvc-graphics-pipeline.c new file mode 100644 index 0000000..54cc438 --- /dev/null +++ b/grd-rdp-dvc-graphics-pipeline.c @@ -0,0 +1,2287 @@ +/* + * Copyright (C) 2021 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-rdp-dvc-graphics-pipeline.h" + +#include +#include +#include + +#include "grd-avc-frame-info.h" +#include "grd-bitstream.h" +#include "grd-hwaccel-nvidia.h" +#include "grd-rdp-damage-detector.h" +#include "grd-rdp-frame.h" +#include "grd-rdp-frame-info.h" +#include "grd-rdp-gfx-frame-controller.h" +#include "grd-rdp-gfx-surface.h" +#include "grd-rdp-legacy-buffer.h" +#include "grd-rdp-network-autodetection.h" +#include "grd-rdp-render-context.h" +#include "grd-rdp-renderer.h" +#include "grd-rdp-surface.h" +#include "grd-rdp-surface-renderer.h" +#include "grd-session-rdp.h" +#include "grd-utils.h" + +#define PROTOCOL_TIMEOUT_MS (10 * 1000) + +#define ENC_TIMES_CHECK_INTERVAL_MS 1000 +#define MAX_TRACKED_ENC_FRAMES 1000 +#define MIN_BW_MEASURE_SIZE (10 * 1024) + +typedef enum _HwAccelAPI +{ + HW_ACCEL_API_NONE = 0, + HW_ACCEL_API_NVENC = 1 << 0, +} HwAccelAPI; + +typedef struct _HWAccelContext +{ + HwAccelAPI api; + uint32_t encode_session_id; + gboolean has_first_frame; +} HWAccelContext; + +typedef struct _GfxSurfaceContext +{ + GrdRdpGfxSurface *gfx_surface; + uint64_t ref_count; +} GfxSurfaceContext; + +typedef struct _GfxFrameInfo +{ + GrdRdpFrameInfo frame_info; + uint32_t surface_serial; +} GfxFrameInfo; + +struct _GrdRdpDvcGraphicsPipeline +{ + GrdRdpDvc parent; + + RdpgfxServerContext *rdpgfx_context; + gboolean channel_opened; + gboolean channel_unavailable; + gboolean received_first_cap_sets; + gboolean initialized; + uint32_t initial_version; + + GrdSessionRdp *session_rdp; + GrdRdpRenderer *renderer; + GrdRdpNetworkAutodetection *network_autodetection; + wStream *encode_stream; + RFX_CONTEXT *rfx_context; + + GSource *protocol_timeout_source; + + GSource *protocol_reset_source; + GMutex caps_mutex; + RDPGFX_CAPSET *cap_sets; + uint16_t n_cap_sets; + + GMutex gfx_mutex; + GHashTable *surface_table; + GHashTable *codec_context_table; + + /* Unacknowledged Frames ADM element ([MS-RDPEGFX] 3.2.1.2) */ + GHashTable *frame_serial_table; + + GHashTable *serial_surface_table; + gboolean frame_acks_suspended; + + GQueue *encoded_frames; + uint32_t total_frames_encoded; + + GSource *rtt_pause_source; + GQueue *enc_times; + + GHashTable *surface_hwaccel_table; + GrdHwAccelNvidia *hwaccel_nvidia; + + unsigned long gfx_initable_id; + + uint32_t next_frame_id; + uint16_t next_surface_id; + uint32_t next_serial; +}; + +G_DEFINE_TYPE (GrdRdpDvcGraphicsPipeline, grd_rdp_dvc_graphics_pipeline, + GRD_TYPE_RDP_DVC) + +static gboolean +initiate_session_teardown (gpointer user_data) +{ + GrdRdpDvcGraphicsPipeline *graphics_pipeline = user_data; + + g_warning ("[RDP.RDPGFX] Client did not respond to protocol initiation. " + "Terminating session"); + + g_clear_pointer (&graphics_pipeline->protocol_timeout_source, g_source_unref); + + grd_session_rdp_notify_error (graphics_pipeline->session_rdp, + GRD_SESSION_RDP_ERROR_BAD_CAPS); + + return G_SOURCE_REMOVE; +} + +static void +grd_rdp_dvc_graphics_pipeline_maybe_init (GrdRdpDvc *dvc) +{ + GrdRdpDvcGraphicsPipeline *graphics_pipeline = + GRD_RDP_DVC_GRAPHICS_PIPELINE (dvc); + GMainContext *graphics_context = + grd_rdp_renderer_get_graphics_context (graphics_pipeline->renderer); + RdpgfxServerContext *rdpgfx_context; + + if (graphics_pipeline->channel_opened || + graphics_pipeline->channel_unavailable) + return; + + rdpgfx_context = graphics_pipeline->rdpgfx_context; + if (!rdpgfx_context->Open (rdpgfx_context)) + { + g_warning ("[RDP.RDPGFX] Failed to open channel. Terminating session"); + graphics_pipeline->channel_unavailable = TRUE; + grd_session_rdp_notify_error (graphics_pipeline->session_rdp, + GRD_SESSION_RDP_ERROR_GRAPHICS_SUBSYSTEM_FAILED); + return; + } + graphics_pipeline->channel_opened = TRUE; + + g_assert (!graphics_pipeline->protocol_timeout_source); + + graphics_pipeline->protocol_timeout_source = + g_timeout_source_new (PROTOCOL_TIMEOUT_MS); + g_source_set_callback (graphics_pipeline->protocol_timeout_source, + initiate_session_teardown, graphics_pipeline, NULL); + g_source_attach (graphics_pipeline->protocol_timeout_source, + graphics_context); +} + +void +grd_rdp_dvc_graphics_pipeline_get_capabilities (GrdRdpDvcGraphicsPipeline *graphics_pipeline, + gboolean *have_avc444, + gboolean *have_avc420) +{ + RdpgfxServerContext *rdpgfx_context = graphics_pipeline->rdpgfx_context; + rdpSettings *rdp_settings = rdpgfx_context->rdpcontext->settings; + + *have_avc444 = freerdp_settings_get_bool (rdp_settings, FreeRDP_GfxAVC444v2); + *have_avc420 = freerdp_settings_get_bool (rdp_settings, FreeRDP_GfxH264); +} + +void +grd_rdp_dvc_graphics_pipeline_set_hwaccel_nvidia (GrdRdpDvcGraphicsPipeline *graphics_pipeline, + GrdHwAccelNvidia *hwaccel_nvidia) +{ + graphics_pipeline->hwaccel_nvidia = hwaccel_nvidia; +} + +static uint16_t +get_next_free_surface_id (GrdRdpDvcGraphicsPipeline *graphics_pipeline) +{ + uint16_t surface_id = graphics_pipeline->next_surface_id; + + g_mutex_lock (&graphics_pipeline->gfx_mutex); + while (g_hash_table_contains (graphics_pipeline->surface_table, + GUINT_TO_POINTER (surface_id))) + ++surface_id; + g_mutex_unlock (&graphics_pipeline->gfx_mutex); + + graphics_pipeline->next_surface_id = surface_id + 1; + + return surface_id; +} + +static uint32_t +get_next_free_serial (GrdRdpDvcGraphicsPipeline *graphics_pipeline) +{ + uint32_t serial = graphics_pipeline->next_serial; + + g_mutex_lock (&graphics_pipeline->gfx_mutex); + while (g_hash_table_contains (graphics_pipeline->serial_surface_table, + GUINT_TO_POINTER (serial))) + ++serial; + g_mutex_unlock (&graphics_pipeline->gfx_mutex); + + graphics_pipeline->next_serial = serial + 1; + + return serial; +} + +void +grd_rdp_dvc_graphics_pipeline_create_surface (GrdRdpDvcGraphicsPipeline *graphics_pipeline, + GrdRdpGfxSurface *gfx_surface) +{ + RdpgfxServerContext *rdpgfx_context = graphics_pipeline->rdpgfx_context; + rdpSettings *rdp_settings = rdpgfx_context->rdpcontext->settings; + RDPGFX_CREATE_SURFACE_PDU create_surface = {0}; + GrdRdpSurface *rdp_surface = grd_rdp_gfx_surface_get_rdp_surface (gfx_surface); + GrdRdpSurfaceRenderer *surface_renderer = + grd_rdp_surface_get_surface_renderer (rdp_surface); + uint32_t refresh_rate = + grd_rdp_surface_renderer_get_refresh_rate (surface_renderer); + uint16_t surface_id = grd_rdp_gfx_surface_get_surface_id (gfx_surface); + uint32_t surface_serial = grd_rdp_gfx_surface_get_serial (gfx_surface); + uint16_t surface_width = grd_rdp_gfx_surface_get_width (gfx_surface); + uint16_t surface_height = grd_rdp_gfx_surface_get_height (gfx_surface); + GfxSurfaceContext *surface_context; + gboolean needs_separate_render_surface = FALSE; + HWAccelContext *hwaccel_context; + uint32_t encode_session_id; + uint16_t aligned_width; + uint16_t aligned_height; + + g_debug ("[RDP.RDPGFX] Creating surface with id %u", surface_id); + + surface_context = g_malloc0 (sizeof (GfxSurfaceContext)); + + g_mutex_lock (&graphics_pipeline->gfx_mutex); + g_hash_table_insert (graphics_pipeline->surface_table, + GUINT_TO_POINTER (surface_id), gfx_surface); + + surface_context->gfx_surface = gfx_surface; + g_hash_table_insert (graphics_pipeline->serial_surface_table, + GUINT_TO_POINTER (surface_serial), surface_context); + + if (!grd_rdp_gfx_surface_disallows_hwaccel_sessions (gfx_surface) && + (freerdp_settings_get_bool (rdp_settings, FreeRDP_GfxAVC444v2) || + freerdp_settings_get_bool (rdp_settings, FreeRDP_GfxAVC444) || + freerdp_settings_get_bool (rdp_settings, FreeRDP_GfxH264)) && + graphics_pipeline->hwaccel_nvidia && + grd_hwaccel_nvidia_create_nvenc_session (graphics_pipeline->hwaccel_nvidia, + &encode_session_id, + surface_width, surface_height, + &aligned_width, &aligned_height, + refresh_rate)) + { + uint16_t aligned_width_16; + uint16_t aligned_height_16; + + g_debug ("[RDP.RDPGFX] Created NVENC session for surface %u", surface_id); + + aligned_width_16 = grd_get_aligned_size (surface_width, 16); + aligned_height_16 = grd_get_aligned_size (surface_height, 16); + if (aligned_width != aligned_width_16 || aligned_height != aligned_height_16) + needs_separate_render_surface = TRUE; + + hwaccel_context = g_malloc0 (sizeof (HWAccelContext)); + hwaccel_context->api = HW_ACCEL_API_NVENC; + hwaccel_context->encode_session_id = encode_session_id; + + g_hash_table_insert (graphics_pipeline->surface_hwaccel_table, + GUINT_TO_POINTER (surface_id), hwaccel_context); + + rdp_surface->needs_no_local_data = TRUE; + } + g_mutex_unlock (&graphics_pipeline->gfx_mutex); + + create_surface.surfaceId = surface_id; + create_surface.width = surface_width; + create_surface.height = surface_height; + create_surface.pixelFormat = GFX_PIXEL_FORMAT_XRGB_8888; + + rdpgfx_context->CreateSurface (rdpgfx_context, &create_surface); + + if (needs_separate_render_surface) + { + g_autoptr (GrdRdpGfxSurface) render_surface = NULL; + GrdRdpGfxSurfaceDescriptor surface_descriptor = {}; + + surface_descriptor.flags = GRD_RDP_GFX_SURFACE_FLAG_ALIGNED_SIZE | + GRD_RDP_GFX_SURFACE_FLAG_NO_HWACCEL_SESSIONS; + surface_descriptor.surface_id = get_next_free_surface_id (graphics_pipeline); + surface_descriptor.serial = get_next_free_serial (graphics_pipeline); + surface_descriptor.rdp_surface = rdp_surface; + + surface_descriptor.aligned_width = aligned_width; + surface_descriptor.aligned_height = aligned_height; + + g_debug ("[RDP.RDPGFX] Creating separate render surface (id %u) for " + "surface %u", surface_descriptor.surface_id, surface_id); + + render_surface = grd_rdp_gfx_surface_new (graphics_pipeline, + &surface_descriptor); + grd_rdp_gfx_surface_override_render_surface (gfx_surface, + g_steal_pointer (&render_surface)); + } +} + +void +grd_rdp_dvc_graphics_pipeline_delete_surface (GrdRdpDvcGraphicsPipeline *graphics_pipeline, + GrdRdpGfxSurface *gfx_surface) +{ + RdpgfxServerContext *rdpgfx_context = graphics_pipeline->rdpgfx_context; + RDPGFX_DELETE_ENCODING_CONTEXT_PDU delete_encoding_context = {0}; + RDPGFX_DELETE_SURFACE_PDU delete_surface = {0}; + GrdRdpSurface *rdp_surface = grd_rdp_gfx_surface_get_rdp_surface (gfx_surface); + gboolean needs_encoding_context_deletion = FALSE; + GfxSurfaceContext *surface_context; + HWAccelContext *hwaccel_context; + uint16_t surface_id; + uint32_t codec_context_id; + uint32_t surface_serial; + + if (!graphics_pipeline->channel_opened) + return; + + surface_id = grd_rdp_gfx_surface_get_surface_id (gfx_surface); + codec_context_id = grd_rdp_gfx_surface_get_codec_context_id (gfx_surface); + surface_serial = grd_rdp_gfx_surface_get_serial (gfx_surface); + + g_debug ("[RDP.RDPGFX] Deleting surface with id %u", surface_id); + + g_mutex_lock (&graphics_pipeline->gfx_mutex); + g_assert (g_hash_table_contains (graphics_pipeline->surface_table, + GUINT_TO_POINTER (surface_id))); + + if (!g_hash_table_lookup_extended (graphics_pipeline->serial_surface_table, + GUINT_TO_POINTER (surface_serial), + NULL, (gpointer *) &surface_context)) + g_assert_not_reached (); + + surface_context->gfx_surface = NULL; + if (surface_context->ref_count == 0) + { + g_hash_table_remove (graphics_pipeline->serial_surface_table, + GUINT_TO_POINTER (surface_serial)); + } + + if (g_hash_table_steal_extended (graphics_pipeline->surface_hwaccel_table, + GUINT_TO_POINTER (surface_id), + NULL, (gpointer *) &hwaccel_context)) + { + g_debug ("[RDP.RDPGFX] Destroying NVENC session for surface %u", surface_id); + rdp_surface->needs_no_local_data = FALSE; + + g_assert (hwaccel_context->api == HW_ACCEL_API_NVENC); + grd_hwaccel_nvidia_free_nvenc_session (graphics_pipeline->hwaccel_nvidia, + hwaccel_context->encode_session_id); + g_free (hwaccel_context); + } + + if (g_hash_table_steal_extended (graphics_pipeline->codec_context_table, + GUINT_TO_POINTER (codec_context_id), + NULL, NULL)) + needs_encoding_context_deletion = TRUE; + + g_hash_table_remove (graphics_pipeline->surface_table, + GUINT_TO_POINTER (surface_id)); + g_mutex_unlock (&graphics_pipeline->gfx_mutex); + + if (needs_encoding_context_deletion) + { + delete_encoding_context.surfaceId = surface_id; + delete_encoding_context.codecContextId = codec_context_id; + + rdpgfx_context->DeleteEncodingContext (rdpgfx_context, + &delete_encoding_context); + } + + delete_surface.surfaceId = surface_id; + + rdpgfx_context->DeleteSurface (rdpgfx_context, &delete_surface); +} + +static void +map_surface_to_output (GrdRdpDvcGraphicsPipeline *graphics_pipeline, + GrdRdpGfxSurface *gfx_surface, + GrdRdpSurfaceMapping *surface_mapping) +{ + RdpgfxServerContext *rdpgfx_context = graphics_pipeline->rdpgfx_context; + GrdRdpSurfaceMappingType mapping_type = surface_mapping->mapping_type; + uint16_t surface_id = grd_rdp_gfx_surface_get_surface_id (gfx_surface); + RDPGFX_MAP_SURFACE_TO_OUTPUT_PDU map_surface_to_output = {}; + + g_assert (mapping_type == GRD_RDP_SURFACE_MAPPING_TYPE_MAP_TO_OUTPUT); + + map_surface_to_output.surfaceId = surface_id; + map_surface_to_output.outputOriginX = surface_mapping->output_origin_x; + map_surface_to_output.outputOriginY = surface_mapping->output_origin_y; + + rdpgfx_context->MapSurfaceToOutput (rdpgfx_context, &map_surface_to_output); +} + +static void +map_surface (GrdRdpDvcGraphicsPipeline *graphics_pipeline, + GrdRdpGfxSurface *gfx_surface) +{ + GrdRdpSurface *rdp_surface = grd_rdp_gfx_surface_get_rdp_surface (gfx_surface); + GrdRdpSurfaceMapping *surface_mapping; + + surface_mapping = grd_rdp_surface_get_mapping (rdp_surface); + switch (surface_mapping->mapping_type) + { + case GRD_RDP_SURFACE_MAPPING_TYPE_MAP_TO_OUTPUT: + map_surface_to_output (graphics_pipeline, gfx_surface, surface_mapping); + break; + } +} + +GrdRdpGfxSurface * +grd_rdp_dvc_graphics_pipeline_acquire_gfx_surface (GrdRdpDvcGraphicsPipeline *graphics_pipeline, + GrdRdpSurface *rdp_surface) +{ + + GrdRdpGfxSurfaceDescriptor surface_descriptor = {}; + GrdRdpGfxSurface *gfx_surface; + GrdRdpGfxFrameController *frame_controller; + + surface_descriptor.surface_id = get_next_free_surface_id (graphics_pipeline); + surface_descriptor.serial = get_next_free_serial (graphics_pipeline); + surface_descriptor.rdp_surface = rdp_surface; + + gfx_surface = grd_rdp_gfx_surface_new (graphics_pipeline, + &surface_descriptor); + + frame_controller = grd_rdp_gfx_frame_controller_new (rdp_surface); + grd_rdp_gfx_surface_attach_frame_controller (gfx_surface, frame_controller); + map_surface (graphics_pipeline, gfx_surface); + + return gfx_surface; +} + +void +grd_rdp_dvc_graphics_pipeline_reset_graphics (GrdRdpDvcGraphicsPipeline *graphics_pipeline, + uint32_t width, + uint32_t height, + MONITOR_DEF *monitors, + uint32_t n_monitors) +{ + RdpgfxServerContext *rdpgfx_context = graphics_pipeline->rdpgfx_context; + RDPGFX_RESET_GRAPHICS_PDU reset_graphics = {0}; + + g_debug ("[RDP.RDPGFX] Resetting graphics"); + + g_mutex_lock (&graphics_pipeline->gfx_mutex); + g_assert (g_hash_table_size (graphics_pipeline->surface_table) == 0); + g_mutex_unlock (&graphics_pipeline->gfx_mutex); + + /* + * width and height refer here to the size of the Graphics Output Buffer + * ADM (Abstract Data Model) element + */ + reset_graphics.width = width; + reset_graphics.height = height; + reset_graphics.monitorCount = n_monitors; + reset_graphics.monitorDefArray = monitors; + + rdpgfx_context->ResetGraphics (rdpgfx_context, &reset_graphics); +} + +void +grd_rdp_dvc_graphics_pipeline_notify_new_round_trip_time (GrdRdpDvcGraphicsPipeline *graphics_pipeline, + uint64_t round_trip_time_us) +{ + GrdRdpGfxSurface *gfx_surface; + GrdRdpGfxFrameController *frame_controller; + GHashTableIter iter; + + g_mutex_lock (&graphics_pipeline->gfx_mutex); + g_hash_table_iter_init (&iter, graphics_pipeline->surface_table); + while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &gfx_surface)) + { + frame_controller = grd_rdp_gfx_surface_get_frame_controller (gfx_surface); + if (!frame_controller) + continue; + + grd_rdp_gfx_frame_controller_notify_new_round_trip_time (frame_controller, + round_trip_time_us); + } + g_mutex_unlock (&graphics_pipeline->gfx_mutex); +} + +static uint32_t +get_next_free_frame_id (GrdRdpDvcGraphicsPipeline *graphics_pipeline) +{ + uint32_t frame_id = graphics_pipeline->next_frame_id; + + g_mutex_lock (&graphics_pipeline->gfx_mutex); + while (g_hash_table_contains (graphics_pipeline->frame_serial_table, + GUINT_TO_POINTER (frame_id))) + ++frame_id; + g_mutex_unlock (&graphics_pipeline->gfx_mutex); + + graphics_pipeline->next_frame_id = frame_id + 1; + + return frame_id; +} + +static uint16_t +get_rdpgfx_codec_id (GrdRdpCodec codec) +{ + switch (codec) + { + case GRD_RDP_CODEC_CAPROGRESSIVE: + return RDPGFX_CODECID_CAPROGRESSIVE; + case GRD_RDP_CODEC_AVC420: + return RDPGFX_CODECID_AVC420; + case GRD_RDP_CODEC_AVC444v2: + return RDPGFX_CODECID_AVC444v2; + } + + g_assert_not_reached (); +} + +static void +set_region_rects (RDPGFX_H264_METABLOCK *avc_meta, + cairo_region_t *region) +{ + int n_rects; + int i; + + avc_meta->numRegionRects = n_rects = cairo_region_num_rectangles (region); + avc_meta->regionRects = g_new0 (RECTANGLE_16, n_rects); + for (i = 0; i < n_rects; ++i) + { + cairo_rectangle_int_t cairo_rect; + + cairo_region_get_rectangle (region, i, &cairo_rect); + + avc_meta->regionRects[i].left = cairo_rect.x; + avc_meta->regionRects[i].top = cairo_rect.y; + avc_meta->regionRects[i].right = cairo_rect.x + cairo_rect.width; + avc_meta->regionRects[i].bottom = cairo_rect.y + cairo_rect.height; + } +} + +static void +set_avc_info (RDPGFX_H264_METABLOCK *avc_meta, + GrdBitstream *bitstream, + int n_blocks) +{ + GrdAVCFrameInfo *avc_frame_info = + grd_bitstream_get_avc_frame_info (bitstream); + int i = 0; + + avc_meta->quantQualityVals = g_new0 (RDPGFX_H264_QUANT_QUALITY, n_blocks); + for (i = 0; i < n_blocks; ++i) + { + avc_meta->quantQualityVals[i].qp = + grd_avc_frame_info_get_qp (avc_frame_info); + avc_meta->quantQualityVals[i].p = + grd_avc_frame_info_get_frame_type (avc_frame_info) == GRD_AVC_FRAME_TYPE_P; + avc_meta->quantQualityVals[i].qualityVal = + grd_avc_frame_info_get_quality_value (avc_frame_info); + } +} + +static void +prepare_avc420_bitstream (RDPGFX_AVC420_BITMAP_STREAM *avc420, + cairo_region_t *region, + GrdBitstream *bitstream) +{ + RDPGFX_H264_METABLOCK *avc_meta = &avc420->meta; + int n_rects; + + n_rects = cairo_region_num_rectangles (region); + + avc420->data = grd_bitstream_get_data (bitstream); + avc420->length = grd_bitstream_get_data_size (bitstream); + + set_region_rects (avc_meta, region); + set_avc_info (avc_meta, bitstream, n_rects); +} + +static void +prepare_avc444_bitstream (RDPGFX_AVC444_BITMAP_STREAM *avc444, + GrdRdpFrame *rdp_frame) +{ + GList *bitstreams = grd_rdp_frame_get_bitstreams (rdp_frame); + cairo_region_t *region = grd_rdp_frame_get_damage_region (rdp_frame); + GrdRdpFrameViewType view_type = grd_rdp_frame_get_avc_view_type (rdp_frame); + GrdBitstream *bitstream0 = NULL; + GrdBitstream *bitstream1 = NULL; + int n_rects; + + g_assert (bitstreams); + g_assert (g_list_length (bitstreams) >= 1); + + bitstream0 = bitstreams->data; + if (bitstreams->next) + bitstream1 = bitstreams->next->data; + + switch (view_type) + { + case GRD_RDP_FRAME_VIEW_TYPE_DUAL: + g_assert (g_list_length (bitstreams) == 2); + avc444->LC = 0; + break; + case GRD_RDP_FRAME_VIEW_TYPE_MAIN: + g_assert (g_list_length (bitstreams) == 1); + avc444->LC = 1; + break; + case GRD_RDP_FRAME_VIEW_TYPE_AUX: + g_assert (g_list_length (bitstreams) == 1); + avc444->LC = 2; + break; + } + + prepare_avc420_bitstream (&avc444->bitstream[0], region, bitstream0); + if (bitstream1) + prepare_avc420_bitstream (&avc444->bitstream[1], region, bitstream1); + + switch (view_type) + { + case GRD_RDP_FRAME_VIEW_TYPE_DUAL: + case GRD_RDP_FRAME_VIEW_TYPE_MAIN: + n_rects = cairo_region_num_rectangles (region); + + /* RFX_AVC420_METABLOCK size + bitstream0 size */ + avc444->cbAvc420EncodedBitstream1 = sizeof (uint32_t) + n_rects * 10 + + avc444->bitstream[0].length; + break; + case GRD_RDP_FRAME_VIEW_TYPE_AUX: + /* + * If no YUV420 frame is present, then this field MUST be set to zero. + * ([MS-RDPEGFX] 2.2.4.6) + * + * The auxiliary view is referred to as Chroma420 frame. + */ + avc444->cbAvc420EncodedBitstream1 = 0; + break; + } +} + +static void +prepare_avc_update (RDPGFX_SURFACE_COMMAND *cmd, + RDPGFX_AVC444_BITMAP_STREAM *avc444, + GrdRdpFrame *rdp_frame) +{ + GrdRdpRenderContext *render_context = + grd_rdp_frame_get_render_context (rdp_frame); + GrdRdpCodec codec = grd_rdp_render_context_get_codec (render_context); + cairo_region_t *region = grd_rdp_frame_get_damage_region (rdp_frame); + GList *bitstreams = grd_rdp_frame_get_bitstreams (rdp_frame); + cairo_rectangle_int_t region_extents = {}; + GrdBitstream *bitstream0; + + g_assert (bitstreams); + g_assert (g_list_length (bitstreams) >= 1); + + bitstream0 = bitstreams->data; + + cairo_region_get_extents (region, ®ion_extents); + + cmd->left = 0; + cmd->top = 0; + cmd->right = region_extents.x + region_extents.width; + cmd->bottom = region_extents.y + region_extents.height; + + switch (codec) + { + case GRD_RDP_CODEC_CAPROGRESSIVE: + g_assert_not_reached (); + break; + case GRD_RDP_CODEC_AVC420: + prepare_avc420_bitstream (&avc444->bitstream[0], region, bitstream0); + cmd->extra = &avc444->bitstream[0]; + break; + case GRD_RDP_CODEC_AVC444v2: + prepare_avc444_bitstream (avc444, rdp_frame); + cmd->extra = avc444; + break; + } +} + +static uint32_t +get_subframe_count (GrdRdpFrame *rdp_frame) +{ + GrdRdpRenderContext *render_context = + grd_rdp_frame_get_render_context (rdp_frame); + GrdRdpCodec codec = grd_rdp_render_context_get_codec (render_context); + GrdRdpFrameViewType view_type; + + if (codec != GRD_RDP_CODEC_AVC444v2) + return 1; + + view_type = grd_rdp_frame_get_avc_view_type (rdp_frame); + + return view_type == GRD_RDP_FRAME_VIEW_TYPE_DUAL ? 2 : 1; +} + +static void +surface_serial_ref (GrdRdpDvcGraphicsPipeline *graphics_pipeline, + uint32_t surface_serial) +{ + GfxSurfaceContext *surface_context; + + if (!g_hash_table_lookup_extended (graphics_pipeline->serial_surface_table, + GUINT_TO_POINTER (surface_serial), + NULL, (gpointer *) &surface_context)) + g_assert_not_reached (); + + ++surface_context->ref_count; +} + +static void +surface_serial_unref (GrdRdpDvcGraphicsPipeline *graphics_pipeline, + uint32_t surface_serial) +{ + GfxSurfaceContext *surface_context; + + if (!g_hash_table_lookup_extended (graphics_pipeline->serial_surface_table, + GUINT_TO_POINTER (surface_serial), + NULL, (gpointer *) &surface_context)) + g_assert_not_reached (); + + g_assert (surface_context->ref_count > 0); + --surface_context->ref_count; + + if (!surface_context->gfx_surface && surface_context->ref_count == 0) + { + g_hash_table_remove (graphics_pipeline->serial_surface_table, + GUINT_TO_POINTER (surface_serial)); + } +} + +static void +gfx_frame_info_free (GrdRdpDvcGraphicsPipeline *graphics_pipeline, + GfxFrameInfo *gfx_frame_info) +{ + uint32_t surface_serial = gfx_frame_info->surface_serial; + + g_hash_table_remove (graphics_pipeline->frame_serial_table, + GUINT_TO_POINTER (gfx_frame_info->frame_info.frame_id)); + surface_serial_unref (graphics_pipeline, surface_serial); + + g_free (gfx_frame_info); +} + +static void +reduce_tracked_frame_infos (GrdRdpDvcGraphicsPipeline *graphics_pipeline, + uint32_t max_tracked_frames) +{ + while (g_queue_peek_head (graphics_pipeline->encoded_frames) && + g_queue_get_length (graphics_pipeline->encoded_frames) > max_tracked_frames) + { + gfx_frame_info_free (graphics_pipeline, + g_queue_pop_head (graphics_pipeline->encoded_frames)); + } +} + +static void +enqueue_tracked_frame_info (GrdRdpDvcGraphicsPipeline *graphics_pipeline, + uint32_t surface_serial, + uint32_t frame_id, + uint32_t n_subframes, + int64_t enc_time_us) +{ + GfxFrameInfo *gfx_frame_info; + + g_assert (MAX_TRACKED_ENC_FRAMES > 1); + reduce_tracked_frame_infos (graphics_pipeline, MAX_TRACKED_ENC_FRAMES - 1); + + gfx_frame_info = g_malloc0 (sizeof (GfxFrameInfo)); + gfx_frame_info->frame_info.frame_id = frame_id; + gfx_frame_info->frame_info.n_subframes = n_subframes; + gfx_frame_info->frame_info.enc_time_us = enc_time_us; + gfx_frame_info->surface_serial = surface_serial; + + g_queue_push_tail (graphics_pipeline->encoded_frames, gfx_frame_info); +} + +static uint32_t +get_frame_size (GrdRdpFrame *rdp_frame) +{ + GList *bitstreams = grd_rdp_frame_get_bitstreams (rdp_frame); + uint32_t frame_size = 0; + + while (bitstreams) + { + GrdBitstream *bitstream = bitstreams->data; + + frame_size += grd_bitstream_get_data_size (bitstream); + bitstreams = bitstreams->next; + } + + return frame_size; +} + +static void +blit_surface_to_surface (GrdRdpDvcGraphicsPipeline *graphics_pipeline, + GrdRdpGfxSurface *dst_surface, + GrdRdpGfxSurface *src_surface, + RECTANGLE_16 *region_rects, + int n_rects) +{ + RdpgfxServerContext *rdpgfx_context = graphics_pipeline->rdpgfx_context; + RDPGFX_SURFACE_TO_SURFACE_PDU surface_to_surface = {}; + RDPGFX_POINT16 dst_point = {}; + int i; + + surface_to_surface.surfaceIdSrc = + grd_rdp_gfx_surface_get_surface_id (src_surface); + surface_to_surface.surfaceIdDest = + grd_rdp_gfx_surface_get_surface_id (dst_surface); + + for (i = 0; i < n_rects; ++i) + { + dst_point.x = region_rects[i].left; + dst_point.y = region_rects[i].top; + + surface_to_surface.rectSrc = region_rects[i]; + surface_to_surface.destPts = &dst_point; + surface_to_surface.destPtsCount = 1; + + rdpgfx_context->SurfaceToSurface (rdpgfx_context, &surface_to_surface); + } +} + +static void +clear_old_enc_times (GrdRdpDvcGraphicsPipeline *graphics_pipeline, + int64_t current_time_us) +{ + int64_t *tracked_enc_time_us; + + while ((tracked_enc_time_us = g_queue_peek_head (graphics_pipeline->enc_times)) && + current_time_us - *tracked_enc_time_us >= 1 * G_USEC_PER_SEC) + g_free (g_queue_pop_head (graphics_pipeline->enc_times)); +} + +static void +track_enc_time (GrdRdpDvcGraphicsPipeline *graphics_pipeline, + int64_t enc_time_us) +{ + int64_t *tracked_enc_time_us; + + tracked_enc_time_us = g_malloc0 (sizeof (int64_t)); + *tracked_enc_time_us = enc_time_us; + + g_queue_push_tail (graphics_pipeline->enc_times, tracked_enc_time_us); +} + +static gboolean +maybe_slow_down_rtts (gpointer user_data) +{ + GrdRdpDvcGraphicsPipeline *graphics_pipeline = user_data; + + g_mutex_lock (&graphics_pipeline->gfx_mutex); + clear_old_enc_times (graphics_pipeline, g_get_monotonic_time ()); + + if (g_queue_get_length (graphics_pipeline->enc_times) == 0) + { + grd_rdp_network_autodetection_set_rtt_consumer_necessity ( + graphics_pipeline->network_autodetection, + GRD_RDP_NW_AUTODETECT_RTT_CONSUMER_RDPGFX, + GRD_RDP_NW_AUTODETECT_RTT_NEC_LOW); + + g_clear_pointer (&graphics_pipeline->rtt_pause_source, g_source_unref); + g_mutex_unlock (&graphics_pipeline->gfx_mutex); + + return G_SOURCE_REMOVE; + } + g_mutex_unlock (&graphics_pipeline->gfx_mutex); + + return G_SOURCE_CONTINUE; +} + +static void +ensure_rtt_receivement (GrdRdpDvcGraphicsPipeline *graphics_pipeline) +{ + GMainContext *graphics_context = + grd_rdp_renderer_get_graphics_context (graphics_pipeline->renderer); + + g_assert (!graphics_pipeline->rtt_pause_source); + + grd_rdp_network_autodetection_set_rtt_consumer_necessity ( + graphics_pipeline->network_autodetection, + GRD_RDP_NW_AUTODETECT_RTT_CONSUMER_RDPGFX, + GRD_RDP_NW_AUTODETECT_RTT_NEC_HIGH); + + graphics_pipeline->rtt_pause_source = + g_timeout_source_new (ENC_TIMES_CHECK_INTERVAL_MS); + g_source_set_callback (graphics_pipeline->rtt_pause_source, maybe_slow_down_rtts, + graphics_pipeline, NULL); + g_source_attach (graphics_pipeline->rtt_pause_source, graphics_context); +} + +void +grd_rdp_dvc_graphics_pipeline_submit_frame (GrdRdpDvcGraphicsPipeline *graphics_pipeline, + GrdRdpFrame *rdp_frame) +{ + RdpgfxServerContext *rdpgfx_context = graphics_pipeline->rdpgfx_context; + rdpSettings *rdp_settings = rdpgfx_context->rdpcontext->settings; + GrdRdpNetworkAutodetection *network_autodetection = + graphics_pipeline->network_autodetection; + GrdRdpRenderContext *render_context = + grd_rdp_frame_get_render_context (rdp_frame); + GrdRdpGfxSurface *gfx_surface = + grd_rdp_render_context_get_gfx_surface (render_context); + GrdRdpGfxSurface *render_surface = + grd_rdp_gfx_surface_get_render_surface (gfx_surface); + GrdRdpGfxFrameController *frame_controller = + grd_rdp_gfx_surface_get_frame_controller (gfx_surface); + GrdRdpCodec codec = grd_rdp_render_context_get_codec (render_context); + uint32_t codec_context_id = + grd_rdp_gfx_surface_get_codec_context_id (gfx_surface); + GList *bitstreams = grd_rdp_frame_get_bitstreams (rdp_frame); + RDPGFX_START_FRAME_PDU cmd_start = {}; + RDPGFX_END_FRAME_PDU cmd_end = {}; + SYSTEMTIME system_time = {}; + RDPGFX_SURFACE_COMMAND cmd = {}; + RDPGFX_AVC444_BITMAP_STREAM avc444 = {}; + gboolean pending_bw_measure_stop = FALSE; + RECTANGLE_16 *region_rects = NULL; + int n_rects = 0; + GrdBitstream *bitstream0; + uint32_t surface_serial; + uint32_t n_subframes; + int64_t enc_ack_time_us; + + g_assert (bitstreams); + g_assert (g_list_length (bitstreams) >= 1); + + bitstream0 = bitstreams->data; + + GetSystemTime (&system_time); + cmd_start.timestamp = system_time.wHour << 22 | + system_time.wMinute << 16 | + system_time.wSecond << 10 | + system_time.wMilliseconds; + cmd_start.frameId = get_next_free_frame_id (graphics_pipeline); + cmd_end.frameId = cmd_start.frameId; + + cmd.surfaceId = grd_rdp_gfx_surface_get_surface_id (render_surface); + cmd.codecId = get_rdpgfx_codec_id (codec); + cmd.format = PIXEL_FORMAT_BGRX32; + + switch (codec) + { + case GRD_RDP_CODEC_CAPROGRESSIVE: + g_assert (g_list_length (bitstreams) == 1); + + cmd.contextId = codec_context_id; + cmd.length = grd_bitstream_get_data_size (bitstream0); + cmd.data = grd_bitstream_get_data (bitstream0); + break; + case GRD_RDP_CODEC_AVC420: + case GRD_RDP_CODEC_AVC444v2: + prepare_avc_update (&cmd, &avc444, rdp_frame); + region_rects = avc444.bitstream[0].meta.regionRects; + n_rects = avc444.bitstream[0].meta.numRegionRects; + break; + } + n_subframes = get_subframe_count (rdp_frame); + + g_mutex_lock (&graphics_pipeline->gfx_mutex); + if (codec == GRD_RDP_CODEC_CAPROGRESSIVE && + !g_hash_table_contains (graphics_pipeline->codec_context_table, + GUINT_TO_POINTER (codec_context_id))) + { + g_hash_table_insert (graphics_pipeline->codec_context_table, + GUINT_TO_POINTER (codec_context_id), gfx_surface); + } + + enc_ack_time_us = g_get_monotonic_time (); + grd_rdp_gfx_frame_controller_unack_frame (frame_controller, cmd_start.frameId, + n_subframes, enc_ack_time_us); + + surface_serial = grd_rdp_gfx_surface_get_serial (gfx_surface); + g_hash_table_insert (graphics_pipeline->frame_serial_table, + GUINT_TO_POINTER (cmd_start.frameId), + GUINT_TO_POINTER (surface_serial)); + surface_serial_ref (graphics_pipeline, surface_serial); + ++graphics_pipeline->total_frames_encoded; + + if (graphics_pipeline->frame_acks_suspended) + { + grd_rdp_gfx_frame_controller_ack_frame (frame_controller, cmd_start.frameId, + enc_ack_time_us); + enqueue_tracked_frame_info (graphics_pipeline, surface_serial, + cmd_start.frameId, n_subframes, + enc_ack_time_us); + } + g_mutex_unlock (&graphics_pipeline->gfx_mutex); + + if (network_autodetection && + get_frame_size (rdp_frame) >= MIN_BW_MEASURE_SIZE) + { + pending_bw_measure_stop = + grd_rdp_network_autodetection_try_bw_measure_start (network_autodetection); + } + + rdpgfx_context->StartFrame (rdpgfx_context, &cmd_start); + rdpgfx_context->SurfaceCommand (rdpgfx_context, &cmd); + + if (render_surface != gfx_surface) + { + blit_surface_to_surface (graphics_pipeline, gfx_surface, render_surface, + region_rects, n_rects); + } + rdpgfx_context->EndFrame (rdpgfx_context, &cmd_end); + + if (pending_bw_measure_stop) + grd_rdp_network_autodetection_queue_bw_measure_stop (network_autodetection); + + g_free (avc444.bitstream[1].meta.quantQualityVals); + g_free (avc444.bitstream[1].meta.regionRects); + g_free (avc444.bitstream[0].meta.quantQualityVals); + g_free (avc444.bitstream[0].meta.regionRects); + + g_mutex_lock (&graphics_pipeline->gfx_mutex); + clear_old_enc_times (graphics_pipeline, g_get_monotonic_time ()); + track_enc_time (graphics_pipeline, enc_ack_time_us); + + if (freerdp_settings_get_bool (rdp_settings, FreeRDP_NetworkAutoDetect) && + !graphics_pipeline->rtt_pause_source) + ensure_rtt_receivement (graphics_pipeline); + g_mutex_unlock (&graphics_pipeline->gfx_mutex); +} + +static gboolean +refresh_gfx_surface_avc420 (GrdRdpDvcGraphicsPipeline *graphics_pipeline, + HWAccelContext *hwaccel_context, + GrdRdpSurface *rdp_surface, + GrdRdpGfxSurface *gfx_surface, + GrdRdpLegacyBuffer *buffer, + int64_t *enc_time_us) +{ + RdpgfxServerContext *rdpgfx_context = graphics_pipeline->rdpgfx_context; + GrdRdpNetworkAutodetection *network_autodetection = + graphics_pipeline->network_autodetection; + GrdRdpGfxSurface *render_surface = + grd_rdp_gfx_surface_get_render_surface (gfx_surface); + GrdRdpGfxFrameController *frame_controller = + grd_rdp_gfx_surface_get_frame_controller (gfx_surface); + CUdeviceptr src_data = grd_rdp_legacy_buffer_get_mapped_cuda_pointer (buffer); + RDPGFX_SURFACE_COMMAND cmd = {0}; + RDPGFX_START_FRAME_PDU cmd_start = {0}; + RDPGFX_END_FRAME_PDU cmd_end = {0}; + RDPGFX_AVC420_BITMAP_STREAM avc420 = {0}; + RECTANGLE_16 *region_rects; + SYSTEMTIME system_time; + cairo_rectangle_int_t cairo_rect, region_extents; + int n_rects; + uint16_t surface_width = grd_rdp_gfx_surface_get_width (gfx_surface); + uint16_t surface_height = grd_rdp_gfx_surface_get_height (gfx_surface); + uint16_t aligned_width; + uint16_t aligned_height; + cairo_region_t *region; + uint32_t surface_serial; + int64_t enc_ack_time_us; + gboolean pending_bw_measure_stop = FALSE; + int i; + + aligned_width = surface_width + (surface_width % 16 ? 16 - surface_width % 16 : 0); + aligned_height = surface_height + (surface_height % 64 ? 64 - surface_height % 64 : 0); + + if (!grd_hwaccel_nvidia_avc420_encode_bgrx_frame (graphics_pipeline->hwaccel_nvidia, + hwaccel_context->encode_session_id, + src_data, + &rdp_surface->avc.main_view, + surface_width, surface_height, + aligned_width, aligned_height, + rdp_surface->cuda_stream)) + { + g_warning ("[RDP.RDPGFX] Failed to encode YUV420 frame"); + return FALSE; + } + + region = grd_rdp_damage_detector_get_damage_region (rdp_surface->detector); + if (!region) + { + grd_hwaccel_nvidia_avc420_retrieve_bitstream (graphics_pipeline->hwaccel_nvidia, + hwaccel_context->encode_session_id, + NULL, NULL); + return FALSE; + } + + if (!grd_hwaccel_nvidia_avc420_retrieve_bitstream (graphics_pipeline->hwaccel_nvidia, + hwaccel_context->encode_session_id, + &avc420.data, &avc420.length)) + { + g_warning ("[RDP.RDPGFX] Failed to retrieve AVC420 bitstream"); + cairo_region_destroy (region); + return FALSE; + } + + GetSystemTime (&system_time); + cmd_start.timestamp = system_time.wHour << 22 | + system_time.wMinute << 16 | + system_time.wSecond << 10 | + system_time.wMilliseconds; + cmd_start.frameId = get_next_free_frame_id (graphics_pipeline); + cmd_end.frameId = cmd_start.frameId; + + cairo_region_get_extents (region, ®ion_extents); + + cmd.surfaceId = grd_rdp_gfx_surface_get_surface_id (render_surface); + cmd.codecId = RDPGFX_CODECID_AVC420; + cmd.format = PIXEL_FORMAT_BGRX32; + cmd.left = 0; + cmd.top = 0; + cmd.right = region_extents.x + region_extents.width; + cmd.bottom = region_extents.y + region_extents.height; + cmd.length = 0; + cmd.data = NULL; + cmd.extra = &avc420; + + avc420.meta.numRegionRects = n_rects = cairo_region_num_rectangles (region); + avc420.meta.regionRects = region_rects = g_new0 (RECTANGLE_16, n_rects); + avc420.meta.quantQualityVals = g_malloc0 (n_rects * sizeof (RDPGFX_H264_QUANT_QUALITY)); + for (i = 0; i < n_rects; ++i) + { + cairo_region_get_rectangle (region, i, &cairo_rect); + + region_rects[i].left = cairo_rect.x; + region_rects[i].top = cairo_rect.y; + region_rects[i].right = cairo_rect.x + cairo_rect.width; + region_rects[i].bottom = cairo_rect.y + cairo_rect.height; + + avc420.meta.quantQualityVals[i].qp = 22; + avc420.meta.quantQualityVals[i].p = hwaccel_context->has_first_frame ? 1 : 0; + avc420.meta.quantQualityVals[i].qualityVal = 100; + } + hwaccel_context->has_first_frame = TRUE; + + g_mutex_lock (&graphics_pipeline->gfx_mutex); + enc_ack_time_us = g_get_monotonic_time (); + grd_rdp_gfx_frame_controller_unack_frame (frame_controller, cmd_start.frameId, + 1, enc_ack_time_us); + + surface_serial = grd_rdp_gfx_surface_get_serial (gfx_surface); + g_hash_table_insert (graphics_pipeline->frame_serial_table, + GUINT_TO_POINTER (cmd_start.frameId), + GUINT_TO_POINTER (surface_serial)); + surface_serial_ref (graphics_pipeline, surface_serial); + ++graphics_pipeline->total_frames_encoded; + + if (graphics_pipeline->frame_acks_suspended) + { + grd_rdp_gfx_frame_controller_ack_frame (frame_controller, cmd_start.frameId, + enc_ack_time_us); + enqueue_tracked_frame_info (graphics_pipeline, surface_serial, + cmd_start.frameId, 1, enc_ack_time_us); + } + g_mutex_unlock (&graphics_pipeline->gfx_mutex); + + if (network_autodetection && + (4 + avc420.meta.numRegionRects * 10 + avc420.length) >= MIN_BW_MEASURE_SIZE) + { + pending_bw_measure_stop = + grd_rdp_network_autodetection_try_bw_measure_start (network_autodetection); + } + + rdpgfx_context->StartFrame (rdpgfx_context, &cmd_start); + rdpgfx_context->SurfaceCommand (rdpgfx_context, &cmd); + + if (render_surface != gfx_surface) + { + blit_surface_to_surface (graphics_pipeline, gfx_surface, render_surface, + region_rects, n_rects); + } + rdpgfx_context->EndFrame (rdpgfx_context, &cmd_end); + + if (pending_bw_measure_stop) + grd_rdp_network_autodetection_queue_bw_measure_stop (network_autodetection); + + *enc_time_us = enc_ack_time_us; + + g_free (avc420.data); + g_free (avc420.meta.quantQualityVals); + g_free (region_rects); + cairo_region_destroy (region); + + return TRUE; +} + +static void +rfx_progressive_write_message (RFX_MESSAGE *rfx_message, + wStream *s, + gboolean needs_progressive_header) +{ + const RFX_RECT *rfx_rects; + uint16_t n_rfx_rects = 0; + const UINT32 *quant_vals; + uint16_t n_quant_vals = 0; + const RFX_TILE **rfx_tiles; + uint16_t n_rfx_tiles = 0; + uint32_t block_len; + const uint32_t *qv; + const RFX_TILE *rfx_tile; + uint32_t tiles_data_size; + uint16_t i; + + rfx_rects = rfx_message_get_rects (rfx_message, &n_rfx_rects); + quant_vals = rfx_message_get_quants (rfx_message, &n_quant_vals); + rfx_tiles = rfx_message_get_tiles (rfx_message, &n_rfx_tiles); + + if (needs_progressive_header) + { + /* RFX_PROGRESSIVE_SYNC */ + block_len = 12; + if (!Stream_EnsureRemainingCapacity (s, block_len)) + g_assert_not_reached (); + + Stream_Write_UINT16 (s, 0xCCC0); /* blockType */ + Stream_Write_UINT32 (s, block_len); /* blockLen */ + Stream_Write_UINT32 (s, 0xCACCACCA); /* magic */ + Stream_Write_UINT16 (s, 0x0100); /* version */ + + /* RFX_PROGRESSIVE_CONTEXT */ + block_len = 10; + if (!Stream_EnsureRemainingCapacity (s, block_len)) + g_assert_not_reached (); + + Stream_Write_UINT16 (s, 0xCCC3); /* blockType */ + Stream_Write_UINT32 (s, block_len); /* blockLen */ + Stream_Write_UINT8 (s, 0); /* ctxId */ + Stream_Write_UINT16 (s, 0x0040); /* tileSize */ + Stream_Write_UINT8 (s, 0); /* flags */ + } + + /* RFX_PROGRESSIVE_FRAME_BEGIN */ + block_len = 12; + if (!Stream_EnsureRemainingCapacity (s, block_len)) + g_assert_not_reached (); + + Stream_Write_UINT16 (s, 0xCCC1); /* blockType */ + Stream_Write_UINT32 (s, block_len); /* blockLen */ + Stream_Write_UINT32 (s, rfx_message_get_frame_idx (rfx_message)); /* frameIndex */ + Stream_Write_UINT16 (s, 1); /* regionCount */ + + /* RFX_PROGRESSIVE_REGION */ + block_len = 18; + block_len += n_rfx_rects * 8; + block_len += n_quant_vals * 5; + tiles_data_size = n_rfx_tiles * 22; + + for (i = 0; i < n_rfx_tiles; i++) + { + rfx_tile = rfx_tiles[i]; + tiles_data_size += rfx_tile->YLen + rfx_tile->CbLen + rfx_tile->CrLen; + } + + block_len += tiles_data_size; + if (!Stream_EnsureRemainingCapacity (s, block_len)) + g_assert_not_reached (); + + Stream_Write_UINT16 (s, 0xCCC4); /* blockType */ + Stream_Write_UINT32 (s, block_len); /* blockLen */ + Stream_Write_UINT8 (s, 0x40); /* tileSize */ + Stream_Write_UINT16 (s, n_rfx_rects); /* numRects */ + Stream_Write_UINT8 (s, n_quant_vals); /* numQuant */ + Stream_Write_UINT8 (s, 0); /* numProgQuant */ + Stream_Write_UINT8 (s, 0); /* flags */ + Stream_Write_UINT16 (s, n_rfx_tiles); /* numTiles */ + Stream_Write_UINT32 (s, tiles_data_size); /* tilesDataSize */ + + for (i = 0; i < n_rfx_rects; i++) + { + /* TS_RFX_RECT */ + Stream_Write_UINT16 (s, rfx_rects[i].x); /* x */ + Stream_Write_UINT16 (s, rfx_rects[i].y); /* y */ + Stream_Write_UINT16 (s, rfx_rects[i].width); /* width */ + Stream_Write_UINT16 (s, rfx_rects[i].height); /* height */ + } + + /* + * The RFX_COMPONENT_CODEC_QUANT structure differs from the + * TS_RFX_CODEC_QUANT ([MS-RDPRFX] section 2.2.2.1.5) structure with respect + * to the order of the bands. + * 0 1 2 3 4 5 6 7 8 9 + * RDPRFX: LL3, LH3, HL3, HH3, LH2, HL2, HH2, LH1, HL1, HH1 + * RDPEGFX: LL3, HL3, LH3, HH3, HL2, LH2, HH2, HL1, LH1, HH1 + */ + for (i = 0, qv = quant_vals; i < n_quant_vals; ++i, qv += 10) + { + /* RFX_COMPONENT_CODEC_QUANT */ + Stream_Write_UINT8 (s, qv[0] + (qv[2] << 4)); /* LL3, HL3 */ + Stream_Write_UINT8 (s, qv[1] + (qv[3] << 4)); /* LH3, HH3 */ + Stream_Write_UINT8 (s, qv[5] + (qv[4] << 4)); /* HL2, LH2 */ + Stream_Write_UINT8 (s, qv[6] + (qv[8] << 4)); /* HH2, HL1 */ + Stream_Write_UINT8 (s, qv[7] + (qv[9] << 4)); /* LH1, HH1 */ + } + + for (i = 0; i < n_rfx_tiles; ++i) + { + /* RFX_PROGRESSIVE_TILE_SIMPLE */ + rfx_tile = rfx_tiles[i]; + block_len = 22 + rfx_tile->YLen + rfx_tile->CbLen + rfx_tile->CrLen; + Stream_Write_UINT16 (s, 0xCCC5); /* blockType */ + Stream_Write_UINT32 (s, block_len); /* blockLen */ + Stream_Write_UINT8 (s, rfx_tile->quantIdxY); /* quantIdxY */ + Stream_Write_UINT8 (s, rfx_tile->quantIdxCb); /* quantIdxCb */ + Stream_Write_UINT8 (s, rfx_tile->quantIdxCr); /* quantIdxCr */ + Stream_Write_UINT16 (s, rfx_tile->xIdx); /* xIdx */ + Stream_Write_UINT16 (s, rfx_tile->yIdx); /* yIdx */ + Stream_Write_UINT8 (s, 0); /* flags */ + Stream_Write_UINT16 (s, rfx_tile->YLen); /* YLen */ + Stream_Write_UINT16 (s, rfx_tile->CbLen); /* CbLen */ + Stream_Write_UINT16 (s, rfx_tile->CrLen); /* CrLen */ + Stream_Write_UINT16 (s, 0); /* tailLen */ + Stream_Write (s, rfx_tile->YData, rfx_tile->YLen); /* YData */ + Stream_Write (s, rfx_tile->CbData, rfx_tile->CbLen); /* CbData */ + Stream_Write (s, rfx_tile->CrData, rfx_tile->CrLen); /* CrData */ + } + + /* RFX_PROGRESSIVE_FRAME_END */ + block_len = 6; + if (!Stream_EnsureRemainingCapacity (s, block_len)) + g_assert_not_reached (); + + Stream_Write_UINT16 (s, 0xCCC2); /* blockType */ + Stream_Write_UINT32 (s, block_len); /* blockLen */ +} + +static gboolean +refresh_gfx_surface_rfx_progressive (GrdRdpDvcGraphicsPipeline *graphics_pipeline, + GrdRdpSurface *rdp_surface, + GrdRdpGfxSurface *gfx_surface, + GrdRdpLegacyBuffer *buffer, + int64_t *enc_time_us) +{ + RdpgfxServerContext *rdpgfx_context = graphics_pipeline->rdpgfx_context; + GrdRdpNetworkAutodetection *network_autodetection = + graphics_pipeline->network_autodetection; + GrdRdpGfxFrameController *frame_controller = + grd_rdp_gfx_surface_get_frame_controller (gfx_surface); + uint16_t surface_width = grd_rdp_gfx_surface_get_width (gfx_surface); + uint16_t surface_height = grd_rdp_gfx_surface_get_height (gfx_surface); + uint32_t src_stride = grd_rdp_legacy_buffer_get_stride (buffer); + RDPGFX_SURFACE_COMMAND cmd = {0}; + RDPGFX_START_FRAME_PDU cmd_start = {0}; + RDPGFX_END_FRAME_PDU cmd_end = {0}; + gboolean needs_progressive_header = FALSE; + cairo_region_t *region; + cairo_rectangle_int_t cairo_rect; + RFX_RECT *rfx_rects, *rfx_rect; + int n_rects; + RFX_MESSAGE *rfx_message; + SYSTEMTIME system_time; + uint32_t codec_context_id; + uint32_t surface_serial; + int64_t enc_ack_time_us; + gboolean pending_bw_measure_stop = FALSE; + int i; + + region = grd_rdp_damage_detector_get_damage_region (rdp_surface->detector); + if (!region) + return FALSE; + + rfx_context_set_mode (graphics_pipeline->rfx_context, RLGR1); + rfx_context_reset (graphics_pipeline->rfx_context, + surface_width, surface_height); + + codec_context_id = grd_rdp_gfx_surface_get_codec_context_id (gfx_surface); + g_mutex_lock (&graphics_pipeline->gfx_mutex); + if (!g_hash_table_contains (graphics_pipeline->codec_context_table, + GUINT_TO_POINTER (codec_context_id))) + needs_progressive_header = TRUE; + g_mutex_unlock (&graphics_pipeline->gfx_mutex); + + n_rects = cairo_region_num_rectangles (region); + rfx_rects = g_malloc0 (n_rects * sizeof (RFX_RECT)); + for (i = 0; i < n_rects; ++i) + { + cairo_region_get_rectangle (region, i, &cairo_rect); + + rfx_rect = &rfx_rects[i]; + rfx_rect->x = cairo_rect.x; + rfx_rect->y = cairo_rect.y; + rfx_rect->width = cairo_rect.width; + rfx_rect->height = cairo_rect.height; + } + + rfx_message = rfx_encode_message (graphics_pipeline->rfx_context, + rfx_rects, + n_rects, + grd_rdp_legacy_buffer_get_local_data (buffer), + surface_width, + surface_height, + src_stride); + g_free (rfx_rects); + + GetSystemTime (&system_time); + cmd_start.timestamp = system_time.wHour << 22 | + system_time.wMinute << 16 | + system_time.wSecond << 10 | + system_time.wMilliseconds; + cmd_start.frameId = get_next_free_frame_id (graphics_pipeline); + cmd_end.frameId = cmd_start.frameId; + + cmd.surfaceId = grd_rdp_gfx_surface_get_surface_id (gfx_surface); + cmd.codecId = RDPGFX_CODECID_CAPROGRESSIVE; + cmd.contextId = codec_context_id; + cmd.format = PIXEL_FORMAT_BGRX32; + + Stream_SetPosition (graphics_pipeline->encode_stream, 0); + rfx_progressive_write_message (rfx_message, + graphics_pipeline->encode_stream, + needs_progressive_header); + rfx_message_free (graphics_pipeline->rfx_context, rfx_message); + + cmd.length = Stream_GetPosition (graphics_pipeline->encode_stream); + cmd.data = Stream_Buffer (graphics_pipeline->encode_stream); + + g_mutex_lock (&graphics_pipeline->gfx_mutex); + if (needs_progressive_header) + { + g_hash_table_insert (graphics_pipeline->codec_context_table, + GUINT_TO_POINTER (codec_context_id), gfx_surface); + } + + enc_ack_time_us = g_get_monotonic_time (); + grd_rdp_gfx_frame_controller_unack_frame (frame_controller, cmd_start.frameId, + 1, enc_ack_time_us); + + surface_serial = grd_rdp_gfx_surface_get_serial (gfx_surface); + g_hash_table_insert (graphics_pipeline->frame_serial_table, + GUINT_TO_POINTER (cmd_start.frameId), + GUINT_TO_POINTER (surface_serial)); + surface_serial_ref (graphics_pipeline, surface_serial); + ++graphics_pipeline->total_frames_encoded; + + if (graphics_pipeline->frame_acks_suspended) + { + grd_rdp_gfx_frame_controller_ack_frame (frame_controller, cmd_start.frameId, + enc_ack_time_us); + enqueue_tracked_frame_info (graphics_pipeline, surface_serial, + cmd_start.frameId, 1, enc_ack_time_us); + } + g_mutex_unlock (&graphics_pipeline->gfx_mutex); + + if (network_autodetection && cmd.length >= MIN_BW_MEASURE_SIZE) + { + pending_bw_measure_stop = + grd_rdp_network_autodetection_try_bw_measure_start (network_autodetection); + } + + rdpgfx_context->SurfaceFrameCommand (rdpgfx_context, &cmd, + &cmd_start, &cmd_end); + + if (pending_bw_measure_stop) + grd_rdp_network_autodetection_queue_bw_measure_stop (network_autodetection); + + *enc_time_us = enc_ack_time_us; + + cairo_region_destroy (region); + + return TRUE; +} + +gboolean +grd_rdp_dvc_graphics_pipeline_refresh_gfx (GrdRdpDvcGraphicsPipeline *graphics_pipeline, + GrdRdpSurface *rdp_surface, + GrdRdpRenderContext *render_context, + GrdRdpLegacyBuffer *buffer) +{ + RdpgfxServerContext *rdpgfx_context = graphics_pipeline->rdpgfx_context; + rdpSettings *rdp_settings = rdpgfx_context->rdpcontext->settings; + GrdRdpGfxSurface *gfx_surface = + grd_rdp_render_context_get_gfx_surface (render_context); + HWAccelContext *hwaccel_context; + uint16_t surface_id; + int64_t enc_time_us; + gboolean success; + + g_mutex_lock (&graphics_pipeline->gfx_mutex); + if (freerdp_settings_get_bool (rdp_settings, FreeRDP_NetworkAutoDetect) && + !graphics_pipeline->rtt_pause_source) + ensure_rtt_receivement (graphics_pipeline); + g_mutex_unlock (&graphics_pipeline->gfx_mutex); + + surface_id = grd_rdp_gfx_surface_get_surface_id (gfx_surface); + if (freerdp_settings_get_bool (rdp_settings, FreeRDP_GfxH264) && + g_hash_table_lookup_extended (graphics_pipeline->surface_hwaccel_table, + GUINT_TO_POINTER (surface_id), + NULL, (gpointer *) &hwaccel_context)) + { + g_assert (hwaccel_context->api == HW_ACCEL_API_NVENC); + success = refresh_gfx_surface_avc420 (graphics_pipeline, hwaccel_context, + rdp_surface, gfx_surface, buffer, + &enc_time_us); + } + else + { + success = refresh_gfx_surface_rfx_progressive (graphics_pipeline, + rdp_surface, gfx_surface, + buffer, &enc_time_us); + } + + if (success) + { + g_mutex_lock (&graphics_pipeline->gfx_mutex); + clear_old_enc_times (graphics_pipeline, g_get_monotonic_time ()); + track_enc_time (graphics_pipeline, enc_time_us); + + if (freerdp_settings_get_bool (rdp_settings, FreeRDP_NetworkAutoDetect) && + !graphics_pipeline->rtt_pause_source) + ensure_rtt_receivement (graphics_pipeline); + g_mutex_unlock (&graphics_pipeline->gfx_mutex); + } + + return success; +} + +static void +dvc_creation_status (gpointer user_data, + int32_t creation_status) +{ + GrdRdpDvcGraphicsPipeline *graphics_pipeline = user_data; + + if (creation_status < 0) + { + g_warning ("[RDP.RDPGFX] Failed to open channel (CreationStatus %i). " + "Terminating session", creation_status); + grd_session_rdp_notify_error (graphics_pipeline->session_rdp, + GRD_SESSION_RDP_ERROR_BAD_CAPS); + } +} + +static BOOL +rdpgfx_channel_id_assigned (RdpgfxServerContext *rdpgfx_context, + uint32_t channel_id) +{ + GrdRdpDvcGraphicsPipeline *graphics_pipeline = rdpgfx_context->custom; + GrdRdpDvc *dvc = GRD_RDP_DVC (graphics_pipeline); + + g_debug ("[RDP.RDPGFX] DVC channel id assigned to id %u", channel_id); + + grd_rdp_dvc_subscribe_creation_status (dvc, channel_id, + dvc_creation_status, + graphics_pipeline); + + return TRUE; +} + +static uint32_t cap_list[] = +{ + RDPGFX_CAPVERSION_107, /* [MS-RDPEGFX] 2.2.3.10 */ + RDPGFX_CAPVERSION_106, /* [MS-RDPEGFX] 2.2.3.9 */ + RDPGFX_CAPVERSION_105, /* [MS-RDPEGFX] 2.2.3.8 */ + RDPGFX_CAPVERSION_104, /* [MS-RDPEGFX] 2.2.3.7 */ + RDPGFX_CAPVERSION_103, /* [MS-RDPEGFX] 2.2.3.6 */ + RDPGFX_CAPVERSION_102, /* [MS-RDPEGFX] 2.2.3.5 */ + RDPGFX_CAPVERSION_101, /* [MS-RDPEGFX] 2.2.3.4 */ + RDPGFX_CAPVERSION_10, /* [MS-RDPEGFX] 2.2.3.3 */ + RDPGFX_CAPVERSION_81, /* [MS-RDPEGFX] 2.2.3.2 */ + RDPGFX_CAPVERSION_8, /* [MS-RDPEGFX] 2.2.3.1 */ +}; + +static const char * +rdpgfx_caps_version_to_string (uint32_t caps_version) +{ + switch (caps_version) + { + case RDPGFX_CAPVERSION_107: + return "RDPGFX_CAPVERSION_107"; + case RDPGFX_CAPVERSION_106: + return "RDPGFX_CAPVERSION_106"; + case RDPGFX_CAPVERSION_105: + return "RDPGFX_CAPVERSION_105"; + case RDPGFX_CAPVERSION_104: + return "RDPGFX_CAPVERSION_104"; + case RDPGFX_CAPVERSION_103: + return "RDPGFX_CAPVERSION_103"; + case RDPGFX_CAPVERSION_102: + return "RDPGFX_CAPVERSION_102"; + case RDPGFX_CAPVERSION_101: + return "RDPGFX_CAPVERSION_101"; + case RDPGFX_CAPVERSION_10: + return "RDPGFX_CAPVERSION_10"; + case RDPGFX_CAPVERSION_81: + return "RDPGFX_CAPVERSION_81"; + case RDPGFX_CAPVERSION_8: + return "RDPGFX_CAPVERSION_8"; + default: + g_assert_not_reached (); + } + + return NULL; +} + +static void +search_and_list_unknown_cap_sets_versions_and_flags (RDPGFX_CAPSET *cap_sets, + uint16_t n_cap_sets) +{ + uint16_t i; + size_t j; + + for (i = 0; i < n_cap_sets; ++i) + { + gboolean cap_found = FALSE; + + for (j = 0; j < G_N_ELEMENTS (cap_list) && !cap_found; ++j) + { + const char *version_string; + gboolean has_flags_field; + + if (cap_sets[i].version != cap_list[j]) + continue; + + cap_found = TRUE; + has_flags_field = cap_sets[i].version != RDPGFX_CAPVERSION_101; + + version_string = rdpgfx_caps_version_to_string (cap_sets[i].version); + g_debug ("[RDP.RDPGFX] Client caps set %s flags: 0x%08X%s", + version_string, cap_sets[i].flags, + has_flags_field ? "" : " (invalid flags field)"); + } + if (!cap_found) + { + g_debug ("[RDP.RDPGFX] Received unknown capability set with " + "version 0x%08X, length: %u Bytes", + cap_sets[i].version, cap_sets[i].length); + if (cap_sets[i].length >= 4) + { + g_debug ("[RDP.RDPGFX] Possible flags of unknown capability set: " + "0x%08X", cap_sets[i].flags); + } + } + } +} + +static gboolean +cap_sets_contains_supported_version (RDPGFX_CAPSET *cap_sets, + uint16_t n_cap_sets) +{ + size_t i; + uint16_t j; + + for (i = 0; i < G_N_ELEMENTS (cap_list); ++i) + { + for (j = 0; j < n_cap_sets; ++j) + { + if (cap_sets[j].version == cap_list[i]) + return TRUE; + } + } + + g_warning ("[RDP.RDPGFX] Client did not advertise any supported " + "capability set"); + + return FALSE; +} + +static gboolean +cap_sets_would_disable_avc (RDPGFX_CAPSET *cap_sets, + uint16_t n_cap_sets) +{ + size_t i; + uint16_t j; + + for (i = 0; i < G_N_ELEMENTS (cap_list); ++i) + { + for (j = 0; j < n_cap_sets; ++j) + { + if (cap_sets[j].version == cap_list[i]) + { + uint32_t flags = cap_sets[i].flags; + + switch (cap_sets[j].version) + { + case RDPGFX_CAPVERSION_107: + case RDPGFX_CAPVERSION_106: + case RDPGFX_CAPVERSION_105: + case RDPGFX_CAPVERSION_104: + case RDPGFX_CAPVERSION_103: + case RDPGFX_CAPVERSION_102: + case RDPGFX_CAPVERSION_101: + case RDPGFX_CAPVERSION_10: + if (flags & RDPGFX_CAPS_FLAG_AVC_DISABLED) + return TRUE; + return FALSE; + case RDPGFX_CAPVERSION_81: + if (!(flags & RDPGFX_CAPS_FLAG_AVC420_ENABLED)) + return TRUE; + return FALSE; + case RDPGFX_CAPVERSION_8: + return TRUE; + default: + g_assert_not_reached (); + } + } + } + } + + g_assert_not_reached (); + + return TRUE; +} + +static uint32_t +rdpgfx_caps_advertise (RdpgfxServerContext *rdpgfx_context, + const RDPGFX_CAPS_ADVERTISE_PDU *caps_advertise) +{ + GrdRdpDvcGraphicsPipeline *graphics_pipeline = rdpgfx_context->custom; + GrdRdpRenderer *renderer = graphics_pipeline->renderer; + + g_debug ("[RDP.RDPGFX] Received a CapsAdvertise PDU"); + search_and_list_unknown_cap_sets_versions_and_flags (caps_advertise->capsSets, + caps_advertise->capsSetCount); + + if (graphics_pipeline->initialized && + graphics_pipeline->initial_version < RDPGFX_CAPVERSION_103) + { + g_warning ("[RDP.RDPGFX] Protocol violation: Received an illegal " + "CapsAdvertise PDU (RDPGFX: initialized, initial " + "version < 103)"); + grd_session_rdp_notify_error (graphics_pipeline->session_rdp, + GRD_SESSION_RDP_ERROR_BAD_CAPS); + + return CHANNEL_RC_ALREADY_INITIALIZED; + } + + if (!cap_sets_contains_supported_version (caps_advertise->capsSets, + caps_advertise->capsSetCount)) + { + g_warning ("[RDP.RDPGFX] CapsAdvertise PDU does NOT contain any supported " + "capability sets"); + grd_session_rdp_notify_error (graphics_pipeline->session_rdp, + GRD_SESSION_RDP_ERROR_BAD_CAPS); + + return CHANNEL_RC_UNSUPPORTED_VERSION; + } + + if (graphics_pipeline->received_first_cap_sets && + cap_sets_would_disable_avc (caps_advertise->capsSets, + caps_advertise->capsSetCount)) + { + g_warning ("[RDP.RDPGFX] CapsAdvertise PDU would reset protocol with " + "unsupported capability sets (disabling AVC)"); + grd_session_rdp_notify_error (graphics_pipeline->session_rdp, + GRD_SESSION_RDP_ERROR_BAD_CAPS); + + return CHANNEL_RC_ALREADY_INITIALIZED; + } + graphics_pipeline->received_first_cap_sets = TRUE; + + g_mutex_lock (&graphics_pipeline->caps_mutex); + g_clear_pointer (&graphics_pipeline->cap_sets, g_free); + + graphics_pipeline->n_cap_sets = caps_advertise->capsSetCount; + graphics_pipeline->cap_sets = g_memdup2 (caps_advertise->capsSets, + graphics_pipeline->n_cap_sets * + sizeof (RDPGFX_CAPSET)); + + grd_rdp_renderer_notify_graphics_pipeline_reset (renderer); + g_mutex_unlock (&graphics_pipeline->caps_mutex); + + return CHANNEL_RC_OK; +} + +static uint32_t +rdpgfx_cache_import_offer (RdpgfxServerContext *rdpgfx_context, + const RDPGFX_CACHE_IMPORT_OFFER_PDU *cache_import_offer) +{ + RDPGFX_CACHE_IMPORT_REPLY_PDU cache_import_reply = {0}; + + return rdpgfx_context->CacheImportReply (rdpgfx_context, &cache_import_reply); +} + +static void +notify_updated_frame_controllers (GHashTable *updated_frame_controllers) +{ + GrdRdpGfxFrameController *frame_controller = NULL; + GHashTableIter iter; + + g_hash_table_iter_init (&iter, updated_frame_controllers); + while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &frame_controller)) + grd_rdp_gfx_frame_controller_notify_history_changed (frame_controller); +} + +static void +maybe_rewrite_frame_history (GrdRdpDvcGraphicsPipeline *graphics_pipeline, + uint32_t pending_frame_acks) +{ + GfxFrameInfo *gfx_frame_info; + GHashTable *updated_frame_controllers; + + if (g_queue_get_length (graphics_pipeline->encoded_frames) == 0) + return; + + reduce_tracked_frame_infos (graphics_pipeline, pending_frame_acks + 1); + updated_frame_controllers = g_hash_table_new (NULL, NULL); + + while ((gfx_frame_info = g_queue_pop_tail (graphics_pipeline->encoded_frames))) + { + GrdRdpFrameInfo *frame_info = &gfx_frame_info->frame_info; + uint32_t surface_serial = gfx_frame_info->surface_serial; + GfxSurfaceContext *surface_context; + + if (!g_hash_table_lookup_extended (graphics_pipeline->serial_surface_table, + GUINT_TO_POINTER (surface_serial), + NULL, (gpointer *) &surface_context)) + g_assert_not_reached (); + + if (surface_context->gfx_surface) + { + GrdRdpGfxFrameController *frame_controller = + grd_rdp_gfx_surface_get_frame_controller (surface_context->gfx_surface); + + grd_rdp_gfx_frame_controller_unack_last_acked_frame (frame_controller, + frame_info->frame_id, + frame_info->n_subframes, + frame_info->enc_time_us); + g_hash_table_add (updated_frame_controllers, frame_controller); + } + + g_free (gfx_frame_info); + } + + notify_updated_frame_controllers (updated_frame_controllers); + g_hash_table_unref (updated_frame_controllers); +} + +static void +clear_all_unacked_frames_in_gfx_surface (gpointer key, + gpointer value, + gpointer user_data) +{ + GrdRdpGfxSurface *gfx_surface = value; + GrdRdpGfxFrameController *frame_controller = + grd_rdp_gfx_surface_get_frame_controller (gfx_surface); + + if (!frame_controller) + return; + + grd_rdp_gfx_frame_controller_clear_all_unacked_frames (frame_controller); +} + +static gboolean +frame_serial_free (gpointer key, + gpointer value, + gpointer user_data) +{ + GrdRdpDvcGraphicsPipeline *graphics_pipeline = user_data; + uint32_t surface_serial = GPOINTER_TO_UINT (value); + + surface_serial_unref (graphics_pipeline, surface_serial); + + return TRUE; +} + +static void +suspend_frame_acknowledgement (GrdRdpDvcGraphicsPipeline *graphics_pipeline) +{ + graphics_pipeline->frame_acks_suspended = TRUE; + + g_hash_table_foreach (graphics_pipeline->surface_table, + clear_all_unacked_frames_in_gfx_surface, NULL); + + reduce_tracked_frame_infos (graphics_pipeline, 0); + g_hash_table_foreach_remove (graphics_pipeline->frame_serial_table, + frame_serial_free, graphics_pipeline); +} + +static void +handle_frame_ack_event (GrdRdpDvcGraphicsPipeline *graphics_pipeline, + const RDPGFX_FRAME_ACKNOWLEDGE_PDU *frame_acknowledge) +{ + uint32_t pending_frame_acks; + gpointer value = NULL; + + pending_frame_acks = graphics_pipeline->total_frames_encoded - + frame_acknowledge->totalFramesDecoded; + if (pending_frame_acks <= MAX_TRACKED_ENC_FRAMES && + !g_hash_table_contains (graphics_pipeline->frame_serial_table, + GUINT_TO_POINTER (frame_acknowledge->frameId))) + return; + + maybe_rewrite_frame_history (graphics_pipeline, pending_frame_acks); + if (frame_acknowledge->queueDepth != SUSPEND_FRAME_ACKNOWLEDGEMENT) + graphics_pipeline->frame_acks_suspended = FALSE; + + if (g_hash_table_steal_extended (graphics_pipeline->frame_serial_table, + GUINT_TO_POINTER (frame_acknowledge->frameId), + NULL, &value)) + { + GfxSurfaceContext *surface_context; + uint32_t surface_serial; + + surface_serial = GPOINTER_TO_UINT (value); + if (!g_hash_table_lookup_extended (graphics_pipeline->serial_surface_table, + GUINT_TO_POINTER (surface_serial), + NULL, (gpointer *) &surface_context)) + g_assert_not_reached (); + + if (surface_context->gfx_surface) + { + GrdRdpGfxFrameController *frame_controller = + grd_rdp_gfx_surface_get_frame_controller (surface_context->gfx_surface); + + grd_rdp_gfx_frame_controller_ack_frame (frame_controller, + frame_acknowledge->frameId, + g_get_monotonic_time ()); + } + + surface_serial_unref (graphics_pipeline, surface_serial); + } + + if (frame_acknowledge->queueDepth == SUSPEND_FRAME_ACKNOWLEDGEMENT) + suspend_frame_acknowledgement (graphics_pipeline); +} + +static uint32_t +rdpgfx_frame_acknowledge (RdpgfxServerContext *rdpgfx_context, + const RDPGFX_FRAME_ACKNOWLEDGE_PDU *frame_acknowledge) +{ + GrdRdpDvcGraphicsPipeline *graphics_pipeline = rdpgfx_context->custom; + + g_mutex_lock (&graphics_pipeline->gfx_mutex); + handle_frame_ack_event (graphics_pipeline, frame_acknowledge); + g_mutex_unlock (&graphics_pipeline->gfx_mutex); + + return CHANNEL_RC_OK; +} + +static uint32_t +rdpgfx_qoe_frame_acknowledge (RdpgfxServerContext *rdpgfx_context, + const RDPGFX_QOE_FRAME_ACKNOWLEDGE_PDU *qoe_frame_acknowledge) +{ + return CHANNEL_RC_OK; +} + +static void +reset_graphics_pipeline (GrdRdpDvcGraphicsPipeline *graphics_pipeline) +{ + g_mutex_lock (&graphics_pipeline->gfx_mutex); + g_hash_table_foreach (graphics_pipeline->surface_table, + clear_all_unacked_frames_in_gfx_surface, NULL); + + reduce_tracked_frame_infos (graphics_pipeline, 0); + g_hash_table_foreach_remove (graphics_pipeline->frame_serial_table, + frame_serial_free, graphics_pipeline); + g_mutex_unlock (&graphics_pipeline->gfx_mutex); + + grd_rdp_renderer_clear_render_contexts (graphics_pipeline->renderer); + + g_mutex_lock (&graphics_pipeline->gfx_mutex); + graphics_pipeline->frame_acks_suspended = FALSE; + graphics_pipeline->total_frames_encoded = 0; + + g_assert (g_hash_table_size (graphics_pipeline->surface_table) == 0); + g_assert (g_hash_table_size (graphics_pipeline->codec_context_table) == 0); + g_assert (g_hash_table_size (graphics_pipeline->frame_serial_table) == 0); + g_assert (g_hash_table_size (graphics_pipeline->serial_surface_table) == 0); + g_assert (g_queue_get_length (graphics_pipeline->encoded_frames) == 0); + g_mutex_unlock (&graphics_pipeline->gfx_mutex); +} + +static gboolean +test_caps_version (GrdRdpDvcGraphicsPipeline *graphics_pipeline, + RDPGFX_CAPSET *cap_sets, + uint16_t n_cap_sets, + uint32_t caps_version) +{ + RdpgfxServerContext *rdpgfx_context = graphics_pipeline->rdpgfx_context; + rdpSettings *rdp_settings = rdpgfx_context->rdpcontext->settings; + RDPGFX_CAPS_CONFIRM_PDU caps_confirm = {0}; + uint16_t i; + + for (i = 0; i < n_cap_sets; ++i) + { + if (cap_sets[i].version == caps_version) + { + uint32_t flags = cap_sets[i].flags; + gboolean have_avc444 = FALSE; + gboolean have_avc420 = FALSE; + + switch (caps_version) + { + case RDPGFX_CAPVERSION_107: + case RDPGFX_CAPVERSION_106: + case RDPGFX_CAPVERSION_105: + case RDPGFX_CAPVERSION_104: + case RDPGFX_CAPVERSION_103: + case RDPGFX_CAPVERSION_102: + case RDPGFX_CAPVERSION_101: + case RDPGFX_CAPVERSION_10: + have_avc444 = !(flags & RDPGFX_CAPS_FLAG_AVC_DISABLED); + have_avc420 = !(flags & RDPGFX_CAPS_FLAG_AVC_DISABLED); + break; + case RDPGFX_CAPVERSION_81: + have_avc420 = !!(flags & RDPGFX_CAPS_FLAG_AVC420_ENABLED); + break; + case RDPGFX_CAPVERSION_8: + break; + default: + g_assert_not_reached (); + } + + freerdp_settings_set_bool (rdp_settings, FreeRDP_GfxAVC444v2, have_avc444); + freerdp_settings_set_bool (rdp_settings, FreeRDP_GfxAVC444, have_avc444); + freerdp_settings_set_bool (rdp_settings, FreeRDP_GfxH264, have_avc420); + + g_message ("[RDP.RDPGFX] CapsAdvertise: Accepting capability set with version " + "%s, Client cap flags: H264 (AVC444): %s, H264 (AVC420): %s", + rdpgfx_caps_version_to_string (caps_version), + have_avc444 ? "true" : "false", + have_avc420 ? "true" : "false"); + if (!graphics_pipeline->initialized) + graphics_pipeline->initial_version = caps_version; + graphics_pipeline->initialized = TRUE; + + reset_graphics_pipeline (graphics_pipeline); + + caps_confirm.capsSet = &cap_sets[i]; + + rdpgfx_context->CapsConfirm (rdpgfx_context, &caps_confirm); + + return TRUE; + } + } + + return FALSE; +} + +static gboolean +reset_protocol (gpointer user_data) +{ + GrdRdpDvcGraphicsPipeline *graphics_pipeline = user_data; + GrdRdpRenderer *renderer = graphics_pipeline->renderer; + g_autofree RDPGFX_CAPSET *cap_sets = NULL; + uint16_t n_cap_sets; + size_t i; + + g_mutex_lock (&graphics_pipeline->caps_mutex); + cap_sets = g_steal_pointer (&graphics_pipeline->cap_sets); + n_cap_sets = graphics_pipeline->n_cap_sets; + g_mutex_unlock (&graphics_pipeline->caps_mutex); + + if (!cap_sets || !n_cap_sets) + { + g_assert (graphics_pipeline->initialized); + + return G_SOURCE_CONTINUE; + } + + if (graphics_pipeline->protocol_timeout_source) + { + g_source_destroy (graphics_pipeline->protocol_timeout_source); + g_clear_pointer (&graphics_pipeline->protocol_timeout_source, g_source_unref); + } + + for (i = 0; i < G_N_ELEMENTS (cap_list); ++i) + { + if (test_caps_version (graphics_pipeline, + cap_sets, n_cap_sets, + cap_list[i])) + { + grd_rdp_renderer_notify_graphics_pipeline_ready (renderer); + + g_mutex_lock (&graphics_pipeline->caps_mutex); + if (graphics_pipeline->cap_sets) + grd_rdp_renderer_notify_graphics_pipeline_reset (renderer); + g_mutex_unlock (&graphics_pipeline->caps_mutex); + + return G_SOURCE_CONTINUE; + } + } + + /* + * CapsAdvertise already checked the capability sets to have at least one + * supported version. + * It is therefore impossible to hit this path. + */ + g_assert_not_reached (); + + return G_SOURCE_CONTINUE; +} + +static gboolean +protocol_reset_source_dispatch (GSource *source, + GSourceFunc callback, + gpointer user_data) +{ + g_source_set_ready_time (source, -1); + + return callback (user_data); +} + +static GSourceFuncs protocol_reset_source_funcs = +{ + .dispatch = protocol_reset_source_dispatch, +}; + +static void +on_gfx_initable (GrdRdpRenderer *renderer, + GrdRdpDvcGraphicsPipeline *graphics_pipeline) +{ + g_source_set_ready_time (graphics_pipeline->protocol_reset_source, 0); +} + +GrdRdpDvcGraphicsPipeline * +grd_rdp_dvc_graphics_pipeline_new (GrdSessionRdp *session_rdp, + GrdRdpRenderer *renderer, + GrdRdpDvcHandler *dvc_handler, + HANDLE vcm, + rdpContext *rdp_context, + GrdRdpNetworkAutodetection *network_autodetection, + wStream *encode_stream, + RFX_CONTEXT *rfx_context) +{ + GMainContext *graphics_context = + grd_rdp_renderer_get_graphics_context (renderer); + rdpSettings *rdp_settings = rdp_context->settings; + GrdRdpDvcGraphicsPipeline *graphics_pipeline; + RdpgfxServerContext *rdpgfx_context; + GSource *protocol_reset_source; + + graphics_pipeline = g_object_new (GRD_TYPE_RDP_DVC_GRAPHICS_PIPELINE, NULL); + rdpgfx_context = rdpgfx_server_context_new (vcm); + if (!rdpgfx_context) + g_error ("[RDP.RDPGFX] Failed to create server context"); + + graphics_pipeline->rdpgfx_context = rdpgfx_context; + graphics_pipeline->session_rdp = session_rdp; + graphics_pipeline->renderer = renderer; + graphics_pipeline->network_autodetection = network_autodetection; + graphics_pipeline->encode_stream = encode_stream; + graphics_pipeline->rfx_context = rfx_context; + + grd_rdp_dvc_initialize_base (GRD_RDP_DVC (graphics_pipeline), + dvc_handler, session_rdp, + GRD_RDP_CHANNEL_GRAPHICS_PIPELINE); + + rdpgfx_context->ChannelIdAssigned = rdpgfx_channel_id_assigned; + rdpgfx_context->CapsAdvertise = rdpgfx_caps_advertise; + rdpgfx_context->CacheImportOffer = rdpgfx_cache_import_offer; + rdpgfx_context->FrameAcknowledge = rdpgfx_frame_acknowledge; + rdpgfx_context->QoeFrameAcknowledge = rdpgfx_qoe_frame_acknowledge; + rdpgfx_context->rdpcontext = rdp_context; + rdpgfx_context->custom = graphics_pipeline; + + protocol_reset_source = g_source_new (&protocol_reset_source_funcs, + sizeof (GSource)); + g_source_set_callback (protocol_reset_source, reset_protocol, + graphics_pipeline, NULL); + g_source_set_ready_time (protocol_reset_source, -1); + g_source_attach (protocol_reset_source, graphics_context); + graphics_pipeline->protocol_reset_source = protocol_reset_source; + + graphics_pipeline->gfx_initable_id = + g_signal_connect (renderer, "gfx-initable", + G_CALLBACK (on_gfx_initable), + graphics_pipeline); + + g_mutex_lock (&graphics_pipeline->gfx_mutex); + if (freerdp_settings_get_bool (rdp_settings, FreeRDP_NetworkAutoDetect) && + !graphics_pipeline->rtt_pause_source) + ensure_rtt_receivement (graphics_pipeline); + g_mutex_unlock (&graphics_pipeline->gfx_mutex); + + return graphics_pipeline; +} + +static void +grd_rdp_dvc_graphics_pipeline_dispose (GObject *object) +{ + GrdRdpDvcGraphicsPipeline *graphics_pipeline = + GRD_RDP_DVC_GRAPHICS_PIPELINE (object); + GrdRdpDvc *dvc = GRD_RDP_DVC (graphics_pipeline); + + if (graphics_pipeline->channel_opened) + { + reset_graphics_pipeline (graphics_pipeline); + graphics_pipeline->rdpgfx_context->Close (graphics_pipeline->rdpgfx_context); + graphics_pipeline->channel_opened = FALSE; + } + grd_rdp_dvc_maybe_unsubscribe_creation_status (dvc); + + g_clear_signal_handler (&graphics_pipeline->gfx_initable_id, + graphics_pipeline->renderer); + + if (graphics_pipeline->protocol_timeout_source) + { + g_source_destroy (graphics_pipeline->protocol_timeout_source); + g_clear_pointer (&graphics_pipeline->protocol_timeout_source, g_source_unref); + } + if (graphics_pipeline->rtt_pause_source) + { + g_source_destroy (graphics_pipeline->rtt_pause_source); + g_clear_pointer (&graphics_pipeline->rtt_pause_source, g_source_unref); + } + if (graphics_pipeline->protocol_reset_source) + { + g_source_destroy (graphics_pipeline->protocol_reset_source); + g_clear_pointer (&graphics_pipeline->protocol_reset_source, g_source_unref); + } + + if (graphics_pipeline->enc_times) + { + g_queue_free_full (graphics_pipeline->enc_times, g_free); + graphics_pipeline->enc_times = NULL; + } + + if (graphics_pipeline->encoded_frames) + { + g_assert (g_queue_get_length (graphics_pipeline->encoded_frames) == 0); + g_clear_pointer (&graphics_pipeline->encoded_frames, g_queue_free); + } + + g_assert (g_hash_table_size (graphics_pipeline->surface_hwaccel_table) == 0); + g_clear_pointer (&graphics_pipeline->surface_hwaccel_table, g_hash_table_destroy); + + g_clear_pointer (&graphics_pipeline->cap_sets, g_free); + + g_assert (g_hash_table_size (graphics_pipeline->serial_surface_table) == 0); + g_clear_pointer (&graphics_pipeline->serial_surface_table, g_hash_table_destroy); + g_clear_pointer (&graphics_pipeline->frame_serial_table, g_hash_table_destroy); + g_clear_pointer (&graphics_pipeline->codec_context_table, g_hash_table_destroy); + g_clear_pointer (&graphics_pipeline->surface_table, g_hash_table_destroy); + g_clear_pointer (&graphics_pipeline->rdpgfx_context, rdpgfx_server_context_free); + + G_OBJECT_CLASS (grd_rdp_dvc_graphics_pipeline_parent_class)->dispose (object); +} + +static void +grd_rdp_dvc_graphics_pipeline_finalize (GObject *object) +{ + GrdRdpDvcGraphicsPipeline *graphics_pipeline = + GRD_RDP_DVC_GRAPHICS_PIPELINE (object); + + g_mutex_clear (&graphics_pipeline->gfx_mutex); + g_mutex_clear (&graphics_pipeline->caps_mutex); + + G_OBJECT_CLASS (grd_rdp_dvc_graphics_pipeline_parent_class)->finalize (object); +} + +static void +grd_rdp_dvc_graphics_pipeline_init (GrdRdpDvcGraphicsPipeline *graphics_pipeline) +{ + graphics_pipeline->surface_table = g_hash_table_new (NULL, NULL); + graphics_pipeline->codec_context_table = g_hash_table_new (NULL, NULL); + + graphics_pipeline->frame_serial_table = g_hash_table_new (NULL, NULL); + graphics_pipeline->serial_surface_table = g_hash_table_new_full (NULL, NULL, + NULL, g_free); + graphics_pipeline->surface_hwaccel_table = g_hash_table_new (NULL, NULL); + graphics_pipeline->encoded_frames = g_queue_new (); + graphics_pipeline->enc_times = g_queue_new (); + + g_mutex_init (&graphics_pipeline->caps_mutex); + g_mutex_init (&graphics_pipeline->gfx_mutex); +} + +static void +grd_rdp_dvc_graphics_pipeline_class_init (GrdRdpDvcGraphicsPipelineClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GrdRdpDvcClass *dvc_class = GRD_RDP_DVC_CLASS (klass); + + object_class->dispose = grd_rdp_dvc_graphics_pipeline_dispose; + object_class->finalize = grd_rdp_dvc_graphics_pipeline_finalize; + + dvc_class->maybe_init = grd_rdp_dvc_graphics_pipeline_maybe_init; +} diff --git a/grd-rdp-dvc-graphics-pipeline.h b/grd-rdp-dvc-graphics-pipeline.h new file mode 100644 index 0000000..f97cb03 --- /dev/null +++ b/grd-rdp-dvc-graphics-pipeline.h @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2021 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include + +#include "grd-rdp-dvc.h" +#include "grd-types.h" + +#define GRD_TYPE_RDP_DVC_GRAPHICS_PIPELINE (grd_rdp_dvc_graphics_pipeline_get_type ()) +G_DECLARE_FINAL_TYPE (GrdRdpDvcGraphicsPipeline, grd_rdp_dvc_graphics_pipeline, + GRD, RDP_DVC_GRAPHICS_PIPELINE, GrdRdpDvc) + +GrdRdpDvcGraphicsPipeline *grd_rdp_dvc_graphics_pipeline_new (GrdSessionRdp *session_rdp, + GrdRdpRenderer *renderer, + GrdRdpDvcHandler *dvc_handler, + HANDLE vcm, + rdpContext *rdp_context, + GrdRdpNetworkAutodetection *network_autodetection, + wStream *encode_stream, + RFX_CONTEXT *rfx_context); + +void grd_rdp_dvc_graphics_pipeline_get_capabilities (GrdRdpDvcGraphicsPipeline *graphics_pipeline, + gboolean *have_avc444, + gboolean *have_avc420); + +void grd_rdp_dvc_graphics_pipeline_set_hwaccel_nvidia (GrdRdpDvcGraphicsPipeline *graphics_pipeline, + GrdHwAccelNvidia *hwaccel_nvidia); + +void grd_rdp_dvc_graphics_pipeline_create_surface (GrdRdpDvcGraphicsPipeline *graphics_pipeline, + GrdRdpGfxSurface *gfx_surface); + +void grd_rdp_dvc_graphics_pipeline_delete_surface (GrdRdpDvcGraphicsPipeline *graphics_pipeline, + GrdRdpGfxSurface *gfx_surface); + +GrdRdpGfxSurface *grd_rdp_dvc_graphics_pipeline_acquire_gfx_surface (GrdRdpDvcGraphicsPipeline *graphics_pipeline, + GrdRdpSurface *rdp_surface); + +void grd_rdp_dvc_graphics_pipeline_reset_graphics (GrdRdpDvcGraphicsPipeline *graphics_pipeline, + uint32_t width, + uint32_t height, + MONITOR_DEF *monitors, + uint32_t n_monitors); + +void grd_rdp_dvc_graphics_pipeline_notify_new_round_trip_time (GrdRdpDvcGraphicsPipeline *graphics_pipeline, + uint64_t round_trip_time_us); + +void grd_rdp_dvc_graphics_pipeline_submit_frame (GrdRdpDvcGraphicsPipeline *graphics_pipeline, + GrdRdpFrame *rdp_frame); + +gboolean grd_rdp_dvc_graphics_pipeline_refresh_gfx (GrdRdpDvcGraphicsPipeline *graphics_pipeline, + GrdRdpSurface *rdp_surface, + GrdRdpRenderContext *render_context, + GrdRdpLegacyBuffer *buffer); diff --git a/grd-rdp-dvc-handler.c b/grd-rdp-dvc-handler.c new file mode 100644 index 0000000..b344234 --- /dev/null +++ b/grd-rdp-dvc-handler.c @@ -0,0 +1,328 @@ +/* + * Copyright (C) 2022 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-rdp-dvc-handler.h" + +typedef struct _DVCSubscription +{ + gboolean notified; + + GrdRdpDVCCreationStatusCallback callback; + gpointer user_data; +} DVCSubscription; + +typedef struct _DVCNotification +{ + int32_t creation_status; + gboolean pending_status; + + GHashTable *subscriptions; + uint32_t next_subscription_id; +} DVCNotification; + +struct _GrdRdpDvcHandler +{ + GObject parent; + + GMutex dvc_notification_mutex; + GHashTable *dvc_table; + GSource *dvc_notification_source; +}; + +G_DEFINE_TYPE (GrdRdpDvcHandler, grd_rdp_dvc_handler, + G_TYPE_OBJECT) + +static DVCNotification * +dvc_notification_new (void) +{ + DVCNotification *dvc_notification; + + dvc_notification = g_new0 (DVCNotification, 1); + dvc_notification->pending_status = TRUE; + dvc_notification->subscriptions = g_hash_table_new_full (NULL, NULL, + NULL, g_free); + + return dvc_notification; +} + +static uint32_t +get_next_free_dvc_subscription_id (DVCNotification *dvc_notification) +{ + uint32_t subscription_id = dvc_notification->next_subscription_id; + + while (g_hash_table_contains (dvc_notification->subscriptions, + GUINT_TO_POINTER (subscription_id))) + ++subscription_id; + + dvc_notification->next_subscription_id = subscription_id + 1; + + return subscription_id; +} + +static uint32_t +dvc_notification_add_subscription (DVCNotification *dvc_notification, + DVCSubscription *dvc_subscription) +{ + uint32_t subscription_id; + + subscription_id = get_next_free_dvc_subscription_id (dvc_notification); + g_hash_table_insert (dvc_notification->subscriptions, + GUINT_TO_POINTER (subscription_id), dvc_subscription); + + return subscription_id; +} + +uint32_t +grd_rdp_dvc_handler_subscribe_dvc_creation_status (GrdRdpDvcHandler *dvc_handler, + uint32_t channel_id, + GrdRdpDVCCreationStatusCallback callback, + gpointer callback_user_data) +{ + DVCNotification *dvc_notification; + g_autofree DVCSubscription *dvc_subscription = NULL; + uint32_t subscription_id; + gboolean pending_notification = FALSE; + + dvc_subscription = g_new0 (DVCSubscription, 1); + dvc_subscription->callback = callback; + dvc_subscription->user_data = callback_user_data; + + g_mutex_lock (&dvc_handler->dvc_notification_mutex); + if (g_hash_table_lookup_extended (dvc_handler->dvc_table, + GUINT_TO_POINTER (channel_id), + NULL, (gpointer *) &dvc_notification)) + { + subscription_id = + dvc_notification_add_subscription (dvc_notification, + g_steal_pointer (&dvc_subscription)); + + if (!dvc_notification->pending_status) + pending_notification = TRUE; + } + else + { + dvc_notification = dvc_notification_new (); + + subscription_id = + dvc_notification_add_subscription (dvc_notification, + g_steal_pointer (&dvc_subscription)); + + g_hash_table_insert (dvc_handler->dvc_table, + GUINT_TO_POINTER (channel_id), dvc_notification); + } + g_mutex_unlock (&dvc_handler->dvc_notification_mutex); + + if (pending_notification) + g_source_set_ready_time (dvc_handler->dvc_notification_source, 0); + + return subscription_id; +} + +void +grd_rdp_dvc_handler_unsubscribe_dvc_creation_status (GrdRdpDvcHandler *dvc_handler, + uint32_t channel_id, + uint32_t subscription_id) +{ + DVCNotification *dvc_notification; + + g_mutex_lock (&dvc_handler->dvc_notification_mutex); + if (!g_hash_table_lookup_extended (dvc_handler->dvc_table, + GUINT_TO_POINTER (channel_id), + NULL, (gpointer *) &dvc_notification)) + g_assert_not_reached (); + + g_hash_table_remove (dvc_notification->subscriptions, + GUINT_TO_POINTER (subscription_id)); + g_mutex_unlock (&dvc_handler->dvc_notification_mutex); +} + +static BOOL +dvc_creation_status (void *user_data, + uint32_t channel_id, + int32_t creation_status) +{ + GrdRdpDvcHandler *dvc_handler = user_data; + DVCNotification *dvc_notification; + gboolean pending_notification = FALSE; + + g_debug ("[RDP.DRDYNVC] DVC channel id %u creation status: %i", + channel_id, creation_status); + + g_mutex_lock (&dvc_handler->dvc_notification_mutex); + if (g_hash_table_lookup_extended (dvc_handler->dvc_table, + GUINT_TO_POINTER (channel_id), + NULL, (gpointer *) &dvc_notification)) + { + if (dvc_notification->pending_status) + { + dvc_notification->creation_status = creation_status; + dvc_notification->pending_status = FALSE; + + if (g_hash_table_size (dvc_notification->subscriptions) > 0) + pending_notification = TRUE; + } + else + { + g_warning ("[RDP.DRDYNVC] Status of channel %u already known. " + "Discarding result", channel_id); + } + } + else + { + dvc_notification = dvc_notification_new (); + + dvc_notification->creation_status = creation_status; + dvc_notification->pending_status = FALSE; + + g_hash_table_insert (dvc_handler->dvc_table, + GUINT_TO_POINTER (channel_id), dvc_notification); + } + g_mutex_unlock (&dvc_handler->dvc_notification_mutex); + + if (pending_notification) + g_source_set_ready_time (dvc_handler->dvc_notification_source, 0); + + return TRUE; +} + +GrdRdpDvcHandler * +grd_rdp_dvc_handler_new (HANDLE vcm) +{ + GrdRdpDvcHandler *dvc_handler; + + dvc_handler = g_object_new (GRD_TYPE_RDP_DVC_HANDLER, NULL); + + WTSVirtualChannelManagerSetDVCCreationCallback (vcm, dvc_creation_status, + dvc_handler); + + return dvc_handler; +} + +static void +grd_rdp_dvc_handler_dispose (GObject *object) +{ + GrdRdpDvcHandler *dvc_handler = GRD_RDP_DVC_HANDLER (object); + + if (dvc_handler->dvc_notification_source) + { + g_source_destroy (dvc_handler->dvc_notification_source); + g_clear_pointer (&dvc_handler->dvc_notification_source, g_source_unref); + } + + g_clear_pointer (&dvc_handler->dvc_table, g_hash_table_unref); + + G_OBJECT_CLASS (grd_rdp_dvc_handler_parent_class)->dispose (object); +} + +static void +grd_rdp_dvc_handler_finalize (GObject *object) +{ + GrdRdpDvcHandler *dvc_handler = GRD_RDP_DVC_HANDLER (object); + + g_mutex_clear (&dvc_handler->dvc_notification_mutex); + + G_OBJECT_CLASS (grd_rdp_dvc_handler_parent_class)->finalize (object); +} + +static void +dvc_notification_free (gpointer data) +{ + DVCNotification *dvc_notification = data; + + g_clear_pointer (&dvc_notification->subscriptions, g_hash_table_unref); + + g_free (dvc_notification); +} + +static gboolean +notify_channels (gpointer user_data) +{ + GrdRdpDvcHandler *dvc_handler = user_data; + GHashTableIter iter; + DVCNotification *dvc_notification; + + g_mutex_lock (&dvc_handler->dvc_notification_mutex); + g_hash_table_iter_init (&iter, dvc_handler->dvc_table); + while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &dvc_notification)) + { + GHashTableIter iter2; + DVCSubscription *dvc_subscription; + + if (dvc_notification->pending_status) + continue; + + g_hash_table_iter_init (&iter2, dvc_notification->subscriptions); + while (g_hash_table_iter_next (&iter2, NULL, (gpointer *) &dvc_subscription)) + { + if (dvc_subscription->notified) + continue; + + dvc_subscription->callback (dvc_subscription->user_data, + dvc_notification->creation_status); + + dvc_subscription->notified = TRUE; + } + } + g_mutex_unlock (&dvc_handler->dvc_notification_mutex); + + return G_SOURCE_CONTINUE; +} + +static gboolean +source_dispatch (GSource *source, + GSourceFunc callback, + gpointer user_data) +{ + g_source_set_ready_time (source, -1); + + return callback (user_data); +} + +static GSourceFuncs source_funcs = +{ + .dispatch = source_dispatch, +}; + +static void +grd_rdp_dvc_handler_init (GrdRdpDvcHandler *dvc_handler) +{ + dvc_handler->dvc_table = + g_hash_table_new_full (NULL, NULL, + NULL, dvc_notification_free); + + g_mutex_init (&dvc_handler->dvc_notification_mutex); + + dvc_handler->dvc_notification_source = g_source_new (&source_funcs, + sizeof (GSource)); + g_source_set_callback (dvc_handler->dvc_notification_source, + notify_channels, dvc_handler, NULL); + g_source_set_ready_time (dvc_handler->dvc_notification_source, -1); + g_source_attach (dvc_handler->dvc_notification_source, NULL); +} + +static void +grd_rdp_dvc_handler_class_init (GrdRdpDvcHandlerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = grd_rdp_dvc_handler_dispose; + object_class->finalize = grd_rdp_dvc_handler_finalize; +} diff --git a/grd-rdp-dvc-handler.h b/grd-rdp-dvc-handler.h new file mode 100644 index 0000000..9aebc77 --- /dev/null +++ b/grd-rdp-dvc-handler.h @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2022 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include +#include + +#define GRD_TYPE_RDP_DVC_HANDLER (grd_rdp_dvc_handler_get_type ()) +G_DECLARE_FINAL_TYPE (GrdRdpDvcHandler, grd_rdp_dvc_handler, + GRD, RDP_DVC_HANDLER, GObject) + +typedef void (* GrdRdpDVCCreationStatusCallback) (gpointer user_data, + int32_t creation_status); + +GrdRdpDvcHandler *grd_rdp_dvc_handler_new (HANDLE vcm); + +uint32_t grd_rdp_dvc_handler_subscribe_dvc_creation_status (GrdRdpDvcHandler *dvc_handler, + uint32_t channel_id, + GrdRdpDVCCreationStatusCallback callback, + gpointer callback_user_data); + +void grd_rdp_dvc_handler_unsubscribe_dvc_creation_status (GrdRdpDvcHandler *dvc_handler, + uint32_t channel_id, + uint32_t subscription_id); diff --git a/grd-rdp-dvc-input.c b/grd-rdp-dvc-input.c new file mode 100644 index 0000000..b563e10 --- /dev/null +++ b/grd-rdp-dvc-input.c @@ -0,0 +1,764 @@ +/* + * Copyright (C) 2025 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-rdp-dvc-input.h" + +#include + +#include "grd-rdp-layout-manager.h" + +#define MAX_TOUCH_CONTACTS 256 + +typedef enum +{ + INPUT_EVENT_TYPE_TOUCH, + INPUT_EVENT_TYPE_DISMISS_HOVERING_TOUCH_CONTACT, + INPUT_EVENT_TYPE_PEN, +} InputEventType; + +/* + * Contact states based on Touch Contact State Transitions + * ([MS-RDPEI] 3.1.1.1) + */ +typedef enum +{ + CONTACT_STATE_OUT_OF_RANGE = 0, + CONTACT_STATE_HOVERING, + CONTACT_STATE_ENGAGED, +} ContactState; + +typedef struct +{ + InputEventType event_type; + + /* INPUT_EVENT_TYPE_TOUCH */ + RDPINPUT_TOUCH_FRAME touch_frame; + + /* INPUT_EVENT_TYPE_DISMISS_HOVERING_TOUCH_CONTACT */ + uint8_t touch_contact_to_dismiss; + + /* INPUT_EVENT_TYPE_PEN */ + /* Missing libei-API to submit (multi-)pen events */ +} InputEvent; + +typedef struct +{ + ContactState contact_state; + + GrdTouchContact *touch_contact; + gboolean ignore_contact; +} TouchContext; + +struct _GrdRdpDvcInput +{ + GrdRdpDvc parent; + + RdpeiServerContext *rdpei_context; + gboolean channel_opened; + + GrdRdpLayoutManager *layout_manager; + GrdSession *session; + + GSource *event_source; + GAsyncQueue *event_queue; + + gboolean pending_touch_device_frame; + GList *touch_contacts_to_dispose; + + TouchContext touch_contexts[MAX_TOUCH_CONTACTS]; + + unsigned long touch_device_added_id; + unsigned long touch_device_removed_id; +}; + +G_DEFINE_TYPE (GrdRdpDvcInput, grd_rdp_dvc_input, + GRD_TYPE_RDP_DVC) + +static void +grd_rdp_dvc_input_maybe_init (GrdRdpDvc *dvc) +{ + GrdRdpDvcInput *input = GRD_RDP_DVC_INPUT (dvc); + RdpeiServerContext *rdpei_context; + + if (input->channel_opened) + return; + + rdpei_context = input->rdpei_context; + if (rdpei_context->Open (rdpei_context)) + { + g_warning ("[RDP.INPUT] Failed to open channel. " + "Terminating protocol"); + grd_rdp_dvc_queue_channel_tear_down (GRD_RDP_DVC (input)); + return; + } + input->channel_opened = TRUE; +} + +static void +dvc_creation_status (gpointer user_data, + int32_t creation_status) +{ + GrdRdpDvcInput *input = user_data; + RdpeiServerContext *rdpei_context = input->rdpei_context; + + if (creation_status < 0) + { + g_debug ("[RDP.INPUT] Failed to open channel (CreationStatus %i). " + "Terminating protocol", creation_status); + grd_rdp_dvc_queue_channel_tear_down (GRD_RDP_DVC (input)); + return; + } + + rdpei_server_send_sc_ready (rdpei_context, RDPINPUT_PROTOCOL_V300, + SC_READY_MULTIPEN_INJECTION_SUPPORTED); +} + +static BOOL +input_channel_id_assigned (RdpeiServerContext *rdpei_context, + uint32_t channel_id) +{ + GrdRdpDvcInput *input = rdpei_context->user_data; + GrdRdpDvc *dvc = GRD_RDP_DVC (input); + + g_debug ("[RDP.INPUT] DVC channel id assigned to id %u", channel_id); + + grd_rdp_dvc_subscribe_creation_status (dvc, channel_id, + dvc_creation_status, + input); + + return TRUE; +} + +static uint32_t +input_client_ready (RdpeiServerContext *rdpei_context) +{ + g_message ("[RDP.INPUT] Client version: 0x%08X, Flags: 0x%08X, " + "Maximum simultaneous touch contacts: %u", + rdpei_context->clientVersion, rdpei_context->protocolFlags, + rdpei_context->maxTouchPoints); + + return CHANNEL_RC_OK; +} + +static uint32_t +input_touch (RdpeiServerContext *rdpei_context, + const RDPINPUT_TOUCH_EVENT *touch_event) +{ + GrdRdpDvcInput *input = rdpei_context->user_data; + uint16_t i; + + for (i = 0; i < touch_event->frameCount; ++i) + { + RDPINPUT_TOUCH_FRAME *touch_frame = &touch_event->frames[i]; + InputEvent *input_event; + + if (touch_frame->contactCount == 0) + continue; + + input_event = g_new0 (InputEvent, 1); + input_event->event_type = INPUT_EVENT_TYPE_TOUCH; + input_event->touch_frame = *touch_frame; + input_event->touch_frame.contacts = + g_memdup2 (touch_frame->contacts, touch_frame->contactCount * + sizeof (RDPINPUT_CONTACT_DATA)); + + g_async_queue_push (input->event_queue, input_event); + g_source_set_ready_time (input->event_source, 0); + } + + return CHANNEL_RC_OK; +} + +static uint32_t +input_dismiss_hovering_touch_contact (RdpeiServerContext *rdpei_context, + uint8_t contact_id) +{ + GrdRdpDvcInput *input = rdpei_context->user_data; + InputEvent *input_event; + + input_event = g_new0 (InputEvent, 1); + input_event->event_type = INPUT_EVENT_TYPE_DISMISS_HOVERING_TOUCH_CONTACT; + input_event->touch_contact_to_dismiss = contact_id; + + g_async_queue_push (input->event_queue, input_event); + g_source_set_ready_time (input->event_source, 0); + + return CHANNEL_RC_OK; +} + +static uint32_t +input_pen (RdpeiServerContext *rdpei_context, + const RDPINPUT_PEN_EVENT *pen_event) +{ + /* Missing libei-API to submit (multi-)pen events */ + + return CHANNEL_RC_OK; +} + +static void +on_touch_device_added (GrdSession *session, + GrdRdpDvcInput *input) +{ + g_source_set_ready_time (input->event_source, 0); +} + +static void +on_touch_device_removed (GrdSession *session, + GrdRdpDvcInput *input) +{ + uint16_t i; + + for (i = 0; i < MAX_TOUCH_CONTACTS; ++i) + { + TouchContext *touch_context = &input->touch_contexts[i]; + GrdTouchContact *touch_contact; + + if (touch_context->contact_state == CONTACT_STATE_OUT_OF_RANGE) + continue; + + g_debug ("[RDP.INPUT] Resetting touch contact %u", i); + + touch_contact = g_steal_pointer (&touch_context->touch_contact); + grd_session_release_touch_contact (session, touch_contact); + + touch_context->contact_state = CONTACT_STATE_OUT_OF_RANGE; + + /* + * When the monitor layout changes, Windows App for the iPhone resets + * each contact state. This also applies to touch contacts, which are + * still pressed by the user. + * However, for some reason, this client still continues to send + * touch-motion events for the reset touch contact. The touch position + * in these events is always the same and does not reflect the actual + * reality. + * This continues to happen until the user taps again, in which case the + * client stops these spurious touch-motion events and sends a + * touch-down event. + * So, ignore these spurious touch-motion events until a valid + * contact-flag combination is sent for the 'Out of Range'-state. + */ + touch_context->ignore_contact = TRUE; + } +} + +GrdRdpDvcInput * +grd_rdp_dvc_input_new (GrdRdpLayoutManager *layout_manager, + GrdSessionRdp *session_rdp, + GrdRdpDvcHandler *dvc_handler, + HANDLE vcm) +{ + GrdRdpDvcInput *input; + RdpeiServerContext *rdpei_context; + + input = g_object_new (GRD_TYPE_RDP_DVC_INPUT, NULL); + rdpei_context = rdpei_server_context_new (vcm); + if (!rdpei_context) + g_error ("[RDP.INPUT] Failed to allocate server context (OOM)"); + + input->rdpei_context = rdpei_context; + input->layout_manager = layout_manager; + input->session = GRD_SESSION (session_rdp); + + grd_rdp_dvc_initialize_base (GRD_RDP_DVC (input), + dvc_handler, session_rdp, + GRD_RDP_CHANNEL_INPUT); + + rdpei_context->onChannelIdAssigned = input_channel_id_assigned; + rdpei_context->onClientReady = input_client_ready; + rdpei_context->onTouchEvent = input_touch; + rdpei_context->onTouchReleased = input_dismiss_hovering_touch_contact; + rdpei_context->onPenEvent = input_pen; + rdpei_context->user_data = input; + + input->touch_device_added_id = + g_signal_connect (input->session, "touch-device-added", + G_CALLBACK (on_touch_device_added), + input); + input->touch_device_removed_id = + g_signal_connect (input->session, "touch-device-removed", + G_CALLBACK (on_touch_device_removed), + input); + + return input; +} + +static void +ensure_queued_touch_device_frame (GrdRdpDvcInput *input) +{ + input->pending_touch_device_frame = TRUE; +} + +static void +queue_touch_contact_disposal (GrdRdpDvcInput *input, + TouchContext *touch_context) +{ + GrdTouchContact *touch_contact; + + g_assert (touch_context->touch_contact); + + touch_contact = g_steal_pointer (&touch_context->touch_contact); + input->touch_contacts_to_dispose = + g_list_prepend (input->touch_contacts_to_dispose, touch_contact); + + touch_context->ignore_contact = FALSE; +} + +/* + * Automata based on Touch Contact State Transitions + * ([MS-RDPEI] 3.1.1.1) + */ +static void +handle_touch_state_out_of_range (GrdRdpDvcInput *input, + RDPINPUT_CONTACT_DATA *touch_contact_data) +{ + uint8_t contact_id = touch_contact_data->contactId; + TouchContext *touch_context = &input->touch_contexts[contact_id]; + GrdSession *session = input->session; + + g_assert (!touch_context->touch_contact); + + if (touch_contact_data->contactFlags == (RDPINPUT_CONTACT_FLAG_DOWN | + RDPINPUT_CONTACT_FLAG_INRANGE | + RDPINPUT_CONTACT_FLAG_INCONTACT)) + { + GrdRdpLayoutManager *layout_manager = input->layout_manager; + GrdEventMotionAbs motion_abs = {}; + GrdStream *stream = NULL; + + touch_context->touch_contact = + grd_session_acquire_touch_contact (session); + touch_context->ignore_contact = FALSE; + + if (grd_rdp_layout_manager_transform_position (layout_manager, + touch_contact_data->x, + touch_contact_data->y, + &stream, &motion_abs)) + { + grd_session_notify_touch_down (session, touch_context->touch_contact, + stream, &motion_abs); + ensure_queued_touch_device_frame (input); + } + else + { + /* + * The client does not know, when a specific touch contact is useless + * due to its position not being transformable. As a result, it will + * deem it as any other touch contact. So ignore all assigned actions + * and just let the touch contact run through all automata states. + * + * See also 3.2.5.3 Processing an RDPINPUT_TOUCH_EVENT_PDU Message + * ([MS-RDPEI]) + */ + touch_context->ignore_contact = TRUE; + } + + touch_context->contact_state = CONTACT_STATE_ENGAGED; + } + else if (touch_contact_data->contactFlags == (RDPINPUT_CONTACT_FLAG_UPDATE | + RDPINPUT_CONTACT_FLAG_INRANGE)) + { + touch_context->touch_contact = + grd_session_acquire_touch_contact (session); + touch_context->ignore_contact = FALSE; + + touch_context->contact_state = CONTACT_STATE_HOVERING; + } + else if (!touch_context->ignore_contact) + { + g_warning ("[RDP.INPUT] Protocol violation: Client sent invalid contact " + "flags 0x%08X in state 'Out of Range' for contact %u", + touch_contact_data->contactFlags, contact_id); + } +} + +/* + * Automata based on Touch Contact State Transitions + * ([MS-RDPEI] 3.1.1.1) + */ +static void +handle_touch_state_hovering (GrdRdpDvcInput *input, + RDPINPUT_CONTACT_DATA *touch_contact_data) +{ + uint8_t contact_id = touch_contact_data->contactId; + TouchContext *touch_context = &input->touch_contexts[contact_id]; + + g_assert (touch_context->touch_contact); + + if (touch_contact_data->contactFlags == (RDPINPUT_CONTACT_FLAG_DOWN | + RDPINPUT_CONTACT_FLAG_INRANGE | + RDPINPUT_CONTACT_FLAG_INCONTACT)) + { + GrdRdpLayoutManager *layout_manager = input->layout_manager; + GrdSession *session = input->session; + GrdEventMotionAbs motion_abs = {}; + GrdStream *stream = NULL; + + if (grd_rdp_layout_manager_transform_position (layout_manager, + touch_contact_data->x, + touch_contact_data->y, + &stream, &motion_abs)) + { + grd_session_notify_touch_down (session, touch_context->touch_contact, + stream, &motion_abs); + ensure_queued_touch_device_frame (input); + } + else + { + /* + * The client does not know, when a specific touch contact is useless + * due to its position not being transformable. As a result, it will + * deem it as any other touch contact. So ignore all assigned actions + * and just let the touch contact run through all automata states. + * + * See also 3.2.5.3 Processing an RDPINPUT_TOUCH_EVENT_PDU Message + * ([MS-RDPEI]) + */ + touch_context->ignore_contact = TRUE; + } + + touch_context->contact_state = CONTACT_STATE_ENGAGED; + } + else if (touch_contact_data->contactFlags == (RDPINPUT_CONTACT_FLAG_UPDATE | + RDPINPUT_CONTACT_FLAG_INRANGE)) + { + touch_context->contact_state = CONTACT_STATE_HOVERING; + } + else if (touch_contact_data->contactFlags == (RDPINPUT_CONTACT_FLAG_UPDATE | + RDPINPUT_CONTACT_FLAG_CANCELED)) + { + queue_touch_contact_disposal (input, touch_context); + + touch_context->contact_state = CONTACT_STATE_OUT_OF_RANGE; + } + else if (touch_contact_data->contactFlags == RDPINPUT_CONTACT_FLAG_UPDATE) + { + queue_touch_contact_disposal (input, touch_context); + + touch_context->contact_state = CONTACT_STATE_OUT_OF_RANGE; + } + else + { + g_warning ("[RDP.INPUT] Protocol violation: Client sent invalid contact " + "flags 0x%08X in state 'Hovering' for contact %u", + touch_contact_data->contactFlags, contact_id); + } +} + +/* + * Automata based on Touch Contact State Transitions + * ([MS-RDPEI] 3.1.1.1) + */ +static void +handle_touch_state_engaged (GrdRdpDvcInput *input, + RDPINPUT_CONTACT_DATA *touch_contact_data) +{ + uint8_t contact_id = touch_contact_data->contactId; + TouchContext *touch_context = &input->touch_contexts[contact_id]; + GrdSession *session = input->session; + + g_assert (touch_context->touch_contact); + + if (touch_contact_data->contactFlags == (RDPINPUT_CONTACT_FLAG_UPDATE | + RDPINPUT_CONTACT_FLAG_INRANGE | + RDPINPUT_CONTACT_FLAG_INCONTACT)) + { + GrdRdpLayoutManager *layout_manager = input->layout_manager; + GrdEventMotionAbs motion_abs = {}; + GrdStream *stream = NULL; + + if (touch_context->ignore_contact || + !grd_rdp_layout_manager_transform_position (layout_manager, + touch_contact_data->x, + touch_contact_data->y, + &stream, &motion_abs)) + return; + + grd_session_notify_touch_motion (session, touch_context->touch_contact, + stream, &motion_abs); + ensure_queued_touch_device_frame (input); + + touch_context->contact_state = CONTACT_STATE_ENGAGED; + } + else if (touch_contact_data->contactFlags == (RDPINPUT_CONTACT_FLAG_UP | + RDPINPUT_CONTACT_FLAG_INRANGE)) + { + if (!touch_context->ignore_contact) + { + grd_session_notify_touch_up (session, touch_context->touch_contact); + ensure_queued_touch_device_frame (input); + } + + queue_touch_contact_disposal (input, touch_context); + touch_context->touch_contact = + grd_session_acquire_touch_contact (session); + + touch_context->contact_state = CONTACT_STATE_HOVERING; + } + else if (touch_contact_data->contactFlags == (RDPINPUT_CONTACT_FLAG_UP | + RDPINPUT_CONTACT_FLAG_CANCELED)) + { + if (!touch_context->ignore_contact) + { + grd_session_notify_touch_cancel (session, touch_context->touch_contact); + ensure_queued_touch_device_frame (input); + } + queue_touch_contact_disposal (input, touch_context); + + touch_context->contact_state = CONTACT_STATE_OUT_OF_RANGE; + } + else if (touch_contact_data->contactFlags == RDPINPUT_CONTACT_FLAG_UP) + { + if (!touch_context->ignore_contact) + { + grd_session_notify_touch_up (session, touch_context->touch_contact); + ensure_queued_touch_device_frame (input); + } + queue_touch_contact_disposal (input, touch_context); + + touch_context->contact_state = CONTACT_STATE_OUT_OF_RANGE; + } + else + { + g_warning ("[RDP.INPUT] Protocol violation: Client sent invalid contact " + "flags 0x%08X in state 'Engaged' for contact %u", + touch_contact_data->contactFlags, contact_id); + } +} + +static void +process_touch_contact (GrdRdpDvcInput *input, + RDPINPUT_CONTACT_DATA *touch_contact_data) +{ + uint8_t contact_id = touch_contact_data->contactId; + TouchContext *touch_context = &input->touch_contexts[contact_id]; + + /* + * The optional contact-rect, orientation, and pressure fields are unhandled + * due to missing libei-API to submit them + */ + + switch (touch_context->contact_state) + { + case CONTACT_STATE_OUT_OF_RANGE: + handle_touch_state_out_of_range (input, touch_contact_data); + break; + case CONTACT_STATE_HOVERING: + handle_touch_state_hovering (input, touch_contact_data); + break; + case CONTACT_STATE_ENGAGED: + handle_touch_state_engaged (input, touch_contact_data); + break; + } +} + +static void +maybe_notify_touch_device_frame (GrdRdpDvcInput *input) +{ + GrdSession *session = input->session; + + if (!input->pending_touch_device_frame) + return; + + grd_session_notify_touch_device_frame (session); + input->pending_touch_device_frame = FALSE; +} + +static void +dispose_touch_contacts (GrdRdpDvcInput *input) +{ + GrdSession *session = input->session; + GList *l; + + for (l = input->touch_contacts_to_dispose; l; l = l->next) + { + GrdTouchContact *touch_contact = l->data; + + grd_session_release_touch_contact (session, touch_contact); + } + g_clear_pointer (&input->touch_contacts_to_dispose, g_list_free); +} + +static void +process_touch_frame (GrdRdpDvcInput *input, + RDPINPUT_TOUCH_FRAME *touch_frame) +{ + uint32_t i; + + for (i = 0; i < touch_frame->contactCount; ++i) + process_touch_contact (input, &touch_frame->contacts[i]); + + maybe_notify_touch_device_frame (input); + dispose_touch_contacts (input); +} + +static void +dismiss_touch_contact (GrdRdpDvcInput *input, + uint8_t contact_id) +{ + TouchContext *touch_context = &input->touch_contexts[contact_id]; + + if (!touch_context->touch_contact) + g_assert (touch_context->contact_state == CONTACT_STATE_OUT_OF_RANGE); + + /* Client did not keep track of touch contact properly */ + if (!touch_context->touch_contact) + return; + + g_assert (touch_context->contact_state != CONTACT_STATE_OUT_OF_RANGE); + + if (touch_context->contact_state == CONTACT_STATE_ENGAGED) + { + GrdSession *session = input->session; + + grd_session_notify_touch_cancel (session, touch_context->touch_contact); + ensure_queued_touch_device_frame (input); + } + queue_touch_contact_disposal (input, touch_context); + + maybe_notify_touch_device_frame (input); + dispose_touch_contacts (input); + + touch_context->contact_state = CONTACT_STATE_OUT_OF_RANGE; +} + +static void +input_event_free (InputEvent *input_event) +{ + g_clear_pointer (&input_event->touch_frame.contacts, g_free); + + g_free (input_event); +} + +static gboolean +handle_input_events (gpointer user_data) +{ + GrdRdpDvcInput *input = user_data; + InputEvent *input_event; + + if (!grd_session_has_touch_device (input->session)) + return G_SOURCE_CONTINUE; + + while ((input_event = g_async_queue_try_pop (input->event_queue))) + { + switch (input_event->event_type) + { + case INPUT_EVENT_TYPE_TOUCH: + process_touch_frame (input, &input_event->touch_frame); + break; + case INPUT_EVENT_TYPE_DISMISS_HOVERING_TOUCH_CONTACT: + dismiss_touch_contact (input, input_event->touch_contact_to_dismiss); + break; + case INPUT_EVENT_TYPE_PEN: + /* Missing libei-API to submit (multi-)pen events */ + g_assert_not_reached (); + break; + } + + input_event_free (input_event); + } + + return G_SOURCE_CONTINUE; +} + +static void +grd_rdp_dvc_input_dispose (GObject *object) +{ + GrdRdpDvcInput *input = GRD_RDP_DVC_INPUT (object); + GrdRdpDvc *dvc = GRD_RDP_DVC (input); + uint16_t i; + + if (input->channel_opened) + { + input->rdpei_context->Close (input->rdpei_context); + input->channel_opened = FALSE; + } + grd_rdp_dvc_maybe_unsubscribe_creation_status (dvc); + + g_clear_signal_handler (&input->touch_device_removed_id, input->session); + g_clear_signal_handler (&input->touch_device_added_id, input->session); + + if (input->event_source) + { + g_source_destroy (input->event_source); + g_clear_pointer (&input->event_source, g_source_unref); + } + + handle_input_events (input); + g_clear_pointer (&input->event_queue, g_async_queue_unref); + + g_assert (!input->touch_contacts_to_dispose); + + for (i = 0; i < MAX_TOUCH_CONTACTS; ++i) + { + TouchContext *touch_context = &input->touch_contexts[i]; + GrdTouchContact *touch_contact; + + touch_contact = g_steal_pointer (&touch_context->touch_contact); + if (touch_contact) + grd_session_release_touch_contact (input->session, touch_contact); + } + + g_clear_pointer (&input->rdpei_context, + rdpei_server_context_free); + + G_OBJECT_CLASS (grd_rdp_dvc_input_parent_class)->dispose (object); +} + +static gboolean +event_source_dispatch (GSource *source, + GSourceFunc callback, + gpointer user_data) +{ + g_source_set_ready_time (source, -1); + + return callback (user_data); +} + +static GSourceFuncs event_source_funcs = +{ + .dispatch = event_source_dispatch, +}; + +static void +grd_rdp_dvc_input_init (GrdRdpDvcInput *input) +{ + GSource *event_source; + + input->event_queue = g_async_queue_new (); + + event_source = g_source_new (&event_source_funcs, sizeof (GSource)); + g_source_set_callback (event_source, handle_input_events, + input, NULL); + g_source_set_ready_time (event_source, -1); + g_source_attach (event_source, NULL); + input->event_source = event_source; +} + +static void +grd_rdp_dvc_input_class_init (GrdRdpDvcInputClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GrdRdpDvcClass *dvc_class = GRD_RDP_DVC_CLASS (klass); + + object_class->dispose = grd_rdp_dvc_input_dispose; + + dvc_class->maybe_init = grd_rdp_dvc_input_maybe_init; +} diff --git a/grd-rdp-dvc-input.h b/grd-rdp-dvc-input.h new file mode 100644 index 0000000..5e856c0 --- /dev/null +++ b/grd-rdp-dvc-input.h @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2025 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include "grd-rdp-dvc.h" + +#define GRD_TYPE_RDP_DVC_INPUT (grd_rdp_dvc_input_get_type ()) +G_DECLARE_FINAL_TYPE (GrdRdpDvcInput, grd_rdp_dvc_input, + GRD, RDP_DVC_INPUT, GrdRdpDvc) + +GrdRdpDvcInput *grd_rdp_dvc_input_new (GrdRdpLayoutManager *layout_manager, + GrdSessionRdp *session_rdp, + GrdRdpDvcHandler *dvc_handler, + HANDLE vcm); diff --git a/grd-rdp-dvc-telemetry.c b/grd-rdp-dvc-telemetry.c new file mode 100644 index 0000000..696f9f1 --- /dev/null +++ b/grd-rdp-dvc-telemetry.c @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2022 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-rdp-dvc-telemetry.h" + +#include + +struct _GrdRdpDvcTelemetry +{ + GrdRdpDvc parent; + + TelemetryServerContext *telemetry_context; + gboolean channel_opened; +}; + +G_DEFINE_TYPE (GrdRdpDvcTelemetry, grd_rdp_dvc_telemetry, + GRD_TYPE_RDP_DVC) + +static void +grd_rdp_dvc_telemetry_maybe_init (GrdRdpDvc *dvc) +{ + GrdRdpDvcTelemetry *telemetry = GRD_RDP_DVC_TELEMETRY (dvc); + TelemetryServerContext *telemetry_context; + + if (telemetry->channel_opened) + return; + + telemetry_context = telemetry->telemetry_context; + if (telemetry_context->Open (telemetry_context)) + { + g_warning ("[RDP.TELEMETRY] Failed to open channel. " + "Terminating protocol"); + grd_rdp_dvc_queue_channel_tear_down (GRD_RDP_DVC (telemetry)); + return; + } + telemetry->channel_opened = TRUE; +} + +static void +dvc_creation_status (gpointer user_data, + int32_t creation_status) +{ + GrdRdpDvcTelemetry *telemetry = user_data; + + if (creation_status < 0) + { + g_debug ("[RDP.TELEMETRY] Failed to open channel (CreationStatus %i). " + "Terminating protocol", creation_status); + grd_rdp_dvc_queue_channel_tear_down (GRD_RDP_DVC (telemetry)); + } +} + +static BOOL +telemetry_channel_id_assigned (TelemetryServerContext *telemetry_context, + uint32_t channel_id) +{ + GrdRdpDvcTelemetry *telemetry = telemetry_context->userdata; + GrdRdpDvc *dvc = GRD_RDP_DVC (telemetry); + + g_debug ("[RDP.TELEMETRY] DVC channel id assigned to id %u", channel_id); + + grd_rdp_dvc_subscribe_creation_status (dvc, channel_id, + dvc_creation_status, + telemetry); + + return TRUE; +} + +static uint32_t +telemetry_rdp_telemetry (TelemetryServerContext *telemetry_context, + const TELEMETRY_RDP_TELEMETRY_PDU *rdp_telemetry) +{ + GrdRdpDvcTelemetry *telemetry = telemetry_context->userdata; + + g_debug ("[RDP.TELEMETRY] Client connection metrics: " + "PromptForCredentialsMillis: %u, " + "PromptForCredentialsDoneMillis: %u, " + "GraphicsChannelOpenedMillis: %u, " + "FirstGraphicsReceivedMillis: %u", + rdp_telemetry->PromptForCredentialsMillis, + rdp_telemetry->PromptForCredentialsDoneMillis, + rdp_telemetry->GraphicsChannelOpenedMillis, + rdp_telemetry->FirstGraphicsReceivedMillis); + + g_debug ("[RDP.TELEMETRY] diff (CredentialsDone, RDPGFX opened): %ums; " + "diff (RDPGFX opened, first graphics): %ums; " + "diff (CredentialsDone, first graphics): %ums", + rdp_telemetry->GraphicsChannelOpenedMillis - + rdp_telemetry->PromptForCredentialsDoneMillis, + rdp_telemetry->FirstGraphicsReceivedMillis - + rdp_telemetry->GraphicsChannelOpenedMillis, + rdp_telemetry->FirstGraphicsReceivedMillis - + rdp_telemetry->PromptForCredentialsDoneMillis); + + grd_rdp_dvc_queue_channel_tear_down (GRD_RDP_DVC (telemetry)); + + return CHANNEL_RC_OK; +} + +GrdRdpDvcTelemetry * +grd_rdp_dvc_telemetry_new (GrdSessionRdp *session_rdp, + GrdRdpDvcHandler *dvc_handler, + HANDLE vcm, + rdpContext *rdp_context) +{ + GrdRdpDvcTelemetry *telemetry; + TelemetryServerContext *telemetry_context; + + telemetry = g_object_new (GRD_TYPE_RDP_DVC_TELEMETRY, NULL); + telemetry_context = telemetry_server_context_new (vcm); + if (!telemetry_context) + g_error ("[RDP.TELEMETRY] Failed to allocate server context (OOM)"); + + telemetry->telemetry_context = telemetry_context; + + grd_rdp_dvc_initialize_base (GRD_RDP_DVC (telemetry), + dvc_handler, session_rdp, + GRD_RDP_CHANNEL_TELEMETRY); + + telemetry_context->ChannelIdAssigned = telemetry_channel_id_assigned; + telemetry_context->RdpTelemetry = telemetry_rdp_telemetry; + telemetry_context->rdpcontext = rdp_context; + telemetry_context->userdata = telemetry; + + return telemetry; +} + +static void +grd_rdp_dvc_telemetry_dispose (GObject *object) +{ + GrdRdpDvcTelemetry *telemetry = GRD_RDP_DVC_TELEMETRY (object); + GrdRdpDvc *dvc = GRD_RDP_DVC (telemetry); + + if (telemetry->channel_opened) + { + telemetry->telemetry_context->Close (telemetry->telemetry_context); + telemetry->channel_opened = FALSE; + } + grd_rdp_dvc_maybe_unsubscribe_creation_status (dvc); + + g_clear_pointer (&telemetry->telemetry_context, + telemetry_server_context_free); + + G_OBJECT_CLASS (grd_rdp_dvc_telemetry_parent_class)->dispose (object); +} + +static void +grd_rdp_dvc_telemetry_init (GrdRdpDvcTelemetry *telemetry) +{ +} + +static void +grd_rdp_dvc_telemetry_class_init (GrdRdpDvcTelemetryClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GrdRdpDvcClass *dvc_class = GRD_RDP_DVC_CLASS (klass); + + object_class->dispose = grd_rdp_dvc_telemetry_dispose; + + dvc_class->maybe_init = grd_rdp_dvc_telemetry_maybe_init; +} diff --git a/grd-rdp-dvc-telemetry.h b/grd-rdp-dvc-telemetry.h new file mode 100644 index 0000000..def4ffc --- /dev/null +++ b/grd-rdp-dvc-telemetry.h @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2022 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include "grd-rdp-dvc.h" + +#define GRD_TYPE_RDP_DVC_TELEMETRY (grd_rdp_dvc_telemetry_get_type ()) +G_DECLARE_FINAL_TYPE (GrdRdpDvcTelemetry, grd_rdp_dvc_telemetry, + GRD, RDP_DVC_TELEMETRY, GrdRdpDvc) + +GrdRdpDvcTelemetry *grd_rdp_dvc_telemetry_new (GrdSessionRdp *session_rdp, + GrdRdpDvcHandler *dvc_handler, + HANDLE vcm, + rdpContext *rdp_context); diff --git a/grd-rdp-dvc.c b/grd-rdp-dvc.c new file mode 100644 index 0000000..c4a6e28 --- /dev/null +++ b/grd-rdp-dvc.c @@ -0,0 +1,204 @@ +/* + * Copyright (C) 2025 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-rdp-dvc.h" + +typedef struct +{ + GrdRdpDvcHandler *dvc_handler; + GrdSessionRdp *session_rdp; + GrdRdpChannel channel; + + gboolean channel_unavailable; + + uint32_t channel_id; + uint32_t subscription_id; + gboolean is_subscribed; + + GSource *channel_teardown_source; +} GrdRdpDvcPrivate; + +G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (GrdRdpDvc, grd_rdp_dvc, + G_TYPE_OBJECT) + +void +grd_rdp_dvc_initialize_base (GrdRdpDvc *dvc, + GrdRdpDvcHandler *dvc_handler, + GrdSessionRdp *session_rdp, + GrdRdpChannel channel) +{ + GrdRdpDvcPrivate *priv = grd_rdp_dvc_get_instance_private (dvc); + + priv->dvc_handler = dvc_handler; + priv->session_rdp = session_rdp; + priv->channel = channel; +} + +void +grd_rdp_dvc_maybe_init (GrdRdpDvc *dvc) +{ + GrdRdpDvcPrivate *priv = grd_rdp_dvc_get_instance_private (dvc); + GrdRdpDvcClass *klass = GRD_RDP_DVC_GET_CLASS (dvc); + + if (priv->channel_unavailable) + return; + + klass->maybe_init (dvc); +} + +void +grd_rdp_dvc_queue_channel_tear_down (GrdRdpDvc *dvc) +{ + GrdRdpDvcPrivate *priv = grd_rdp_dvc_get_instance_private (dvc); + + priv->channel_unavailable = TRUE; + + g_source_set_ready_time (priv->channel_teardown_source, 0); +} + +void +grd_rdp_dvc_subscribe_creation_status (GrdRdpDvc *dvc, + uint32_t channel_id, + GrdRdpDVCCreationStatusCallback callback, + gpointer callback_user_data) +{ + GrdRdpDvcPrivate *priv = grd_rdp_dvc_get_instance_private (dvc); + + g_assert (!priv->is_subscribed); + + priv->channel_id = channel_id; + + priv->subscription_id = + grd_rdp_dvc_handler_subscribe_dvc_creation_status (priv->dvc_handler, + channel_id, + callback, + callback_user_data); + priv->is_subscribed = TRUE; +} + +void +grd_rdp_dvc_maybe_unsubscribe_creation_status (GrdRdpDvc *dvc) +{ + GrdRdpDvcPrivate *priv = grd_rdp_dvc_get_instance_private (dvc); + + if (!priv->is_subscribed) + return; + + grd_rdp_dvc_handler_unsubscribe_dvc_creation_status (priv->dvc_handler, + priv->channel_id, + priv->subscription_id); + priv->is_subscribed = FALSE; +} + +static void +grd_rdp_dvc_dispose (GObject *object) +{ + GrdRdpDvc *dvc = GRD_RDP_DVC (object); + GrdRdpDvcPrivate *priv = grd_rdp_dvc_get_instance_private (dvc); + + if (priv->channel_teardown_source) + { + g_source_destroy (priv->channel_teardown_source); + g_clear_pointer (&priv->channel_teardown_source, g_source_unref); + } + + G_OBJECT_CLASS (grd_rdp_dvc_parent_class)->dispose (object); +} + +static const char * +channel_to_string (GrdRdpChannel channel) +{ + switch (channel) + { + case GRD_RDP_CHANNEL_NONE: + g_assert_not_reached (); + break; + case GRD_RDP_CHANNEL_AUDIO_INPUT: + return "AUDIO_INPUT"; + case GRD_RDP_CHANNEL_AUDIO_PLAYBACK: + return "AUDIO_PLAYBACK"; + case GRD_RDP_CHANNEL_CAMERA: + return "CAM"; + case GRD_RDP_CHANNEL_CAMERA_ENUMERATOR: + return "CAM_ENUMERATOR"; + case GRD_RDP_CHANNEL_DISPLAY_CONTROL: + return "DISP"; + case GRD_RDP_CHANNEL_GRAPHICS_PIPELINE: + return "RDPGFX"; + case GRD_RDP_CHANNEL_INPUT: + return "INPUT"; + case GRD_RDP_CHANNEL_TELEMETRY: + return "TELEMETRY"; + } + + g_assert_not_reached (); +} + +static gboolean +tear_down_channel (gpointer user_data) +{ + GrdRdpDvc *dvc = user_data; + GrdRdpDvcPrivate *priv = grd_rdp_dvc_get_instance_private (dvc); + + g_debug ("[RDP.%s] Tearing down channel", channel_to_string (priv->channel)); + + g_clear_pointer (&priv->channel_teardown_source, g_source_unref); + grd_session_rdp_tear_down_channel (priv->session_rdp, priv->channel); + + return G_SOURCE_REMOVE; +} + +static gboolean +source_dispatch (GSource *source, + GSourceFunc callback, + gpointer user_data) +{ + g_source_set_ready_time (source, -1); + + return callback (user_data); +} + +static GSourceFuncs source_funcs = +{ + .dispatch = source_dispatch, +}; + +static void +grd_rdp_dvc_init (GrdRdpDvc *dvc) +{ + GrdRdpDvcPrivate *priv = grd_rdp_dvc_get_instance_private (dvc); + GSource *channel_teardown_source; + + channel_teardown_source = g_source_new (&source_funcs, sizeof (GSource)); + g_source_set_callback (channel_teardown_source, tear_down_channel, + dvc, NULL); + g_source_set_ready_time (channel_teardown_source, -1); + g_source_attach (channel_teardown_source, NULL); + priv->channel_teardown_source = channel_teardown_source; +} + +static void +grd_rdp_dvc_class_init (GrdRdpDvcClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = grd_rdp_dvc_dispose; +} diff --git a/grd-rdp-dvc.h b/grd-rdp-dvc.h new file mode 100644 index 0000000..7ae51e3 --- /dev/null +++ b/grd-rdp-dvc.h @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2025 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include + +#include "grd-rdp-dvc-handler.h" +#include "grd-session-rdp.h" + +#define GRD_TYPE_RDP_DVC (grd_rdp_dvc_get_type ()) +G_DECLARE_DERIVABLE_TYPE (GrdRdpDvc, grd_rdp_dvc, + GRD, RDP_DVC, GObject) + +struct _GrdRdpDvcClass +{ + GObjectClass parent_class; + + void (* maybe_init) (GrdRdpDvc *dvc); +}; + +void grd_rdp_dvc_initialize_base (GrdRdpDvc *dvc, + GrdRdpDvcHandler *dvc_handler, + GrdSessionRdp *session_rdp, + GrdRdpChannel channel); + +void grd_rdp_dvc_maybe_init (GrdRdpDvc *dvc); + +void grd_rdp_dvc_queue_channel_tear_down (GrdRdpDvc *dvc); + +void grd_rdp_dvc_subscribe_creation_status (GrdRdpDvc *dvc, + uint32_t channel_id, + GrdRdpDVCCreationStatusCallback callback, + gpointer callback_user_data); + +void grd_rdp_dvc_maybe_unsubscribe_creation_status (GrdRdpDvc *dvc); diff --git a/grd-rdp-event-queue.c b/grd-rdp-event-queue.c new file mode 100644 index 0000000..76eaaba --- /dev/null +++ b/grd-rdp-event-queue.c @@ -0,0 +1,485 @@ +/* + * Copyright (C) 2021 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-rdp-event-queue.h" + +#include +#include + +typedef enum _RdpEventType +{ + RDP_EVENT_TYPE_NONE, + RDP_EVENT_TYPE_INPUT_SYNC, + RDP_EVENT_TYPE_INPUT_KBD_KEYCODE, + RDP_EVENT_TYPE_INPUT_KBD_KEYSYM, + RDP_EVENT_TYPE_INPUT_PTR_MOTION, + RDP_EVENT_TYPE_INPUT_PTR_MOTION_ABS, + RDP_EVENT_TYPE_INPUT_PTR_BUTTON, + RDP_EVENT_TYPE_INPUT_PTR_AXIS, +} RdpEventType; + +typedef struct _RdpEvent +{ + RdpEventType type; + + /* RDP_EVENT_TYPE_INPUT_KBD_KEYCODE */ + struct + { + uint32_t keycode; + GrdKeyState state; + } input_kbd_keycode; + + /* RDP_EVENT_TYPE_INPUT_KBD_KEYSYM */ + struct + { + uint32_t keysym; + GrdKeyState state; + } input_kbd_keysym; + + /* RDP_EVENT_TYPE_INPUT_PTR_MOTION */ + struct + { + double dx; + double dy; + } input_ptr_motion; + + /* RDP_EVENT_TYPE_INPUT_PTR_MOTION_ABS */ + struct + { + GrdStream *stream; + GrdEventMotionAbs motion_abs; + } input_ptr_motion_abs; + + /* RDP_EVENT_TYPE_INPUT_PTR_BUTTON */ + struct + { + int32_t button; + GrdButtonState state; + } input_ptr_button; + + /* RDP_EVENT_TYPE_INPUT_PTR_AXIS */ + struct + { + double dx; + double dy; + GrdPointerAxisFlags flags; + } input_ptr_axis; + + /* RDP_EVENT_TYPE_INPUT_SYNC */ + struct + { + gboolean caps_lock_state; + gboolean num_lock_state; + gboolean needs_sync_ping; + } input_sync; +} RdpEvent; + +struct _GrdRdpEventQueue +{ + GObject parent; + + GrdSessionRdp *session_rdp; + GSource *flush_source; + + GMutex event_mutex; + GQueue *queue; + + GCancellable *pending_sync_cancellable; + gboolean expected_caps_lock_state; + gboolean expected_num_lock_state; + gboolean caps_lock_state; + gboolean num_lock_state; +}; + +G_DEFINE_TYPE (GrdRdpEventQueue, grd_rdp_event_queue, G_TYPE_OBJECT) + +void +free_rdp_event (gpointer data) +{ + RdpEvent *rdp_event = data; + + g_clear_object (&rdp_event->input_ptr_motion_abs.stream); + g_free (rdp_event); +} + +static void +flush_input_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GrdSession *session = GRD_SESSION (source_object); + g_autoptr (GError) error = NULL; + GrdRdpEventQueue *rdp_event_queue; + + if (!grd_session_flush_input_finish (session, result, &error)) + { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("Failed to flush input: %s", error->message); + + return; + } + + rdp_event_queue = GRD_RDP_EVENT_QUEUE (user_data); + g_clear_object (&rdp_event_queue->pending_sync_cancellable); + + if (rdp_event_queue->expected_caps_lock_state != rdp_event_queue->caps_lock_state) + { + g_debug ("Synchronizing caps lock state to be %s, pressing caps lock key", + rdp_event_queue->expected_caps_lock_state ? "locked": "unlocked"); + + grd_session_notify_keyboard_keysym (session, XKB_KEY_Caps_Lock, + GRD_KEY_STATE_PRESSED); + grd_session_notify_keyboard_keysym (session, XKB_KEY_Caps_Lock, + GRD_KEY_STATE_RELEASED); + + } + if (rdp_event_queue->expected_num_lock_state != rdp_event_queue->num_lock_state) + { + g_debug ("Synchronizing num lock state to be %s, pressing num lock key", + rdp_event_queue->expected_num_lock_state ? "locked": "unlocked"); + + grd_session_notify_keyboard_keysym (session, XKB_KEY_Num_Lock, + GRD_KEY_STATE_PRESSED); + grd_session_notify_keyboard_keysym (session, XKB_KEY_Num_Lock, + GRD_KEY_STATE_RELEASED); + } +} + +static void +handle_synchronization_event (GrdRdpEventQueue *rdp_event_queue, + RdpEvent *rdp_event) +{ + GrdSession *session = GRD_SESSION (rdp_event_queue->session_rdp); + + g_cancellable_cancel (rdp_event_queue->pending_sync_cancellable); + g_clear_object (&rdp_event_queue->pending_sync_cancellable); + + rdp_event_queue->expected_caps_lock_state = rdp_event->input_sync.caps_lock_state; + rdp_event_queue->expected_num_lock_state = rdp_event->input_sync.num_lock_state; + rdp_event_queue->pending_sync_cancellable = g_cancellable_new (); + + if (!grd_session_is_ready (session)) + return; + + grd_rdp_event_queue_flush_synchronization (rdp_event_queue); +} + +void +grd_rdp_event_queue_flush_synchronization (GrdRdpEventQueue *rdp_event_queue) +{ + g_assert (grd_session_is_ready (GRD_SESSION (rdp_event_queue->session_rdp))); + + if (!rdp_event_queue->pending_sync_cancellable) + return; + + grd_session_flush_input_async (GRD_SESSION (rdp_event_queue->session_rdp), + rdp_event_queue->pending_sync_cancellable, + flush_input_cb, + rdp_event_queue); +} + +static void +process_rdp_events (GrdRdpEventQueue *rdp_event_queue) +{ + GrdSession *session = GRD_SESSION (rdp_event_queue->session_rdp); + RdpEvent *rdp_event; + + while ((rdp_event = g_queue_pop_head (rdp_event_queue->queue))) + { + switch (rdp_event->type) + { + case RDP_EVENT_TYPE_NONE: + break; + case RDP_EVENT_TYPE_INPUT_SYNC: + handle_synchronization_event (rdp_event_queue, rdp_event); + break; + case RDP_EVENT_TYPE_INPUT_KBD_KEYCODE: + grd_session_notify_keyboard_keycode ( + session, rdp_event->input_kbd_keycode.keycode, + rdp_event->input_kbd_keycode.state); + break; + case RDP_EVENT_TYPE_INPUT_KBD_KEYSYM: + grd_session_notify_keyboard_keysym (session, + rdp_event->input_kbd_keysym.keysym, + rdp_event->input_kbd_keysym.state); + break; + case RDP_EVENT_TYPE_INPUT_PTR_MOTION: + grd_session_notify_pointer_motion (session, + rdp_event->input_ptr_motion.dx, + rdp_event->input_ptr_motion.dy); + break; + case RDP_EVENT_TYPE_INPUT_PTR_MOTION_ABS: + grd_session_notify_pointer_motion_absolute ( + session, rdp_event->input_ptr_motion_abs.stream, + &rdp_event->input_ptr_motion_abs.motion_abs); + break; + case RDP_EVENT_TYPE_INPUT_PTR_BUTTON: + grd_session_notify_pointer_button (session, + rdp_event->input_ptr_button.button, + rdp_event->input_ptr_button.state); + break; + case RDP_EVENT_TYPE_INPUT_PTR_AXIS: + grd_session_notify_pointer_axis (session, + rdp_event->input_ptr_axis.dx, + rdp_event->input_ptr_axis.dy, + rdp_event->input_ptr_axis.flags); + break; + } + + free_rdp_event (rdp_event); + } +} + +void +grd_rdp_event_queue_flush (GrdRdpEventQueue *rdp_event_queue) +{ + g_mutex_lock (&rdp_event_queue->event_mutex); + process_rdp_events (rdp_event_queue); + g_mutex_unlock (&rdp_event_queue->event_mutex); +} + +static void +queue_rdp_event (GrdRdpEventQueue *rdp_event_queue, + RdpEvent *rdp_event) +{ + g_mutex_lock (&rdp_event_queue->event_mutex); + g_queue_push_tail (rdp_event_queue->queue, rdp_event); + g_mutex_unlock (&rdp_event_queue->event_mutex); + + g_source_set_ready_time (rdp_event_queue->flush_source, 0); +} + +void +grd_rdp_event_queue_add_input_event_keyboard_keycode (GrdRdpEventQueue *rdp_event_queue, + uint32_t keycode, + GrdKeyState state) +{ + RdpEvent *rdp_event; + + rdp_event = g_malloc0 (sizeof (RdpEvent)); + rdp_event->type = RDP_EVENT_TYPE_INPUT_KBD_KEYCODE; + rdp_event->input_kbd_keycode.keycode = keycode; + rdp_event->input_kbd_keycode.state = state; + + queue_rdp_event (rdp_event_queue, rdp_event); +} + +void +grd_rdp_event_queue_add_input_event_keyboard_keysym (GrdRdpEventQueue *rdp_event_queue, + uint32_t keysym, + GrdKeyState state) +{ + RdpEvent *rdp_event; + + rdp_event = g_malloc0 (sizeof (RdpEvent)); + rdp_event->type = RDP_EVENT_TYPE_INPUT_KBD_KEYSYM; + rdp_event->input_kbd_keysym.keysym = keysym; + rdp_event->input_kbd_keysym.state = state; + + queue_rdp_event (rdp_event_queue, rdp_event); +} + +void +grd_rdp_event_queue_add_input_event_pointer_motion (GrdRdpEventQueue *rdp_event_queue, + double dx, + double dy) +{ + RdpEvent *rdp_event; + + rdp_event = g_new0 (RdpEvent, 1); + rdp_event->type = RDP_EVENT_TYPE_INPUT_PTR_MOTION; + rdp_event->input_ptr_motion.dx = dx; + rdp_event->input_ptr_motion.dy = dy; + + queue_rdp_event (rdp_event_queue, rdp_event); +} + +void +grd_rdp_event_queue_add_input_event_pointer_motion_abs (GrdRdpEventQueue *rdp_event_queue, + GrdStream *stream, + const GrdEventMotionAbs *motion_abs) +{ + RdpEvent *rdp_event; + + rdp_event = g_malloc0 (sizeof (RdpEvent)); + rdp_event->type = RDP_EVENT_TYPE_INPUT_PTR_MOTION_ABS; + rdp_event->input_ptr_motion_abs.stream = stream; + rdp_event->input_ptr_motion_abs.motion_abs = *motion_abs; + + queue_rdp_event (rdp_event_queue, rdp_event); +} + +void +grd_rdp_event_queue_add_input_event_pointer_button (GrdRdpEventQueue *rdp_event_queue, + int32_t button, + GrdButtonState state) +{ + RdpEvent *rdp_event; + + rdp_event = g_malloc0 (sizeof (RdpEvent)); + rdp_event->type = RDP_EVENT_TYPE_INPUT_PTR_BUTTON; + rdp_event->input_ptr_button.button = button; + rdp_event->input_ptr_button.state = state; + + queue_rdp_event (rdp_event_queue, rdp_event); +} + +void +grd_rdp_event_queue_add_input_event_pointer_axis (GrdRdpEventQueue *rdp_event_queue, + double dx, + double dy, + GrdPointerAxisFlags flags) +{ + RdpEvent *rdp_event; + + rdp_event = g_malloc0 (sizeof (RdpEvent)); + rdp_event->type = RDP_EVENT_TYPE_INPUT_PTR_AXIS; + rdp_event->input_ptr_axis.dx = dx; + rdp_event->input_ptr_axis.dy = dy; + rdp_event->input_ptr_axis.flags = flags; + + queue_rdp_event (rdp_event_queue, rdp_event); +} + +void +grd_rdp_event_queue_update_caps_lock_state (GrdRdpEventQueue *rdp_event_queue, + gboolean caps_lock_state) +{ + g_debug ("Updated current caps lock state to %s", + caps_lock_state ? "locked" : "unlocked"); + rdp_event_queue->caps_lock_state = caps_lock_state; +} + +void +grd_rdp_event_queue_update_num_lock_state (GrdRdpEventQueue *rdp_event_queue, + gboolean num_lock_state) +{ + g_debug ("Updated current num lock state to %s", + num_lock_state ? "locked" : "unlocked"); + rdp_event_queue->num_lock_state = num_lock_state; +} + +void +grd_rdp_event_queue_add_synchronization_event (GrdRdpEventQueue *rdp_event_queue, + gboolean caps_lock_state, + gboolean num_lock_state) +{ + RdpEvent *rdp_event; + + rdp_event = g_new0 (RdpEvent, 1); + rdp_event->type = RDP_EVENT_TYPE_INPUT_SYNC; + rdp_event->input_sync.caps_lock_state = caps_lock_state; + rdp_event->input_sync.num_lock_state = num_lock_state; + rdp_event->input_sync.needs_sync_ping = TRUE; + + queue_rdp_event (rdp_event_queue, rdp_event); +} + +static gboolean +flush_rdp_events (gpointer user_data) +{ + GrdRdpEventQueue *rdp_event_queue = user_data; + + g_mutex_lock (&rdp_event_queue->event_mutex); + process_rdp_events (rdp_event_queue); + g_mutex_unlock (&rdp_event_queue->event_mutex); + + return G_SOURCE_CONTINUE; +} + +static gboolean +flush_source_dispatch (GSource *source, + GSourceFunc callback, + gpointer user_data) +{ + g_source_set_ready_time (source, -1); + + return callback (user_data); +} + +static GSourceFuncs flush_source_funcs = +{ + .dispatch = flush_source_dispatch, +}; + +GrdRdpEventQueue * +grd_rdp_event_queue_new (GrdSessionRdp *session_rdp) +{ + GrdRdpEventQueue *rdp_event_queue; + GSource *flush_source; + + rdp_event_queue = g_object_new (GRD_TYPE_RDP_EVENT_QUEUE, NULL); + rdp_event_queue->session_rdp = session_rdp; + + flush_source = g_source_new (&flush_source_funcs, sizeof (GSource)); + g_source_set_callback (flush_source, flush_rdp_events, rdp_event_queue, NULL); + g_source_set_ready_time (flush_source, -1); + g_source_attach (flush_source, NULL); + rdp_event_queue->flush_source = flush_source; + + return rdp_event_queue; +} + +static void +grd_rdp_event_queue_dispose (GObject *object) +{ + GrdRdpEventQueue *rdp_event_queue = GRD_RDP_EVENT_QUEUE (object); + + if (rdp_event_queue->flush_source) + { + g_source_destroy (rdp_event_queue->flush_source); + g_clear_pointer (&rdp_event_queue->flush_source, g_source_unref); + } + + G_OBJECT_CLASS (grd_rdp_event_queue_parent_class)->dispose (object); +} + +static void +grd_rdp_event_queue_finalize (GObject *object) +{ + GrdRdpEventQueue *rdp_event_queue = GRD_RDP_EVENT_QUEUE (object); + + g_cancellable_cancel (rdp_event_queue->pending_sync_cancellable); + g_clear_object (&rdp_event_queue->pending_sync_cancellable); + + g_mutex_clear (&rdp_event_queue->event_mutex); + + g_queue_free_full (rdp_event_queue->queue, free_rdp_event); + + G_OBJECT_CLASS (grd_rdp_event_queue_parent_class)->finalize (object); +} + +static void +grd_rdp_event_queue_init (GrdRdpEventQueue *rdp_event_queue) +{ + rdp_event_queue->queue = g_queue_new (); + + g_mutex_init (&rdp_event_queue->event_mutex); +} + +static void +grd_rdp_event_queue_class_init (GrdRdpEventQueueClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = grd_rdp_event_queue_dispose; + object_class->finalize = grd_rdp_event_queue_finalize; +} diff --git a/grd-rdp-event-queue.h b/grd-rdp-event-queue.h new file mode 100644 index 0000000..4a84028 --- /dev/null +++ b/grd-rdp-event-queue.h @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2021 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include +#include + +#include "grd-session.h" +#include "grd-types.h" + +#define GRD_TYPE_RDP_EVENT_QUEUE (grd_rdp_event_queue_get_type ()) +G_DECLARE_FINAL_TYPE (GrdRdpEventQueue, grd_rdp_event_queue, + GRD, RDP_EVENT_QUEUE, GObject) + +GrdRdpEventQueue *grd_rdp_event_queue_new (GrdSessionRdp *session_rdp); + +void grd_rdp_event_queue_flush (GrdRdpEventQueue *rdp_event_queue); + +void grd_rdp_event_queue_add_input_event_keyboard_keycode (GrdRdpEventQueue *rdp_event_queue, + uint32_t keycode, + GrdKeyState state); + +void grd_rdp_event_queue_add_input_event_keyboard_keysym (GrdRdpEventQueue *rdp_event_queue, + uint32_t keysym, + GrdKeyState state); + +void grd_rdp_event_queue_add_input_event_pointer_motion (GrdRdpEventQueue *rdp_event_queue, + double dx, + double dy); + +void grd_rdp_event_queue_add_input_event_pointer_motion_abs (GrdRdpEventQueue *rdp_event_queue, + GrdStream *stream, + const GrdEventMotionAbs *motion_abs); + +void grd_rdp_event_queue_add_input_event_pointer_button (GrdRdpEventQueue *rdp_event_queue, + int32_t button, + GrdButtonState state); + +void grd_rdp_event_queue_add_input_event_pointer_axis (GrdRdpEventQueue *rdp_event_queue, + double dx, + double dy, + GrdPointerAxisFlags flags); + +void grd_rdp_event_queue_update_caps_lock_state (GrdRdpEventQueue *rdp_event_queue, + gboolean caps_lock_state); + +void grd_rdp_event_queue_update_num_lock_state (GrdRdpEventQueue *rdp_event_queue, + gboolean num_lock_state); + +void grd_rdp_event_queue_add_synchronization_event (GrdRdpEventQueue *rdp_event_queue, + gboolean caps_lock_state, + gboolean num_lock_state); + +void grd_rdp_event_queue_flush_synchronization (GrdRdpEventQueue *rdp_event_queue); diff --git a/grd-rdp-frame-info.h b/grd-rdp-frame-info.h new file mode 100644 index 0000000..cb8564e --- /dev/null +++ b/grd-rdp-frame-info.h @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2021 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +typedef struct _GrdRdpFrameInfo +{ + uint32_t frame_id; + uint32_t n_subframes; + + int64_t enc_time_us; + int64_t ack_time_us; +} GrdRdpFrameInfo; diff --git a/grd-rdp-frame-stats.c b/grd-rdp-frame-stats.c new file mode 100644 index 0000000..8251e50 --- /dev/null +++ b/grd-rdp-frame-stats.c @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2024 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-rdp-frame-stats.h" + +struct _GrdRdpFrameStats +{ + /* + * Amount of dual frames (frames containing both a main view and an + * auxiliary view), that were sent to the client, but not acknowledged yet. + * Each of those frames is either in the sending process, waiting on the + * client side to be processed, currently being decoded, currently in the + * process of being displayed, or their respective frame acknowledge message + * is currently in the process of being sent to the server side. + */ + uint32_t missing_dual_frame_acks; + + /* + * Current encoding rate per second showing how many frames are currently on + * average produced by the server side every second. + */ + uint32_t enc_rate; + /* + * Current acknowledge rate per second showing how many frames are currently + * on average acknowledged by the client every second. Every acknowledged + * frame was sent to the client, decoded and displayed by the client, and + * its frame acknowledge message was sent to the server side. + */ + uint32_t ack_rate; +}; + +uint32_t +grd_rdp_frame_stats_get_missing_dual_frame_acks (GrdRdpFrameStats *frame_stats) +{ + return frame_stats->missing_dual_frame_acks; +} + +uint32_t +grd_rdp_frame_stats_get_enc_rate (GrdRdpFrameStats *frame_stats) +{ + return frame_stats->enc_rate; +} + +uint32_t +grd_rdp_frame_stats_get_ack_rate (GrdRdpFrameStats *frame_stats) +{ + return frame_stats->ack_rate; +} + +GrdRdpFrameStats * +grd_rdp_frame_stats_new (uint32_t missing_dual_frame_acks, + uint32_t enc_rate, + uint32_t ack_rate) +{ + GrdRdpFrameStats *frame_stats; + + frame_stats = g_new0 (GrdRdpFrameStats, 1); + frame_stats->missing_dual_frame_acks = missing_dual_frame_acks; + frame_stats->enc_rate = enc_rate; + frame_stats->ack_rate = ack_rate; + + return frame_stats; +} + +void +grd_rdp_frame_stats_free (GrdRdpFrameStats *frame_stats) +{ + g_free (frame_stats); +} diff --git a/grd-rdp-frame-stats.h b/grd-rdp-frame-stats.h new file mode 100644 index 0000000..9a70807 --- /dev/null +++ b/grd-rdp-frame-stats.h @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2024 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include +#include + +#include "grd-types.h" + +GrdRdpFrameStats *grd_rdp_frame_stats_new (uint32_t missing_dual_frame_acks, + uint32_t enc_rate, + uint32_t ack_rate); + +void grd_rdp_frame_stats_free (GrdRdpFrameStats *frame_stats); + +uint32_t grd_rdp_frame_stats_get_missing_dual_frame_acks (GrdRdpFrameStats *frame_stats); + +uint32_t grd_rdp_frame_stats_get_enc_rate (GrdRdpFrameStats *frame_stats); + +uint32_t grd_rdp_frame_stats_get_ack_rate (GrdRdpFrameStats *frame_stats); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (GrdRdpFrameStats, grd_rdp_frame_stats_free) diff --git a/grd-rdp-frame.c b/grd-rdp-frame.c new file mode 100644 index 0000000..24449e9 --- /dev/null +++ b/grd-rdp-frame.c @@ -0,0 +1,382 @@ +/* + * Copyright (C) 2024 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-rdp-frame.h" + +#include "grd-encode-context.h" +#include "grd-rdp-render-context.h" +#include "grd-rdp-renderer.h" + +struct _GrdRdpFrame +{ + GrdRdpRenderer *renderer; + GrdRdpRenderContext *render_context; + + GrdRdpFrameCallback frame_picked_up; + GrdRdpFrameCallback view_finalized; + GrdRdpFrameCallback frame_submitted; + GrdRdpFrameCallback frame_finalized; + gpointer callback_user_data; + GDestroyNotify user_data_destroy; + + gboolean pending_view_finalization; + + GrdEncodeContext *encode_context; + + GList *acquired_image_views; + GQueue *unused_image_views; + + GrdRdpBuffer *src_buffer_new; + GrdRdpBuffer *src_buffer_old; + + GrdRdpFrameViewType view_type; + + cairo_region_t *damage_region; + GList *bitstreams; +}; + +GrdRdpRenderer * +grd_rdp_frame_get_renderer (GrdRdpFrame *rdp_frame) +{ + return rdp_frame->renderer; +} + +GrdRdpRenderContext * +grd_rdp_frame_get_render_context (GrdRdpFrame *rdp_frame) +{ + return rdp_frame->render_context; +} + +GrdEncodeContext * +grd_rdp_frame_get_encode_context (GrdRdpFrame *rdp_frame) +{ + return rdp_frame->encode_context; +} + +GList * +grd_rdp_frame_get_image_views (GrdRdpFrame *rdp_frame) +{ + return rdp_frame->acquired_image_views; +} + +GrdRdpBuffer * +grd_rdp_frame_get_source_buffer (GrdRdpFrame *rdp_frame) +{ + return rdp_frame->src_buffer_new; +} + +GrdRdpBuffer * +grd_rdp_frame_get_last_source_buffer (GrdRdpFrame *rdp_frame) +{ + return rdp_frame->src_buffer_old; +} + +GrdRdpFrameViewType +grd_rdp_frame_get_avc_view_type (GrdRdpFrame *rdp_frame) +{ + return rdp_frame->view_type; +} + +cairo_region_t * +grd_rdp_frame_get_damage_region (GrdRdpFrame *rdp_frame) +{ + return rdp_frame->damage_region; +} + +GList * +grd_rdp_frame_get_bitstreams (GrdRdpFrame *rdp_frame) +{ + return rdp_frame->bitstreams; +} + +gboolean +grd_rdp_frame_has_valid_view (GrdRdpFrame *rdp_frame) +{ + return !!rdp_frame->damage_region; +} + +gboolean +grd_rdp_frame_is_surface_damaged (GrdRdpFrame *rdp_frame) +{ + return cairo_region_num_rectangles (rdp_frame->damage_region) > 0; +} + +void +grd_rdp_frame_set_renderer (GrdRdpFrame *rdp_frame, + GrdRdpRenderer *renderer) +{ + rdp_frame->renderer = renderer; +} + +void +grd_rdp_frame_set_avc_view_type (GrdRdpFrame *rdp_frame, + GrdRdpFrameViewType view_type) +{ + g_assert (view_type != GRD_RDP_FRAME_VIEW_TYPE_DUAL); + g_assert (view_type == GRD_RDP_FRAME_VIEW_TYPE_MAIN || + view_type == GRD_RDP_FRAME_VIEW_TYPE_AUX); + + switch (rdp_frame->view_type) + { + case GRD_RDP_FRAME_VIEW_TYPE_DUAL: + g_assert (g_queue_get_length (rdp_frame->unused_image_views) == 2); + g_queue_pop_tail (rdp_frame->unused_image_views); + break; + case GRD_RDP_FRAME_VIEW_TYPE_MAIN: + case GRD_RDP_FRAME_VIEW_TYPE_AUX: + g_assert (g_queue_get_length (rdp_frame->unused_image_views) == 1); + break; + } + + rdp_frame->view_type = view_type; +} + +static void +finalize_view (GrdRdpFrame *rdp_frame) +{ + g_assert (rdp_frame->pending_view_finalization); + + rdp_frame->view_finalized (rdp_frame, rdp_frame->callback_user_data); + rdp_frame->pending_view_finalization = FALSE; +} + +void +grd_rdp_frame_set_damage_region (GrdRdpFrame *rdp_frame, + cairo_region_t *damage_region) +{ + GrdRdpRenderContext *render_context = rdp_frame->render_context; + + g_assert (!rdp_frame->damage_region); + + rdp_frame->damage_region = damage_region; + grd_encode_context_set_damage_region (rdp_frame->encode_context, + damage_region); + + if (!grd_rdp_render_context_must_delay_view_finalization (render_context)) + finalize_view (rdp_frame); +} + +void +grd_rdp_frame_set_bitstreams (GrdRdpFrame *rdp_frame, + GList *bitstreams) +{ + rdp_frame->bitstreams = bitstreams; +} + +void +grd_rdp_frame_notify_picked_up (GrdRdpFrame *rdp_frame) +{ + rdp_frame->frame_picked_up (rdp_frame, rdp_frame->callback_user_data); +} + +void +grd_rdp_frame_notify_frame_submission (GrdRdpFrame *rdp_frame) +{ + rdp_frame->frame_submitted (rdp_frame, rdp_frame->callback_user_data); +} + +GrdImageView * +grd_rdp_frame_pop_image_view (GrdRdpFrame *rdp_frame) +{ + return g_queue_pop_head (rdp_frame->unused_image_views); +} + +static void +set_view_type (GrdRdpFrame *rdp_frame, + gboolean frame_upgrade) +{ + GrdRdpRenderContext *render_context = rdp_frame->render_context; + GrdRdpCodec codec = grd_rdp_render_context_get_codec (render_context); + + switch (codec) + { + case GRD_RDP_CODEC_CAPROGRESSIVE: + case GRD_RDP_CODEC_AVC420: + rdp_frame->view_type = GRD_RDP_FRAME_VIEW_TYPE_MAIN; + break; + case GRD_RDP_CODEC_AVC444v2: + rdp_frame->view_type = + frame_upgrade ? GRD_RDP_FRAME_VIEW_TYPE_AUX + : GRD_RDP_FRAME_VIEW_TYPE_DUAL; + break; + } +} + +static uint32_t +get_n_required_image_views (GrdRdpFrame *rdp_frame) +{ + GrdRdpRenderContext *render_context = rdp_frame->render_context; + GrdRdpCodec codec = grd_rdp_render_context_get_codec (render_context); + + switch (codec) + { + case GRD_RDP_CODEC_CAPROGRESSIVE: + return 1; + case GRD_RDP_CODEC_AVC420: + case GRD_RDP_CODEC_AVC444v2: + return 2; + } + + g_assert_not_reached (); +} + +static uint32_t +get_n_image_views_to_be_encoded (GrdRdpFrame *rdp_frame) +{ + switch (grd_rdp_frame_get_avc_view_type (rdp_frame)) + { + case GRD_RDP_FRAME_VIEW_TYPE_DUAL: + return 2; + case GRD_RDP_FRAME_VIEW_TYPE_MAIN: + case GRD_RDP_FRAME_VIEW_TYPE_AUX: + return 1; + } + + g_assert_not_reached (); +} + +static void +acquire_image_views (GrdRdpFrame *rdp_frame) +{ + uint32_t n_image_views = get_n_required_image_views (rdp_frame); + uint32_t n_image_views_to_be_encoded = + get_n_image_views_to_be_encoded (rdp_frame); + uint32_t i; + + g_assert (n_image_views_to_be_encoded <= n_image_views); + + for (i = 0; i < n_image_views; ++i) + { + GrdRdpRenderContext *render_context = rdp_frame->render_context; + GrdImageView *image_view; + + image_view = grd_rdp_render_context_acquire_image_view (render_context); + rdp_frame->acquired_image_views = + g_list_append (rdp_frame->acquired_image_views, image_view); + + if (i < n_image_views_to_be_encoded) + g_queue_push_tail (rdp_frame->unused_image_views, image_view); + } +} + +static void +prepare_new_frame (GrdRdpFrame *rdp_frame) +{ + rdp_frame->pending_view_finalization = TRUE; + + set_view_type (rdp_frame, FALSE); + acquire_image_views (rdp_frame); +} + +static void +prepare_frame_upgrade (GrdRdpFrame *rdp_frame) +{ + GrdRdpRenderContext *render_context = rdp_frame->render_context; + cairo_region_t *damage_region = NULL; + GrdImageView *image_view = NULL; + + set_view_type (rdp_frame, TRUE); + + grd_rdp_render_context_fetch_progressive_render_state (render_context, + &image_view, + &damage_region); + g_assert (image_view); + g_assert (damage_region); + + rdp_frame->acquired_image_views = + g_list_append (rdp_frame->acquired_image_views, image_view); + g_queue_push_tail (rdp_frame->unused_image_views, image_view); + + rdp_frame->damage_region = damage_region; +} + +GrdRdpFrame * +grd_rdp_frame_new (GrdRdpRenderContext *render_context, + GrdRdpBuffer *src_buffer_new, + GrdRdpBuffer *src_buffer_old, + GrdRdpFrameCallback frame_picked_up, + GrdRdpFrameCallback view_finalized, + GrdRdpFrameCallback frame_submitted, + GrdRdpFrameCallback frame_finalized, + gpointer callback_user_data, + GDestroyNotify user_data_destroy) +{ + GrdRdpFrame *rdp_frame; + + rdp_frame = g_new0 (GrdRdpFrame, 1); + rdp_frame->render_context = render_context; + rdp_frame->src_buffer_new = src_buffer_new; + rdp_frame->src_buffer_old = src_buffer_old; + + rdp_frame->frame_picked_up = frame_picked_up; + rdp_frame->view_finalized = view_finalized; + rdp_frame->frame_submitted = frame_submitted; + rdp_frame->frame_finalized = frame_finalized; + rdp_frame->callback_user_data = callback_user_data; + rdp_frame->user_data_destroy = user_data_destroy; + + rdp_frame->encode_context = grd_encode_context_new (); + rdp_frame->unused_image_views = g_queue_new (); + + if (src_buffer_new) + prepare_new_frame (rdp_frame); + else + prepare_frame_upgrade (rdp_frame); + + return rdp_frame; +} + +static void +release_image_views (GrdRdpFrame *rdp_frame) +{ + GList *l; + + for (l = rdp_frame->acquired_image_views; l; l = l->next) + { + GrdImageView *image_view = l->data; + + grd_rdp_render_context_release_image_view (rdp_frame->render_context, + image_view); + } + g_clear_pointer (&rdp_frame->acquired_image_views, g_list_free); +} + +void +grd_rdp_frame_free (GrdRdpFrame *rdp_frame) +{ + g_assert (!rdp_frame->bitstreams); + g_assert (rdp_frame->renderer); + + if (rdp_frame->pending_view_finalization) + finalize_view (rdp_frame); + + g_clear_pointer (&rdp_frame->encode_context, grd_encode_context_free); + g_clear_pointer (&rdp_frame->damage_region, cairo_region_destroy); + + g_clear_pointer (&rdp_frame->unused_image_views, g_queue_free); + release_image_views (rdp_frame); + + rdp_frame->frame_finalized (rdp_frame, rdp_frame->callback_user_data); + g_clear_pointer (&rdp_frame->callback_user_data, + rdp_frame->user_data_destroy); + + g_free (rdp_frame); +} diff --git a/grd-rdp-frame.h b/grd-rdp-frame.h new file mode 100644 index 0000000..d4e0f2f --- /dev/null +++ b/grd-rdp-frame.h @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2024 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include +#include + +#include "grd-types.h" + +typedef enum +{ + GRD_RDP_FRAME_VIEW_TYPE_DUAL, + GRD_RDP_FRAME_VIEW_TYPE_MAIN, + GRD_RDP_FRAME_VIEW_TYPE_AUX, +} GrdRdpFrameViewType; + +typedef void (* GrdRdpFrameCallback) (GrdRdpFrame *rdp_frame, + gpointer user_data); + +GrdRdpFrame *grd_rdp_frame_new (GrdRdpRenderContext *render_context, + GrdRdpBuffer *src_buffer_new, + GrdRdpBuffer *src_buffer_old, + GrdRdpFrameCallback frame_picked_up, + GrdRdpFrameCallback view_finalized, + GrdRdpFrameCallback frame_submitted, + GrdRdpFrameCallback frame_finalized, + gpointer callback_user_data, + GDestroyNotify user_data_destroy); + +void grd_rdp_frame_free (GrdRdpFrame *rdp_frame); + +GrdRdpRenderer *grd_rdp_frame_get_renderer (GrdRdpFrame *rdp_frame); + +GrdRdpRenderContext *grd_rdp_frame_get_render_context (GrdRdpFrame *rdp_frame); + +GrdEncodeContext *grd_rdp_frame_get_encode_context (GrdRdpFrame *rdp_frame); + +GList *grd_rdp_frame_get_image_views (GrdRdpFrame *rdp_frame); + +GrdRdpBuffer *grd_rdp_frame_get_source_buffer (GrdRdpFrame *rdp_frame); + +GrdRdpBuffer *grd_rdp_frame_get_last_source_buffer (GrdRdpFrame *rdp_frame); + +GrdRdpFrameViewType grd_rdp_frame_get_avc_view_type (GrdRdpFrame *rdp_frame); + +cairo_region_t *grd_rdp_frame_get_damage_region (GrdRdpFrame *rdp_frame); + +GList *grd_rdp_frame_get_bitstreams (GrdRdpFrame *rdp_frame); + +gboolean grd_rdp_frame_has_valid_view (GrdRdpFrame *rdp_frame); + +gboolean grd_rdp_frame_is_surface_damaged (GrdRdpFrame *rdp_frame); + +void grd_rdp_frame_set_renderer (GrdRdpFrame *rdp_frame, + GrdRdpRenderer *renderer); + +void grd_rdp_frame_set_avc_view_type (GrdRdpFrame *rdp_frame, + GrdRdpFrameViewType view_type); + +void grd_rdp_frame_set_damage_region (GrdRdpFrame *rdp_frame, + cairo_region_t *damage_region); + +void grd_rdp_frame_set_bitstreams (GrdRdpFrame *rdp_frame, + GList *bitstreams); + +void grd_rdp_frame_notify_picked_up (GrdRdpFrame *rdp_frame); + +void grd_rdp_frame_notify_frame_submission (GrdRdpFrame *rdp_frame); + +GrdImageView *grd_rdp_frame_pop_image_view (GrdRdpFrame *rdp_frame); diff --git a/grd-rdp-fuse-clipboard.c b/grd-rdp-fuse-clipboard.c new file mode 100644 index 0000000..f39d1a6 --- /dev/null +++ b/grd-rdp-fuse-clipboard.c @@ -0,0 +1,1591 @@ +/* + * Copyright (C) 2020-2021 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-rdp-fuse-clipboard.h" + +#define FUSE_USE_VERSION 35 + +#include +#include + +#include "grd-clipboard-rdp.h" +#include "grd-utils.h" + +#define CLIP_DATA_ENTRY_DROP_TIMEOUT_MS (60 * 1000) +#define CLIP_DATA_ENTRY_DROP_TIMEOUT_DELTA_US (10 * G_USEC_PER_SEC) +#define WIN32_FILETIME_TO_UNIX_EPOCH UINT64_C (11644473600) + +typedef enum _FuseLowlevelOperationType +{ + FUSE_LL_OPERATION_NONE, + FUSE_LL_OPERATION_LOOKUP, + FUSE_LL_OPERATION_GETATTR, + FUSE_LL_OPERATION_READ, +} FuseLowlevelOperationType; + +typedef struct _FuseFileStealContext +{ + gboolean all_files; + gboolean has_clip_data_id; + uint32_t clip_data_id; + + GList *fuse_files; +} FuseFileStealContext; + +typedef struct _ClearRdpFuseRequestContext +{ + GrdRdpFuseClipboard *rdp_fuse_clipboard; + gboolean all_files; + gboolean has_clip_data_id; + uint32_t clip_data_id; +} ClearRdpFuseRequestContext; + +typedef struct _FuseFile FuseFile; +typedef struct _ClipDataEntry ClipDataEntry; + +struct _FuseFile +{ + FuseFile *parent; + GList *children; + + char *filename; + char *filename_with_root; + uint32_t list_idx; + fuse_ino_t ino; + + gboolean is_directory; + gboolean is_readonly; + + gboolean has_size; + uint64_t size; + + gboolean has_last_write_time; + uint64_t last_write_time_unix; + + gboolean has_clip_data_id; + uint32_t clip_data_id; + + ClipDataEntry *entry; +}; + +struct _ClipDataEntry +{ + FuseFile *clip_data_dir; + + gboolean has_clip_data_id; + uint32_t clip_data_id; + + GrdClipboardRdp *clipboard_rdp; + + gboolean had_file_contents_request; + + struct + { + GrdRdpFuseClipboard *rdp_fuse_clipboard; + unsigned int drop_id; + int64_t drop_id_timeout_set_us; + } drop_context; +}; + +typedef struct _RdpFuseFileContentsRequest +{ + FuseFile *fuse_file; + + fuse_req_t fuse_req; + FuseLowlevelOperationType operation_type; + + uint32_t stream_id; +} RdpFuseFileContentsRequest; + +struct _GrdRdpFuseClipboard +{ + GObject parent; + + GrdClipboardRdp *clipboard_rdp; + char *mount_path; + + GThread *fuse_thread; + struct fuse_session *fuse_handle; + GrdSyncPoint sync_point_start; + GrdSyncPoint sync_point_stop; + + GSource *timeout_reset_source; + GHashTable *timeouts_to_reset; + + GMutex filesystem_mutex; + GMutex selection_mutex; + GHashTable *inode_table; + GHashTable *clip_data_table; + GHashTable *request_table; + FuseFile *root_dir; + + ClipDataEntry *no_cdi_entry; + + fuse_ino_t next_ino; + uint32_t next_clip_data_id; + uint32_t next_stream_id; +}; + +G_DEFINE_TYPE (GrdRdpFuseClipboard, grd_rdp_fuse_clipboard, G_TYPE_OBJECT) + +static gboolean +should_remove_fuse_file (FuseFile *fuse_file, + gboolean all_files, + gboolean has_clip_data_id, + uint32_t clip_data_id) +{ + if (all_files) + return TRUE; + + if (fuse_file->ino == FUSE_ROOT_ID) + return FALSE; + if (!fuse_file->has_clip_data_id && !has_clip_data_id) + return TRUE; + if (fuse_file->has_clip_data_id && has_clip_data_id && + fuse_file->clip_data_id == clip_data_id) + return TRUE; + + return FALSE; +} + +static gboolean +collect_fuse_file_to_steal (gpointer key, + gpointer value, + gpointer user_data) +{ + FuseFile *fuse_file = value; + FuseFileStealContext *steal_context = user_data; + + if (!should_remove_fuse_file (fuse_file, + steal_context->all_files, + steal_context->has_clip_data_id, + steal_context->clip_data_id)) + return FALSE; + + steal_context->fuse_files = g_list_prepend (steal_context->fuse_files, + fuse_file); + + return TRUE; +} + +static gboolean +maybe_clear_rdp_fuse_request (gpointer key, + gpointer value, + gpointer user_data) +{ + RdpFuseFileContentsRequest *rdp_fuse_request = value; + ClearRdpFuseRequestContext *clear_context = user_data; + GrdRdpFuseClipboard *rdp_fuse_clipboard = clear_context->rdp_fuse_clipboard; + + if (!should_remove_fuse_file (rdp_fuse_request->fuse_file, + clear_context->all_files, + clear_context->has_clip_data_id, + clear_context->clip_data_id)) + return FALSE; + + if (rdp_fuse_clipboard->fuse_handle) + fuse_reply_err (rdp_fuse_request->fuse_req, EIO); + g_free (rdp_fuse_request); + + return TRUE; +} + +void +grd_rdp_fuse_clipboard_dismiss_all_no_cdi_requests (GrdRdpFuseClipboard *rdp_fuse_clipboard) +{ + ClearRdpFuseRequestContext clear_context = {0}; + + clear_context.rdp_fuse_clipboard = rdp_fuse_clipboard; + + g_mutex_lock (&rdp_fuse_clipboard->filesystem_mutex); + g_hash_table_foreach_remove (rdp_fuse_clipboard->request_table, + maybe_clear_rdp_fuse_request, &clear_context); + g_mutex_unlock (&rdp_fuse_clipboard->filesystem_mutex); +} + +static void +invalidate_inode (gpointer data, + gpointer user_data) +{ + GrdRdpFuseClipboard *rdp_fuse_clipboard = user_data; + FuseFile *fuse_file = data; + FuseFile *child; + GList *l; + + for (l = fuse_file->children; l; l = l->next) + { + child = l->data; + + fuse_lowlevel_notify_delete (rdp_fuse_clipboard->fuse_handle, + fuse_file->ino, child->ino, + child->filename, strlen (child->filename)); + } + g_debug ("[FUSE Clipboard] Invalidating inode %lu for file \"%s\"", + fuse_file->ino, fuse_file->filename); + fuse_lowlevel_notify_inval_inode (rdp_fuse_clipboard->fuse_handle, + fuse_file->ino, 0, 0); + g_debug ("[FUSE Clipboard] Inode %lu invalidated", fuse_file->ino); +} + +static void +fuse_file_free (gpointer data) +{ + FuseFile *fuse_file = data; + + g_list_free (fuse_file->children); + g_free (fuse_file->filename_with_root); + g_free (fuse_file); +} + +static void +clear_selection (GrdRdpFuseClipboard *rdp_fuse_clipboard, + gboolean all_selections, + ClipDataEntry *entry) +{ + FuseFileStealContext steal_context = {0}; + ClearRdpFuseRequestContext clear_context = {0}; + FuseFile *clip_data_dir = NULL; + + g_assert (!g_mutex_trylock (&rdp_fuse_clipboard->selection_mutex)); + g_assert (!g_mutex_trylock (&rdp_fuse_clipboard->filesystem_mutex)); + + if (entry) + { + FuseFile *root_dir = rdp_fuse_clipboard->root_dir; + + clip_data_dir = g_steal_pointer (&entry->clip_data_dir); + root_dir->children = g_list_remove (root_dir->children, clip_data_dir); + + steal_context.has_clip_data_id = clear_context.has_clip_data_id = + entry->has_clip_data_id; + steal_context.clip_data_id = clear_context.clip_data_id = + entry->clip_data_id; + } + steal_context.all_files = clear_context.all_files = all_selections; + clear_context.rdp_fuse_clipboard = rdp_fuse_clipboard; + + if (entry && entry->has_clip_data_id) + g_debug ("[FUSE Clipboard] Clearing selection for clipDataId %u", entry->clip_data_id); + else + g_debug ("[FUSE Clipboard] Clearing selection%s", all_selections ? "s" : ""); + g_hash_table_foreach_remove (rdp_fuse_clipboard->request_table, + maybe_clear_rdp_fuse_request, &clear_context); + + g_hash_table_foreach_steal (rdp_fuse_clipboard->inode_table, + collect_fuse_file_to_steal, &steal_context); + g_mutex_unlock (&rdp_fuse_clipboard->filesystem_mutex); + + /** + * fuse_lowlevel_notify_inval_inode() is a blocking operation. If we receive + * a FUSE request (e.g. read()), then FUSE would block in read() since + * filesystem_mutex would still be locked, if we wouldn't unlock it here. + * fuse_lowlevel_notify_inval_inode() will block, since it waits on the FUSE + * operation to finish. + * So, to avoid a deadlock here, unlock the mutex and reply all incoming + * operations with -ENOENT until the invalidation process is complete. + */ + g_list_foreach (steal_context.fuse_files, invalidate_inode, rdp_fuse_clipboard); + if (clip_data_dir) + { + fuse_lowlevel_notify_delete (rdp_fuse_clipboard->fuse_handle, + rdp_fuse_clipboard->root_dir->ino, + clip_data_dir->ino, clip_data_dir->filename, + strlen (clip_data_dir->filename)); + } + g_list_free_full (steal_context.fuse_files, fuse_file_free); + + g_mutex_lock (&rdp_fuse_clipboard->filesystem_mutex); + if (entry && entry->has_clip_data_id) + g_debug ("[FUSE Clipboard] Selection cleared for clipDataId %u", entry->clip_data_id); + else + g_debug ("[FUSE Clipboard] Selection%s cleared", all_selections ? "s" : ""); +} + +static void +clear_all_selections (GrdRdpFuseClipboard *rdp_fuse_clipboard) +{ + g_mutex_lock (&rdp_fuse_clipboard->selection_mutex); + g_mutex_lock (&rdp_fuse_clipboard->filesystem_mutex); + + clear_selection (rdp_fuse_clipboard, TRUE, NULL); + + g_mutex_unlock (&rdp_fuse_clipboard->filesystem_mutex); + g_mutex_unlock (&rdp_fuse_clipboard->selection_mutex); +} + +static void +clear_entry_selection (GrdRdpFuseClipboard *rdp_fuse_clipboard, + ClipDataEntry *entry) +{ + clear_selection (rdp_fuse_clipboard, FALSE, entry); +} + +uint32_t +grd_rdp_fuse_clipboard_clip_data_id_new (GrdRdpFuseClipboard *rdp_fuse_clipboard) +{ + GrdClipboardRdp *clipboard_rdp = rdp_fuse_clipboard->clipboard_rdp; + ClipDataEntry *entry = NULL; + + g_mutex_lock (&rdp_fuse_clipboard->selection_mutex); + g_mutex_lock (&rdp_fuse_clipboard->filesystem_mutex); + + if (G_UNLIKELY (g_hash_table_size (rdp_fuse_clipboard->clip_data_table) >= UINT32_MAX)) + { + GHashTableIter iter; + ClipDataEntry *iter_value; + + g_debug ("[FUSE Clipboard] All clipDataIds used. Removing the oldest one"); + + g_hash_table_iter_init (&iter, rdp_fuse_clipboard->clip_data_table); + while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &iter_value)) + { + g_assert (iter_value->drop_context.drop_id); + if (!entry || entry->drop_context.drop_id_timeout_set_us > + iter_value->drop_context.drop_id_timeout_set_us) + entry = iter_value; + } + + g_debug ("[FUSE Clipboard] Force clearing selection with clipDataId %u", + entry->clip_data_id); + clear_entry_selection (rdp_fuse_clipboard, entry); + + g_hash_table_remove (rdp_fuse_clipboard->clip_data_table, + GUINT_TO_POINTER (entry->clip_data_id)); + } + + entry = g_malloc0 (sizeof (ClipDataEntry)); + entry->clipboard_rdp = clipboard_rdp; + entry->has_clip_data_id = TRUE; + + entry->clip_data_id = rdp_fuse_clipboard->next_clip_data_id; + while (g_hash_table_contains (rdp_fuse_clipboard->clip_data_table, + GUINT_TO_POINTER (entry->clip_data_id))) + ++entry->clip_data_id; + + rdp_fuse_clipboard->next_clip_data_id = entry->clip_data_id + 1; + + g_hash_table_insert (rdp_fuse_clipboard->clip_data_table, + GUINT_TO_POINTER (entry->clip_data_id), entry); + g_mutex_unlock (&rdp_fuse_clipboard->filesystem_mutex); + g_mutex_unlock (&rdp_fuse_clipboard->selection_mutex); + + grd_clipboard_rdp_lock_remote_clipboard_data (clipboard_rdp, entry->clip_data_id); + + return entry->clip_data_id; +} + +void +grd_rdp_fuse_clipboard_clip_data_id_free (GrdRdpFuseClipboard *rdp_fuse_clipboard, + uint32_t clip_data_id) +{ + ClipDataEntry *entry; + + g_mutex_lock (&rdp_fuse_clipboard->selection_mutex); + g_mutex_lock (&rdp_fuse_clipboard->filesystem_mutex); + if (!g_hash_table_lookup_extended (rdp_fuse_clipboard->clip_data_table, + GUINT_TO_POINTER (clip_data_id), + NULL, (gpointer *) &entry)) + { + g_mutex_unlock (&rdp_fuse_clipboard->filesystem_mutex); + g_mutex_unlock (&rdp_fuse_clipboard->selection_mutex); + return; + } + clear_entry_selection (rdp_fuse_clipboard, entry); + + g_hash_table_remove (rdp_fuse_clipboard->clip_data_table, + GUINT_TO_POINTER (clip_data_id)); + g_mutex_unlock (&rdp_fuse_clipboard->filesystem_mutex); + g_mutex_unlock (&rdp_fuse_clipboard->selection_mutex); +} + +void +grd_rdp_fuse_clipboard_clear_no_cdi_selection (GrdRdpFuseClipboard *rdp_fuse_clipboard) +{ + g_mutex_lock (&rdp_fuse_clipboard->selection_mutex); + g_mutex_lock (&rdp_fuse_clipboard->filesystem_mutex); + + if (rdp_fuse_clipboard->no_cdi_entry) + clear_entry_selection (rdp_fuse_clipboard, rdp_fuse_clipboard->no_cdi_entry); + + g_mutex_unlock (&rdp_fuse_clipboard->filesystem_mutex); + g_mutex_unlock (&rdp_fuse_clipboard->selection_mutex); +} + +static gboolean +drop_clip_data_entry (gpointer user_data) +{ + ClipDataEntry *entry = user_data; + GrdRdpFuseClipboard *rdp_fuse_clipboard = entry->drop_context.rdp_fuse_clipboard; + + g_debug ("[FUSE Clipboard] Dropping ClipDataEntry with clipDataId %u", + entry->clip_data_id); + + g_mutex_lock (&rdp_fuse_clipboard->selection_mutex); + g_mutex_lock (&rdp_fuse_clipboard->filesystem_mutex); + clear_entry_selection (rdp_fuse_clipboard, entry); + + entry->drop_context.drop_id = 0; + g_hash_table_remove (rdp_fuse_clipboard->clip_data_table, + GUINT_TO_POINTER (entry->clip_data_id)); + g_mutex_unlock (&rdp_fuse_clipboard->filesystem_mutex); + g_mutex_unlock (&rdp_fuse_clipboard->selection_mutex); + + return G_SOURCE_REMOVE; +} + +static void +collect_instant_droppable_clip_data_entry (gpointer key, + gpointer value, + gpointer user_data) +{ + ClipDataEntry *entry = value; + GList **droppable_clip_data_entries = user_data; + + if (!entry->had_file_contents_request) + { + *droppable_clip_data_entries = g_list_prepend (*droppable_clip_data_entries, + entry); + } +} + +static void +clear_instant_droppable_clip_data_entries (GrdRdpFuseClipboard *rdp_fuse_clipboard) +{ + GList *droppable_clip_data_entries = NULL; + GList *l; + + g_assert (!g_mutex_trylock (&rdp_fuse_clipboard->selection_mutex)); + g_assert (!g_mutex_trylock (&rdp_fuse_clipboard->filesystem_mutex)); + + g_hash_table_foreach (rdp_fuse_clipboard->clip_data_table, + collect_instant_droppable_clip_data_entry, + &droppable_clip_data_entries); + for (l = droppable_clip_data_entries; l; l = l->next) + { + ClipDataEntry *entry = l->data; + + g_debug ("[FUSE Clipboard] Instantly clearing selection for clipDataId %u", + entry->clip_data_id); + clear_entry_selection (rdp_fuse_clipboard, entry); + + g_hash_table_remove (rdp_fuse_clipboard->clip_data_table, + GUINT_TO_POINTER (entry->clip_data_id)); + } + + g_list_free (droppable_clip_data_entries); +} + +static void +maybe_set_clip_data_entry_timeout (gpointer key, + gpointer value, + gpointer user_data) +{ + ClipDataEntry *entry = value; + + if (entry->drop_context.drop_id) + return; + + g_debug ("[FUSE Clipboard] Setting timeout for selection with clipDataId %u", + entry->clip_data_id); + entry->drop_context.rdp_fuse_clipboard = user_data; + entry->drop_context.drop_id = g_timeout_add (CLIP_DATA_ENTRY_DROP_TIMEOUT_MS, + drop_clip_data_entry, entry); + entry->drop_context.drop_id_timeout_set_us = g_get_monotonic_time (); +} + +void +grd_rdp_fuse_clipboard_lazily_clear_all_cdi_selections (GrdRdpFuseClipboard *rdp_fuse_clipboard) +{ + g_debug ("[FUSE Clipboard] Lazily clearing all selections with clipDataId"); + + g_mutex_lock (&rdp_fuse_clipboard->selection_mutex); + g_mutex_lock (&rdp_fuse_clipboard->filesystem_mutex); + clear_instant_droppable_clip_data_entries (rdp_fuse_clipboard); + + g_hash_table_foreach (rdp_fuse_clipboard->clip_data_table, + maybe_set_clip_data_entry_timeout, + rdp_fuse_clipboard); + g_mutex_unlock (&rdp_fuse_clipboard->filesystem_mutex); + g_mutex_unlock (&rdp_fuse_clipboard->selection_mutex); +} + +static fuse_ino_t +get_next_free_inode (GrdRdpFuseClipboard *rdp_fuse_clipboard) +{ + fuse_ino_t ino = rdp_fuse_clipboard->next_ino; + + while (ino == 0 || ino == FUSE_ROOT_ID || + g_hash_table_contains (rdp_fuse_clipboard->inode_table, + GUINT_TO_POINTER (ino))) + ++ino; + + rdp_fuse_clipboard->next_ino = ino + 1; + + return ino; +} + +static FuseFile * +clip_data_dir_new (GrdRdpFuseClipboard *rdp_fuse_clipboard, + gboolean has_clip_data_id, + uint32_t clip_data_id) +{ + FuseFile *root_dir = rdp_fuse_clipboard->root_dir; + FuseFile *clip_data_dir; + + clip_data_dir = g_malloc0 (sizeof (FuseFile)); + clip_data_dir->filename_with_root = + has_clip_data_id ? g_strdup_printf ("/%u", clip_data_id) + : g_strdup_printf ("/%lu", GRD_RDP_FUSE_CLIPBOARD_NO_CLIP_DATA_ID); + clip_data_dir->filename = + strrchr (clip_data_dir->filename_with_root, '/') + 1; + clip_data_dir->ino = get_next_free_inode (rdp_fuse_clipboard); + clip_data_dir->is_directory = TRUE; + clip_data_dir->is_readonly = TRUE; + clip_data_dir->has_clip_data_id = has_clip_data_id; + clip_data_dir->clip_data_id = clip_data_id; + + root_dir->children = g_list_append (root_dir->children, clip_data_dir); + clip_data_dir->parent = root_dir; + + g_hash_table_insert (rdp_fuse_clipboard->inode_table, + GUINT_TO_POINTER (clip_data_dir->ino), clip_data_dir); + + return clip_data_dir; +} + +static gboolean +is_fuse_file_parent (gpointer key, + gpointer value, + gpointer user_data) +{ + FuseFile *fuse_file = value; + const char *parent_path = user_data; + + if (!fuse_file->is_directory) + return FALSE; + + if (strcmp (parent_path, fuse_file->filename_with_root) == 0) + return TRUE; + + return FALSE; +} + +static FuseFile * +get_parent_directory (GrdRdpFuseClipboard *rdp_fuse_clipboard, + const char *path) +{ + FuseFile *parent; + char *parent_path; + + parent_path = g_path_get_dirname (path); + parent = g_hash_table_find (rdp_fuse_clipboard->inode_table, + is_fuse_file_parent, parent_path); + + g_free (parent_path); + + return parent; +} + +static gboolean +set_selection_for_clip_data_entry (GrdRdpFuseClipboard *rdp_fuse_clipboard, + FILEDESCRIPTORW *files, + uint32_t n_files, + ClipDataEntry *entry) +{ + FuseFile *clip_data_dir = entry->clip_data_dir; + uint32_t clip_data_id = clip_data_dir->clip_data_id; + uint32_t i; + + if (entry->has_clip_data_id) + g_debug ("[FUSE Clipboard] Setting selection for clipDataId %u", clip_data_id); + else + g_debug ("[FUSE Clipboard] Setting selection"); + + for (i = 0; i < n_files; ++i) + { + FILEDESCRIPTORW *file; + FuseFile *fuse_file, *parent; + char *filename = NULL; + uint32_t j; + + file = &files[i]; + + fuse_file = g_malloc0 (sizeof (FuseFile)); + if (!(file->dwFlags & FD_ATTRIBUTES)) + g_warning ("[RDP.CLIPRDR] Client did not set the FD_ATTRIBUTES flag"); + + filename = ConvertWCharToUtf8Alloc (file->cFileName, NULL); + if (!filename) + { + g_warning ("[RDP.CLIPRDR] Failed to convert filename. Aborting " + "SelectionTransfer"); + clear_entry_selection (rdp_fuse_clipboard, entry); + fuse_file_free (fuse_file); + + return FALSE; + } + + for (j = 0; filename[j]; ++j) + { + if (filename[j] == '\\') + filename[j] = '/'; + } + fuse_file->filename_with_root = + g_strdup_printf ("%s/%s", clip_data_dir->filename_with_root, filename); + fuse_file->filename = strrchr (fuse_file->filename_with_root, '/') + 1; + g_free (filename); + + parent = get_parent_directory (rdp_fuse_clipboard, + fuse_file->filename_with_root); + if (!parent) + { + g_warning ("[RDP.CLIPRDR] Failed to find parent directory. Aborting " + "SelectionTransfer"); + clear_entry_selection (rdp_fuse_clipboard, entry); + fuse_file_free (fuse_file); + + return FALSE; + } + + parent->children = g_list_append (parent->children, fuse_file); + fuse_file->parent = parent; + + fuse_file->list_idx = i; + fuse_file->ino = get_next_free_inode (rdp_fuse_clipboard); + fuse_file->has_clip_data_id = entry->has_clip_data_id; + fuse_file->clip_data_id = clip_data_id; + fuse_file->entry = entry; + if (file->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) + fuse_file->is_directory = TRUE; + if (file->dwFileAttributes & FILE_ATTRIBUTE_READONLY) + fuse_file->is_readonly = TRUE; + if (file->dwFlags & FD_FILESIZE) + { + fuse_file->size = ((uint64_t) file->nFileSizeHigh << 32) + + file->nFileSizeLow; + fuse_file->has_size = TRUE; + } + if (file->dwFlags & FD_WRITESTIME) + { + uint64_t filetime; + + filetime = file->ftLastWriteTime.dwHighDateTime; + filetime <<= 32; + filetime += file->ftLastWriteTime.dwLowDateTime; + + fuse_file->last_write_time_unix = filetime / (10 * G_USEC_PER_SEC) - + WIN32_FILETIME_TO_UNIX_EPOCH; + fuse_file->has_last_write_time = TRUE; + } + g_hash_table_insert (rdp_fuse_clipboard->inode_table, + GUINT_TO_POINTER (fuse_file->ino), fuse_file); + } + if (entry->has_clip_data_id) + g_debug ("[FUSE Clipboard] Selection set for clipDataId %u", clip_data_id); + else + g_debug ("[FUSE Clipboard] Selection set"); + + return TRUE; +} + +gboolean +grd_rdp_fuse_clipboard_set_cdi_selection (GrdRdpFuseClipboard *rdp_fuse_clipboard, + FILEDESCRIPTORW *files, + uint32_t n_files, + uint32_t clip_data_id) +{ + ClipDataEntry *entry; + gboolean result; + + g_mutex_lock (&rdp_fuse_clipboard->selection_mutex); + g_mutex_lock (&rdp_fuse_clipboard->filesystem_mutex); + if (!g_hash_table_lookup_extended (rdp_fuse_clipboard->clip_data_table, + GUINT_TO_POINTER (clip_data_id), + NULL, (gpointer *) &entry)) + { + g_mutex_unlock (&rdp_fuse_clipboard->filesystem_mutex); + g_mutex_unlock (&rdp_fuse_clipboard->selection_mutex); + return FALSE; + } + + entry->clip_data_dir = clip_data_dir_new (rdp_fuse_clipboard, + TRUE, clip_data_id); + + result = set_selection_for_clip_data_entry (rdp_fuse_clipboard, + files, n_files, entry); + g_mutex_unlock (&rdp_fuse_clipboard->filesystem_mutex); + g_mutex_unlock (&rdp_fuse_clipboard->selection_mutex); + + return result; +} + +gboolean +grd_rdp_fuse_clipboard_set_no_cdi_selection (GrdRdpFuseClipboard *rdp_fuse_clipboard, + FILEDESCRIPTORW *files, + uint32_t n_files) +{ + gboolean result; + + g_mutex_lock (&rdp_fuse_clipboard->selection_mutex); + g_mutex_lock (&rdp_fuse_clipboard->filesystem_mutex); + if (!rdp_fuse_clipboard->no_cdi_entry) + rdp_fuse_clipboard->no_cdi_entry = g_malloc0 (sizeof (ClipDataEntry)); + if (rdp_fuse_clipboard->no_cdi_entry->clip_data_dir) + clear_entry_selection (rdp_fuse_clipboard, rdp_fuse_clipboard->no_cdi_entry); + + rdp_fuse_clipboard->no_cdi_entry->clip_data_dir = + clip_data_dir_new (rdp_fuse_clipboard, FALSE, 0); + + result = set_selection_for_clip_data_entry (rdp_fuse_clipboard, files, n_files, + rdp_fuse_clipboard->no_cdi_entry); + g_mutex_unlock (&rdp_fuse_clipboard->filesystem_mutex); + g_mutex_unlock (&rdp_fuse_clipboard->selection_mutex); + + return result; +} + +static void +write_file_attributes (FuseFile *fuse_file, + struct stat *attr) +{ + memset (attr, 0, sizeof (struct stat)); + + if (!fuse_file) + return; + + attr->st_ino = fuse_file->ino; + if (fuse_file->is_directory) + { + attr->st_mode = S_IFDIR | (fuse_file->is_readonly ? 0555 : 0755); + attr->st_nlink = 2; + } + else + { + attr->st_mode = S_IFREG | (fuse_file->is_readonly ? 0444 : 0644); + attr->st_nlink = 1; + attr->st_size = fuse_file->size; + } + attr->st_uid = getuid (); + attr->st_gid = getgid (); + attr->st_atime = attr->st_mtime = attr->st_ctime = + (fuse_file->has_last_write_time ? fuse_file->last_write_time_unix + : time (NULL)); +} + +static void +maybe_queue_clip_data_entry_timeout_reset (GrdRdpFuseClipboard *rdp_fuse_clipboard, + ClipDataEntry *entry) +{ + int64_t drop_id_timeout_set_us = entry->drop_context.drop_id_timeout_set_us; + int64_t now_us; + + if (!entry->drop_context.drop_id) + return; + + now_us = g_get_monotonic_time (); + if (now_us - drop_id_timeout_set_us < CLIP_DATA_ENTRY_DROP_TIMEOUT_DELTA_US) + return; + + g_debug ("[FUSE Clipboard] Queueing a timeout reset for selection with " + "clipDataId %u", entry->clip_data_id); + g_hash_table_add (rdp_fuse_clipboard->timeouts_to_reset, + GUINT_TO_POINTER (entry->clip_data_id)); + g_source_set_ready_time (rdp_fuse_clipboard->timeout_reset_source, 0); +} + +void +grd_rdp_fuse_clipboard_submit_file_contents_response (GrdRdpFuseClipboard *rdp_fuse_clipboard, + uint32_t stream_id, + gboolean response_ok, + const uint8_t *data, + uint32_t size) +{ + RdpFuseFileContentsRequest *rdp_fuse_request; + struct fuse_entry_param entry = {0}; + + g_mutex_lock (&rdp_fuse_clipboard->filesystem_mutex); + if (!g_hash_table_steal_extended (rdp_fuse_clipboard->request_table, + GUINT_TO_POINTER (stream_id), + NULL, (gpointer *) &rdp_fuse_request)) + { + g_mutex_unlock (&rdp_fuse_clipboard->filesystem_mutex); + return; + } + + if (!response_ok) + { + g_warning ("[RDP.CLIPRDR] Failed to retrieve file data for file \"%s\" " + "from the client", rdp_fuse_request->fuse_file->filename); + g_mutex_unlock (&rdp_fuse_clipboard->filesystem_mutex); + + fuse_reply_err (rdp_fuse_request->fuse_req, EIO); + g_free (rdp_fuse_request); + return; + } + + if ((rdp_fuse_request->operation_type == FUSE_LL_OPERATION_LOOKUP || + rdp_fuse_request->operation_type == FUSE_LL_OPERATION_GETATTR) && + size != sizeof (uint64_t)) + { + g_warning ("[RDP.CLIPRDR] Received invalid file size for file \"%s\" " + "from the client", rdp_fuse_request->fuse_file->filename); + g_mutex_unlock (&rdp_fuse_clipboard->filesystem_mutex); + + fuse_reply_err (rdp_fuse_request->fuse_req, EIO); + g_free (rdp_fuse_request); + return; + } + + maybe_queue_clip_data_entry_timeout_reset (rdp_fuse_clipboard, + rdp_fuse_request->fuse_file->entry); + + if (rdp_fuse_request->operation_type == FUSE_LL_OPERATION_LOOKUP || + rdp_fuse_request->operation_type == FUSE_LL_OPERATION_GETATTR) + { + g_debug ("[FUSE Clipboard] Received file size for file \"%s\" with stream " + "id %u", rdp_fuse_request->fuse_file->filename, stream_id); + + rdp_fuse_request->fuse_file->size = *((uint64_t *) data); + rdp_fuse_request->fuse_file->has_size = TRUE; + + entry.ino = rdp_fuse_request->fuse_file->ino; + write_file_attributes (rdp_fuse_request->fuse_file, &entry.attr); + entry.attr_timeout = 1.0; + entry.entry_timeout = 1.0; + } + else if (rdp_fuse_request->operation_type == FUSE_LL_OPERATION_READ) + { + g_debug ("[FUSE Clipboard] Received file range for file \"%s\" with stream " + "id %u", rdp_fuse_request->fuse_file->filename, stream_id); + } + else + { + g_assert_not_reached (); + } + g_mutex_unlock (&rdp_fuse_clipboard->filesystem_mutex); + + switch (rdp_fuse_request->operation_type) + { + case FUSE_LL_OPERATION_NONE: + break; + case FUSE_LL_OPERATION_LOOKUP: + fuse_reply_entry (rdp_fuse_request->fuse_req, &entry); + break; + case FUSE_LL_OPERATION_GETATTR: + fuse_reply_attr (rdp_fuse_request->fuse_req, &entry.attr, entry.attr_timeout); + break; + case FUSE_LL_OPERATION_READ: + fuse_reply_buf (rdp_fuse_request->fuse_req, (const char *) data, size); + break; + } + + g_free (rdp_fuse_request); +} + +static RdpFuseFileContentsRequest * +rdp_fuse_file_contents_request_new (GrdRdpFuseClipboard *rdp_fuse_clipboard, + FuseFile *fuse_file, + fuse_req_t fuse_req) +{ + RdpFuseFileContentsRequest *rdp_fuse_request; + uint32_t stream_id = rdp_fuse_clipboard->next_stream_id; + + fuse_file->entry->had_file_contents_request = TRUE; + maybe_queue_clip_data_entry_timeout_reset (rdp_fuse_clipboard, + fuse_file->entry); + + rdp_fuse_request = g_malloc0 (sizeof (RdpFuseFileContentsRequest)); + rdp_fuse_request->fuse_file = fuse_file; + rdp_fuse_request->fuse_req = fuse_req; + + while (g_hash_table_contains (rdp_fuse_clipboard->request_table, + GUINT_TO_POINTER (stream_id))) + ++stream_id; + rdp_fuse_request->stream_id = stream_id; + + rdp_fuse_clipboard->next_stream_id = stream_id + 1; + + return rdp_fuse_request; +} + +static void +request_file_size_async (GrdRdpFuseClipboard *rdp_fuse_clipboard, + FuseFile *fuse_file, + fuse_req_t fuse_req, + FuseLowlevelOperationType operation_type) +{ + GrdClipboardRdp *clipboard_rdp = rdp_fuse_clipboard->clipboard_rdp; + RdpFuseFileContentsRequest *rdp_fuse_request; + + rdp_fuse_request = rdp_fuse_file_contents_request_new (rdp_fuse_clipboard, + fuse_file, + fuse_req); + rdp_fuse_request->operation_type = operation_type; + + g_hash_table_insert (rdp_fuse_clipboard->request_table, + GUINT_TO_POINTER (rdp_fuse_request->stream_id), + rdp_fuse_request); + + g_debug ("[FUSE Clipboard] Requesting file size for file \"%s\" with stream id" + " %u", fuse_file->filename, rdp_fuse_request->stream_id); + grd_clipboard_rdp_request_remote_file_size_async (clipboard_rdp, + rdp_fuse_request->stream_id, + fuse_file->list_idx, + fuse_file->has_clip_data_id, + fuse_file->clip_data_id); +} + +static void +request_file_range_async (GrdRdpFuseClipboard *rdp_fuse_clipboard, + FuseFile *fuse_file, + fuse_req_t fuse_req, + uint64_t offset, + uint32_t requested_size) +{ + GrdClipboardRdp *clipboard_rdp = rdp_fuse_clipboard->clipboard_rdp; + RdpFuseFileContentsRequest *rdp_fuse_request; + + rdp_fuse_request = rdp_fuse_file_contents_request_new (rdp_fuse_clipboard, + fuse_file, + fuse_req); + rdp_fuse_request->operation_type = FUSE_LL_OPERATION_READ; + + g_hash_table_insert (rdp_fuse_clipboard->request_table, + GUINT_TO_POINTER (rdp_fuse_request->stream_id), + rdp_fuse_request); + + g_debug ("[FUSE Clipboard] Requesting file range (%u Bytes at offset %lu) for " + "file \"%s\" with stream id %u", requested_size, offset, + fuse_file->filename, rdp_fuse_request->stream_id); + grd_clipboard_rdp_request_remote_file_range_async (clipboard_rdp, + rdp_fuse_request->stream_id, + fuse_file->list_idx, + offset, requested_size, + fuse_file->has_clip_data_id, + fuse_file->clip_data_id); +} + +static FuseFile * +get_fuse_file_by_ino (GrdRdpFuseClipboard *rdp_fuse_clipboard, + fuse_ino_t fuse_ino) +{ + FuseFile *fuse_file; + + fuse_file = g_hash_table_lookup (rdp_fuse_clipboard->inode_table, + GUINT_TO_POINTER (fuse_ino)); + + return fuse_file; +} + +static FuseFile * +get_fuse_file_by_name_from_parent (GrdRdpFuseClipboard *rdp_fuse_clipboard, + FuseFile *parent, + const char *name) +{ + FuseFile *child; + GList *l; + + for (l = parent->children; l; l = l->next) + { + child = l->data; + + if (strcmp (name, child->filename) == 0) + return child; + } + + /** + * This is not an error since several applications try to find specific files, + * e.g. nautilus tries to find "/.Trash", etc. + */ + g_debug ("[FUSE Clipboard] Requested file \"%s\" in directory \"%s\" does not " + "exist", name, parent->filename); + + return NULL; +} + +static void +fuse_ll_lookup (fuse_req_t fuse_req, + fuse_ino_t parent_ino, + const char *name) +{ + GrdRdpFuseClipboard *rdp_fuse_clipboard = fuse_req_userdata (fuse_req); + FuseFile *parent, *fuse_file; + struct fuse_entry_param entry = {0}; + + g_mutex_lock (&rdp_fuse_clipboard->filesystem_mutex); + if (!(parent = get_fuse_file_by_ino (rdp_fuse_clipboard, parent_ino)) || + !parent->is_directory) + { + g_mutex_unlock (&rdp_fuse_clipboard->filesystem_mutex); + fuse_reply_err (fuse_req, ENOENT); + return; + } + if (!(fuse_file = get_fuse_file_by_name_from_parent ( + rdp_fuse_clipboard, parent, name))) + { + g_mutex_unlock (&rdp_fuse_clipboard->filesystem_mutex); + fuse_reply_err (fuse_req, ENOENT); + return; + } + + g_debug ("[FUSE Clipboard] lookup() has been called for \"%s\"", name); + g_debug ("[FUSE Clipboard] Parent is \"%s\", child is \"%s\"", + parent->filename_with_root, fuse_file->filename_with_root); + + if (!fuse_file->is_directory && !fuse_file->has_size) + { + request_file_size_async (rdp_fuse_clipboard, fuse_file, fuse_req, + FUSE_LL_OPERATION_LOOKUP); + g_mutex_unlock (&rdp_fuse_clipboard->filesystem_mutex); + return; + } + + entry.ino = fuse_file->ino; + write_file_attributes (fuse_file, &entry.attr); + entry.attr_timeout = 1.0; + entry.entry_timeout = 1.0; + g_mutex_unlock (&rdp_fuse_clipboard->filesystem_mutex); + + fuse_reply_entry (fuse_req, &entry); +} + +static void +fuse_ll_getattr (fuse_req_t fuse_req, + fuse_ino_t fuse_ino, + struct fuse_file_info *file_info) +{ + GrdRdpFuseClipboard *rdp_fuse_clipboard = fuse_req_userdata (fuse_req); + FuseFile *fuse_file; + struct stat attr = {0}; + + g_mutex_lock (&rdp_fuse_clipboard->filesystem_mutex); + if (!(fuse_file = get_fuse_file_by_ino (rdp_fuse_clipboard, fuse_ino))) + { + g_mutex_unlock (&rdp_fuse_clipboard->filesystem_mutex); + fuse_reply_err (fuse_req, ENOENT); + return; + } + + g_debug ("[FUSE Clipboard] getattr() has been called for file \"%s\"", + fuse_file->filename_with_root); + + if (!fuse_file->is_directory && !fuse_file->has_size) + { + request_file_size_async (rdp_fuse_clipboard, fuse_file, fuse_req, + FUSE_LL_OPERATION_GETATTR); + g_mutex_unlock (&rdp_fuse_clipboard->filesystem_mutex); + return; + } + + write_file_attributes (fuse_file, &attr); + g_mutex_unlock (&rdp_fuse_clipboard->filesystem_mutex); + + fuse_reply_attr (fuse_req, &attr, 1.0); +} + +static void +fuse_ll_open (fuse_req_t fuse_req, + fuse_ino_t fuse_ino, + struct fuse_file_info *file_info) +{ + GrdRdpFuseClipboard *rdp_fuse_clipboard = fuse_req_userdata (fuse_req); + FuseFile *fuse_file; + g_autofree char *filename_with_root = NULL; + + g_mutex_lock (&rdp_fuse_clipboard->filesystem_mutex); + if (!(fuse_file = get_fuse_file_by_ino (rdp_fuse_clipboard, fuse_ino))) + { + g_mutex_unlock (&rdp_fuse_clipboard->filesystem_mutex); + fuse_reply_err (fuse_req, ENOENT); + return; + } + if (fuse_file->is_directory) + { + g_mutex_unlock (&rdp_fuse_clipboard->filesystem_mutex); + fuse_reply_err (fuse_req, EISDIR); + return; + } + + filename_with_root = g_strdup (fuse_file->filename_with_root); + g_mutex_unlock (&rdp_fuse_clipboard->filesystem_mutex); + + if ((file_info->flags & O_ACCMODE) != O_RDONLY) + { + fuse_reply_err (fuse_req, EACCES); + return; + } + + /* Using direct_io also increases FUSE_MAX_PAGES_PER_REQ */ + file_info->direct_io = 1; + + g_debug ("[FUSE Clipboard] Opening file \"%s\"", filename_with_root); + fuse_reply_open (fuse_req, file_info); +} + +static void +fuse_ll_read (fuse_req_t fuse_req, + fuse_ino_t fuse_ino, + size_t size, + off_t offset, + struct fuse_file_info *file_info) +{ + GrdRdpFuseClipboard *rdp_fuse_clipboard = fuse_req_userdata (fuse_req); + FuseFile *fuse_file; + + g_mutex_lock (&rdp_fuse_clipboard->filesystem_mutex); + if (!(fuse_file = get_fuse_file_by_ino (rdp_fuse_clipboard, fuse_ino))) + { + g_mutex_unlock (&rdp_fuse_clipboard->filesystem_mutex); + fuse_reply_err (fuse_req, ENOENT); + return; + } + if (fuse_file->is_directory) + { + g_mutex_unlock (&rdp_fuse_clipboard->filesystem_mutex); + fuse_reply_err (fuse_req, EISDIR); + return; + } + if (!fuse_file->has_size || offset > fuse_file->size) + { + g_mutex_unlock (&rdp_fuse_clipboard->filesystem_mutex); + fuse_reply_err (fuse_req, EINVAL); + return; + } + + size = MIN (size, 8 * 1024 * 1024); + g_assert (size > 0); + + request_file_range_async (rdp_fuse_clipboard, fuse_file, fuse_req, + offset, size); + g_mutex_unlock (&rdp_fuse_clipboard->filesystem_mutex); +} + +static void +fuse_ll_opendir (fuse_req_t fuse_req, + fuse_ino_t fuse_ino, + struct fuse_file_info *file_info) +{ + GrdRdpFuseClipboard *rdp_fuse_clipboard = fuse_req_userdata (fuse_req); + FuseFile *fuse_file; + g_autofree char *filename_with_root = NULL; + + g_mutex_lock (&rdp_fuse_clipboard->filesystem_mutex); + if (!(fuse_file = get_fuse_file_by_ino (rdp_fuse_clipboard, fuse_ino))) + { + g_mutex_unlock (&rdp_fuse_clipboard->filesystem_mutex); + fuse_reply_err (fuse_req, ENOENT); + return; + } + if (!fuse_file->is_directory) + { + g_mutex_unlock (&rdp_fuse_clipboard->filesystem_mutex); + fuse_reply_err (fuse_req, ENOTDIR); + return; + } + + filename_with_root = g_strdup (fuse_file->filename_with_root); + g_mutex_unlock (&rdp_fuse_clipboard->filesystem_mutex); + + if ((file_info->flags & O_ACCMODE) != O_RDONLY) + { + fuse_reply_err (fuse_req, EACCES); + return; + } + + g_debug ("[FUSE Clipboard] Opening directory \"%s\"", filename_with_root); + fuse_reply_open (fuse_req, file_info); +} + +static void +fuse_ll_readdir (fuse_req_t fuse_req, + fuse_ino_t fuse_ino, + size_t max_size, + off_t offset, + struct fuse_file_info *file_info) +{ + GrdRdpFuseClipboard *rdp_fuse_clipboard = fuse_req_userdata (fuse_req); + FuseFile *fuse_file, *child; + struct stat attr = {0}; + size_t written_size, entry_size; + char *filename; + char *buf; + off_t i; + GList *l; + + g_mutex_lock (&rdp_fuse_clipboard->filesystem_mutex); + if (!(fuse_file = get_fuse_file_by_ino (rdp_fuse_clipboard, fuse_ino))) + { + g_mutex_unlock (&rdp_fuse_clipboard->filesystem_mutex); + fuse_reply_err (fuse_req, ENOENT); + return; + } + if (!fuse_file->is_directory) + { + g_mutex_unlock (&rdp_fuse_clipboard->filesystem_mutex); + fuse_reply_err (fuse_req, ENOTDIR); + return; + } + + g_debug ("[FUSE Clipboard] Reading directory \"%s\" at offset %lu", + fuse_file->filename_with_root, offset); + + if (offset >= g_list_length (fuse_file->children) + 1) + { + g_mutex_unlock (&rdp_fuse_clipboard->filesystem_mutex); + fuse_reply_buf (fuse_req, NULL, 0); + return; + } + + buf = g_malloc0 (max_size); + written_size = 0; + + for (i = offset; i < 2; ++i) + { + if (i == 0) + { + write_file_attributes (fuse_file, &attr); + filename = "."; + } + else if (i == 1) + { + write_file_attributes (fuse_file->parent, &attr); + attr.st_ino = fuse_file->parent ? attr.st_ino : FUSE_ROOT_ID; + attr.st_mode = fuse_file->parent ? attr.st_mode : 0555; + filename = ".."; + } + else + { + g_assert_not_reached (); + } + + /** + * buf needs to be large enough to hold the entry. If it's not, then the + * entry is not filled in but the size of the entry is still returned. + */ + entry_size = fuse_add_direntry (fuse_req, buf + written_size, + max_size - written_size, + filename, &attr, i); + if (entry_size > max_size - written_size) + break; + + written_size += entry_size; + } + + for (l = fuse_file->children, i = 2; l; l = l->next, ++i) + { + if (i <= offset) + continue; + + child = l->data; + + write_file_attributes (child, &attr); + entry_size = fuse_add_direntry (fuse_req, buf + written_size, + max_size - written_size, + child->filename, &attr, i); + if (entry_size > max_size - written_size) + break; + + written_size += entry_size; + } + g_mutex_unlock (&rdp_fuse_clipboard->filesystem_mutex); + + fuse_reply_buf (fuse_req, buf, written_size); + g_free (buf); +} + +static const struct fuse_lowlevel_ops fuse_ll_ops = +{ + .lookup = fuse_ll_lookup, + .getattr = fuse_ll_getattr, + .open = fuse_ll_open, + .read = fuse_ll_read, + .opendir = fuse_ll_opendir, + .readdir = fuse_ll_readdir, +}; + +static gpointer +fuse_thread_func (gpointer data) +{ + GrdRdpFuseClipboard *rdp_fuse_clipboard = data; + struct fuse_args args = {0}; + char *argv[1]; + int result; + + g_debug ("[FUSE Clipboard] FUSE thread started"); + + argv[0] = program_invocation_name; + args.argc = 1; + args.argv = argv; + + rdp_fuse_clipboard->fuse_handle = fuse_session_new (&args, &fuse_ll_ops, + sizeof (fuse_ll_ops), + rdp_fuse_clipboard); + if (!rdp_fuse_clipboard->fuse_handle) + g_error ("[FUSE Clipboard] Failed to create FUSE filesystem"); + + if (fuse_session_mount (rdp_fuse_clipboard->fuse_handle, + rdp_fuse_clipboard->mount_path)) + g_error ("[FUSE Clipboard] Failed to mount FUSE filesystem"); + + fuse_daemonize (1); + + grd_sync_point_complete (&rdp_fuse_clipboard->sync_point_start, TRUE); + + g_debug ("[FUSE Clipboard] Starting FUSE session"); + result = fuse_session_loop (rdp_fuse_clipboard->fuse_handle); + if (result < 0) + g_error ("fuse_loop() failed: %s", g_strerror (-result)); + + g_debug ("[FUSE Clipboard] Unmounting FUSE filesystem"); + fuse_session_unmount (rdp_fuse_clipboard->fuse_handle); + + grd_sync_point_wait_for_completion (&rdp_fuse_clipboard->sync_point_stop); + g_clear_pointer (&rdp_fuse_clipboard->fuse_handle, fuse_session_destroy); + + return NULL; +} + +GrdRdpFuseClipboard * +grd_rdp_fuse_clipboard_new (GrdClipboardRdp *clipboard_rdp, + const char *mount_path) +{ + GrdRdpFuseClipboard *rdp_fuse_clipboard; + + rdp_fuse_clipboard = g_object_new (GRD_TYPE_RDP_FUSE_CLIPBOARD, NULL); + rdp_fuse_clipboard->clipboard_rdp = clipboard_rdp; + rdp_fuse_clipboard->mount_path = g_strdup (mount_path); + + rdp_fuse_clipboard->fuse_thread = g_thread_new ("RDP FUSE clipboard thread", + fuse_thread_func, + rdp_fuse_clipboard); + if (!rdp_fuse_clipboard->fuse_thread) + g_error ("[FUSE Clipboard] Failed to create FUSE thread"); + + grd_sync_point_wait_for_completion (&rdp_fuse_clipboard->sync_point_start); + + return rdp_fuse_clipboard; +} + +static void +dismiss_all_requests (GrdRdpFuseClipboard *rdp_fuse_clipboard) +{ + ClearRdpFuseRequestContext clear_context = {0}; + + clear_context.rdp_fuse_clipboard = rdp_fuse_clipboard; + clear_context.all_files = TRUE; + + g_mutex_lock (&rdp_fuse_clipboard->filesystem_mutex); + g_hash_table_foreach_remove (rdp_fuse_clipboard->request_table, + maybe_clear_rdp_fuse_request, &clear_context); + g_mutex_unlock (&rdp_fuse_clipboard->filesystem_mutex); +} + +static void +grd_rdp_fuse_clipboard_dispose (GObject *object) +{ + GrdRdpFuseClipboard *rdp_fuse_clipboard = GRD_RDP_FUSE_CLIPBOARD (object); + + if (rdp_fuse_clipboard->timeout_reset_source) + { + g_source_destroy (rdp_fuse_clipboard->timeout_reset_source); + g_clear_pointer (&rdp_fuse_clipboard->timeout_reset_source, g_source_unref); + } + + if (rdp_fuse_clipboard->fuse_thread) + { + struct stat attr; + + g_assert (rdp_fuse_clipboard->fuse_handle); + + clear_all_selections (rdp_fuse_clipboard); + + g_debug ("[FUSE Clipboard] Stopping FUSE thread"); + fuse_session_exit (rdp_fuse_clipboard->fuse_handle); + dismiss_all_requests (rdp_fuse_clipboard); + grd_sync_point_complete (&rdp_fuse_clipboard->sync_point_stop, TRUE); + + /** + * FUSE does not immediately stop the session after fuse_session_exit() has + * been called. + * Instead, it waits on a new operation. Upon retrieving this new + * operation, it checks the exit flag and then stops the session loop. + * So, trigger a FUSE operation by poking at the FUSE root dir to + * effectively stop the session. + */ + stat (rdp_fuse_clipboard->mount_path, &attr); + + g_debug ("[FUSE Clipboard] Waiting on FUSE thread"); + g_clear_pointer (&rdp_fuse_clipboard->fuse_thread, g_thread_join); + g_debug ("[FUSE Clipboard] FUSE thread stopped"); + } + + g_clear_pointer (&rdp_fuse_clipboard->mount_path, g_free); + + if (rdp_fuse_clipboard->request_table) + { + dismiss_all_requests (rdp_fuse_clipboard); + g_clear_pointer (&rdp_fuse_clipboard->request_table, g_hash_table_unref); + } + g_clear_pointer (&rdp_fuse_clipboard->no_cdi_entry, g_free); + g_clear_pointer (&rdp_fuse_clipboard->clip_data_table, g_hash_table_destroy); + g_clear_pointer (&rdp_fuse_clipboard->inode_table, g_hash_table_destroy); + g_clear_pointer (&rdp_fuse_clipboard->timeouts_to_reset, g_hash_table_destroy); + + G_OBJECT_CLASS (grd_rdp_fuse_clipboard_parent_class)->dispose (object); +} + +static void +grd_rdp_fuse_clipboard_finalize (GObject *object) +{ + GrdRdpFuseClipboard *rdp_fuse_clipboard = GRD_RDP_FUSE_CLIPBOARD (object); + + grd_sync_point_clear (&rdp_fuse_clipboard->sync_point_stop); + grd_sync_point_clear (&rdp_fuse_clipboard->sync_point_start); + + g_mutex_clear (&rdp_fuse_clipboard->selection_mutex); + g_mutex_clear (&rdp_fuse_clipboard->filesystem_mutex); + + G_OBJECT_CLASS (grd_rdp_fuse_clipboard_parent_class)->finalize (object); +} + +static void +clip_data_entry_free (gpointer data) +{ + ClipDataEntry *entry = data; + + grd_clipboard_rdp_unlock_remote_clipboard_data (entry->clipboard_rdp, + entry->clip_data_id); + + g_clear_handle_id (&entry->drop_context.drop_id, g_source_remove); + + g_free (entry); +} + +static FuseFile * +fuse_file_new_root (void) +{ + FuseFile *root_dir; + + root_dir = g_malloc0 (sizeof (FuseFile)); + root_dir->filename_with_root = g_strdup ("/"); + root_dir->filename = root_dir->filename_with_root; + root_dir->ino = FUSE_ROOT_ID; + root_dir->is_directory = TRUE; + root_dir->is_readonly = TRUE; + + return root_dir; +} + +static gboolean +maybe_reset_clip_data_entry_timeout (gpointer key, + gpointer value, + gpointer user_data) +{ + GrdRdpFuseClipboard *rdp_fuse_clipboard = user_data; + uint32_t clip_data_id = GPOINTER_TO_UINT (key); + ClipDataEntry *entry; + + if (!g_hash_table_lookup_extended (rdp_fuse_clipboard->clip_data_table, + GUINT_TO_POINTER (clip_data_id), + NULL, (gpointer *) &entry)) + return TRUE; + + if (!entry->drop_context.drop_id) + return TRUE; + + g_debug ("[FUSE Clipboard] Resetting timeout for selection with clipDataId %u", + clip_data_id); + g_clear_handle_id (&entry->drop_context.drop_id, g_source_remove); + entry->drop_context.drop_id = g_timeout_add (CLIP_DATA_ENTRY_DROP_TIMEOUT_MS, + drop_clip_data_entry, entry); + entry->drop_context.drop_id_timeout_set_us = g_get_monotonic_time (); + + return TRUE; +} + +static gboolean +reset_clip_data_entry_timeouts (gpointer user_data) +{ + GrdRdpFuseClipboard *rdp_fuse_clipboard = user_data; + + g_mutex_lock (&rdp_fuse_clipboard->filesystem_mutex); + g_hash_table_foreach_remove (rdp_fuse_clipboard->timeouts_to_reset, + maybe_reset_clip_data_entry_timeout, + rdp_fuse_clipboard); + g_mutex_unlock (&rdp_fuse_clipboard->filesystem_mutex); + + return G_SOURCE_CONTINUE; +} + +static gboolean +timeout_reset_source_dispatch (GSource *source, + GSourceFunc callback, + gpointer user_data) +{ + g_source_set_ready_time (source, -1); + + return callback (user_data); +} + +static GSourceFuncs timeout_reset_source_funcs = +{ + .dispatch = timeout_reset_source_dispatch, +}; + +static void +grd_rdp_fuse_clipboard_init (GrdRdpFuseClipboard *rdp_fuse_clipboard) +{ + GSource *timeout_reset_source; + + rdp_fuse_clipboard->timeouts_to_reset = g_hash_table_new (NULL, NULL); + rdp_fuse_clipboard->inode_table = g_hash_table_new (NULL, NULL); + rdp_fuse_clipboard->clip_data_table = g_hash_table_new_full (NULL, NULL, NULL, + clip_data_entry_free); + rdp_fuse_clipboard->request_table = g_hash_table_new (NULL, NULL); + + rdp_fuse_clipboard->root_dir = fuse_file_new_root (); + g_hash_table_insert (rdp_fuse_clipboard->inode_table, + GUINT_TO_POINTER (rdp_fuse_clipboard->root_dir->ino), + rdp_fuse_clipboard->root_dir); + + g_mutex_init (&rdp_fuse_clipboard->filesystem_mutex); + g_mutex_init (&rdp_fuse_clipboard->selection_mutex); + + grd_sync_point_init (&rdp_fuse_clipboard->sync_point_start); + grd_sync_point_init (&rdp_fuse_clipboard->sync_point_stop); + + timeout_reset_source = g_source_new (&timeout_reset_source_funcs, + sizeof (GSource)); + g_source_set_callback (timeout_reset_source, reset_clip_data_entry_timeouts, + rdp_fuse_clipboard, NULL); + g_source_set_ready_time (timeout_reset_source, -1); + g_source_attach (timeout_reset_source, NULL); + rdp_fuse_clipboard->timeout_reset_source = timeout_reset_source; +} + +static void +grd_rdp_fuse_clipboard_class_init (GrdRdpFuseClipboardClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = grd_rdp_fuse_clipboard_dispose; + object_class->finalize = grd_rdp_fuse_clipboard_finalize; +} diff --git a/grd-rdp-fuse-clipboard.h b/grd-rdp-fuse-clipboard.h new file mode 100644 index 0000000..a9ea955 --- /dev/null +++ b/grd-rdp-fuse-clipboard.h @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2020 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include +#include + +#include "grd-types.h" + +#define GRD_RDP_FUSE_CLIPBOARD_NO_CLIP_DATA_ID (UINT64_C (1) << 32) + +#define GRD_TYPE_RDP_FUSE_CLIPBOARD (grd_rdp_fuse_clipboard_get_type ()) +G_DECLARE_FINAL_TYPE (GrdRdpFuseClipboard, grd_rdp_fuse_clipboard, + GRD, RDP_FUSE_CLIPBOARD, GObject) + +GrdRdpFuseClipboard *grd_rdp_fuse_clipboard_new (GrdClipboardRdp *clipboard_rdp, + const char *mount_path); + +uint32_t grd_rdp_fuse_clipboard_clip_data_id_new (GrdRdpFuseClipboard *rdp_fuse_clipboard); + +void grd_rdp_fuse_clipboard_clip_data_id_free (GrdRdpFuseClipboard *rdp_fuse_clipboard, + uint32_t clip_data_id); + +void grd_rdp_fuse_clipboard_dismiss_all_no_cdi_requests (GrdRdpFuseClipboard *rdp_fuse_clipboard); + +void grd_rdp_fuse_clipboard_clear_no_cdi_selection (GrdRdpFuseClipboard *rdp_fuse_clipboard); + +void grd_rdp_fuse_clipboard_lazily_clear_all_cdi_selections (GrdRdpFuseClipboard *rdp_fuse_clipboard); + +gboolean grd_rdp_fuse_clipboard_set_cdi_selection (GrdRdpFuseClipboard *rdp_fuse_clipboard, + FILEDESCRIPTORW *files, + uint32_t n_files, + uint32_t clip_data_id); + +gboolean grd_rdp_fuse_clipboard_set_no_cdi_selection (GrdRdpFuseClipboard *rdp_fuse_clipboard, + FILEDESCRIPTORW *files, + uint32_t n_files); + +void grd_rdp_fuse_clipboard_submit_file_contents_response (GrdRdpFuseClipboard *rdp_fuse_clipboard, + uint32_t stream_id, + gboolean response_ok, + const uint8_t *data, + uint32_t size); diff --git a/grd-rdp-gfx-frame-controller.c b/grd-rdp-gfx-frame-controller.c new file mode 100644 index 0000000..087f49a --- /dev/null +++ b/grd-rdp-gfx-frame-controller.c @@ -0,0 +1,395 @@ +/* + * Copyright (C) 2022 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-rdp-gfx-frame-controller.h" + +#include "grd-rdp-frame-stats.h" +#include "grd-rdp-gfx-frame-log.h" +#include "grd-rdp-gfx-framerate-log.h" +#include "grd-rdp-surface.h" +#include "grd-rdp-surface-renderer.h" + +#define ACTIVATE_THROTTLING_TH_DEFAULT 2 +#define DEACTIVATE_THROTTLING_TH_DEFAULT 1 + +#define UNLIMITED_FRAME_SLOTS (UINT32_MAX) + +typedef enum _ThrottlingState +{ + THROTTLING_STATE_INACTIVE, + THROTTLING_STATE_ACTIVE, + THROTTLING_STATE_ACTIVE_LOWERING_LATENCY, +} ThrottlingState; + +struct _GrdRdpGfxFrameController +{ + GObject parent; + + GrdRdpSurface *rdp_surface; + + GrdRdpGfxFrameLog *frame_log; + GrdRdpGfxFramerateLog *framerate_log; + + int64_t nw_auto_last_rtt_us; + + ThrottlingState throttling_state; + /* Throttling triggers on >= activate_throttling_th */ + uint32_t activate_throttling_th; + /* Throttling triggers on <= deactivate_throttling_th */ + uint32_t deactivate_throttling_th; +}; + +G_DEFINE_TYPE (GrdRdpGfxFrameController, + grd_rdp_gfx_frame_controller, + G_TYPE_OBJECT) + +GrdRdpGfxFramerateLog * +grd_rdp_gfx_frame_controller_get_framerate_log (GrdRdpGfxFrameController *frame_controller) +{ + return frame_controller->framerate_log; +} + +static void +notify_frame_stats (GrdRdpGfxFrameController *frame_controller, + uint32_t enc_rate, + uint32_t ack_rate) +{ + GrdRdpGfxFrameLog *frame_log = frame_controller->frame_log; + uint32_t missing_dual_frame_acks = + grd_rdp_gfx_frame_log_get_unacked_dual_frames_count (frame_log); + g_autoptr (GrdRdpFrameStats) frame_stats = NULL; + + frame_stats = grd_rdp_frame_stats_new (missing_dual_frame_acks, + enc_rate, ack_rate); + + grd_rdp_gfx_framerate_log_notify_frame_stats (frame_controller->framerate_log, + frame_stats); +} + +void +grd_rdp_gfx_frame_controller_notify_history_changed (GrdRdpGfxFrameController *frame_controller) +{ + GrdRdpGfxFrameLog *frame_log = frame_controller->frame_log; + uint32_t enc_rate = 0; + uint32_t ack_rate = 0; + + grd_rdp_gfx_frame_log_update_rates (frame_log, &enc_rate, &ack_rate); + notify_frame_stats (frame_controller, enc_rate, ack_rate); +} + +static gboolean +is_rendering_suspended (GrdRdpGfxFrameController *frame_controller) +{ + GrdRdpSurface *rdp_surface = frame_controller->rdp_surface; + GrdRdpSurfaceRenderer *surface_renderer = + grd_rdp_surface_get_surface_renderer (rdp_surface); + uint32_t total_frame_slots = + grd_rdp_surface_renderer_get_total_frame_slots (surface_renderer); + + return total_frame_slots == 0; +} + +static uint32_t +get_activate_throttling_th_from_rtt (GrdRdpGfxFrameController *frame_controller, + int64_t rtt_us) +{ + GrdRdpSurface *rdp_surface = frame_controller->rdp_surface; + GrdRdpSurfaceRenderer *surface_renderer = + grd_rdp_surface_get_surface_renderer (rdp_surface); + int64_t refresh_rate = + grd_rdp_surface_renderer_get_refresh_rate (surface_renderer); + uint32_t activate_throttling_th; + uint32_t delayed_frames; + + delayed_frames = rtt_us * refresh_rate / G_USEC_PER_SEC; + + activate_throttling_th = MAX (2, MIN (delayed_frames + 2, refresh_rate)); + g_assert (activate_throttling_th > frame_controller->deactivate_throttling_th); + + return activate_throttling_th; +} + +void +grd_rdp_gfx_frame_controller_unack_frame (GrdRdpGfxFrameController *frame_controller, + uint32_t frame_id, + uint32_t n_subframes, + int64_t enc_time_us) +{ + GrdRdpSurface *rdp_surface = frame_controller->rdp_surface; + GrdRdpGfxFrameLog *frame_log = frame_controller->frame_log; + GrdRdpSurfaceRenderer *surface_renderer = + grd_rdp_surface_get_surface_renderer (rdp_surface); + uint32_t current_activate_throttling_th; + uint32_t n_unacked_frames; + uint32_t enc_rate = 0; + uint32_t ack_rate = 0; + + grd_rdp_gfx_frame_log_track_frame (frame_log, frame_id, n_subframes, + enc_time_us); + + n_unacked_frames = grd_rdp_gfx_frame_log_get_unacked_frames_count (frame_log); + grd_rdp_gfx_frame_log_update_rates (frame_log, &enc_rate, &ack_rate); + notify_frame_stats (frame_controller, enc_rate, ack_rate); + + switch (frame_controller->throttling_state) + { + case THROTTLING_STATE_INACTIVE: + frame_controller->activate_throttling_th = + get_activate_throttling_th_from_rtt (frame_controller, + frame_controller->nw_auto_last_rtt_us); + + if (n_unacked_frames >= frame_controller->activate_throttling_th) + { + frame_controller->throttling_state = THROTTLING_STATE_ACTIVE; + grd_rdp_surface_renderer_update_total_frame_slots (surface_renderer, 0); + } + break; + case THROTTLING_STATE_ACTIVE: + current_activate_throttling_th = + get_activate_throttling_th_from_rtt (frame_controller, + frame_controller->nw_auto_last_rtt_us); + + if (current_activate_throttling_th < frame_controller->activate_throttling_th) + { + frame_controller->throttling_state = THROTTLING_STATE_ACTIVE_LOWERING_LATENCY; + grd_rdp_surface_renderer_update_total_frame_slots (surface_renderer, 0); + } + else + { + uint32_t total_frame_slots; + + frame_controller->activate_throttling_th = current_activate_throttling_th; + + total_frame_slots = enc_rate > ack_rate + 1 ? 0 : ack_rate + 2 - enc_rate; + grd_rdp_surface_renderer_update_total_frame_slots (surface_renderer, + total_frame_slots); + } + break; + case THROTTLING_STATE_ACTIVE_LOWERING_LATENCY: + g_assert (is_rendering_suspended (frame_controller)); + break; + } +} + +void +grd_rdp_gfx_frame_controller_ack_frame (GrdRdpGfxFrameController *frame_controller, + uint32_t frame_id, + int64_t ack_time_us) +{ + GrdRdpSurface *rdp_surface = frame_controller->rdp_surface; + GrdRdpGfxFrameLog *frame_log = frame_controller->frame_log; + GrdRdpSurfaceRenderer *surface_renderer = + grd_rdp_surface_get_surface_renderer (rdp_surface); + uint32_t current_activate_throttling_th; + uint32_t n_unacked_frames; + uint32_t enc_rate = 0; + uint32_t ack_rate = 0; + + grd_rdp_gfx_frame_log_ack_tracked_frame (frame_log, frame_id, ack_time_us); + + n_unacked_frames = grd_rdp_gfx_frame_log_get_unacked_frames_count (frame_log); + grd_rdp_gfx_frame_log_update_rates (frame_log, &enc_rate, &ack_rate); + notify_frame_stats (frame_controller, enc_rate, ack_rate); + + switch (frame_controller->throttling_state) + { + case THROTTLING_STATE_INACTIVE: + break; + case THROTTLING_STATE_ACTIVE: + if (n_unacked_frames <= frame_controller->deactivate_throttling_th) + { + frame_controller->throttling_state = THROTTLING_STATE_INACTIVE; + grd_rdp_surface_renderer_update_total_frame_slots (surface_renderer, + UNLIMITED_FRAME_SLOTS); + break; + } + + current_activate_throttling_th = + get_activate_throttling_th_from_rtt (frame_controller, + frame_controller->nw_auto_last_rtt_us); + if (current_activate_throttling_th < frame_controller->activate_throttling_th) + { + frame_controller->throttling_state = THROTTLING_STATE_ACTIVE_LOWERING_LATENCY; + grd_rdp_surface_renderer_update_total_frame_slots (surface_renderer, 0); + } + else + { + uint32_t total_frame_slots; + + frame_controller->activate_throttling_th = current_activate_throttling_th; + + total_frame_slots = enc_rate > ack_rate ? 0 : ack_rate + 1 - enc_rate; + grd_rdp_surface_renderer_update_total_frame_slots (surface_renderer, + total_frame_slots); + } + break; + case THROTTLING_STATE_ACTIVE_LOWERING_LATENCY: + current_activate_throttling_th = + get_activate_throttling_th_from_rtt (frame_controller, + frame_controller->nw_auto_last_rtt_us); + + if (n_unacked_frames < current_activate_throttling_th) + { + frame_controller->throttling_state = THROTTLING_STATE_INACTIVE; + grd_rdp_surface_renderer_update_total_frame_slots (surface_renderer, + UNLIMITED_FRAME_SLOTS); + } + else if (n_unacked_frames == current_activate_throttling_th) + { + uint32_t total_frame_slots; + + frame_controller->throttling_state = THROTTLING_STATE_ACTIVE; + + total_frame_slots = enc_rate > ack_rate ? 0 : ack_rate + 1 - enc_rate; + grd_rdp_surface_renderer_update_total_frame_slots (surface_renderer, + total_frame_slots); + } + else if (n_unacked_frames > current_activate_throttling_th) + { + g_assert (is_rendering_suspended (frame_controller)); + } + else + { + g_assert_not_reached (); + } + break; + } +} + +static void +reevaluate_encoding_suspension_state (GrdRdpGfxFrameController *frame_controller) +{ + GrdRdpSurface *rdp_surface = frame_controller->rdp_surface; + GrdRdpGfxFrameLog *frame_log = frame_controller->frame_log; + GrdRdpSurfaceRenderer *surface_renderer = + grd_rdp_surface_get_surface_renderer (rdp_surface); + uint32_t n_unacked_frames; + uint32_t enc_rate = 0; + uint32_t ack_rate = 0; + + n_unacked_frames = grd_rdp_gfx_frame_log_get_unacked_frames_count (frame_log); + grd_rdp_gfx_frame_log_update_rates (frame_log, &enc_rate, &ack_rate); + + switch (frame_controller->throttling_state) + { + case THROTTLING_STATE_INACTIVE: + frame_controller->activate_throttling_th = + get_activate_throttling_th_from_rtt (frame_controller, + frame_controller->nw_auto_last_rtt_us); + + if (n_unacked_frames >= frame_controller->activate_throttling_th) + { + frame_controller->throttling_state = THROTTLING_STATE_ACTIVE; + grd_rdp_surface_renderer_update_total_frame_slots (surface_renderer, 0); + } + break; + case THROTTLING_STATE_ACTIVE: + g_assert (frame_controller->activate_throttling_th > + frame_controller->deactivate_throttling_th); + g_assert (n_unacked_frames > frame_controller->deactivate_throttling_th); + g_assert (is_rendering_suspended (frame_controller)); + break; + case THROTTLING_STATE_ACTIVE_LOWERING_LATENCY: + /* + * While the graphics pipeline rewrites the frame history, the RTT + * detection mechanism cannot submit a new round trip time. + */ + g_assert_not_reached (); + break; + } +} + +void +grd_rdp_gfx_frame_controller_unack_last_acked_frame (GrdRdpGfxFrameController *frame_controller, + uint32_t frame_id, + uint32_t n_subframes, + int64_t enc_ack_time_us) +{ + grd_rdp_gfx_frame_log_unack_last_acked_frame (frame_controller->frame_log, + frame_id, n_subframes, + enc_ack_time_us); + reevaluate_encoding_suspension_state (frame_controller); +} + +void +grd_rdp_gfx_frame_controller_clear_all_unacked_frames (GrdRdpGfxFrameController *frame_controller) +{ + GrdRdpSurface *rdp_surface = frame_controller->rdp_surface; + GrdRdpSurfaceRenderer *surface_renderer = + grd_rdp_surface_get_surface_renderer (rdp_surface); + + grd_rdp_gfx_frame_log_clear (frame_controller->frame_log); + + frame_controller->throttling_state = THROTTLING_STATE_INACTIVE; + grd_rdp_surface_renderer_update_total_frame_slots (surface_renderer, + UNLIMITED_FRAME_SLOTS); +} + +void +grd_rdp_gfx_frame_controller_notify_new_round_trip_time (GrdRdpGfxFrameController *frame_controller, + int64_t round_trip_time_us) +{ + frame_controller->nw_auto_last_rtt_us = round_trip_time_us; +} + +GrdRdpGfxFrameController * +grd_rdp_gfx_frame_controller_new (GrdRdpSurface *rdp_surface) +{ + GrdRdpGfxFrameController *frame_controller; + + frame_controller = g_object_new (GRD_TYPE_RDP_GFX_FRAME_CONTROLLER, NULL); + frame_controller->rdp_surface = rdp_surface; + + return frame_controller; +} + +static void +grd_rdp_gfx_frame_controller_dispose (GObject *object) +{ + GrdRdpGfxFrameController *frame_controller = GRD_RDP_GFX_FRAME_CONTROLLER (object); + + g_clear_object (&frame_controller->framerate_log); + g_clear_object (&frame_controller->frame_log); + + G_OBJECT_CLASS (grd_rdp_gfx_frame_controller_parent_class)->dispose (object); +} + +static void +grd_rdp_gfx_frame_controller_init (GrdRdpGfxFrameController *frame_controller) +{ + frame_controller->throttling_state = THROTTLING_STATE_INACTIVE; + frame_controller->activate_throttling_th = ACTIVATE_THROTTLING_TH_DEFAULT; + frame_controller->deactivate_throttling_th = DEACTIVATE_THROTTLING_TH_DEFAULT; + + g_assert (frame_controller->activate_throttling_th > + frame_controller->deactivate_throttling_th); + + frame_controller->frame_log = grd_rdp_gfx_frame_log_new (); + frame_controller->framerate_log = grd_rdp_gfx_framerate_log_new (); +} + +static void +grd_rdp_gfx_frame_controller_class_init (GrdRdpGfxFrameControllerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = grd_rdp_gfx_frame_controller_dispose; +} diff --git a/grd-rdp-gfx-frame-controller.h b/grd-rdp-gfx-frame-controller.h new file mode 100644 index 0000000..0a7a27a --- /dev/null +++ b/grd-rdp-gfx-frame-controller.h @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2022 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include +#include + +#include "grd-types.h" + +#define GRD_TYPE_RDP_GFX_FRAME_CONTROLLER (grd_rdp_gfx_frame_controller_get_type ()) +G_DECLARE_FINAL_TYPE (GrdRdpGfxFrameController, grd_rdp_gfx_frame_controller, + GRD, RDP_GFX_FRAME_CONTROLLER, GObject) + +GrdRdpGfxFrameController *grd_rdp_gfx_frame_controller_new (GrdRdpSurface *rdp_surface); + +GrdRdpGfxFramerateLog *grd_rdp_gfx_frame_controller_get_framerate_log (GrdRdpGfxFrameController *frame_controller); + +void grd_rdp_gfx_frame_controller_notify_history_changed (GrdRdpGfxFrameController *frame_controller); + +void grd_rdp_gfx_frame_controller_unack_frame (GrdRdpGfxFrameController *frame_controller, + uint32_t frame_id, + uint32_t n_subframes, + int64_t enc_time_us); + +void grd_rdp_gfx_frame_controller_ack_frame (GrdRdpGfxFrameController *frame_controller, + uint32_t frame_id, + int64_t ack_time_us); + +void grd_rdp_gfx_frame_controller_unack_last_acked_frame (GrdRdpGfxFrameController *frame_controller, + uint32_t frame_id, + uint32_t n_subframes, + int64_t enc_ack_time_us); + +void grd_rdp_gfx_frame_controller_clear_all_unacked_frames (GrdRdpGfxFrameController *frame_controller); + +void grd_rdp_gfx_frame_controller_notify_new_round_trip_time (GrdRdpGfxFrameController *frame_controller, + int64_t round_trip_time_us); diff --git a/grd-rdp-gfx-frame-log.c b/grd-rdp-gfx-frame-log.c new file mode 100644 index 0000000..d6a282c --- /dev/null +++ b/grd-rdp-gfx-frame-log.c @@ -0,0 +1,236 @@ +/* + * Copyright (C) 2021 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-rdp-gfx-frame-log.h" + +#include "grd-rdp-frame-info.h" + +struct _GrdRdpGfxFrameLog +{ + GObject parent; + + GQueue *encoded_frames; + GQueue *acked_frames; + + GHashTable *tracked_frames; + GHashTable *tracked_dual_frames; +}; + +G_DEFINE_TYPE (GrdRdpGfxFrameLog, grd_rdp_gfx_frame_log, G_TYPE_OBJECT) + +static void +track_enc_frame_info (GrdRdpGfxFrameLog *frame_log, + uint32_t frame_id, + int64_t enc_time_us) +{ + GrdRdpFrameInfo *frame_info; + + frame_info = g_malloc0 (sizeof (GrdRdpFrameInfo)); + frame_info->frame_id = frame_id; + frame_info->enc_time_us = enc_time_us; + + g_queue_push_tail (frame_log->encoded_frames, frame_info); +} + +static void +track_ack_frame_info (GrdRdpGfxFrameLog *frame_log, + uint32_t frame_id, + int64_t ack_time_us) +{ + GrdRdpFrameInfo *frame_info; + + frame_info = g_malloc0 (sizeof (GrdRdpFrameInfo)); + frame_info->frame_id = frame_id; + frame_info->ack_time_us = ack_time_us; + + g_queue_push_tail (frame_log->acked_frames, frame_info); +} + +static void +track_frame (GrdRdpGfxFrameLog *frame_log, + uint32_t frame_id, + uint32_t n_subframes) +{ + g_assert (n_subframes > 0); + g_assert (n_subframes <= 2); + + g_hash_table_add (frame_log->tracked_frames, GUINT_TO_POINTER (frame_id)); + if (n_subframes == 2) + { + g_hash_table_add (frame_log->tracked_dual_frames, + GUINT_TO_POINTER (frame_id)); + } +} + +void +grd_rdp_gfx_frame_log_track_frame (GrdRdpGfxFrameLog *frame_log, + uint32_t frame_id, + uint32_t n_subframes, + int64_t enc_time_us) +{ + track_enc_frame_info (frame_log, frame_id, enc_time_us); + track_frame (frame_log, frame_id, n_subframes); +} + +void +grd_rdp_gfx_frame_log_ack_tracked_frame (GrdRdpGfxFrameLog *frame_log, + uint32_t frame_id, + int64_t ack_time_us) +{ + g_hash_table_remove (frame_log->tracked_dual_frames, + GUINT_TO_POINTER (frame_id)); + if (!g_hash_table_remove (frame_log->tracked_frames, + GUINT_TO_POINTER (frame_id))) + return; + + track_ack_frame_info (frame_log, frame_id, ack_time_us); +} + +void +grd_rdp_gfx_frame_log_unack_last_acked_frame (GrdRdpGfxFrameLog *frame_log, + uint32_t frame_id, + uint32_t n_subframes, + int64_t enc_ack_time_us) +{ + GrdRdpFrameInfo *frame_info; + + if ((frame_info = g_queue_pop_tail (frame_log->acked_frames))) + { + g_assert (frame_info->frame_id == frame_id); + g_assert (frame_info->ack_time_us == enc_ack_time_us); + } + g_free (frame_info); + + track_frame (frame_log, frame_id, n_subframes); +} + +static void +clear_old_enc_frame_infos (GrdRdpGfxFrameLog *frame_log, + int64_t current_time_us) +{ + GrdRdpFrameInfo *frame_info; + + while ((frame_info = g_queue_peek_head (frame_log->encoded_frames)) && + current_time_us - frame_info->enc_time_us >= 1 * G_USEC_PER_SEC) + g_free (g_queue_pop_head (frame_log->encoded_frames)); +} + +static void +clear_old_ack_frame_infos (GrdRdpGfxFrameLog *frame_log, + int64_t current_time_us) +{ + GrdRdpFrameInfo *frame_info; + + while ((frame_info = g_queue_peek_head (frame_log->acked_frames)) && + current_time_us - frame_info->ack_time_us >= 1 * G_USEC_PER_SEC) + g_free (g_queue_pop_head (frame_log->acked_frames)); +} + +void +grd_rdp_gfx_frame_log_update_rates (GrdRdpGfxFrameLog *frame_log, + uint32_t *enc_rate, + uint32_t *ack_rate) +{ + int64_t current_time_us; + + current_time_us = g_get_monotonic_time (); + clear_old_enc_frame_infos (frame_log, current_time_us); + clear_old_ack_frame_infos (frame_log, current_time_us); + + /* + * Every remaining frame time, tracked in encoded_frames or acked_frames, is + * now younger than 1 second. + * The list lengths therefore represent the encoded_frames/s and + * acked_frames/s values. + */ + *enc_rate = g_queue_get_length (frame_log->encoded_frames); + *ack_rate = g_queue_get_length (frame_log->acked_frames); +} + +uint32_t +grd_rdp_gfx_frame_log_get_unacked_frames_count (GrdRdpGfxFrameLog *frame_log) +{ + return g_hash_table_size (frame_log->tracked_frames); +} + +uint32_t +grd_rdp_gfx_frame_log_get_unacked_dual_frames_count (GrdRdpGfxFrameLog *frame_log) +{ + return g_hash_table_size (frame_log->tracked_dual_frames); +} + +void +grd_rdp_gfx_frame_log_clear (GrdRdpGfxFrameLog *frame_log) +{ + g_hash_table_remove_all (frame_log->tracked_dual_frames); + g_hash_table_remove_all (frame_log->tracked_frames); +} + +GrdRdpGfxFrameLog * +grd_rdp_gfx_frame_log_new (void) +{ + GrdRdpGfxFrameLog *frame_log; + + frame_log = g_object_new (GRD_TYPE_RDP_GFX_FRAME_LOG, NULL); + + return frame_log; +} + +static void +grd_rdp_gfx_frame_log_dispose (GObject *object) +{ + GrdRdpGfxFrameLog *frame_log = GRD_RDP_GFX_FRAME_LOG (object); + + if (frame_log->acked_frames) + { + g_queue_free_full (frame_log->acked_frames, g_free); + frame_log->acked_frames = NULL; + } + + if (frame_log->encoded_frames) + { + g_queue_free_full (frame_log->encoded_frames, g_free); + frame_log->encoded_frames = NULL; + } + + g_clear_pointer (&frame_log->tracked_dual_frames, g_hash_table_destroy); + g_clear_pointer (&frame_log->tracked_frames, g_hash_table_destroy); + + G_OBJECT_CLASS (grd_rdp_gfx_frame_log_parent_class)->dispose (object); +} + +static void +grd_rdp_gfx_frame_log_init (GrdRdpGfxFrameLog *frame_log) +{ + frame_log->tracked_frames = g_hash_table_new (NULL, NULL); + frame_log->tracked_dual_frames = g_hash_table_new (NULL, NULL); + + frame_log->encoded_frames = g_queue_new (); + frame_log->acked_frames = g_queue_new (); +} + +static void +grd_rdp_gfx_frame_log_class_init (GrdRdpGfxFrameLogClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = grd_rdp_gfx_frame_log_dispose; +} diff --git a/grd-rdp-gfx-frame-log.h b/grd-rdp-gfx-frame-log.h new file mode 100644 index 0000000..4d66e23 --- /dev/null +++ b/grd-rdp-gfx-frame-log.h @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2021 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include +#include + +#include "grd-types.h" + +#define GRD_TYPE_RDP_GFX_FRAME_LOG (grd_rdp_gfx_frame_log_get_type ()) +G_DECLARE_FINAL_TYPE (GrdRdpGfxFrameLog, grd_rdp_gfx_frame_log, + GRD, RDP_GFX_FRAME_LOG, GObject) + +GrdRdpGfxFrameLog *grd_rdp_gfx_frame_log_new (void); + +void grd_rdp_gfx_frame_log_track_frame (GrdRdpGfxFrameLog *frame_log, + uint32_t frame_id, + uint32_t n_subframes, + int64_t enc_time_us); + +void grd_rdp_gfx_frame_log_ack_tracked_frame (GrdRdpGfxFrameLog *frame_log, + uint32_t frame_id, + int64_t ack_time_us); + +void grd_rdp_gfx_frame_log_unack_last_acked_frame (GrdRdpGfxFrameLog *frame_log, + uint32_t frame_id, + uint32_t n_subframes, + int64_t enc_ack_time_us); + +void grd_rdp_gfx_frame_log_update_rates (GrdRdpGfxFrameLog *frame_log, + uint32_t *enc_rate, + uint32_t *ack_rate); + +uint32_t grd_rdp_gfx_frame_log_get_unacked_frames_count (GrdRdpGfxFrameLog *frame_log); + +uint32_t grd_rdp_gfx_frame_log_get_unacked_dual_frames_count (GrdRdpGfxFrameLog *frame_log); + +void grd_rdp_gfx_frame_log_clear (GrdRdpGfxFrameLog *frame_log); diff --git a/grd-rdp-gfx-framerate-log.c b/grd-rdp-gfx-framerate-log.c new file mode 100644 index 0000000..7960683 --- /dev/null +++ b/grd-rdp-gfx-framerate-log.c @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2024 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-rdp-gfx-framerate-log.h" + +#include + +#include "grd-rdp-frame-stats.h" + +#define STABLE_ENCODING_RATE_THRESHOLD_PERCENT 0.8 + +#define MIN_N_ENC_RATES 4 +#define MIN_ENC_RATE_THRESHOLD 5 +#define MIN_VIDEO_FRAMERATE 24 + +typedef struct +{ + uint32_t enc_rate; + int64_t tracked_time_us; +} EncRateInfo; + +struct _GrdRdpGfxFramerateLog +{ + GObject parent; + + GMutex framerate_log_mutex; + GQueue *enc_rates; + uint32_t last_ack_rate; + + uint32_t missing_dual_frame_acks; +}; + +G_DEFINE_TYPE (GrdRdpGfxFramerateLog, grd_rdp_gfx_framerate_log, + G_TYPE_OBJECT) + +static void +clear_old_enc_rates (GrdRdpGfxFramerateLog *framerate_log) +{ + EncRateInfo *enc_rate_info; + int64_t current_time_us; + + current_time_us = g_get_monotonic_time (); + while ((enc_rate_info = g_queue_peek_head (framerate_log->enc_rates)) && + current_time_us - enc_rate_info->tracked_time_us >= G_USEC_PER_SEC >> 1) + g_free (g_queue_pop_head (framerate_log->enc_rates)); +} + +void +grd_rdp_gfx_framerate_log_notify_frame_stats (GrdRdpGfxFramerateLog *framerate_log, + GrdRdpFrameStats *frame_stats) +{ + EncRateInfo *enc_rate_info; + + enc_rate_info = g_new0 (EncRateInfo, 1); + enc_rate_info->enc_rate = grd_rdp_frame_stats_get_enc_rate (frame_stats); + enc_rate_info->tracked_time_us = g_get_monotonic_time (); + + g_mutex_lock (&framerate_log->framerate_log_mutex); + clear_old_enc_rates (framerate_log); + g_queue_push_tail (framerate_log->enc_rates, enc_rate_info); + + framerate_log->last_ack_rate = grd_rdp_frame_stats_get_ack_rate (frame_stats); + framerate_log->missing_dual_frame_acks = + grd_rdp_frame_stats_get_missing_dual_frame_acks (frame_stats); + g_mutex_unlock (&framerate_log->framerate_log_mutex); +} + +static int +cmp_enc_rates (gconstpointer a, + gconstpointer b, + gpointer user_data) +{ + const EncRateInfo *enc_rate_info_a = a; + const EncRateInfo *enc_rate_info_b = b; + int64_t enc_rate_a = enc_rate_info_a->enc_rate; + int64_t enc_rate_b = enc_rate_info_b->enc_rate; + + return enc_rate_a - enc_rate_b; +} + +static gboolean +has_stable_enc_rate (double enc_rate_min, + double enc_rate_median) +{ + return enc_rate_min >= + floor (enc_rate_median * STABLE_ENCODING_RATE_THRESHOLD_PERCENT); +} + +gboolean +grd_rdp_gfx_framerate_log_should_avoid_dual_frame (GrdRdpGfxFramerateLog *framerate_log) +{ + g_autoptr (GMutexLocker) locker = NULL; + g_autoptr (GQueue) tmp = NULL; + EncRateInfo *enc_rate_info; + uint32_t n_enc_rates; + uint32_t enc_rate_min; + uint32_t enc_rate_quartile3; + uint32_t enc_rate_median; + uint32_t last_ack_rate; + uint32_t missing_dual_frame_acks; + uint32_t i; + + locker = g_mutex_locker_new (&framerate_log->framerate_log_mutex); + clear_old_enc_rates (framerate_log); + + n_enc_rates = g_queue_get_length (framerate_log->enc_rates); + if (n_enc_rates < MIN_N_ENC_RATES) + return FALSE; + + tmp = g_queue_copy (framerate_log->enc_rates); + g_queue_sort (tmp, cmp_enc_rates, NULL); + + enc_rate_info = g_queue_peek_head (tmp); + enc_rate_min = enc_rate_info->enc_rate; + + for (i = 0; i < n_enc_rates >> 2; ++i) + g_queue_pop_tail (tmp); + + enc_rate_info = g_queue_peek_tail (tmp); + enc_rate_quartile3 = enc_rate_info->enc_rate; + + for (i = 0; i < n_enc_rates >> 2; ++i) + g_queue_pop_tail (tmp); + + enc_rate_info = g_queue_peek_tail (tmp); + enc_rate_median = enc_rate_info->enc_rate; + + last_ack_rate = framerate_log->last_ack_rate; + missing_dual_frame_acks = framerate_log->missing_dual_frame_acks; + g_clear_pointer (&locker, g_mutex_locker_free); + + if (enc_rate_median < MIN_ENC_RATE_THRESHOLD) + return FALSE; + + if (enc_rate_median >= MIN_VIDEO_FRAMERATE || + has_stable_enc_rate (enc_rate_min, enc_rate_median)) + return enc_rate_quartile3 + 3 * missing_dual_frame_acks >= last_ack_rate; + + return FALSE; +} + +GrdRdpGfxFramerateLog * +grd_rdp_gfx_framerate_log_new (void) +{ + return g_object_new (GRD_TYPE_RDP_GFX_FRAMERATE_LOG, NULL); +} + +static void +grd_rdp_gfx_framerate_log_dispose (GObject *object) +{ + GrdRdpGfxFramerateLog *framerate_log = GRD_RDP_GFX_FRAMERATE_LOG (object); + + if (framerate_log->enc_rates) + { + g_queue_free_full (framerate_log->enc_rates, g_free); + framerate_log->enc_rates = NULL; + } + + G_OBJECT_CLASS (grd_rdp_gfx_framerate_log_parent_class)->dispose (object); +} + +static void +grd_rdp_gfx_framerate_log_finalize (GObject *object) +{ + GrdRdpGfxFramerateLog *framerate_log = GRD_RDP_GFX_FRAMERATE_LOG (object); + + g_mutex_clear (&framerate_log->framerate_log_mutex); + + G_OBJECT_CLASS (grd_rdp_gfx_framerate_log_parent_class)->finalize (object); +} + +static void +grd_rdp_gfx_framerate_log_init (GrdRdpGfxFramerateLog *framerate_log) +{ + framerate_log->enc_rates = g_queue_new (); + + g_mutex_init (&framerate_log->framerate_log_mutex); +} + +static void +grd_rdp_gfx_framerate_log_class_init (GrdRdpGfxFramerateLogClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = grd_rdp_gfx_framerate_log_dispose; + object_class->finalize = grd_rdp_gfx_framerate_log_finalize; +} diff --git a/grd-rdp-gfx-framerate-log.h b/grd-rdp-gfx-framerate-log.h new file mode 100644 index 0000000..e0001a2 --- /dev/null +++ b/grd-rdp-gfx-framerate-log.h @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2024 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include + +#include "grd-types.h" + +#define GRD_TYPE_RDP_GFX_FRAMERATE_LOG (grd_rdp_gfx_framerate_log_get_type ()) +G_DECLARE_FINAL_TYPE (GrdRdpGfxFramerateLog, grd_rdp_gfx_framerate_log, + GRD, RDP_GFX_FRAMERATE_LOG, GObject) + +GrdRdpGfxFramerateLog *grd_rdp_gfx_framerate_log_new (void); + +void grd_rdp_gfx_framerate_log_notify_frame_stats (GrdRdpGfxFramerateLog *framerate_log, + GrdRdpFrameStats *frame_stats); + +gboolean grd_rdp_gfx_framerate_log_should_avoid_dual_frame (GrdRdpGfxFramerateLog *framerate_log); diff --git a/grd-rdp-gfx-surface.c b/grd-rdp-gfx-surface.c new file mode 100644 index 0000000..b84082e --- /dev/null +++ b/grd-rdp-gfx-surface.c @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2021 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-rdp-gfx-surface.h" + +#include "grd-rdp-dvc-graphics-pipeline.h" +#include "grd-rdp-surface.h" + +struct _GrdRdpGfxSurface +{ + GObject parent; + + GrdRdpDvcGraphicsPipeline *graphics_pipeline; + GrdRdpGfxSurfaceFlag flags; + GrdRdpSurface *rdp_surface; + gboolean created; + + uint16_t surface_id; + uint32_t codec_context_id; + uint32_t serial; + + uint16_t width; + uint16_t height; + + GrdRdpGfxSurface *render_surface; + + GrdRdpGfxFrameController *frame_controller; +}; + +G_DEFINE_TYPE (GrdRdpGfxSurface, grd_rdp_gfx_surface, G_TYPE_OBJECT) + +uint16_t +grd_rdp_gfx_surface_get_surface_id (GrdRdpGfxSurface *gfx_surface) +{ + return gfx_surface->surface_id; +} + +uint32_t +grd_rdp_gfx_surface_get_codec_context_id (GrdRdpGfxSurface *gfx_surface) +{ + return gfx_surface->codec_context_id; +} + +uint32_t +grd_rdp_gfx_surface_get_serial (GrdRdpGfxSurface *gfx_surface) +{ + return gfx_surface->serial; +} + +GrdRdpSurface * +grd_rdp_gfx_surface_get_rdp_surface (GrdRdpGfxSurface *gfx_surface) +{ + return gfx_surface->rdp_surface; +} + +uint16_t +grd_rdp_gfx_surface_get_width (GrdRdpGfxSurface *gfx_surface) +{ + return gfx_surface->width; +} + +uint16_t +grd_rdp_gfx_surface_get_height (GrdRdpGfxSurface *gfx_surface) +{ + return gfx_surface->height; +} + +gboolean +grd_rdp_gfx_surface_disallows_hwaccel_sessions (GrdRdpGfxSurface *gfx_surface) +{ + return !!(gfx_surface->flags & GRD_RDP_GFX_SURFACE_FLAG_NO_HWACCEL_SESSIONS); +} + +GrdRdpGfxSurface * +grd_rdp_gfx_surface_get_render_surface (GrdRdpGfxSurface *gfx_surface) +{ + if (gfx_surface->render_surface) + return gfx_surface->render_surface; + + return gfx_surface; +} + +void +grd_rdp_gfx_surface_override_render_surface (GrdRdpGfxSurface *gfx_surface, + GrdRdpGfxSurface *render_surface) +{ + g_assert (!gfx_surface->render_surface); + + gfx_surface->render_surface = render_surface; +} + +GrdRdpGfxFrameController * +grd_rdp_gfx_surface_get_frame_controller (GrdRdpGfxSurface *gfx_surface) +{ + return gfx_surface->frame_controller; +} + +void +grd_rdp_gfx_surface_attach_frame_controller (GrdRdpGfxSurface *gfx_surface, + GrdRdpGfxFrameController *frame_controller) +{ + g_assert (!gfx_surface->frame_controller); + + gfx_surface->frame_controller = frame_controller; +} + +GrdRdpGfxSurface * +grd_rdp_gfx_surface_new (GrdRdpDvcGraphicsPipeline *graphics_pipeline, + const GrdRdpGfxSurfaceDescriptor *surface_descriptor) +{ + GrdRdpGfxSurface *gfx_surface; + + gfx_surface = g_object_new (GRD_TYPE_RDP_GFX_SURFACE, NULL); + gfx_surface->graphics_pipeline = graphics_pipeline; + gfx_surface->flags = surface_descriptor->flags; + gfx_surface->rdp_surface = surface_descriptor->rdp_surface; + gfx_surface->surface_id = surface_descriptor->surface_id; + gfx_surface->serial = surface_descriptor->serial; + /* + * Use the same id for the codec context as for the surface + * (only relevant for RDPGFX_WIRE_TO_SURFACE_PDU_2 PDUs) + */ + gfx_surface->codec_context_id = surface_descriptor->surface_id; + + if (surface_descriptor->flags & GRD_RDP_GFX_SURFACE_FLAG_ALIGNED_SIZE) + { + gfx_surface->width = surface_descriptor->aligned_width; + gfx_surface->height = surface_descriptor->aligned_height; + } + else + { + gfx_surface->width = grd_rdp_surface_get_width (gfx_surface->rdp_surface); + gfx_surface->height = grd_rdp_surface_get_height (gfx_surface->rdp_surface); + } + + grd_rdp_dvc_graphics_pipeline_create_surface (graphics_pipeline, gfx_surface); + gfx_surface->created = TRUE; + + return gfx_surface; +} + +static void +grd_rdp_gfx_surface_dispose (GObject *object) +{ + GrdRdpGfxSurface *gfx_surface = GRD_RDP_GFX_SURFACE (object); + GrdRdpDvcGraphicsPipeline *graphics_pipeline = gfx_surface->graphics_pipeline; + + if (gfx_surface->created) + { + grd_rdp_dvc_graphics_pipeline_delete_surface (graphics_pipeline, + gfx_surface); + gfx_surface->created = FALSE; + } + + g_clear_object (&gfx_surface->frame_controller); + g_clear_object (&gfx_surface->render_surface); + + G_OBJECT_CLASS (grd_rdp_gfx_surface_parent_class)->dispose (object); +} + +static void +grd_rdp_gfx_surface_init (GrdRdpGfxSurface *gfx_surface) +{ +} + +static void +grd_rdp_gfx_surface_class_init (GrdRdpGfxSurfaceClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = grd_rdp_gfx_surface_dispose; +} diff --git a/grd-rdp-gfx-surface.h b/grd-rdp-gfx-surface.h new file mode 100644 index 0000000..47df538 --- /dev/null +++ b/grd-rdp-gfx-surface.h @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2021 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include +#include + +#include "grd-types.h" + +#define GRD_TYPE_RDP_GFX_SURFACE (grd_rdp_gfx_surface_get_type ()) +G_DECLARE_FINAL_TYPE (GrdRdpGfxSurface, grd_rdp_gfx_surface, + GRD, RDP_GFX_SURFACE, GObject) + +typedef enum _GrdRdpGfxSurfaceFlag +{ + GRD_RDP_GFX_SURFACE_FLAG_NONE = 0, + GRD_RDP_GFX_SURFACE_FLAG_ALIGNED_SIZE = 1 << 0, + GRD_RDP_GFX_SURFACE_FLAG_NO_HWACCEL_SESSIONS = 1 << 1, +} GrdRdpGfxSurfaceFlag; + +typedef struct _GrdRdpGfxSurfaceDescriptor +{ + /* Mandatory */ + GrdRdpGfxSurfaceFlag flags; + uint16_t surface_id; + uint32_t serial; + + GrdRdpSurface *rdp_surface; + + /* GRD_RDP_GFX_SURFACE_FLAG_ALIGNED_SIZE */ + uint16_t aligned_width; + uint16_t aligned_height; +} GrdRdpGfxSurfaceDescriptor; + +GrdRdpGfxSurface *grd_rdp_gfx_surface_new (GrdRdpDvcGraphicsPipeline *graphics_pipeline, + const GrdRdpGfxSurfaceDescriptor *surface_descriptor); + +uint16_t grd_rdp_gfx_surface_get_surface_id (GrdRdpGfxSurface *gfx_surface); + +uint32_t grd_rdp_gfx_surface_get_codec_context_id (GrdRdpGfxSurface *gfx_surface); + +uint32_t grd_rdp_gfx_surface_get_serial (GrdRdpGfxSurface *gfx_surface); + +GrdRdpSurface *grd_rdp_gfx_surface_get_rdp_surface (GrdRdpGfxSurface *gfx_surface); + +uint16_t grd_rdp_gfx_surface_get_width (GrdRdpGfxSurface *gfx_surface); + +uint16_t grd_rdp_gfx_surface_get_height (GrdRdpGfxSurface *gfx_surface); + +gboolean grd_rdp_gfx_surface_disallows_hwaccel_sessions (GrdRdpGfxSurface *gfx_surface); + +GrdRdpGfxSurface *grd_rdp_gfx_surface_get_render_surface (GrdRdpGfxSurface *gfx_surface); + +void grd_rdp_gfx_surface_override_render_surface (GrdRdpGfxSurface *gfx_surface, + GrdRdpGfxSurface *render_surface); + +GrdRdpGfxFrameController *grd_rdp_gfx_surface_get_frame_controller (GrdRdpGfxSurface *gfx_surface); + +void grd_rdp_gfx_surface_attach_frame_controller (GrdRdpGfxSurface *gfx_surface, + GrdRdpGfxFrameController *frame_controller); diff --git a/grd-rdp-layout-manager.c b/grd-rdp-layout-manager.c new file mode 100644 index 0000000..324c50f --- /dev/null +++ b/grd-rdp-layout-manager.c @@ -0,0 +1,1043 @@ +/* + * Copyright (C) 2023 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-rdp-layout-manager.h" + +#include "grd-context.h" +#include "grd-rdp-cursor-renderer.h" +#include "grd-rdp-pipewire-stream.h" +#include "grd-rdp-renderer.h" +#include "grd-rdp-server.h" +#include "grd-rdp-session-metrics.h" +#include "grd-rdp-surface.h" +#include "grd-session-rdp.h" +#include "grd-settings-user.h" +#include "grd-stream.h" + +#define LAYOUT_RECREATION_TIMEOUT_MS 50 +#define TARGET_SURFACE_REFRESH_RATE 60 + +typedef enum +{ + UPDATE_STATE_FATAL_ERROR, + UPDATE_STATE_AWAIT_CONFIG, + UPDATE_STATE_AWAIT_INHIBITION_DONE, + UPDATE_STATE_PREPARE_SURFACES, + UPDATE_STATE_AWAIT_STREAMS, + UPDATE_STATE_AWAIT_VIDEO_SIZES, + UPDATE_STATE_START_RENDERING, +} UpdateState; + +typedef struct +{ + GrdRdpLayoutManager *layout_manager; + + GrdRdpSurface *rdp_surface; + uint32_t output_origin_x; + uint32_t output_origin_y; + + GrdRdpVirtualMonitor *virtual_monitor; + const char *connector; + + uint32_t stream_id; + GrdStream *stream; + GrdRdpPipeWireStream *pipewire_stream; +} SurfaceContext; + +struct _GrdRdpLayoutManager +{ + GrdRdpStreamOwner parent; + + gboolean session_started; + + GMutex state_mutex; + UpdateState state; + + GrdSessionRdp *session_rdp; + + GSource *layout_update_source; + GSource *preparation_source; + GHashTable *surface_table; + + GHashTable *pending_streams; + GHashTable *pending_video_sizes; + + GMutex monitor_config_mutex; + GrdRdpMonitorConfig *current_monitor_config; + GrdRdpMonitorConfig *pending_monitor_config; + + unsigned long inhibition_done_id; + + unsigned int layout_recreation_id; +}; + +G_DEFINE_TYPE (GrdRdpLayoutManager, grd_rdp_layout_manager, + GRD_TYPE_RDP_STREAM_OWNER) + +static SurfaceContext * +surface_context_new (GrdRdpLayoutManager *layout_manager, + GError **error) +{ + g_autofree SurfaceContext *surface_context = NULL; + GrdRdpRenderer *renderer = + grd_session_rdp_get_renderer (layout_manager->session_rdp); + + surface_context = g_new0 (SurfaceContext, 1); + surface_context->layout_manager = layout_manager; + + surface_context->rdp_surface = + grd_rdp_renderer_try_acquire_surface (renderer, + TARGET_SURFACE_REFRESH_RATE); + if (!surface_context->rdp_surface) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create RDP surface"); + return NULL; + } + + return g_steal_pointer (&surface_context); +} + +static void +surface_context_free (SurfaceContext *surface_context) +{ + GrdRdpLayoutManager *layout_manager = surface_context->layout_manager; + GrdRdpRenderer *renderer = + grd_session_rdp_get_renderer (layout_manager->session_rdp); + + g_clear_object (&surface_context->pipewire_stream); + if (surface_context->stream) + { + grd_stream_disconnect_proxy_signals (surface_context->stream); + g_clear_pointer (&surface_context->stream, grd_stream_destroy); + } + + grd_session_rdp_release_stream_id (layout_manager->session_rdp, + surface_context->stream_id); + + grd_rdp_renderer_release_surface (renderer, surface_context->rdp_surface); + + g_clear_pointer (&surface_context->virtual_monitor, g_free); + g_free (surface_context); +} + +static const char * +update_state_to_string (UpdateState state) +{ + switch (state) + { + case UPDATE_STATE_FATAL_ERROR: + return "FATAL_ERROR"; + case UPDATE_STATE_AWAIT_CONFIG: + return "AWAIT_CONFIG"; + case UPDATE_STATE_AWAIT_INHIBITION_DONE: + return "AWAIT_INHIBITION_DONE"; + case UPDATE_STATE_PREPARE_SURFACES: + return "PREPARE_SURFACES"; + case UPDATE_STATE_AWAIT_STREAMS: + return "AWAIT_STREAMS"; + case UPDATE_STATE_AWAIT_VIDEO_SIZES: + return "AWAIT_VIDEO_SIZES"; + case UPDATE_STATE_START_RENDERING: + return "START_RENDERING"; + } + + g_assert_not_reached (); +} + +static void +disconnect_proxy_signals (GrdRdpLayoutManager *layout_manager) +{ + SurfaceContext *surface_context = NULL; + GHashTableIter iter; + + g_hash_table_iter_init (&iter, layout_manager->surface_table); + while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &surface_context)) + { + if (!surface_context->stream) + continue; + + grd_stream_disconnect_proxy_signals (surface_context->stream); + } +} + +static void +transition_to_state (GrdRdpLayoutManager *layout_manager, + UpdateState state) +{ + g_mutex_lock (&layout_manager->state_mutex); + g_debug ("[RDP] Layout manager: Updating state from %s to %s", + update_state_to_string (layout_manager->state), + update_state_to_string (state)); + + g_assert (layout_manager->state != state); + layout_manager->state = state; + g_mutex_unlock (&layout_manager->state_mutex); + + switch (state) + { + case UPDATE_STATE_FATAL_ERROR: + disconnect_proxy_signals (layout_manager); + grd_session_rdp_notify_error (layout_manager->session_rdp, + GRD_SESSION_RDP_ERROR_CLOSE_STACK_ON_DRIVER_FAILURE); + break; + case UPDATE_STATE_AWAIT_CONFIG: + g_debug ("[RDP] Layout manager: Finished applying new monitor config"); + g_source_set_ready_time (layout_manager->layout_update_source, 0); + break; + case UPDATE_STATE_AWAIT_INHIBITION_DONE: + break; + case UPDATE_STATE_PREPARE_SURFACES: + g_source_set_ready_time (layout_manager->layout_update_source, 0); + break; + case UPDATE_STATE_AWAIT_STREAMS: + case UPDATE_STATE_AWAIT_VIDEO_SIZES: + break; + case UPDATE_STATE_START_RENDERING: + g_source_set_ready_time (layout_manager->layout_update_source, 0); + break; + } +} + +static void +on_pipewire_stream_error (GrdRdpPipeWireStream *pipewire_stream, + GrdRdpLayoutManager *layout_manager) +{ + if (layout_manager->state == UPDATE_STATE_FATAL_ERROR) + return; + + g_warning ("[RDP] PipeWire stream closed due to error. Terminating session"); + transition_to_state (layout_manager, UPDATE_STATE_FATAL_ERROR); +} + +static gboolean +is_virtual_stream (GrdRdpLayoutManager *layout_manager, + GrdRdpPipeWireStream *pipewire_stream) +{ + SurfaceContext *surface_context = NULL; + GHashTableIter iter; + + g_hash_table_iter_init (&iter, layout_manager->surface_table); + while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &surface_context)) + { + if (surface_context->pipewire_stream == pipewire_stream) + return surface_context->virtual_monitor != NULL; + } + + g_assert_not_reached (); +} + +static gboolean +maybe_invoke_layout_recreation (gpointer user_data) +{ + GrdRdpLayoutManager *layout_manager = user_data; + + g_mutex_lock (&layout_manager->monitor_config_mutex); + if (!layout_manager->pending_monitor_config) + { + g_debug ("[RDP] Layout manager: Trying to restore last config."); + + layout_manager->pending_monitor_config = + g_steal_pointer (&layout_manager->current_monitor_config); + + g_source_set_ready_time (layout_manager->layout_update_source, 0); + } + g_mutex_unlock (&layout_manager->monitor_config_mutex); + + layout_manager->layout_recreation_id = 0; + + return G_SOURCE_REMOVE; +} + +static void +on_pipewire_stream_closed (GrdRdpPipeWireStream *pipewire_stream, + GrdRdpLayoutManager *layout_manager) +{ + if (layout_manager->state == UPDATE_STATE_FATAL_ERROR || + is_virtual_stream (layout_manager, pipewire_stream) || + layout_manager->layout_recreation_id) + return; + + g_debug ("[RDP] Layout manager: Monitor stream closed. " + "Setting timeout for recreation"); + + layout_manager->layout_recreation_id = + g_timeout_add (LAYOUT_RECREATION_TIMEOUT_MS, maybe_invoke_layout_recreation, + layout_manager); +} + +static SurfaceContext * +get_surface_context_from_pipewire_stream (GrdRdpLayoutManager *layout_manager, + GrdRdpPipeWireStream *pipewire_stream) +{ + SurfaceContext *surface_context = NULL; + GHashTableIter iter; + + g_hash_table_iter_init (&iter, layout_manager->surface_table); + while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &surface_context)) + { + if (surface_context->pipewire_stream == pipewire_stream) + return surface_context; + } + + g_assert_not_reached (); +} + +static void +write_virtual_monitor_data (GrdRdpVirtualMonitor *virtual_monitor, + rdpMonitor *monitor) +{ + monitor->x = virtual_monitor->pos_x; + monitor->y = virtual_monitor->pos_y; + monitor->width = virtual_monitor->width; + monitor->height = virtual_monitor->height; + + monitor->is_primary = virtual_monitor->is_primary; +} + +static void +write_connector_data (GrdRdpSurface *rdp_surface, + rdpMonitor *monitor) +{ + int32_t surface_width = grd_rdp_surface_get_width (rdp_surface); + int32_t surface_height = grd_rdp_surface_get_height (rdp_surface); + + monitor->x = 0; + monitor->y = 0; + monitor->width = surface_width; + monitor->height = surface_height; + + monitor->is_primary = TRUE; +} + +static void +update_monitor_data (GrdRdpLayoutManager *layout_manager) +{ + rdpContext *rdp_context = + grd_session_rdp_get_rdp_context (layout_manager->session_rdp); + rdpSettings *rdp_settings = rdp_context->settings; + SurfaceContext *surface_context = NULL; + GHashTableIter iter; + uint32_t i = 0; + + g_hash_table_iter_init (&iter, layout_manager->surface_table); + while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &surface_context)) + { + rdpMonitor monitor = {}; + + if (surface_context->connector) + g_assert (i == 0); + + if (surface_context->virtual_monitor) + write_virtual_monitor_data (surface_context->virtual_monitor, &monitor); + if (surface_context->connector) + write_connector_data (surface_context->rdp_surface, &monitor); + + if (!freerdp_settings_set_pointer_array (rdp_settings, + FreeRDP_MonitorDefArray, i++, + &monitor)) + g_assert_not_reached (); + } + + freerdp_settings_set_uint32 (rdp_settings, FreeRDP_MonitorCount, i); +} + +static void +on_pipewire_stream_video_resized (GrdRdpPipeWireStream *pipewire_stream, + uint32_t width, + uint32_t height, + GrdRdpLayoutManager *layout_manager) +{ + SurfaceContext *surface_context; + GrdRdpVirtualMonitor *virtual_monitor; + uint32_t stream_id; + + if (layout_manager->state == UPDATE_STATE_FATAL_ERROR) + return; + + surface_context = get_surface_context_from_pipewire_stream (layout_manager, + pipewire_stream); + + virtual_monitor = surface_context->virtual_monitor; + if (virtual_monitor && + (virtual_monitor->width != width || virtual_monitor->height != height)) + { + g_warning ("[RDP] Layout manager: Unexpected video size change of PipeWire " + "stream (Expected: %ux%u, Got: %ux%u). Ignoring resize event...", + virtual_monitor->width, virtual_monitor->height, width, height); + return; + } + + if (surface_context->connector) + { + GrdRdpRenderer *renderer = + grd_session_rdp_get_renderer (layout_manager->session_rdp); + + g_assert (!layout_manager->current_monitor_config->is_virtual); + g_assert (layout_manager->current_monitor_config->monitor_count == 1); + g_assert (g_hash_table_size (layout_manager->surface_table) == 1); + + update_monitor_data (layout_manager); + grd_rdp_renderer_notify_new_desktop_layout (renderer, width, height); + } + + stream_id = grd_stream_get_stream_id (surface_context->stream); + if (!g_hash_table_remove (layout_manager->pending_video_sizes, + GUINT_TO_POINTER (stream_id))) + return; + + g_assert (layout_manager->state == UPDATE_STATE_AWAIT_STREAMS || + layout_manager->state == UPDATE_STATE_AWAIT_VIDEO_SIZES); + g_assert (g_hash_table_size (layout_manager->pending_streams) <= + g_hash_table_size (layout_manager->pending_video_sizes)); + + if (g_hash_table_size (layout_manager->pending_video_sizes) == 0) + transition_to_state (layout_manager, UPDATE_STATE_START_RENDERING); +} + +static void +on_stream_ready (GrdStream *stream, + GrdRdpLayoutManager *layout_manager) +{ + uint32_t stream_id = grd_stream_get_stream_id (stream); + uint32_t pipewire_node_id = grd_stream_get_pipewire_node_id (stream); + SurfaceContext *surface_context = NULL; + g_autoptr (GError) error = NULL; + + g_assert (layout_manager->state == UPDATE_STATE_FATAL_ERROR || + layout_manager->state == UPDATE_STATE_AWAIT_STREAMS); + + if (layout_manager->state == UPDATE_STATE_FATAL_ERROR) + return; + + if (!g_hash_table_lookup_extended (layout_manager->surface_table, + GUINT_TO_POINTER (stream_id), + NULL, (gpointer *) &surface_context)) + g_assert_not_reached (); + + g_assert (stream_id == surface_context->stream_id); + g_assert (!surface_context->pipewire_stream); + + g_debug ("[RDP] Layout manager: Creating PipeWire stream for node id %u", + pipewire_node_id); + surface_context->pipewire_stream = + grd_rdp_pipewire_stream_new (layout_manager->session_rdp, + surface_context->rdp_surface, + surface_context->virtual_monitor, + pipewire_node_id, + &error); + if (!surface_context->pipewire_stream) + { + g_warning ("[RDP] Failed to establish PipeWire stream: %s", + error->message); + transition_to_state (layout_manager, UPDATE_STATE_FATAL_ERROR); + return; + } + + g_signal_connect (surface_context->pipewire_stream, "error", + G_CALLBACK (on_pipewire_stream_error), + layout_manager); + g_signal_connect (surface_context->pipewire_stream, "closed", + G_CALLBACK (on_pipewire_stream_closed), + layout_manager); + g_signal_connect (surface_context->pipewire_stream, "video-resized", + G_CALLBACK (on_pipewire_stream_video_resized), + layout_manager); + + g_hash_table_remove (layout_manager->pending_streams, + GUINT_TO_POINTER (stream_id)); + + g_assert (g_hash_table_size (layout_manager->pending_streams) < + g_hash_table_size (layout_manager->pending_video_sizes)); + g_assert (g_hash_table_size (layout_manager->pending_video_sizes) > 0); + + if (g_hash_table_size (layout_manager->pending_streams) == 0) + transition_to_state (layout_manager, UPDATE_STATE_AWAIT_VIDEO_SIZES); +} + +static void +grd_rdp_layout_manager_on_stream_created (GrdRdpStreamOwner *stream_owner, + uint32_t stream_id, + GrdStream *stream) +{ + GrdRdpLayoutManager *layout_manager = GRD_RDP_LAYOUT_MANAGER (stream_owner); + SurfaceContext *surface_context = NULL; + + g_assert (layout_manager->state == UPDATE_STATE_FATAL_ERROR || + layout_manager->state == UPDATE_STATE_AWAIT_STREAMS); + + if (!g_hash_table_lookup_extended (layout_manager->surface_table, + GUINT_TO_POINTER (stream_id), + NULL, (gpointer *) &surface_context)) + g_assert_not_reached (); + + g_assert (!surface_context->stream); + surface_context->stream = stream; + + if (layout_manager->state == UPDATE_STATE_FATAL_ERROR) + return; + + g_signal_connect (stream, "ready", G_CALLBACK (on_stream_ready), + layout_manager); +} + +static gboolean +is_extending_physical_desktop (GrdRdpLayoutManager *layout_manager) +{ + GrdSession *session = GRD_SESSION (layout_manager->session_rdp); + GrdContext *context = grd_session_get_context (session); + GrdSettings *settings = grd_context_get_settings (context); + GrdRdpScreenShareMode screen_share_mode = + grd_session_rdp_get_screen_share_mode (layout_manager->session_rdp); + + return GRD_IS_SETTINGS_USER (settings) && + screen_share_mode == GRD_RDP_SCREEN_SHARE_MODE_EXTEND; +} + +static void +hide_default_cursor (GrdRdpLayoutManager *layout_manager) +{ + GrdRdpCursorRenderer *cursor_renderer = + grd_session_rdp_get_cursor_renderer (layout_manager->session_rdp); + GrdRdpCursorUpdate *cursor_update; + + cursor_update = g_new0 (GrdRdpCursorUpdate, 1); + cursor_update->update_type = GRD_RDP_CURSOR_UPDATE_TYPE_HIDDEN; + + grd_rdp_cursor_renderer_submit_cursor_update (cursor_renderer, + cursor_update); +} + +void +grd_rdp_layout_manager_notify_session_started (GrdRdpLayoutManager *layout_manager) +{ + layout_manager->session_started = TRUE; + + if (is_extending_physical_desktop (layout_manager)) + hide_default_cursor (layout_manager); + + g_source_set_ready_time (layout_manager->layout_update_source, 0); +} + +void +grd_rdp_layout_manager_submit_new_monitor_config (GrdRdpLayoutManager *layout_manager, + GrdRdpMonitorConfig *monitor_config) +{ + g_assert (monitor_config); + if (!monitor_config->is_virtual) + g_assert (monitor_config->monitor_count == 1); + + g_mutex_lock (&layout_manager->monitor_config_mutex); + g_clear_pointer (&layout_manager->pending_monitor_config, grd_rdp_monitor_config_free); + + layout_manager->pending_monitor_config = monitor_config; + g_mutex_unlock (&layout_manager->monitor_config_mutex); + + g_source_set_ready_time (layout_manager->layout_update_source, 0); +} + +gboolean +grd_rdp_layout_manager_transform_position (GrdRdpLayoutManager *layout_manager, + uint32_t x, + uint32_t y, + GrdStream **stream, + GrdEventMotionAbs *motion_abs) +{ + g_autoptr (GMutexLocker) locker = NULL; + SurfaceContext *surface_context = NULL; + GHashTableIter iter; + + locker = g_mutex_locker_new (&layout_manager->state_mutex); + if (layout_manager->state != UPDATE_STATE_AWAIT_CONFIG) + return FALSE; + + g_hash_table_iter_init (&iter, layout_manager->surface_table); + while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &surface_context)) + { + GrdRdpSurface *rdp_surface = surface_context->rdp_surface; + uint32_t surface_width = grd_rdp_surface_get_width (rdp_surface); + uint32_t surface_height = grd_rdp_surface_get_height (rdp_surface); + + if (!surface_context->stream) + continue; + + if (x < surface_context->output_origin_x || + y < surface_context->output_origin_y || + x >= surface_context->output_origin_x + surface_width || + y >= surface_context->output_origin_y + surface_height) + continue; + + *stream = g_object_ref (surface_context->stream); + motion_abs->input_rect_width = surface_width; + motion_abs->input_rect_height = surface_height; + motion_abs->x = x - surface_context->output_origin_x; + motion_abs->y = y - surface_context->output_origin_y; + return TRUE; + } + + return FALSE; +} + +static void +on_inhibition_done (GrdRdpRenderer *renderer, + GrdRdpLayoutManager *layout_manager) +{ + g_source_set_ready_time (layout_manager->preparation_source, 0); +} + +GrdRdpLayoutManager * +grd_rdp_layout_manager_new (GrdSessionRdp *session_rdp) +{ + GrdRdpRenderer *renderer = grd_session_rdp_get_renderer (session_rdp); + GrdRdpLayoutManager *layout_manager; + + layout_manager = g_object_new (GRD_TYPE_RDP_LAYOUT_MANAGER, NULL); + layout_manager->session_rdp = session_rdp; + + layout_manager->inhibition_done_id = + g_signal_connect (renderer, "inhibition-done", + G_CALLBACK (on_inhibition_done), + layout_manager); + + return layout_manager; +} + +static void +grd_rdp_layout_manager_dispose (GObject *object) +{ + GrdRdpLayoutManager *layout_manager = GRD_RDP_LAYOUT_MANAGER (object); + GrdRdpRenderer *renderer = + grd_session_rdp_get_renderer (layout_manager->session_rdp); + + if (layout_manager->surface_table) + g_hash_table_remove_all (layout_manager->surface_table); + + g_clear_signal_handler (&layout_manager->inhibition_done_id, renderer); + + g_clear_handle_id (&layout_manager->layout_recreation_id, g_source_remove); + + if (layout_manager->preparation_source) + { + g_source_destroy (layout_manager->preparation_source); + g_clear_pointer (&layout_manager->preparation_source, g_source_unref); + } + if (layout_manager->layout_update_source) + { + g_source_destroy (layout_manager->layout_update_source); + g_clear_pointer (&layout_manager->layout_update_source, g_source_unref); + } + + g_clear_pointer (&layout_manager->current_monitor_config, + grd_rdp_monitor_config_free); + g_clear_pointer (&layout_manager->pending_monitor_config, + grd_rdp_monitor_config_free); + + g_clear_pointer (&layout_manager->pending_video_sizes, g_hash_table_unref); + g_clear_pointer (&layout_manager->pending_streams, g_hash_table_unref); + g_clear_pointer (&layout_manager->surface_table, g_hash_table_unref); + + G_OBJECT_CLASS (grd_rdp_layout_manager_parent_class)->dispose (object); +} + +static void +grd_rdp_layout_manager_finalize (GObject *object) +{ + GrdRdpLayoutManager *layout_manager = GRD_RDP_LAYOUT_MANAGER (object); + + g_mutex_clear (&layout_manager->monitor_config_mutex); + g_mutex_clear (&layout_manager->state_mutex); + + G_OBJECT_CLASS (grd_rdp_layout_manager_parent_class)->finalize (object); +} + +static gboolean +maybe_pick_up_queued_monitor_config (GrdRdpLayoutManager *layout_manager) +{ + g_autoptr (GMutexLocker) locker = NULL; + + locker = g_mutex_locker_new (&layout_manager->monitor_config_mutex); + if (!layout_manager->pending_monitor_config) + return FALSE; + + if (layout_manager->current_monitor_config) + { + g_assert (layout_manager->current_monitor_config->is_virtual == + layout_manager->pending_monitor_config->is_virtual); + } + + g_clear_pointer (&layout_manager->current_monitor_config, + grd_rdp_monitor_config_free); + layout_manager->current_monitor_config = + g_steal_pointer (&layout_manager->pending_monitor_config); + + g_clear_handle_id (&layout_manager->layout_recreation_id, g_source_remove); + + return TRUE; +} + +static void +dispose_physical_monitor_streams (GrdRdpLayoutManager *layout_manager) +{ + GrdRdpMonitorConfig *monitor_config = layout_manager->current_monitor_config; + + /* + * Monitor configs containing physical monitors cannot be mixed in sessions + * with monitor configs containing virtual monitors and vice versa. + */ + if (monitor_config->is_virtual) + return; + + g_hash_table_remove_all (layout_manager->surface_table); +} + +static void +dispose_unneeded_surfaces (GrdRdpLayoutManager *layout_manager) +{ + GrdRdpMonitorConfig *monitor_config = layout_manager->current_monitor_config; + uint32_t n_target_surfaces = monitor_config->monitor_count; + uint32_t n_surfaces_to_dispose; + SurfaceContext *surface_context = NULL; + GHashTableIter iter; + + dispose_physical_monitor_streams (layout_manager); + + if (g_hash_table_size (layout_manager->surface_table) <= n_target_surfaces) + return; + + n_surfaces_to_dispose = g_hash_table_size (layout_manager->surface_table) - + n_target_surfaces; + g_debug ("[RDP] Layout manager: Disposing %u virtual monitor(s)", + n_surfaces_to_dispose); + + g_hash_table_iter_init (&iter, layout_manager->surface_table); + while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &surface_context) && + n_surfaces_to_dispose > 0) + { + g_hash_table_iter_remove (&iter); + --n_surfaces_to_dispose; + } + g_assert (g_hash_table_size (layout_manager->surface_table) <= n_target_surfaces); + g_assert (n_surfaces_to_dispose == 0); +} + +static gboolean +prepare_surface_contexts (GrdRdpLayoutManager *layout_manager, + GError **error) +{ + GrdRdpMonitorConfig *monitor_config = layout_manager->current_monitor_config; + uint32_t n_target_surfaces = monitor_config->monitor_count; + SurfaceContext *surface_context = NULL; + GHashTableIter iter; + uint32_t i = 0; + + while (g_hash_table_size (layout_manager->surface_table) < n_target_surfaces) + { + GrdRdpStreamOwner *stream_owner = GRD_RDP_STREAM_OWNER (layout_manager); + uint32_t stream_id; + + surface_context = surface_context_new (layout_manager, error); + if (!surface_context) + return FALSE; + + stream_id = grd_session_rdp_acquire_stream_id (layout_manager->session_rdp, + stream_owner); + surface_context->stream_id = stream_id; + + g_hash_table_insert (layout_manager->surface_table, + GUINT_TO_POINTER (stream_id), surface_context); + } + + g_hash_table_iter_init (&iter, layout_manager->surface_table); + while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &surface_context)) + { + g_autofree GrdRdpSurfaceMapping *surface_mapping = NULL; + + grd_rdp_surface_set_size (surface_context->rdp_surface, 0, 0); + g_clear_pointer (&surface_context->virtual_monitor, g_free); + + if (monitor_config->is_virtual) + { + GrdRdpVirtualMonitor *virtual_monitor; + int32_t surface_pos_x; + int32_t surface_pos_y; + + virtual_monitor = &monitor_config->virtual_monitors[i++]; + + g_debug ("[RDP] Layout manager: Preparing virtual monitor with " + "geometry [x, y, w, h] = [%i, %i, %u, %u]", + virtual_monitor->pos_x, virtual_monitor->pos_y, + virtual_monitor->width, virtual_monitor->height); + + surface_pos_x = virtual_monitor->pos_x - monitor_config->layout_offset_x; + surface_pos_y = virtual_monitor->pos_y - monitor_config->layout_offset_y; + g_assert (surface_pos_x >= 0); + g_assert (surface_pos_y >= 0); + + surface_context->output_origin_x = surface_pos_x; + surface_context->output_origin_y = surface_pos_y; + + surface_context->virtual_monitor = + g_memdup2 (virtual_monitor, sizeof (GrdRdpVirtualMonitor)); + } + else + { + surface_context->connector = monitor_config->connectors[i++]; + } + + surface_mapping = g_new0 (GrdRdpSurfaceMapping, 1); + surface_mapping->mapping_type = GRD_RDP_SURFACE_MAPPING_TYPE_MAP_TO_OUTPUT; + surface_mapping->output_origin_x = surface_context->output_origin_x; + surface_mapping->output_origin_y = surface_context->output_origin_y; + + grd_rdp_surface_set_mapping (surface_context->rdp_surface, + g_steal_pointer (&surface_mapping)); + } + + return TRUE; +} + +static void +update_stream_params (SurfaceContext *surface_context, + uint32_t stream_id) +{ + GrdRdpLayoutManager *layout_manager = surface_context->layout_manager; + + g_assert (surface_context->stream); + g_assert (surface_context->pipewire_stream); + + g_hash_table_add (layout_manager->pending_video_sizes, + GUINT_TO_POINTER (stream_id)); + + grd_rdp_pipewire_stream_resize (surface_context->pipewire_stream, + surface_context->virtual_monitor); +} + +static void +create_stream (SurfaceContext *surface_context, + uint32_t stream_id) +{ + GrdRdpLayoutManager *layout_manager = surface_context->layout_manager; + GrdSession *session = GRD_SESSION (layout_manager->session_rdp); + + g_assert (surface_context->virtual_monitor || surface_context->connector); + + g_hash_table_add (layout_manager->pending_streams, + GUINT_TO_POINTER (stream_id)); + g_hash_table_add (layout_manager->pending_video_sizes, + GUINT_TO_POINTER (stream_id)); + + if (surface_context->virtual_monitor) + { + GrdScreenCastCursorMode cursor_mode; + + if (is_extending_physical_desktop (layout_manager)) + cursor_mode = GRD_SCREEN_CAST_CURSOR_MODE_EMBEDDED; + else + cursor_mode = GRD_SCREEN_CAST_CURSOR_MODE_METADATA; + + grd_session_record_virtual (session, stream_id, cursor_mode, TRUE); + } + else + { + grd_session_record_monitor (session, stream_id, + surface_context->connector, + GRD_SCREEN_CAST_CURSOR_MODE_METADATA); + } +} + +static UpdateState +create_or_update_streams (GrdRdpLayoutManager *layout_manager) +{ + GrdRdpSessionMetrics *session_metrics = + grd_session_rdp_get_session_metrics (layout_manager->session_rdp); + GrdRdpMonitorConfig *monitor_config = layout_manager->current_monitor_config; + SurfaceContext *surface_context = NULL; + GList *surfaces = NULL; + GHashTableIter iter; + gpointer key; + + g_hash_table_iter_init (&iter, layout_manager->surface_table); + while (g_hash_table_iter_next (&iter, &key, (gpointer *) &surface_context)) + { + uint32_t stream_id = GPOINTER_TO_UINT (key); + + if (surface_context->stream) + update_stream_params (surface_context, stream_id); + else + create_stream (surface_context, stream_id); + + surfaces = g_list_prepend (surfaces, surface_context->rdp_surface); + } + + grd_rdp_session_metrics_prepare_surface_metrics (session_metrics, surfaces); + g_list_free (surfaces); + + if (monitor_config->is_virtual) + { + GrdRdpRenderer *renderer = + grd_session_rdp_get_renderer (layout_manager->session_rdp); + + update_monitor_data (layout_manager); + grd_rdp_renderer_notify_new_desktop_layout (renderer, + monitor_config->desktop_width, + monitor_config->desktop_height); + } + + if (g_hash_table_size (layout_manager->pending_streams) > 0) + return UPDATE_STATE_AWAIT_STREAMS; + if (g_hash_table_size (layout_manager->pending_video_sizes) > 0) + return UPDATE_STATE_AWAIT_VIDEO_SIZES; + + return UPDATE_STATE_START_RENDERING; +} + +static gboolean +update_monitor_layout (gpointer user_data) +{ + GrdRdpLayoutManager *layout_manager = user_data; + GrdRdpRenderer *renderer = + grd_session_rdp_get_renderer (layout_manager->session_rdp); + g_autoptr (GError) error = NULL; + + if (!layout_manager->session_started) + return G_SOURCE_CONTINUE; + + switch (layout_manager->state) + { + case UPDATE_STATE_FATAL_ERROR: + break; + case UPDATE_STATE_AWAIT_CONFIG: + if (maybe_pick_up_queued_monitor_config (layout_manager)) + { + grd_rdp_renderer_inhibit_rendering (renderer); + + transition_to_state (layout_manager, UPDATE_STATE_AWAIT_INHIBITION_DONE); + return G_SOURCE_CONTINUE; + } + break; + case UPDATE_STATE_AWAIT_INHIBITION_DONE: + break; + case UPDATE_STATE_PREPARE_SURFACES: + dispose_unneeded_surfaces (layout_manager); + + if (!prepare_surface_contexts (layout_manager, &error)) + { + g_warning ("[RDP] Layout manager: Failed to prepare surface contexts: %s", + error->message); + transition_to_state (layout_manager, UPDATE_STATE_FATAL_ERROR); + return G_SOURCE_CONTINUE; + } + + transition_to_state (layout_manager, + create_or_update_streams (layout_manager)); + break; + case UPDATE_STATE_AWAIT_STREAMS: + case UPDATE_STATE_AWAIT_VIDEO_SIZES: + break; + case UPDATE_STATE_START_RENDERING: + grd_rdp_renderer_uninhibit_rendering (renderer); + + transition_to_state (layout_manager, UPDATE_STATE_AWAIT_CONFIG); + break; + } + + return G_SOURCE_CONTINUE; +} + +static gboolean +finish_layout_change_preparation (gpointer user_data) +{ + GrdRdpLayoutManager *layout_manager = user_data; + GrdRdpSessionMetrics *session_metrics = + grd_session_rdp_get_session_metrics (layout_manager->session_rdp); + + g_assert (layout_manager->state == UPDATE_STATE_FATAL_ERROR || + layout_manager->state == UPDATE_STATE_AWAIT_INHIBITION_DONE); + + if (layout_manager->state == UPDATE_STATE_FATAL_ERROR) + return G_SOURCE_CONTINUE; + + grd_rdp_session_metrics_notify_layout_change (session_metrics); + transition_to_state (layout_manager, UPDATE_STATE_PREPARE_SURFACES); + + return G_SOURCE_CONTINUE; +} + +static gboolean +source_dispatch (GSource *source, + GSourceFunc callback, + gpointer user_data) +{ + g_source_set_ready_time (source, -1); + + return callback (user_data); +} + +static GSourceFuncs source_funcs = +{ + .dispatch = source_dispatch, +}; + +static void +grd_rdp_layout_manager_init (GrdRdpLayoutManager *layout_manager) +{ + GSource *layout_update_source; + GSource *preparation_source; + + layout_manager->state = UPDATE_STATE_AWAIT_CONFIG; + + layout_manager->surface_table = + g_hash_table_new_full (NULL, NULL, + NULL, (GDestroyNotify) surface_context_free); + layout_manager->pending_streams = g_hash_table_new (NULL, NULL); + layout_manager->pending_video_sizes = g_hash_table_new (NULL, NULL); + + g_mutex_init (&layout_manager->state_mutex); + g_mutex_init (&layout_manager->monitor_config_mutex); + + layout_update_source = g_source_new (&source_funcs, sizeof (GSource)); + g_source_set_callback (layout_update_source, update_monitor_layout, + layout_manager, NULL); + g_source_set_ready_time (layout_update_source, -1); + g_source_attach (layout_update_source, NULL); + layout_manager->layout_update_source = layout_update_source; + + preparation_source = g_source_new (&source_funcs, sizeof (GSource)); + g_source_set_callback (preparation_source, finish_layout_change_preparation, + layout_manager, NULL); + g_source_set_ready_time (preparation_source, -1); + g_source_attach (preparation_source, NULL); + layout_manager->preparation_source = preparation_source; +} + +static void +grd_rdp_layout_manager_class_init (GrdRdpLayoutManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GrdRdpStreamOwnerClass *stream_owner_class = + GRD_RDP_STREAM_OWNER_CLASS (klass); + + object_class->dispose = grd_rdp_layout_manager_dispose; + object_class->finalize = grd_rdp_layout_manager_finalize; + + stream_owner_class->on_stream_created = + grd_rdp_layout_manager_on_stream_created; +} diff --git a/grd-rdp-layout-manager.h b/grd-rdp-layout-manager.h new file mode 100644 index 0000000..0010a39 --- /dev/null +++ b/grd-rdp-layout-manager.h @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2023 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include + +#include "grd-enums.h" +#include "grd-rdp-monitor-config.h" +#include "grd-rdp-stream-owner.h" +#include "grd-session.h" + +#define GRD_TYPE_RDP_LAYOUT_MANAGER (grd_rdp_layout_manager_get_type ()) +G_DECLARE_FINAL_TYPE (GrdRdpLayoutManager, grd_rdp_layout_manager, + GRD, RDP_LAYOUT_MANAGER, GrdRdpStreamOwner) + +GrdRdpLayoutManager *grd_rdp_layout_manager_new (GrdSessionRdp *session_rdp); + +void grd_rdp_layout_manager_notify_session_started (GrdRdpLayoutManager *layout_manager); + +void grd_rdp_layout_manager_submit_new_monitor_config (GrdRdpLayoutManager *layout_manager, + GrdRdpMonitorConfig *monitor_config); + +gboolean grd_rdp_layout_manager_transform_position (GrdRdpLayoutManager *layout_manager, + uint32_t x, + uint32_t y, + GrdStream **stream, + GrdEventMotionAbs *motion_abs); diff --git a/grd-rdp-legacy-buffer.c b/grd-rdp-legacy-buffer.c new file mode 100644 index 0000000..f219f84 --- /dev/null +++ b/grd-rdp-legacy-buffer.c @@ -0,0 +1,334 @@ +/* + * Copyright (C) 2022 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-rdp-legacy-buffer.h" + +#include "grd-context.h" +#include "grd-egl-thread.h" +#include "grd-hwaccel-nvidia.h" +#include "grd-rdp-buffer-pool.h" +#include "grd-rdp-renderer.h" +#include "grd-rdp-server.h" +#include "grd-rdp-surface.h" +#include "grd-session-rdp.h" +#include "grd-utils.h" + +typedef struct +{ + GrdHwAccelNvidia *hwaccel_nvidia; + CUgraphicsResource cuda_resource; + CUstream cuda_stream; + gboolean is_mapped; +} ClearBufferData; + +typedef struct +{ + GrdHwAccelNvidia *hwaccel_nvidia; + GrdRdpLegacyBuffer *buffer; +} AllocateBufferData; + +typedef struct +{ + GrdHwAccelNvidia *hwaccel_nvidia; + CUgraphicsResource cuda_resource; + CUstream cuda_stream; +} UnmapBufferData; + +struct _GrdRdpLegacyBuffer +{ + GrdRdpBufferPool *buffer_pool; + + uint32_t stride; + + uint8_t *local_data; + + uint32_t pbo; + + CUgraphicsResource cuda_resource; + CUstream cuda_stream; + CUdeviceptr mapped_cuda_pointer; +}; + +static gboolean +cuda_allocate_buffer (gpointer user_data, + uint32_t pbo) +{ + AllocateBufferData *data = user_data; + GrdRdpLegacyBuffer *buffer = data->buffer; + gboolean success; + + success = grd_hwaccel_nvidia_register_read_only_gl_buffer (data->hwaccel_nvidia, + &buffer->cuda_resource, + pbo); + if (success) + buffer->pbo = pbo; + + return success; +} + +static void +resources_ready (gboolean success, + gpointer user_data) +{ + GrdSyncPoint *sync_point = user_data; + + if (success) + g_debug ("[RDP] Allocating GL resources was successful"); + else + g_warning ("[RDP] Failed to allocate GL resources"); + + grd_sync_point_complete (sync_point, success); +} + +static GrdHwAccelNvidia * +hwaccel_nvidia_from_buffer (GrdRdpLegacyBuffer *buffer) +{ + GrdRdpSurface *rdp_surface = + grd_rdp_buffer_pool_get_surface (buffer->buffer_pool); + GrdSessionRdp *session_rdp = + grd_rdp_renderer_get_session (rdp_surface->renderer); + GrdRdpServer *rdp_server = grd_session_rdp_get_server (session_rdp); + + return grd_rdp_server_get_hwaccel_nvidia (rdp_server); +} + +static GrdEglThread * +egl_thread_from_buffer (GrdRdpLegacyBuffer *buffer) +{ + GrdRdpSurface *rdp_surface = + grd_rdp_buffer_pool_get_surface (buffer->buffer_pool); + GrdSessionRdp *session_rdp = + grd_rdp_renderer_get_session (rdp_surface->renderer); + GrdRdpServer *rdp_server = grd_session_rdp_get_server (session_rdp); + GrdContext *context = grd_rdp_server_get_context (rdp_server); + + return grd_context_get_egl_thread (context); +} + +GrdRdpLegacyBuffer * +grd_rdp_legacy_buffer_new (GrdRdpBufferPool *buffer_pool, + uint32_t height, + uint32_t stride, + gboolean preallocate_on_gpu) +{ + GrdRdpSurface *rdp_surface = grd_rdp_buffer_pool_get_surface (buffer_pool); + GrdHwAccelNvidia *hwaccel_nvidia; + g_autoptr (GrdRdpLegacyBuffer) buffer = NULL; + gboolean success = TRUE; + + buffer = g_new0 (GrdRdpLegacyBuffer, 1); + buffer->buffer_pool = buffer_pool; + + buffer->cuda_stream = rdp_surface->cuda_stream; + + buffer->stride = stride; + buffer->local_data = g_malloc0 (stride * height * sizeof (uint8_t)); + + hwaccel_nvidia = hwaccel_nvidia_from_buffer (buffer); + if (preallocate_on_gpu && hwaccel_nvidia) + { + GrdEglThread *egl_thread = egl_thread_from_buffer (buffer); + AllocateBufferData data = {}; + GrdSyncPoint sync_point = {}; + + g_assert (egl_thread); + + grd_sync_point_init (&sync_point); + data.hwaccel_nvidia = hwaccel_nvidia; + data.buffer = buffer; + + grd_egl_thread_allocate (egl_thread, + height, + stride, + cuda_allocate_buffer, + &data, + resources_ready, + &sync_point, + NULL); + + success = grd_sync_point_wait_for_completion (&sync_point); + grd_sync_point_clear (&sync_point); + } + + if (!success) + return NULL; + + return g_steal_pointer (&buffer); +} + +static void +cuda_deallocate_buffer (gpointer user_data) +{ + ClearBufferData *data = user_data; + + if (data->is_mapped) + { + grd_hwaccel_nvidia_unmap_cuda_resource (data->hwaccel_nvidia, + data->cuda_resource, + data->cuda_stream); + } + + grd_hwaccel_nvidia_unregister_cuda_resource (data->hwaccel_nvidia, + data->cuda_resource, + data->cuda_stream); +} + +void +grd_rdp_legacy_buffer_free (GrdRdpLegacyBuffer *buffer) +{ + if (buffer->cuda_resource) + { + GrdHwAccelNvidia *hwaccel_nvidia = hwaccel_nvidia_from_buffer (buffer); + GrdEglThread *egl_thread = egl_thread_from_buffer (buffer); + ClearBufferData *data; + + data = g_new0 (ClearBufferData, 1); + data->hwaccel_nvidia = hwaccel_nvidia; + data->cuda_resource = buffer->cuda_resource; + data->cuda_stream = buffer->cuda_stream; + data->is_mapped = !!buffer->mapped_cuda_pointer; + grd_egl_thread_deallocate (egl_thread, + buffer->pbo, + cuda_deallocate_buffer, + data, + NULL, data, g_free); + + buffer->mapped_cuda_pointer = 0; + buffer->cuda_resource = NULL; + buffer->pbo = 0; + } + + g_clear_pointer (&buffer->local_data, g_free); + + g_free (buffer); +} + +uint32_t +grd_rdp_legacy_buffer_get_stride (GrdRdpLegacyBuffer *buffer) +{ + return buffer->stride; +} + +uint8_t * +grd_rdp_legacy_buffer_get_local_data (GrdRdpLegacyBuffer *buffer) +{ + return buffer->local_data; +} + +uint32_t +grd_rdp_legacy_buffer_get_pbo (GrdRdpLegacyBuffer *buffer) +{ + return buffer->pbo; +} + +CUdeviceptr +grd_rdp_legacy_buffer_get_mapped_cuda_pointer (GrdRdpLegacyBuffer *buffer) +{ + return buffer->mapped_cuda_pointer; +} + +void +grd_rdp_legacy_buffer_release (GrdRdpLegacyBuffer *buffer) +{ + grd_rdp_buffer_pool_release_buffer (buffer->buffer_pool, buffer); +} + +gboolean +grd_rdp_legacy_buffer_register_read_only_gl_buffer (GrdRdpLegacyBuffer *buffer, + uint32_t pbo) +{ + GrdHwAccelNvidia *hwaccel_nvidia = hwaccel_nvidia_from_buffer (buffer); + gboolean success; + + success = + grd_hwaccel_nvidia_register_read_only_gl_buffer (hwaccel_nvidia, + &buffer->cuda_resource, + pbo); + if (success) + buffer->pbo = pbo; + + return success; +} + +gboolean +grd_rdp_legacy_buffer_map_cuda_resource (GrdRdpLegacyBuffer *buffer) +{ + GrdHwAccelNvidia *hwaccel_nvidia = hwaccel_nvidia_from_buffer (buffer); + size_t mapped_size = 0; + + return grd_hwaccel_nvidia_map_cuda_resource (hwaccel_nvidia, + buffer->cuda_resource, + &buffer->mapped_cuda_pointer, + &mapped_size, + buffer->cuda_stream); +} + +void +grd_rdp_legacy_buffer_unmap_cuda_resource (GrdRdpLegacyBuffer *buffer) +{ + GrdHwAccelNvidia *hwaccel_nvidia = hwaccel_nvidia_from_buffer (buffer); + + if (!buffer->mapped_cuda_pointer) + return; + + grd_hwaccel_nvidia_unmap_cuda_resource (hwaccel_nvidia, + buffer->cuda_resource, + buffer->cuda_stream); + buffer->mapped_cuda_pointer = 0; +} + +static gboolean +cuda_unmap_resource (gpointer user_data) +{ + UnmapBufferData *data = user_data; + + grd_hwaccel_nvidia_unmap_cuda_resource (data->hwaccel_nvidia, + data->cuda_resource, + data->cuda_stream); + + return TRUE; +} + +void +grd_rdp_legacy_buffer_queue_resource_unmap (GrdRdpLegacyBuffer *buffer) +{ + GrdEglThread *egl_thread = egl_thread_from_buffer (buffer); + GrdHwAccelNvidia *hwaccel_nvidia = hwaccel_nvidia_from_buffer (buffer); + UnmapBufferData *data; + + if (!buffer->mapped_cuda_pointer) + return; + + data = g_new0 (UnmapBufferData, 1); + data->hwaccel_nvidia = hwaccel_nvidia; + data->cuda_resource = buffer->cuda_resource; + data->cuda_stream = buffer->cuda_stream; + grd_egl_thread_run_custom_task (egl_thread, + cuda_unmap_resource, + data, + NULL, data, g_free); + + /* + * The mapped CUDA pointer indicates whether the resource is mapped, but + * it is itself not needed to unmap the resource. + */ + buffer->mapped_cuda_pointer = 0; +} diff --git a/grd-rdp-legacy-buffer.h b/grd-rdp-legacy-buffer.h new file mode 100644 index 0000000..cc194e4 --- /dev/null +++ b/grd-rdp-legacy-buffer.h @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2022 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include +#include + +#include "grd-types.h" + +GrdRdpLegacyBuffer *grd_rdp_legacy_buffer_new (GrdRdpBufferPool *buffer_pool, + uint32_t height, + uint32_t stride, + gboolean preallocate_on_gpu); + +void grd_rdp_legacy_buffer_free (GrdRdpLegacyBuffer *buffer); + +uint32_t grd_rdp_legacy_buffer_get_stride (GrdRdpLegacyBuffer *buffer); + +uint8_t *grd_rdp_legacy_buffer_get_local_data (GrdRdpLegacyBuffer *buffer); + +uint32_t grd_rdp_legacy_buffer_get_pbo (GrdRdpLegacyBuffer *buffer); + +CUdeviceptr grd_rdp_legacy_buffer_get_mapped_cuda_pointer (GrdRdpLegacyBuffer *buffer); + +void grd_rdp_legacy_buffer_release (GrdRdpLegacyBuffer *buffer); + +gboolean grd_rdp_legacy_buffer_register_read_only_gl_buffer (GrdRdpLegacyBuffer *buffer, + uint32_t pbo); + +gboolean grd_rdp_legacy_buffer_map_cuda_resource (GrdRdpLegacyBuffer *buffer); + +void grd_rdp_legacy_buffer_unmap_cuda_resource (GrdRdpLegacyBuffer *buffer); + +void grd_rdp_legacy_buffer_queue_resource_unmap (GrdRdpLegacyBuffer *buffer); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (GrdRdpLegacyBuffer, grd_rdp_legacy_buffer_free) diff --git a/grd-rdp-monitor-config.c b/grd-rdp-monitor-config.c new file mode 100644 index 0000000..b11ea70 --- /dev/null +++ b/grd-rdp-monitor-config.c @@ -0,0 +1,370 @@ +/* + * Copyright (C) 2021 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-rdp-monitor-config.h" + +#include + +static uint32_t +sanitize_value (uint32_t value, + uint32_t lower_bound, + uint32_t upper_bound) +{ + g_assert (lower_bound < upper_bound); + g_assert (lower_bound > 0); + + if (value < lower_bound || value > upper_bound) + return 0; + + return value; +} + +static GrdRdpMonitorOrientation +transform_monitor_orientation (uint32_t value) +{ + switch (value) + { + case ORIENTATION_LANDSCAPE: + return GRD_RDP_MONITOR_DIRECTION_LANDSCAPE; + case ORIENTATION_PORTRAIT: + return GRD_RDP_MONITOR_DIRECTION_PORTRAIT; + case ORIENTATION_LANDSCAPE_FLIPPED: + return GRD_RDP_MONITOR_DIRECTION_LANDSCAPE_FLIPPED; + case ORIENTATION_PORTRAIT_FLIPPED: + return GRD_RDP_MONITOR_DIRECTION_PORTRAIT_FLIPPED; + default: + return GRD_RDP_MONITOR_DIRECTION_LANDSCAPE; + } + + g_assert_not_reached (); +} + +static gboolean +write_sanitized_monitor_data (GrdRdpVirtualMonitor *monitor, + int32_t pos_x, + int32_t pos_y, + uint32_t width, + uint32_t height, + gboolean is_primary, + uint32_t physical_width, + uint32_t physical_height, + uint32_t orientation, + uint32_t scale, + GError **error) +{ + if (width < 200 || height < 200 || + width > 8192 || height > 8192) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Invalid monitor dimensions"); + return FALSE; + } + + monitor->pos_x = pos_x; + monitor->pos_y = pos_y; + monitor->width = width; + monitor->height = height; + monitor->is_primary = is_primary; + + monitor->physical_width = sanitize_value (physical_width, 10, 10000); + monitor->physical_height = sanitize_value (physical_height, 10, 10000); + if (monitor->physical_width == 0 || monitor->physical_height == 0) + monitor->physical_width = monitor->physical_height = 0; + + monitor->orientation = transform_monitor_orientation (orientation); + monitor->scale = sanitize_value (scale, 100, 500); + + return TRUE; +} + +/** + * Create a new monitor config from the Client Core Data data block + * ([MS-RDPBCGR] 2.2.1.3.2 Client Core Data (TS_UD_CS_CORE)) + */ +static GrdRdpMonitorConfig * +create_monitor_config_from_client_core_data (rdpSettings *rdp_settings, + GError **error) +{ + g_autoptr (GrdRdpMonitorConfig) monitor_config = NULL; + GrdRdpVirtualMonitor *virtual_monitor; + uint32_t desktop_width = + freerdp_settings_get_uint32 (rdp_settings, FreeRDP_DesktopWidth); + uint32_t desktop_height = + freerdp_settings_get_uint32 (rdp_settings, FreeRDP_DesktopHeight); + uint32_t desktop_physical_width = + freerdp_settings_get_uint32 (rdp_settings, FreeRDP_DesktopPhysicalWidth); + uint32_t desktop_physical_height = + freerdp_settings_get_uint32 (rdp_settings, FreeRDP_DesktopPhysicalHeight); + uint32_t desktop_orientation = + freerdp_settings_get_uint16 (rdp_settings, FreeRDP_DesktopOrientation); + uint32_t desktop_scale_factor = + freerdp_settings_get_uint32 (rdp_settings, FreeRDP_DesktopScaleFactor); + + monitor_config = g_malloc0 (sizeof (GrdRdpMonitorConfig)); + monitor_config->is_virtual = TRUE; + monitor_config->monitor_count = 1; + monitor_config->virtual_monitors = g_new0 (GrdRdpVirtualMonitor, 1); + + virtual_monitor = &monitor_config->virtual_monitors[0]; + + /* Ignore the DeviceScaleFactor. It is deprecated (Win 8.1 only) */ + if (!write_sanitized_monitor_data (virtual_monitor, + 0, 0, + desktop_width, + desktop_height, + TRUE, + desktop_physical_width, + desktop_physical_height, + desktop_orientation, + desktop_scale_factor, + error)) + return NULL; + + monitor_config->desktop_width = virtual_monitor->width; + monitor_config->desktop_height = virtual_monitor->height; + + return g_steal_pointer (&monitor_config); +} + +static gboolean +determine_primary_monitor (GrdRdpMonitorConfig *monitor_config, + GError **error) +{ + uint32_t i; + + for (i = 0; i < monitor_config->monitor_count; ++i) + { + if (monitor_config->virtual_monitors[i].pos_x == 0 && + monitor_config->virtual_monitors[i].pos_y == 0) + { + monitor_config->virtual_monitors[i].is_primary = TRUE; + return TRUE; + } + } + + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "No suitable primary monitor in monitor layout"); + + return FALSE; +} + +static gboolean +verify_monitor_config (GrdRdpMonitorConfig *monitor_config, + GError **error) +{ + cairo_region_t *region; + cairo_rectangle_int_t rect; + uint32_t i; + + /* Check for overlapping monitors */ + region = cairo_region_create (); + for (i = 0; i < monitor_config->monitor_count; ++i) + { + rect.x = monitor_config->virtual_monitors[i].pos_x; + rect.y = monitor_config->virtual_monitors[i].pos_y; + rect.width = monitor_config->virtual_monitors[i].width; + rect.height = monitor_config->virtual_monitors[i].height; + + if (cairo_region_contains_rectangle (region, &rect) != CAIRO_REGION_OVERLAP_OUT) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Monitor overlaps other monitor in layout"); + cairo_region_destroy (region); + return FALSE; + } + cairo_region_union_rectangle (region, &rect); + } + + /* Calculate the size of the desktop and Graphics Output Buffer ADM element */ + cairo_region_get_extents (region, &rect); + monitor_config->desktop_width = rect.width; + monitor_config->desktop_height = rect.height; + + /* We already checked, that the primary monitor is at (0, 0) */ + g_assert (rect.x <= 0); + g_assert (rect.y <= 0); + + /* Determine monitor offset in input- and output-region */ + monitor_config->layout_offset_x = rect.x; + monitor_config->layout_offset_y = rect.y; + cairo_region_destroy (region); + + return TRUE; +} + +/** + * Create a new monitor config from the Client Monitor Data packet + * ([MS-RDPBCGR] 2.2.1.3.6 Client Monitor Data (TS_UD_CS_MONITOR)) + */ +static GrdRdpMonitorConfig * +create_monitor_config_from_client_monitor_data (rdpSettings *rdp_settings, + GError **error) +{ + g_autoptr (GrdRdpMonitorConfig) monitor_config = NULL; + const rdpMonitor *monitors = + freerdp_settings_get_pointer_array (rdp_settings, FreeRDP_MonitorDefArray, 0); + uint32_t monitor_count = + freerdp_settings_get_uint32 (rdp_settings, FreeRDP_MonitorCount); + gboolean found_primary_monitor = FALSE; + uint32_t i; + + g_assert (monitor_count > 0); + + monitor_config = g_malloc0 (sizeof (GrdRdpMonitorConfig)); + monitor_config->is_virtual = TRUE; + monitor_config->monitor_count = monitor_count; + monitor_config->virtual_monitors = g_new0 (GrdRdpVirtualMonitor, + monitor_count); + + for (i = 0; i < monitor_count; ++i) + { + gboolean has_monitor_attributes = + freerdp_settings_get_bool (rdp_settings, FreeRDP_HasMonitorAttributes); + const rdpMonitor *monitor = &monitors[i]; + const MONITOR_ATTRIBUTES *monitor_attributes = &monitor->attributes; + gboolean is_primary = !!monitor->is_primary; + uint32_t physical_width = 0; + uint32_t physical_height = 0; + uint32_t orientation = 0; + uint32_t scale = 0; + + if (found_primary_monitor || monitor->x != 0 || monitor->y != 0) + is_primary = FALSE; + if (!found_primary_monitor && is_primary) + found_primary_monitor = TRUE; + + if (has_monitor_attributes) + { + physical_width = monitor_attributes->physicalWidth; + physical_height = monitor_attributes->physicalHeight; + orientation = monitor_attributes->orientation; + scale = monitor_attributes->desktopScaleFactor; + } + + /* Ignore the DeviceScaleFactor. It is deprecated (Win 8.1 only) */ + if (!write_sanitized_monitor_data (&monitor_config->virtual_monitors[i], + monitor->x, + monitor->y, + monitor->width, + monitor->height, + is_primary, + physical_width, + physical_height, + orientation, + scale, + error)) + return NULL; + } + + if (!found_primary_monitor && + !determine_primary_monitor (monitor_config, error)) + return NULL; + + if (!verify_monitor_config (monitor_config, error)) + return NULL; + + return g_steal_pointer (&monitor_config); +} + +GrdRdpMonitorConfig * +grd_rdp_monitor_config_new_from_client_data (rdpSettings *rdp_settings, + uint32_t max_monitor_count, + GError **error) +{ + uint32_t monitor_count = + freerdp_settings_get_uint32 (rdp_settings, FreeRDP_MonitorCount); + + if (monitor_count == 0 || + monitor_count > max_monitor_count) + return create_monitor_config_from_client_core_data (rdp_settings, error); + + return create_monitor_config_from_client_monitor_data (rdp_settings, error); +} + +/** + * Create a new monitor config from DISPLAYCONTROL_MONITOR_LAYOUT PDU + * ([MS-RDPEDISP] 2.2.2.2.1 DISPLAYCONTROL_MONITOR_LAYOUT) + */ +GrdRdpMonitorConfig * +grd_rdp_monitor_config_new_from_disp_monitor_layout (const DISPLAY_CONTROL_MONITOR_LAYOUT_PDU *monitor_layout, + GError **error) +{ + g_autoptr (GrdRdpMonitorConfig) monitor_config = NULL; + uint32_t monitor_count = monitor_layout->NumMonitors; + gboolean found_primary_monitor = FALSE; + uint32_t i; + + if (monitor_count == 0) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Monitor Layout PDU contains no monitors"); + return NULL; + } + + monitor_config = g_malloc0 (sizeof (GrdRdpMonitorConfig)); + monitor_config->is_virtual = TRUE; + monitor_config->monitor_count = monitor_count; + monitor_config->virtual_monitors = g_new0 (GrdRdpVirtualMonitor, + monitor_count); + + for (i = 0; i < monitor_count; ++i) + { + DISPLAY_CONTROL_MONITOR_LAYOUT *monitor = &monitor_layout->Monitors[i]; + gboolean is_primary; + + is_primary = !!(monitor->Flags & DISPLAY_CONTROL_MONITOR_PRIMARY); + if (found_primary_monitor || monitor->Left != 0 || monitor->Top != 0) + is_primary = FALSE; + if (!found_primary_monitor && is_primary) + found_primary_monitor = TRUE; + + /* Ignore the DeviceScaleFactor. It is deprecated (Win 8.1 only) */ + if (!write_sanitized_monitor_data (&monitor_config->virtual_monitors[i], + monitor->Left, + monitor->Top, + monitor->Width, + monitor->Height, + is_primary, + monitor->PhysicalWidth, + monitor->PhysicalHeight, + monitor->Orientation, + monitor->DesktopScaleFactor, + error)) + return NULL; + } + + if (!found_primary_monitor && + !determine_primary_monitor (monitor_config, error)) + return NULL; + + if (!verify_monitor_config (monitor_config, error)) + return NULL; + + return g_steal_pointer (&monitor_config); +} + +void +grd_rdp_monitor_config_free (GrdRdpMonitorConfig *monitor_config) +{ + g_clear_pointer (&monitor_config->connectors, g_strfreev); + g_free (monitor_config->virtual_monitors); + g_free (monitor_config); +} diff --git a/grd-rdp-monitor-config.h b/grd-rdp-monitor-config.h new file mode 100644 index 0000000..0dd3172 --- /dev/null +++ b/grd-rdp-monitor-config.h @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2021 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include +#include +#include + +typedef enum _GrdRdpMonitorOrientation +{ + /* The monitor is not rotated */ + GRD_RDP_MONITOR_DIRECTION_LANDSCAPE = 0, + /* The monitor is rotated clockwise by 90 degrees */ + GRD_RDP_MONITOR_DIRECTION_PORTRAIT = 1, + /* The monitor is rotated clockwise by 180 degrees */ + GRD_RDP_MONITOR_DIRECTION_LANDSCAPE_FLIPPED = 2, + /* The monitor is rotated clockwise by 270 degrees */ + GRD_RDP_MONITOR_DIRECTION_PORTRAIT_FLIPPED = 3, +} GrdRdpMonitorOrientation; + +typedef struct _GrdRdpVirtualMonitor +{ + int32_t pos_x; + int32_t pos_y; + uint32_t width; /* Valid values are in range of [200, 8192] */ + uint32_t height; /* Valid values are in range of [200, 8192] */ + gboolean is_primary; + + /* Physical width of the monitor in millimeters. Ignore, if 0 */ + uint32_t physical_width; /* Valid values are in range of [10, 10000] */ + /* Physical height of the monitor in millimeters. Ignore, if 0 */ + uint32_t physical_height; /* Valid values are in range of [10, 10000] */ + + GrdRdpMonitorOrientation orientation; + + /* Monitor scale in percent. Ignore, if 0 */ + uint32_t scale; /* Valid values are in range of [100, 500] */ +} GrdRdpVirtualMonitor; + +typedef struct _GrdRdpMonitorConfig +{ + gboolean is_virtual; + + /* Server configs only */ + char **connectors; + /* Client configs only */ + GrdRdpVirtualMonitor *virtual_monitors; + /* Count of items in connectors or virtual_monitors */ + uint32_t monitor_count; + + /* Offset between monitor layout, and input- and output-region */ + int32_t layout_offset_x; + int32_t layout_offset_y; + + /* Size of the Desktop (and the Graphics Output Buffer ADM element) */ + uint32_t desktop_width; + uint32_t desktop_height; +} GrdRdpMonitorConfig; + +GrdRdpMonitorConfig *grd_rdp_monitor_config_new_from_client_data (rdpSettings *rdp_settings, + uint32_t max_monitor_count, + GError **error); + +GrdRdpMonitorConfig *grd_rdp_monitor_config_new_from_disp_monitor_layout (const DISPLAY_CONTROL_MONITOR_LAYOUT_PDU *monitor_layout, + GError **error); + +void grd_rdp_monitor_config_free (GrdRdpMonitorConfig *monitor_config); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (GrdRdpMonitorConfig, grd_rdp_monitor_config_free) diff --git a/grd-rdp-network-autodetection.c b/grd-rdp-network-autodetection.c new file mode 100644 index 0000000..7fdaed0 --- /dev/null +++ b/grd-rdp-network-autodetection.c @@ -0,0 +1,747 @@ +/* + * Copyright (C) 2021 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-rdp-network-autodetection.h" + +#include "grd-rdp-connect-time-autodetection.h" +#include "grd-rdp-dvc-graphics-pipeline.h" +#include "grd-rdp-private.h" + +#define BW_MEASURE_SEQUENCE_NUMBER 0 +#define MIN_NW_CHAR_RES_INTERVAL_US G_USEC_PER_SEC +#define PING_INTERVAL_HIGH_MS 70 +#define PING_INTERVAL_LOW_MS 700 +#define RTT_AVG_PERIOD_US (500 * 1000) + +typedef enum _PingInterval +{ + PING_INTERVAL_NONE, + PING_INTERVAL_HIGH, + PING_INTERVAL_LOW, +} PingInterval; + +typedef enum _BwMeasureState +{ + BW_MEASURE_STATE_NONE, + BW_MEASURE_STATE_PENDING_STOP, + BW_MEASURE_STATE_QUEUED_STOP, + BW_MEASURE_STATE_PENDING_RESULTS, +} BwMeasureState; + +typedef struct _PingInfo +{ + uint16_t sequence_number; + int64_t ping_time_us; +} PingInfo; + +typedef struct _RTTInfo +{ + int64_t round_trip_time_us; + int64_t response_time_us; +} RTTInfo; + +struct _GrdRdpNetworkAutodetection +{ + GObject parent; + + rdpAutoDetect *rdp_autodetect; + GMutex shutdown_mutex; + gboolean in_shutdown; + + GrdRdpConnectTimeAutodetection *ct_autodetection; + + GMutex consumer_mutex; + GrdRdpNwAutodetectRTTConsumer rtt_consumers; + GrdRdpNwAutodetectRTTConsumer rtt_high_nec_consumers; + + GMutex sequence_mutex; + GHashTable *sequences; + + GSource *ping_source; + GQueue *pings; + GQueue *round_trip_times; + PingInterval ping_interval; + + GMutex bw_measure_mutex; + uint32_t bandwidth_kbits; + BwMeasureState bw_measure_state; + HANDLE bw_measure_stop_event; + + int64_t last_nw_char_res_notification_us; + + uint16_t next_sequence_number; +}; + +G_DEFINE_TYPE (GrdRdpNetworkAutodetection, + grd_rdp_network_autodetection, + G_TYPE_OBJECT) + +GrdRdpConnectTimeAutodetection * +grd_rdp_network_autodetection_get_ct_handler (GrdRdpNetworkAutodetection *network_autodetection) +{ + return network_autodetection->ct_autodetection; +} + +void +grd_rdp_network_autodetection_start_connect_time_autodetection (GrdRdpNetworkAutodetection *network_autodetection) +{ + GrdRdpConnectTimeAutodetection *ct_autodetection = + network_autodetection->ct_autodetection; + + grd_rdp_connect_time_autodetection_start_detection (ct_autodetection); +} + +void +grd_rdp_network_autodetection_invoke_shutdown (GrdRdpNetworkAutodetection *network_autodetection) +{ + GrdRdpConnectTimeAutodetection *ct_autodetection = + network_autodetection->ct_autodetection; + + g_mutex_lock (&network_autodetection->shutdown_mutex); + network_autodetection->in_shutdown = TRUE; + g_mutex_unlock (&network_autodetection->shutdown_mutex); + + grd_rdp_connect_time_autodetection_invoke_shutdown (ct_autodetection); +} + +static uint16_t +get_next_free_sequence_number (GrdRdpNetworkAutodetection *network_autodetection) +{ + uint16_t sequence_number = network_autodetection->next_sequence_number; + + if (g_hash_table_size (network_autodetection->sequences) > UINT16_MAX >> 2) + { + g_warning ("[RDP] Network Autodetect: Protocol violation: Client leaves " + "requests unanswered"); + g_hash_table_remove_all (network_autodetection->sequences); + } + + while (sequence_number == BW_MEASURE_SEQUENCE_NUMBER || + g_hash_table_contains (network_autodetection->sequences, + GUINT_TO_POINTER (sequence_number))) + ++sequence_number; + + network_autodetection->next_sequence_number = sequence_number + 1; + + return sequence_number; +} + +static void +emit_ping_with_callback (GrdRdpNetworkAutodetection *network_autodetection, + GrdRdpNwAutodetectSequenceNumberReadyCallback callback, + gpointer callback_user_data) +{ + rdpAutoDetect *rdp_autodetect = network_autodetection->rdp_autodetect; + uint16_t sequence_number; + PingInfo *ping_info; + + ping_info = g_new0 (PingInfo, 1); + + g_mutex_lock (&network_autodetection->sequence_mutex); + sequence_number = get_next_free_sequence_number (network_autodetection); + + ping_info->sequence_number = sequence_number; + ping_info->ping_time_us = g_get_monotonic_time (); + + g_hash_table_add (network_autodetection->sequences, + GUINT_TO_POINTER (sequence_number)); + g_queue_push_tail (network_autodetection->pings, ping_info); + g_mutex_unlock (&network_autodetection->sequence_mutex); + + if (callback) + callback (callback_user_data, sequence_number); + + rdp_autodetect->RTTMeasureRequest (rdp_autodetect, RDP_TRANSPORT_TCP, + sequence_number); +} + +void +grd_rdp_network_autodetection_emit_ping (GrdRdpNetworkAutodetection *network_autodetection, + GrdRdpNwAutodetectSequenceNumberReadyCallback callback, + gpointer callback_user_data) +{ + emit_ping_with_callback (network_autodetection, callback, callback_user_data); +} + +static gboolean +has_rtt_consumer (GrdRdpNetworkAutodetection *network_autodetection, + GrdRdpNwAutodetectRTTConsumer rtt_consumer) +{ + g_assert (!g_mutex_trylock (&network_autodetection->consumer_mutex)); + + return !!(network_autodetection->rtt_consumers & rtt_consumer); +} + +static gboolean +is_active_high_nec_rtt_consumer (GrdRdpNetworkAutodetection *network_autodetection, + GrdRdpNwAutodetectRTTConsumer rtt_consumer) +{ + if (!has_rtt_consumer (network_autodetection, rtt_consumer)) + return FALSE; + if (network_autodetection->rtt_high_nec_consumers & rtt_consumer) + return TRUE; + + return FALSE; +} + +static gboolean +has_active_high_nec_rtt_consumers (GrdRdpNetworkAutodetection *network_autodetection) +{ + if (is_active_high_nec_rtt_consumer (network_autodetection, + GRD_RDP_NW_AUTODETECT_RTT_CONSUMER_RDPGFX)) + return TRUE; + + return FALSE; +} + +static gboolean +emit_ping (gpointer user_data) +{ + GrdRdpNetworkAutodetection *network_autodetection = user_data; + + emit_ping_with_callback (network_autodetection, NULL, NULL); + + return G_SOURCE_CONTINUE; +} + +static void +update_ping_source (GrdRdpNetworkAutodetection *network_autodetection) +{ + PingInterval new_ping_interval_type; + uint32_t ping_interval_ms; + + if (network_autodetection->rtt_consumers == GRD_RDP_NW_AUTODETECT_RTT_CONSUMER_NONE) + new_ping_interval_type = PING_INTERVAL_NONE; + else if (has_active_high_nec_rtt_consumers (network_autodetection)) + new_ping_interval_type = PING_INTERVAL_HIGH; + else + new_ping_interval_type = PING_INTERVAL_LOW; + + if (network_autodetection->ping_interval == new_ping_interval_type) + return; + + if (network_autodetection->ping_source) + { + g_source_destroy (network_autodetection->ping_source); + g_clear_pointer (&network_autodetection->ping_source, g_source_unref); + } + + network_autodetection->ping_interval = new_ping_interval_type; + + if (network_autodetection->ping_interval == PING_INTERVAL_NONE) + return; + + g_assert (!network_autodetection->ping_source); + emit_ping (network_autodetection); + + switch (new_ping_interval_type) + { + case PING_INTERVAL_NONE: + g_assert_not_reached (); + case PING_INTERVAL_HIGH: + ping_interval_ms = PING_INTERVAL_HIGH_MS; + break; + case PING_INTERVAL_LOW: + ping_interval_ms = PING_INTERVAL_LOW_MS; + break; + } + + network_autodetection->ping_source = g_timeout_source_new (ping_interval_ms); + g_source_set_callback (network_autodetection->ping_source, emit_ping, + network_autodetection, NULL); + g_source_attach (network_autodetection->ping_source, NULL); +} + +void +grd_rdp_network_autodetection_ensure_rtt_consumer (GrdRdpNetworkAutodetection *network_autodetection, + GrdRdpNwAutodetectRTTConsumer rtt_consumer) +{ + g_assert (rtt_consumer != GRD_RDP_NW_AUTODETECT_RTT_CONSUMER_NONE); + + g_mutex_lock (&network_autodetection->consumer_mutex); + network_autodetection->rtt_consumers |= rtt_consumer; + + update_ping_source (network_autodetection); + g_mutex_unlock (&network_autodetection->consumer_mutex); +} + +void +grd_rdp_network_autodetection_remove_rtt_consumer (GrdRdpNetworkAutodetection *network_autodetection, + GrdRdpNwAutodetectRTTConsumer rtt_consumer) +{ + g_assert (rtt_consumer != GRD_RDP_NW_AUTODETECT_RTT_CONSUMER_NONE); + + g_mutex_lock (&network_autodetection->consumer_mutex); + network_autodetection->rtt_consumers &= ~rtt_consumer; + + update_ping_source (network_autodetection); + g_mutex_unlock (&network_autodetection->consumer_mutex); +} + +void +grd_rdp_network_autodetection_set_rtt_consumer_necessity (GrdRdpNetworkAutodetection *network_autodetection, + GrdRdpNwAutodetectRTTConsumer rtt_consumer, + GrdRdpNwAutodetectRTTNecessity rtt_necessity) +{ + GrdRdpNwAutodetectRTTNecessity current_rtt_necessity; + + g_assert (rtt_consumer != GRD_RDP_NW_AUTODETECT_RTT_CONSUMER_NONE); + g_assert (rtt_necessity == GRD_RDP_NW_AUTODETECT_RTT_NEC_HIGH || + rtt_necessity == GRD_RDP_NW_AUTODETECT_RTT_NEC_LOW); + + g_mutex_lock (&network_autodetection->consumer_mutex); + if (network_autodetection->rtt_high_nec_consumers & rtt_consumer) + current_rtt_necessity = GRD_RDP_NW_AUTODETECT_RTT_NEC_HIGH; + else + current_rtt_necessity = GRD_RDP_NW_AUTODETECT_RTT_NEC_LOW; + + if (current_rtt_necessity == rtt_necessity) + { + g_mutex_unlock (&network_autodetection->consumer_mutex); + return; + } + + switch (rtt_necessity) + { + case GRD_RDP_NW_AUTODETECT_RTT_NEC_HIGH: + network_autodetection->rtt_high_nec_consumers |= rtt_consumer; + break; + case GRD_RDP_NW_AUTODETECT_RTT_NEC_LOW: + network_autodetection->rtt_high_nec_consumers &= ~rtt_consumer; + break; + } + + if (has_rtt_consumer (network_autodetection, rtt_consumer)) + update_ping_source (network_autodetection); + g_mutex_unlock (&network_autodetection->consumer_mutex); +} + +gboolean +grd_rdp_network_autodetection_try_bw_measure_start (GrdRdpNetworkAutodetection *network_autodetection) +{ + rdpAutoDetect *rdp_autodetect = network_autodetection->rdp_autodetect; + g_autoptr (GMutexLocker) locker = NULL; + + locker = g_mutex_locker_new (&network_autodetection->bw_measure_mutex); + if (network_autodetection->bw_measure_state != BW_MEASURE_STATE_NONE) + return FALSE; + + network_autodetection->bw_measure_state = BW_MEASURE_STATE_PENDING_STOP; + g_clear_pointer (&locker, g_mutex_locker_free); + + rdp_autodetect->BandwidthMeasureStart (rdp_autodetect, RDP_TRANSPORT_TCP, + BW_MEASURE_SEQUENCE_NUMBER); + + return TRUE; +} + +void +grd_rdp_network_autodetection_bw_measure_stop (GrdRdpNetworkAutodetection *network_autodetection) +{ + rdpAutoDetect *rdp_autodetect = network_autodetection->rdp_autodetect; + g_autoptr (GMutexLocker) locker = NULL; + + locker = g_mutex_locker_new (&network_autodetection->bw_measure_mutex); + ResetEvent (network_autodetection->bw_measure_stop_event); + + if (network_autodetection->bw_measure_state != BW_MEASURE_STATE_QUEUED_STOP) + return; + + network_autodetection->bw_measure_state = BW_MEASURE_STATE_PENDING_RESULTS; + g_clear_pointer (&locker, g_mutex_locker_free); + + rdp_autodetect->BandwidthMeasureStop (rdp_autodetect, RDP_TRANSPORT_TCP, + BW_MEASURE_SEQUENCE_NUMBER, 0); +} + +void +grd_rdp_network_autodetection_queue_bw_measure_stop (GrdRdpNetworkAutodetection *network_autodetection) +{ + g_mutex_lock (&network_autodetection->bw_measure_mutex); + g_assert (network_autodetection->bw_measure_state == BW_MEASURE_STATE_PENDING_STOP); + + SetEvent (network_autodetection->bw_measure_stop_event); + network_autodetection->bw_measure_state = BW_MEASURE_STATE_QUEUED_STOP; + g_mutex_unlock (&network_autodetection->bw_measure_mutex); +} + +HANDLE +grd_rdp_network_autodetection_get_bw_measure_stop_event_handle (GrdRdpNetworkAutodetection *network_autodetection) +{ + return network_autodetection->bw_measure_stop_event; +} + +static void +track_round_trip_time (GrdRdpNetworkAutodetection *network_autodetection, + int64_t ping_time_us, + int64_t pong_time_us) +{ + RTTInfo *rtt_info; + + rtt_info = g_malloc0 (sizeof (RTTInfo)); + rtt_info->round_trip_time_us = pong_time_us - ping_time_us; + rtt_info->response_time_us = pong_time_us; + + g_queue_push_tail (network_autodetection->round_trip_times, rtt_info); +} + +static void +remove_old_round_trip_times (GrdRdpNetworkAutodetection *network_autodetection) +{ + int64_t current_time_us; + RTTInfo *rtt_info; + + current_time_us = g_get_monotonic_time (); + while ((rtt_info = g_queue_peek_head (network_autodetection->round_trip_times)) && + current_time_us - rtt_info->response_time_us >= RTT_AVG_PERIOD_US) + g_free (g_queue_pop_head (network_autodetection->round_trip_times)); +} + +static void +update_round_trip_time_values (GrdRdpNetworkAutodetection *network_autodetection, + int64_t *base_round_trip_time_us, + int64_t *avg_round_trip_time_us) +{ + int64_t sum_round_trip_times_us = 0; + uint32_t total_round_trip_times; + RTTInfo *rtt_info; + GQueue *tmp; + + *base_round_trip_time_us = 0; + *avg_round_trip_time_us = 0; + + remove_old_round_trip_times (network_autodetection); + if (g_queue_get_length (network_autodetection->round_trip_times) == 0) + return; + + tmp = g_queue_copy (network_autodetection->round_trip_times); + total_round_trip_times = g_queue_get_length (tmp); + g_assert (total_round_trip_times > 0); + + *base_round_trip_time_us = INT64_MAX; + + while ((rtt_info = g_queue_pop_head (tmp))) + { + *base_round_trip_time_us = MIN (*base_round_trip_time_us, + rtt_info->round_trip_time_us); + sum_round_trip_times_us += rtt_info->round_trip_time_us; + } + + g_queue_free (tmp); + + *avg_round_trip_time_us = sum_round_trip_times_us / total_round_trip_times; +} + +static void +maybe_send_network_characteristics_results (GrdRdpNetworkAutodetection *network_autodetection, + uint32_t base_round_trip_time_ms, + uint32_t avg_round_trip_time_ms) +{ + rdpAutoDetect *rdp_autodetect = network_autodetection->rdp_autodetect; + uint32_t bandwidth_kbits = network_autodetection->bandwidth_kbits; + rdpNetworkCharacteristicsResult result = {}; + int64_t last_notification_us; + int64_t current_time_us; + uint16_t sequence_number; + + if (bandwidth_kbits == 0) + return; + + if (g_queue_get_length (network_autodetection->round_trip_times) == 0) + return; + + current_time_us = g_get_monotonic_time (); + last_notification_us = network_autodetection->last_nw_char_res_notification_us; + + if (current_time_us - last_notification_us < MIN_NW_CHAR_RES_INTERVAL_US) + return; + + result.type = RDP_NETCHAR_RESULT_TYPE_BASE_RTT_BW_AVG_RTT; + result.baseRTT = base_round_trip_time_ms; + result.averageRTT = avg_round_trip_time_ms; + result.bandwidth = bandwidth_kbits; + + g_mutex_lock (&network_autodetection->sequence_mutex); + sequence_number = get_next_free_sequence_number (network_autodetection); + g_mutex_unlock (&network_autodetection->sequence_mutex); + + rdp_autodetect->NetworkCharacteristicsResult (rdp_autodetect, + RDP_TRANSPORT_TCP, + sequence_number, + &result); + + network_autodetection->last_nw_char_res_notification_us = current_time_us; +} + +static BOOL +autodetect_rtt_measure_response (rdpAutoDetect *rdp_autodetect, + RDP_TRANSPORT_TYPE transport_type, + uint16_t sequence_number) +{ + RdpPeerContext *rdp_peer_context = (RdpPeerContext *) rdp_autodetect->context; + GrdRdpNetworkAutodetection *network_autodetection = rdp_autodetect->custom; + GrdRdpConnectTimeAutodetection *ct_autodetection = + network_autodetection->ct_autodetection; + g_autofree PingInfo *ping_info = NULL; + int64_t pong_time_us; + int64_t ping_time_us; + int64_t base_round_trip_time_us; + int64_t avg_round_trip_time_us; + gboolean has_rtt_consumer_rdpgfx = FALSE; + + g_assert (transport_type == RDP_TRANSPORT_TCP); + + pong_time_us = g_get_monotonic_time (); + + g_mutex_lock (&network_autodetection->sequence_mutex); + if (!g_hash_table_contains (network_autodetection->sequences, + GUINT_TO_POINTER (sequence_number))) + { + g_mutex_unlock (&network_autodetection->sequence_mutex); + return TRUE; + } + + while ((ping_info = g_queue_pop_head (network_autodetection->pings)) && + ping_info->sequence_number != sequence_number) + { + g_hash_table_remove (network_autodetection->sequences, + GUINT_TO_POINTER (ping_info->sequence_number)); + g_clear_pointer (&ping_info, g_free); + } + + g_assert (ping_info); + g_assert (ping_info->sequence_number == sequence_number); + + g_hash_table_remove (network_autodetection->sequences, + GUINT_TO_POINTER (ping_info->sequence_number)); + g_mutex_unlock (&network_autodetection->sequence_mutex); + + ping_time_us = ping_info->ping_time_us; + track_round_trip_time (network_autodetection, ping_time_us, pong_time_us); + update_round_trip_time_values (network_autodetection, + &base_round_trip_time_us, + &avg_round_trip_time_us); + + if (!grd_rdp_connect_time_autodetection_is_complete (ct_autodetection)) + { + grd_rdp_connect_time_autodetection_notify_rtt_measure_response (ct_autodetection, + sequence_number, + base_round_trip_time_us, + avg_round_trip_time_us); + return TRUE; + } + + g_mutex_lock (&network_autodetection->consumer_mutex); + has_rtt_consumer_rdpgfx = has_rtt_consumer ( + network_autodetection, GRD_RDP_NW_AUTODETECT_RTT_CONSUMER_RDPGFX); + g_mutex_unlock (&network_autodetection->consumer_mutex); + + g_mutex_lock (&network_autodetection->shutdown_mutex); + if (!network_autodetection->in_shutdown && has_rtt_consumer_rdpgfx && + rdp_peer_context->graphics_pipeline) + { + grd_rdp_dvc_graphics_pipeline_notify_new_round_trip_time ( + rdp_peer_context->graphics_pipeline, avg_round_trip_time_us); + } + g_mutex_unlock (&network_autodetection->shutdown_mutex); + + maybe_send_network_characteristics_results (network_autodetection, + base_round_trip_time_us / 1000, + avg_round_trip_time_us / 1000); + + return TRUE; +} + +static BOOL +handle_bw_measure_results_connect_time (GrdRdpNetworkAutodetection *network_autodetection, + RDP_TRANSPORT_TYPE transport_type, + uint32_t time_delta_ms, + uint32_t byte_count) +{ + GrdRdpConnectTimeAutodetection *ct_autodetection = + network_autodetection->ct_autodetection; + uint64_t bit_count; + uint32_t bandwidth_kbits; + + g_assert (transport_type == RDP_TRANSPORT_TCP); + + if (!grd_rdp_connect_time_autodetection_notify_bw_measure_results (ct_autodetection, + time_delta_ms, + byte_count)) + return FALSE; + + bit_count = ((uint64_t) byte_count) * UINT64_C (8); + bandwidth_kbits = bit_count / MAX (time_delta_ms, 1); + + network_autodetection->bandwidth_kbits = bandwidth_kbits; + + return TRUE; +} + +static BOOL +handle_bw_measure_results_continuous (GrdRdpNetworkAutodetection *network_autodetection, + uint32_t time_delta_ms, + uint32_t byte_count) +{ + g_autoptr (GMutexLocker) locker = NULL; + int64_t base_round_trip_time_us; + int64_t avg_round_trip_time_us; + uint64_t bit_count; + + locker = g_mutex_locker_new (&network_autodetection->bw_measure_mutex); + if (network_autodetection->bw_measure_state != BW_MEASURE_STATE_PENDING_RESULTS) + return TRUE; + + network_autodetection->bw_measure_state = BW_MEASURE_STATE_NONE; + g_clear_pointer (&locker, g_mutex_locker_free); + + bit_count = ((uint64_t) byte_count) * UINT64_C (8); + network_autodetection->bandwidth_kbits = bit_count / MAX (time_delta_ms, 1); + + update_round_trip_time_values (network_autodetection, + &base_round_trip_time_us, + &avg_round_trip_time_us); + + maybe_send_network_characteristics_results (network_autodetection, + base_round_trip_time_us / 1000, + avg_round_trip_time_us / 1000); + + return TRUE; +} + +static BOOL +autodetect_bw_measure_results (rdpAutoDetect *rdp_autodetect, + RDP_TRANSPORT_TYPE transport_type, + uint16_t sequence_number, + uint16_t response_type, + uint32_t time_delta_ms, + uint32_t byte_count) +{ + GrdRdpNetworkAutodetection *network_autodetection = rdp_autodetect->custom; + GrdRdpConnectTimeAutodetection *ct_autodetection = + network_autodetection->ct_autodetection; + + if (!grd_rdp_connect_time_autodetection_is_complete (ct_autodetection)) + { + return handle_bw_measure_results_connect_time (network_autodetection, + transport_type, + time_delta_ms, byte_count); + } + + return handle_bw_measure_results_continuous (network_autodetection, + time_delta_ms, byte_count); +} + +GrdRdpNetworkAutodetection * +grd_rdp_network_autodetection_new (rdpContext *rdp_context) +{ + GrdRdpNetworkAutodetection *network_autodetection; + rdpAutoDetect *rdp_autodetect = autodetect_get (rdp_context); + + network_autodetection = g_object_new (GRD_TYPE_RDP_NETWORK_AUTODETECTION, + NULL); + network_autodetection->rdp_autodetect = rdp_autodetect; + + rdp_autodetect->RTTMeasureResponse = autodetect_rtt_measure_response; + rdp_autodetect->BandwidthMeasureResults = autodetect_bw_measure_results; + rdp_autodetect->custom = network_autodetection; + + network_autodetection->ct_autodetection = + grd_rdp_connect_time_autodetection_new (network_autodetection, + rdp_autodetect); + + return network_autodetection; +} + +static void +grd_rdp_network_autodetection_dispose (GObject *object) +{ + GrdRdpNetworkAutodetection *network_autodetection = + GRD_RDP_NETWORK_AUTODETECTION (object); + + g_clear_object (&network_autodetection->ct_autodetection); + + if (network_autodetection->ping_source) + { + g_source_destroy (network_autodetection->ping_source); + g_clear_pointer (&network_autodetection->ping_source, g_source_unref); + } + + if (network_autodetection->round_trip_times) + { + g_queue_free_full (network_autodetection->round_trip_times, g_free); + network_autodetection->round_trip_times = NULL; + } + + if (network_autodetection->pings) + { + g_queue_free_full (network_autodetection->pings, g_free); + network_autodetection->pings = NULL; + } + + g_clear_pointer (&network_autodetection->sequences, g_hash_table_destroy); + + g_clear_pointer (&network_autodetection->bw_measure_stop_event, CloseHandle); + + G_OBJECT_CLASS (grd_rdp_network_autodetection_parent_class)->dispose (object); +} + +static void +grd_rdp_network_autodetection_finalize (GObject *object) +{ + GrdRdpNetworkAutodetection *network_autodetection = + GRD_RDP_NETWORK_AUTODETECTION (object); + + g_mutex_clear (&network_autodetection->bw_measure_mutex); + g_mutex_clear (&network_autodetection->sequence_mutex); + g_mutex_clear (&network_autodetection->consumer_mutex); + g_mutex_clear (&network_autodetection->shutdown_mutex); + + G_OBJECT_CLASS (grd_rdp_network_autodetection_parent_class)->finalize (object); +} + +static void +grd_rdp_network_autodetection_init (GrdRdpNetworkAutodetection *network_autodetection) +{ + network_autodetection->ping_interval = PING_INTERVAL_NONE; + network_autodetection->bw_measure_state = BW_MEASURE_STATE_NONE; + + network_autodetection->bw_measure_stop_event = + CreateEvent (NULL, TRUE, FALSE, NULL); + + network_autodetection->sequences = g_hash_table_new (NULL, NULL); + network_autodetection->pings = g_queue_new (); + network_autodetection->round_trip_times = g_queue_new (); + + g_mutex_init (&network_autodetection->shutdown_mutex); + g_mutex_init (&network_autodetection->consumer_mutex); + g_mutex_init (&network_autodetection->sequence_mutex); + g_mutex_init (&network_autodetection->bw_measure_mutex); +} + +static void +grd_rdp_network_autodetection_class_init (GrdRdpNetworkAutodetectionClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = grd_rdp_network_autodetection_dispose; + object_class->finalize = grd_rdp_network_autodetection_finalize; +} diff --git a/grd-rdp-network-autodetection.h b/grd-rdp-network-autodetection.h new file mode 100644 index 0000000..a212c99 --- /dev/null +++ b/grd-rdp-network-autodetection.h @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2021 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include +#include + +#include "grd-types.h" + +#define GRD_TYPE_RDP_NETWORK_AUTODETECTION (grd_rdp_network_autodetection_get_type ()) +G_DECLARE_FINAL_TYPE (GrdRdpNetworkAutodetection, grd_rdp_network_autodetection, + GRD, RDP_NETWORK_AUTODETECTION, GObject) + +typedef enum _GrdRdpNwAutodetectRTTConsumer +{ + GRD_RDP_NW_AUTODETECT_RTT_CONSUMER_NONE = 0, + GRD_RDP_NW_AUTODETECT_RTT_CONSUMER_RDPGFX = 1 << 0, +} GrdRdpNwAutodetectRTTConsumer; + +typedef enum _GrdRdpNwAutodetectRTTNecessity +{ + GRD_RDP_NW_AUTODETECT_RTT_NEC_HIGH, + GRD_RDP_NW_AUTODETECT_RTT_NEC_LOW, +} GrdRdpNwAutodetectRTTNecessity; + +typedef void (* GrdRdpNwAutodetectSequenceNumberReadyCallback) (gpointer user_data, + uint16_t sequence_number); + +GrdRdpNetworkAutodetection *grd_rdp_network_autodetection_new (rdpContext *rdp_context); + +GrdRdpConnectTimeAutodetection *grd_rdp_network_autodetection_get_ct_handler (GrdRdpNetworkAutodetection *network_autodetection); + +void grd_rdp_network_autodetection_start_connect_time_autodetection (GrdRdpNetworkAutodetection *network_autodetection); + +void grd_rdp_network_autodetection_invoke_shutdown (GrdRdpNetworkAutodetection *network_autodetection); + +void grd_rdp_network_autodetection_emit_ping (GrdRdpNetworkAutodetection *network_autodetection, + GrdRdpNwAutodetectSequenceNumberReadyCallback callback, + gpointer callback_user_data); + +void grd_rdp_network_autodetection_ensure_rtt_consumer (GrdRdpNetworkAutodetection *network_autodetection, + GrdRdpNwAutodetectRTTConsumer rtt_consumer); + +void grd_rdp_network_autodetection_remove_rtt_consumer (GrdRdpNetworkAutodetection *network_autodetection, + GrdRdpNwAutodetectRTTConsumer rtt_consumer); + +void grd_rdp_network_autodetection_set_rtt_consumer_necessity (GrdRdpNetworkAutodetection *network_autodetection, + GrdRdpNwAutodetectRTTConsumer rtt_consumer, + GrdRdpNwAutodetectRTTNecessity rtt_necessity); + +gboolean grd_rdp_network_autodetection_try_bw_measure_start (GrdRdpNetworkAutodetection *network_autodetection); + +void grd_rdp_network_autodetection_bw_measure_stop (GrdRdpNetworkAutodetection *network_autodetection); + +void grd_rdp_network_autodetection_queue_bw_measure_stop (GrdRdpNetworkAutodetection *network_autodetection); + +HANDLE grd_rdp_network_autodetection_get_bw_measure_stop_event_handle (GrdRdpNetworkAutodetection *network_autodetection); diff --git a/grd-rdp-pipewire-stream.c b/grd-rdp-pipewire-stream.c new file mode 100644 index 0000000..1825fd1 --- /dev/null +++ b/grd-rdp-pipewire-stream.c @@ -0,0 +1,1263 @@ +/* + * Copyright (C) 2020 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-rdp-pipewire-stream.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "grd-context.h" +#include "grd-egl-thread.h" +#include "grd-hwaccel-vulkan.h" +#include "grd-pipewire-utils.h" +#include "grd-rdp-buffer-pool.h" +#include "grd-rdp-cursor-renderer.h" +#include "grd-rdp-damage-detector.h" +#include "grd-rdp-legacy-buffer.h" +#include "grd-rdp-pw-buffer.h" +#include "grd-rdp-renderer.h" +#include "grd-rdp-server.h" +#include "grd-rdp-session-metrics.h" +#include "grd-rdp-surface.h" +#include "grd-rdp-surface-renderer.h" +#include "grd-utils.h" +#include "grd-vk-device.h" + +#define DEFAULT_BUFFER_POOL_SIZE 5 +#define MAX_PW_PARAMS 3 + +enum +{ + ERROR, + CLOSED, + VIDEO_RESIZED, + + N_SIGNALS +}; + +static guint signals[N_SIGNALS]; + +typedef struct _GrdRdpFrame GrdRdpFrame; + +typedef void (* GrdRdpFrameReadyCallback) (GrdRdpPipeWireStream *stream, + GrdRdpFrame *frame, + gboolean success, + gpointer user_data); + +struct _GrdRdpFrame +{ + gatomicrefcount refcount; + + GrdRdpLegacyBuffer *buffer; + + GrdRdpPipeWireStream *stream; + GrdRdpFrameReadyCallback callback; + gpointer callback_user_data; +}; + +typedef struct +{ + GrdRdpLegacyBuffer *rdp_legacy_buffer; +} UnmapBufferData; + +typedef struct +{ + GrdRdpLegacyBuffer *rdp_legacy_buffer; +} AllocateBufferData; + +typedef struct +{ + GrdRdpLegacyBuffer *rdp_legacy_buffer; +} RealizeBufferData; + +struct _GrdRdpPipeWireStream +{ + GObject parent; + + GrdSessionRdp *session_rdp; + GrdRdpSurface *rdp_surface; + GrdEglThreadSlot egl_slot; + + GMutex dequeue_mutex; + gboolean dequeuing_disallowed; + + GSource *pipewire_source; + struct pw_context *pipewire_context; + struct pw_core *pipewire_core; + + struct spa_hook pipewire_core_listener; + + struct pw_registry *pipewire_registry; + struct spa_hook pipewire_registry_listener; + + GrdRdpBufferPool *buffer_pool; + + struct pw_stream *pipewire_stream; + struct spa_hook pipewire_stream_listener; + + GHashTable *pipewire_buffers; + gboolean ignore_new_buffers; + + uint32_t src_node_id; + + gboolean pending_resize; + struct spa_video_info_raw spa_format; +}; + +G_DEFINE_TYPE (GrdRdpPipeWireStream, grd_rdp_pipewire_stream, + G_TYPE_OBJECT) + +static void grd_rdp_frame_unref (GrdRdpFrame *frame); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (GrdRdpFrame, grd_rdp_frame_unref) + +static void +acquire_pipewire_buffer_lock (GrdRdpPipeWireStream *stream, + struct pw_buffer *buffer) +{ + GrdRdpPwBuffer *rdp_pw_buffer = NULL; + + if (!g_hash_table_lookup_extended (stream->pipewire_buffers, buffer, + NULL, (gpointer *) &rdp_pw_buffer)) + g_assert_not_reached (); + + grd_rdp_pw_buffer_acquire_lock (rdp_pw_buffer); +} + +static void +maybe_release_pipewire_buffer_lock (GrdRdpPipeWireStream *stream, + struct pw_buffer *buffer) +{ + GrdRdpPwBuffer *rdp_pw_buffer = NULL; + + if (!g_hash_table_lookup_extended (stream->pipewire_buffers, buffer, + NULL, (gpointer *) &rdp_pw_buffer)) + g_assert_not_reached (); + + grd_rdp_pw_buffer_maybe_release_lock (rdp_pw_buffer); +} + +static GrdRdpFrame * +grd_rdp_frame_new (GrdRdpPipeWireStream *stream, + GrdRdpFrameReadyCallback callback, + gpointer callback_user_data) +{ + GrdRdpFrame *frame; + + frame = g_new0 (GrdRdpFrame, 1); + + g_atomic_ref_count_init (&frame->refcount); + frame->stream = stream; + frame->callback = callback; + frame->callback_user_data = callback_user_data; + + return frame; +} + +static GrdRdpFrame * +grd_rdp_frame_ref (GrdRdpFrame *frame) +{ + g_atomic_ref_count_inc (&frame->refcount); + return frame; +} + +static void +grd_rdp_frame_unref (GrdRdpFrame *frame) +{ + if (g_atomic_ref_count_dec (&frame->refcount)) + { + g_clear_pointer (&frame->buffer, grd_rdp_legacy_buffer_release); + g_free (frame); + } +} + +static void +grd_rdp_frame_invoke_callback (GrdRdpFrame *frame, + gboolean success) +{ + frame->callback (frame->stream, + frame, + success, + frame->callback_user_data); +} + +static void +add_common_format_params (struct spa_pod_builder *pod_builder, + enum spa_video_format spa_format, + const GrdRdpVirtualMonitor *virtual_monitor, + uint32_t refresh_rate) +{ + struct spa_rectangle min_rect; + struct spa_rectangle max_rect; + struct spa_fraction min_framerate; + struct spa_fraction max_framerate; + + min_rect = SPA_RECTANGLE (1, 1); + max_rect = SPA_RECTANGLE (INT32_MAX, INT32_MAX); + min_framerate = SPA_FRACTION (1, 1); + max_framerate = SPA_FRACTION (refresh_rate, 1); + + spa_pod_builder_add (pod_builder, + SPA_FORMAT_mediaType, + SPA_POD_Id (SPA_MEDIA_TYPE_video), 0); + spa_pod_builder_add (pod_builder, + SPA_FORMAT_mediaSubtype, + SPA_POD_Id (SPA_MEDIA_SUBTYPE_raw), 0); + spa_pod_builder_add (pod_builder, + SPA_FORMAT_VIDEO_format, + SPA_POD_Id (spa_format), 0); + + if (virtual_monitor) + { + struct spa_rectangle virtual_monitor_rect; + + virtual_monitor_rect = SPA_RECTANGLE (virtual_monitor->width, + virtual_monitor->height); + spa_pod_builder_add (pod_builder, + SPA_FORMAT_VIDEO_size, + SPA_POD_Rectangle (&virtual_monitor_rect), 0); + } + else + { + spa_pod_builder_add (pod_builder, + SPA_FORMAT_VIDEO_size, + SPA_POD_CHOICE_RANGE_Rectangle (&min_rect, + &min_rect, + &max_rect), 0); + } + spa_pod_builder_add (pod_builder, + SPA_FORMAT_VIDEO_framerate, + SPA_POD_Fraction (&SPA_FRACTION (0, 1)), 0); + spa_pod_builder_add (pod_builder, + SPA_FORMAT_VIDEO_maxFramerate, + SPA_POD_CHOICE_RANGE_Fraction (&max_framerate, + &min_framerate, + &max_framerate), 0); +} + +static gboolean +get_modifiers_for_format (GrdRdpPipeWireStream *stream, + uint32_t drm_format, + int *out_n_modifiers, + uint64_t **out_modifiers) +{ + GrdSessionRdp *session_rdp = stream->session_rdp; + GrdRdpRenderer *renderer = grd_session_rdp_get_renderer (session_rdp); + GrdSession *session = GRD_SESSION (session_rdp); + GrdContext *context = grd_session_get_context (session); + GrdEglThread *egl_thread = grd_context_get_egl_thread (context); + GrdVkDevice *vk_device; + + g_assert (egl_thread); + + vk_device = grd_rdp_renderer_get_vk_device (renderer); + if (vk_device) + { + GrdRdpServer *rdp_server = grd_session_rdp_get_server (session_rdp); + GrdHwAccelVulkan *hwaccel_vulkan = + grd_rdp_server_get_hwaccel_vulkan (rdp_server); + GrdVkPhysicalDevice *vk_physical_device = + grd_vk_device_get_physical_device (vk_device); + + g_assert (hwaccel_vulkan); + + return grd_hwaccel_vulkan_get_modifiers_for_format (hwaccel_vulkan, + vk_physical_device, + drm_format, + out_n_modifiers, + out_modifiers); + } + + return grd_egl_thread_get_modifiers_for_format (egl_thread, drm_format, + out_n_modifiers, + out_modifiers); +} + +static uint32_t +add_format_params (GrdRdpPipeWireStream *stream, + const GrdRdpVirtualMonitor *virtual_monitor, + struct spa_pod_builder *pod_builder, + const struct spa_pod **params, + uint32_t n_available_params) +{ + GrdSession *session = GRD_SESSION (stream->session_rdp); + GrdContext *context = grd_session_get_context (session); + GrdRdpServer *rdp_server = grd_session_rdp_get_server (stream->session_rdp); + GrdHwAccelNvidia *hwaccel_nvidia = + grd_rdp_server_get_hwaccel_nvidia (rdp_server); + GrdEglThread *egl_thread = grd_context_get_egl_thread (context); + GrdRdpSurfaceRenderer *surface_renderer = + grd_rdp_surface_get_surface_renderer (stream->rdp_surface); + uint32_t refresh_rate = + grd_rdp_surface_renderer_get_refresh_rate (surface_renderer); + struct spa_pod_frame format_frame; + enum spa_video_format spa_format = SPA_VIDEO_FORMAT_BGRx; + gboolean need_fallback_format = FALSE; + uint32_t n_params = 0; + + g_assert (n_available_params >= 2); + + spa_pod_builder_push_object (pod_builder, &format_frame, + SPA_TYPE_OBJECT_Format, + SPA_PARAM_EnumFormat); + add_common_format_params (pod_builder, spa_format, virtual_monitor, + refresh_rate); + + if (egl_thread && !hwaccel_nvidia) + { + uint32_t drm_format; + int n_modifiers; + g_autofree uint64_t *modifiers = NULL; + + grd_get_spa_format_details (spa_format, &drm_format, NULL); + if (get_modifiers_for_format (stream, drm_format, + &n_modifiers, &modifiers)) + { + struct spa_pod_frame modifier_frame; + int i; + + spa_pod_builder_prop (pod_builder, + SPA_FORMAT_VIDEO_modifier, + (SPA_POD_PROP_FLAG_MANDATORY | + SPA_POD_PROP_FLAG_DONT_FIXATE)); + + spa_pod_builder_push_choice (pod_builder, &modifier_frame, + SPA_CHOICE_Enum, 0); + spa_pod_builder_long (pod_builder, modifiers[0]); + + for (i = 0; i < n_modifiers; i++) + { + uint64_t modifier = modifiers[i]; + + spa_pod_builder_long (pod_builder, modifier); + } + spa_pod_builder_long (pod_builder, DRM_FORMAT_MOD_INVALID); + spa_pod_builder_pop (pod_builder, &modifier_frame); + + need_fallback_format = TRUE; + } + } + + params[n_params++] = spa_pod_builder_pop (pod_builder, &format_frame); + + if (need_fallback_format) + { + spa_pod_builder_push_object (pod_builder, &format_frame, + SPA_TYPE_OBJECT_Format, + SPA_PARAM_EnumFormat); + add_common_format_params (pod_builder, spa_format, virtual_monitor, + refresh_rate); + params[n_params++] = spa_pod_builder_pop (pod_builder, &format_frame); + } + + return n_params; +} + +static uint32_t +add_tag_params (GrdRdpPipeWireStream *stream, + const GrdRdpVirtualMonitor *virtual_monitor, + struct spa_pod_builder *pod_builder, + const struct spa_pod **params, + uint32_t n_available_params) +{ + struct spa_pod_frame tag_frame; + struct spa_dict_item items[1]; + char scale_string[G_ASCII_DTOSTR_BUF_SIZE]; + uint32_t n_params = 0; + double scale; + + g_assert (n_available_params >= 1); + + spa_tag_build_start (pod_builder, &tag_frame, + SPA_PARAM_Tag, SPA_DIRECTION_INPUT); + + scale = virtual_monitor->scale ? virtual_monitor->scale / 100.0 : 1.0; + g_ascii_dtostr (scale_string, G_ASCII_DTOSTR_BUF_SIZE, scale); + items[0] = SPA_DICT_ITEM_INIT ("org.gnome.preferred-scale", scale_string); + + spa_tag_build_add_dict (pod_builder, + &SPA_DICT_INIT (items, G_N_ELEMENTS (items))); + + params[n_params++] = spa_tag_build_end (pod_builder, &tag_frame); + + return n_params; +} + +void +grd_rdp_pipewire_stream_resize (GrdRdpPipeWireStream *stream, + GrdRdpVirtualMonitor *virtual_monitor) +{ + uint8_t params_buffer[1024]; + struct spa_pod_builder pod_builder; + const struct spa_pod *params[MAX_PW_PARAMS] = {}; + uint32_t n_params = 0; + + stream->pending_resize = TRUE; + + pod_builder = SPA_POD_BUILDER_INIT (params_buffer, sizeof (params_buffer)); + + n_params += add_format_params (stream, virtual_monitor, &pod_builder, + ¶ms[n_params], MAX_PW_PARAMS - n_params); + if (virtual_monitor) + { + n_params += add_tag_params (stream, virtual_monitor, &pod_builder, + ¶ms[n_params], MAX_PW_PARAMS - n_params); + } + + g_assert (n_params > 0); + pw_stream_update_params (stream->pipewire_stream, params, n_params); +} + +static void +on_stream_state_changed (void *user_data, + enum pw_stream_state old, + enum pw_stream_state state, + const char *error) +{ + g_debug ("PipeWire stream state changed from %s to %s", + pw_stream_state_as_string (old), + pw_stream_state_as_string (state)); + + switch (state) + { + case PW_STREAM_STATE_ERROR: + g_warning ("PipeWire stream error: %s", error); + break; + case PW_STREAM_STATE_PAUSED: + case PW_STREAM_STATE_STREAMING: + case PW_STREAM_STATE_UNCONNECTED: + case PW_STREAM_STATE_CONNECTING: + break; + } +} + +static void +on_sync_complete (gboolean success, + gpointer user_data) +{ + GrdSyncPoint *sync_point = user_data; + + grd_sync_point_complete (sync_point, success); +} + +static void +sync_egl_thread (GrdEglThread *egl_thread) +{ + GrdSyncPoint sync_point = {}; + + grd_sync_point_init (&sync_point); + grd_egl_thread_sync (egl_thread, on_sync_complete, &sync_point, NULL); + + grd_sync_point_wait_for_completion (&sync_point); + grd_sync_point_clear (&sync_point); +} + +static void +release_all_buffers (GrdRdpPipeWireStream *stream) +{ + GrdRdpSurfaceRenderer *surface_renderer; + + surface_renderer = grd_rdp_surface_get_surface_renderer (stream->rdp_surface); + grd_rdp_surface_renderer_reset (surface_renderer); +} + +static void +on_stream_param_changed (void *user_data, + uint32_t id, + const struct spa_pod *format) +{ + GrdRdpPipeWireStream *stream = GRD_RDP_PIPEWIRE_STREAM (user_data); + GrdSession *session = GRD_SESSION (stream->session_rdp); + GrdContext *context = grd_session_get_context (session); + GrdEglThread *egl_thread = grd_context_get_egl_thread (context); + GrdRdpServer *rdp_server = grd_session_rdp_get_server (stream->session_rdp); + GrdHwAccelNvidia *hwaccel_nvidia = + grd_rdp_server_get_hwaccel_nvidia (rdp_server); + uint32_t width; + uint32_t height; + uint32_t stride; + uint8_t params_buffer[1024]; + struct spa_pod_builder pod_builder; + enum spa_data_type allowed_buffer_types; + const struct spa_pod *params[3]; + + if (!format || id != SPA_PARAM_Format) + return; + + spa_format_video_raw_parse (format, &stream->spa_format); + height = stream->spa_format.size.height; + width = stream->spa_format.size.width; + stride = width * 4; + + g_debug ("[RDP] Stream parameters changed. New surface size: [%u, %u]", + width, height); + + if (egl_thread) + sync_egl_thread (egl_thread); + release_all_buffers (stream); + grd_rdp_surface_reset (stream->rdp_surface); + + if (!grd_rdp_damage_detector_resize_surface (stream->rdp_surface->detector, + width, height) || + !grd_rdp_buffer_pool_resize_buffers (stream->buffer_pool, height, stride)) + { + stream->dequeuing_disallowed = TRUE; + grd_session_rdp_notify_error ( + stream->session_rdp, GRD_SESSION_RDP_ERROR_GRAPHICS_SUBSYSTEM_FAILED); + return; + } + + grd_rdp_surface_set_size (stream->rdp_surface, width, height); + g_signal_emit (stream, signals[VIDEO_RESIZED], 0, width, height); + stream->pending_resize = FALSE; + + pod_builder = SPA_POD_BUILDER_INIT (params_buffer, sizeof (params_buffer)); + + allowed_buffer_types = 1 << SPA_DATA_MemFd; + if (egl_thread && !hwaccel_nvidia) + allowed_buffer_types |= 1 << SPA_DATA_DmaBuf; + + params[0] = spa_pod_builder_add_object ( + &pod_builder, + SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers, + SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int (8, 2, 8), + SPA_PARAM_BUFFERS_dataType, SPA_POD_Int (allowed_buffer_types), + 0); + + params[1] = spa_pod_builder_add_object ( + &pod_builder, + SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta, + SPA_PARAM_META_type, SPA_POD_Id (SPA_META_Header), + SPA_PARAM_META_size, SPA_POD_Int (sizeof (struct spa_meta_header)), + 0); + + params[2] = spa_pod_builder_add_object ( + &pod_builder, + SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta, + SPA_PARAM_META_type, SPA_POD_Id (SPA_META_Cursor), + SPA_PARAM_META_size, SPA_POD_CHOICE_RANGE_Int (CURSOR_META_SIZE (384, 384), + CURSOR_META_SIZE (1, 1), + CURSOR_META_SIZE (384, 384)), + 0); + + pw_stream_update_params (stream->pipewire_stream, + params, G_N_ELEMENTS (params)); +} + +static void +handle_graphics_subsystem_failure (GrdRdpPipeWireStream *stream) +{ + stream->dequeuing_disallowed = TRUE; + stream->ignore_new_buffers = TRUE; + + grd_session_rdp_notify_error (stream->session_rdp, + GRD_SESSION_RDP_ERROR_GRAPHICS_SUBSYSTEM_FAILED); +} + +static void +on_stream_add_buffer (void *user_data, + struct pw_buffer *buffer) +{ + GrdRdpPipeWireStream *stream = user_data; + GrdRdpSurfaceRenderer *surface_renderer; + GrdRdpPwBuffer *rdp_pw_buffer; + g_autoptr (GError) error = NULL; + uint32_t drm_format; + + if (stream->ignore_new_buffers) + return; + + rdp_pw_buffer = grd_rdp_pw_buffer_new (stream->pipewire_stream, buffer, + &error); + if (!rdp_pw_buffer) + { + g_warning ("[RDP] Failed to add PipeWire buffer: %s", error->message); + handle_graphics_subsystem_failure (stream); + return; + } + + grd_get_spa_format_details (stream->spa_format.format, &drm_format, NULL); + + surface_renderer = grd_rdp_surface_get_surface_renderer (stream->rdp_surface); + if (!grd_rdp_surface_renderer_register_pw_buffer (surface_renderer, + rdp_pw_buffer, + drm_format, + stream->spa_format.modifier, + &error)) + { + g_warning ("[RDP] Failed to register PipeWire buffer: %s", error->message); + handle_graphics_subsystem_failure (stream); + return; + } + + g_hash_table_insert (stream->pipewire_buffers, buffer, rdp_pw_buffer); +} + +static void +on_stream_remove_buffer (void *user_data, + struct pw_buffer *buffer) +{ + GrdRdpPipeWireStream *stream = user_data; + GrdRdpSurfaceRenderer *surface_renderer; + GrdRdpPwBuffer *rdp_pw_buffer = NULL; + g_autoptr (GMutexLocker) locker = NULL; + + if (!g_hash_table_lookup_extended (stream->pipewire_buffers, buffer, + NULL, (gpointer *) &rdp_pw_buffer)) + return; + + locker = g_mutex_locker_new (&stream->dequeue_mutex); + grd_rdp_pw_buffer_ensure_unlocked (rdp_pw_buffer); + + surface_renderer = grd_rdp_surface_get_surface_renderer (stream->rdp_surface); + grd_rdp_surface_renderer_unregister_pw_buffer (surface_renderer, + rdp_pw_buffer); + + g_hash_table_remove (stream->pipewire_buffers, rdp_pw_buffer); +} + +static void +process_mouse_cursor_data (GrdRdpPipeWireStream *stream, + struct spa_buffer *buffer) +{ + struct spa_meta_cursor *spa_meta_cursor; + struct spa_meta_bitmap *spa_meta_bitmap; + GrdRdpCursorUpdate *cursor_update = NULL; + GrdPixelFormat format; + + spa_meta_cursor = spa_buffer_find_meta_data (buffer, SPA_META_Cursor, + sizeof *spa_meta_cursor); + + g_assert (spa_meta_cursor); + g_assert (spa_meta_cursor_is_valid (spa_meta_cursor)); + g_assert (spa_meta_cursor->bitmap_offset); + + spa_meta_bitmap = SPA_MEMBER (spa_meta_cursor, + spa_meta_cursor->bitmap_offset, + struct spa_meta_bitmap); + + if (spa_meta_bitmap && + spa_meta_bitmap->size.width > 0 && + spa_meta_bitmap->size.height > 0 && + grd_spa_pixel_format_to_grd_pixel_format (spa_meta_bitmap->format, + &format)) + { + uint8_t *buf; + + buf = SPA_MEMBER (spa_meta_bitmap, spa_meta_bitmap->offset, uint8_t); + + cursor_update = g_new0 (GrdRdpCursorUpdate, 1); + cursor_update->update_type = GRD_RDP_CURSOR_UPDATE_TYPE_NORMAL; + cursor_update->hotspot_x = spa_meta_cursor->hotspot.x; + cursor_update->hotspot_y = spa_meta_cursor->hotspot.y; + cursor_update->width = spa_meta_bitmap->size.width; + cursor_update->height = spa_meta_bitmap->size.height; + cursor_update->bitmap = + g_memdup2 (buf, spa_meta_bitmap->size.height * spa_meta_bitmap->stride); + } + else if (spa_meta_bitmap) + { + cursor_update = g_new0 (GrdRdpCursorUpdate, 1); + cursor_update->update_type = GRD_RDP_CURSOR_UPDATE_TYPE_HIDDEN; + } + + if (cursor_update) + { + GrdRdpCursorRenderer *cursor_renderer = + grd_session_rdp_get_cursor_renderer (stream->session_rdp); + + grd_rdp_cursor_renderer_submit_cursor_update (cursor_renderer, + cursor_update); + } +} + +static void +on_frame_ready (GrdRdpPipeWireStream *stream, + GrdRdpFrame *frame, + gboolean success, + gpointer user_data) +{ + struct pw_buffer *buffer = user_data; + GrdRdpSurface *rdp_surface = stream->rdp_surface; + GrdRdpSurfaceRenderer *surface_renderer; + + g_assert (frame); + g_assert (buffer); + + if (!success) + goto out; + + g_assert (frame->buffer); + + if (!stream->pending_resize) + { + GrdRdpSessionMetrics *session_metrics = + grd_session_rdp_get_session_metrics (stream->session_rdp); + + grd_rdp_session_metrics_notify_frame_reception (session_metrics, + rdp_surface); + } + + surface_renderer = grd_rdp_surface_get_surface_renderer (rdp_surface); + grd_rdp_surface_renderer_submit_legacy_buffer (surface_renderer, + g_steal_pointer (&frame->buffer)); +out: + pw_stream_queue_buffer (stream->pipewire_stream, buffer); + maybe_release_pipewire_buffer_lock (stream, buffer); + + g_clear_pointer (&frame, grd_rdp_frame_unref); +} + +static void +copy_frame_data (GrdRdpFrame *frame, + uint8_t *src_data, + int width, + int height, + int dst_stride, + int src_stride, + int bpp) +{ + GrdRdpLegacyBuffer *buffer = frame->buffer; + int y; + + for (y = 0; y < height; ++y) + { + memcpy (grd_rdp_legacy_buffer_get_local_data (buffer) + y * dst_stride, + ((uint8_t *) src_data) + y * src_stride, + width * 4); + } +} + +static gboolean +cuda_unmap_resource (gpointer user_data) +{ + UnmapBufferData *data = user_data; + + grd_rdp_legacy_buffer_unmap_cuda_resource (data->rdp_legacy_buffer); + + return TRUE; +} + +static void +unmap_cuda_resources (GrdEglThread *egl_thread, + GrdRdpLegacyBuffer *rdp_legacy_buffer) +{ + UnmapBufferData *data; + + data = g_new0 (UnmapBufferData, 1); + data->rdp_legacy_buffer = rdp_legacy_buffer; + + grd_egl_thread_run_custom_task (egl_thread, + cuda_unmap_resource, data, + NULL, data, g_free); +} + +static gboolean +cuda_allocate_buffer (gpointer user_data, + uint32_t pbo) +{ + AllocateBufferData *data = user_data; + GrdRdpLegacyBuffer *rdp_legacy_buffer = data->rdp_legacy_buffer; + + return grd_rdp_legacy_buffer_register_read_only_gl_buffer (rdp_legacy_buffer, pbo); +} + +static gboolean +cuda_map_resource (gpointer user_data) +{ + RealizeBufferData *data = user_data; + GrdRdpLegacyBuffer *rdp_legacy_buffer = data->rdp_legacy_buffer; + + return grd_rdp_legacy_buffer_map_cuda_resource (rdp_legacy_buffer); +} + +static void +on_framebuffer_ready (gboolean success, + gpointer user_data) +{ + GrdRdpFrame *frame = user_data; + + grd_rdp_frame_invoke_callback (frame, success); +} + +static void +process_frame_data (GrdRdpPipeWireStream *stream, + struct pw_buffer *pw_buffer) +{ + GrdSession *session = GRD_SESSION (stream->session_rdp); + GrdContext *context = grd_session_get_context (session); + GrdEglThread *egl_thread = grd_context_get_egl_thread (context); + GrdRdpServer *rdp_server = grd_session_rdp_get_server (stream->session_rdp); + GrdHwAccelNvidia *hwaccel_nvidia = + grd_rdp_server_get_hwaccel_nvidia (rdp_server); + struct spa_buffer *buffer = pw_buffer->buffer; + GrdRdpPwBuffer *rdp_pw_buffer = NULL; + g_autoptr (GrdRdpFrame) frame = NULL; + AllocateBufferData *allocate_buffer_data; + RealizeBufferData *realize_buffer_data; + GrdRdpLegacyBuffer *rdp_legacy_buffer; + uint32_t drm_format; + int bpp; + int width; + int height; + int32_t src_stride; + int dst_stride; + void *src_data; + uint32_t pbo; + uint8_t *data_to_upload; + + g_assert (hwaccel_nvidia); + g_assert (buffer->datas[0].type == SPA_DATA_MemFd); + g_assert (buffer->datas[0].chunk->size > 0); + + height = stream->spa_format.size.height; + width = stream->spa_format.size.width; + src_stride = buffer->datas[0].chunk->stride; + grd_get_spa_format_details (stream->spa_format.format, + &drm_format, &bpp); + + frame = grd_rdp_frame_new (stream, on_frame_ready, pw_buffer); + + acquire_pipewire_buffer_lock (stream, pw_buffer); + if (!g_hash_table_lookup_extended (stream->pipewire_buffers, pw_buffer, + NULL, (gpointer *) &rdp_pw_buffer)) + g_assert_not_reached (); + + src_data = grd_rdp_pw_buffer_get_mapped_data (rdp_pw_buffer, &src_stride); + + frame->buffer = grd_rdp_buffer_pool_acquire (stream->buffer_pool); + if (!frame->buffer) + { + grd_session_rdp_notify_error (stream->session_rdp, + GRD_SESSION_RDP_ERROR_GRAPHICS_SUBSYSTEM_FAILED); + grd_rdp_frame_invoke_callback (g_steal_pointer (&frame), FALSE); + return; + } + rdp_legacy_buffer = frame->buffer; + dst_stride = grd_rdp_legacy_buffer_get_stride (rdp_legacy_buffer); + pbo = grd_rdp_legacy_buffer_get_pbo (rdp_legacy_buffer); + + if (stream->rdp_surface->needs_no_local_data && + src_stride == dst_stride) + { + data_to_upload = src_data; + } + else + { + copy_frame_data (frame, + src_data, + width, height, + dst_stride, + src_stride, + bpp); + + data_to_upload = grd_rdp_legacy_buffer_get_local_data (rdp_legacy_buffer); + } + + unmap_cuda_resources (egl_thread, rdp_legacy_buffer); + + allocate_buffer_data = g_new0 (AllocateBufferData, 1); + allocate_buffer_data->rdp_legacy_buffer = rdp_legacy_buffer; + + realize_buffer_data = g_new0 (RealizeBufferData, 1); + realize_buffer_data->rdp_legacy_buffer = rdp_legacy_buffer; + + grd_egl_thread_upload (egl_thread, + stream->egl_slot, + pbo, + height, + dst_stride, + data_to_upload, + cuda_allocate_buffer, + allocate_buffer_data, + g_free, + cuda_map_resource, + realize_buffer_data, + g_free, + on_framebuffer_ready, + grd_rdp_frame_ref (g_steal_pointer (&frame)), + (GDestroyNotify) grd_rdp_frame_unref); +} + +static void +submit_framebuffer (GrdRdpPipeWireStream *stream, + struct pw_buffer *pw_buffer) +{ + GrdRdpSurface *rdp_surface = stream->rdp_surface; + GrdRdpSurfaceRenderer *surface_renderer = + grd_rdp_surface_get_surface_renderer (stream->rdp_surface); + GrdRdpPwBuffer *rdp_pw_buffer = NULL; + + if (!stream->pending_resize) + { + GrdRdpSessionMetrics *session_metrics = + grd_session_rdp_get_session_metrics (stream->session_rdp); + + grd_rdp_session_metrics_notify_frame_reception (session_metrics, + rdp_surface); + } + + if (!g_hash_table_lookup_extended (stream->pipewire_buffers, pw_buffer, + NULL, (gpointer *) &rdp_pw_buffer)) + g_assert_not_reached (); + + grd_rdp_surface_renderer_submit_buffer (surface_renderer, rdp_pw_buffer); +} + +static void +on_stream_process (void *user_data) +{ + GrdRdpPipeWireStream *stream = GRD_RDP_PIPEWIRE_STREAM (user_data); + GrdRdpServer *rdp_server = grd_session_rdp_get_server (stream->session_rdp); + GrdHwAccelNvidia *hwaccel_nvidia = + grd_rdp_server_get_hwaccel_nvidia (rdp_server); + g_autoptr (GMutexLocker) locker = NULL; + struct pw_buffer *last_pointer_buffer = NULL; + struct pw_buffer *last_frame_buffer = NULL; + struct pw_buffer *next_buffer; + + locker = g_mutex_locker_new (&stream->dequeue_mutex); + if (stream->dequeuing_disallowed) + return; + + while ((next_buffer = pw_stream_dequeue_buffer (stream->pipewire_stream))) + { + struct spa_meta_header *spa_meta_header; + + spa_meta_header = spa_buffer_find_meta_data (next_buffer->buffer, + SPA_META_Header, + sizeof (struct spa_meta_header)); + if (spa_meta_header && + spa_meta_header->flags & SPA_META_HEADER_FLAG_CORRUPTED) + { + pw_stream_queue_buffer (stream->pipewire_stream, next_buffer); + continue; + } + + if (grd_pipewire_buffer_has_pointer_bitmap (next_buffer)) + { + if (last_pointer_buffer == last_frame_buffer) + last_pointer_buffer = NULL; + + if (last_pointer_buffer) + pw_stream_queue_buffer (stream->pipewire_stream, last_pointer_buffer); + last_pointer_buffer = next_buffer; + } + if (grd_pipewire_buffer_has_frame_data (next_buffer)) + { + if (last_pointer_buffer == last_frame_buffer) + last_frame_buffer = NULL; + + if (last_frame_buffer) + pw_stream_queue_buffer (stream->pipewire_stream, last_frame_buffer); + last_frame_buffer = next_buffer; + } + + if (next_buffer != last_pointer_buffer && + next_buffer != last_frame_buffer) + pw_stream_queue_buffer (stream->pipewire_stream, next_buffer); + } + if (!last_pointer_buffer && !last_frame_buffer) + return; + + if (last_pointer_buffer) + { + process_mouse_cursor_data (stream, last_pointer_buffer->buffer); + if (last_pointer_buffer != last_frame_buffer) + pw_stream_queue_buffer (stream->pipewire_stream, last_pointer_buffer); + } + if (!last_frame_buffer) + return; + + if (hwaccel_nvidia) + process_frame_data (stream, last_frame_buffer); + else + submit_framebuffer (stream, last_frame_buffer); +} + +static const struct pw_stream_events stream_events = { + PW_VERSION_STREAM_EVENTS, + .state_changed = on_stream_state_changed, + .param_changed = on_stream_param_changed, + .add_buffer = on_stream_add_buffer, + .remove_buffer = on_stream_remove_buffer, + .process = on_stream_process, +}; + +static gboolean +connect_to_stream (GrdRdpPipeWireStream *stream, + const GrdRdpVirtualMonitor *virtual_monitor, + GError **error) +{ + struct pw_stream *pipewire_stream; + uint8_t params_buffer[1024]; + struct spa_pod_builder pod_builder; + const struct spa_pod *params[MAX_PW_PARAMS] = {}; + uint32_t n_params = 0; + int ret; + + pipewire_stream = pw_stream_new (stream->pipewire_core, + "grd-rdp-pipewire-stream", + NULL); + + pod_builder = SPA_POD_BUILDER_INIT (params_buffer, sizeof (params_buffer)); + + n_params += add_format_params (stream, virtual_monitor, &pod_builder, + ¶ms[n_params], MAX_PW_PARAMS - n_params); + if (virtual_monitor) + { + n_params += add_tag_params (stream, virtual_monitor, &pod_builder, + ¶ms[n_params], MAX_PW_PARAMS - n_params); + } + + stream->pipewire_stream = pipewire_stream; + + pw_stream_add_listener (pipewire_stream, + &stream->pipewire_stream_listener, + &stream_events, + stream); + + g_assert (n_params > 0); + ret = pw_stream_connect (stream->pipewire_stream, + PW_DIRECTION_INPUT, + stream->src_node_id, + (PW_STREAM_FLAG_RT_PROCESS | + PW_STREAM_FLAG_AUTOCONNECT), + params, n_params); + if (ret < 0) + { + g_set_error_literal (error, G_IO_ERROR, g_io_error_from_errno (-ret), + strerror (-ret)); + return FALSE; + } + + return TRUE; +} + +static void +on_core_error (void *user_data, + uint32_t id, + int seq, + int res, + const char *message) +{ + GrdRdpPipeWireStream *stream = GRD_RDP_PIPEWIRE_STREAM (user_data); + + g_warning ("PipeWire core error: id:%u %s", id, message); + + if (id == PW_ID_CORE && res == -EPIPE) + g_signal_emit (stream, signals[ERROR], 0); +} + +static const struct pw_core_events core_events = { + PW_VERSION_CORE_EVENTS, + .error = on_core_error, +}; + +static void +registry_event_global_remove (void *user_data, + uint32_t id) +{ + GrdRdpPipeWireStream *stream = user_data; + + if (id != stream->src_node_id) + return; + + g_debug ("[RDP] PipeWire stream with node id %u closed", id); + g_signal_emit (stream, signals[CLOSED], 0); +} + +static const struct pw_registry_events registry_events = +{ + .version = PW_VERSION_REGISTRY_EVENTS, + .global_remove = registry_event_global_remove, +}; + +GrdRdpPipeWireStream * +grd_rdp_pipewire_stream_new (GrdSessionRdp *session_rdp, + GrdRdpSurface *rdp_surface, + const GrdRdpVirtualMonitor *virtual_monitor, + uint32_t src_node_id, + GError **error) +{ + GrdSession *session = GRD_SESSION (session_rdp); + GrdContext *context = grd_session_get_context (session); + GrdEglThread *egl_thread = grd_context_get_egl_thread (context); + GrdRdpServer *rdp_server = grd_session_rdp_get_server (session_rdp); + GrdHwAccelNvidia *hwaccel_nvidia = + grd_rdp_server_get_hwaccel_nvidia (rdp_server); + g_autoptr (GrdRdpPipeWireStream) stream = NULL; + GrdPipeWireSource *pipewire_source; + + stream = g_object_new (GRD_TYPE_RDP_PIPEWIRE_STREAM, NULL); + stream->session_rdp = session_rdp; + stream->rdp_surface = rdp_surface; + stream->src_node_id = src_node_id; + + stream->buffer_pool = grd_rdp_buffer_pool_new (rdp_surface, + DEFAULT_BUFFER_POOL_SIZE); + + if (egl_thread && !hwaccel_nvidia) + stream->egl_slot = grd_egl_thread_acquire_slot (egl_thread); + + pw_init (NULL, NULL); + + pipewire_source = grd_attached_pipewire_source_new ("RDP", error); + if (!pipewire_source) + return NULL; + + stream->pipewire_source = (GSource *) pipewire_source; + + stream->pipewire_context = pw_context_new (pipewire_source->pipewire_loop, + NULL, 0); + if (!stream->pipewire_context) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create PipeWire context"); + return NULL; + } + + stream->pipewire_core = pw_context_connect (stream->pipewire_context, NULL, 0); + if (!stream->pipewire_core) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to connect PipeWire context"); + return NULL; + } + + pw_core_add_listener (stream->pipewire_core, + &stream->pipewire_core_listener, + &core_events, + stream); + + stream->pipewire_registry = pw_core_get_registry (stream->pipewire_core, + PW_VERSION_REGISTRY, 0); + if (!stream->pipewire_registry) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to retrieve PipeWire registry"); + return NULL; + } + + pw_registry_add_listener (stream->pipewire_registry, + &stream->pipewire_registry_listener, + ®istry_events, stream); + + if (!connect_to_stream (stream, virtual_monitor, error)) + return NULL; + + return g_steal_pointer (&stream); +} + +static void +grd_rdp_pipewire_stream_finalize (GObject *object) +{ + GrdRdpPipeWireStream *stream = GRD_RDP_PIPEWIRE_STREAM (object); + GrdSession *session = GRD_SESSION (stream->session_rdp); + GrdContext *context = grd_session_get_context (session); + GrdEglThread *egl_thread; + + g_mutex_lock (&stream->dequeue_mutex); + stream->dequeuing_disallowed = TRUE; + g_mutex_unlock (&stream->dequeue_mutex); + + egl_thread = grd_context_get_egl_thread (context); + if (egl_thread && stream->pipewire_stream) + sync_egl_thread (egl_thread); + + g_clear_pointer (&stream->pipewire_stream, pw_stream_destroy); + + if (stream->pipewire_registry) + { + spa_hook_remove (&stream->pipewire_registry_listener); + pw_proxy_destroy ((struct pw_proxy *) stream->pipewire_registry); + stream->pipewire_registry = NULL; + } + + g_clear_pointer (&stream->pipewire_core, pw_core_disconnect); + g_clear_pointer (&stream->pipewire_context, pw_context_destroy); + if (stream->pipewire_source) + { + g_source_destroy (stream->pipewire_source); + g_clear_pointer (&stream->pipewire_source, g_source_unref); + } + + grd_rdp_damage_detector_invalidate_surface (stream->rdp_surface->detector); + + release_all_buffers (stream); + g_clear_object (&stream->buffer_pool); + + g_mutex_clear (&stream->dequeue_mutex); + + g_clear_pointer (&stream->pipewire_buffers, g_hash_table_unref); + + pw_deinit (); + + if (egl_thread) + grd_egl_thread_release_slot (egl_thread, stream->egl_slot); + + G_OBJECT_CLASS (grd_rdp_pipewire_stream_parent_class)->finalize (object); +} + +static void +grd_rdp_pipewire_stream_init (GrdRdpPipeWireStream *stream) +{ + stream->pending_resize = TRUE; + + stream->pipewire_buffers = + g_hash_table_new_full (NULL, NULL, + NULL, (GDestroyNotify) grd_rdp_pw_buffer_free); + + g_mutex_init (&stream->dequeue_mutex); +} + +static void +grd_rdp_pipewire_stream_class_init (GrdRdpPipeWireStreamClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = grd_rdp_pipewire_stream_finalize; + + signals[ERROR] = g_signal_new ("error", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, 0); + signals[CLOSED] = g_signal_new ("closed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, 0); + signals[VIDEO_RESIZED] = g_signal_new ("video-resized", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, 2, + G_TYPE_UINT, G_TYPE_UINT); +} diff --git a/grd-rdp-pipewire-stream.h b/grd-rdp-pipewire-stream.h new file mode 100644 index 0000000..daa9f89 --- /dev/null +++ b/grd-rdp-pipewire-stream.h @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2020 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include +#include + +#include "grd-rdp-monitor-config.h" +#include "grd-session-rdp.h" + +#define GRD_TYPE_RDP_PIPEWIRE_STREAM grd_rdp_pipewire_stream_get_type () +G_DECLARE_FINAL_TYPE (GrdRdpPipeWireStream, grd_rdp_pipewire_stream, + GRD, RDP_PIPEWIRE_STREAM, + GObject) + +GrdRdpPipeWireStream *grd_rdp_pipewire_stream_new (GrdSessionRdp *session_rdp, + GrdRdpSurface *rdp_surface, + const GrdRdpVirtualMonitor *virtual_monitor, + uint32_t src_node_id, + GError **error); + +void grd_rdp_pipewire_stream_resize (GrdRdpPipeWireStream *stream, + GrdRdpVirtualMonitor *virtual_monitor); diff --git a/grd-rdp-private.h b/grd-rdp-private.h new file mode 100644 index 0000000..d827a8a --- /dev/null +++ b/grd-rdp-private.h @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2021 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include + +#include "grd-types.h" + +typedef struct _RdpPeerContext +{ + rdpContext rdp_context; + + GrdSessionRdp *session_rdp; + + RFX_CONTEXT *rfx_context; + wStream *encode_stream; + + GrdRdpNetworkAutodetection *network_autodetection; + + /* Virtual Channel Manager */ + HANDLE vcm; + + GrdRdpDvcHandler *dvc_handler; + + GMutex channel_mutex; + + GrdClipboardRdp *clipboard_rdp; + GrdRdpDvcAudioInput *audio_input; + GrdRdpDvcAudioPlayback *audio_playback; + GrdRdpDvcCameraEnumerator *camera_enumerator; + GrdRdpDvcDisplayControl *display_control; + GrdRdpDvcGraphicsPipeline *graphics_pipeline; + GrdRdpDvcInput *input; + GrdRdpDvcTelemetry *telemetry; +} RdpPeerContext; diff --git a/grd-rdp-pw-buffer.c b/grd-rdp-pw-buffer.c new file mode 100644 index 0000000..38db280 --- /dev/null +++ b/grd-rdp-pw-buffer.c @@ -0,0 +1,233 @@ +/* + * Copyright (C) 2024 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-rdp-pw-buffer.h" + +#include + +typedef struct +{ + uint8_t *map; + size_t map_size; + + uint8_t *data; +} GrdRdpMemFd; + +struct _GrdRdpPwBuffer +{ + struct pw_stream *pw_stream; + struct pw_buffer *pw_buffer; + + GrdRdpBufferType buffer_type; + GrdRdpPwBufferDmaBufInfo *dma_buf_info; + + GMutex buffer_mutex; + gboolean is_locked; + + GrdRdpMemFd mem_fd; +}; + +GrdRdpBufferType +grd_rdp_pw_buffer_get_buffer_type (GrdRdpPwBuffer *rdp_pw_buffer) +{ + return rdp_pw_buffer->buffer_type; +} + +const GrdRdpPwBufferDmaBufInfo * +grd_rdp_pw_buffer_get_dma_buf_info (GrdRdpPwBuffer *rdp_pw_buffer) +{ + return rdp_pw_buffer->dma_buf_info; +} + +uint8_t * +grd_rdp_pw_buffer_get_mapped_data (GrdRdpPwBuffer *rdp_pw_buffer, + int32_t *stride) +{ + struct pw_buffer *pw_buffer = rdp_pw_buffer->pw_buffer; + struct spa_buffer *spa_buffer = pw_buffer->buffer; + GrdRdpMemFd *mem_fd = &rdp_pw_buffer->mem_fd; + + g_assert (mem_fd->data); + + *stride = spa_buffer->datas[0].chunk->stride; + + return mem_fd->data; +} + +void +grd_rdp_pw_buffer_queue_pw_buffer (GrdRdpPwBuffer *rdp_pw_buffer) +{ + pw_stream_queue_buffer (rdp_pw_buffer->pw_stream, rdp_pw_buffer->pw_buffer); +} + +void +grd_rdp_pw_buffer_ensure_unlocked (GrdRdpPwBuffer *rdp_pw_buffer) +{ + g_mutex_lock (&rdp_pw_buffer->buffer_mutex); + g_mutex_unlock (&rdp_pw_buffer->buffer_mutex); +} + +void +grd_rdp_pw_buffer_acquire_lock (GrdRdpPwBuffer *rdp_pw_buffer) +{ + g_mutex_lock (&rdp_pw_buffer->buffer_mutex); + g_assert (!rdp_pw_buffer->is_locked); + rdp_pw_buffer->is_locked = TRUE; +} + +void +grd_rdp_pw_buffer_maybe_release_lock (GrdRdpPwBuffer *rdp_pw_buffer) +{ + if (!rdp_pw_buffer->is_locked) + return; + + rdp_pw_buffer->is_locked = FALSE; + g_mutex_unlock (&rdp_pw_buffer->buffer_mutex); +} + +static enum spa_data_type +get_pw_buffer_type (struct pw_buffer *pw_buffer) +{ + struct spa_buffer *spa_buffer = pw_buffer->buffer; + + return spa_buffer->datas[0].type; +} + +static gboolean +is_supported_dma_buf_buffer (struct pw_buffer *pw_buffer, + GError **error) +{ + struct spa_buffer *spa_buffer = pw_buffer->buffer; + uint32_t n_planes = spa_buffer->n_datas; + + if (n_planes != 1) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Unsupported dma-buf: Expected exactly 1 plane for format"); + return FALSE; + } + + return TRUE; +} + +static GrdRdpPwBufferDmaBufInfo * +dma_buf_info_new (struct pw_buffer *pw_buffer) +{ + struct spa_buffer *spa_buffer = pw_buffer->buffer; + GrdRdpPwBufferDmaBufInfo *dma_buf_info; + + g_assert (spa_buffer->n_datas == 1); + + dma_buf_info = g_new0 (GrdRdpPwBufferDmaBufInfo, 1); + dma_buf_info->fd = spa_buffer->datas[0].fd; + dma_buf_info->offset = spa_buffer->datas[0].chunk->offset; + dma_buf_info->stride = spa_buffer->datas[0].chunk->stride; + + return dma_buf_info; +} + +static gboolean +try_mmap_buffer (GrdRdpPwBuffer *rdp_pw_buffer, + GError **error) +{ + struct pw_buffer *pw_buffer = rdp_pw_buffer->pw_buffer; + struct spa_buffer *spa_buffer = pw_buffer->buffer; + GrdRdpMemFd *mem_fd = &rdp_pw_buffer->mem_fd; + + mem_fd->map_size = spa_buffer->datas[0].maxsize + + spa_buffer->datas[0].mapoffset; + mem_fd->map = mmap (NULL, mem_fd->map_size, PROT_READ, MAP_PRIVATE, + spa_buffer->datas[0].fd, 0); + if (mem_fd->map == MAP_FAILED) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to mmap buffer: %s", g_strerror (errno)); + return FALSE; + } + mem_fd->data = SPA_MEMBER (mem_fd->map, spa_buffer->datas[0].mapoffset, + uint8_t); + + return TRUE; +} + +GrdRdpPwBuffer * +grd_rdp_pw_buffer_new (struct pw_stream *pw_stream, + struct pw_buffer *pw_buffer, + GError **error) +{ + g_autoptr (GrdRdpPwBuffer) rdp_pw_buffer = NULL; + + rdp_pw_buffer = g_new0 (GrdRdpPwBuffer, 1); + rdp_pw_buffer->pw_stream = pw_stream; + rdp_pw_buffer->pw_buffer = pw_buffer; + + g_mutex_init (&rdp_pw_buffer->buffer_mutex); + + switch (get_pw_buffer_type (pw_buffer)) + { + case SPA_DATA_MemFd: + rdp_pw_buffer->buffer_type = GRD_RDP_BUFFER_TYPE_MEM_FD; + break; + case SPA_DATA_DmaBuf: + rdp_pw_buffer->buffer_type = GRD_RDP_BUFFER_TYPE_DMA_BUF; + break; + default: + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "PipeWire buffer contains invalid data type 0x%08X", + get_pw_buffer_type (pw_buffer)); + return NULL; + } + + if (get_pw_buffer_type (pw_buffer) == SPA_DATA_DmaBuf && + !is_supported_dma_buf_buffer (pw_buffer, error)) + return NULL; + + if (get_pw_buffer_type (pw_buffer) == SPA_DATA_DmaBuf) + rdp_pw_buffer->dma_buf_info = dma_buf_info_new (pw_buffer); + + if (get_pw_buffer_type (pw_buffer) == SPA_DATA_MemFd && + !try_mmap_buffer (rdp_pw_buffer, error)) + return NULL; + + return g_steal_pointer (&rdp_pw_buffer); +} + +static void +maybe_unmap_buffer (GrdRdpPwBuffer* rdp_pw_buffer) +{ + GrdRdpMemFd *mem_fd = &rdp_pw_buffer->mem_fd; + + if (!mem_fd->data) + return; + + munmap (mem_fd->map, mem_fd->map_size); + mem_fd->data = NULL; +} + +void +grd_rdp_pw_buffer_free (GrdRdpPwBuffer *rdp_pw_buffer) +{ + maybe_unmap_buffer (rdp_pw_buffer); + g_clear_pointer (&rdp_pw_buffer->dma_buf_info, g_free); + + g_mutex_clear (&rdp_pw_buffer->buffer_mutex); + g_free (rdp_pw_buffer); +} diff --git a/grd-rdp-pw-buffer.h b/grd-rdp-pw-buffer.h new file mode 100644 index 0000000..603de90 --- /dev/null +++ b/grd-rdp-pw-buffer.h @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2024 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include +#include + +#include "grd-rdp-buffer-info.h" +#include "grd-types.h" + +typedef struct +{ + int fd; + uint32_t offset; + int32_t stride; +} GrdRdpPwBufferDmaBufInfo; + +GrdRdpPwBuffer *grd_rdp_pw_buffer_new (struct pw_stream *pw_stream, + struct pw_buffer *pw_buffer, + GError **error); + +void grd_rdp_pw_buffer_free (GrdRdpPwBuffer *rdp_pw_buffer); + +GrdRdpBufferType grd_rdp_pw_buffer_get_buffer_type (GrdRdpPwBuffer *rdp_pw_buffer); + +const GrdRdpPwBufferDmaBufInfo *grd_rdp_pw_buffer_get_dma_buf_info (GrdRdpPwBuffer *rdp_pw_buffer); + +uint8_t *grd_rdp_pw_buffer_get_mapped_data (GrdRdpPwBuffer *rdp_pw_buffer, + int32_t *stride); + +void grd_rdp_pw_buffer_queue_pw_buffer (GrdRdpPwBuffer *rdp_pw_buffer); + +void grd_rdp_pw_buffer_ensure_unlocked (GrdRdpPwBuffer *rdp_pw_buffer); + +void grd_rdp_pw_buffer_acquire_lock (GrdRdpPwBuffer *rdp_pw_buffer); + +void grd_rdp_pw_buffer_maybe_release_lock (GrdRdpPwBuffer *rdp_pw_buffer); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (GrdRdpPwBuffer, grd_rdp_pw_buffer_free) diff --git a/grd-rdp-render-context.c b/grd-rdp-render-context.c new file mode 100644 index 0000000..d463e3a --- /dev/null +++ b/grd-rdp-render-context.c @@ -0,0 +1,749 @@ +/* + * Copyright (C) 2023 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-rdp-render-context.h" + +#include + +#include "grd-context.h" +#include "grd-debug.h" +#include "grd-encode-session.h" +#include "grd-encode-session-ca-sw.h" +#include "grd-hwaccel-vaapi.h" +#include "grd-image-view.h" +#include "grd-rdp-buffer-info.h" +#include "grd-rdp-damage-detector.h" +#include "grd-rdp-dvc-graphics-pipeline.h" +#include "grd-rdp-frame.h" +#include "grd-rdp-gfx-frame-controller.h" +#include "grd-rdp-gfx-framerate-log.h" +#include "grd-rdp-gfx-surface.h" +#include "grd-rdp-render-state.h" +#include "grd-rdp-renderer.h" +#include "grd-rdp-server.h" +#include "grd-rdp-surface.h" +#include "grd-rdp-surface-renderer.h" +#include "grd-rdp-view-creator-avc.h" +#include "grd-rdp-view-creator-gen-gl.h" +#include "grd-rdp-view-creator-gen-sw.h" +#include "grd-utils.h" +#include "grd-vk-device.h" + +#define STATE_TILE_WIDTH 64 +#define STATE_TILE_HEIGHT 64 + +struct _GrdRdpRenderContext +{ + GObject parent; + + GrdRdpRenderer *renderer; + + GrdRdpCodec codec; + + GrdRdpGfxSurface *gfx_surface; + GrdRdpViewCreator *view_creator; + GrdEncodeSession *encode_session; + + GHashTable *image_views; + GHashTable *acquired_image_views; + + GrdImageView *last_acquired_image_view; + + gboolean delay_view_finalization; + + uint32_t *chroma_state_buffer; + uint32_t state_buffer_length; +}; + +G_DEFINE_TYPE (GrdRdpRenderContext, grd_rdp_render_context, G_TYPE_OBJECT) + +GrdRdpRenderer * +grd_rdp_render_context_get_renderer (GrdRdpRenderContext *render_context) +{ + return render_context->renderer; +} + +GrdRdpCodec +grd_rdp_render_context_get_codec (GrdRdpRenderContext *render_context) +{ + return render_context->codec; +} + +GrdRdpGfxSurface * +grd_rdp_render_context_get_gfx_surface (GrdRdpRenderContext *render_context) +{ + return render_context->gfx_surface; +} + +GrdRdpViewCreator * +grd_rdp_render_context_get_view_creator (GrdRdpRenderContext *render_context) +{ + return render_context->view_creator; +} + +GrdEncodeSession * +grd_rdp_render_context_get_encode_session (GrdRdpRenderContext *render_context) +{ + return render_context->encode_session; +} + +gboolean +grd_rdp_render_context_must_delay_view_finalization (GrdRdpRenderContext *render_context) +{ + return render_context->delay_view_finalization; +} + +gboolean +grd_rdp_render_context_should_avoid_dual_frame (GrdRdpRenderContext *render_context) +{ + GrdRdpGfxSurface *gfx_surface = render_context->gfx_surface; + GrdRdpGfxFrameController *frame_controller; + GrdRdpGfxFramerateLog *framerate_log; + + g_assert (gfx_surface); + + frame_controller = grd_rdp_gfx_surface_get_frame_controller (gfx_surface); + framerate_log = grd_rdp_gfx_frame_controller_get_framerate_log (frame_controller); + + return grd_rdp_gfx_framerate_log_should_avoid_dual_frame (framerate_log); +} + +static void +notify_frame_upgrade_state (GrdRdpRenderContext *render_context, + gboolean can_upgrade_frame) +{ + GrdRdpGfxSurface *gfx_surface = render_context->gfx_surface; + GrdRdpSurface *rdp_surface; + GrdRdpSurfaceRenderer *surface_renderer; + + g_assert (gfx_surface); + rdp_surface = grd_rdp_gfx_surface_get_rdp_surface (gfx_surface); + surface_renderer = grd_rdp_surface_get_surface_renderer (rdp_surface); + + grd_rdp_surface_renderer_notify_frame_upgrade_state (surface_renderer, + can_upgrade_frame); +} + +static void +update_frame_upgrade_state (GrdRdpRenderContext *render_context) +{ + gboolean can_upgrade_frame; + + can_upgrade_frame = + !!render_context->chroma_state_buffer && + g_hash_table_size (render_context->acquired_image_views) == 0; + + notify_frame_upgrade_state (render_context, can_upgrade_frame); +} + +GrdImageView * +grd_rdp_render_context_acquire_image_view (GrdRdpRenderContext *render_context) +{ + GrdImageView *image_view = NULL; + GHashTableIter iter; + + g_hash_table_iter_init (&iter, render_context->image_views); + while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &image_view)) + { + if (g_hash_table_contains (render_context->acquired_image_views, + image_view)) + continue; + + g_hash_table_add (render_context->acquired_image_views, image_view); + render_context->last_acquired_image_view = image_view; + + update_frame_upgrade_state (render_context); + + return image_view; + } + + g_assert_not_reached (); + return NULL; +} + +void +grd_rdp_render_context_release_image_view (GrdRdpRenderContext *render_context, + GrdImageView *image_view) +{ + if (!g_hash_table_remove (render_context->acquired_image_views, image_view)) + g_assert_not_reached (); + + grd_image_view_notify_image_view_release (image_view); + update_frame_upgrade_state (render_context); +} + +static void +maybe_downgrade_view_type (GrdRdpRenderContext *render_context, + GrdRdpFrame *rdp_frame) +{ + GrdRdpFrameViewType view_type; + + view_type = grd_rdp_frame_get_avc_view_type (rdp_frame); + if (view_type != GRD_RDP_FRAME_VIEW_TYPE_DUAL) + return; + + if (grd_rdp_render_context_should_avoid_dual_frame (render_context)) + grd_rdp_frame_set_avc_view_type (rdp_frame, GRD_RDP_FRAME_VIEW_TYPE_MAIN); +} + +static gboolean +is_auxiliary_view_needed (GrdRdpRenderState *render_state) +{ + uint32_t *chroma_state_buffer = + grd_rdp_render_state_get_chroma_state_buffer (render_state); + uint32_t state_buffer_length = + grd_rdp_render_state_get_state_buffer_length (render_state); + uint32_t i; + + for (i = 0; i < state_buffer_length; ++i) + { + if (chroma_state_buffer[i] != 0) + return TRUE; + } + + return FALSE; +} + +static void +update_dual_frame (GrdRdpRenderContext *render_context, + GrdRdpFrame *rdp_frame, + GrdRdpRenderState *render_state) +{ + uint32_t *damage_buffer = + grd_rdp_render_state_get_damage_buffer (render_state); + uint32_t state_buffer_length = + grd_rdp_render_state_get_state_buffer_length (render_state); + uint32_t i; + + if (!render_context->chroma_state_buffer) + { + if (!is_auxiliary_view_needed (render_state)) + { + grd_rdp_frame_set_avc_view_type (rdp_frame, + GRD_RDP_FRAME_VIEW_TYPE_MAIN); + } + + return; + } + + g_assert (render_context->state_buffer_length == state_buffer_length); + + for (i = 0; i < render_context->state_buffer_length; ++i) + { + if (render_context->chroma_state_buffer[i] != 0) + damage_buffer[i] = 1; + } + g_clear_pointer (&render_context->chroma_state_buffer, g_free); +} + +static void +update_main_frame (GrdRdpRenderContext *render_context, + GrdRdpFrame *rdp_frame, + GrdRdpRenderState *render_state) +{ + uint32_t *damage_buffer = + grd_rdp_render_state_get_damage_buffer (render_state); + uint32_t *chroma_state_buffer = + grd_rdp_render_state_get_chroma_state_buffer (render_state); + uint32_t state_buffer_length = + grd_rdp_render_state_get_state_buffer_length (render_state); + gboolean pending_auxiliary_view = FALSE; + uint32_t i; + + if (!render_context->chroma_state_buffer) + { + if (is_auxiliary_view_needed (render_state)) + { + render_context->chroma_state_buffer = + g_memdup2 (chroma_state_buffer, + state_buffer_length * sizeof (uint32_t)); + render_context->state_buffer_length = state_buffer_length; + } + + return; + } + + g_assert (render_context->state_buffer_length == state_buffer_length); + + for (i = 0; i < render_context->state_buffer_length; ++i) + { + if (chroma_state_buffer[i] != 0) + render_context->chroma_state_buffer[i] = 1; + else if (damage_buffer[i] != 0) + render_context->chroma_state_buffer[i] = 0; + + if (render_context->chroma_state_buffer[i] != 0) + pending_auxiliary_view = TRUE; + } + if (!pending_auxiliary_view) + g_clear_pointer (&render_context->chroma_state_buffer, g_free); +} + +static void +update_avc444_render_state (GrdRdpRenderContext *render_context, + GrdRdpFrame *rdp_frame, + GrdRdpRenderState *render_state) +{ + maybe_downgrade_view_type (render_context, rdp_frame); + + switch (grd_rdp_frame_get_avc_view_type (rdp_frame)) + { + case GRD_RDP_FRAME_VIEW_TYPE_DUAL: + update_dual_frame (render_context, rdp_frame, render_state); + break; + case GRD_RDP_FRAME_VIEW_TYPE_MAIN: + update_main_frame (render_context, rdp_frame, render_state); + break; + case GRD_RDP_FRAME_VIEW_TYPE_AUX: + g_assert_not_reached (); + break; + } + update_frame_upgrade_state (render_context); +} + +static cairo_region_t * +create_damage_region (GrdRdpRenderContext *render_context, + GrdRdpRenderState *render_state) +{ + GrdRdpGfxSurface *gfx_surface = render_context->gfx_surface; + uint32_t *damage_buffer = + grd_rdp_render_state_get_damage_buffer (render_state); + GrdRdpSurface *rdp_surface; + uint32_t surface_width; + uint32_t surface_height; + uint32_t state_buffer_width; + uint32_t state_buffer_height; + uint32_t state_buffer_stride; + cairo_region_t *damage_region; + uint32_t x, y; + + g_assert (gfx_surface); + rdp_surface = grd_rdp_gfx_surface_get_rdp_surface (gfx_surface); + + surface_width = grd_rdp_surface_get_width (rdp_surface); + surface_height = grd_rdp_surface_get_height (rdp_surface); + + state_buffer_width = grd_get_aligned_size (surface_width, 64) / 64; + state_buffer_height = grd_get_aligned_size (surface_height, 64) / 64; + state_buffer_stride = state_buffer_width; + + damage_region = cairo_region_create (); + g_assert (cairo_region_status (damage_region) == CAIRO_STATUS_SUCCESS); + + for (y = 0; y < state_buffer_height; ++y) + { + for (x = 0; x < state_buffer_width; ++x) + { + cairo_rectangle_int_t tile = {}; + uint32_t target_pos; + + target_pos = y * state_buffer_stride + x; + if (damage_buffer[target_pos] == 0) + continue; + + tile.x = x * STATE_TILE_WIDTH; + tile.y = y * STATE_TILE_HEIGHT; + tile.width = surface_width - tile.x < STATE_TILE_WIDTH ? + surface_width - tile.x : STATE_TILE_WIDTH; + tile.height = surface_height - tile.y < STATE_TILE_HEIGHT ? + surface_height - tile.y : STATE_TILE_HEIGHT; + + cairo_region_union_rectangle (damage_region, &tile); + } + } + + return damage_region; +} + +void +grd_rdp_render_context_update_frame_state (GrdRdpRenderContext *render_context, + GrdRdpFrame *rdp_frame, + GrdRdpRenderState *render_state) +{ + cairo_region_t *damage_region; + + switch (render_context->codec) + { + case GRD_RDP_CODEC_CAPROGRESSIVE: + case GRD_RDP_CODEC_AVC420: + break; + case GRD_RDP_CODEC_AVC444v2: + update_avc444_render_state (render_context, rdp_frame, render_state); + break; + } + + damage_region = create_damage_region (render_context, render_state); + grd_rdp_frame_set_damage_region (rdp_frame, damage_region); + + grd_rdp_render_state_free (render_state); +} + +void +grd_rdp_render_context_fetch_progressive_render_state (GrdRdpRenderContext *render_context, + GrdImageView **image_view, + cairo_region_t **damage_region) +{ + g_autoptr (GrdRdpRenderState) render_state = NULL; + + g_assert (render_context->chroma_state_buffer); + + render_state = grd_rdp_render_state_new (render_context->chroma_state_buffer, + NULL, + render_context->state_buffer_length); + *damage_region = create_damage_region (render_context, render_state); + + *image_view = render_context->last_acquired_image_view; + g_assert (!g_hash_table_contains (render_context->acquired_image_views, + *image_view)); + + g_hash_table_add (render_context->acquired_image_views, *image_view); + + g_clear_pointer (&render_context->chroma_state_buffer, g_free); + update_frame_upgrade_state (render_context); +} + +static gboolean +is_gpu_driver_amd (GrdRdpRenderContext *render_context) +{ + GrdVkDevice *vk_device = + grd_rdp_renderer_get_vk_device (render_context->renderer); + VkDriverId driver_id = grd_vk_device_get_driver_id (vk_device); + + return driver_id == VK_DRIVER_ID_AMD_PROPRIETARY || + driver_id == VK_DRIVER_ID_AMD_OPEN_SOURCE || + driver_id == VK_DRIVER_ID_MESA_RADV; +} + +static void +try_create_vaapi_session (GrdRdpRenderContext *render_context, + GrdRdpSurface *rdp_surface, + gboolean have_avc444) +{ + GrdVkDevice *vk_device = + grd_rdp_renderer_get_vk_device (render_context->renderer); + GrdRdpSurfaceRenderer *surface_renderer = + grd_rdp_surface_get_surface_renderer (rdp_surface); + GrdHwAccelVaapi *hwaccel_vaapi = + grd_rdp_renderer_get_hwaccel_vaapi (render_context->renderer); + GrdRdpBufferInfo *buffer_info = + grd_rdp_surface_renderer_get_buffer_info (surface_renderer); + uint32_t refresh_rate = + grd_rdp_surface_renderer_get_refresh_rate (surface_renderer); + uint32_t surface_width = grd_rdp_surface_get_width (rdp_surface); + uint32_t surface_height = grd_rdp_surface_get_height (rdp_surface); + g_autoptr (GrdEncodeSession) encode_session = NULL; + GrdRdpViewCreatorAVC *view_creator_avc; + uint32_t render_surface_width = 0; + uint32_t render_surface_height = 0; + g_autoptr (GError) error = NULL; + g_autoptr (GList) image_views = NULL; + GList *l; + + g_assert (buffer_info->buffer_type == GRD_RDP_BUFFER_TYPE_DMA_BUF); + + if (!buffer_info->has_vk_image) + return; + if (buffer_info->drm_format_modifier == DRM_FORMAT_MOD_INVALID) + return; + + encode_session = + grd_hwaccel_vaapi_create_encode_session (hwaccel_vaapi, + surface_width, surface_height, + refresh_rate, &error); + if (!encode_session) + { + g_debug ("[HWAccel.VAAPI] Could not create VAAPI encode session: %s", + error->message); + return; + } + + grd_encode_session_get_surface_size (encode_session, + &render_surface_width, + &render_surface_height); + + g_assert (render_surface_width % 16 == 0); + g_assert (render_surface_height % 16 == 0); + g_assert (render_surface_width >= 16); + g_assert (render_surface_height >= 16); + + view_creator_avc = + grd_rdp_view_creator_avc_new (vk_device, + render_surface_width, + render_surface_height, + surface_width, + surface_height, + &error); + if (!view_creator_avc) + { + g_debug ("[HWAccel.Vulkan] Failed to create view creator for VAAPI " + "encode session: %s", error->message); + return; + } + + image_views = grd_encode_session_get_image_views (encode_session); + for (l = image_views; l; l = l->next) + { + GrdImageView *image_view = l->data; + + g_hash_table_add (render_context->image_views, image_view); + } + + render_context->view_creator = GRD_RDP_VIEW_CREATOR (view_creator_avc); + render_context->encode_session = g_steal_pointer (&encode_session); + + if (have_avc444) + render_context->codec = GRD_RDP_CODEC_AVC444v2; + else + render_context->codec = GRD_RDP_CODEC_AVC420; + + g_debug ("[HWAccel.VAAPI] Created VAAPI encode session for surface with " + "size %ux%u", surface_width, surface_height); +} + +static gboolean +create_egl_based_rfx_progressive_encode_session (GrdRdpRenderContext *render_context, + GrdRdpSurface *rdp_surface, + GError **error) +{ + GrdRdpSwEncoderCa *encoder_ca = + grd_rdp_renderer_get_encoder_ca (render_context->renderer); + uint32_t surface_width = grd_rdp_surface_get_width (rdp_surface); + uint32_t surface_height = grd_rdp_surface_get_height (rdp_surface); + GrdEncodeSessionCaSw *encode_session_ca; + GrdRdpViewCreatorGenGL *view_creator_gen_gl; + GrdEncodeSession *encode_session; + g_autoptr (GList) image_views = NULL; + GList *l; + + encode_session_ca = + grd_encode_session_ca_sw_new (encoder_ca, + surface_width, surface_height, + error); + if (!encode_session_ca) + return FALSE; + + view_creator_gen_gl = + grd_rdp_view_creator_gen_gl_new (render_context, surface_width, surface_height); + + encode_session = GRD_ENCODE_SESSION (encode_session_ca); + image_views = grd_encode_session_get_image_views (encode_session); + + for (l = image_views; l; l = l->next) + { + GrdImageView *image_view = l->data; + + g_hash_table_add (render_context->image_views, image_view); + } + + render_context->view_creator = GRD_RDP_VIEW_CREATOR (view_creator_gen_gl); + render_context->encode_session = encode_session; + + render_context->codec = GRD_RDP_CODEC_CAPROGRESSIVE; + + return TRUE; +} + +static gboolean +create_hw_accelerated_encode_session (GrdRdpRenderContext *render_context, + GrdRdpSurface *rdp_surface, + GError **error) +{ + GrdSessionRdp *session_rdp = + grd_rdp_renderer_get_session (render_context->renderer); + GrdRdpDvcGraphicsPipeline *graphics_pipeline = + grd_session_rdp_get_graphics_pipeline (session_rdp); + GrdHwAccelVaapi *hwaccel_vaapi = + grd_rdp_renderer_get_hwaccel_vaapi (render_context->renderer); + gboolean have_avc444 = FALSE; + gboolean have_avc420 = FALSE; + + grd_rdp_dvc_graphics_pipeline_get_capabilities (graphics_pipeline, + &have_avc444, &have_avc420); + if ((have_avc444 || have_avc420) && hwaccel_vaapi && + !is_gpu_driver_amd (render_context) && + grd_get_debug_flags () & GRD_DEBUG_VKVA) + try_create_vaapi_session (render_context, rdp_surface, have_avc444); + + if (render_context->encode_session) + return TRUE; + + return create_egl_based_rfx_progressive_encode_session (render_context, + rdp_surface, + error); +} + +static gboolean +create_sw_based_rfx_progressive_encode_session (GrdRdpRenderContext *render_context, + GrdRdpSurface *rdp_surface, + GError **error) +{ + GrdRdpSwEncoderCa *encoder_ca = + grd_rdp_renderer_get_encoder_ca (render_context->renderer); + uint32_t surface_width = grd_rdp_surface_get_width (rdp_surface); + uint32_t surface_height = grd_rdp_surface_get_height (rdp_surface); + GrdEncodeSessionCaSw *encode_session_ca; + GrdRdpViewCreatorGenSW *view_creator_gen_sw; + GrdEncodeSession *encode_session; + g_autoptr (GList) image_views = NULL; + GList *l; + + encode_session_ca = + grd_encode_session_ca_sw_new (encoder_ca, + surface_width, surface_height, + error); + if (!encode_session_ca) + return FALSE; + + view_creator_gen_sw = grd_rdp_view_creator_gen_sw_new (surface_width, + surface_height); + + encode_session = GRD_ENCODE_SESSION (encode_session_ca); + image_views = grd_encode_session_get_image_views (encode_session); + + for (l = image_views; l; l = l->next) + { + GrdImageView *image_view = l->data; + + g_hash_table_add (render_context->image_views, image_view); + } + + render_context->view_creator = GRD_RDP_VIEW_CREATOR (view_creator_gen_sw); + render_context->encode_session = encode_session; + + render_context->codec = GRD_RDP_CODEC_CAPROGRESSIVE; + render_context->delay_view_finalization = TRUE; + + return TRUE; +} + +static gboolean +create_sw_based_encode_session (GrdRdpRenderContext *render_context, + GrdRdpSurface *rdp_surface, + GError **error) +{ + return create_sw_based_rfx_progressive_encode_session (render_context, + rdp_surface, + error); +} + +static gboolean +create_encode_session (GrdRdpRenderContext *render_context, + GrdRdpSurface *rdp_surface, + GError **error) +{ + GrdRdpSurfaceRenderer *surface_renderer = + grd_rdp_surface_get_surface_renderer (rdp_surface); + GrdRdpBufferInfo *buffer_info = + grd_rdp_surface_renderer_get_buffer_info (surface_renderer); + + switch (buffer_info->buffer_type) + { + case GRD_RDP_BUFFER_TYPE_DMA_BUF: + return create_hw_accelerated_encode_session (render_context, + rdp_surface, + error); + case GRD_RDP_BUFFER_TYPE_MEM_FD: + return create_sw_based_encode_session (render_context, + rdp_surface, + error); + case GRD_RDP_BUFFER_TYPE_NONE: + g_assert_not_reached (); + return FALSE; + } + + g_assert_not_reached (); + return FALSE; +} + +GrdRdpRenderContext * +grd_rdp_render_context_new (GrdRdpRenderer *renderer, + GrdRdpSurface *rdp_surface) +{ + GrdSessionRdp *session_rdp = grd_rdp_renderer_get_session (renderer); + GrdRdpDvcGraphicsPipeline *graphics_pipeline = + grd_session_rdp_get_graphics_pipeline (session_rdp); + g_autoptr (GrdRdpRenderContext) render_context = NULL; + g_autoptr (GError) error = NULL; + + g_assert (graphics_pipeline); + + if (!grd_rdp_damage_detector_invalidate_surface (rdp_surface->detector)) + return NULL; + + render_context = g_object_new (GRD_TYPE_RDP_RENDER_CONTEXT, NULL); + render_context->renderer = renderer; + render_context->gfx_surface = + grd_rdp_dvc_graphics_pipeline_acquire_gfx_surface (graphics_pipeline, + rdp_surface); + + if (!create_encode_session (render_context, rdp_surface, &error)) + { + g_warning ("[RDP] Failed to create encode session: %s", error->message); + return NULL; + } + + return g_steal_pointer (&render_context); +} + +static void +grd_rdp_render_context_dispose (GObject *object) +{ + GrdRdpRenderContext *render_context = GRD_RDP_RENDER_CONTEXT (object); + + if (render_context->acquired_image_views) + g_assert (g_hash_table_size (render_context->acquired_image_views) == 0); + + g_clear_pointer (&render_context->chroma_state_buffer, g_free); + update_frame_upgrade_state (render_context); + + g_clear_object (&render_context->view_creator); + g_clear_object (&render_context->encode_session); + g_clear_object (&render_context->gfx_surface); + + G_OBJECT_CLASS (grd_rdp_render_context_parent_class)->dispose (object); +} + +static void +grd_rdp_render_context_finalize (GObject *object) +{ + GrdRdpRenderContext *render_context = GRD_RDP_RENDER_CONTEXT (object); + + g_clear_pointer (&render_context->acquired_image_views, g_hash_table_unref); + g_clear_pointer (&render_context->image_views, g_hash_table_unref); + + G_OBJECT_CLASS (grd_rdp_render_context_parent_class)->finalize (object); +} + +static void +grd_rdp_render_context_init (GrdRdpRenderContext *render_context) +{ + render_context->image_views = g_hash_table_new (NULL, NULL); + render_context->acquired_image_views = g_hash_table_new (NULL, NULL); +} + +static void +grd_rdp_render_context_class_init (GrdRdpRenderContextClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = grd_rdp_render_context_dispose; + object_class->finalize = grd_rdp_render_context_finalize; +} diff --git a/grd-rdp-render-context.h b/grd-rdp-render-context.h new file mode 100644 index 0000000..d3fbf6f --- /dev/null +++ b/grd-rdp-render-context.h @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2023 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include +#include + +#include "grd-types.h" + +#define GRD_TYPE_RDP_RENDER_CONTEXT (grd_rdp_render_context_get_type ()) +G_DECLARE_FINAL_TYPE (GrdRdpRenderContext, grd_rdp_render_context, + GRD, RDP_RENDER_CONTEXT, GObject) + +typedef enum +{ + GRD_RDP_CODEC_CAPROGRESSIVE, + GRD_RDP_CODEC_AVC420, + GRD_RDP_CODEC_AVC444v2, +} GrdRdpCodec; + +GrdRdpRenderContext *grd_rdp_render_context_new (GrdRdpRenderer *renderer, + GrdRdpSurface *rdp_surface); + +GrdRdpRenderer *grd_rdp_render_context_get_renderer (GrdRdpRenderContext *render_context); + +GrdRdpCodec grd_rdp_render_context_get_codec (GrdRdpRenderContext *render_context); + +GrdRdpGfxSurface *grd_rdp_render_context_get_gfx_surface (GrdRdpRenderContext *render_context); + +GrdRdpViewCreator *grd_rdp_render_context_get_view_creator (GrdRdpRenderContext *render_context); + +GrdEncodeSession *grd_rdp_render_context_get_encode_session (GrdRdpRenderContext *render_context); + +gboolean grd_rdp_render_context_must_delay_view_finalization (GrdRdpRenderContext *render_context); + +gboolean grd_rdp_render_context_should_avoid_dual_frame (GrdRdpRenderContext *render_context); + +GrdImageView *grd_rdp_render_context_acquire_image_view (GrdRdpRenderContext *render_context); + +void grd_rdp_render_context_release_image_view (GrdRdpRenderContext *render_context, + GrdImageView *image_view); + +void grd_rdp_render_context_update_frame_state (GrdRdpRenderContext *render_context, + GrdRdpFrame *rdp_frame, + GrdRdpRenderState *render_state); + +void grd_rdp_render_context_fetch_progressive_render_state (GrdRdpRenderContext *render_context, + GrdImageView **image_view, + cairo_region_t **damage_region); diff --git a/grd-rdp-render-state.c b/grd-rdp-render-state.c new file mode 100644 index 0000000..c3cba40 --- /dev/null +++ b/grd-rdp-render-state.c @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2024 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-rdp-render-state.h" + +struct _GrdRdpRenderState +{ + uint32_t *damage_buffer; + uint32_t *chroma_state_buffer; + uint32_t state_buffer_length; +}; + +uint32_t * +grd_rdp_render_state_get_damage_buffer (GrdRdpRenderState *render_state) +{ + return render_state->damage_buffer; +} + +uint32_t * +grd_rdp_render_state_get_chroma_state_buffer (GrdRdpRenderState *render_state) +{ + return render_state->chroma_state_buffer; +} + +uint32_t +grd_rdp_render_state_get_state_buffer_length (GrdRdpRenderState *render_state) +{ + return render_state->state_buffer_length; +} + +GrdRdpRenderState * +grd_rdp_render_state_new (uint32_t *damage_buffer, + uint32_t *chroma_state_buffer, + uint32_t state_buffer_length) +{ + GrdRdpRenderState *render_state; + + render_state = g_new0 (GrdRdpRenderState, 1); + render_state->damage_buffer = damage_buffer; + render_state->chroma_state_buffer = chroma_state_buffer; + render_state->state_buffer_length = state_buffer_length; + + return render_state; +} + +void +grd_rdp_render_state_free (GrdRdpRenderState *render_state) +{ + g_free (render_state); +} diff --git a/grd-rdp-render-state.h b/grd-rdp-render-state.h new file mode 100644 index 0000000..da83359 --- /dev/null +++ b/grd-rdp-render-state.h @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2024 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include +#include + +#include "grd-types.h" + +GrdRdpRenderState *grd_rdp_render_state_new (uint32_t *damage_buffer, + uint32_t *chroma_state_buffer, + uint32_t state_buffer_length); + +void grd_rdp_render_state_free (GrdRdpRenderState *render_state); + +uint32_t *grd_rdp_render_state_get_damage_buffer (GrdRdpRenderState *render_state); + +uint32_t *grd_rdp_render_state_get_chroma_state_buffer (GrdRdpRenderState *render_state); + +uint32_t grd_rdp_render_state_get_state_buffer_length (GrdRdpRenderState *render_state); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (GrdRdpRenderState, grd_rdp_render_state_free) diff --git a/grd-rdp-renderer.c b/grd-rdp-renderer.c new file mode 100644 index 0000000..9bce12d --- /dev/null +++ b/grd-rdp-renderer.c @@ -0,0 +1,1201 @@ +/* + * Copyright (C) 2023 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-rdp-renderer.h" + +#include "grd-context.h" +#include "grd-encode-session.h" +#include "grd-hwaccel-nvidia.h" +#include "grd-hwaccel-vaapi.h" +#include "grd-hwaccel-vulkan.h" +#include "grd-rdp-dvc-graphics-pipeline.h" +#include "grd-rdp-frame.h" +#include "grd-rdp-private.h" +#include "grd-rdp-render-context.h" +#include "grd-rdp-server.h" +#include "grd-rdp-surface.h" +#include "grd-rdp-surface-renderer.h" +#include "grd-rdp-sw-encoder-ca.h" +#include "grd-rdp-view-creator.h" +#include "grd-session-rdp.h" + +enum +{ + INHIBITION_DONE, + GFX_INITABLE, + + N_SIGNALS +}; + +static guint signals[N_SIGNALS]; + +struct _GrdRdpRenderer +{ + GObject parent; + + gboolean in_shutdown; + + GrdSessionRdp *session_rdp; + GrdVkPhysicalDevice *vk_physical_device; + GrdVkDevice *vk_device; + GrdHwAccelVaapi *hwaccel_vaapi; + GrdRdpSwEncoderCa *encoder_ca; + + GThread *graphics_thread; + GMainContext *graphics_context; + + gboolean graphics_subsystem_failed; + + gboolean stop_rendering; + GCond stop_rendering_cond; + + gboolean rendering_inhibited; + gboolean output_suppressed; + + gboolean pending_gfx_init; + gboolean pending_gfx_graphics_reset; + + GMutex surface_renderers_mutex; + GHashTable *surface_renderer_table; + + GMutex inhibition_mutex; + GHashTable *render_context_table; + GHashTable *acquired_render_contexts; + GHashTable *render_resource_mappings; + + GSource *surface_disposal_source; + GAsyncQueue *disposal_queue; + + GSource *surface_render_source; + GHashTable *queued_frames; + + GMutex view_creations_mutex; + GHashTable *finished_view_creations; + + GMutex frame_encodings_mutex; + GHashTable *finished_frame_encodings; +}; + +G_DEFINE_TYPE (GrdRdpRenderer, grd_rdp_renderer, G_TYPE_OBJECT) + +GMainContext * +grd_rdp_renderer_get_graphics_context (GrdRdpRenderer *renderer) +{ + return renderer->graphics_context; +} + +GrdSessionRdp * +grd_rdp_renderer_get_session (GrdRdpRenderer *renderer) +{ + return renderer->session_rdp; +} + +GrdVkDevice * +grd_rdp_renderer_get_vk_device (GrdRdpRenderer *renderer) +{ + return renderer->vk_device; +} + +GrdHwAccelVaapi * +grd_rdp_renderer_get_hwaccel_vaapi (GrdRdpRenderer *renderer) +{ + return renderer->hwaccel_vaapi; +} + +GrdRdpSwEncoderCa * +grd_rdp_renderer_get_encoder_ca (GrdRdpRenderer *renderer) +{ + return renderer->encoder_ca; +} + +static void +trigger_render_sources (GrdRdpRenderer *renderer) +{ + GrdRdpSurfaceRenderer *surface_renderer = NULL; + g_autoptr (GMutexLocker) locker = NULL; + GHashTableIter iter; + + locker = g_mutex_locker_new (&renderer->surface_renderers_mutex); + g_hash_table_iter_init (&iter, renderer->surface_renderer_table); + while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &surface_renderer)) + grd_rdp_surface_renderer_trigger_render_source (surface_renderer); +} + +void +grd_rdp_renderer_update_output_suppression_state (GrdRdpRenderer *renderer, + gboolean suppress_output) +{ + renderer->output_suppressed = suppress_output; + + if (!renderer->output_suppressed) + trigger_render_sources (renderer); +} + +static void +stop_rendering (GrdRdpRenderer *renderer) +{ + g_mutex_lock (&renderer->inhibition_mutex); + renderer->stop_rendering = TRUE; + + while (g_hash_table_size (renderer->acquired_render_contexts) > 0) + g_cond_wait (&renderer->stop_rendering_cond, &renderer->inhibition_mutex); + g_mutex_unlock (&renderer->inhibition_mutex); +} + +void +grd_rdp_renderer_invoke_shutdown (GrdRdpRenderer *renderer) +{ + g_assert (renderer->graphics_context); + + stop_rendering (renderer); + + renderer->in_shutdown = TRUE; + + g_main_context_wakeup (renderer->graphics_context); + g_clear_pointer (&renderer->graphics_thread, g_thread_join); +} + +static gpointer +graphics_thread_func (gpointer data) +{ + GrdRdpRenderer *renderer = data; + GrdRdpServer *rdp_server = grd_session_rdp_get_server (renderer->session_rdp); + GrdHwAccelNvidia *hwaccel_nvidia = + grd_rdp_server_get_hwaccel_nvidia (rdp_server); + + if (hwaccel_nvidia) + grd_hwaccel_nvidia_push_cuda_context (hwaccel_nvidia); + + while (!renderer->in_shutdown) + g_main_context_iteration (renderer->graphics_context, TRUE); + + if (hwaccel_nvidia) + grd_hwaccel_nvidia_pop_cuda_context (hwaccel_nvidia); + + return NULL; +} + +static gboolean +maybe_initialize_hardware_acceleration (GrdRdpRenderer *renderer, + GrdHwAccelVulkan *hwaccel_vulkan) +{ + GrdVkPhysicalDevice *vk_physical_device; + g_autoptr (GError) error = NULL; + + vk_physical_device = + grd_hwaccel_vulkan_acquire_physical_device (hwaccel_vulkan, &error); + if (!vk_physical_device) + { + g_message ("[HWAccel.Vulkan] Could not acquire Vulkan physical " + "device: %s", error->message); + return TRUE; + } + renderer->vk_physical_device = vk_physical_device; + + renderer->vk_device = grd_hwaccel_vulkan_acquire_device (hwaccel_vulkan, + vk_physical_device, + &error); + if (!renderer->vk_device) + { + g_warning ("[RDP] Failed to acquire Vulkan device: %s", + error->message); + return FALSE; + } + + renderer->hwaccel_vaapi = grd_hwaccel_vaapi_new (renderer->vk_device, + &error); + if (!renderer->hwaccel_vaapi) + { + g_message ("[RDP] Did not initialize VAAPI: %s", error->message); + g_clear_object (&renderer->vk_device); + } + + return TRUE; +} + +gboolean +grd_rdp_renderer_start (GrdRdpRenderer *renderer) +{ + GrdRdpServer *rdp_server = grd_session_rdp_get_server (renderer->session_rdp); + GrdHwAccelVulkan *hwaccel_vulkan = + grd_rdp_server_get_hwaccel_vulkan (rdp_server); + g_autoptr (GError) error = NULL; + + if (hwaccel_vulkan && + !maybe_initialize_hardware_acceleration (renderer, hwaccel_vulkan)) + return FALSE; + + renderer->encoder_ca = grd_rdp_sw_encoder_ca_new (&error); + if (!renderer->encoder_ca) + { + g_warning ("[RDP] Failed to create fallback software renderer: %s", + error->message); + return FALSE; + } + + renderer->graphics_thread = g_thread_new ("RDP graphics thread", + graphics_thread_func, + renderer); + + return TRUE; +} + +static GrdRdpDvcGraphicsPipeline * +graphics_pipeline_from_renderer (GrdRdpRenderer *renderer) +{ + return grd_session_rdp_get_graphics_pipeline (renderer->session_rdp); +} + +void +grd_rdp_renderer_notify_new_desktop_layout (GrdRdpRenderer *renderer, + uint32_t desktop_width, + uint32_t desktop_height) +{ + rdpContext *rdp_context = + grd_session_rdp_get_rdp_context (renderer->session_rdp); + rdpSettings *rdp_settings = rdp_context->settings; + + g_assert (graphics_pipeline_from_renderer (renderer)); + renderer->pending_gfx_graphics_reset = TRUE; + + freerdp_settings_set_uint32 (rdp_settings, FreeRDP_DesktopWidth, + desktop_width); + freerdp_settings_set_uint32 (rdp_settings, FreeRDP_DesktopHeight, + desktop_height); +} + +void +grd_rdp_renderer_notify_graphics_pipeline_ready (GrdRdpRenderer *renderer) +{ + g_debug ("[RDP] Renderer: Received Graphics Pipeline ready notification"); + + renderer->pending_gfx_graphics_reset = TRUE; + renderer->pending_gfx_init = FALSE; + + trigger_render_sources (renderer); +} + +void +grd_rdp_renderer_notify_graphics_pipeline_reset (GrdRdpRenderer *renderer) +{ + gboolean gfx_initable = FALSE; + + g_debug ("[RDP] Renderer: Received Graphics Pipeline reset notification"); + + g_mutex_lock (&renderer->inhibition_mutex); + renderer->pending_gfx_init = TRUE; + + if (g_hash_table_size (renderer->acquired_render_contexts) == 0) + gfx_initable = TRUE; + g_mutex_unlock (&renderer->inhibition_mutex); + + if (gfx_initable) + g_signal_emit (renderer, signals[GFX_INITABLE], 0); +} + +void +grd_rdp_renderer_inhibit_rendering (GrdRdpRenderer *renderer) +{ + gboolean inhibition_done = FALSE; + + g_mutex_lock (&renderer->inhibition_mutex); + renderer->rendering_inhibited = TRUE; + + if (g_hash_table_size (renderer->acquired_render_contexts) == 0) + inhibition_done = TRUE; + g_mutex_unlock (&renderer->inhibition_mutex); + + if (inhibition_done) + g_signal_emit (renderer, signals[INHIBITION_DONE], 0); +} + +void +grd_rdp_renderer_uninhibit_rendering (GrdRdpRenderer *renderer) +{ + renderer->rendering_inhibited = FALSE; + + trigger_render_sources (renderer); +} + +GrdRdpSurface * +grd_rdp_renderer_try_acquire_surface (GrdRdpRenderer *renderer, + uint32_t refresh_rate) +{ + GrdRdpSurface *rdp_surface; + GrdRdpSurfaceRenderer *surface_renderer; + + rdp_surface = grd_rdp_surface_new (renderer); + if (!rdp_surface) + return NULL; + + surface_renderer = grd_rdp_surface_renderer_new (rdp_surface, renderer, + refresh_rate); + grd_rdp_surface_attach_surface_renderer (rdp_surface, surface_renderer); + + g_mutex_lock (&renderer->surface_renderers_mutex); + g_hash_table_insert (renderer->surface_renderer_table, + rdp_surface, surface_renderer); + g_mutex_unlock (&renderer->surface_renderers_mutex); + + return rdp_surface; +} + +void +grd_rdp_renderer_release_surface (GrdRdpRenderer *renderer, + GrdRdpSurface *rdp_surface) +{ + g_assert (rdp_surface); + + g_async_queue_push (renderer->disposal_queue, rdp_surface); + g_source_set_ready_time (renderer->surface_disposal_source, 0); +} + +static void +invalidate_surfaces (GrdRdpRenderer *renderer, + gboolean locked) +{ + GrdRdpSurfaceRenderer *surface_renderer = NULL; + g_autoptr (GMutexLocker) locker = NULL; + GHashTableIter iter; + + locker = g_mutex_locker_new (&renderer->surface_renderers_mutex); + g_hash_table_iter_init (&iter, renderer->surface_renderer_table); + while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &surface_renderer)) + { + if (!locked) + grd_rdp_surface_renderer_invalidate_surface_unlocked (surface_renderer); + else + grd_rdp_surface_renderer_invalidate_surface (surface_renderer); + } +} + +static void +clear_render_contexts (GrdRdpRenderer *renderer, + gboolean locked) +{ + g_assert (g_hash_table_size (renderer->acquired_render_contexts) == 0); + g_assert (g_hash_table_size (renderer->queued_frames) == 0); + + invalidate_surfaces (renderer, locked); + + g_hash_table_remove_all (renderer->render_resource_mappings); + g_hash_table_remove_all (renderer->render_context_table); +} + +static void +maybe_reset_graphics (GrdRdpRenderer *renderer) +{ + GrdRdpDvcGraphicsPipeline *graphics_pipeline = + graphics_pipeline_from_renderer (renderer); + rdpContext *rdp_context = + grd_session_rdp_get_rdp_context (renderer->session_rdp); + rdpSettings *rdp_settings = rdp_context->settings; + uint32_t desktop_width = + freerdp_settings_get_uint32 (rdp_settings, FreeRDP_DesktopWidth); + uint32_t desktop_height = + freerdp_settings_get_uint32 (rdp_settings, FreeRDP_DesktopHeight); + g_autofree MONITOR_DEF *monitor_defs = NULL; + uint32_t n_monitors; + uint32_t i; + + if (!renderer->pending_gfx_graphics_reset) + return; + + clear_render_contexts (renderer, FALSE); + + n_monitors = freerdp_settings_get_uint32 (rdp_settings, FreeRDP_MonitorCount); + g_assert (n_monitors > 0); + + monitor_defs = g_new0 (MONITOR_DEF, n_monitors); + + for (i = 0; i < n_monitors; ++i) + { + const rdpMonitor *monitor = + freerdp_settings_get_pointer_array (rdp_settings, + FreeRDP_MonitorDefArray, i); + MONITOR_DEF *monitor_def = &monitor_defs[i]; + + monitor_def->left = monitor->x; + monitor_def->top = monitor->y; + monitor_def->right = monitor_def->left + monitor->width - 1; + monitor_def->bottom = monitor_def->top + monitor->height - 1; + + if (monitor->is_primary) + monitor_def->flags = MONITOR_PRIMARY; + } + + grd_rdp_dvc_graphics_pipeline_reset_graphics (graphics_pipeline, + desktop_width, desktop_height, + monitor_defs, n_monitors); + renderer->pending_gfx_graphics_reset = FALSE; +} + +static void +destroy_render_context_locked (GrdRdpRenderer *renderer, + GrdRdpSurface *rdp_surface) +{ + GrdRdpRenderContext *render_context = NULL; + + if (!g_hash_table_lookup_extended (renderer->render_context_table, + rdp_surface, + NULL, (gpointer *) &render_context)) + return; + + g_assert (render_context); + g_assert (!g_hash_table_contains (renderer->acquired_render_contexts, + render_context)); + g_assert (!g_hash_table_contains (renderer->queued_frames, + render_context)); + + g_hash_table_remove (renderer->render_resource_mappings, render_context); + g_hash_table_remove (renderer->render_context_table, rdp_surface); +} + +static void +destroy_render_context (GrdRdpRenderer *renderer, + GrdRdpSurface *rdp_surface) +{ + g_autoptr (GMutexLocker) locker = NULL; + + locker = g_mutex_locker_new (&renderer->inhibition_mutex); + destroy_render_context_locked (renderer, rdp_surface); +} + +static GrdRdpRenderContext * +render_context_ref (GrdRdpRenderer *renderer, + GrdRdpRenderContext *render_context) +{ + uint32_t *ref_count = NULL; + + if (g_hash_table_lookup_extended (renderer->acquired_render_contexts, + render_context, + NULL, (gpointer *) &ref_count)) + { + g_assert (*ref_count > 0); + ++(*ref_count); + + return render_context; + } + + ref_count = g_new0 (uint32_t, 1); + *ref_count = 1; + + g_hash_table_insert (renderer->acquired_render_contexts, + render_context, ref_count); + + return render_context; +} + +static void +render_context_unref (GrdRdpRenderer *renderer, + GrdRdpRenderContext *render_context) +{ + uint32_t *ref_count = NULL; + + if (!g_hash_table_lookup_extended (renderer->acquired_render_contexts, + render_context, + NULL, (gpointer *) &ref_count)) + g_assert_not_reached (); + + g_assert (*ref_count > 0); + --(*ref_count); + + if (*ref_count == 0) + g_hash_table_remove (renderer->acquired_render_contexts, render_context); +} + +static void +handle_graphics_subsystem_failure (GrdRdpRenderer *renderer) +{ + renderer->graphics_subsystem_failed = TRUE; + renderer->stop_rendering = TRUE; + + grd_session_rdp_notify_error (renderer->session_rdp, + GRD_SESSION_RDP_ERROR_GRAPHICS_SUBSYSTEM_FAILED); +} + +GrdRdpRenderContext * +grd_rdp_renderer_try_acquire_render_context (GrdRdpRenderer *renderer, + GrdRdpSurface *rdp_surface, + GrdRdpAcquireContextFlags flags) +{ + GrdRdpRenderContext *render_context = NULL; + g_autoptr (GMutexLocker) locker = NULL; + + g_assert (!((flags & GRD_RDP_ACQUIRE_CONTEXT_FLAG_FORCE_RESET) && + (flags & GRD_RDP_ACQUIRE_CONTEXT_FLAG_RETAIN_OR_NULL))); + + locker = g_mutex_locker_new (&renderer->inhibition_mutex); + if (renderer->stop_rendering || + renderer->rendering_inhibited || + renderer->pending_gfx_init || + renderer->output_suppressed) + return NULL; + + maybe_reset_graphics (renderer); + + if (flags & GRD_RDP_ACQUIRE_CONTEXT_FLAG_FORCE_RESET) + destroy_render_context_locked (renderer, rdp_surface); + + if (g_hash_table_lookup_extended (renderer->render_context_table, rdp_surface, + NULL, (gpointer *) &render_context)) + return render_context_ref (renderer, render_context); + + if (flags & GRD_RDP_ACQUIRE_CONTEXT_FLAG_RETAIN_OR_NULL) + return NULL; + + render_context = grd_rdp_render_context_new (renderer, rdp_surface); + if (!render_context) + { + handle_graphics_subsystem_failure (renderer); + return NULL; + } + + g_hash_table_insert (renderer->render_context_table, + rdp_surface, render_context); + g_hash_table_insert (renderer->render_resource_mappings, + render_context, g_hash_table_new (NULL, NULL)); + + return render_context_ref (renderer, render_context); +} + +void +grd_rdp_renderer_release_render_context (GrdRdpRenderer *renderer, + GrdRdpRenderContext *render_context) +{ + gboolean inhibition_done = FALSE; + gboolean gfx_initable = FALSE; + + g_mutex_lock (&renderer->inhibition_mutex); + render_context_unref (renderer, render_context); + + if (renderer->stop_rendering && + g_hash_table_size (renderer->acquired_render_contexts) == 0) + g_cond_signal (&renderer->stop_rendering_cond); + + if (renderer->rendering_inhibited && + g_hash_table_size (renderer->acquired_render_contexts) == 0) + inhibition_done = TRUE; + + if (renderer->pending_gfx_init && + g_hash_table_size (renderer->acquired_render_contexts) == 0) + gfx_initable = TRUE; + g_mutex_unlock (&renderer->inhibition_mutex); + + if (inhibition_done) + g_signal_emit (renderer, signals[INHIBITION_DONE], 0); + if (gfx_initable) + g_signal_emit (renderer, signals[GFX_INITABLE], 0); +} + +void +grd_rdp_renderer_clear_render_contexts (GrdRdpRenderer *renderer) +{ + clear_render_contexts (renderer, TRUE); +} + +static void +queue_prepared_frame (GrdRdpRenderer *renderer, + GrdRdpRenderContext *render_context, + GrdRdpFrame *rdp_frame) +{ + GHashTable *acquired_resources = NULL; + GrdRdpViewCreator *view_creator; + + if (!g_hash_table_lookup_extended (renderer->render_resource_mappings, + render_context, + NULL, (gpointer *) &acquired_resources)) + g_assert_not_reached (); + + view_creator = grd_rdp_render_context_get_view_creator (render_context); + g_assert (!g_hash_table_contains (acquired_resources, view_creator)); + + grd_rdp_frame_notify_picked_up (rdp_frame); + g_hash_table_insert (acquired_resources, view_creator, rdp_frame); + + g_mutex_lock (&renderer->view_creations_mutex); + g_hash_table_add (renderer->finished_view_creations, rdp_frame); + g_mutex_unlock (&renderer->view_creations_mutex); +} + +void +grd_rdp_renderer_submit_frame (GrdRdpRenderer *renderer, + GrdRdpRenderContext *render_context, + GrdRdpFrame *rdp_frame) +{ + grd_rdp_frame_set_renderer (rdp_frame, renderer); + + if (grd_rdp_frame_has_valid_view (rdp_frame)) + queue_prepared_frame (renderer, render_context, rdp_frame); + else + g_hash_table_insert (renderer->queued_frames, render_context, rdp_frame); + + g_source_set_ready_time (renderer->surface_render_source, 0); +} + +gboolean +grd_rdp_renderer_render_frame (GrdRdpRenderer *renderer, + GrdRdpSurface *rdp_surface, + GrdRdpRenderContext *render_context, + GrdRdpLegacyBuffer *buffer) +{ + GrdRdpDvcGraphicsPipeline *graphics_pipeline = + graphics_pipeline_from_renderer (renderer); + + return grd_rdp_dvc_graphics_pipeline_refresh_gfx (graphics_pipeline, + rdp_surface, render_context, + buffer); +} + +GrdRdpRenderer * +grd_rdp_renderer_new (GrdSessionRdp *session_rdp) +{ + GrdRdpRenderer *renderer; + + renderer = g_object_new (GRD_TYPE_RDP_RENDERER, NULL); + renderer->session_rdp = session_rdp; + + return renderer; +} + +static gboolean +dispose_surfaces (gpointer user_data) +{ + GrdRdpRenderer *renderer = user_data; + GrdRdpSurface *rdp_surface; + + while ((rdp_surface = g_async_queue_try_pop (renderer->disposal_queue))) + { + destroy_render_context (renderer, rdp_surface); + + g_mutex_lock (&renderer->surface_renderers_mutex); + g_hash_table_remove (renderer->surface_renderer_table, rdp_surface); + g_mutex_unlock (&renderer->surface_renderers_mutex); + + grd_rdp_surface_free (rdp_surface); + } + + return G_SOURCE_CONTINUE; +} + +static void +grd_rdp_renderer_dispose (GObject *object) +{ + GrdRdpRenderer *renderer = GRD_RDP_RENDERER (object); + + grd_rdp_renderer_invoke_shutdown (renderer); + + if (renderer->acquired_render_contexts) + g_assert (g_hash_table_size (renderer->acquired_render_contexts) == 0); + if (renderer->render_resource_mappings) + g_assert (g_hash_table_size (renderer->render_resource_mappings) == 0); + if (renderer->queued_frames) + g_assert (g_hash_table_size (renderer->queued_frames) == 0); + if (renderer->finished_view_creations) + g_assert (g_hash_table_size (renderer->finished_view_creations) == 0); + if (renderer->finished_frame_encodings) + g_assert (g_hash_table_size (renderer->finished_frame_encodings) == 0); + + if (renderer->surface_render_source) + { + g_source_destroy (renderer->surface_render_source); + g_clear_pointer (&renderer->surface_render_source, g_source_unref); + } + if (renderer->surface_disposal_source) + { + g_source_destroy (renderer->surface_disposal_source); + g_clear_pointer (&renderer->surface_disposal_source, g_source_unref); + } + + g_clear_pointer (&renderer->graphics_context, g_main_context_unref); + dispose_surfaces (renderer); + + g_clear_pointer (&renderer->finished_frame_encodings, g_hash_table_unref); + g_clear_pointer (&renderer->finished_view_creations, g_hash_table_unref); + g_clear_pointer (&renderer->queued_frames, g_hash_table_unref); + g_clear_pointer (&renderer->render_resource_mappings, g_hash_table_unref); + g_clear_pointer (&renderer->acquired_render_contexts, g_hash_table_unref); + g_clear_pointer (&renderer->render_context_table, g_hash_table_unref); + + g_clear_pointer (&renderer->disposal_queue, g_async_queue_unref); + + g_assert (g_hash_table_size (renderer->surface_renderer_table) == 0); + + g_clear_object (&renderer->encoder_ca); + g_clear_object (&renderer->hwaccel_vaapi); + g_clear_object (&renderer->vk_device); + g_clear_object (&renderer->vk_physical_device); + + G_OBJECT_CLASS (grd_rdp_renderer_parent_class)->dispose (object); +} + +static void +grd_rdp_renderer_finalize (GObject *object) +{ + GrdRdpRenderer *renderer = GRD_RDP_RENDERER (object); + + g_mutex_clear (&renderer->frame_encodings_mutex); + g_mutex_clear (&renderer->view_creations_mutex); + + g_cond_clear (&renderer->stop_rendering_cond); + g_mutex_clear (&renderer->inhibition_mutex); + g_mutex_clear (&renderer->surface_renderers_mutex); + + g_clear_pointer (&renderer->surface_renderer_table, g_hash_table_unref); + + G_OBJECT_CLASS (grd_rdp_renderer_parent_class)->finalize (object); +} + +static void +release_acquired_resource (GrdRdpRenderer *renderer, + GrdRdpRenderContext *render_context, + gpointer resource) +{ + GHashTable *acquired_resources = NULL; + + if (!g_hash_table_lookup_extended (renderer->render_resource_mappings, + render_context, + NULL, (gpointer *) &acquired_resources)) + g_assert_not_reached (); + + if (!g_hash_table_remove (acquired_resources, resource)) + g_assert_not_reached (); +} + +static GList * +fetch_rendered_frames (GrdRdpRenderer *renderer) +{ + g_autoptr (GMutexLocker) locker = NULL; + GrdRdpFrame *rdp_frame = NULL; + GHashTableIter iter; + GList *rendered_frames; + + locker = g_mutex_locker_new (&renderer->frame_encodings_mutex); + g_hash_table_iter_init (&iter, renderer->finished_frame_encodings); + while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &rdp_frame)) + { + GrdRdpRenderContext *render_context = + grd_rdp_frame_get_render_context (rdp_frame); + GrdEncodeSession *encode_session = + grd_rdp_render_context_get_encode_session (render_context); + + release_acquired_resource (renderer, render_context, encode_session); + + if (!grd_rdp_frame_get_bitstreams (rdp_frame)) + g_hash_table_iter_remove (&iter); + } + + rendered_frames = + g_hash_table_get_values (renderer->finished_frame_encodings); + g_hash_table_steal_all (renderer->finished_frame_encodings); + + return rendered_frames; +} + +static void +on_bitstream_locked (GrdEncodeSession *encode_session, + GrdBitstream *bitstream, + gpointer user_data, + GError *error) +{ + GrdRdpFrame *rdp_frame = user_data; + GrdRdpRenderer *renderer = grd_rdp_frame_get_renderer (rdp_frame); + + if (bitstream) + { + GList *bitstreams = grd_rdp_frame_get_bitstreams (rdp_frame); + + bitstreams = g_list_append (bitstreams, bitstream); + grd_rdp_frame_set_bitstreams (rdp_frame, bitstreams); + } + else + { + g_warning ("[RDP] Failed to lock bitstream: %s", error->message); + } + + g_mutex_lock (&renderer->frame_encodings_mutex); + if (!grd_encode_session_has_pending_frames (encode_session)) + g_hash_table_add (renderer->finished_frame_encodings, rdp_frame); + + if (!bitstream) + renderer->graphics_subsystem_failed = TRUE; + g_mutex_unlock (&renderer->frame_encodings_mutex); + + g_source_set_ready_time (renderer->surface_render_source, 0); +} + +static gboolean +encode_image_views (GrdRdpRenderer *renderer, + GrdRdpFrame *rdp_frame) +{ + g_autoptr (GMutexLocker) locker = NULL; + gboolean submitted_frame = FALSE; + g_autoptr (GError) error = NULL; + GrdImageView *image_view; + + locker = g_mutex_locker_new (&renderer->frame_encodings_mutex); + while ((image_view = grd_rdp_frame_pop_image_view (rdp_frame))) + { + GrdRdpRenderContext *render_context = + grd_rdp_frame_get_render_context (rdp_frame); + GrdEncodeSession *encode_session = + grd_rdp_render_context_get_encode_session (render_context); + GrdEncodeContext *encode_context = + grd_rdp_frame_get_encode_context (rdp_frame); + + if (!grd_encode_session_encode_frame (encode_session, encode_context, + image_view, &error)) + { + g_warning ("[RDP] Failed to encode frame: %s", error->message); + + if (!submitted_frame) + { + release_acquired_resource (renderer, render_context, + encode_session); + grd_rdp_frame_free (rdp_frame); + } + + return FALSE; + } + submitted_frame = TRUE; + + grd_encode_session_lock_bitstream (encode_session, image_view, + on_bitstream_locked, rdp_frame); + } + + return TRUE; +} + +static gboolean +maybe_start_encodings (GrdRdpRenderer *renderer) +{ + g_autoptr (GMutexLocker) locker = NULL; + GrdRdpFrame *rdp_frame = NULL; + GHashTableIter iter; + + if (renderer->stop_rendering) + return TRUE; + + locker = g_mutex_locker_new (&renderer->view_creations_mutex); + if (renderer->graphics_subsystem_failed) + return FALSE; + + g_hash_table_iter_init (&iter, renderer->finished_view_creations); + while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &rdp_frame)) + { + GHashTable *acquired_resources = NULL; + GrdRdpRenderContext *render_context; + GrdRdpViewCreator *view_creator; + GrdEncodeSession *encode_session; + + g_assert (grd_rdp_frame_has_valid_view (rdp_frame)); + + render_context = grd_rdp_frame_get_render_context (rdp_frame); + view_creator = grd_rdp_render_context_get_view_creator (render_context); + + if (!grd_rdp_frame_is_surface_damaged (rdp_frame)) + { + release_acquired_resource (renderer, render_context, view_creator); + g_hash_table_iter_remove (&iter); + continue; + } + + if (!g_hash_table_lookup_extended (renderer->render_resource_mappings, + render_context, + NULL, (gpointer *) &acquired_resources)) + g_assert_not_reached (); + + encode_session = + grd_rdp_render_context_get_encode_session (render_context); + if (g_hash_table_contains (acquired_resources, encode_session)) + continue; + + release_acquired_resource (renderer, render_context, view_creator); + g_hash_table_insert (acquired_resources, encode_session, rdp_frame); + g_hash_table_iter_steal (&iter); + + if (!encode_image_views (renderer, rdp_frame)) + return FALSE; + } + + return TRUE; +} + +static void +on_view_created (GrdRdpFrame *rdp_frame, + GError *error) +{ + GrdRdpRenderer *renderer = grd_rdp_frame_get_renderer (rdp_frame); + + if (!grd_rdp_frame_has_valid_view (rdp_frame)) + g_warning ("[RDP] Failed to create image view: %s", error->message); + + g_mutex_lock (&renderer->view_creations_mutex); + g_hash_table_add (renderer->finished_view_creations, rdp_frame); + + if (!grd_rdp_frame_has_valid_view (rdp_frame)) + renderer->graphics_subsystem_failed = TRUE; + g_mutex_unlock (&renderer->view_creations_mutex); + + g_source_set_ready_time (renderer->surface_render_source, 0); +} + +static gboolean +maybe_start_view_creations (GrdRdpRenderer *renderer) +{ + GrdRdpFrame *rdp_frame = NULL; + GHashTableIter iter; + + if (renderer->stop_rendering) + return TRUE; + + g_hash_table_iter_init (&iter, renderer->queued_frames); + while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &rdp_frame)) + { + GHashTable *acquired_resources = NULL; + GrdRdpRenderContext *render_context; + GrdRdpViewCreator *view_creator; + g_autoptr (GError) error = NULL; + + render_context = grd_rdp_frame_get_render_context (rdp_frame); + if (!g_hash_table_lookup_extended (renderer->render_resource_mappings, + render_context, + NULL, (gpointer *) &acquired_resources)) + g_assert_not_reached (); + + view_creator = grd_rdp_render_context_get_view_creator (render_context); + if (g_hash_table_contains (acquired_resources, view_creator)) + continue; + + grd_rdp_frame_notify_picked_up (rdp_frame); + + /* There is no resource to release here (no predecessor) */ + g_hash_table_insert (acquired_resources, view_creator, rdp_frame); + g_hash_table_iter_steal (&iter); + + if (!grd_rdp_view_creator_create_view (view_creator, rdp_frame, + on_view_created, &error)) + { + g_warning ("[RDP] Failed to create view: %s", error->message); + grd_rdp_frame_free (rdp_frame); + return FALSE; + } + } + + return TRUE; +} + +static void +submit_rendered_frames (GrdRdpRenderer *renderer, + GList *frames) +{ + GrdRdpDvcGraphicsPipeline *graphics_pipeline = + graphics_pipeline_from_renderer (renderer); + GList *l; + + for (l = frames; l; l = l->next) + { + GrdRdpFrame *rdp_frame = l->data; + + grd_rdp_dvc_graphics_pipeline_submit_frame (graphics_pipeline, + rdp_frame); + grd_rdp_frame_notify_frame_submission (rdp_frame); + } +} + +static void +release_bitstreams (gpointer data, + gpointer user_data) +{ + GrdRdpRenderer *renderer = user_data; + GrdRdpFrame *rdp_frame = data; + GList *bitstreams = grd_rdp_frame_get_bitstreams (rdp_frame); + GList *l; + + for (l = bitstreams; l; l = l->next) + { + GrdRdpRenderContext *render_context = + grd_rdp_frame_get_render_context (rdp_frame); + GrdEncodeSession *encode_session = + grd_rdp_render_context_get_encode_session (render_context); + GrdBitstream *bitstream = l->data; + g_autoptr (GError) error = NULL; + + if (!grd_encode_session_unlock_bitstream (encode_session, bitstream, + &error)) + { + g_warning ("[RDP] Renderer: Failed to unlock bitstream %s", + error->message); + handle_graphics_subsystem_failure (renderer); + } + } + g_list_free (bitstreams); + + grd_rdp_frame_set_bitstreams (rdp_frame, NULL); +} + +static void +clear_pending_frames (GrdRdpRenderer *renderer) +{ + GrdRdpFrame *rdp_frame = NULL; + GHashTableIter iter; + + g_hash_table_remove_all (renderer->queued_frames); + + g_mutex_lock (&renderer->view_creations_mutex); + g_hash_table_iter_init (&iter, renderer->finished_view_creations); + while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &rdp_frame)) + { + GrdRdpRenderContext *render_context = + grd_rdp_frame_get_render_context (rdp_frame); + GrdRdpViewCreator *view_creator = + grd_rdp_render_context_get_view_creator (render_context); + + release_acquired_resource (renderer, render_context, view_creator); + g_hash_table_iter_remove (&iter); + } + g_mutex_unlock (&renderer->view_creations_mutex); + + g_mutex_lock (&renderer->frame_encodings_mutex); + g_hash_table_iter_init (&iter, renderer->finished_frame_encodings); + while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &rdp_frame)) + { + GrdRdpRenderContext *render_context = + grd_rdp_frame_get_render_context (rdp_frame); + GrdEncodeSession *encode_session = + grd_rdp_render_context_get_encode_session (render_context); + + release_acquired_resource (renderer, render_context, encode_session); + release_bitstreams (rdp_frame, renderer); + g_hash_table_iter_remove (&iter); + } + g_mutex_unlock (&renderer->frame_encodings_mutex); +} + +static gboolean +render_surfaces (gpointer user_data) +{ + GrdRdpRenderer *renderer = user_data; + GList *finished_frames; + + finished_frames = fetch_rendered_frames (renderer); + + if (!maybe_start_encodings (renderer) || + !maybe_start_view_creations (renderer)) + handle_graphics_subsystem_failure (renderer); + + if (!renderer->stop_rendering) + submit_rendered_frames (renderer, finished_frames); + else + clear_pending_frames (renderer); + + g_list_foreach (finished_frames, release_bitstreams, renderer); + g_clear_list (&finished_frames, (GDestroyNotify) grd_rdp_frame_free); + + return G_SOURCE_CONTINUE; +} + +static gboolean +source_dispatch (GSource *source, + GSourceFunc callback, + gpointer user_data) +{ + g_source_set_ready_time (source, -1); + + return callback (user_data); +} + +static GSourceFuncs source_funcs = +{ + .dispatch = source_dispatch, +}; + +static void +grd_rdp_renderer_init (GrdRdpRenderer *renderer) +{ + GSource *surface_disposal_source; + GSource *surface_render_source; + + renderer->surface_renderer_table = g_hash_table_new (NULL, NULL); + renderer->render_context_table = g_hash_table_new_full (NULL, NULL, + NULL, g_object_unref); + renderer->acquired_render_contexts = g_hash_table_new_full (NULL, NULL, + NULL, g_free); + renderer->render_resource_mappings = + g_hash_table_new_full (NULL, NULL, + NULL, (GDestroyNotify) g_hash_table_unref); + renderer->disposal_queue = g_async_queue_new (); + + renderer->queued_frames = + g_hash_table_new_full (NULL, NULL, + NULL, (GDestroyNotify) grd_rdp_frame_free); + renderer->finished_view_creations = + g_hash_table_new_full (NULL, NULL, + NULL, (GDestroyNotify) grd_rdp_frame_free); + renderer->finished_frame_encodings = + g_hash_table_new_full (NULL, NULL, + NULL, (GDestroyNotify) grd_rdp_frame_free); + + g_mutex_init (&renderer->surface_renderers_mutex); + g_mutex_init (&renderer->inhibition_mutex); + g_cond_init (&renderer->stop_rendering_cond); + + g_mutex_init (&renderer->view_creations_mutex); + g_mutex_init (&renderer->frame_encodings_mutex); + + renderer->graphics_context = g_main_context_new (); + + surface_disposal_source = g_source_new (&source_funcs, sizeof (GSource)); + g_source_set_callback (surface_disposal_source, dispose_surfaces, + renderer, NULL); + g_source_set_ready_time (surface_disposal_source, -1); + g_source_attach (surface_disposal_source, renderer->graphics_context); + renderer->surface_disposal_source = surface_disposal_source; + + surface_render_source = g_source_new (&source_funcs, sizeof (GSource)); + g_source_set_callback (surface_render_source, render_surfaces, + renderer, NULL); + g_source_set_ready_time (surface_render_source, -1); + g_source_attach (surface_render_source, renderer->graphics_context); + renderer->surface_render_source = surface_render_source; +} + +static void +grd_rdp_renderer_class_init (GrdRdpRendererClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = grd_rdp_renderer_dispose; + object_class->finalize = grd_rdp_renderer_finalize; + + signals[INHIBITION_DONE] = g_signal_new ("inhibition-done", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, 0); + signals[GFX_INITABLE] = g_signal_new ("gfx-initable", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, 0); +} diff --git a/grd-rdp-renderer.h b/grd-rdp-renderer.h new file mode 100644 index 0000000..4020bb4 --- /dev/null +++ b/grd-rdp-renderer.h @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2023 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include +#include +#include + +#include "grd-rdp-renderer.h" +#include "grd-types.h" + +#define GRD_TYPE_RDP_RENDERER (grd_rdp_renderer_get_type ()) +G_DECLARE_FINAL_TYPE (GrdRdpRenderer, grd_rdp_renderer, + GRD, RDP_RENDERER, GObject) + +typedef enum +{ + GRD_RDP_ACQUIRE_CONTEXT_FLAG_NONE = 0, + GRD_RDP_ACQUIRE_CONTEXT_FLAG_FORCE_RESET = 1 << 0, + GRD_RDP_ACQUIRE_CONTEXT_FLAG_RETAIN_OR_NULL = 1 << 1, +} GrdRdpAcquireContextFlags; + +GrdRdpRenderer *grd_rdp_renderer_new (GrdSessionRdp *session_rdp); + +GMainContext *grd_rdp_renderer_get_graphics_context (GrdRdpRenderer *renderer); + +GrdSessionRdp *grd_rdp_renderer_get_session (GrdRdpRenderer *renderer); + +GrdVkDevice *grd_rdp_renderer_get_vk_device (GrdRdpRenderer *renderer); + +GrdHwAccelVaapi *grd_rdp_renderer_get_hwaccel_vaapi (GrdRdpRenderer *renderer); + +GrdRdpSwEncoderCa *grd_rdp_renderer_get_encoder_ca (GrdRdpRenderer *renderer); + +void grd_rdp_renderer_update_output_suppression_state (GrdRdpRenderer *renderer, + gboolean suppress_output); + +void grd_rdp_renderer_invoke_shutdown (GrdRdpRenderer *renderer); + +gboolean grd_rdp_renderer_start (GrdRdpRenderer *renderer); + +void grd_rdp_renderer_notify_new_desktop_layout (GrdRdpRenderer *renderer, + uint32_t desktop_width, + uint32_t desktop_height); + +void grd_rdp_renderer_notify_graphics_pipeline_ready (GrdRdpRenderer *renderer); + +void grd_rdp_renderer_notify_graphics_pipeline_reset (GrdRdpRenderer *renderer); + +void grd_rdp_renderer_inhibit_rendering (GrdRdpRenderer *renderer); + +void grd_rdp_renderer_uninhibit_rendering (GrdRdpRenderer *renderer); + +GrdRdpSurface *grd_rdp_renderer_try_acquire_surface (GrdRdpRenderer *renderer, + uint32_t refresh_rate); + +void grd_rdp_renderer_release_surface (GrdRdpRenderer *renderer, + GrdRdpSurface *rdp_surface); + +GrdRdpRenderContext *grd_rdp_renderer_try_acquire_render_context (GrdRdpRenderer *renderer, + GrdRdpSurface *rdp_surface, + GrdRdpAcquireContextFlags flags); + +void grd_rdp_renderer_release_render_context (GrdRdpRenderer *renderer, + GrdRdpRenderContext *render_context); + +void grd_rdp_renderer_clear_render_contexts (GrdRdpRenderer *renderer); + +void grd_rdp_renderer_submit_frame (GrdRdpRenderer *renderer, + GrdRdpRenderContext *render_context, + GrdRdpFrame *rdp_frame); + +gboolean grd_rdp_renderer_render_frame (GrdRdpRenderer *renderer, + GrdRdpSurface *rdp_surface, + GrdRdpRenderContext *render_context, + GrdRdpLegacyBuffer *buffer); diff --git a/grd-rdp-routing-token.c b/grd-rdp-routing-token.c new file mode 100644 index 0000000..72f53a1 --- /dev/null +++ b/grd-rdp-routing-token.c @@ -0,0 +1,374 @@ +/* + * Copyright (C) 2022 SUSE Software Solutions Germany GmbH + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + * Written by: + * Joan Torres + */ + +#include "config.h" + +#include "grd-rdp-routing-token.h" + +#include +#include + +#define MAX_PEEK_TIME_MS 2000 +#define PROTOCOL_RDSTLS 0x00000004 + +typedef struct _RoutingTokenContext +{ + GrdRdpServer *rdp_server; + GSocketConnection *connection; + + GCancellable *cancellable; + unsigned int abort_peek_source_id; + + GCancellable *server_cancellable; + + gboolean requested_rdstls; +} RoutingTokenContext; + +static void +wstream_free_full (wStream *s); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (wStream, wstream_free_full) + +static void +wstream_free_full (wStream *s) +{ + Stream_Free (s, TRUE); +} + +static int +find_cr_lf (const char *buffer, + int length) +{ + int i; + + for (i = 0; i < length - 1; ++i) + { + if (buffer[i] == 0x0D && buffer[i + 1] == 0x0A) + return i; + } + + return -1; +} + +static gboolean +peek_bytes (int fd, + uint8_t *buffer, + int length, + GCancellable *cancellable, + GError **error) +{ + GPollFD poll_fds[2] = {}; + int n_fds = 0; + int ret; + + poll_fds[n_fds].fd = fd; + poll_fds[n_fds].events = G_IO_IN; + n_fds++; + + if (!g_cancellable_make_pollfd (cancellable, &poll_fds[n_fds])) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failure preparing the cancellable for pollfd"); + return FALSE; + } + + n_fds++; + + do + { + do + ret = g_poll (poll_fds, n_fds, MAX_PEEK_TIME_MS); + while (ret == -1 && errno == EINTR); + + if (ret == -1) + { + g_set_error (error, G_IO_ERROR, + g_io_error_from_errno (errno), + "On poll command: %s", strerror (errno)); + g_cancellable_release_fd (cancellable); + return FALSE; + } + + if (g_cancellable_is_cancelled (cancellable)) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_CANCELLED, + "Cancelled"); + g_cancellable_release_fd (cancellable); + return FALSE; + } + + do + ret = recv (fd, (void *) buffer, (size_t) length, MSG_PEEK); + while (ret == -1 && errno == EINTR); + + if (ret == -1) + { + if (errno == EAGAIN || errno == EWOULDBLOCK) + continue; + + g_set_error (error, G_IO_ERROR, + g_io_error_from_errno (errno), + "On recv command: %s", strerror (errno)); + g_cancellable_release_fd (cancellable); + return FALSE; + } + + } + while (ret < length); + + g_cancellable_release_fd (cancellable); + + return TRUE; +} + +static char * +get_routing_token_without_prefix (char *buffer, + size_t buffer_length, + size_t *routing_token_length) +{ + g_autofree char *peeked_prefix = NULL; + g_autofree char *prefix = NULL; + size_t prefix_length; + + prefix = g_strdup ("Cookie: msts="); + prefix_length = strlen (prefix); + + if (buffer_length < prefix_length) + return NULL; + + peeked_prefix = g_strndup (buffer, prefix_length); + if (g_strcmp0 (peeked_prefix, prefix) != 0) + return NULL; + + *routing_token_length = find_cr_lf (buffer, buffer_length); + if (*routing_token_length == -1) + return NULL; + + return g_strndup (buffer + prefix_length, + *routing_token_length - prefix_length); +} + +static gboolean +peek_routing_token (int fd, + char **routing_token, + gboolean *requested_rdstls, + GCancellable *cancellable, + GError **error) +{ + g_autoptr (wStream) s = NULL; + + /* TPKT values */ + uint8_t version; + uint16_t tpkt_length; + + /* x224Crq values */ + uint8_t length_indicator; + uint8_t cr_cdt; + uint16_t dst_ref; + uint8_t class_opt; + + size_t routing_token_length; + + /* rdpNegReq values */ + uint8_t rdp_neg_type; + uint16_t rdp_neg_length; + uint32_t requested_protocols; + + /* Peek TPKT Header */ + s = Stream_New (NULL, 4); + g_assert (s); + + if (!peek_bytes (fd, Stream_Buffer (s), 4, cancellable, error)) + return FALSE; + + Stream_Read_UINT8 (s, version); + Stream_Seek (s, 1); + Stream_Read_UINT16_BE (s, tpkt_length); + + if (version != 3) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "The TPKT Header doesn't have version 3"); + return FALSE; + } + if (tpkt_length < 4 + 7) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "The x224Crq TPDU length is too short"); + return FALSE; + } + + /* Peek full PDU */ + Stream_Free (s, TRUE); + s = Stream_New (NULL, tpkt_length); + g_assert (s); + + if (!peek_bytes (fd, Stream_Buffer (s), tpkt_length, cancellable, error)) + return FALSE; + + Stream_Seek (s, 4); + + /* Check x224Crq */ + Stream_Read_UINT8 (s, length_indicator); + Stream_Read_UINT8 (s, cr_cdt); + Stream_Read_UINT16 (s, dst_ref); + Stream_Seek (s, 2); + Stream_Read_UINT8 (s, class_opt); + if (tpkt_length - 5 != length_indicator || + cr_cdt != 0xE0 || + dst_ref != 0 || + (class_opt & 0xFC) != 0) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Wrong info on x224Crq"); + return FALSE; + } + + /* Check routingToken */ + *routing_token = + get_routing_token_without_prefix ((char *) Stream_Pointer (s), + Stream_GetRemainingLength (s), + &routing_token_length); + if (!(*routing_token)) + return TRUE; + + /* Check rdpNegReq */ + Stream_Seek (s, routing_token_length + 2); + if (Stream_GetRemainingLength (s) < 8) + return TRUE; + + Stream_Read_UINT8 (s, rdp_neg_type); + Stream_Seek (s, 1); + Stream_Read_UINT16 (s, rdp_neg_length); + Stream_Read_UINT32 (s, requested_protocols); + if (rdp_neg_type != 0x01 || rdp_neg_length != 8) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Wrong info on rdpNegReq"); + return FALSE; + } + + *requested_rdstls = !!(requested_protocols & PROTOCOL_RDSTLS); + + return TRUE; +} + +static void +peek_routing_token_in_thread (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + RoutingTokenContext *routing_token_context = task_data; + GSocket *socket; + int fd; + char *routing_token = NULL; + GError *error = NULL; + + socket = g_socket_connection_get_socket (routing_token_context->connection); + fd = g_socket_get_fd (socket); + + if (!peek_routing_token (fd, + &routing_token, + &routing_token_context->requested_rdstls, + routing_token_context->cancellable, + &error)) + g_task_return_error (task, error); + else + g_task_return_pointer (task, routing_token, g_free); +} + +static gboolean +abort_peek_routing_token (gpointer user_data) +{ + RoutingTokenContext *routing_token_context = user_data; + + g_assert (routing_token_context->abort_peek_source_id); + + g_debug ("RoutingToken: Aborting current peek operation " + "(Timeout reached)"); + + g_cancellable_cancel (routing_token_context->cancellable); + + routing_token_context->abort_peek_source_id = 0; + + return G_SOURCE_REMOVE; +} + +static void +clear_routing_token_context (gpointer data) +{ + RoutingTokenContext *routing_token_context = data; + + g_clear_object (&routing_token_context->connection); + g_clear_object (&routing_token_context->server_cancellable); + g_clear_object (&routing_token_context->cancellable); + g_clear_handle_id (&routing_token_context->abort_peek_source_id, + g_source_remove); + + g_free (routing_token_context); +} + +void +grd_routing_token_peek_async (GrdRdpServer *rdp_server, + GSocketConnection *connection, + GCancellable *cancellable, + GAsyncReadyCallback on_finished_callback) +{ + RoutingTokenContext *routing_token_context; + GTask *task; + + routing_token_context = g_new0 (RoutingTokenContext, 1); + routing_token_context->rdp_server = rdp_server; + routing_token_context->connection = g_object_ref (connection); + routing_token_context->cancellable = g_cancellable_new (); + routing_token_context->server_cancellable = g_object_ref (cancellable); + + task = g_task_new (NULL, NULL, on_finished_callback, NULL); + g_task_set_task_data (task, routing_token_context, clear_routing_token_context); + g_task_run_in_thread (task, peek_routing_token_in_thread); + g_object_unref (task); + + routing_token_context->abort_peek_source_id = + g_timeout_add (MAX_PEEK_TIME_MS, + abort_peek_routing_token, + routing_token_context); +} + +char * +grd_routing_token_peek_finish (GAsyncResult *result, + GrdRdpServer **rdp_server, + GSocketConnection **connection, + GCancellable **server_cancellable, + gboolean *requested_rdstls, + GError **error) +{ + RoutingTokenContext *routing_token_context = + g_task_get_task_data (G_TASK (result)); + + *rdp_server = routing_token_context->rdp_server; + *connection = routing_token_context->connection; + *server_cancellable = routing_token_context->server_cancellable; + *requested_rdstls = routing_token_context->requested_rdstls; + + return g_task_propagate_pointer (G_TASK (result), error); +} diff --git a/grd-rdp-routing-token.h b/grd-rdp-routing-token.h new file mode 100644 index 0000000..405dd8d --- /dev/null +++ b/grd-rdp-routing-token.h @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2022 SUSE Software Solutions Germany GmbH + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + * Written by: + * Joan Torres + */ + +#pragma once + +#include + +#include "grd-types.h" + +void grd_routing_token_peek_async (GrdRdpServer *rdp_server, + GSocketConnection *connection, + GCancellable *cancellable, + GAsyncReadyCallback callback); + +char *grd_routing_token_peek_finish (GAsyncResult *result, + GrdRdpServer **rdp_server, + GSocketConnection **connection, + GCancellable **server_cancellable, + gboolean *requested_rdstls, + GError **error); diff --git a/grd-rdp-sam.c b/grd-rdp-sam.c new file mode 100644 index 0000000..daa3ebb --- /dev/null +++ b/grd-rdp-sam.c @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2020 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-rdp-sam.h" + +#include +#include +#include +#include +#include +#include + +void +grd_rdp_sam_free_sam_file (GrdRdpSAMFile *rdp_sam_file) +{ + if (rdp_sam_file->fd >= 0) + { + unlink (rdp_sam_file->filename); + close (rdp_sam_file->fd); + } + + g_free (rdp_sam_file->filename); + g_free (rdp_sam_file); +} + +static char * +create_sam_string (const char *username, + const char *password) +{ + uint32_t username_length; + uint32_t password_length; + uint32_t i; + char *sam_string; + uint8_t nt_hash[16]; + + username_length = strlen (username); + password_length = strlen (password); + + sam_string = g_malloc0 ((username_length + 3 + 32 + 3 + 1 + 1) * sizeof (char)); + + NTOWFv1A ((LPSTR) password, password_length, nt_hash); + + sprintf (sam_string, "%s:::", username); + for (i = 0; i < 16; ++i) + sprintf (sam_string + strlen (sam_string), "%02" PRIx8 "", nt_hash[i]); + sprintf (sam_string + strlen (sam_string), ":::\n"); + + return sam_string; +} + +GrdRdpSAMFile * +grd_rdp_sam_create_sam_file (const char *username, + const char *password) +{ + const char *grd_path = "/gnome-remote-desktop"; + const char *template = "/rdp-sam-XXXXXX"; + int duped_fd; + GrdRdpSAMFile *rdp_sam_file; + g_autofree char *file_dir = NULL; + g_autofree char *filename = NULL; + g_autofree char *sam_string = NULL; + g_autofd int fd = -1; + FILE *sam_file; + + file_dir = g_strdup_printf ("%s%s", g_get_user_runtime_dir (), grd_path); + filename = g_strdup_printf ("%s%s%s", g_get_user_runtime_dir (), grd_path, + template); + + if (g_access (file_dir, F_OK) && + mkdir (file_dir, 0700)) + { + g_warning ("Failed to create base runtime directory for " + "gnome-remote-desktop: %s", g_strerror (errno)); + return NULL; + } + + fd = g_mkstemp (filename); + if (fd < 0) + { + g_warning ("[RDP] g_mkstemp() failed: %s", g_strerror (errno)); + return NULL; + } + + sam_file = fdopen (fd, "w+"); + if (!sam_file) + { + g_warning ("[RDP] Failed to open SAM database: %s", g_strerror (errno)); + return NULL; + } + + duped_fd = dup (fd); + if (duped_fd < 0) + { + fclose (sam_file); + g_warning ("[RDP] Failed to dup fd: %s", g_strerror (errno)); + return NULL; + } + + rdp_sam_file = g_new0 (GrdRdpSAMFile, 1); + rdp_sam_file->fd = duped_fd; + rdp_sam_file->filename = g_steal_pointer (&filename); + + g_steal_fd (&fd); + + sam_string = create_sam_string (username, password); + + fputs (sam_string, sam_file); + fclose (sam_file); + + return rdp_sam_file; +} diff --git a/grd-rdp-sam.h b/grd-rdp-sam.h new file mode 100644 index 0000000..42776f9 --- /dev/null +++ b/grd-rdp-sam.h @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2020 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include "grd-types.h" + +struct _GrdRdpSAMFile +{ + int fd; + char *filename; +}; + +GrdRdpSAMFile *grd_rdp_sam_create_sam_file (const char *username, + const char *password); + +void grd_rdp_sam_free_sam_file (GrdRdpSAMFile *rdp_sam_file); diff --git a/grd-rdp-server.c b/grd-rdp-server.c new file mode 100644 index 0000000..2180126 --- /dev/null +++ b/grd-rdp-server.c @@ -0,0 +1,652 @@ +/* + * Copyright (C) 2020 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-rdp-server.h" + +#include +#include +#include +#include +#include + +#include "grd-context.h" +#include "grd-hwaccel-nvidia.h" +#include "grd-hwaccel-vulkan.h" +#include "grd-rdp-routing-token.h" +#include "grd-session-rdp.h" +#include "grd-throttler.h" +#include "grd-utils.h" + +#define RDP_SERVER_N_BINDING_ATTEMPTS 10 +#define RDP_SERVER_BINDING_ATTEMPT_INTERVAL_MS 500 +#define RDP_SERVER_SOCKET_BACKLOG_COUNT 5 + +enum +{ + PROP_0, + + PROP_CONTEXT, +}; + +enum +{ + INCOMING_NEW_CONNECTION, + INCOMING_REDIRECTED_CONNECTION, + BINDING_FAILED, + + N_SIGNALS +}; + +static guint signals[N_SIGNALS]; + +struct _GrdRdpServer +{ + GSocketService parent; + + GrdThrottler *throttler; + + GList *sessions; + + GList *stopped_sessions; + guint cleanup_sessions_idle_id; + + GCancellable *cancellable; + + GrdContext *context; + GrdHwAccelVulkan *hwaccel_vulkan; + GrdHwAccelNvidia *hwaccel_nvidia; + + uint32_t pending_binding_attempts; + unsigned int binding_timeout_source_id; +}; + +G_DEFINE_TYPE (GrdRdpServer, grd_rdp_server, G_TYPE_SOCKET_SERVICE) + +GrdContext * +grd_rdp_server_get_context (GrdRdpServer *rdp_server) +{ + return rdp_server->context; +} + +GrdHwAccelNvidia * +grd_rdp_server_get_hwaccel_nvidia (GrdRdpServer *rdp_server) +{ + return rdp_server->hwaccel_nvidia; +} + +GrdHwAccelVulkan * +grd_rdp_server_get_hwaccel_vulkan (GrdRdpServer *rdp_server) +{ + return rdp_server->hwaccel_vulkan; +} + +GrdRdpServer * +grd_rdp_server_new (GrdContext *context) +{ + GrdRdpServer *rdp_server; + GrdEglThread *egl_thread; + g_autoptr (GError) error = NULL; + + rdp_server = g_object_new (GRD_TYPE_RDP_SERVER, + "context", context, + NULL); + + egl_thread = grd_context_get_egl_thread (rdp_server->context); + if (!egl_thread) + return rdp_server; + + rdp_server->hwaccel_nvidia = grd_hwaccel_nvidia_new (egl_thread); + if (rdp_server->hwaccel_nvidia) + { + g_message ("[HWAccel.CUDA] Initialization of CUDA was successful"); + return rdp_server; + } + + rdp_server->hwaccel_vulkan = grd_hwaccel_vulkan_new (egl_thread, &error); + if (!rdp_server->hwaccel_vulkan) + g_debug ("[HWAccel.Vulkan] Could not initialize Vulkan: %s", error->message); + else + g_message ("[HWAccel.Vulkan] Initialization of Vulkan was successful"); + + return rdp_server; +} + +static void +grd_rdp_server_cleanup_stopped_sessions (GrdRdpServer *rdp_server) +{ + g_list_free_full (rdp_server->stopped_sessions, g_object_unref); + rdp_server->stopped_sessions = NULL; +} + +static gboolean +cleanup_stopped_sessions_idle (GrdRdpServer *rdp_server) +{ + grd_rdp_server_cleanup_stopped_sessions (rdp_server); + rdp_server->cleanup_sessions_idle_id = 0; + + return G_SOURCE_REMOVE; +} + +static void +on_session_stopped (GrdSession *session, + GrdRdpServer *rdp_server) +{ + g_debug ("RDP session stopped"); + + rdp_server->stopped_sessions = g_list_append (rdp_server->stopped_sessions, + session); + rdp_server->sessions = g_list_remove (rdp_server->sessions, session); + if (!rdp_server->cleanup_sessions_idle_id) + { + rdp_server->cleanup_sessions_idle_id = + g_idle_add ((GSourceFunc) cleanup_stopped_sessions_idle, + rdp_server); + } +} + +static void +maybe_stop_session (GrdSession *session, + GrdSession *session_to_ignore) +{ + if (session == session_to_ignore) + return; + + grd_session_stop (session); +} + +static void +on_session_post_connect (GrdSessionRdp *session_rdp, + GrdRdpServer *rdp_server) +{ + GrdDBusRemoteDesktopRdpServer *rdp_server_iface = + grd_context_get_rdp_server_interface (rdp_server->context); + GrdRuntimeMode runtime_mode = + grd_context_get_runtime_mode (rdp_server->context); + + g_signal_emit (rdp_server, signals[INCOMING_NEW_CONNECTION], 0, session_rdp); + + grd_dbus_remote_desktop_rdp_server_emit_new_connection (rdp_server_iface); + + if (runtime_mode == GRD_RUNTIME_MODE_HANDOVER || + runtime_mode == GRD_RUNTIME_MODE_HEADLESS) + { + g_list_foreach (rdp_server->sessions, + (GFunc) maybe_stop_session, + GRD_SESSION (session_rdp)); + } +} + +static void +on_routing_token_peeked (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GrdRdpServer *rdp_server; + GSocketConnection *connection; + GrdSessionRdp *session_rdp; + GCancellable *server_cancellable; + g_autofree char *routing_token = NULL; + g_autoptr (GError) error = NULL; + gboolean requested_rdstls = FALSE; + + routing_token = grd_routing_token_peek_finish (result, + &rdp_server, + &connection, + &server_cancellable, + &requested_rdstls, + &error); + if (g_cancellable_is_cancelled (server_cancellable)) + return; + + if (error) + { + g_warning ("Failed to peek routing token: %s", error->message); + return; + } + + if (routing_token) + { + g_signal_emit (rdp_server, signals[INCOMING_REDIRECTED_CONNECTION], + 0, routing_token, requested_rdstls, connection); + } + else + { + if (!(session_rdp = grd_session_rdp_new (rdp_server, connection))) + return; + + rdp_server->sessions = g_list_append (rdp_server->sessions, session_rdp); + + g_signal_connect (session_rdp, "stopped", + G_CALLBACK (on_session_stopped), + rdp_server); + + g_signal_connect (session_rdp, "post-connected", + G_CALLBACK (on_session_post_connect), + rdp_server); + } +} + +static void +allow_connection_peek_cb (GrdThrottler *throttler, + GSocketConnection *connection, + gpointer user_data) +{ + GrdRdpServer *rdp_server = GRD_RDP_SERVER (user_data); + + grd_routing_token_peek_async (rdp_server, + connection, + rdp_server->cancellable, + on_routing_token_peeked); +} + +static gboolean +on_incoming (GSocketService *service, + GSocketConnection *connection) +{ + GrdRdpServer *rdp_server = GRD_RDP_SERVER (service); + + grd_throttler_handle_connection (rdp_server->throttler, + connection); + return TRUE; +} + +static void +accept_connection (GrdRdpServer *rdp_server, + GSocketConnection *connection) +{ + GrdSessionRdp *session_rdp; + + g_debug ("Creating new RDP session"); + + if (!(session_rdp = grd_session_rdp_new (rdp_server, connection))) + return; + + rdp_server->sessions = g_list_append (rdp_server->sessions, session_rdp); + + g_signal_connect (session_rdp, "stopped", + G_CALLBACK (on_session_stopped), + rdp_server); + + g_signal_connect (session_rdp, "post-connected", + G_CALLBACK (on_session_post_connect), + rdp_server); +} + +static void +allow_connection_accept_cb (GrdThrottler *throttler, + GSocketConnection *connection, + gpointer user_data) +{ + GrdRdpServer *rdp_server = GRD_RDP_SERVER (user_data); + + accept_connection (rdp_server, connection); +} + +void +grd_rdp_server_notify_incoming (GSocketService *service, + GSocketConnection *connection) +{ + GrdRdpServer *rdp_server = GRD_RDP_SERVER (service); + GrdRuntimeMode runtime_mode = grd_context_get_runtime_mode (rdp_server->context); + + switch (runtime_mode) + { + case GRD_RUNTIME_MODE_HANDOVER: + accept_connection (rdp_server, connection); + break; + case GRD_RUNTIME_MODE_SYSTEM: + case GRD_RUNTIME_MODE_SCREEN_SHARE: + case GRD_RUNTIME_MODE_HEADLESS: + g_assert_not_reached (); + } +} + +void +grd_rdp_server_stop_sessions (GrdRdpServer *rdp_server) +{ + while (rdp_server->sessions) + { + GrdSession *session = rdp_server->sessions->data; + + grd_session_stop (session); + } +} + +static gboolean +attempt_to_bind_port (gpointer user_data) +{ + GrdRdpServer *rdp_server = user_data; + GrdSettings *settings = grd_context_get_settings (rdp_server->context); + GrdDBusRemoteDesktopRdpServer *rdp_server_iface = + grd_context_get_rdp_server_interface (rdp_server->context); + int rdp_port = 0; + uint16_t selected_rdp_port = 0; + + g_assert (rdp_server->pending_binding_attempts > 0); + --rdp_server->pending_binding_attempts; + + g_object_get (G_OBJECT (settings), + "rdp-port", &rdp_port, + NULL); + g_assert (rdp_port != 0); + + g_debug ("[RDP] Trying to bind to TCP socket:"); + if (grd_bind_socket (G_SOCKET_LISTENER (rdp_server), + rdp_port, + &selected_rdp_port, + FALSE, + NULL)) + { + g_assert (selected_rdp_port != 0); + grd_dbus_remote_desktop_rdp_server_set_port (rdp_server_iface, + selected_rdp_port); + + rdp_server->binding_timeout_source_id = 0; + return G_SOURCE_REMOVE; + } + + if (rdp_server->pending_binding_attempts == 0) + { + g_warning ("Failed to bind port %d after %u attempts", + rdp_port, RDP_SERVER_N_BINDING_ATTEMPTS); + + g_signal_emit (rdp_server, signals[BINDING_FAILED], 0); + + rdp_server->binding_timeout_source_id = 0; + return G_SOURCE_REMOVE; + } + + return G_SOURCE_CONTINUE; +} + +static gboolean +bind_socket (GrdRdpServer *rdp_server, + GError **error) +{ + GrdSettings *settings = grd_context_get_settings (rdp_server->context); + GrdRuntimeMode runtime_mode = grd_context_get_runtime_mode (rdp_server->context); + GrdDBusRemoteDesktopRdpServer *rdp_server_iface = + grd_context_get_rdp_server_interface (rdp_server->context); + int rdp_port = 0; + uint16_t selected_rdp_port = 0; + gboolean negotiate_port; + + g_socket_listener_set_backlog (G_SOCKET_LISTENER (rdp_server), + RDP_SERVER_SOCKET_BACKLOG_COUNT); + + g_object_get (G_OBJECT (settings), + "rdp-port", &rdp_port, + "rdp-negotiate-port", &negotiate_port, + NULL); + + if (runtime_mode != GRD_RUNTIME_MODE_HANDOVER) + { + grd_dbus_remote_desktop_rdp_server_emit_binding (rdp_server_iface, + rdp_port); + } + + switch (runtime_mode) + { + case GRD_RUNTIME_MODE_SCREEN_SHARE: + case GRD_RUNTIME_MODE_HEADLESS: + g_debug ("[RDP] Trying to bind to TCP socket:"); + if (!grd_bind_socket (G_SOCKET_LISTENER (rdp_server), + rdp_port, + &selected_rdp_port, + negotiate_port, + error)) + return FALSE; + break; + case GRD_RUNTIME_MODE_SYSTEM: + g_debug ("[RDP] Trying to bind to TCP socket:"); + if (grd_bind_socket (G_SOCKET_LISTENER (rdp_server), + rdp_port, + &selected_rdp_port, + FALSE, + error)) + break; + + g_assert (!rdp_server->binding_timeout_source_id); + rdp_server->binding_timeout_source_id = + g_timeout_add (RDP_SERVER_BINDING_ATTEMPT_INTERVAL_MS, + attempt_to_bind_port, + rdp_server); + return TRUE; + case GRD_RUNTIME_MODE_HANDOVER: + return TRUE; + } + + g_assert (selected_rdp_port != 0); + grd_dbus_remote_desktop_rdp_server_set_port (rdp_server_iface, + selected_rdp_port); + + return TRUE; +} + +gboolean +grd_rdp_server_start (GrdRdpServer *rdp_server, + GError **error) +{ + GrdRuntimeMode runtime_mode = grd_context_get_runtime_mode (rdp_server->context); + GrdDBusRemoteDesktopRdpServer *rdp_server_iface = + grd_context_get_rdp_server_interface (rdp_server->context); + + if (!bind_socket (rdp_server, error)) + return FALSE; + + switch (runtime_mode) + { + case GRD_RUNTIME_MODE_SYSTEM: + g_assert (!rdp_server->cancellable); + rdp_server->cancellable = g_cancellable_new (); + G_GNUC_FALLTHROUGH; + case GRD_RUNTIME_MODE_SCREEN_SHARE: + case GRD_RUNTIME_MODE_HEADLESS: + g_signal_connect (rdp_server, "incoming", G_CALLBACK (on_incoming), NULL); + break; + case GRD_RUNTIME_MODE_HANDOVER: + break; + } + + grd_dbus_remote_desktop_rdp_server_set_enabled (rdp_server_iface, TRUE); + + return TRUE; +} + +void +grd_rdp_server_stop (GrdRdpServer *rdp_server) +{ + GrdDBusRemoteDesktopRdpServer *rdp_server_iface; + + g_socket_service_stop (G_SOCKET_SERVICE (rdp_server)); + g_socket_listener_close (G_SOCKET_LISTENER (rdp_server)); + + rdp_server_iface = grd_context_get_rdp_server_interface (rdp_server->context); + grd_dbus_remote_desktop_rdp_server_set_enabled (rdp_server_iface, FALSE); + grd_dbus_remote_desktop_rdp_server_set_port (rdp_server_iface, -1); + + while (rdp_server->sessions) + { + GrdSession *session = rdp_server->sessions->data; + + grd_session_stop (session); + } + + g_clear_handle_id (&rdp_server->cleanup_sessions_idle_id, g_source_remove); + grd_rdp_server_cleanup_stopped_sessions (rdp_server); + + g_clear_object (&rdp_server->throttler); + + if (rdp_server->cancellable) + { + g_cancellable_cancel (rdp_server->cancellable); + g_clear_object (&rdp_server->cancellable); + } + + g_clear_handle_id (&rdp_server->binding_timeout_source_id, g_source_remove); + + g_clear_object (&rdp_server->hwaccel_nvidia); + g_clear_object (&rdp_server->hwaccel_vulkan); +} + +static void +grd_rdp_server_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GrdRdpServer *rdp_server = GRD_RDP_SERVER (object); + + switch (prop_id) + { + case PROP_CONTEXT: + rdp_server->context = g_value_get_object (value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +grd_rdp_server_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GrdRdpServer *rdp_server = GRD_RDP_SERVER (object); + + switch (prop_id) + { + case PROP_CONTEXT: + g_value_set_object (value, rdp_server->context); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +grd_rdp_server_dispose (GObject *object) +{ + GrdRdpServer *rdp_server = GRD_RDP_SERVER (object); + + g_assert (!rdp_server->sessions); + g_assert (!rdp_server->binding_timeout_source_id); + g_assert (!rdp_server->cleanup_sessions_idle_id); + g_assert (!rdp_server->stopped_sessions); + g_assert (!rdp_server->throttler); + + g_assert (!rdp_server->hwaccel_nvidia); + g_assert (!rdp_server->hwaccel_vulkan); + + G_OBJECT_CLASS (grd_rdp_server_parent_class)->dispose (object); +} + +static void +grd_rdp_server_constructed (GObject *object) +{ + GrdRdpServer *rdp_server = GRD_RDP_SERVER (object); + GrdRuntimeMode runtime_mode = + grd_context_get_runtime_mode (rdp_server->context); + GrdThrottlerAllowCallback allow_callback = NULL; + + switch (runtime_mode) + { + case GRD_RUNTIME_MODE_SCREEN_SHARE: + case GRD_RUNTIME_MODE_HEADLESS: + allow_callback = allow_connection_accept_cb; + break; + case GRD_RUNTIME_MODE_SYSTEM: + allow_callback = allow_connection_peek_cb; + break; + case GRD_RUNTIME_MODE_HANDOVER: + break; + } + + if (allow_callback) + { + rdp_server->throttler = + grd_throttler_new (grd_throttler_limits_new (rdp_server->context), + allow_callback, + rdp_server); + } + + rdp_server->pending_binding_attempts = RDP_SERVER_N_BINDING_ATTEMPTS; + + winpr_InitializeSSL (WINPR_SSL_INIT_DEFAULT); + WTSRegisterWtsApiFunctionTable (FreeRDP_InitWtsApi ()); + + /* + * Run the primitives benchmark here to save time, when initializing a session + */ + primitives_get (); + + G_OBJECT_CLASS (grd_rdp_server_parent_class)->constructed (object); +} + +static void +grd_rdp_server_init (GrdRdpServer *rdp_server) +{ +} + +static void +grd_rdp_server_class_init (GrdRdpServerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->set_property = grd_rdp_server_set_property; + object_class->get_property = grd_rdp_server_get_property; + object_class->dispose = grd_rdp_server_dispose; + object_class->constructed = grd_rdp_server_constructed; + + g_object_class_install_property (object_class, + PROP_CONTEXT, + g_param_spec_object ("context", + "GrdContext", + "The GrdContext instance", + GRD_TYPE_CONTEXT, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + signals[INCOMING_NEW_CONNECTION] = g_signal_new ("incoming-new-connection", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, 1, GRD_TYPE_SESSION); + signals[INCOMING_REDIRECTED_CONNECTION] = g_signal_new ("incoming-redirected-connection", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, 3, + G_TYPE_STRING, + G_TYPE_BOOLEAN, + G_TYPE_SOCKET_CONNECTION); + signals[BINDING_FAILED] = g_signal_new ("binding-failed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, 0); +} diff --git a/grd-rdp-server.h b/grd-rdp-server.h new file mode 100644 index 0000000..7f01d30 --- /dev/null +++ b/grd-rdp-server.h @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2020 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include +#include + +#include "grd-types.h" + +#define GRD_TYPE_RDP_SERVER (grd_rdp_server_get_type ()) +G_DECLARE_FINAL_TYPE (GrdRdpServer, + grd_rdp_server, + GRD, RDP_SERVER, + GSocketService) + +GrdContext *grd_rdp_server_get_context (GrdRdpServer *rdp_server); + +GrdHwAccelNvidia *grd_rdp_server_get_hwaccel_nvidia (GrdRdpServer *rdp_server); + +GrdHwAccelVulkan *grd_rdp_server_get_hwaccel_vulkan (GrdRdpServer *rdp_server); + +gboolean grd_rdp_server_start (GrdRdpServer *rdp_server, + GError **error); + +void grd_rdp_server_stop (GrdRdpServer *rdp_server); + +GrdRdpServer *grd_rdp_server_new (GrdContext *context); + +void grd_rdp_server_notify_incoming (GSocketService *service, + GSocketConnection *connection); + +void grd_rdp_server_stop_sessions (GrdRdpServer *rdp_server); diff --git a/grd-rdp-session-metrics.c b/grd-rdp-session-metrics.c new file mode 100644 index 0000000..15e959d --- /dev/null +++ b/grd-rdp-session-metrics.c @@ -0,0 +1,291 @@ +/* + * Copyright (C) 2023 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-rdp-session-metrics.h" + +#include "grd-rdp-surface.h" + +typedef struct +{ + gboolean pending_frame_reception; + gboolean pending_frame_transmission; + int64_t first_frame_reception; + int64_t first_frame_transmission; + uint32_t skipped_frames; +} SurfaceMetrics; + +struct _GrdRdpSessionMetrics +{ + GObject parent; + + GrdRdpPhase phase; + + int64_t rd_session_init_us; + int64_t rd_session_post_connect_us; + int64_t rd_session_ready_us; + int64_t rd_session_started_us; + + gboolean pending_layout_change; + int64_t layout_change_notification; + + GMutex metrics_mutex; + GHashTable *surface_metrics_table; + uint32_t n_pending_surface_metrics; + + gboolean pending_output; +}; + +G_DEFINE_TYPE (GrdRdpSessionMetrics, grd_rdp_session_metrics, G_TYPE_OBJECT) + +void +grd_rdp_session_metrics_notify_phase_completion (GrdRdpSessionMetrics *session_metrics, + GrdRdpPhase phase) +{ + g_assert (session_metrics->pending_output); + + g_assert (phase > session_metrics->phase); + session_metrics->phase = phase; + + switch (phase) + { + case GRD_RDP_PHASE_SESSION_INITIALIZATION: + g_assert_not_reached (); + break; + case GRD_RDP_PHASE_POST_CONNECT: + session_metrics->rd_session_post_connect_us = g_get_monotonic_time (); + break; + case GRD_RDP_PHASE_SESSION_READY: + session_metrics->rd_session_ready_us = g_get_monotonic_time (); + break; + case GRD_RDP_PHASE_SESSION_STARTED: + session_metrics->rd_session_started_us = g_get_monotonic_time (); + break; + } +} + +void +grd_rdp_session_metrics_notify_frame_reception (GrdRdpSessionMetrics *session_metrics, + GrdRdpSurface *rdp_surface) +{ + SurfaceMetrics *surface_metrics = NULL; + g_autoptr (GMutexLocker) locker = NULL; + + g_assert (session_metrics->phase == GRD_RDP_PHASE_SESSION_STARTED); + + locker = g_mutex_locker_new (&session_metrics->metrics_mutex); + if (session_metrics->pending_layout_change) + return; + + if (!session_metrics->pending_output) + return; + + if (!g_hash_table_lookup_extended (session_metrics->surface_metrics_table, + rdp_surface, + NULL, (gpointer *) &surface_metrics)) + g_assert_not_reached (); + + if (surface_metrics->pending_frame_reception) + { + g_assert (surface_metrics->pending_frame_transmission); + + surface_metrics->first_frame_reception = g_get_monotonic_time (); + surface_metrics->pending_frame_reception = FALSE; + } + else if (surface_metrics->pending_frame_transmission) + { + ++surface_metrics->skipped_frames; + } +} + +static void +print_session_metrics (GrdRdpSessionMetrics *session_metrics) +{ + GrdRdpSurface *rdp_surface = NULL; + SurfaceMetrics *surface_metrics = NULL; + GHashTableIter iter; + + g_debug ("[RDP] Remote Desktop session metrics: _post_connect: %lims, " + "_ready: %lims, _started: %lims, whole startup: %lims", + (session_metrics->rd_session_post_connect_us - + session_metrics->rd_session_init_us) / 1000, + (session_metrics->rd_session_ready_us - + session_metrics->rd_session_post_connect_us) / 1000, + (session_metrics->rd_session_started_us - + session_metrics->rd_session_ready_us) / 1000, + (session_metrics->rd_session_started_us - + session_metrics->rd_session_init_us) / 1000); + + g_hash_table_iter_init (&iter, session_metrics->surface_metrics_table); + while (g_hash_table_iter_next (&iter, (gpointer *) &rdp_surface, + (gpointer *) &surface_metrics)) + { + GrdRdpSurfaceMapping *surface_mapping = + grd_rdp_surface_get_mapping (rdp_surface); + GrdRdpSurfaceMappingType mapping_type = surface_mapping->mapping_type; + + g_assert (!surface_metrics->pending_frame_reception); + g_assert (!surface_metrics->pending_frame_transmission); + + g_assert (mapping_type == GRD_RDP_SURFACE_MAPPING_TYPE_MAP_TO_OUTPUT); + + g_debug ("[RDP] Surface %ux%u (%i, %i) Metrics: First frame: " + "Reception: %lims, Transmission: %lims, Skipped frames: %u", + grd_rdp_surface_get_width (rdp_surface), + grd_rdp_surface_get_height (rdp_surface), + surface_mapping->output_origin_x, + surface_mapping->output_origin_y, + (surface_metrics->first_frame_reception - + session_metrics->layout_change_notification) / 1000, + (surface_metrics->first_frame_transmission - + surface_metrics->first_frame_reception) / 1000, + surface_metrics->skipped_frames); + } +} + +void +grd_rdp_session_metrics_notify_frame_transmission (GrdRdpSessionMetrics *session_metrics, + GrdRdpSurface *rdp_surface) +{ + SurfaceMetrics *surface_metrics = NULL; + g_autoptr (GMutexLocker) locker = NULL; + + g_assert (session_metrics->phase == GRD_RDP_PHASE_SESSION_STARTED); + + locker = g_mutex_locker_new (&session_metrics->metrics_mutex); + g_assert (!session_metrics->pending_layout_change); + + if (!session_metrics->pending_output) + return; + + if (!g_hash_table_lookup_extended (session_metrics->surface_metrics_table, + rdp_surface, + NULL, (gpointer *) &surface_metrics)) + g_assert_not_reached (); + + g_assert (!surface_metrics->pending_frame_reception); + + if (surface_metrics->pending_frame_transmission) + { + surface_metrics->first_frame_transmission = g_get_monotonic_time (); + surface_metrics->pending_frame_transmission = FALSE; + + g_assert (session_metrics->n_pending_surface_metrics > 0); + --session_metrics->n_pending_surface_metrics; + } + + if (session_metrics->n_pending_surface_metrics == 0) + { + print_session_metrics (session_metrics); + session_metrics->pending_output = FALSE; + } +} + +void +grd_rdp_session_metrics_notify_layout_change (GrdRdpSessionMetrics *session_metrics) +{ + g_autoptr (GMutexLocker) locker = NULL; + + g_assert (session_metrics->phase == GRD_RDP_PHASE_SESSION_STARTED); + + locker = g_mutex_locker_new (&session_metrics->metrics_mutex); + session_metrics->layout_change_notification = g_get_monotonic_time (); + session_metrics->pending_layout_change = TRUE; +} + +void +grd_rdp_session_metrics_prepare_surface_metrics (GrdRdpSessionMetrics *session_metrics, + GList *surfaces) +{ + g_autoptr (GMutexLocker) locker = NULL; + GList *l; + + g_assert (session_metrics->phase == GRD_RDP_PHASE_SESSION_STARTED); + + locker = g_mutex_locker_new (&session_metrics->metrics_mutex); + g_hash_table_remove_all (session_metrics->surface_metrics_table); + session_metrics->n_pending_surface_metrics = 0; + + for (l = surfaces; l; l = l->next) + { + GrdRdpSurface *rdp_surface = l->data; + SurfaceMetrics *surface_metrics; + + surface_metrics = g_new0 (SurfaceMetrics, 1); + surface_metrics->pending_frame_reception = TRUE; + surface_metrics->pending_frame_transmission = TRUE; + + ++session_metrics->n_pending_surface_metrics; + g_hash_table_insert (session_metrics->surface_metrics_table, + rdp_surface, surface_metrics); + } + + session_metrics->pending_layout_change = FALSE; +} + +GrdRdpSessionMetrics * +grd_rdp_session_metrics_new (void) +{ + return g_object_new (GRD_TYPE_RDP_SESSION_METRICS, NULL); +} + +static void +grd_rdp_session_metrics_dispose (GObject *object) +{ + GrdRdpSessionMetrics *session_metrics = GRD_RDP_SESSION_METRICS (object); + + g_clear_pointer (&session_metrics->surface_metrics_table, g_hash_table_unref); + + G_OBJECT_CLASS (grd_rdp_session_metrics_parent_class)->dispose (object); +} + +static void +grd_rdp_session_metrics_finalize (GObject *object) +{ + GrdRdpSessionMetrics *session_metrics = GRD_RDP_SESSION_METRICS (object); + + g_mutex_clear (&session_metrics->metrics_mutex); + + G_OBJECT_CLASS (grd_rdp_session_metrics_parent_class)->finalize (object); +} + +static void +grd_rdp_session_metrics_init (GrdRdpSessionMetrics *session_metrics) +{ + session_metrics->pending_layout_change = TRUE; + session_metrics->pending_output = TRUE; + + session_metrics->phase = GRD_RDP_PHASE_SESSION_INITIALIZATION; + session_metrics->rd_session_init_us = g_get_monotonic_time (); + + session_metrics->surface_metrics_table = g_hash_table_new_full (NULL, NULL, + NULL, g_free); + + g_mutex_init (&session_metrics->metrics_mutex); +} + +static void +grd_rdp_session_metrics_class_init (GrdRdpSessionMetricsClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = grd_rdp_session_metrics_dispose; + object_class->finalize = grd_rdp_session_metrics_finalize; +} diff --git a/grd-rdp-session-metrics.h b/grd-rdp-session-metrics.h new file mode 100644 index 0000000..64e0cc8 --- /dev/null +++ b/grd-rdp-session-metrics.h @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2023 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include + +#include "grd-types.h" + +#define GRD_TYPE_RDP_SESSION_METRICS (grd_rdp_session_metrics_get_type ()) +G_DECLARE_FINAL_TYPE (GrdRdpSessionMetrics, grd_rdp_session_metrics, + GRD, RDP_SESSION_METRICS, GObject) + +typedef enum +{ + GRD_RDP_PHASE_SESSION_INITIALIZATION, + GRD_RDP_PHASE_POST_CONNECT, + GRD_RDP_PHASE_SESSION_READY, + GRD_RDP_PHASE_SESSION_STARTED, +} GrdRdpPhase; + +GrdRdpSessionMetrics *grd_rdp_session_metrics_new (void); + +void grd_rdp_session_metrics_notify_phase_completion (GrdRdpSessionMetrics *session_metrics, + GrdRdpPhase phase); + +void grd_rdp_session_metrics_notify_frame_reception (GrdRdpSessionMetrics *session_metrics, + GrdRdpSurface *rdp_surface); + +void grd_rdp_session_metrics_notify_frame_transmission (GrdRdpSessionMetrics *session_metrics, + GrdRdpSurface *rdp_surface); + +void grd_rdp_session_metrics_notify_layout_change (GrdRdpSessionMetrics *session_metrics); + +void grd_rdp_session_metrics_prepare_surface_metrics (GrdRdpSessionMetrics *session_metrics, + GList *surfaces); diff --git a/grd-rdp-stream-owner.c b/grd-rdp-stream-owner.c new file mode 100644 index 0000000..ac1615e --- /dev/null +++ b/grd-rdp-stream-owner.c @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2023 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-rdp-stream-owner.h" + +G_DEFINE_ABSTRACT_TYPE (GrdRdpStreamOwner, grd_rdp_stream_owner, G_TYPE_OBJECT) + +void +grd_rdp_stream_owner_notify_stream_created (GrdRdpStreamOwner *stream_owner, + uint32_t stream_id, + GrdStream *stream) +{ + GrdRdpStreamOwnerClass *klass = GRD_RDP_STREAM_OWNER_GET_CLASS (stream_owner); + + klass->on_stream_created (stream_owner, stream_id, stream); +} + +static void +grd_rdp_stream_owner_init (GrdRdpStreamOwner *stream_owner) +{ +} + +static void +grd_rdp_stream_owner_class_init (GrdRdpStreamOwnerClass *klass) +{ +} diff --git a/grd-rdp-stream-owner.h b/grd-rdp-stream-owner.h new file mode 100644 index 0000000..39da84f --- /dev/null +++ b/grd-rdp-stream-owner.h @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2023 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include +#include + +#include "grd-types.h" + +#define GRD_TYPE_RDP_STREAM_OWNER (grd_rdp_stream_owner_get_type ()) +G_DECLARE_DERIVABLE_TYPE (GrdRdpStreamOwner, grd_rdp_stream_owner, + GRD, RDP_STREAM_OWNER, GObject) + +struct _GrdRdpStreamOwnerClass +{ + GObjectClass parent_class; + + void (* on_stream_created) (GrdRdpStreamOwner *stream_owner, + uint32_t stream_id, + GrdStream *stream); +}; + +void grd_rdp_stream_owner_notify_stream_created (GrdRdpStreamOwner *stream_owner, + uint32_t stream_id, + GrdStream *stream); diff --git a/grd-rdp-surface-renderer.c b/grd-rdp-surface-renderer.c new file mode 100644 index 0000000..1cb4b61 --- /dev/null +++ b/grd-rdp-surface-renderer.c @@ -0,0 +1,1020 @@ +/* + * Copyright (C) 2023 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-rdp-surface-renderer.h" + +#include + +#include "grd-rdp-buffer.h" +#include "grd-rdp-damage-detector.h" +#include "grd-rdp-frame.h" +#include "grd-rdp-legacy-buffer.h" +#include "grd-rdp-pw-buffer.h" +#include "grd-rdp-render-context.h" +#include "grd-rdp-renderer.h" +#include "grd-rdp-session-metrics.h" +#include "grd-rdp-surface.h" +#include "grd-session-rdp.h" + +#define FRAME_UPGRADE_DELAY_US (60 * 1000) +#define UNLIMITED_FRAME_SLOTS (UINT32_MAX) +/* + * The value of the transition time is based upon testing. During times where + * a lot of frames are submitted, the frame-controller could quickly alternate + * between throttling and no-throttling. In these situations, the client is + * ready for a frame containing only a main view, but not for a dual view. + */ +#define TRANSITION_TIME_US (200 * 1000) + +typedef enum +{ + OBJECT_TYPE_BUFFER, + OBJECT_TYPE_RENDER_CONTEXT, +} ObjectType; + +typedef struct +{ + ObjectType object_type; + gpointer object; +} UnrefContext; + +typedef struct +{ + GrdRdpSurfaceRenderer *surface_renderer; + + GrdRdpBuffer *current_buffer; + GrdRdpBuffer *last_buffer; +} GrdRdpFrameContext; + +struct _GrdRdpSurfaceRenderer +{ + GObject parent; + + GrdRdpSurface *rdp_surface; + GrdRdpRenderer *renderer; + + uint32_t refresh_rate; + + GSource *object_unref_source; + GAsyncQueue *unref_queue; + + GSource *render_source; + gboolean pending_render_context_reset; + + GSource *frame_upgrade_source; + gboolean pending_frame_upgrade; + + GSource *trigger_frame_upgrade_source; + + gboolean graphics_subsystem_failed; + + uint32_t total_frame_slots; + GHashTable *assigned_frame_slots; + int64_t unthrottled_since_us; + + GMutex render_mutex; + GrdRdpBuffer *pending_buffer; + GrdRdpBuffer *last_buffer; + GHashTable *acquired_buffers; + + GHashTable *registered_buffers; + GrdRdpBufferInfo *rdp_buffer_info; +}; + +G_DEFINE_TYPE (GrdRdpSurfaceRenderer, grd_rdp_surface_renderer, G_TYPE_OBJECT) + +static UnrefContext * +unref_context_new (ObjectType object_type, + gpointer object) +{ + UnrefContext *unref_context; + + unref_context = g_new0 (UnrefContext, 1); + unref_context->object_type = object_type; + unref_context->object = object; + + return unref_context; +} + +uint32_t +grd_rdp_surface_renderer_get_refresh_rate (GrdRdpSurfaceRenderer *surface_renderer) +{ + return surface_renderer->refresh_rate; +} + +GrdRdpBufferInfo * +grd_rdp_surface_renderer_get_buffer_info (GrdRdpSurfaceRenderer *surface_renderer) +{ + return surface_renderer->rdp_buffer_info; +} + +uint32_t +grd_rdp_surface_renderer_get_total_frame_slots (GrdRdpSurfaceRenderer *surface_renderer) +{ + return surface_renderer->total_frame_slots; +} + +static void +trigger_frame_upgrade_schedule (GrdRdpSurfaceRenderer *surface_renderer) +{ + g_source_set_ready_time (surface_renderer->trigger_frame_upgrade_source, 0); +} + +void +grd_rdp_surface_renderer_update_total_frame_slots (GrdRdpSurfaceRenderer *surface_renderer, + uint32_t total_frame_slots) +{ + uint32_t old_slot_count = surface_renderer->total_frame_slots; + + if (old_slot_count < UNLIMITED_FRAME_SLOTS && + total_frame_slots == UNLIMITED_FRAME_SLOTS) + surface_renderer->unthrottled_since_us = g_get_monotonic_time (); + + surface_renderer->total_frame_slots = total_frame_slots; + if (old_slot_count < surface_renderer->total_frame_slots) + grd_rdp_surface_renderer_trigger_render_source (surface_renderer); + + if (old_slot_count < UNLIMITED_FRAME_SLOTS && + surface_renderer->total_frame_slots == UNLIMITED_FRAME_SLOTS) + trigger_frame_upgrade_schedule (surface_renderer); +} + +void +grd_rdp_surface_renderer_notify_frame_upgrade_state (GrdRdpSurfaceRenderer *surface_renderer, + gboolean can_upgrade_frame) +{ + surface_renderer->pending_frame_upgrade = can_upgrade_frame; +} + +static gboolean +is_buffer_combination_valid (GrdRdpSurfaceRenderer *surface_renderer, + GrdRdpPwBuffer *rdp_pw_buffer, + GError **error) +{ + GrdRdpBufferInfo *rdp_buffer_info = surface_renderer->rdp_buffer_info; + GrdRdpBufferType buffer_type; + + if (g_hash_table_size (surface_renderer->registered_buffers) == 0) + return TRUE; + + g_assert (rdp_buffer_info); + + buffer_type = grd_rdp_pw_buffer_get_buffer_type (rdp_pw_buffer); + if (buffer_type != rdp_buffer_info->buffer_type) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Invalid buffer combination: Mixed buffer types"); + return FALSE; + } + + /* + * No need to check whether an implicit or explicit DRM format modifier is + * used per buffer, since per PipeWire stream there is only one DRM format + * modifier and that one is used by all PW buffers of that PW stream. + */ + + return TRUE; +} + +static gboolean +is_render_context_reset_required (GrdRdpSurfaceRenderer *surface_renderer, + GrdRdpPwBuffer *rdp_pw_buffer, + uint64_t new_drm_format_modifier) +{ + GrdRdpBufferInfo *rdp_buffer_info; + GrdRdpBufferType buffer_type; + uint64_t current_drm_format_modifier; + + if (g_hash_table_size (surface_renderer->registered_buffers) > 0) + return FALSE; + + rdp_buffer_info = surface_renderer->rdp_buffer_info; + if (!rdp_buffer_info) + return FALSE; + + buffer_type = rdp_buffer_info->buffer_type; + if (buffer_type != grd_rdp_pw_buffer_get_buffer_type (rdp_pw_buffer)) + return TRUE; + + current_drm_format_modifier = rdp_buffer_info->drm_format_modifier; + if (current_drm_format_modifier != DRM_FORMAT_MOD_INVALID && + new_drm_format_modifier == DRM_FORMAT_MOD_INVALID) + return TRUE; + if (current_drm_format_modifier == DRM_FORMAT_MOD_INVALID && + new_drm_format_modifier != DRM_FORMAT_MOD_INVALID) + return TRUE; + + return FALSE; +} + +static GrdRdpBufferInfo * +rdp_buffer_info_new (GrdRdpPwBuffer *rdp_pw_buffer, + uint32_t drm_format, + uint64_t drm_format_modifier) +{ + GrdRdpBufferType buffer_type = + grd_rdp_pw_buffer_get_buffer_type (rdp_pw_buffer); + GrdRdpBufferInfo *rdp_buffer_info; + + rdp_buffer_info = g_new0 (GrdRdpBufferInfo, 1); + rdp_buffer_info->buffer_type = buffer_type; + rdp_buffer_info->drm_format = drm_format; + rdp_buffer_info->drm_format_modifier = drm_format_modifier; + + return rdp_buffer_info; +} + +gboolean +grd_rdp_surface_renderer_register_pw_buffer (GrdRdpSurfaceRenderer *surface_renderer, + GrdRdpPwBuffer *rdp_pw_buffer, + uint32_t drm_format, + uint64_t drm_format_modifier, + GError **error) +{ + GrdRdpSurface *rdp_surface = surface_renderer->rdp_surface; + g_autofree GrdRdpBufferInfo *rdp_buffer_info = NULL; + GrdVkDevice *vk_device = + grd_rdp_renderer_get_vk_device (surface_renderer->renderer); + GrdRdpBuffer *rdp_buffer; + + if (!is_buffer_combination_valid (surface_renderer, rdp_pw_buffer, error)) + return FALSE; + + rdp_buffer_info = rdp_buffer_info_new (rdp_pw_buffer, drm_format, + drm_format_modifier); + + rdp_buffer = grd_rdp_buffer_new (rdp_pw_buffer, rdp_buffer_info, rdp_surface, + vk_device, error); + if (!rdp_buffer) + return FALSE; + + if (is_render_context_reset_required (surface_renderer, rdp_pw_buffer, + drm_format_modifier)) + surface_renderer->pending_render_context_reset = TRUE; + + if (surface_renderer->rdp_buffer_info && + surface_renderer->rdp_buffer_info->buffer_type == GRD_RDP_BUFFER_TYPE_DMA_BUF && + surface_renderer->rdp_buffer_info->drm_format_modifier != drm_format_modifier) + g_assert (g_hash_table_size (surface_renderer->registered_buffers) == 0); + + if (g_hash_table_size (surface_renderer->registered_buffers) == 0) + g_clear_pointer (&surface_renderer->rdp_buffer_info, g_free); + + if (surface_renderer->pending_render_context_reset) + g_assert (!surface_renderer->rdp_buffer_info); + + if (!surface_renderer->rdp_buffer_info) + surface_renderer->rdp_buffer_info = g_steal_pointer (&rdp_buffer_info); + + g_hash_table_insert (surface_renderer->registered_buffers, + rdp_pw_buffer, rdp_buffer); + + return TRUE; +} + +static void +release_pw_buffer (GrdRdpBuffer *rdp_buffer) +{ + GrdRdpPwBuffer *rdp_pw_buffer = grd_rdp_buffer_get_rdp_pw_buffer (rdp_buffer); + + grd_rdp_pw_buffer_queue_pw_buffer (rdp_pw_buffer); +} + +void +grd_rdp_surface_renderer_unregister_pw_buffer (GrdRdpSurfaceRenderer *surface_renderer, + GrdRdpPwBuffer *rdp_pw_buffer) +{ + GrdRdpBuffer *rdp_buffer = NULL; + GrdRdpBuffer *buffer_to_release = NULL; + + if (!g_hash_table_lookup_extended (surface_renderer->registered_buffers, + rdp_pw_buffer, + NULL, (gpointer *) &rdp_buffer)) + g_assert_not_reached (); + + /* Ensure buffer cannot be acquired anymore */ + g_mutex_lock (&surface_renderer->render_mutex); + if (surface_renderer->pending_buffer || surface_renderer->last_buffer) + g_assert (surface_renderer->pending_buffer != surface_renderer->last_buffer); + + if (surface_renderer->pending_buffer == rdp_buffer) + buffer_to_release = g_steal_pointer (&surface_renderer->pending_buffer); + if (surface_renderer->last_buffer == rdp_buffer) + buffer_to_release = g_steal_pointer (&surface_renderer->last_buffer); + + grd_rdp_buffer_mark_for_removal (rdp_buffer); + g_mutex_unlock (&surface_renderer->render_mutex); + + /* Ensure buffer lock is released and PipeWire buffer queued again */ + grd_rdp_pw_buffer_ensure_unlocked (rdp_pw_buffer); + g_clear_pointer (&buffer_to_release, release_pw_buffer); + + g_hash_table_remove (surface_renderer->registered_buffers, rdp_pw_buffer); +} + +void +grd_rdp_surface_renderer_submit_buffer (GrdRdpSurfaceRenderer *surface_renderer, + GrdRdpPwBuffer *rdp_pw_buffer) +{ + GrdRdpBuffer *rdp_buffer = NULL; + + if (!g_hash_table_lookup_extended (surface_renderer->registered_buffers, + rdp_pw_buffer, + NULL, (gpointer *) &rdp_buffer)) + g_assert_not_reached (); + + g_mutex_lock (&surface_renderer->render_mutex); + g_clear_pointer (&surface_renderer->pending_buffer, release_pw_buffer); + + surface_renderer->pending_buffer = rdp_buffer; + g_mutex_unlock (&surface_renderer->render_mutex); + + grd_rdp_surface_renderer_trigger_render_source (surface_renderer); +} + +void +grd_rdp_surface_renderer_submit_legacy_buffer (GrdRdpSurfaceRenderer *surface_renderer, + GrdRdpLegacyBuffer *buffer) +{ + GrdRdpSurface *rdp_surface = surface_renderer->rdp_surface; + + g_mutex_lock (&surface_renderer->render_mutex); + g_clear_pointer (&rdp_surface->pending_framebuffer, grd_rdp_legacy_buffer_release); + + rdp_surface->pending_framebuffer = buffer; + g_mutex_unlock (&surface_renderer->render_mutex); + + grd_rdp_surface_renderer_trigger_render_source (surface_renderer); +} + +void +grd_rdp_surface_renderer_invalidate_surface (GrdRdpSurfaceRenderer *surface_renderer) +{ + g_autoptr (GMutexLocker) locker = NULL; + + locker = g_mutex_locker_new (&surface_renderer->render_mutex); + grd_rdp_surface_renderer_invalidate_surface_unlocked (surface_renderer); +} + +void +grd_rdp_surface_renderer_invalidate_surface_unlocked (GrdRdpSurfaceRenderer *surface_renderer) +{ + g_assert (g_hash_table_size (surface_renderer->acquired_buffers) == 0); + + if (!surface_renderer->pending_buffer) + { + surface_renderer->pending_buffer = + g_steal_pointer (&surface_renderer->last_buffer); + } + g_clear_pointer (&surface_renderer->last_buffer, release_pw_buffer); +} + +void +grd_rdp_surface_renderer_trigger_render_source (GrdRdpSurfaceRenderer *surface_renderer) +{ + g_source_set_ready_time (surface_renderer->render_source, 0); + + trigger_frame_upgrade_schedule (surface_renderer); +} + +void +grd_rdp_surface_renderer_reset (GrdRdpSurfaceRenderer *surface_renderer) +{ + GrdRdpSurface *rdp_surface = surface_renderer->rdp_surface; + + g_mutex_lock (&surface_renderer->render_mutex); + g_clear_pointer (&rdp_surface->pending_framebuffer, grd_rdp_legacy_buffer_release); + g_mutex_unlock (&surface_renderer->render_mutex); +} + +static GrdRdpBuffer * +rdp_buffer_ref (GrdRdpSurfaceRenderer *surface_renderer, + GrdRdpBuffer *rdp_buffer) +{ + GrdRdpPwBuffer *rdp_pw_buffer = grd_rdp_buffer_get_rdp_pw_buffer (rdp_buffer); + uint32_t *ref_count = NULL; + + if (g_hash_table_lookup_extended (surface_renderer->acquired_buffers, + rdp_buffer, + NULL, (gpointer *) &ref_count)) + { + g_assert (*ref_count > 0); + ++(*ref_count); + + return rdp_buffer; + } + + ref_count = g_new0 (uint32_t, 1); + *ref_count = 1; + + grd_rdp_pw_buffer_acquire_lock (rdp_pw_buffer); + g_hash_table_insert (surface_renderer->acquired_buffers, + rdp_buffer, ref_count); + + return rdp_buffer; +} + +static void +rdp_buffer_unref (GrdRdpSurfaceRenderer *surface_renderer, + GrdRdpBuffer *rdp_buffer) +{ + GrdRdpPwBuffer *rdp_pw_buffer = grd_rdp_buffer_get_rdp_pw_buffer (rdp_buffer); + uint32_t *ref_count = NULL; + + if (!g_hash_table_lookup_extended (surface_renderer->acquired_buffers, + rdp_buffer, + NULL, (gpointer *) &ref_count)) + g_assert_not_reached (); + + g_assert (*ref_count > 0); + --(*ref_count); + + if (*ref_count == 0) + { + g_assert (surface_renderer->pending_buffer != rdp_buffer); + + /* + * unregister_pw_buffer () already takes care of the PW buffer release + * here. In such case, the second condition + * (surface_renderer->last_buffer != rdp_buffer) is always true. + */ + if (!grd_rdp_buffer_is_marked_for_removal (rdp_buffer) && + surface_renderer->last_buffer != rdp_buffer) + release_pw_buffer (rdp_buffer); + + g_hash_table_remove (surface_renderer->acquired_buffers, rdp_buffer); + grd_rdp_pw_buffer_maybe_release_lock (rdp_pw_buffer); + } +} + +static void +unref_buffer (GrdRdpSurfaceRenderer *surface_renderer, + GrdRdpBuffer *rdp_buffer) +{ + g_mutex_lock (&surface_renderer->render_mutex); + g_assert (surface_renderer->pending_buffer != rdp_buffer); + + rdp_buffer_unref (surface_renderer, rdp_buffer); + g_mutex_unlock (&surface_renderer->render_mutex); +} + +static void +unref_render_context (GrdRdpSurfaceRenderer *surface_renderer, + GrdRdpRenderContext *render_context) +{ + grd_rdp_renderer_release_render_context (surface_renderer->renderer, + render_context); +} + +static gboolean +unref_objects (gpointer user_data) +{ + GrdRdpSurfaceRenderer *surface_renderer = user_data; + UnrefContext *unref_context; + + while ((unref_context = g_async_queue_try_pop (surface_renderer->unref_queue))) + { + switch (unref_context->object_type) + { + case OBJECT_TYPE_BUFFER: + unref_buffer (surface_renderer, unref_context->object); + break; + case OBJECT_TYPE_RENDER_CONTEXT: + unref_render_context (surface_renderer, unref_context->object); + break; + } + + g_free (unref_context); + } + + return G_SOURCE_CONTINUE; +} + +static gboolean +can_prepare_new_frame (GrdRdpSurfaceRenderer *surface_renderer) +{ + uint32_t total_frame_slots = surface_renderer->total_frame_slots; + uint32_t used_frame_slots; + + used_frame_slots = g_hash_table_size (surface_renderer->assigned_frame_slots); + + return total_frame_slots > used_frame_slots; +} + +static void +on_frame_picked_up (GrdRdpFrame *rdp_frame, + gpointer user_data) +{ + GrdRdpFrameContext *frame_context = user_data; + GrdRdpSurfaceRenderer *surface_renderer = frame_context->surface_renderer; + GrdRdpBuffer *current_buffer = frame_context->current_buffer; + + g_hash_table_add (surface_renderer->assigned_frame_slots, rdp_frame); + + if (!current_buffer) + return; + + g_mutex_lock (&surface_renderer->render_mutex); + if (!grd_rdp_buffer_is_marked_for_removal (current_buffer)) + surface_renderer->last_buffer = current_buffer; + else + surface_renderer->last_buffer = NULL; + g_mutex_unlock (&surface_renderer->render_mutex); +} + +static void +queue_buffer_unref (GrdRdpSurfaceRenderer *surface_renderer, + GrdRdpBuffer *rdp_buffer) +{ + UnrefContext *unref_context; + + unref_context = unref_context_new (OBJECT_TYPE_BUFFER, rdp_buffer); + g_async_queue_push (surface_renderer->unref_queue, unref_context); +} + +static void +on_view_finalization (GrdRdpFrame *rdp_frame, + gpointer user_data) +{ + GrdRdpFrameContext *frame_context = user_data; + GrdRdpSurfaceRenderer *surface_renderer = frame_context->surface_renderer; + GrdRdpBuffer *current_buffer = frame_context->current_buffer; + GrdRdpBuffer *last_buffer = frame_context->last_buffer; + + queue_buffer_unref (surface_renderer, current_buffer); + if (last_buffer) + queue_buffer_unref (surface_renderer, last_buffer); + + g_source_set_ready_time (surface_renderer->object_unref_source, 0); +} + +static void +on_frame_submission (GrdRdpFrame *rdp_frame, + gpointer user_data) +{ + GrdRdpFrameContext *frame_context = user_data; + GrdRdpSurfaceRenderer *surface_renderer = frame_context->surface_renderer; + GrdRdpSurface *rdp_surface = surface_renderer->rdp_surface; + GrdSessionRdp *session_rdp = + grd_rdp_renderer_get_session (surface_renderer->renderer); + GrdRdpSessionMetrics *session_metrics = + grd_session_rdp_get_session_metrics (session_rdp); + + grd_rdp_session_metrics_notify_frame_transmission (session_metrics, + rdp_surface); +} + +static void +reset_frame_upgrade_schedule (GrdRdpSurfaceRenderer *surface_renderer) +{ + g_source_set_ready_time (surface_renderer->frame_upgrade_source, -1); + + trigger_frame_upgrade_schedule (surface_renderer); +} + +static void +queue_render_context_unref (GrdRdpSurfaceRenderer *surface_renderer, + GrdRdpFrame *rdp_frame) +{ + GrdRdpRenderContext *render_context = + grd_rdp_frame_get_render_context (rdp_frame); + UnrefContext *unref_context; + + unref_context = unref_context_new (OBJECT_TYPE_RENDER_CONTEXT, + render_context); + g_async_queue_push (surface_renderer->unref_queue, unref_context); + + g_source_set_ready_time (surface_renderer->object_unref_source, 0); +} + +static void +on_frame_finalization (GrdRdpFrame *rdp_frame, + gpointer user_data) +{ + GrdRdpFrameContext *frame_context = user_data; + GrdRdpSurfaceRenderer *surface_renderer = frame_context->surface_renderer; + + g_hash_table_remove (surface_renderer->assigned_frame_slots, rdp_frame); + grd_rdp_surface_renderer_trigger_render_source (surface_renderer); + reset_frame_upgrade_schedule (surface_renderer); + + queue_render_context_unref (surface_renderer, rdp_frame); +} + +static gboolean +should_avoid_auxiliary_frame (GrdRdpSurfaceRenderer *surface_renderer) +{ + int64_t current_time_us; + + current_time_us = g_get_monotonic_time (); + + return g_hash_table_size (surface_renderer->assigned_frame_slots) > 0 || + surface_renderer->total_frame_slots < UNLIMITED_FRAME_SLOTS || + current_time_us - surface_renderer->unthrottled_since_us < TRANSITION_TIME_US; +} + +static void +maybe_downgrade_view_type (GrdRdpSurfaceRenderer *surface_renderer, + GrdRdpRenderContext *render_context, + GrdRdpFrame *rdp_frame) +{ + GrdRdpFrameViewType view_type; + + view_type = grd_rdp_frame_get_avc_view_type (rdp_frame); + if (view_type != GRD_RDP_FRAME_VIEW_TYPE_DUAL) + return; + + if (should_avoid_auxiliary_frame (surface_renderer) || + grd_rdp_render_context_should_avoid_dual_frame (render_context)) + grd_rdp_frame_set_avc_view_type (rdp_frame, GRD_RDP_FRAME_VIEW_TYPE_MAIN); +} + +static void +handle_pending_buffer (GrdRdpSurfaceRenderer *surface_renderer, + GrdRdpRenderContext *render_context) +{ + GrdRdpRenderer *renderer = surface_renderer->renderer; + GrdRdpFrameContext *frame_context; + GrdRdpBuffer *current_buffer = NULL; + GrdRdpBuffer *last_buffer = NULL; + GrdRdpFrame *rdp_frame; + + frame_context = g_new0 (GrdRdpFrameContext, 1); + frame_context->surface_renderer = surface_renderer; + + current_buffer = g_steal_pointer (&surface_renderer->pending_buffer); + frame_context->current_buffer = rdp_buffer_ref (surface_renderer, + current_buffer); + + if (surface_renderer->last_buffer) + { + last_buffer = surface_renderer->last_buffer; + frame_context->last_buffer = rdp_buffer_ref (surface_renderer, + last_buffer); + } + + rdp_frame = grd_rdp_frame_new (render_context, + current_buffer, last_buffer, + on_frame_picked_up, on_view_finalization, + on_frame_submission, on_frame_finalization, + frame_context, g_free); + maybe_downgrade_view_type (surface_renderer, render_context, rdp_frame); + + grd_rdp_renderer_submit_frame (renderer, render_context, rdp_frame); +} + +static void +handle_graphics_subsystem_failure (GrdRdpSurfaceRenderer *surface_renderer) +{ + GrdSessionRdp *session_rdp = + grd_rdp_renderer_get_session (surface_renderer->renderer); + + surface_renderer->graphics_subsystem_failed = TRUE; + + grd_session_rdp_notify_error (session_rdp, + GRD_SESSION_RDP_ERROR_GRAPHICS_SUBSYSTEM_FAILED); +} + +static void +maybe_encode_pending_frame (GrdRdpSurfaceRenderer *surface_renderer, + GrdRdpRenderContext *render_context) +{ + GrdRdpSurface *rdp_surface = surface_renderer->rdp_surface; + GrdSessionRdp *session_rdp = + grd_rdp_renderer_get_session (surface_renderer->renderer); + GrdRdpSessionMetrics *session_metrics = + grd_session_rdp_get_session_metrics (session_rdp); + GrdRdpLegacyBuffer *buffer; + + buffer = g_steal_pointer (&rdp_surface->pending_framebuffer); + if (!grd_rdp_damage_detector_submit_new_framebuffer (rdp_surface->detector, + buffer)) + { + grd_rdp_legacy_buffer_release (buffer); + handle_graphics_subsystem_failure (surface_renderer); + return; + } + + if (!grd_rdp_damage_detector_is_region_damaged (rdp_surface->detector)) + return; + + if (!grd_rdp_renderer_render_frame (surface_renderer->renderer, + rdp_surface, render_context, buffer)) + { + handle_graphics_subsystem_failure (surface_renderer); + return; + } + + grd_rdp_session_metrics_notify_frame_transmission (session_metrics, + rdp_surface); +} + +static gboolean +maybe_render_frame (gpointer user_data) +{ + GrdRdpSurfaceRenderer *surface_renderer = user_data; + GrdRdpRenderer *renderer = surface_renderer->renderer; + GrdRdpSurface *rdp_surface = surface_renderer->rdp_surface; + GrdRdpAcquireContextFlags acquire_flags; + GrdRdpRenderContext *render_context; + g_autoptr (GMutexLocker) locker = NULL; + + if (surface_renderer->graphics_subsystem_failed) + return G_SOURCE_CONTINUE; + + locker = g_mutex_locker_new (&surface_renderer->render_mutex); + if (!surface_renderer->pending_buffer && + !rdp_surface->pending_framebuffer) + return G_SOURCE_CONTINUE; + + g_assert (!surface_renderer->pending_buffer || + !rdp_surface->pending_framebuffer); + + g_source_set_ready_time (surface_renderer->frame_upgrade_source, -1); + + if (!can_prepare_new_frame (surface_renderer)) + return G_SOURCE_CONTINUE; + + acquire_flags = GRD_RDP_ACQUIRE_CONTEXT_FLAG_NONE; + if (surface_renderer->pending_render_context_reset) + acquire_flags |= GRD_RDP_ACQUIRE_CONTEXT_FLAG_FORCE_RESET; + + render_context = + grd_rdp_renderer_try_acquire_render_context (renderer, rdp_surface, + acquire_flags); + if (!render_context) + return G_SOURCE_CONTINUE; + + surface_renderer->pending_render_context_reset = FALSE; + + if (surface_renderer->pending_buffer) + { + handle_pending_buffer (surface_renderer, render_context); + } + else if (rdp_surface->pending_framebuffer) /* Legacy render handling */ + { + maybe_encode_pending_frame (surface_renderer, render_context); + grd_rdp_renderer_release_render_context (renderer, render_context); + } + else + { + g_assert_not_reached (); + } + + return G_SOURCE_CONTINUE; +} + +static gboolean +should_retry_frame_upgrade (GrdRdpSurfaceRenderer *surface_renderer) +{ + /* + * should_avoid_auxiliary_frame() returned true. If the following conditions + * are true too, then the transition period from throttled to unthrottled is + * not over yet. + */ + return g_hash_table_size (surface_renderer->assigned_frame_slots) == 0 && + surface_renderer->total_frame_slots == UNLIMITED_FRAME_SLOTS; +} + +static void +upgrade_frame (GrdRdpSurfaceRenderer *surface_renderer, + GrdRdpRenderContext *render_context) +{ + GrdRdpRenderer *renderer = surface_renderer->renderer; + GrdRdpFrameContext *frame_context; + GrdRdpFrame *rdp_frame; + + frame_context = g_new0 (GrdRdpFrameContext, 1); + frame_context->surface_renderer = surface_renderer; + + rdp_frame = grd_rdp_frame_new (render_context, NULL, NULL, + on_frame_picked_up, NULL, + on_frame_submission, on_frame_finalization, + frame_context, g_free); + grd_rdp_renderer_submit_frame (renderer, render_context, rdp_frame); +} + +static gboolean +maybe_upgrade_frame (gpointer user_data) +{ + GrdRdpSurfaceRenderer *surface_renderer = user_data; + GrdRdpRenderer *renderer = surface_renderer->renderer; + GrdRdpSurface *rdp_surface = surface_renderer->rdp_surface; + GrdRdpAcquireContextFlags acquire_flags; + GrdRdpRenderContext *render_context; + g_autoptr (GMutexLocker) locker = NULL; + + locker = g_mutex_locker_new (&surface_renderer->render_mutex); + if (surface_renderer->pending_buffer) + return G_SOURCE_CONTINUE; + + g_clear_pointer (&locker, g_mutex_locker_free); + + if (!surface_renderer->pending_frame_upgrade) + return G_SOURCE_CONTINUE; + + if (!can_prepare_new_frame (surface_renderer)) + return G_SOURCE_CONTINUE; + + if (should_avoid_auxiliary_frame (surface_renderer)) + { + if (should_retry_frame_upgrade (surface_renderer)) + trigger_frame_upgrade_schedule (surface_renderer); + + return G_SOURCE_CONTINUE; + } + + acquire_flags = GRD_RDP_ACQUIRE_CONTEXT_FLAG_RETAIN_OR_NULL; + + render_context = + grd_rdp_renderer_try_acquire_render_context (renderer, rdp_surface, + acquire_flags); + if (!render_context) + return G_SOURCE_CONTINUE; + + upgrade_frame (surface_renderer, render_context); + + return G_SOURCE_CONTINUE; +} + +static gboolean +trigger_frame_upgrade (gpointer user_data) +{ + GrdRdpSurfaceRenderer *surface_renderer = user_data; + int64_t current_time_us; + + if (g_source_get_ready_time (surface_renderer->frame_upgrade_source) != -1) + return G_SOURCE_CONTINUE; + + current_time_us = g_source_get_time (surface_renderer->frame_upgrade_source); + g_source_set_ready_time (surface_renderer->frame_upgrade_source, + current_time_us + FRAME_UPGRADE_DELAY_US); + + return G_SOURCE_CONTINUE; +} + +static gboolean +source_dispatch (GSource *source, + GSourceFunc callback, + gpointer user_data) +{ + g_source_set_ready_time (source, -1); + + return callback (user_data); +} + +static GSourceFuncs source_funcs = +{ + .dispatch = source_dispatch, +}; + +GrdRdpSurfaceRenderer * +grd_rdp_surface_renderer_new (GrdRdpSurface *rdp_surface, + GrdRdpRenderer *renderer, + uint32_t refresh_rate) +{ + GMainContext *graphics_context = + grd_rdp_renderer_get_graphics_context (renderer); + GrdRdpSurfaceRenderer *surface_renderer; + GSource *object_unref_source; + GSource *render_source; + GSource *frame_upgrade_source; + GSource *trigger_frame_upgrade_source; + + surface_renderer = g_object_new (GRD_TYPE_RDP_SURFACE_RENDERER, NULL); + surface_renderer->rdp_surface = rdp_surface; + surface_renderer->renderer = renderer; + surface_renderer->refresh_rate = refresh_rate; + + object_unref_source = g_source_new (&source_funcs, sizeof (GSource)); + g_source_set_callback (object_unref_source, unref_objects, + surface_renderer, NULL); + g_source_set_ready_time (object_unref_source, -1); + g_source_attach (object_unref_source, graphics_context); + surface_renderer->object_unref_source = object_unref_source; + + render_source = g_source_new (&source_funcs, sizeof (GSource)); + g_source_set_callback (render_source, maybe_render_frame, + surface_renderer, NULL); + g_source_set_ready_time (render_source, -1); + g_source_attach (render_source, graphics_context); + surface_renderer->render_source = render_source; + + frame_upgrade_source = g_source_new (&source_funcs, sizeof (GSource)); + g_source_set_callback (frame_upgrade_source, maybe_upgrade_frame, + surface_renderer, NULL); + g_source_set_ready_time (frame_upgrade_source, -1); + g_source_attach (frame_upgrade_source, graphics_context); + surface_renderer->frame_upgrade_source = frame_upgrade_source; + + trigger_frame_upgrade_source = g_source_new (&source_funcs, sizeof (GSource)); + g_source_set_callback (trigger_frame_upgrade_source, trigger_frame_upgrade, + surface_renderer, NULL); + g_source_set_ready_time (trigger_frame_upgrade_source, -1); + g_source_attach (trigger_frame_upgrade_source, graphics_context); + surface_renderer->trigger_frame_upgrade_source = trigger_frame_upgrade_source; + + return surface_renderer; +} + +static void +grd_rdp_surface_renderer_dispose (GObject *object) +{ + GrdRdpSurfaceRenderer *surface_renderer = GRD_RDP_SURFACE_RENDERER (object); + + /* + * No need to lock the render mutex here, the surface renderer outlives the + * PipeWire stream. + */ + if (surface_renderer->acquired_buffers) + g_assert (g_hash_table_size (surface_renderer->acquired_buffers) == 0); + if (surface_renderer->assigned_frame_slots) + g_assert (g_hash_table_size (surface_renderer->assigned_frame_slots) == 0); + + g_assert (g_async_queue_try_pop (surface_renderer->unref_queue) == NULL); + + if (surface_renderer->trigger_frame_upgrade_source) + { + g_source_destroy (surface_renderer->trigger_frame_upgrade_source); + g_clear_pointer (&surface_renderer->trigger_frame_upgrade_source, g_source_unref); + } + if (surface_renderer->frame_upgrade_source) + { + g_source_destroy (surface_renderer->frame_upgrade_source); + g_clear_pointer (&surface_renderer->frame_upgrade_source, g_source_unref); + } + if (surface_renderer->render_source) + { + g_source_destroy (surface_renderer->render_source); + g_clear_pointer (&surface_renderer->render_source, g_source_unref); + } + if (surface_renderer->object_unref_source) + { + g_source_destroy (surface_renderer->object_unref_source); + g_clear_pointer (&surface_renderer->object_unref_source, g_source_unref); + } + + g_clear_pointer (&surface_renderer->assigned_frame_slots, g_hash_table_unref); + g_clear_pointer (&surface_renderer->acquired_buffers, g_hash_table_unref); + g_clear_pointer (&surface_renderer->registered_buffers, g_hash_table_unref); + g_clear_pointer (&surface_renderer->rdp_buffer_info, g_free); + + G_OBJECT_CLASS (grd_rdp_surface_renderer_parent_class)->dispose (object); +} + +static void +grd_rdp_surface_renderer_finalize (GObject *object) +{ + GrdRdpSurfaceRenderer *surface_renderer = GRD_RDP_SURFACE_RENDERER (object); + + g_clear_pointer (&surface_renderer->unref_queue, g_async_queue_unref); + + g_mutex_clear (&surface_renderer->render_mutex); + + G_OBJECT_CLASS (grd_rdp_surface_renderer_parent_class)->finalize (object); +} + +static void +grd_rdp_surface_renderer_init (GrdRdpSurfaceRenderer *surface_renderer) +{ + surface_renderer->total_frame_slots = UNLIMITED_FRAME_SLOTS; + + surface_renderer->registered_buffers = + g_hash_table_new_full (NULL, NULL, + NULL, g_object_unref); + surface_renderer->acquired_buffers = + g_hash_table_new_full (NULL, NULL, + NULL, g_free); + surface_renderer->assigned_frame_slots = + g_hash_table_new (NULL, NULL); + surface_renderer->unref_queue = g_async_queue_new (); + + g_mutex_init (&surface_renderer->render_mutex); +} + +static void +grd_rdp_surface_renderer_class_init (GrdRdpSurfaceRendererClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = grd_rdp_surface_renderer_dispose; + object_class->finalize = grd_rdp_surface_renderer_finalize; +} diff --git a/grd-rdp-surface-renderer.h b/grd-rdp-surface-renderer.h new file mode 100644 index 0000000..bffa07f --- /dev/null +++ b/grd-rdp-surface-renderer.h @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2023 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include +#include + +#include "grd-types.h" + +#define GRD_TYPE_RDP_SURFACE_RENDERER (grd_rdp_surface_renderer_get_type ()) +G_DECLARE_FINAL_TYPE (GrdRdpSurfaceRenderer, grd_rdp_surface_renderer, + GRD, RDP_SURFACE_RENDERER, GObject) + +GrdRdpSurfaceRenderer *grd_rdp_surface_renderer_new (GrdRdpSurface *rdp_surface, + GrdRdpRenderer *renderer, + uint32_t refresh_rate); + +uint32_t grd_rdp_surface_renderer_get_refresh_rate (GrdRdpSurfaceRenderer *surface_renderer); + +GrdRdpBufferInfo *grd_rdp_surface_renderer_get_buffer_info (GrdRdpSurfaceRenderer *surface_renderer); + +uint32_t grd_rdp_surface_renderer_get_total_frame_slots (GrdRdpSurfaceRenderer *surface_renderer); + +void grd_rdp_surface_renderer_update_total_frame_slots (GrdRdpSurfaceRenderer *surface_renderer, + uint32_t total_frame_slots); + +void grd_rdp_surface_renderer_notify_frame_upgrade_state (GrdRdpSurfaceRenderer *surface_renderer, + gboolean can_upgrade_frame); + +gboolean grd_rdp_surface_renderer_register_pw_buffer (GrdRdpSurfaceRenderer *surface_renderer, + GrdRdpPwBuffer *rdp_pw_buffer, + uint32_t drm_format, + uint64_t drm_format_modifier, + GError **error); + +void grd_rdp_surface_renderer_unregister_pw_buffer (GrdRdpSurfaceRenderer *surface_renderer, + GrdRdpPwBuffer *rdp_pw_buffer); + +void grd_rdp_surface_renderer_submit_buffer (GrdRdpSurfaceRenderer *surface_renderer, + GrdRdpPwBuffer *rdp_pw_buffer); + +void grd_rdp_surface_renderer_submit_legacy_buffer (GrdRdpSurfaceRenderer *surface_renderer, + GrdRdpLegacyBuffer *buffer); + +void grd_rdp_surface_renderer_invalidate_surface (GrdRdpSurfaceRenderer *surface_renderer); + +void grd_rdp_surface_renderer_invalidate_surface_unlocked (GrdRdpSurfaceRenderer *surface_renderer); + +void grd_rdp_surface_renderer_trigger_render_source (GrdRdpSurfaceRenderer *surface_renderer); + +void grd_rdp_surface_renderer_reset (GrdRdpSurfaceRenderer *surface_renderer); diff --git a/grd-rdp-surface.c b/grd-rdp-surface.c new file mode 100644 index 0000000..d5da127 --- /dev/null +++ b/grd-rdp-surface.c @@ -0,0 +1,178 @@ +/* + * Copyright (C) 2021 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-rdp-surface.h" + +#include "grd-hwaccel-nvidia.h" +#include "grd-rdp-damage-detector-cuda.h" +#include "grd-rdp-damage-detector-memcmp.h" +#include "grd-rdp-renderer.h" +#include "grd-rdp-server.h" +#include "grd-session-rdp.h" + +static GrdHwAccelNvidia * +hwaccel_nvidia_from_surface (GrdRdpSurface *rdp_surface) +{ + GrdSessionRdp *session_rdp = + grd_rdp_renderer_get_session (rdp_surface->renderer); + GrdRdpServer *rdp_server = + grd_session_rdp_get_server (session_rdp); + + return grd_rdp_server_get_hwaccel_nvidia (rdp_server); +} + +static void +destroy_hwaccel_util_objects (GrdRdpSurface *rdp_surface) +{ + GrdHwAccelNvidia *hwaccel_nvidia = hwaccel_nvidia_from_surface (rdp_surface); + + if (rdp_surface->cuda_stream) + { + grd_hwaccel_nvidia_destroy_cuda_stream (hwaccel_nvidia, + rdp_surface->cuda_stream); + rdp_surface->cuda_stream = NULL; + } + if (rdp_surface->avc.main_view) + { + grd_hwaccel_nvidia_clear_mem_ptr (hwaccel_nvidia, + &rdp_surface->avc.main_view); + } +} + +GrdRdpSurface * +grd_rdp_surface_new (GrdRdpRenderer *renderer) +{ + g_autofree GrdRdpSurface *rdp_surface = NULL; + GrdHwAccelNvidia *hwaccel_nvidia; + + rdp_surface = g_malloc0 (sizeof (GrdRdpSurface)); + rdp_surface->renderer = renderer; + + hwaccel_nvidia = hwaccel_nvidia_from_surface (rdp_surface); + if (hwaccel_nvidia && + !grd_hwaccel_nvidia_create_cuda_stream (hwaccel_nvidia, + &rdp_surface->cuda_stream)) + return NULL; + + if (hwaccel_nvidia) + { + GrdRdpDamageDetectorCuda *detector; + + detector = grd_rdp_damage_detector_cuda_new (hwaccel_nvidia, + rdp_surface->cuda_stream); + if (!detector) + { + destroy_hwaccel_util_objects (rdp_surface); + return NULL; + } + + rdp_surface->detector = GRD_RDP_DAMAGE_DETECTOR (detector); + } + else + { + GrdRdpDamageDetectorMemcmp *detector; + + detector = grd_rdp_damage_detector_memcmp_new (); + rdp_surface->detector = GRD_RDP_DAMAGE_DETECTOR (detector); + } + + return g_steal_pointer (&rdp_surface); +} + +void +grd_rdp_surface_free (GrdRdpSurface *rdp_surface) +{ + g_assert (!rdp_surface->pending_framebuffer); + + g_clear_object (&rdp_surface->surface_renderer); + + g_clear_pointer (&rdp_surface->surface_mapping, g_free); + + g_clear_object (&rdp_surface->detector); + destroy_hwaccel_util_objects (rdp_surface); + + g_free (rdp_surface); +} + +uint32_t +grd_rdp_surface_get_width (GrdRdpSurface *rdp_surface) +{ + return rdp_surface->width; +} + +uint32_t +grd_rdp_surface_get_height (GrdRdpSurface *rdp_surface) +{ + return rdp_surface->height; +} + +GrdRdpSurfaceMapping * +grd_rdp_surface_get_mapping (GrdRdpSurface *rdp_surface) +{ + return rdp_surface->surface_mapping; +} + +GrdRdpSurfaceRenderer * +grd_rdp_surface_get_surface_renderer (GrdRdpSurface *rdp_surface) +{ + return rdp_surface->surface_renderer; +} + +void +grd_rdp_surface_set_size (GrdRdpSurface *rdp_surface, + uint32_t width, + uint32_t height) +{ + rdp_surface->width = width; + rdp_surface->height = height; +} + +void +grd_rdp_surface_set_mapping (GrdRdpSurface *rdp_surface, + GrdRdpSurfaceMapping *surface_mapping) +{ + g_clear_pointer (&rdp_surface->surface_mapping, g_free); + rdp_surface->surface_mapping = surface_mapping; +} + +void +grd_rdp_surface_attach_surface_renderer (GrdRdpSurface *rdp_surface, + GrdRdpSurfaceRenderer *surface_renderer) +{ + g_assert (!rdp_surface->surface_renderer); + + rdp_surface->surface_renderer = surface_renderer; +} + +void +grd_rdp_surface_reset (GrdRdpSurface *rdp_surface) +{ + rdp_surface->needs_no_local_data = FALSE; + + if (rdp_surface->avc.main_view) + { + GrdHwAccelNvidia *hwaccel_nvidia = + hwaccel_nvidia_from_surface (rdp_surface); + + grd_hwaccel_nvidia_clear_mem_ptr (hwaccel_nvidia, + &rdp_surface->avc.main_view); + } +} diff --git a/grd-rdp-surface.h b/grd-rdp-surface.h new file mode 100644 index 0000000..458bf3a --- /dev/null +++ b/grd-rdp-surface.h @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2021 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include +#include +#include + +#include "grd-types.h" + +typedef enum +{ + GRD_RDP_SURFACE_MAPPING_TYPE_MAP_TO_OUTPUT, +} GrdRdpSurfaceMappingType; + +typedef struct +{ + /* Mandatory */ + GrdRdpSurfaceMappingType mapping_type; + + /* GRD_RDP_SURFACE_MAPPING_TYPE_MAP_TO_OUTPUT */ + uint32_t output_origin_x; + uint32_t output_origin_y; +} GrdRdpSurfaceMapping; + +struct _GrdRdpSurface +{ + GrdRdpRenderer *renderer; + + uint16_t width; + uint16_t height; + + GrdRdpSurfaceMapping *surface_mapping; + + GrdRdpSurfaceRenderer *surface_renderer; + + GrdRdpLegacyBuffer *pending_framebuffer; + GrdRdpDamageDetector *detector; + + CUstream cuda_stream; + + struct + { + CUdeviceptr main_view; + } avc; + + gboolean needs_no_local_data; +}; + +GrdRdpSurface *grd_rdp_surface_new (GrdRdpRenderer *renderer); + +void grd_rdp_surface_free (GrdRdpSurface *rdp_surface); + +uint32_t grd_rdp_surface_get_width (GrdRdpSurface *rdp_surface); + +uint32_t grd_rdp_surface_get_height (GrdRdpSurface *rdp_surface); + +GrdRdpSurfaceMapping *grd_rdp_surface_get_mapping (GrdRdpSurface *rdp_surface); + +GrdRdpSurfaceRenderer *grd_rdp_surface_get_surface_renderer (GrdRdpSurface *rdp_surface); + +void grd_rdp_surface_set_size (GrdRdpSurface *rdp_surface, + uint32_t width, + uint32_t height); + +void grd_rdp_surface_set_mapping (GrdRdpSurface *rdp_surface, + GrdRdpSurfaceMapping *surface_mapping); + +void grd_rdp_surface_attach_surface_renderer (GrdRdpSurface *rdp_surface, + GrdRdpSurfaceRenderer *surface_renderer); + +void grd_rdp_surface_reset (GrdRdpSurface *rdp_surface); diff --git a/grd-rdp-sw-encoder-ca.c b/grd-rdp-sw-encoder-ca.c new file mode 100644 index 0000000..76f5655 --- /dev/null +++ b/grd-rdp-sw-encoder-ca.c @@ -0,0 +1,282 @@ +/* + * Copyright (C) 2024 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-rdp-sw-encoder-ca.h" + +#include + +struct _GrdRdpSwEncoderCa +{ + GObject parent; + + GMutex encode_mutex; + RFX_CONTEXT *rfx_context; +}; + +G_DEFINE_TYPE (GrdRdpSwEncoderCa, grd_rdp_sw_encoder_ca, G_TYPE_OBJECT) + +static void +rfx_progressive_write_message (RFX_MESSAGE *rfx_message, + wStream *s, + gboolean write_header) +{ + const RFX_RECT *rfx_rects; + uint16_t n_rfx_rects = 0; + const UINT32 *quant_vals; + uint16_t n_quant_vals = 0; + const RFX_TILE **rfx_tiles; + uint16_t n_rfx_tiles = 0; + uint32_t block_len; + const uint32_t *qv; + const RFX_TILE *rfx_tile; + uint32_t tiles_data_size; + uint16_t i; + + rfx_rects = rfx_message_get_rects (rfx_message, &n_rfx_rects); + quant_vals = rfx_message_get_quants (rfx_message, &n_quant_vals); + rfx_tiles = rfx_message_get_tiles (rfx_message, &n_rfx_tiles); + + if (write_header) + { + /* RFX_PROGRESSIVE_SYNC */ + block_len = 12; + if (!Stream_EnsureRemainingCapacity (s, block_len)) + g_assert_not_reached (); + + Stream_Write_UINT16 (s, 0xCCC0); /* blockType */ + Stream_Write_UINT32 (s, block_len); /* blockLen */ + Stream_Write_UINT32 (s, 0xCACCACCA); /* magic */ + Stream_Write_UINT16 (s, 0x0100); /* version */ + + /* RFX_PROGRESSIVE_CONTEXT */ + block_len = 10; + if (!Stream_EnsureRemainingCapacity (s, block_len)) + g_assert_not_reached (); + + Stream_Write_UINT16 (s, 0xCCC3); /* blockType */ + Stream_Write_UINT32 (s, block_len); /* blockLen */ + Stream_Write_UINT8 (s, 0); /* ctxId */ + Stream_Write_UINT16 (s, 0x0040); /* tileSize */ + Stream_Write_UINT8 (s, 0); /* flags */ + } + + /* RFX_PROGRESSIVE_FRAME_BEGIN */ + block_len = 12; + if (!Stream_EnsureRemainingCapacity (s, block_len)) + g_assert_not_reached (); + + Stream_Write_UINT16 (s, 0xCCC1); /* blockType */ + Stream_Write_UINT32 (s, block_len); /* blockLen */ + Stream_Write_UINT32 (s, rfx_message_get_frame_idx (rfx_message)); /* frameIndex */ + Stream_Write_UINT16 (s, 1); /* regionCount */ + + /* RFX_PROGRESSIVE_REGION */ + block_len = 18; + block_len += n_rfx_rects * 8; + block_len += n_quant_vals * 5; + tiles_data_size = n_rfx_tiles * 22; + + for (i = 0; i < n_rfx_tiles; i++) + { + rfx_tile = rfx_tiles[i]; + tiles_data_size += rfx_tile->YLen + rfx_tile->CbLen + rfx_tile->CrLen; + } + + block_len += tiles_data_size; + if (!Stream_EnsureRemainingCapacity (s, block_len)) + g_assert_not_reached (); + + Stream_Write_UINT16 (s, 0xCCC4); /* blockType */ + Stream_Write_UINT32 (s, block_len); /* blockLen */ + Stream_Write_UINT8 (s, 0x40); /* tileSize */ + Stream_Write_UINT16 (s, n_rfx_rects); /* numRects */ + Stream_Write_UINT8 (s, n_quant_vals); /* numQuant */ + Stream_Write_UINT8 (s, 0); /* numProgQuant */ + Stream_Write_UINT8 (s, 0); /* flags */ + Stream_Write_UINT16 (s, n_rfx_tiles); /* numTiles */ + Stream_Write_UINT32 (s, tiles_data_size); /* tilesDataSize */ + + for (i = 0; i < n_rfx_rects; i++) + { + /* TS_RFX_RECT */ + Stream_Write_UINT16 (s, rfx_rects[i].x); /* x */ + Stream_Write_UINT16 (s, rfx_rects[i].y); /* y */ + Stream_Write_UINT16 (s, rfx_rects[i].width); /* width */ + Stream_Write_UINT16 (s, rfx_rects[i].height); /* height */ + } + + /* + * The RFX_COMPONENT_CODEC_QUANT structure differs from the + * TS_RFX_CODEC_QUANT ([MS-RDPRFX] section 2.2.2.1.5) structure with respect + * to the order of the bands. + * 0 1 2 3 4 5 6 7 8 9 + * RDPRFX: LL3, LH3, HL3, HH3, LH2, HL2, HH2, LH1, HL1, HH1 + * RDPEGFX: LL3, HL3, LH3, HH3, HL2, LH2, HH2, HL1, LH1, HH1 + */ + for (i = 0, qv = quant_vals; i < n_quant_vals; ++i, qv += 10) + { + /* RFX_COMPONENT_CODEC_QUANT */ + Stream_Write_UINT8 (s, qv[0] + (qv[2] << 4)); /* LL3, HL3 */ + Stream_Write_UINT8 (s, qv[1] + (qv[3] << 4)); /* LH3, HH3 */ + Stream_Write_UINT8 (s, qv[5] + (qv[4] << 4)); /* HL2, LH2 */ + Stream_Write_UINT8 (s, qv[6] + (qv[8] << 4)); /* HH2, HL1 */ + Stream_Write_UINT8 (s, qv[7] + (qv[9] << 4)); /* LH1, HH1 */ + } + + for (i = 0; i < n_rfx_tiles; ++i) + { + /* RFX_PROGRESSIVE_TILE_SIMPLE */ + rfx_tile = rfx_tiles[i]; + block_len = 22 + rfx_tile->YLen + rfx_tile->CbLen + rfx_tile->CrLen; + Stream_Write_UINT16 (s, 0xCCC5); /* blockType */ + Stream_Write_UINT32 (s, block_len); /* blockLen */ + Stream_Write_UINT8 (s, rfx_tile->quantIdxY); /* quantIdxY */ + Stream_Write_UINT8 (s, rfx_tile->quantIdxCb); /* quantIdxCb */ + Stream_Write_UINT8 (s, rfx_tile->quantIdxCr); /* quantIdxCr */ + Stream_Write_UINT16 (s, rfx_tile->xIdx); /* xIdx */ + Stream_Write_UINT16 (s, rfx_tile->yIdx); /* yIdx */ + Stream_Write_UINT8 (s, 0); /* flags */ + Stream_Write_UINT16 (s, rfx_tile->YLen); /* YLen */ + Stream_Write_UINT16 (s, rfx_tile->CbLen); /* CbLen */ + Stream_Write_UINT16 (s, rfx_tile->CrLen); /* CrLen */ + Stream_Write_UINT16 (s, 0); /* tailLen */ + Stream_Write (s, rfx_tile->YData, rfx_tile->YLen); /* YData */ + Stream_Write (s, rfx_tile->CbData, rfx_tile->CbLen); /* CbData */ + Stream_Write (s, rfx_tile->CrData, rfx_tile->CrLen); /* CrData */ + } + + /* RFX_PROGRESSIVE_FRAME_END */ + block_len = 6; + if (!Stream_EnsureRemainingCapacity (s, block_len)) + g_assert_not_reached (); + + Stream_Write_UINT16 (s, 0xCCC2); /* blockType */ + Stream_Write_UINT32 (s, block_len); /* blockLen */ + + Stream_SealLength (s); +} + +void +grd_rdp_sw_encoder_ca_encode_progressive_frame (GrdRdpSwEncoderCa *encoder_ca, + uint32_t surface_width, + uint32_t surface_height, + uint8_t *src_buffer, + uint32_t src_stride, + cairo_region_t *damage_region, + wStream *encode_stream, + gboolean write_header) +{ + g_autoptr (GMutexLocker) locker = NULL; + g_autofree RFX_RECT *rfx_rects = NULL; + int n_rects; + RFX_MESSAGE *rfx_message; + int i; + + locker = g_mutex_locker_new (&encoder_ca->encode_mutex); + rfx_context_set_mode (encoder_ca->rfx_context, RLGR1); + rfx_context_reset (encoder_ca->rfx_context, surface_width, surface_height); + + n_rects = cairo_region_num_rectangles (damage_region); + rfx_rects = g_new0 (RFX_RECT, n_rects); + + for (i = 0; i < n_rects; ++i) + { + RFX_RECT *rfx_rect = &rfx_rects[i]; + cairo_rectangle_int_t cairo_rect; + + cairo_region_get_rectangle (damage_region, i, &cairo_rect); + + rfx_rect->x = cairo_rect.x; + rfx_rect->y = cairo_rect.y; + rfx_rect->width = cairo_rect.width; + rfx_rect->height = cairo_rect.height; + } + + rfx_message = rfx_encode_message (encoder_ca->rfx_context, + rfx_rects, n_rects, + src_buffer, + surface_width, surface_height, + src_stride); + + Stream_SetPosition (encode_stream, 0); + rfx_progressive_write_message (rfx_message, encode_stream, write_header); + rfx_message_free (encoder_ca->rfx_context, rfx_message); +} + +GrdRdpSwEncoderCa * +grd_rdp_sw_encoder_ca_new (GError **error) +{ + g_autoptr (GrdRdpSwEncoderCa) encoder_ca = NULL; + + encoder_ca = g_object_new (GRD_TYPE_RDP_SW_ENCODER_CA, NULL); + + encoder_ca->rfx_context = rfx_context_new (TRUE); + if (!encoder_ca->rfx_context) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create RFX context"); + return NULL; + } +#if G_BYTE_ORDER == G_LITTLE_ENDIAN + rfx_context_set_pixel_format (encoder_ca->rfx_context, + PIXEL_FORMAT_BGRX32); +#else + rfx_context_set_pixel_format (encoder_ca->rfx_context, + PIXEL_FORMAT_XRGB32); +#endif + + return g_steal_pointer (&encoder_ca); +} + +static void +grd_rdp_sw_encoder_ca_dispose (GObject *object) +{ + GrdRdpSwEncoderCa *encoder_ca = GRD_RDP_SW_ENCODER_CA (object); + + g_clear_pointer (&encoder_ca->rfx_context, rfx_context_free); + + G_OBJECT_CLASS (grd_rdp_sw_encoder_ca_parent_class)->dispose (object); +} + +static void +grd_rdp_sw_encoder_ca_finalize (GObject *object) +{ + GrdRdpSwEncoderCa *encoder_ca = GRD_RDP_SW_ENCODER_CA (object); + + g_mutex_clear (&encoder_ca->encode_mutex); + + G_OBJECT_CLASS (grd_rdp_sw_encoder_ca_parent_class)->finalize (object); +} + +static void +grd_rdp_sw_encoder_ca_init (GrdRdpSwEncoderCa *encoder_ca) +{ + g_mutex_init (&encoder_ca->encode_mutex); +} + +static void +grd_rdp_sw_encoder_ca_class_init (GrdRdpSwEncoderCaClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = grd_rdp_sw_encoder_ca_dispose; + object_class->finalize = grd_rdp_sw_encoder_ca_finalize; +} diff --git a/grd-rdp-sw-encoder-ca.h b/grd-rdp-sw-encoder-ca.h new file mode 100644 index 0000000..b11b0e9 --- /dev/null +++ b/grd-rdp-sw-encoder-ca.h @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2024 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include +#include +#include + +#define GRD_TYPE_RDP_SW_ENCODER_CA (grd_rdp_sw_encoder_ca_get_type ()) +G_DECLARE_FINAL_TYPE (GrdRdpSwEncoderCa, grd_rdp_sw_encoder_ca, + GRD, RDP_SW_ENCODER_CA, GObject) + +GrdRdpSwEncoderCa *grd_rdp_sw_encoder_ca_new (GError **error); + +void grd_rdp_sw_encoder_ca_encode_progressive_frame (GrdRdpSwEncoderCa *encoder_ca, + uint32_t surface_width, + uint32_t surface_height, + uint8_t *src_buffer, + uint32_t src_stride, + cairo_region_t *damage_region, + wStream *encode_stream, + gboolean write_header); diff --git a/grd-rdp-view-creator-avc.c b/grd-rdp-view-creator-avc.c new file mode 100644 index 0000000..05760ce --- /dev/null +++ b/grd-rdp-view-creator-avc.c @@ -0,0 +1,1575 @@ +/* + * Copyright (C) 2024 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-rdp-view-creator-avc.h" + +#include "grd-debug.h" +#include "grd-image-view-nv12.h" +#include "grd-rdp-buffer.h" +#include "grd-rdp-render-state.h" +#include "grd-utils.h" +#include "grd-vk-buffer.h" +#include "grd-vk-device.h" +#include "grd-vk-image.h" +#include "grd-vk-memory.h" +#include "grd-vk-physical-device.h" +#include "grd-vk-queue.h" +#include "grd-vk-utils.h" + +#define N_SPECIALIZATION_CONSTANTS 6 + +typedef struct +{ + uint32_t source_width; + uint32_t source_height; + + uint32_t target_width; + uint32_t target_height; + + uint32_t perform_dmg_detection; + uint32_t state_buffer_stride; +} ViewCreateInfo; + +typedef struct +{ + VkCommandBuffer init_state_buffers; + VkCommandBuffer synchronize_state_buffers; + + VkCommandBuffer init_layouts; + VkCommandBuffer create_dual_view_simple; + VkCommandBuffer create_dual_view_difference; +} CommandBuffers; + +typedef struct +{ + GrdRdpViewCreatorAVC *view_creator_avc; + + VkPipelineLayout vk_pipeline_layout; + VkPipeline vk_pipeline; +} Pipeline; + +struct _GrdRdpViewCreatorAVC +{ + GrdRdpViewCreator parent; + + GrdVkDevice *device; + gboolean debug_vk_times; + gboolean supports_update_after_bind; + + ViewCreateInfo view_create_info; + + VkDeviceSize state_buffer_size; + + GrdVkBuffer *dmg_buffer_device; + GrdVkBuffer *dmg_buffer_host; + uint32_t *mapped_dmg_buffer; + + GrdVkBuffer *chroma_check_buffer_device; + GrdVkBuffer *chroma_check_buffer_host; + uint32_t *mapped_chroma_check_buffer; + + VkSampler vk_src_new_sampler; + VkSampler vk_src_old_sampler; + + VkDescriptorSetLayout vk_target_descriptor_set_layout; + VkDescriptorSetLayout vk_source_descriptor_set_layout; + VkDescriptorSetLayout vk_state_descriptor_set_layout; + + VkDescriptorPool vk_image_descriptor_pool; + VkDescriptorPool vk_buffer_descriptor_pool; + VkDescriptorSet vk_descriptor_set_main_view; + VkDescriptorSet vk_descriptor_set_aux_view; + VkDescriptorSet vk_descriptor_set_state; + VkDescriptorSet vk_descriptor_set_sources; + + gboolean pending_view_creation_recording; + gboolean pending_layout_transition; + + GrdVkQueue *queue; + VkQueryPool vk_timestamp_query_pool; + VkCommandPool vk_command_pool; + CommandBuffers command_buffers; + VkFence vk_fence; + + Pipeline *dual_view_pipeline; + Pipeline *dual_view_dmg_pipeline; +}; + +G_DEFINE_TYPE (GrdRdpViewCreatorAVC, grd_rdp_view_creator_avc, + GRD_TYPE_RDP_VIEW_CREATOR) + +static void +pipeline_free (Pipeline *pipeline); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (Pipeline, pipeline_free) + +static void +pipeline_free (Pipeline *pipeline) +{ + GrdRdpViewCreatorAVC *view_creator_avc = pipeline->view_creator_avc; + VkDevice vk_device = grd_vk_device_get_device (view_creator_avc->device); + + g_assert (vk_device != VK_NULL_HANDLE); + + if (pipeline->vk_pipeline != VK_NULL_HANDLE) + vkDestroyPipeline (vk_device, pipeline->vk_pipeline, NULL); + if (pipeline->vk_pipeline_layout != VK_NULL_HANDLE) + vkDestroyPipelineLayout (vk_device, pipeline->vk_pipeline_layout, NULL); + + g_free (pipeline); +} + +static void +write_image_descriptor_sets (GrdRdpViewCreatorAVC *view_creator_avc, + GrdImageViewNV12 *main_image_view, + GrdImageViewNV12 *aux_image_view, + GrdVkImage *src_image_new, + GrdVkImage *src_image_old) +{ + VkDevice vk_device = grd_vk_device_get_device (view_creator_avc->device); + VkWriteDescriptorSet write_descriptor_sets[6] = {}; + VkDescriptorImageInfo image_infos[6] = {}; + + image_infos[0].sampler = VK_NULL_HANDLE; + image_infos[0].imageView = grd_image_view_nv12_get_y_layer (main_image_view); + image_infos[0].imageLayout = VK_IMAGE_LAYOUT_GENERAL; + + image_infos[1] = image_infos[0]; + image_infos[1].imageView = grd_image_view_nv12_get_uv_layer (main_image_view); + + image_infos[2] = image_infos[0]; + image_infos[2].imageView = grd_image_view_nv12_get_y_layer (aux_image_view); + + image_infos[3] = image_infos[0]; + image_infos[3].imageView = grd_image_view_nv12_get_uv_layer (aux_image_view); + + image_infos[4].sampler = view_creator_avc->vk_src_new_sampler; + image_infos[4].imageView = grd_vk_image_get_image_view (src_image_new); + image_infos[4].imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + + image_infos[5] = image_infos[4]; + image_infos[5].sampler = view_creator_avc->vk_src_old_sampler; + if (src_image_old) + image_infos[5].imageView = grd_vk_image_get_image_view (src_image_old); + + write_descriptor_sets[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + write_descriptor_sets[0].dstSet = + view_creator_avc->vk_descriptor_set_main_view; + write_descriptor_sets[0].dstBinding = 0; + write_descriptor_sets[0].descriptorCount = 1; + write_descriptor_sets[0].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; + write_descriptor_sets[0].pImageInfo = &image_infos[0]; + + write_descriptor_sets[1] = write_descriptor_sets[0]; + write_descriptor_sets[1].dstBinding = 1; + write_descriptor_sets[1].pImageInfo = &image_infos[1]; + + write_descriptor_sets[2] = write_descriptor_sets[0]; + write_descriptor_sets[2].dstSet = + view_creator_avc->vk_descriptor_set_aux_view; + write_descriptor_sets[2].dstBinding = 0; + write_descriptor_sets[2].pImageInfo = &image_infos[2]; + + write_descriptor_sets[3] = write_descriptor_sets[2]; + write_descriptor_sets[3].dstBinding = 1; + write_descriptor_sets[3].pImageInfo = &image_infos[3]; + + write_descriptor_sets[4] = write_descriptor_sets[0]; + write_descriptor_sets[4].dstSet = + view_creator_avc->vk_descriptor_set_sources; + write_descriptor_sets[4].dstBinding = 0; + write_descriptor_sets[4].descriptorType = + VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + write_descriptor_sets[4].pImageInfo = &image_infos[4]; + + write_descriptor_sets[5] = write_descriptor_sets[4]; + write_descriptor_sets[5].dstBinding = 1; + write_descriptor_sets[5].pImageInfo = &image_infos[5]; + + vkUpdateDescriptorSets (vk_device, 6, write_descriptor_sets, 0, NULL); +} + +static void +maybe_init_image_layout (GrdRdpViewCreatorAVC *view_creator_avc, + VkCommandBuffer command_buffer, + GrdVkImage *image, + VkImageLayout new_layout, + VkAccessFlags2 dst_access_mask) +{ + GrdVkDevice *device = view_creator_avc->device; + GrdVkDeviceFuncs *device_funcs = grd_vk_device_get_device_funcs (device); + VkDependencyInfo dependency_info = {}; + VkImageMemoryBarrier2 image_memory_barrier_2 = {}; + + g_assert (device_funcs->vkCmdPipelineBarrier2KHR); + + if (grd_vk_image_get_image_layout (image) == new_layout) + return; + + image_memory_barrier_2.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2; + image_memory_barrier_2.srcStageMask = VK_PIPELINE_STAGE_2_NONE; + image_memory_barrier_2.srcAccessMask = VK_ACCESS_2_NONE; + image_memory_barrier_2.dstStageMask = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT; + image_memory_barrier_2.dstAccessMask = dst_access_mask; + image_memory_barrier_2.oldLayout = grd_vk_image_get_image_layout (image); + image_memory_barrier_2.newLayout = new_layout; + image_memory_barrier_2.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + image_memory_barrier_2.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + image_memory_barrier_2.image = grd_vk_image_get_image (image); + image_memory_barrier_2.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + image_memory_barrier_2.subresourceRange.baseMipLevel = 0; + image_memory_barrier_2.subresourceRange.levelCount = VK_REMAINING_MIP_LEVELS; + image_memory_barrier_2.subresourceRange.baseArrayLayer = 0; + image_memory_barrier_2.subresourceRange.layerCount = VK_REMAINING_ARRAY_LAYERS; + + dependency_info.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO; + dependency_info.imageMemoryBarrierCount = 1; + dependency_info.pImageMemoryBarriers = &image_memory_barrier_2; + + device_funcs->vkCmdPipelineBarrier2KHR (command_buffer, &dependency_info); +} + +static gboolean +maybe_record_init_layouts (GrdRdpViewCreatorAVC *view_creator_avc, + GrdImageViewNV12 *main_image_view, + GrdImageViewNV12 *aux_image_view, + GrdVkImage *src_image_new, + GrdVkImage *src_image_old, + GError **error) +{ + VkCommandBuffer command_buffer = + view_creator_avc->command_buffers.init_layouts; + VkCommandBufferBeginInfo begin_info = {}; + VkResult vk_result; + g_autoptr (GList) images = NULL; + GList *l; + + view_creator_avc->pending_layout_transition = + grd_image_view_nv12_get_image_layout (main_image_view) != VK_IMAGE_LAYOUT_GENERAL || + grd_image_view_nv12_get_image_layout (aux_image_view) != VK_IMAGE_LAYOUT_GENERAL || + grd_vk_image_get_image_layout (src_image_new) != VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL || + (src_image_old && + grd_vk_image_get_image_layout (src_image_old) != VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + + if (!view_creator_avc->pending_layout_transition) + return TRUE; + + begin_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; + + vk_result = vkBeginCommandBuffer (command_buffer, &begin_info); + if (vk_result != VK_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to begin command buffer: %i", vk_result); + return FALSE; + } + + images = grd_image_view_nv12_get_images (main_image_view); + images = g_list_concat (images, + grd_image_view_nv12_get_images (aux_image_view)); + + for (l = images; l; l = l->next) + { + GrdVkImage *image = l->data; + + maybe_init_image_layout (view_creator_avc, command_buffer, image, + VK_IMAGE_LAYOUT_GENERAL, + VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT); + } + + maybe_init_image_layout (view_creator_avc, command_buffer, src_image_new, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_ACCESS_2_SHADER_STORAGE_READ_BIT); + if (src_image_old) + { + maybe_init_image_layout (view_creator_avc, command_buffer, src_image_old, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_ACCESS_2_SHADER_STORAGE_READ_BIT); + } + + vk_result = vkEndCommandBuffer (command_buffer); + if (vk_result != VK_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to end command buffer: %i", vk_result); + return FALSE; + } + + return TRUE; +} + +static gboolean +record_create_view (GrdRdpViewCreatorAVC *view_creator_avc, + VkCommandBuffer command_buffer, + Pipeline *pipeline, + GError **error) +{ + GrdVkDevice *device = view_creator_avc->device; + GrdVkDeviceFuncs *device_funcs = grd_vk_device_get_device_funcs (device); + ViewCreateInfo *view_create_info = &view_creator_avc->view_create_info; + VkCommandBufferBeginInfo begin_info = {}; + VkDescriptorSet descriptor_sets[4] = {}; + uint32_t n_groups_x; + uint32_t n_groups_y; + VkResult vk_result; + + g_assert (device_funcs->vkCmdWriteTimestamp2KHR); + + g_assert (view_create_info->target_width % 16 == 0); + g_assert (view_create_info->target_height % 16 == 0); + g_assert (view_create_info->target_width > 0); + g_assert (view_create_info->target_height > 0); + + begin_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; + + vk_result = vkBeginCommandBuffer (command_buffer, &begin_info); + if (vk_result != VK_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to begin command buffer: %i", vk_result); + return FALSE; + } + + vkCmdResetQueryPool (command_buffer, + view_creator_avc->vk_timestamp_query_pool, 0, 2); + device_funcs->vkCmdWriteTimestamp2KHR (command_buffer, + VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, + view_creator_avc->vk_timestamp_query_pool, + 0); + + descriptor_sets[0] = view_creator_avc->vk_descriptor_set_main_view; + descriptor_sets[1] = view_creator_avc->vk_descriptor_set_aux_view; + descriptor_sets[2] = view_creator_avc->vk_descriptor_set_state; + descriptor_sets[3] = view_creator_avc->vk_descriptor_set_sources; + + vkCmdBindPipeline (command_buffer, VK_PIPELINE_BIND_POINT_COMPUTE, + pipeline->vk_pipeline); + vkCmdBindDescriptorSets (command_buffer, VK_PIPELINE_BIND_POINT_COMPUTE, + pipeline->vk_pipeline_layout, + 0, 4, descriptor_sets, 0, NULL); + + n_groups_x = view_create_info->target_width / 2 / 16 + + (view_create_info->target_width / 2 % 16 ? 1 : 0); + n_groups_y = view_create_info->target_height / 2 / 16 + + (view_create_info->target_height / 2 % 16 ? 1 : 0); + vkCmdDispatch (command_buffer, n_groups_x, n_groups_y, 1); + + device_funcs->vkCmdWriteTimestamp2KHR (command_buffer, + VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, + view_creator_avc->vk_timestamp_query_pool, + 1); + + vk_result = vkEndCommandBuffer (command_buffer); + if (vk_result != VK_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to end command buffer: %i", vk_result); + return FALSE; + } + + return TRUE; +} + +static gboolean +record_create_view_command_buffers (GrdRdpViewCreatorAVC *view_creator_avc, + gboolean record_create_simple_view, + gboolean record_create_difference_view, + GError **error) +{ + CommandBuffers *command_buffers = &view_creator_avc->command_buffers; + + if (record_create_simple_view && + !record_create_view (view_creator_avc, + command_buffers->create_dual_view_simple, + view_creator_avc->dual_view_pipeline, + error)) + return FALSE; + + if (record_create_difference_view && + !record_create_view (view_creator_avc, + command_buffers->create_dual_view_difference, + view_creator_avc->dual_view_dmg_pipeline, + error)) + return FALSE; + + return TRUE; +} + +static gboolean +record_command_buffers (GrdRdpViewCreatorAVC *view_creator_avc, + GrdImageViewNV12 *main_image_view, + GrdImageViewNV12 *aux_image_view, + GrdVkImage *src_image_new, + GrdVkImage *src_image_old, + GError **error) +{ + if (!maybe_record_init_layouts (view_creator_avc, + main_image_view, aux_image_view, + src_image_new, src_image_old, error)) + return FALSE; + if (view_creator_avc->pending_view_creation_recording && + !record_create_view_command_buffers (view_creator_avc, + !src_image_old, !!src_image_old, + error)) + return FALSE; + + return TRUE; +} + +static void +update_image_layout_states (GrdImageViewNV12 *main_image_view, + GrdImageViewNV12 *aux_image_view, + GrdVkImage *src_image_new, + GrdVkImage *src_image_old) +{ + grd_image_view_nv12_set_image_layout (main_image_view, + VK_IMAGE_LAYOUT_GENERAL); + grd_image_view_nv12_set_image_layout (aux_image_view, + VK_IMAGE_LAYOUT_GENERAL); + + grd_vk_image_set_image_layout (src_image_new, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + if (src_image_old) + { + grd_vk_image_set_image_layout (src_image_old, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + } +} + +static gboolean +grd_rdp_view_creator_avc_create_view (GrdRdpViewCreator *view_creator, + GList *image_views, + GrdRdpBuffer *src_buffer_new, + GrdRdpBuffer *src_buffer_old, + GError **error) +{ + GrdRdpViewCreatorAVC *view_creator_avc = + GRD_RDP_VIEW_CREATOR_AVC (view_creator); + VkDevice vk_device = grd_vk_device_get_device (view_creator_avc->device); + CommandBuffers *command_buffers = &view_creator_avc->command_buffers; + GrdVkImage *src_image_new = NULL; + GrdVkImage *src_image_old = NULL; + VkCommandBufferSubmitInfo buffer_submit_infos[4] = {}; + VkCommandBuffer create_dual_view = VK_NULL_HANDLE; + uint32_t n_buffer_submit_infos = 0; + VkSubmitInfo2 submit_info_2 = {}; + GrdImageViewNV12 *main_image_view; + GrdImageViewNV12 *aux_image_view; + VkResult vk_result; + + g_assert (image_views); + g_assert (image_views->next); + + main_image_view = GRD_IMAGE_VIEW_NV12 (image_views->data); + aux_image_view = GRD_IMAGE_VIEW_NV12 (image_views->next->data); + + src_image_new = grd_rdp_buffer_get_dma_buf_image (src_buffer_new); + if (src_buffer_old) + src_image_old = grd_rdp_buffer_get_dma_buf_image (src_buffer_old); + + write_image_descriptor_sets (view_creator_avc, + main_image_view, aux_image_view, + src_image_new, src_image_old); + + if (!record_command_buffers (view_creator_avc, + main_image_view, aux_image_view, + src_image_new, src_image_old, error)) + return FALSE; + + vk_result = vkResetFences (vk_device, 1, &view_creator_avc->vk_fence); + if (vk_result != VK_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to reset fence: %i", vk_result); + return FALSE; + } + + if (view_creator_avc->pending_layout_transition) + { + buffer_submit_infos[n_buffer_submit_infos].sType = + VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO; + buffer_submit_infos[n_buffer_submit_infos].commandBuffer = + command_buffers->init_layouts; + ++n_buffer_submit_infos; + } + + buffer_submit_infos[n_buffer_submit_infos].sType = + VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO; + buffer_submit_infos[n_buffer_submit_infos].commandBuffer = + command_buffers->init_state_buffers; + ++n_buffer_submit_infos; + + if (src_image_old) + create_dual_view = command_buffers->create_dual_view_difference; + else + create_dual_view = command_buffers->create_dual_view_simple; + + buffer_submit_infos[n_buffer_submit_infos].sType = + VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO; + buffer_submit_infos[n_buffer_submit_infos].commandBuffer = create_dual_view; + ++n_buffer_submit_infos; + + buffer_submit_infos[n_buffer_submit_infos].sType = + VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO; + buffer_submit_infos[n_buffer_submit_infos].commandBuffer = + command_buffers->synchronize_state_buffers; + ++n_buffer_submit_infos; + + submit_info_2.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2; + submit_info_2.commandBufferInfoCount = n_buffer_submit_infos; + submit_info_2.pCommandBufferInfos = buffer_submit_infos; + + if (!grd_vk_queue_submit (view_creator_avc->queue, &submit_info_2, 1, + view_creator_avc->vk_fence, error)) + return FALSE; + + update_image_layout_states (main_image_view, aux_image_view, + src_image_new, src_image_old); + + return TRUE; +} + +static gboolean +print_view_creation_time (GrdRdpViewCreatorAVC *view_creator_avc, + GError **error) +{ + VkDevice vk_device = grd_vk_device_get_device (view_creator_avc->device); + float timestamp_period = + grd_vk_device_get_timestamp_period (view_creator_avc->device); + uint64_t gpu_timestamps[2] = {}; + VkQueryResultFlags query_flags; + float execution_time_us; + VkResult vk_result; + + query_flags = VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT; + vk_result = vkGetQueryPoolResults (vk_device, + view_creator_avc->vk_timestamp_query_pool, + 0, 2, + sizeof (gpu_timestamps), gpu_timestamps, + sizeof (uint64_t), query_flags); + if (vk_result != VK_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to get query pool results: %i", vk_result); + return FALSE; + } + + execution_time_us = (gpu_timestamps[1] - gpu_timestamps[0]) * + timestamp_period / 1000; + g_debug ("[HWAccel.Vulkan] CreateView[ExecutionTime]: %liµs", + (int64_t) execution_time_us); + + return TRUE; +} + +static GrdRdpRenderState * +grd_rdp_view_creator_avc_finish_view (GrdRdpViewCreator *view_creator, + GError **error) +{ + GrdRdpViewCreatorAVC *view_creator_avc = + GRD_RDP_VIEW_CREATOR_AVC (view_creator); + VkDevice vk_device = grd_vk_device_get_device (view_creator_avc->device); + uint32_t state_buffer_length; + VkResult vk_result; + + do + { + vk_result = vkWaitForFences (vk_device, 1, &view_creator_avc->vk_fence, + VK_FALSE, 100 * 1000); + } + while (vk_result == VK_TIMEOUT); + + if (vk_result != VK_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to wait for fence: %i", vk_result); + return NULL; + } + + if (view_creator_avc->debug_vk_times && + !print_view_creation_time (view_creator_avc, error)) + return NULL; + + state_buffer_length = view_creator_avc->state_buffer_size / + sizeof (uint32_t); + + return grd_rdp_render_state_new (view_creator_avc->mapped_dmg_buffer, + view_creator_avc->mapped_chroma_check_buffer, + state_buffer_length); +} + +static void +prepare_view_create_info (GrdRdpViewCreatorAVC *view_creator_avc, + uint32_t target_width, + uint32_t target_height, + uint32_t source_width, + uint32_t source_height) +{ + ViewCreateInfo *view_create_info = &view_creator_avc->view_create_info; + + g_assert (target_width % 16 == 0); + g_assert (target_height % 16 == 0); + g_assert (target_width > 0); + g_assert (target_height > 0); + + view_create_info->source_width = source_width; + view_create_info->source_height = source_height; + + view_create_info->target_width = target_width; + view_create_info->target_height = target_height; + + view_create_info->state_buffer_stride = + grd_get_aligned_size (source_width, 64) / 64; +} + +static gboolean +create_state_buffer (GrdRdpViewCreatorAVC *view_creator_avc, + GrdVkBuffer **state_buffer_device, + GrdVkBuffer **state_buffer_host, + uint32_t **mapped_state_buffer, + GError **error) +{ + GrdVkBufferDescriptor buffer_descriptor = {}; + VkMemoryPropertyFlagBits memory_flags; + GrdVkMemory *memory; + + buffer_descriptor.usage_flags = VK_BUFFER_USAGE_TRANSFER_DST_BIT | + VK_BUFFER_USAGE_STORAGE_BUFFER_BIT; + buffer_descriptor.size = view_creator_avc->state_buffer_size; + buffer_descriptor.memory_flags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | + VK_MEMORY_PROPERTY_HOST_COHERENT_BIT | + VK_MEMORY_PROPERTY_HOST_CACHED_BIT; + + *state_buffer_host = grd_vk_buffer_new (view_creator_avc->device, + &buffer_descriptor, + error); + if (!(*state_buffer_host)) + return FALSE; + + memory = grd_vk_buffer_get_memory (*state_buffer_host); + + *mapped_state_buffer = grd_vk_memory_get_mapped_pointer (memory, error); + if (!(*mapped_state_buffer)) + return FALSE; + + memory_flags = grd_vk_memory_get_memory_flags (memory); + if (memory_flags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) + return TRUE; + + buffer_descriptor.usage_flags |= VK_BUFFER_USAGE_TRANSFER_SRC_BIT; + buffer_descriptor.memory_flags = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; + + *state_buffer_device = grd_vk_buffer_new (view_creator_avc->device, + &buffer_descriptor, + error); + if (!(*state_buffer_device)) + return FALSE; + + return TRUE; +} + +static gboolean +create_state_buffers (GrdRdpViewCreatorAVC *view_creator_avc, + uint32_t source_width, + uint32_t source_height, + GError **error) +{ + view_creator_avc->state_buffer_size = + (grd_get_aligned_size (source_width, 64) / 64) * + (grd_get_aligned_size (source_height, 64) / 64) * + sizeof (uint32_t); + + if (!create_state_buffer (view_creator_avc, + &view_creator_avc->dmg_buffer_device, + &view_creator_avc->dmg_buffer_host, + &view_creator_avc->mapped_dmg_buffer, + error)) + return FALSE; + if (!create_state_buffer (view_creator_avc, + &view_creator_avc->chroma_check_buffer_device, + &view_creator_avc->chroma_check_buffer_host, + &view_creator_avc->mapped_chroma_check_buffer, + error)) + return FALSE; + + return TRUE; +} + +static gboolean +create_sampler (GrdRdpViewCreatorAVC *view_creator_avc, + VkSampler *sampler, + GError **error) +{ + VkDevice vk_device = grd_vk_device_get_device (view_creator_avc->device); + VkSamplerCreateInfo sampler_create_info = {}; + VkResult vk_result; + + g_assert (sampler); + + sampler_create_info.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; + sampler_create_info.magFilter = VK_FILTER_NEAREST; + sampler_create_info.minFilter = VK_FILTER_NEAREST; + sampler_create_info.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST; + sampler_create_info.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + sampler_create_info.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + sampler_create_info.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + sampler_create_info.borderColor = VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK; + sampler_create_info.unnormalizedCoordinates = VK_TRUE; + + vk_result = vkCreateSampler (vk_device, &sampler_create_info, NULL, sampler); + if (vk_result != VK_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create sampler: %i", vk_result); + return FALSE; + } + g_assert (*sampler != VK_NULL_HANDLE); + + return TRUE; +} + +static gboolean +create_samplers (GrdRdpViewCreatorAVC *view_creator_avc, + GError **error) +{ + if (!create_sampler (view_creator_avc, + &view_creator_avc->vk_src_new_sampler, + error)) + return FALSE; + if (!create_sampler (view_creator_avc, + &view_creator_avc->vk_src_old_sampler, + error)) + return FALSE; + + return TRUE; +} + +static gboolean +create_descriptor_set_layout (GrdRdpViewCreatorAVC *view_creator_avc, + VkDescriptorSetLayout *descriptor_set_layout, + VkDescriptorType descriptor_type, + GError **error) +{ + VkDevice vk_device = grd_vk_device_get_device (view_creator_avc->device); + VkDescriptorSetLayoutCreateInfo layout_create_info = {}; + VkDescriptorSetLayoutBinding layout_bindings[2] = {}; + VkDescriptorSetLayoutBindingFlagsCreateInfo layout_binding_flags_info = {}; + VkDescriptorBindingFlags binding_flags[2] = {}; + VkResult vk_result; + + g_assert (descriptor_set_layout); + + layout_bindings[0].binding = 0; + layout_bindings[0].descriptorType = descriptor_type; + layout_bindings[0].descriptorCount = 1; + layout_bindings[0].stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + + layout_bindings[1] = layout_bindings[0]; + layout_bindings[1].binding = 1; + + layout_create_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; + layout_create_info.bindingCount = 2; + layout_create_info.pBindings = layout_bindings; + + binding_flags[0] = VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT; + binding_flags[1] = binding_flags[0]; + + layout_binding_flags_info.sType = + VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_BINDING_FLAGS_CREATE_INFO; + layout_binding_flags_info.bindingCount = 2; + layout_binding_flags_info.pBindingFlags = binding_flags; + + if (view_creator_avc->supports_update_after_bind && + descriptor_type != VK_DESCRIPTOR_TYPE_STORAGE_BUFFER) + { + layout_create_info.flags = + VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT; + + grd_vk_append_to_chain (&layout_create_info, &layout_binding_flags_info); + } + + vk_result = vkCreateDescriptorSetLayout (vk_device, &layout_create_info, NULL, + descriptor_set_layout); + if (vk_result != VK_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create descriptor set layout: %i", vk_result); + return FALSE; + } + g_assert (*descriptor_set_layout != VK_NULL_HANDLE); + + return TRUE; +} + +static gboolean +create_descriptor_set_layouts (GrdRdpViewCreatorAVC *view_creator_avc, + GError **error) +{ + VkDescriptorSetLayout *target_descriptor_set_layout = + &view_creator_avc->vk_target_descriptor_set_layout; + VkDescriptorSetLayout *source_descriptor_set_layout = + &view_creator_avc->vk_source_descriptor_set_layout; + VkDescriptorSetLayout *state_descriptor_set_layout = + &view_creator_avc->vk_state_descriptor_set_layout; + + if (!create_descriptor_set_layout (view_creator_avc, + target_descriptor_set_layout, + VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, + error)) + return FALSE; + if (!create_descriptor_set_layout (view_creator_avc, + source_descriptor_set_layout, + VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, + error)) + return FALSE; + if (!create_descriptor_set_layout (view_creator_avc, + state_descriptor_set_layout, + VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, + error)) + return FALSE; + + return TRUE; +} + +static gboolean +create_image_descriptor_sets (GrdRdpViewCreatorAVC *view_creator_avc, + GError **error) +{ + VkDevice vk_device = grd_vk_device_get_device (view_creator_avc->device); + VkDescriptorPoolCreateInfo pool_create_info = {}; + VkDescriptorPoolSize pool_sizes[2] = {}; + VkDescriptorSetAllocateInfo allocate_info = {}; + VkDescriptorSetLayout descriptor_set_layouts[3] = {}; + VkDescriptorSet descriptor_sets[3] = {}; + VkResult vk_result; + + pool_sizes[0].type = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; + pool_sizes[0].descriptorCount = 4; + + pool_sizes[1].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + pool_sizes[1].descriptorCount = 2; + + pool_create_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; + pool_create_info.maxSets = 3; + pool_create_info.poolSizeCount = 2; + pool_create_info.pPoolSizes = pool_sizes; + + if (view_creator_avc->supports_update_after_bind) + pool_create_info.flags = VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT; + + vk_result = vkCreateDescriptorPool (vk_device, &pool_create_info, NULL, + &view_creator_avc->vk_image_descriptor_pool); + if (vk_result != VK_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create descriptor pool: %i", vk_result); + return FALSE; + } + g_assert (view_creator_avc->vk_image_descriptor_pool != VK_NULL_HANDLE); + + descriptor_set_layouts[0] = view_creator_avc->vk_target_descriptor_set_layout; + descriptor_set_layouts[1] = view_creator_avc->vk_target_descriptor_set_layout; + descriptor_set_layouts[2] = view_creator_avc->vk_source_descriptor_set_layout; + + allocate_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; + allocate_info.descriptorPool = view_creator_avc->vk_image_descriptor_pool; + allocate_info.descriptorSetCount = 3; + allocate_info.pSetLayouts = descriptor_set_layouts; + + vk_result = vkAllocateDescriptorSets (vk_device, &allocate_info, + descriptor_sets); + if (vk_result != VK_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to allocate descriptor sets: %i", vk_result); + return FALSE; + } + g_assert (descriptor_sets[0] != VK_NULL_HANDLE); + g_assert (descriptor_sets[1] != VK_NULL_HANDLE); + g_assert (descriptor_sets[2] != VK_NULL_HANDLE); + + view_creator_avc->vk_descriptor_set_main_view = descriptor_sets[0]; + view_creator_avc->vk_descriptor_set_aux_view = descriptor_sets[1]; + view_creator_avc->vk_descriptor_set_sources = descriptor_sets[2]; + + return TRUE; +} + +static gboolean +create_buffer_descriptor_set (GrdRdpViewCreatorAVC *view_creator_avc, + GError **error) +{ + VkDevice vk_device = grd_vk_device_get_device (view_creator_avc->device); + VkDescriptorPoolCreateInfo pool_create_info = {}; + VkDescriptorPoolSize pool_size = {}; + VkDescriptorSetAllocateInfo allocate_info = {}; + VkDescriptorSetLayout descriptor_set_layout; + VkDescriptorSet descriptor_set = VK_NULL_HANDLE; + VkResult vk_result; + + pool_size.type = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + pool_size.descriptorCount = 2; + + pool_create_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; + pool_create_info.maxSets = 1; + pool_create_info.poolSizeCount = 1; + pool_create_info.pPoolSizes = &pool_size; + + vk_result = vkCreateDescriptorPool (vk_device, &pool_create_info, NULL, + &view_creator_avc->vk_buffer_descriptor_pool); + if (vk_result != VK_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create descriptor pool: %i", vk_result); + return FALSE; + } + g_assert (view_creator_avc->vk_buffer_descriptor_pool != VK_NULL_HANDLE); + + descriptor_set_layout = view_creator_avc->vk_state_descriptor_set_layout; + + allocate_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; + allocate_info.descriptorPool = view_creator_avc->vk_buffer_descriptor_pool; + allocate_info.descriptorSetCount = 1; + allocate_info.pSetLayouts = &descriptor_set_layout; + + vk_result = vkAllocateDescriptorSets (vk_device, &allocate_info, + &descriptor_set); + if (vk_result != VK_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to allocate descriptor sets: %i", vk_result); + return FALSE; + } + g_assert (descriptor_set != VK_NULL_HANDLE); + + view_creator_avc->vk_descriptor_set_state = descriptor_set; + + return TRUE; +} + +static gboolean +create_descriptor_sets (GrdRdpViewCreatorAVC *view_creator_avc, + GError **error) +{ + if (!create_image_descriptor_sets (view_creator_avc, error)) + return FALSE; + if (!create_buffer_descriptor_set (view_creator_avc, error)) + return FALSE; + + return TRUE; +} + +static gboolean +create_pipeline_layout (GrdRdpViewCreatorAVC *view_creator_avc, + const VkDescriptorSetLayout *descriptor_set_layouts, + const uint32_t n_descriptor_set_layouts, + VkPipelineLayout *vk_pipeline_layout, + GError **error) +{ + VkDevice vk_device = grd_vk_device_get_device (view_creator_avc->device); + VkPipelineLayoutCreateInfo pipeline_layout_create_info = {}; + VkResult vk_result; + + g_assert (vk_pipeline_layout); + + pipeline_layout_create_info.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; + pipeline_layout_create_info.setLayoutCount = n_descriptor_set_layouts; + pipeline_layout_create_info.pSetLayouts = descriptor_set_layouts; + + vk_result = vkCreatePipelineLayout (vk_device, &pipeline_layout_create_info, + NULL, vk_pipeline_layout); + if (vk_result != VK_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create pipeline layout: %i", vk_result); + return FALSE; + } + g_assert (*vk_pipeline_layout != VK_NULL_HANDLE); + + return TRUE; +} + +static gboolean +create_pipeline (GrdRdpViewCreatorAVC *view_creator_avc, + VkShaderModule vk_shader_module, + VkPipelineLayout vk_pipeline_layout, + VkPipeline *vk_pipeline, + GError **error) +{ + GrdVkDevice *device = view_creator_avc->device; + VkDevice vk_device = grd_vk_device_get_device (device); + VkPipelineCache vk_pipeline_cache = grd_vk_device_get_pipeline_cache (device); + ViewCreateInfo *view_create_info = &view_creator_avc->view_create_info; + VkComputePipelineCreateInfo pipeline_create_info = {}; + VkSpecializationInfo specialization_info = {}; + VkSpecializationMapEntry map_entries[N_SPECIALIZATION_CONSTANTS] = {}; + VkResult vk_result; + uint32_t i; + + g_assert (vk_shader_module != VK_NULL_HANDLE); + g_assert (vk_pipeline); + g_assert (sizeof (ViewCreateInfo) / + sizeof (uint32_t) == N_SPECIALIZATION_CONSTANTS); + + for (i = 0; i < N_SPECIALIZATION_CONSTANTS; ++i) + { + map_entries[i].constantID = i; + map_entries[i].offset = i * sizeof (uint32_t); + map_entries[i].size = sizeof (uint32_t); + } + + specialization_info.mapEntryCount = N_SPECIALIZATION_CONSTANTS; + specialization_info.pMapEntries = map_entries; + specialization_info.dataSize = sizeof (ViewCreateInfo); + specialization_info.pData = view_create_info; + + pipeline_create_info.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO; + pipeline_create_info.stage.sType = + VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; + pipeline_create_info.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT; + pipeline_create_info.stage.module = vk_shader_module; + pipeline_create_info.stage.pName = "main"; + pipeline_create_info.stage.pSpecializationInfo = &specialization_info; + pipeline_create_info.layout = vk_pipeline_layout; + + vk_result = vkCreateComputePipelines (vk_device, vk_pipeline_cache, + 1, &pipeline_create_info, + NULL, vk_pipeline); + if (vk_result != VK_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create compute pipeline: %i", vk_result); + return FALSE; + } + g_assert (*vk_pipeline != VK_NULL_HANDLE); + + return TRUE; +} + +static Pipeline * +dual_view_pipeline_new (GrdRdpViewCreatorAVC *view_creator_avc, + gboolean perform_dmg_detection, + GError **error) +{ + ViewCreateInfo *view_create_info = &view_creator_avc->view_create_info; + const GrdVkShaderModules *shader_modules = + grd_vk_device_get_shader_modules (view_creator_avc->device); + g_autoptr (Pipeline) dual_view_pipeline = NULL; + VkDescriptorSetLayout descriptor_set_layouts[4] = {}; + + dual_view_pipeline = g_new0 (Pipeline, 1); + dual_view_pipeline->view_creator_avc = view_creator_avc; + + descriptor_set_layouts[0] = view_creator_avc->vk_target_descriptor_set_layout; + descriptor_set_layouts[1] = view_creator_avc->vk_target_descriptor_set_layout; + descriptor_set_layouts[2] = view_creator_avc->vk_state_descriptor_set_layout; + descriptor_set_layouts[3] = view_creator_avc->vk_source_descriptor_set_layout; + + if (!create_pipeline_layout (view_creator_avc, descriptor_set_layouts, 4, + &dual_view_pipeline->vk_pipeline_layout, error)) + return NULL; + + view_create_info->perform_dmg_detection = perform_dmg_detection; + if (!create_pipeline (view_creator_avc, shader_modules->create_avc_dual_view, + dual_view_pipeline->vk_pipeline_layout, + &dual_view_pipeline->vk_pipeline, error)) + return NULL; + + return g_steal_pointer (&dual_view_pipeline); +} + +static gboolean +create_pipelines (GrdRdpViewCreatorAVC *view_creator_avc, + GError **error) +{ + view_creator_avc->dual_view_pipeline = + dual_view_pipeline_new (view_creator_avc, FALSE, error); + if (!view_creator_avc->dual_view_pipeline) + return FALSE; + + view_creator_avc->dual_view_dmg_pipeline = + dual_view_pipeline_new (view_creator_avc, TRUE, error); + if (!view_creator_avc->dual_view_dmg_pipeline) + return FALSE; + + return TRUE; +} + +static gboolean +create_timestamp_query_pool (GrdRdpViewCreatorAVC *view_creator_avc, + GError **error) +{ + VkDevice vk_device = grd_vk_device_get_device (view_creator_avc->device); + VkQueryPoolCreateInfo pool_create_info = {}; + VkResult vk_result; + + pool_create_info.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO; + pool_create_info.queryType = VK_QUERY_TYPE_TIMESTAMP; + pool_create_info.queryCount = 2; + + vk_result = vkCreateQueryPool (vk_device, &pool_create_info, NULL, + &view_creator_avc->vk_timestamp_query_pool); + if (vk_result != VK_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create query pool: %i", vk_result); + return FALSE; + } + g_assert (view_creator_avc->vk_timestamp_query_pool != VK_NULL_HANDLE); + + return TRUE; +} + +static gboolean +create_command_buffers (GrdRdpViewCreatorAVC *view_creator_avc, + GError **error) +{ + VkDevice vk_device = grd_vk_device_get_device (view_creator_avc->device); + CommandBuffers *command_buffers = &view_creator_avc->command_buffers; + GrdVkQueue *queue = view_creator_avc->queue; + VkCommandPoolCreateInfo pool_create_info = {}; + VkCommandBufferAllocateInfo allocate_info = {}; + VkCommandBuffer vk_command_buffers[5] = {}; + VkResult vk_result; + + pool_create_info.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; + pool_create_info.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; + pool_create_info.queueFamilyIndex = grd_vk_queue_get_queue_family_idx (queue); + + vk_result = vkCreateCommandPool (vk_device, &pool_create_info, NULL, + &view_creator_avc->vk_command_pool); + if (vk_result != VK_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create command pool: %i", vk_result); + return FALSE; + } + g_assert (view_creator_avc->vk_command_pool != VK_NULL_HANDLE); + + allocate_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; + allocate_info.commandPool = view_creator_avc->vk_command_pool; + allocate_info.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; + allocate_info.commandBufferCount = 5; + + vk_result = vkAllocateCommandBuffers (vk_device, &allocate_info, + vk_command_buffers); + if (vk_result != VK_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to allocate command buffers: %i", vk_result); + return FALSE; + } + g_assert (vk_command_buffers[0] != VK_NULL_HANDLE); + g_assert (vk_command_buffers[1] != VK_NULL_HANDLE); + g_assert (vk_command_buffers[2] != VK_NULL_HANDLE); + g_assert (vk_command_buffers[3] != VK_NULL_HANDLE); + g_assert (vk_command_buffers[4] != VK_NULL_HANDLE); + + command_buffers->init_state_buffers = vk_command_buffers[0]; + command_buffers->synchronize_state_buffers = vk_command_buffers[1]; + command_buffers->init_layouts = vk_command_buffers[2]; + command_buffers->create_dual_view_simple = vk_command_buffers[3]; + command_buffers->create_dual_view_difference = vk_command_buffers[4]; + + return TRUE; +} + +static gboolean +create_fence (GrdRdpViewCreatorAVC *view_creator_avc, + GError **error) +{ + VkDevice vk_device = grd_vk_device_get_device (view_creator_avc->device); + VkFenceCreateInfo fence_create_info = {}; + VkResult vk_result; + + fence_create_info.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; + + vk_result = vkCreateFence (vk_device, &fence_create_info, NULL, + &view_creator_avc->vk_fence); + if (vk_result != VK_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create fence: %i", vk_result); + return FALSE; + } + g_assert (view_creator_avc->vk_fence != VK_NULL_HANDLE); + + return TRUE; +} + +static void +write_state_descriptor_set (GrdRdpViewCreatorAVC *view_creator_avc) +{ + VkDevice vk_device = grd_vk_device_get_device (view_creator_avc->device); + VkWriteDescriptorSet write_descriptor_sets[2] = {}; + VkDescriptorBufferInfo buffer_infos[2] = {}; + GrdVkBuffer *dmg_buffer; + GrdVkBuffer *chroma_check_buffer; + + dmg_buffer = view_creator_avc->dmg_buffer_device; + if (!dmg_buffer) + dmg_buffer = view_creator_avc->dmg_buffer_host; + + chroma_check_buffer = view_creator_avc->chroma_check_buffer_device; + if (!chroma_check_buffer) + chroma_check_buffer = view_creator_avc->chroma_check_buffer_host; + + buffer_infos[0].buffer = grd_vk_buffer_get_buffer (dmg_buffer); + buffer_infos[0].offset = 0; + buffer_infos[0].range = VK_WHOLE_SIZE; + + buffer_infos[1] = buffer_infos[0]; + buffer_infos[1].buffer = grd_vk_buffer_get_buffer (chroma_check_buffer); + + write_descriptor_sets[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + write_descriptor_sets[0].dstSet = view_creator_avc->vk_descriptor_set_state; + write_descriptor_sets[0].dstBinding = 0; + write_descriptor_sets[0].descriptorCount = 1; + write_descriptor_sets[0].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + write_descriptor_sets[0].pBufferInfo = &buffer_infos[0]; + + write_descriptor_sets[1] = write_descriptor_sets[0]; + write_descriptor_sets[1].dstBinding = 1; + write_descriptor_sets[1].pBufferInfo = &buffer_infos[1]; + + vkUpdateDescriptorSets (vk_device, 2, write_descriptor_sets, 0, NULL); +} + +static gboolean +record_init_state_buffers (GrdRdpViewCreatorAVC *view_creator_avc, + GError **error) +{ + CommandBuffers *command_buffers = &view_creator_avc->command_buffers; + VkCommandBuffer command_buffer = command_buffers->init_state_buffers; + GrdVkDevice *device = view_creator_avc->device; + GrdVkDeviceFuncs *device_funcs = grd_vk_device_get_device_funcs (device); + VkCommandBufferBeginInfo begin_info = {}; + VkDependencyInfo dependency_info = {}; + VkMemoryBarrier2 memory_barrier_2 = {}; + GrdVkBuffer *dmg_buffer; + GrdVkBuffer *chroma_check_buffer; + VkResult vk_result; + + begin_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; + + vk_result = vkBeginCommandBuffer (command_buffer, &begin_info); + if (vk_result != VK_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to begin command buffer: %i", vk_result); + return FALSE; + } + + dmg_buffer = view_creator_avc->dmg_buffer_device; + if (!dmg_buffer) + dmg_buffer = view_creator_avc->dmg_buffer_host; + + chroma_check_buffer = view_creator_avc->chroma_check_buffer_device; + if (!chroma_check_buffer) + chroma_check_buffer = view_creator_avc->chroma_check_buffer_host; + + vkCmdFillBuffer (command_buffer, + grd_vk_buffer_get_buffer (dmg_buffer), + 0, VK_WHOLE_SIZE, + 0); + vkCmdFillBuffer (command_buffer, + grd_vk_buffer_get_buffer (chroma_check_buffer), + 0, VK_WHOLE_SIZE, + 0); + + memory_barrier_2.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2; + memory_barrier_2.srcStageMask = VK_PIPELINE_STAGE_2_CLEAR_BIT; + memory_barrier_2.srcAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT; + memory_barrier_2.dstStageMask = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT; + memory_barrier_2.dstAccessMask = VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT; + + dependency_info.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO; + dependency_info.memoryBarrierCount = 1; + dependency_info.pMemoryBarriers = &memory_barrier_2; + + device_funcs->vkCmdPipelineBarrier2KHR (command_buffer, &dependency_info); + + vk_result = vkEndCommandBuffer (command_buffer); + if (vk_result != VK_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to end command buffer: %i", vk_result); + return FALSE; + } + + return TRUE; +} + +static gboolean +have_device_state_buffers (GrdRdpViewCreatorAVC *view_creator_avc) +{ + gboolean have_device_state_buffers; + + have_device_state_buffers = view_creator_avc->dmg_buffer_device || + view_creator_avc->chroma_check_buffer_device; + if (have_device_state_buffers) + { + g_assert (view_creator_avc->dmg_buffer_device && + view_creator_avc->chroma_check_buffer_device); + } + + return have_device_state_buffers; +} + +static void +record_synchronize_device_state_buffers (GrdRdpViewCreatorAVC *view_creator_avc, + VkCommandBuffer command_buffer) +{ + GrdVkDevice *device = view_creator_avc->device; + GrdVkDeviceFuncs *device_funcs = grd_vk_device_get_device_funcs (device); + GrdVkBuffer *dmg_buffer_device = view_creator_avc->dmg_buffer_device; + GrdVkBuffer *dmg_buffer_host = view_creator_avc->dmg_buffer_host; + GrdVkBuffer *chroma_check_buffer_device = + view_creator_avc->chroma_check_buffer_device; + GrdVkBuffer *chroma_check_buffer_host = + view_creator_avc->chroma_check_buffer_host; + VkDependencyInfo dependency_info = {}; + VkMemoryBarrier2 memory_barrier_2 = {}; + VkBufferCopy buffer_copy = {}; + + memory_barrier_2.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2; + memory_barrier_2.srcStageMask = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT; + memory_barrier_2.srcAccessMask = VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT; + memory_barrier_2.dstStageMask = VK_PIPELINE_STAGE_2_COPY_BIT; + memory_barrier_2.dstAccessMask = VK_ACCESS_2_TRANSFER_READ_BIT; + + dependency_info.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO; + dependency_info.memoryBarrierCount = 1; + dependency_info.pMemoryBarriers = &memory_barrier_2; + + device_funcs->vkCmdPipelineBarrier2KHR (command_buffer, &dependency_info); + + buffer_copy.srcOffset = 0; + buffer_copy.dstOffset = 0; + buffer_copy.size = view_creator_avc->state_buffer_size; + + vkCmdCopyBuffer (command_buffer, + grd_vk_buffer_get_buffer (dmg_buffer_device), + grd_vk_buffer_get_buffer (dmg_buffer_host), + 1, &buffer_copy); + vkCmdCopyBuffer (command_buffer, + grd_vk_buffer_get_buffer (chroma_check_buffer_device), + grd_vk_buffer_get_buffer (chroma_check_buffer_host), + 1, &buffer_copy); + + memory_barrier_2.srcStageMask = VK_PIPELINE_STAGE_2_COPY_BIT; + memory_barrier_2.srcAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT; + memory_barrier_2.dstStageMask = VK_PIPELINE_STAGE_2_HOST_BIT; + memory_barrier_2.dstAccessMask = VK_ACCESS_2_HOST_READ_BIT; + + device_funcs->vkCmdPipelineBarrier2KHR (command_buffer, &dependency_info); +} + +static void +record_synchronize_host_state_buffers (GrdRdpViewCreatorAVC *view_creator_avc, + VkCommandBuffer command_buffer) +{ + GrdVkDevice *device = view_creator_avc->device; + GrdVkDeviceFuncs *device_funcs = grd_vk_device_get_device_funcs (device); + VkDependencyInfo dependency_info = {}; + VkMemoryBarrier2 memory_barrier_2 = {}; + + memory_barrier_2.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2; + memory_barrier_2.srcStageMask = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT; + memory_barrier_2.srcAccessMask = VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT; + memory_barrier_2.dstStageMask = VK_PIPELINE_STAGE_2_HOST_BIT; + memory_barrier_2.dstAccessMask = VK_ACCESS_2_HOST_READ_BIT; + + dependency_info.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO; + dependency_info.memoryBarrierCount = 1; + dependency_info.pMemoryBarriers = &memory_barrier_2; + + device_funcs->vkCmdPipelineBarrier2KHR (command_buffer, &dependency_info); +} + +static gboolean +record_synchronize_state_buffers (GrdRdpViewCreatorAVC *view_creator_avc, + GError **error) +{ + CommandBuffers *command_buffers = &view_creator_avc->command_buffers; + VkCommandBuffer command_buffer = command_buffers->synchronize_state_buffers; + VkCommandBufferBeginInfo begin_info = {}; + VkResult vk_result; + + begin_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; + + vk_result = vkBeginCommandBuffer (command_buffer, &begin_info); + if (vk_result != VK_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to begin command buffer: %i", vk_result); + return FALSE; + } + + if (have_device_state_buffers (view_creator_avc)) + record_synchronize_device_state_buffers (view_creator_avc, command_buffer); + else + record_synchronize_host_state_buffers (view_creator_avc, command_buffer); + + vk_result = vkEndCommandBuffer (command_buffer); + if (vk_result != VK_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to end command buffer: %i", vk_result); + return FALSE; + } + + return TRUE; +} + +static gboolean +record_state_handling_command_buffers (GrdRdpViewCreatorAVC *view_creator_avc, + GError **error) +{ + if (!record_init_state_buffers (view_creator_avc, error)) + return FALSE; + if (!record_synchronize_state_buffers (view_creator_avc, error)) + return FALSE; + + return TRUE; +} + +GrdRdpViewCreatorAVC * +grd_rdp_view_creator_avc_new (GrdVkDevice *device, + uint32_t target_width, + uint32_t target_height, + uint32_t source_width, + uint32_t source_height, + GError **error) +{ + g_autoptr (GrdRdpViewCreatorAVC) view_creator_avc = NULL; + GrdVkPhysicalDevice *physical_device = + grd_vk_device_get_physical_device (device); + GrdVkDeviceFeatures device_features = + grd_vk_physical_device_get_device_features (physical_device); + + view_creator_avc = g_object_new (GRD_TYPE_RDP_VIEW_CREATOR_AVC, NULL); + view_creator_avc->device = device; + + if (device_features & GRD_VK_DEVICE_FEATURE_UPDATE_AFTER_BIND_SAMPLED_IMAGE && + device_features & GRD_VK_DEVICE_FEATURE_UPDATE_AFTER_BIND_STORAGE_IMAGE) + view_creator_avc->supports_update_after_bind = TRUE; + + prepare_view_create_info (view_creator_avc, + target_width, target_height, + source_width, source_height); + + if (!create_state_buffers (view_creator_avc, + source_width, source_height, + error)) + return NULL; + + if (!create_samplers (view_creator_avc, error)) + return NULL; + if (!create_descriptor_set_layouts (view_creator_avc, error)) + return NULL; + if (!create_descriptor_sets (view_creator_avc, error)) + return NULL; + if (!create_pipelines (view_creator_avc, error)) + return NULL; + + view_creator_avc->queue = grd_vk_device_acquire_queue (device); + if (!create_timestamp_query_pool (view_creator_avc, error)) + return NULL; + + if (!create_command_buffers (view_creator_avc, error)) + return NULL; + if (!create_fence (view_creator_avc, error)) + return NULL; + + write_state_descriptor_set (view_creator_avc); + if (!record_state_handling_command_buffers (view_creator_avc, error)) + return NULL; + + if (view_creator_avc->supports_update_after_bind && + !record_create_view_command_buffers (view_creator_avc, TRUE, TRUE, error)) + return NULL; + + if (view_creator_avc->supports_update_after_bind) + view_creator_avc->pending_view_creation_recording = FALSE; + + return g_steal_pointer (&view_creator_avc); +} + +static void +grd_rdp_view_creator_avc_dispose (GObject *object) +{ + GrdRdpViewCreatorAVC *view_creator_avc = GRD_RDP_VIEW_CREATOR_AVC (object); + GrdVkDevice *device = view_creator_avc->device; + + grd_vk_clear_fence (device, &view_creator_avc->vk_fence); + grd_vk_clear_command_pool (device, &view_creator_avc->vk_command_pool); + grd_vk_clear_query_pool (device, &view_creator_avc->vk_timestamp_query_pool); + + if (view_creator_avc->queue) + { + grd_vk_device_release_queue (device, view_creator_avc->queue); + view_creator_avc->queue = NULL; + } + + g_clear_pointer (&view_creator_avc->dual_view_dmg_pipeline, pipeline_free); + g_clear_pointer (&view_creator_avc->dual_view_pipeline, pipeline_free); + + grd_vk_clear_descriptor_pool (device, &view_creator_avc->vk_buffer_descriptor_pool); + grd_vk_clear_descriptor_pool (device, &view_creator_avc->vk_image_descriptor_pool); + grd_vk_clear_descriptor_set_layout (device, + &view_creator_avc->vk_state_descriptor_set_layout); + grd_vk_clear_descriptor_set_layout (device, + &view_creator_avc->vk_source_descriptor_set_layout); + grd_vk_clear_descriptor_set_layout (device, + &view_creator_avc->vk_target_descriptor_set_layout); + grd_vk_clear_sampler (device, &view_creator_avc->vk_src_old_sampler); + grd_vk_clear_sampler (device, &view_creator_avc->vk_src_new_sampler); + + g_clear_object (&view_creator_avc->chroma_check_buffer_device); + g_clear_object (&view_creator_avc->chroma_check_buffer_host); + g_clear_object (&view_creator_avc->dmg_buffer_device); + g_clear_object (&view_creator_avc->dmg_buffer_host); + + G_OBJECT_CLASS (grd_rdp_view_creator_avc_parent_class)->dispose (object); +} + +static void +grd_rdp_view_creator_avc_init (GrdRdpViewCreatorAVC *view_creator_avc) +{ + if (grd_get_debug_flags () & GRD_DEBUG_VK_TIMES) + view_creator_avc->debug_vk_times = TRUE; + + view_creator_avc->pending_view_creation_recording = TRUE; +} + +static void +grd_rdp_view_creator_avc_class_init (GrdRdpViewCreatorAVCClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GrdRdpViewCreatorClass *view_creator_class = + GRD_RDP_VIEW_CREATOR_CLASS (klass); + + object_class->dispose = grd_rdp_view_creator_avc_dispose; + + view_creator_class->create_view = grd_rdp_view_creator_avc_create_view; + view_creator_class->finish_view = grd_rdp_view_creator_avc_finish_view; +} diff --git a/grd-rdp-view-creator-avc.h b/grd-rdp-view-creator-avc.h new file mode 100644 index 0000000..fd2e8b9 --- /dev/null +++ b/grd-rdp-view-creator-avc.h @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2024 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include + +#include "grd-rdp-view-creator.h" + +#define GRD_TYPE_RDP_VIEW_CREATOR_AVC (grd_rdp_view_creator_avc_get_type ()) +G_DECLARE_FINAL_TYPE (GrdRdpViewCreatorAVC, grd_rdp_view_creator_avc, + GRD, RDP_VIEW_CREATOR_AVC, GrdRdpViewCreator) + +GrdRdpViewCreatorAVC *grd_rdp_view_creator_avc_new (GrdVkDevice *device, + uint32_t target_width, + uint32_t target_height, + uint32_t source_width, + uint32_t source_height, + GError **error); diff --git a/grd-rdp-view-creator-gen-gl.c b/grd-rdp-view-creator-gen-gl.c new file mode 100644 index 0000000..3d1b6c0 --- /dev/null +++ b/grd-rdp-view-creator-gen-gl.c @@ -0,0 +1,362 @@ +/* + * Copyright (C) 2024 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-rdp-view-creator-gen-gl.h" + +#include + +#include "grd-context.h" +#include "grd-damage-detector-sw.h" +#include "grd-egl-thread.h" +#include "grd-image-view-rgb.h" +#include "grd-local-buffer-copy.h" +#include "grd-rdp-buffer.h" +#include "grd-rdp-pw-buffer.h" +#include "grd-rdp-render-context.h" +#include "grd-rdp-render-state.h" +#include "grd-rdp-renderer.h" +#include "grd-rdp-server.h" +#include "grd-session-rdp.h" +#include "grd-utils.h" + +/* + * One buffer is needed, when encoding a frame, + * one is needed as download-target for the current dma-buf image, + * one is needed for an already encoded frame, that was not yet + * submitted to the client + * + * In total, this makes three needed buffers + */ +#define N_LOCAL_BUFFERS 3 + +typedef struct +{ + GrdRdpViewCreatorGenGL *view_creator_gen_gl; + + GrdImageView *image_view; + GrdRdpBuffer *src_buffer_new; + + GrdLocalBuffer *local_buffer_new; + GrdLocalBuffer *local_buffer_old; + + GrdSyncPoint sync_point; +} ViewContext; + +struct _GrdRdpViewCreatorGenGL +{ + GrdRdpViewCreator parent; + + GrdRdpRenderContext *render_context; + uint32_t surface_width; + uint32_t surface_height; + + GrdLocalBuffer *local_buffers[N_LOCAL_BUFFERS]; + GHashTable *acquired_buffers; + + GrdDamageDetectorSw *damage_detector; + + GrdRdpBuffer *last_src_buffer; + GrdLocalBuffer *last_local_buffer; + + ViewContext *current_view_context; +}; + +G_DEFINE_TYPE (GrdRdpViewCreatorGenGL, grd_rdp_view_creator_gen_gl, + GRD_TYPE_RDP_VIEW_CREATOR) + +static void +view_context_free (ViewContext *view_context); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (ViewContext, view_context_free) + +static GrdLocalBuffer * +acquire_local_buffer (GrdRdpViewCreatorGenGL *view_creator_gen_gl) +{ + uint32_t i; + + for (i = 0; i < N_LOCAL_BUFFERS; ++i) + { + GrdLocalBuffer *local_buffer = view_creator_gen_gl->local_buffers[i]; + + if (g_hash_table_contains (view_creator_gen_gl->acquired_buffers, + local_buffer)) + continue; + if (local_buffer == view_creator_gen_gl->last_local_buffer) + continue; + + g_hash_table_add (view_creator_gen_gl->acquired_buffers, local_buffer); + + return local_buffer; + } + + g_assert_not_reached (); + return NULL; +} + +static void +release_local_buffer (GrdRdpViewCreatorGenGL *view_creator_gen_gl, + GrdLocalBuffer *local_buffer) +{ + if (!g_hash_table_remove (view_creator_gen_gl->acquired_buffers, local_buffer)) + g_assert_not_reached (); +} + +static ViewContext * +view_context_new (GrdRdpViewCreatorGenGL *view_creator_gen_gl, + GrdImageView *image_view, + GrdRdpBuffer *src_buffer_new, + GrdRdpBuffer *src_buffer_old) +{ + ViewContext *view_context; + + view_context = g_new0 (ViewContext, 1); + view_context->view_creator_gen_gl = view_creator_gen_gl; + view_context->image_view = image_view; + view_context->src_buffer_new = src_buffer_new; + + view_context->local_buffer_new = acquire_local_buffer (view_creator_gen_gl); + + if (src_buffer_old && + src_buffer_old == view_creator_gen_gl->last_src_buffer) + view_context->local_buffer_old = view_creator_gen_gl->last_local_buffer; + + grd_sync_point_init (&view_context->sync_point); + + return view_context; +} + +static void +view_context_free (ViewContext *view_context) +{ + GrdRdpViewCreatorGenGL *view_creator_gen_gl = + view_context->view_creator_gen_gl; + + grd_sync_point_clear (&view_context->sync_point); + if (view_context->local_buffer_new) + release_local_buffer (view_creator_gen_gl, view_context->local_buffer_new); + + g_free (view_context); +} + +static uint32_t +get_bpp_from_drm_format (uint32_t drm_format) +{ + switch (drm_format) + { + case DRM_FORMAT_BGRA8888: + case DRM_FORMAT_ARGB8888: + case DRM_FORMAT_BGRX8888: + case DRM_FORMAT_XRGB8888: + return 4; + default: + g_assert_not_reached (); + } +} + +static void +on_dma_buf_downloaded (gboolean success, + gpointer user_data) +{ + GrdSyncPoint *sync_point = user_data; + + grd_sync_point_complete (sync_point, success); +} + +static gboolean +grd_rdp_view_creator_gen_gl_create_view (GrdRdpViewCreator *view_creator, + GList *image_views, + GrdRdpBuffer *src_buffer_new, + GrdRdpBuffer *src_buffer_old, + GError **error) +{ + GrdRdpViewCreatorGenGL *view_creator_gen_gl = + GRD_RDP_VIEW_CREATOR_GEN_GL (view_creator); + GrdRdpRenderer *renderer = + grd_rdp_render_context_get_renderer (view_creator_gen_gl->render_context); + GrdSessionRdp *session_rdp = grd_rdp_renderer_get_session (renderer); + GrdRdpServer *rdp_server = grd_session_rdp_get_server (session_rdp); + GrdContext *context = grd_rdp_server_get_context (rdp_server); + GrdEglThread *egl_thread = grd_context_get_egl_thread (context); + GrdRdpPwBuffer *rdp_pw_buffer = + grd_rdp_buffer_get_rdp_pw_buffer (src_buffer_new); + const GrdRdpPwBufferDmaBufInfo *dma_buf_info = + grd_rdp_pw_buffer_get_dma_buf_info (rdp_pw_buffer); + const GrdRdpBufferInfo *rdp_buffer_info = + grd_rdp_buffer_get_rdp_buffer_info (src_buffer_new); + ViewContext *view_context; + GrdLocalBuffer *local_buffer_new; + int row_width; + + g_assert (image_views); + g_assert (!image_views->next); + + g_assert (!view_creator_gen_gl->current_view_context); + + view_context = view_context_new (view_creator_gen_gl, image_views->data, + src_buffer_new, src_buffer_old); + local_buffer_new = view_context->local_buffer_new; + + row_width = grd_local_buffer_get_buffer_stride (local_buffer_new) / + get_bpp_from_drm_format (rdp_buffer_info->drm_format); + + grd_egl_thread_download (egl_thread, + NULL, + grd_local_buffer_get_buffer (local_buffer_new), + row_width, + rdp_buffer_info->drm_format, + view_creator_gen_gl->surface_width, + view_creator_gen_gl->surface_height, + 1, + &dma_buf_info->fd, + (uint32_t *) &dma_buf_info->stride, + &dma_buf_info->offset, + &rdp_buffer_info->drm_format_modifier, + on_dma_buf_downloaded, + &view_context->sync_point, + NULL); + + view_creator_gen_gl->current_view_context = view_context; + + return TRUE; +} + +static void +on_image_view_release (gpointer user_data, + GrdLocalBuffer *local_buffer) +{ + GrdRdpViewCreatorGenGL *view_creator_gen_gl = user_data; + + release_local_buffer (view_creator_gen_gl, local_buffer); +} + +static GrdRdpRenderState * +grd_rdp_view_creator_gen_gl_finish_view (GrdRdpViewCreator *view_creator, + GError **error) +{ + GrdRdpViewCreatorGenGL *view_creator_gen_gl = + GRD_RDP_VIEW_CREATOR_GEN_GL (view_creator); + GrdDamageDetectorSw *damage_detector = view_creator_gen_gl->damage_detector; + g_autoptr (ViewContext) view_context = NULL; + GrdLocalBuffer *local_buffer_new; + GrdImageViewRGB *image_view_rgb; + uint32_t *damage_buffer; + uint32_t damage_buffer_length; + + view_context = g_steal_pointer (&view_creator_gen_gl->current_view_context); + + if (!grd_sync_point_wait_for_completion (&view_context->sync_point)) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to download dma-buf image"); + return NULL; + } + + grd_damage_detector_sw_compute_damage (damage_detector, + view_context->local_buffer_new, + view_context->local_buffer_old); + + local_buffer_new = g_steal_pointer (&view_context->local_buffer_new); + image_view_rgb = GRD_IMAGE_VIEW_RGB (view_context->image_view); + + view_creator_gen_gl->last_src_buffer = view_context->src_buffer_new; + view_creator_gen_gl->last_local_buffer = local_buffer_new; + + grd_image_view_rgb_attach_local_buffer (image_view_rgb, + local_buffer_new, + on_image_view_release, + view_creator_gen_gl); + + damage_buffer = grd_damage_detector_sw_get_damage_buffer (damage_detector); + damage_buffer_length = + grd_damage_detector_sw_get_damage_buffer_length (damage_detector); + + return grd_rdp_render_state_new (damage_buffer, NULL, damage_buffer_length); +} + +GrdRdpViewCreatorGenGL * +grd_rdp_view_creator_gen_gl_new (GrdRdpRenderContext *render_context, + uint32_t surface_width, + uint32_t surface_height) +{ + GrdRdpViewCreatorGenGL *view_creator_gen_gl; + uint32_t i; + + view_creator_gen_gl = g_object_new (GRD_TYPE_RDP_VIEW_CREATOR_GEN_GL, NULL); + view_creator_gen_gl->render_context = render_context; + view_creator_gen_gl->surface_width = surface_width; + view_creator_gen_gl->surface_height = surface_height; + + view_creator_gen_gl->damage_detector = + grd_damage_detector_sw_new (surface_width, surface_height); + + for (i = 0; i < N_LOCAL_BUFFERS; ++i) + { + GrdLocalBufferCopy *local_buffer_copy; + + local_buffer_copy = grd_local_buffer_copy_new (surface_width, + surface_height); + view_creator_gen_gl->local_buffers[i] = + GRD_LOCAL_BUFFER (local_buffer_copy); + } + + return view_creator_gen_gl; +} + +static void +grd_rdp_view_creator_gen_gl_dispose (GObject *object) +{ + GrdRdpViewCreatorGenGL *view_creator_gen_gl = + GRD_RDP_VIEW_CREATOR_GEN_GL (object); + uint32_t i; + + if (view_creator_gen_gl->acquired_buffers) + g_assert (g_hash_table_size (view_creator_gen_gl->acquired_buffers) == 0); + + g_assert (!view_creator_gen_gl->current_view_context); + + g_clear_object (&view_creator_gen_gl->damage_detector); + + g_clear_pointer (&view_creator_gen_gl->acquired_buffers, g_hash_table_unref); + + for (i = 0; i < N_LOCAL_BUFFERS; ++i) + g_clear_object (&view_creator_gen_gl->local_buffers[i]); + + G_OBJECT_CLASS (grd_rdp_view_creator_gen_gl_parent_class)->dispose (object); +} + +static void +grd_rdp_view_creator_gen_gl_init (GrdRdpViewCreatorGenGL *view_creator_gen_gl) +{ + view_creator_gen_gl->acquired_buffers = g_hash_table_new (NULL, NULL); +} + +static void +grd_rdp_view_creator_gen_gl_class_init (GrdRdpViewCreatorGenGLClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GrdRdpViewCreatorClass *view_creator_class = + GRD_RDP_VIEW_CREATOR_CLASS (klass); + + object_class->dispose = grd_rdp_view_creator_gen_gl_dispose; + + view_creator_class->create_view = grd_rdp_view_creator_gen_gl_create_view; + view_creator_class->finish_view = grd_rdp_view_creator_gen_gl_finish_view; +} diff --git a/grd-rdp-view-creator-gen-gl.h b/grd-rdp-view-creator-gen-gl.h new file mode 100644 index 0000000..03c1b99 --- /dev/null +++ b/grd-rdp-view-creator-gen-gl.h @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2024 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include + +#include "grd-rdp-view-creator.h" + +#define GRD_TYPE_RDP_VIEW_CREATOR_GEN_GL (grd_rdp_view_creator_gen_gl_get_type ()) +G_DECLARE_FINAL_TYPE (GrdRdpViewCreatorGenGL, grd_rdp_view_creator_gen_gl, + GRD, RDP_VIEW_CREATOR_GEN_GL, GrdRdpViewCreator) + +GrdRdpViewCreatorGenGL *grd_rdp_view_creator_gen_gl_new (GrdRdpRenderContext *render_context, + uint32_t surface_width, + uint32_t surface_height); diff --git a/grd-rdp-view-creator-gen-sw.c b/grd-rdp-view-creator-gen-sw.c new file mode 100644 index 0000000..21a9793 --- /dev/null +++ b/grd-rdp-view-creator-gen-sw.c @@ -0,0 +1,273 @@ +/* + * Copyright (C) 2025 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-rdp-view-creator-gen-sw.h" + +#include "grd-damage-detector-sw.h" +#include "grd-image-view-rgb.h" +#include "grd-local-buffer-wrapper-rdp.h" +#include "grd-rdp-render-state.h" + +/* + * One buffer is needed, when encoding a frame, + * one is needed for the current view creation, + * one is needed for an already encoded frame, + * that was not yet submitted to the client + * + * In total, this makes three needed buffers + */ +#define N_LOCAL_BUFFERS 3 + +typedef struct +{ + GrdRdpViewCreatorGenSW *view_creator_gen_sw; + + GrdImageView *image_view; + GrdRdpBuffer *src_buffer_new; + + GrdLocalBuffer *local_buffer_new; + GrdLocalBuffer *local_buffer_old; +} ViewContext; + +struct _GrdRdpViewCreatorGenSW +{ + GrdRdpViewCreator parent; + + GrdLocalBuffer *local_buffers[N_LOCAL_BUFFERS]; + GHashTable *acquired_buffers; + + GrdDamageDetectorSw *damage_detector; + + GrdRdpBuffer *last_src_buffer; + GrdLocalBuffer *last_local_buffer; + + ViewContext *current_view_context; +}; + +G_DEFINE_TYPE (GrdRdpViewCreatorGenSW, grd_rdp_view_creator_gen_sw, + GRD_TYPE_RDP_VIEW_CREATOR) + +static void +view_context_free (ViewContext *view_context); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (ViewContext, view_context_free) + +static GrdLocalBuffer * +acquire_local_buffer (GrdRdpViewCreatorGenSW *view_creator_gen_sw) +{ + uint32_t i; + + for (i = 0; i < N_LOCAL_BUFFERS; ++i) + { + GrdLocalBuffer *local_buffer = view_creator_gen_sw->local_buffers[i]; + + if (g_hash_table_contains (view_creator_gen_sw->acquired_buffers, + local_buffer)) + continue; + if (local_buffer == view_creator_gen_sw->last_local_buffer) + continue; + + g_hash_table_add (view_creator_gen_sw->acquired_buffers, local_buffer); + + return local_buffer; + } + + g_assert_not_reached (); + return NULL; +} + +static void +release_local_buffer (GrdRdpViewCreatorGenSW *view_creator_gen_sw, + GrdLocalBuffer *local_buffer) +{ + if (!g_hash_table_remove (view_creator_gen_sw->acquired_buffers, local_buffer)) + g_assert_not_reached (); +} + +static ViewContext * +view_context_new (GrdRdpViewCreatorGenSW *view_creator_gen_sw, + GrdImageView *image_view, + GrdRdpBuffer *src_buffer_new, + GrdRdpBuffer *src_buffer_old) +{ + ViewContext *view_context; + GrdLocalBufferWrapperRdp *buffer_wrapper; + + view_context = g_new0 (ViewContext, 1); + view_context->view_creator_gen_sw = view_creator_gen_sw; + view_context->image_view = image_view; + view_context->src_buffer_new = src_buffer_new; + + view_context->local_buffer_new = acquire_local_buffer (view_creator_gen_sw); + + buffer_wrapper = + GRD_LOCAL_BUFFER_WRAPPER_RDP (view_context->local_buffer_new); + grd_local_buffer_wrapper_rdp_attach_rdp_buffer (buffer_wrapper, + src_buffer_new); + + if (src_buffer_old && + src_buffer_old == view_creator_gen_sw->last_src_buffer) + view_context->local_buffer_old = view_creator_gen_sw->last_local_buffer; + + return view_context; +} + +static void +view_context_free (ViewContext *view_context) +{ + GrdRdpViewCreatorGenSW *view_creator_gen_sw = + view_context->view_creator_gen_sw; + + if (view_context->local_buffer_new) + release_local_buffer (view_creator_gen_sw, view_context->local_buffer_new); + + g_free (view_context); +} + +static gboolean +grd_rdp_view_creator_gen_sw_create_view (GrdRdpViewCreator *view_creator, + GList *image_views, + GrdRdpBuffer *src_buffer_new, + GrdRdpBuffer *src_buffer_old, + GError **error) +{ + GrdRdpViewCreatorGenSW *view_creator_gen_sw = + GRD_RDP_VIEW_CREATOR_GEN_SW (view_creator); + + g_assert (image_views); + g_assert (!image_views->next); + + g_assert (!view_creator_gen_sw->current_view_context); + + view_creator_gen_sw->current_view_context = + view_context_new (view_creator_gen_sw, image_views->data, + src_buffer_new, src_buffer_old); + + return TRUE; +} + +static void +on_image_view_release (gpointer user_data, + GrdLocalBuffer *local_buffer) +{ + GrdRdpViewCreatorGenSW *view_creator_gen_sw = user_data; + + release_local_buffer (view_creator_gen_sw, local_buffer); +} + +static GrdRdpRenderState * +grd_rdp_view_creator_gen_sw_finish_view (GrdRdpViewCreator *view_creator, + GError **error) +{ + GrdRdpViewCreatorGenSW *view_creator_gen_sw = + GRD_RDP_VIEW_CREATOR_GEN_SW (view_creator); + GrdDamageDetectorSw *damage_detector = view_creator_gen_sw->damage_detector; + g_autoptr (ViewContext) view_context = NULL; + GrdLocalBuffer *local_buffer_new; + GrdImageViewRGB *image_view_rgb; + uint32_t *damage_buffer; + uint32_t damage_buffer_length; + + view_context = g_steal_pointer (&view_creator_gen_sw->current_view_context); + + grd_damage_detector_sw_compute_damage (damage_detector, + view_context->local_buffer_new, + view_context->local_buffer_old); + + local_buffer_new = g_steal_pointer (&view_context->local_buffer_new); + image_view_rgb = GRD_IMAGE_VIEW_RGB (view_context->image_view); + + view_creator_gen_sw->last_src_buffer = view_context->src_buffer_new; + view_creator_gen_sw->last_local_buffer = local_buffer_new; + + grd_image_view_rgb_attach_local_buffer (image_view_rgb, + local_buffer_new, + on_image_view_release, + view_creator_gen_sw); + + damage_buffer = grd_damage_detector_sw_get_damage_buffer (damage_detector); + damage_buffer_length = + grd_damage_detector_sw_get_damage_buffer_length (damage_detector); + + return grd_rdp_render_state_new (damage_buffer, NULL, damage_buffer_length); +} + +GrdRdpViewCreatorGenSW * +grd_rdp_view_creator_gen_sw_new (uint32_t surface_width, + uint32_t surface_height) +{ + GrdRdpViewCreatorGenSW *view_creator_gen_sw; + uint32_t i; + + view_creator_gen_sw = g_object_new (GRD_TYPE_RDP_VIEW_CREATOR_GEN_SW, NULL); + + view_creator_gen_sw->damage_detector = + grd_damage_detector_sw_new (surface_width, surface_height); + + for (i = 0; i < N_LOCAL_BUFFERS; ++i) + { + view_creator_gen_sw->local_buffers[i] = + GRD_LOCAL_BUFFER (grd_local_buffer_wrapper_rdp_new ()); + } + + return view_creator_gen_sw; +} + +static void +grd_rdp_view_creator_gen_sw_dispose (GObject *object) +{ + GrdRdpViewCreatorGenSW *view_creator_gen_sw = + GRD_RDP_VIEW_CREATOR_GEN_SW (object); + uint32_t i; + + if (view_creator_gen_sw->acquired_buffers) + g_assert (g_hash_table_size (view_creator_gen_sw->acquired_buffers) == 0); + + g_assert (!view_creator_gen_sw->current_view_context); + + g_clear_object (&view_creator_gen_sw->damage_detector); + + g_clear_pointer (&view_creator_gen_sw->acquired_buffers, g_hash_table_unref); + + for (i = 0; i < N_LOCAL_BUFFERS; ++i) + g_clear_object (&view_creator_gen_sw->local_buffers[i]); + + G_OBJECT_CLASS (grd_rdp_view_creator_gen_sw_parent_class)->dispose (object); +} + +static void +grd_rdp_view_creator_gen_sw_init (GrdRdpViewCreatorGenSW *view_creator_gen_sw) +{ + view_creator_gen_sw->acquired_buffers = g_hash_table_new (NULL, NULL); +} + +static void +grd_rdp_view_creator_gen_sw_class_init (GrdRdpViewCreatorGenSWClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GrdRdpViewCreatorClass *view_creator_class = + GRD_RDP_VIEW_CREATOR_CLASS (klass); + + object_class->dispose = grd_rdp_view_creator_gen_sw_dispose; + + view_creator_class->create_view = grd_rdp_view_creator_gen_sw_create_view; + view_creator_class->finish_view = grd_rdp_view_creator_gen_sw_finish_view; +} diff --git a/grd-rdp-view-creator-gen-sw.h b/grd-rdp-view-creator-gen-sw.h new file mode 100644 index 0000000..18f1118 --- /dev/null +++ b/grd-rdp-view-creator-gen-sw.h @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2025 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include + +#include "grd-rdp-view-creator.h" + +#define GRD_TYPE_RDP_VIEW_CREATOR_GEN_SW (grd_rdp_view_creator_gen_sw_get_type ()) +G_DECLARE_FINAL_TYPE (GrdRdpViewCreatorGenSW, grd_rdp_view_creator_gen_sw, + GRD, RDP_VIEW_CREATOR_GEN_SW, GrdRdpViewCreator) + +GrdRdpViewCreatorGenSW *grd_rdp_view_creator_gen_sw_new (uint32_t surface_width, + uint32_t surface_height); diff --git a/grd-rdp-view-creator.c b/grd-rdp-view-creator.c new file mode 100644 index 0000000..89b2101 --- /dev/null +++ b/grd-rdp-view-creator.c @@ -0,0 +1,215 @@ +/* + * Copyright (C) 2024 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-rdp-view-creator.h" + +#include "grd-rdp-frame.h" +#include "grd-rdp-render-context.h" + +typedef struct +{ + GrdRdpViewCreatorOnViewCreatedFunc callback; + GrdRdpFrame *rdp_frame; +} GrdViewCreationTask; + +typedef struct +{ + gboolean in_shutdown; + + GThread *view_creation_thread; + GMainContext *view_creation_context; + + GSource *view_creation_source; + GAsyncQueue *task_queue; +} GrdRdpViewCreatorPrivate; + +G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (GrdRdpViewCreator, grd_rdp_view_creator, + G_TYPE_OBJECT) + +gboolean +grd_rdp_view_creator_create_view (GrdRdpViewCreator *view_creator, + GrdRdpFrame *rdp_frame, + GrdRdpViewCreatorOnViewCreatedFunc on_view_created, + GError **error) +{ + GrdRdpViewCreatorClass *klass = GRD_RDP_VIEW_CREATOR_GET_CLASS (view_creator); + GrdRdpViewCreatorPrivate *priv = + grd_rdp_view_creator_get_instance_private (view_creator); + GList *image_views = grd_rdp_frame_get_image_views (rdp_frame); + GrdRdpBuffer *src_buffer_new = grd_rdp_frame_get_source_buffer (rdp_frame); + GrdRdpBuffer *src_buffer_old = grd_rdp_frame_get_last_source_buffer (rdp_frame); + GrdViewCreationTask *task; + + if (!klass->create_view (view_creator, image_views, + src_buffer_new, src_buffer_old, error)) + return FALSE; + + task = g_new0 (GrdViewCreationTask, 1); + task->callback = on_view_created; + task->rdp_frame = rdp_frame; + + g_async_queue_push (priv->task_queue, task); + g_source_set_ready_time (priv->view_creation_source, 0); + + return TRUE; +} + +static void +stop_view_creation_thread (GrdRdpViewCreator *view_creator) +{ + GrdRdpViewCreatorPrivate *priv = + grd_rdp_view_creator_get_instance_private (view_creator); + + g_assert (priv->view_creation_context); + g_assert (priv->view_creation_thread); + + priv->in_shutdown = TRUE; + + g_main_context_wakeup (priv->view_creation_context); + g_clear_pointer (&priv->view_creation_thread, g_thread_join); +} + +static void +grd_rdp_view_creator_dispose (GObject *object) +{ + GrdRdpViewCreator *view_creator = GRD_RDP_VIEW_CREATOR (object); + GrdRdpViewCreatorPrivate *priv = + grd_rdp_view_creator_get_instance_private (view_creator); + + g_assert (g_async_queue_try_pop (priv->task_queue) == NULL); + + if (priv->view_creation_thread) + stop_view_creation_thread (view_creator); + + if (priv->view_creation_source) + { + g_source_destroy (priv->view_creation_source); + g_clear_pointer (&priv->view_creation_source, g_source_unref); + } + + g_clear_pointer (&priv->view_creation_context, g_main_context_unref); + + G_OBJECT_CLASS (grd_rdp_view_creator_parent_class)->dispose (object); +} + +static void +grd_rdp_view_creator_finalize (GObject *object) +{ + GrdRdpViewCreator *view_creator = GRD_RDP_VIEW_CREATOR (object); + GrdRdpViewCreatorPrivate *priv = + grd_rdp_view_creator_get_instance_private (view_creator); + + g_clear_pointer (&priv->task_queue, g_async_queue_unref); + + G_OBJECT_CLASS (grd_rdp_view_creator_parent_class)->finalize (object); +} + +static gpointer +view_creation_thread_func (gpointer data) +{ + GrdRdpViewCreator *view_creator = data; + GrdRdpViewCreatorPrivate *priv = + grd_rdp_view_creator_get_instance_private (view_creator); + + while (!priv->in_shutdown) + g_main_context_iteration (priv->view_creation_context, TRUE); + + return NULL; +} + +static gboolean +finish_views (gpointer user_data) +{ + GrdRdpViewCreator *view_creator = user_data; + GrdRdpViewCreatorClass *klass = GRD_RDP_VIEW_CREATOR_GET_CLASS (view_creator); + GrdRdpViewCreatorPrivate *priv = + grd_rdp_view_creator_get_instance_private (view_creator); + GrdViewCreationTask *task; + + while ((task = g_async_queue_try_pop (priv->task_queue))) + { + GrdRdpFrame *rdp_frame = task->rdp_frame; + GrdRdpRenderState *render_state; + g_autoptr (GError) error = NULL; + + render_state = klass->finish_view (view_creator, &error); + if (render_state) + { + GrdRdpRenderContext *render_context = + grd_rdp_frame_get_render_context (rdp_frame); + + grd_rdp_render_context_update_frame_state (render_context, rdp_frame, + render_state); + } + + task->callback (rdp_frame, error); + + g_free (task); + } + + return G_SOURCE_CONTINUE; +} + +static gboolean +source_dispatch (GSource *source, + GSourceFunc callback, + gpointer user_data) +{ + g_source_set_ready_time (source, -1); + + return callback (user_data); +} + +static GSourceFuncs source_funcs = +{ + .dispatch = source_dispatch, +}; + +static void +grd_rdp_view_creator_init (GrdRdpViewCreator *view_creator) +{ + GrdRdpViewCreatorPrivate *priv = + grd_rdp_view_creator_get_instance_private (view_creator); + GSource *view_creation_source; + + priv->task_queue = g_async_queue_new (); + + priv->view_creation_context = g_main_context_new (); + priv->view_creation_thread = g_thread_new ("View creation thread", + view_creation_thread_func, + view_creator); + + view_creation_source = g_source_new (&source_funcs, sizeof (GSource)); + g_source_set_callback (view_creation_source, finish_views, + view_creator, NULL); + g_source_set_ready_time (view_creation_source, -1); + g_source_attach (view_creation_source, priv->view_creation_context); + priv->view_creation_source = view_creation_source; +} + +static void +grd_rdp_view_creator_class_init (GrdRdpViewCreatorClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = grd_rdp_view_creator_dispose; + object_class->finalize = grd_rdp_view_creator_finalize; +} diff --git a/grd-rdp-view-creator.h b/grd-rdp-view-creator.h new file mode 100644 index 0000000..e0b4b9d --- /dev/null +++ b/grd-rdp-view-creator.h @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2024 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include + +#include "grd-types.h" + +#define GRD_TYPE_RDP_VIEW_CREATOR (grd_rdp_view_creator_get_type ()) +G_DECLARE_DERIVABLE_TYPE (GrdRdpViewCreator, grd_rdp_view_creator, + GRD, RDP_VIEW_CREATOR, GObject) + +typedef void (* GrdRdpViewCreatorOnViewCreatedFunc) (GrdRdpFrame *rdp_frame, + GError *error); + +struct _GrdRdpViewCreatorClass +{ + GObjectClass parent_class; + + gboolean (* create_view) (GrdRdpViewCreator *view_creator, + GList *image_views, + GrdRdpBuffer *src_buffer_new, + GrdRdpBuffer *src_buffer_old, + GError **error); + GrdRdpRenderState *(* finish_view) (GrdRdpViewCreator *view_creator, + GError **error); +}; + +gboolean grd_rdp_view_creator_create_view (GrdRdpViewCreator *view_creator, + GrdRdpFrame *rdp_frame, + GrdRdpViewCreatorOnViewCreatedFunc on_view_created, + GError **error); diff --git a/grd-sample-buffer.c b/grd-sample-buffer.c new file mode 100644 index 0000000..a22f4a8 --- /dev/null +++ b/grd-sample-buffer.c @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2025 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-sample-buffer.h" + +#include + +struct _GrdSampleBuffer +{ + uint8_t *data; + size_t max_size; + + size_t used_size; +}; + +uint8_t * +grd_sample_buffer_get_data_pointer (GrdSampleBuffer *sample_buffer) +{ + return sample_buffer->data; +} + +gboolean +grd_sample_buffer_load_sample (GrdSampleBuffer *sample_buffer, + uint8_t *sample_data, + size_t sample_size, + GError **error) +{ + if (sample_size > sample_buffer->max_size) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "The sample to load " + "is too large (have: %zu Bytes, need: %zu Bytes)", + sample_buffer->max_size, sample_size); + return FALSE; + } + + memcpy (sample_buffer->data, sample_data, sample_size); + sample_buffer->used_size = sample_size; + + return TRUE; +} + +size_t +grd_sample_buffer_get_sample_size (GrdSampleBuffer *sample_buffer) +{ + return sample_buffer->used_size; +} + +GrdSampleBuffer * +grd_sample_buffer_new (uint8_t *data, + size_t data_size) +{ + GrdSampleBuffer *sample_buffer; + + sample_buffer = g_new0 (GrdSampleBuffer, 1); + sample_buffer->data = data; + sample_buffer->max_size = data_size; + + return sample_buffer; +} + +void +grd_sample_buffer_free (GrdSampleBuffer *sample_buffer) +{ + g_free (sample_buffer); +} diff --git a/grd-sample-buffer.h b/grd-sample-buffer.h new file mode 100644 index 0000000..54fa4ce --- /dev/null +++ b/grd-sample-buffer.h @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2025 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include +#include + +#include "grd-types.h" + +GrdSampleBuffer *grd_sample_buffer_new (uint8_t *data, + size_t data_size); + +void grd_sample_buffer_free (GrdSampleBuffer *sample_buffer); + +uint8_t *grd_sample_buffer_get_data_pointer (GrdSampleBuffer *sample_buffer); + +gboolean grd_sample_buffer_load_sample (GrdSampleBuffer *sample_buffer, + uint8_t *sample_data, + size_t sample_size, + GError **error); + +size_t grd_sample_buffer_get_sample_size (GrdSampleBuffer *sample_buffer); diff --git a/grd-session-rdp.c b/grd-session-rdp.c new file mode 100644 index 0000000..cfa7d6c --- /dev/null +++ b/grd-session-rdp.c @@ -0,0 +1,2165 @@ +/* + * Copyright (C) 2020-2023 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-session-rdp.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "grd-clipboard-rdp.h" +#include "grd-context.h" +#include "grd-rdp-cursor-renderer.h" +#include "grd-rdp-dvc-audio-input.h" +#include "grd-rdp-dvc-audio-playback.h" +#include "grd-rdp-dvc-camera-enumerator.h" +#include "grd-rdp-dvc-display-control.h" +#include "grd-rdp-dvc-graphics-pipeline.h" +#include "grd-rdp-dvc-handler.h" +#include "grd-rdp-dvc-input.h" +#include "grd-rdp-dvc-telemetry.h" +#include "grd-rdp-event-queue.h" +#include "grd-rdp-layout-manager.h" +#include "grd-rdp-network-autodetection.h" +#include "grd-rdp-private.h" +#include "grd-rdp-renderer.h" +#include "grd-rdp-sam.h" +#include "grd-rdp-server.h" +#include "grd-rdp-session-metrics.h" +#include "grd-settings.h" +#include "grd-utils.h" + +#define MAX_MONITOR_COUNT_HEADLESS 16 +#define MAX_MONITOR_COUNT_SCREEN_SHARE 1 +#define DISCRETE_SCROLL_STEP 10.0 +#define ELEMENT_TYPE_CERTIFICATE 32 + +enum +{ + POST_CONNECTED, + + N_SIGNALS +}; + +static guint signals[N_SIGNALS]; + +typedef enum _RdpPeerFlag +{ + RDP_PEER_ACTIVATED = 1 << 0, +} RdpPeerFlag; + +typedef enum _PauseKeyState +{ + PAUSE_KEY_STATE_NONE, + PAUSE_KEY_STATE_CTRL_DOWN, + PAUSE_KEY_STATE_NUMLOCK_DOWN, + PAUSE_KEY_STATE_CTRL_UP, +} PauseKeyState; + +struct _GrdSessionRdp +{ + GrdSession parent; + + GrdRdpServer *server; + GSocketConnection *connection; + freerdp_peer *peer; + GrdRdpSAMFile *sam_file; + uint32_t rdp_error_info; + GrdRdpScreenShareMode screen_share_mode; + gboolean is_view_only; + gboolean session_should_stop; + + GrdRdpSessionMetrics *session_metrics; + + GMutex rdp_flags_mutex; + RdpPeerFlag rdp_flags; + + GThread *socket_thread; + HANDLE stop_event; + + GrdRdpRenderer *renderer; + GrdRdpCursorRenderer *cursor_renderer; + + GHashTable *pressed_keys; + GHashTable *pressed_unicode_keys; + PauseKeyState pause_key_state; + + GrdRdpEventQueue *rdp_event_queue; + + GrdRdpLayoutManager *layout_manager; + + GHashTable *stream_table; + + GMutex close_session_mutex; + unsigned int close_session_idle_id; + + GMutex notify_post_connected_mutex; + unsigned int notify_post_connected_source_id; + + uint32_t next_stream_id; +}; + +G_DEFINE_TYPE (GrdSessionRdp, grd_session_rdp, GRD_TYPE_SESSION) + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (rdpCertificate, freerdp_certificate_free) +G_DEFINE_AUTOPTR_CLEANUP_FUNC (rdpRedirection, redirection_free) + +static gboolean +close_session_idle (gpointer user_data); + +GrdRdpServer * +grd_session_rdp_get_server (GrdSessionRdp *session_rdp) +{ + return session_rdp->server; +} + +GrdRdpRenderer * +grd_session_rdp_get_renderer (GrdSessionRdp *session_rdp) +{ + return session_rdp->renderer; +} + +GrdRdpCursorRenderer * +grd_session_rdp_get_cursor_renderer (GrdSessionRdp *session_rdp) +{ + return session_rdp->cursor_renderer; +} + +rdpContext * +grd_session_rdp_get_rdp_context (GrdSessionRdp *session_rdp) +{ + return session_rdp->peer->context; +} + +GrdRdpDvcGraphicsPipeline * +grd_session_rdp_get_graphics_pipeline (GrdSessionRdp *session_rdp) +{ + rdpContext *rdp_context = grd_session_rdp_get_rdp_context (session_rdp); + RdpPeerContext *rdp_peer_context = (RdpPeerContext *) rdp_context; + + return rdp_peer_context->graphics_pipeline; +} + +GrdRdpScreenShareMode +grd_session_rdp_get_screen_share_mode (GrdSessionRdp *session_rdp) +{ + return session_rdp->screen_share_mode; +} + +static gboolean +is_rdp_peer_flag_set (GrdSessionRdp *session_rdp, + RdpPeerFlag flag) +{ + gboolean state; + + g_mutex_lock (&session_rdp->rdp_flags_mutex); + state = !!(session_rdp->rdp_flags & flag); + g_mutex_unlock (&session_rdp->rdp_flags_mutex); + + return state; +} + +static void +set_rdp_peer_flag (GrdSessionRdp *session_rdp, + RdpPeerFlag flag) +{ + g_mutex_lock (&session_rdp->rdp_flags_mutex); + session_rdp->rdp_flags |= flag; + g_mutex_unlock (&session_rdp->rdp_flags_mutex); +} + +static void +unset_rdp_peer_flag (GrdSessionRdp *session_rdp, + RdpPeerFlag flag) +{ + g_mutex_lock (&session_rdp->rdp_flags_mutex); + session_rdp->rdp_flags &= ~flag; + g_mutex_unlock (&session_rdp->rdp_flags_mutex); +} + +GrdRdpSessionMetrics * +grd_session_rdp_get_session_metrics (GrdSessionRdp *session_rdp) +{ + return session_rdp->session_metrics; +} + +static uint32_t +get_next_free_stream_id (GrdSessionRdp *session_rdp) +{ + uint32_t stream_id = session_rdp->next_stream_id; + + while (g_hash_table_contains (session_rdp->stream_table, + GUINT_TO_POINTER (stream_id))) + ++stream_id; + + session_rdp->next_stream_id = stream_id + 1; + + return stream_id; +} + +uint32_t +grd_session_rdp_acquire_stream_id (GrdSessionRdp *session_rdp, + GrdRdpStreamOwner *stream_owner) +{ + uint32_t stream_id; + + stream_id = get_next_free_stream_id (session_rdp); + g_hash_table_insert (session_rdp->stream_table, + GUINT_TO_POINTER (stream_id), stream_owner); + + return stream_id; +} + +void +grd_session_rdp_release_stream_id (GrdSessionRdp *session_rdp, + uint32_t stream_id) +{ + g_hash_table_remove (session_rdp->stream_table, GUINT_TO_POINTER (stream_id)); +} + +gboolean +grd_session_rdp_is_client_mstsc (GrdSessionRdp *session_rdp) +{ + rdpContext *rdp_context = session_rdp->peer->context; + rdpSettings *rdp_settings = rdp_context->settings; + + return freerdp_settings_get_uint32 (rdp_settings, FreeRDP_OsMajorType) == + OSMAJORTYPE_WINDOWS && + freerdp_settings_get_uint32 (rdp_settings, FreeRDP_OsMinorType) == + OSMINORTYPE_WINDOWS_NT; +} + +static WCHAR * +get_utf16_string (const char *str, + size_t *size) +{ + WCHAR *utf16_string; + + *size = 0; + + utf16_string = ConvertUtf8ToWCharAlloc (str, size); + if (!utf16_string) + return NULL; + + *size = (*size + 1) * sizeof (WCHAR); + + return utf16_string; +} + +static WCHAR * +generate_encoded_redirection_guid (size_t *size) +{ + BYTE redirection_guid[16] = {}; + g_autofree char *redirection_guid_base_64 = NULL; + WCHAR *encoded_redirection_guid; + + *size = 0; + + if (winpr_RAND (redirection_guid, 16) == -1) + return NULL; + + redirection_guid_base_64 = crypto_base64_encode (redirection_guid, 16); + if (!redirection_guid_base_64) + return NULL; + + encoded_redirection_guid = get_utf16_string (redirection_guid_base_64, size); + if (!encoded_redirection_guid) + return NULL; + + return encoded_redirection_guid; +} + +static BYTE * +get_certificate_container (const char *certificate, + size_t *size) +{ + g_autofree BYTE *der_certificate = NULL; + g_autoptr (rdpCertificate) rdp_certificate = NULL; + BYTE *certificate_container = NULL; + size_t der_certificate_len = 0; + wStream *s; + + *size = 0; + + rdp_certificate = freerdp_certificate_new_from_pem (certificate); + if (!rdp_certificate) + return NULL; + + der_certificate = freerdp_certificate_get_der (rdp_certificate, + &der_certificate_len); + if (!der_certificate) + return NULL; + + s = Stream_New (NULL, 2048); + g_assert (s); + + if (!Stream_EnsureRemainingCapacity (s, 12)) + g_assert_not_reached (); + + Stream_Write_UINT32 (s, ELEMENT_TYPE_CERTIFICATE); + Stream_Write_UINT32 (s, ENCODING_TYPE_ASN1_DER); + Stream_Write_UINT32 (s, der_certificate_len); + + if (!Stream_EnsureRemainingCapacity (s, der_certificate_len)) + g_assert_not_reached (); + + Stream_Write (s, der_certificate, der_certificate_len); + + *size = Stream_GetPosition (s); + certificate_container = Stream_Buffer (s); + + Stream_Free (s, FALSE); + + return certificate_container; +} + +gboolean +grd_session_rdp_send_server_redirection (GrdSessionRdp *session_rdp, + const char *routing_token, + const char *username, + const char *password, + const char *certificate) +{ + freerdp_peer *peer = session_rdp->peer; + rdpSettings *rdp_settings = peer->context->settings; + uint32_t os_major_type = + freerdp_settings_get_uint32 (rdp_settings, FreeRDP_OsMajorType); + g_autoptr (rdpRedirection) redirection = NULL; + g_autofree BYTE *certificate_container = NULL; + g_autofree WCHAR *utf16_password = NULL; + g_autofree WCHAR *utf16_encoded_redirection_guid = NULL; + size_t size = 0; + uint32_t redirection_flags = 0; + uint32_t redirection_incorrect_flags = 0; + + g_assert (routing_token); + g_assert (username); + g_assert (password); + g_assert (certificate); + + redirection = redirection_new (); + g_assert (redirection); + + /* Load Balance Info */ + redirection_flags |= LB_LOAD_BALANCE_INFO; + redirection_set_byte_option (redirection, LB_LOAD_BALANCE_INFO, + (BYTE *) routing_token, + strlen (routing_token)); + + /* Username */ + redirection_flags |= LB_USERNAME; + redirection_set_string_option (redirection, LB_USERNAME, + username); + + /* Password */ + redirection_flags |= LB_PASSWORD; + if (os_major_type != OSMAJORTYPE_IOS && os_major_type != OSMAJORTYPE_ANDROID) + redirection_flags |= LB_PASSWORD_IS_PK_ENCRYPTED; + utf16_password = get_utf16_string (password, &size); + g_assert (utf16_password); + redirection_set_byte_option (redirection, LB_PASSWORD, + (BYTE *) utf16_password, + size); + + /* Redirection GUID */ + redirection_flags |= LB_REDIRECTION_GUID; + utf16_encoded_redirection_guid = generate_encoded_redirection_guid (&size); + g_assert (utf16_encoded_redirection_guid); + redirection_set_byte_option (redirection, LB_REDIRECTION_GUID, + (BYTE *) utf16_encoded_redirection_guid, + size); + + /* Target Certificate */ + redirection_flags |= LB_TARGET_CERTIFICATE; + certificate_container = get_certificate_container (certificate, &size); + g_assert (certificate_container); + redirection_set_byte_option (redirection, + LB_TARGET_CERTIFICATE, + certificate_container, + size); + + redirection_set_flags (redirection, redirection_flags); + + if (!redirection_settings_are_valid (redirection, &redirection_incorrect_flags)) + { + g_warning ("[RDP] Something went wrong sending Server Redirection PDU. " + "Incorrect flag/s: 0x%08x", redirection_incorrect_flags); + return FALSE; + } + + g_message ("[RDP] Sending server redirection"); + if (!peer->SendServerRedirection (peer, redirection)) + { + g_warning ("[RDP] Error sending server Redirection"); + return FALSE; + } + + return TRUE; +} + +static void +maybe_queue_close_session_idle (GrdSessionRdp *session_rdp) +{ + g_mutex_lock (&session_rdp->close_session_mutex); + if (session_rdp->close_session_idle_id) + { + g_mutex_unlock (&session_rdp->close_session_mutex); + return; + } + + session_rdp->close_session_idle_id = + g_idle_add (close_session_idle, session_rdp); + g_mutex_unlock (&session_rdp->close_session_mutex); + + session_rdp->session_should_stop = TRUE; + SetEvent (session_rdp->stop_event); +} + +void +grd_session_rdp_notify_error (GrdSessionRdp *session_rdp, + GrdSessionRdpError error_info) +{ + switch (error_info) + { + case GRD_SESSION_RDP_ERROR_NONE: + g_assert_not_reached (); + break; + case GRD_SESSION_RDP_ERROR_BAD_CAPS: + session_rdp->rdp_error_info = ERRINFO_BAD_CAPABILITIES; + break; + case GRD_SESSION_RDP_ERROR_BAD_MONITOR_DATA: + session_rdp->rdp_error_info = ERRINFO_BAD_MONITOR_DATA; + break; + case GRD_SESSION_RDP_ERROR_CLOSE_STACK_ON_DRIVER_FAILURE: + session_rdp->rdp_error_info = ERRINFO_CLOSE_STACK_ON_DRIVER_FAILURE; + break; + case GRD_SESSION_RDP_ERROR_GRAPHICS_SUBSYSTEM_FAILED: + session_rdp->rdp_error_info = ERRINFO_GRAPHICS_SUBSYSTEM_FAILED; + break; + case GRD_SESSION_RDP_ERROR_SERVER_REDIRECTION: + session_rdp->rdp_error_info = ERRINFO_CB_CONNECTION_CANCELLED; + break; + } + + unset_rdp_peer_flag (session_rdp, RDP_PEER_ACTIVATED); + maybe_queue_close_session_idle (session_rdp); +} + +void +grd_session_rdp_tear_down_channel (GrdSessionRdp *session_rdp, + GrdRdpChannel channel) +{ + freerdp_peer *peer = session_rdp->peer; + RdpPeerContext *rdp_peer_context = (RdpPeerContext *) peer->context; + + g_mutex_lock (&rdp_peer_context->channel_mutex); + switch (channel) + { + case GRD_RDP_CHANNEL_NONE: + g_assert_not_reached (); + break; + case GRD_RDP_CHANNEL_AUDIO_INPUT: + g_clear_object (&rdp_peer_context->audio_input); + break; + case GRD_RDP_CHANNEL_AUDIO_PLAYBACK: + g_clear_object (&rdp_peer_context->audio_playback); + break; + case GRD_RDP_CHANNEL_CAMERA: + g_assert_not_reached (); + break; + case GRD_RDP_CHANNEL_CAMERA_ENUMERATOR: + g_clear_object (&rdp_peer_context->camera_enumerator); + break; + case GRD_RDP_CHANNEL_DISPLAY_CONTROL: + g_clear_object (&rdp_peer_context->display_control); + break; + case GRD_RDP_CHANNEL_GRAPHICS_PIPELINE: + g_assert_not_reached (); + break; + case GRD_RDP_CHANNEL_INPUT: + g_clear_object (&rdp_peer_context->input); + break; + case GRD_RDP_CHANNEL_TELEMETRY: + g_clear_object (&rdp_peer_context->telemetry); + break; + } + g_mutex_unlock (&rdp_peer_context->channel_mutex); +} + +static void +handle_client_gone (GrdSessionRdp *session_rdp) +{ + g_debug ("RDP client gone"); + + unset_rdp_peer_flag (session_rdp, RDP_PEER_ACTIVATED); + maybe_queue_close_session_idle (session_rdp); +} + +static gboolean +notify_keycode_released (gpointer key, + gpointer value, + gpointer user_data) +{ + GrdSessionRdp *session_rdp = user_data; + GrdRdpEventQueue *rdp_event_queue = session_rdp->rdp_event_queue; + uint32_t keycode = GPOINTER_TO_UINT (key); + + grd_rdp_event_queue_add_input_event_keyboard_keycode (rdp_event_queue, + keycode, + GRD_KEY_STATE_RELEASED); + + return TRUE; +} + +static gboolean +notify_keysym_released (gpointer key, + gpointer value, + gpointer user_data) +{ + GrdSessionRdp *session_rdp = user_data; + GrdRdpEventQueue *rdp_event_queue = session_rdp->rdp_event_queue; + xkb_keysym_t keysym = GPOINTER_TO_UINT (key); + + grd_rdp_event_queue_add_input_event_keyboard_keysym (rdp_event_queue, + keysym, + GRD_KEY_STATE_RELEASED); + + return TRUE; +} + +static BOOL +rdp_input_synchronize_event (rdpInput *rdp_input, + uint32_t flags) +{ + RdpPeerContext *rdp_peer_context = (RdpPeerContext *) rdp_input->context; + GrdSessionRdp *session_rdp = rdp_peer_context->session_rdp; + GrdRdpEventQueue *rdp_event_queue = session_rdp->rdp_event_queue; + + g_debug ("[RDP] Received Synchronize event with flags 0x%08X", flags); + + if (!is_rdp_peer_flag_set (session_rdp, RDP_PEER_ACTIVATED) || + session_rdp->is_view_only) + return TRUE; + + g_hash_table_foreach_remove (session_rdp->pressed_keys, + notify_keycode_released, + session_rdp); + + g_hash_table_foreach_remove (session_rdp->pressed_unicode_keys, + notify_keysym_released, + session_rdp); + + grd_rdp_event_queue_add_synchronization_event (rdp_event_queue, + !!(flags & KBD_SYNC_CAPS_LOCK), + !!(flags & KBD_SYNC_NUM_LOCK)); + + return TRUE; +} + +static BOOL +rdp_input_mouse_event (rdpInput *rdp_input, + uint16_t flags, + uint16_t x, + uint16_t y) +{ + RdpPeerContext *rdp_peer_context = (RdpPeerContext *) rdp_input->context; + GrdSessionRdp *session_rdp = rdp_peer_context->session_rdp; + GrdRdpEventQueue *rdp_event_queue = session_rdp->rdp_event_queue; + GrdEventMotionAbs motion_abs = {}; + GrdStream *stream = NULL; + GrdButtonState button_state; + int32_t button = 0; + uint16_t axis_value; + double axis_step; + + if (!is_rdp_peer_flag_set (session_rdp, RDP_PEER_ACTIVATED) || + session_rdp->is_view_only) + return TRUE; + + if (!(flags & PTR_FLAGS_WHEEL) && !(flags & PTR_FLAGS_HWHEEL) && + grd_rdp_layout_manager_transform_position (session_rdp->layout_manager, + x, y, + &stream, &motion_abs)) + { + grd_rdp_event_queue_add_input_event_pointer_motion_abs (rdp_event_queue, + stream, + &motion_abs); + } + + button_state = flags & PTR_FLAGS_DOWN ? GRD_BUTTON_STATE_PRESSED + : GRD_BUTTON_STATE_RELEASED; + + if (flags & PTR_FLAGS_BUTTON1) + button = BTN_LEFT; + else if (flags & PTR_FLAGS_BUTTON2) + button = BTN_RIGHT; + else if (flags & PTR_FLAGS_BUTTON3) + button = BTN_MIDDLE; + + if (button) + { + grd_rdp_event_queue_add_input_event_pointer_button (rdp_event_queue, + button, button_state); + } + + if (!(flags & PTR_FLAGS_WHEEL) && !(flags & PTR_FLAGS_HWHEEL)) + return TRUE; + + axis_value = flags & WheelRotationMask; + if (axis_value & PTR_FLAGS_WHEEL_NEGATIVE) + { + axis_value = ~axis_value & WheelRotationMask; + ++axis_value; + } + + axis_step = -axis_value / 120.0; + if (flags & PTR_FLAGS_WHEEL_NEGATIVE) + axis_step = -axis_step; + + if (flags & PTR_FLAGS_WHEEL) + { + grd_rdp_event_queue_add_input_event_pointer_axis ( + rdp_event_queue, 0, axis_step * DISCRETE_SCROLL_STEP, + GRD_POINTER_AXIS_FLAGS_SOURCE_WHEEL); + } + if (flags & PTR_FLAGS_HWHEEL) + { + grd_rdp_event_queue_add_input_event_pointer_axis ( + rdp_event_queue, -axis_step * DISCRETE_SCROLL_STEP, 0, + GRD_POINTER_AXIS_FLAGS_SOURCE_WHEEL); + } + + return TRUE; +} + +static BOOL +rdp_input_extended_mouse_event (rdpInput *rdp_input, + uint16_t flags, + uint16_t x, + uint16_t y) +{ + RdpPeerContext *rdp_peer_context = (RdpPeerContext *) rdp_input->context; + GrdSessionRdp *session_rdp = rdp_peer_context->session_rdp; + GrdRdpEventQueue *rdp_event_queue = session_rdp->rdp_event_queue; + GrdButtonState button_state; + int32_t button = 0; + + if (!is_rdp_peer_flag_set (session_rdp, RDP_PEER_ACTIVATED) || + session_rdp->is_view_only) + return TRUE; + + rdp_input_mouse_event (rdp_input, PTR_FLAGS_MOVE, x, y); + + button_state = flags & PTR_XFLAGS_DOWN ? GRD_BUTTON_STATE_PRESSED + : GRD_BUTTON_STATE_RELEASED; + + if (flags & PTR_XFLAGS_BUTTON1) + button = BTN_SIDE; + else if (flags & PTR_XFLAGS_BUTTON2) + button = BTN_EXTRA; + + if (button) + { + grd_rdp_event_queue_add_input_event_pointer_button (rdp_event_queue, + button, button_state); + } + + return TRUE; +} + +static BOOL +rdp_input_rel_mouse_event (rdpInput *rdp_input, + uint16_t flags, + int16_t dx, + int16_t dy) +{ + RdpPeerContext *rdp_peer_context = (RdpPeerContext *) rdp_input->context; + GrdSessionRdp *session_rdp = rdp_peer_context->session_rdp; + GrdRdpEventQueue *rdp_event_queue = session_rdp->rdp_event_queue; + GrdButtonState button_state; + int32_t button = 0; + + grd_rdp_event_queue_add_input_event_pointer_motion (rdp_event_queue, dx, dy); + + button_state = flags & PTR_FLAGS_DOWN ? GRD_BUTTON_STATE_PRESSED + : GRD_BUTTON_STATE_RELEASED; + + if (flags & PTR_FLAGS_BUTTON1) + button = BTN_LEFT; + else if (flags & PTR_FLAGS_BUTTON2) + button = BTN_RIGHT; + else if (flags & PTR_FLAGS_BUTTON3) + button = BTN_MIDDLE; + else if (flags & PTR_XFLAGS_BUTTON1) + button = BTN_SIDE; + else if (flags & PTR_XFLAGS_BUTTON2) + button = BTN_EXTRA; + + if (button) + { + grd_rdp_event_queue_add_input_event_pointer_button (rdp_event_queue, + button, button_state); + } + + return TRUE; +} + +static gboolean +is_pause_key_sequence (GrdSessionRdp *session_rdp, + uint16_t vkcode, + uint16_t flags) +{ + GrdRdpEventQueue *rdp_event_queue = session_rdp->rdp_event_queue; + + switch (session_rdp->pause_key_state) + { + case PAUSE_KEY_STATE_NONE: + if (vkcode == VK_LCONTROL && + !(flags & KBD_FLAGS_RELEASE) && + flags & KBD_FLAGS_EXTENDED1) + { + session_rdp->pause_key_state = PAUSE_KEY_STATE_CTRL_DOWN; + return TRUE; + } + return FALSE; + case PAUSE_KEY_STATE_CTRL_DOWN: + if (vkcode == VK_NUMLOCK && + !(flags & KBD_FLAGS_RELEASE)) + { + session_rdp->pause_key_state = PAUSE_KEY_STATE_NUMLOCK_DOWN; + return TRUE; + } + break; + case PAUSE_KEY_STATE_NUMLOCK_DOWN: + if (vkcode == VK_LCONTROL && + flags & KBD_FLAGS_RELEASE && + flags & KBD_FLAGS_EXTENDED1) + { + session_rdp->pause_key_state = PAUSE_KEY_STATE_CTRL_UP; + return TRUE; + } + break; + case PAUSE_KEY_STATE_CTRL_UP: + if (vkcode == VK_NUMLOCK && + flags & KBD_FLAGS_RELEASE) + { + session_rdp->pause_key_state = PAUSE_KEY_STATE_NONE; + grd_rdp_event_queue_add_input_event_keyboard_keysym ( + rdp_event_queue, XKB_KEY_Pause, GRD_KEY_STATE_PRESSED); + grd_rdp_event_queue_add_input_event_keyboard_keysym ( + rdp_event_queue, XKB_KEY_Pause, GRD_KEY_STATE_RELEASED); + + return TRUE; + } + break; + } + + g_warning ("Received invalid pause key sequence"); + session_rdp->pause_key_state = PAUSE_KEY_STATE_NONE; + + return FALSE; +} + +static BOOL +rdp_input_keyboard_event (rdpInput *rdp_input, + uint16_t flags, + uint8_t code) +{ + RdpPeerContext *rdp_peer_context = (RdpPeerContext *) rdp_input->context; + GrdSessionRdp *session_rdp = rdp_peer_context->session_rdp; + GrdRdpEventQueue *rdp_event_queue = session_rdp->rdp_event_queue; + rdpSettings *rdp_settings = rdp_input->context->settings; + uint32_t keyboard_type = + freerdp_settings_get_uint32 (rdp_settings, FreeRDP_KeyboardType); + GrdKeyState key_state; + uint16_t scancode; + uint16_t fullcode; + uint16_t vkcode; + uint16_t keycode; + + if (!is_rdp_peer_flag_set (session_rdp, RDP_PEER_ACTIVATED) || + session_rdp->is_view_only) + return TRUE; + + scancode = code; + fullcode = flags & KBD_FLAGS_EXTENDED ? scancode | KBDEXT : scancode; + vkcode = GetVirtualKeyCodeFromVirtualScanCode (fullcode, keyboard_type); + vkcode = flags & KBD_FLAGS_EXTENDED ? vkcode | KBDEXT : vkcode; + keycode = GetKeycodeFromVirtualKeyCode (vkcode, WINPR_KEYCODE_TYPE_EVDEV); + + key_state = flags & KBD_FLAGS_RELEASE ? GRD_KEY_STATE_RELEASED + : GRD_KEY_STATE_PRESSED; + + if (is_pause_key_sequence (session_rdp, vkcode, flags)) + return TRUE; + + if (key_state == GRD_KEY_STATE_PRESSED) + { + if (!g_hash_table_add (session_rdp->pressed_keys, + GUINT_TO_POINTER (keycode))) + return TRUE; + } + else + { + if (!g_hash_table_remove (session_rdp->pressed_keys, + GUINT_TO_POINTER (keycode))) + return TRUE; + } + + grd_rdp_event_queue_add_input_event_keyboard_keycode (rdp_event_queue, + keycode, key_state); + + return TRUE; +} + +static BOOL +rdp_input_unicode_keyboard_event (rdpInput *rdp_input, + uint16_t flags, + uint16_t code_utf16) +{ + RdpPeerContext *rdp_peer_context = (RdpPeerContext *) rdp_input->context; + GrdSessionRdp *session_rdp = rdp_peer_context->session_rdp; + GrdRdpEventQueue *rdp_event_queue = session_rdp->rdp_event_queue; + uint32_t *code_utf32; + xkb_keysym_t keysym; + GrdKeyState key_state; + + if (!is_rdp_peer_flag_set (session_rdp, RDP_PEER_ACTIVATED) || + session_rdp->is_view_only) + return TRUE; + + code_utf32 = g_utf16_to_ucs4 (&code_utf16, 1, NULL, NULL, NULL); + if (!code_utf32) + return TRUE; + + keysym = xkb_utf32_to_keysym (*code_utf32); + g_free (code_utf32); + + key_state = flags & KBD_FLAGS_RELEASE ? GRD_KEY_STATE_RELEASED + : GRD_KEY_STATE_PRESSED; + + if (key_state == GRD_KEY_STATE_PRESSED) + { + if (!g_hash_table_add (session_rdp->pressed_unicode_keys, + GUINT_TO_POINTER (keysym))) + return TRUE; + } + else + { + if (!g_hash_table_remove (session_rdp->pressed_unicode_keys, + GUINT_TO_POINTER (keysym))) + return TRUE; + } + + grd_rdp_event_queue_add_input_event_keyboard_keysym (rdp_event_queue, + keysym, key_state); + + return TRUE; +} + +static BOOL +rdp_suppress_output (rdpContext *rdp_context, + uint8_t allow, + const RECTANGLE_16 *area) +{ + RdpPeerContext *rdp_peer_context = (RdpPeerContext *) rdp_context; + GrdSessionRdp *session_rdp = rdp_peer_context->session_rdp; + rdpSettings *rdp_settings = rdp_context->settings; + + if (!is_rdp_peer_flag_set (session_rdp, RDP_PEER_ACTIVATED)) + return TRUE; + + grd_rdp_renderer_update_output_suppression_state (session_rdp->renderer, + !allow); + + if (freerdp_settings_get_bool (rdp_settings, FreeRDP_SupportGraphicsPipeline) && + rdp_peer_context->network_autodetection) + { + if (allow) + { + grd_rdp_network_autodetection_ensure_rtt_consumer ( + rdp_peer_context->network_autodetection, + GRD_RDP_NW_AUTODETECT_RTT_CONSUMER_RDPGFX); + } + else + { + grd_rdp_network_autodetection_remove_rtt_consumer ( + rdp_peer_context->network_autodetection, + GRD_RDP_NW_AUTODETECT_RTT_CONSUMER_RDPGFX); + } + } + + return TRUE; +} + +static FREERDP_AUTODETECT_STATE +rdp_autodetect_on_connect_time_autodetect_begin (rdpAutoDetect *rdp_autodetect) +{ + rdpContext *rdp_context = rdp_autodetect->context; + RdpPeerContext *rdp_peer_context = (RdpPeerContext *) rdp_context; + rdpSettings *rdp_settings = rdp_context->settings; + GrdRdpNetworkAutodetection *network_autodetection; + + g_assert (freerdp_settings_get_bool (rdp_settings, FreeRDP_NetworkAutoDetect)); + g_assert (!rdp_peer_context->network_autodetection); + + network_autodetection = grd_rdp_network_autodetection_new (rdp_context); + rdp_peer_context->network_autodetection = network_autodetection; + + grd_rdp_network_autodetection_start_connect_time_autodetection (network_autodetection); + + return FREERDP_AUTODETECT_STATE_REQUEST; +} + +static gboolean +is_using_remote_login (GrdSessionRdp *session_rdp) +{ + GrdContext *context = grd_session_get_context (GRD_SESSION (session_rdp)); + + switch (grd_context_get_runtime_mode (context)) + { + case GRD_RUNTIME_MODE_SCREEN_SHARE: + case GRD_RUNTIME_MODE_HEADLESS: + return FALSE; + case GRD_RUNTIME_MODE_SYSTEM: + case GRD_RUNTIME_MODE_HANDOVER: + return TRUE; + } + + g_assert_not_reached (); +} + +static GrdRdpAuthMethods +get_effective_auth_method (rdpContext *rdp_context) +{ + GrdRdpAuthMethods auth_method = -1; + SECURITY_STATUS status; + SecPkgContext_PackageInfo package_info = {}; + + status = freerdp_nla_QueryContextAttributes (rdp_context, + SECPKG_ATTR_PACKAGE_INFO, + &package_info); + if (status != SEC_E_OK) + { + g_warning ("Failed to query NLA context package info: %s", + GetSecurityStatusString (status)); + return FALSE; + } + + if (g_strcmp0 (package_info.PackageInfo->Name, KERBEROS_SSP_NAME) == 0) + auth_method = GRD_RDP_AUTH_METHOD_KERBEROS; + else if (g_strcmp0 (package_info.PackageInfo->Name, NTLM_SSP_NAME) == 0) + auth_method = GRD_RDP_AUTH_METHOD_CREDENTIALS; + else + g_warning ("Unknown NLA context package '%s'", package_info.PackageInfo->Name); + + freerdp_nla_FreeContextBuffer (rdp_context, package_info.PackageInfo); + return auth_method; +} + +static gboolean +is_auth_identity_current_user (const SecPkgContext_AuthIdentity *auth_identity) +{ + krb5_error_code kret; + g_autofree char *principal_string = NULL; + krb5_context krb5_context = NULL; + krb5_principal principal = NULL; + char local_name[256 + 1]; + g_autofree struct passwd *pwd = NULL; + g_autoptr (GError) error = NULL; + uid_t current_uid; + + kret = krb5_init_context (&krb5_context); + if (kret) + { + g_critical ("Failed to initialize krb5 context"); + return FALSE; + } + + principal_string = g_strdup_printf ("%s@%s", + auth_identity->User, + auth_identity->Domain); + + kret = krb5_parse_name (krb5_context, principal_string, &principal); + if (kret) + { + g_critical ("Failed to parse principal string %s", principal_string); + goto err; + } + + kret = krb5_aname_to_localname (krb5_context, principal, + sizeof (local_name), local_name); + if (kret) + { + g_critical ("Failed to map principal '%s' name to local name", + principal_string); + return FALSE; + goto err; + } + + pwd = g_unix_get_passwd_entry (local_name, &error); + if (error) + { + g_critical ("Failed to get passwd field for %s: %s", + local_name, error->message); + goto err; + } + + if (!pwd) + { + g_warning ("Tried to authenticate with invalid user %s", local_name); + goto err; + } + + krb5_free_principal (krb5_context, principal); + krb5_free_context (krb5_context); + + current_uid = getuid (); + if (pwd->pw_uid == current_uid) + { + g_debug ("[RDP] Kerberos principal %s match current user %s (%u), " + "accepting.", + principal_string, + local_name, + current_uid); + return TRUE; + } + else + { + return FALSE; + } + +err: + if (principal) + krb5_free_principal (krb5_context, principal); + if (krb5_context) + krb5_free_context (krb5_context); + return FALSE; +} + +static BOOL +rdp_peer_logon (freerdp_peer *peer, + const SEC_WINNT_AUTH_IDENTITY *identity, + BOOL automatic) +{ + rdpContext *rdp_context = peer->context; + RdpPeerContext *rdp_peer_context = (RdpPeerContext *) rdp_context; + GrdSessionRdp *session_rdp = rdp_peer_context->session_rdp; + GrdContext *context = grd_session_get_context (GRD_SESSION (session_rdp)); + GrdRdpAuthMethods auth_method; + SECURITY_STATUS status; + SecPkgContext_AuthIdentity auth_identity = {}; + + if (is_using_remote_login (session_rdp)) + return TRUE; + + auth_method = get_effective_auth_method (rdp_context); + + if (auth_method == -1) + return FALSE; + + switch (auth_method) + { + case GRD_RDP_AUTH_METHOD_CREDENTIALS: + g_debug ("[RDP] Authenticated using NTLM, " + "not applying any additional policy."); + return TRUE; + case GRD_RDP_AUTH_METHOD_KERBEROS: + break; + } + + status = freerdp_nla_QueryContextAttributes (rdp_context, + SECPKG_ATTR_AUTH_IDENTITY, + &auth_identity); + + if (status != SEC_E_OK) + { + g_warning ("Failed to authenticate Kerberos principal: %s", + GetSecurityStatusString (status)); + return FALSE; + } + + switch (grd_context_get_runtime_mode (context)) + { + case GRD_RUNTIME_MODE_HEADLESS: + case GRD_RUNTIME_MODE_SCREEN_SHARE: + if (is_auth_identity_current_user (&auth_identity)) + { + return TRUE; + } + else + { + g_debug ("[RDP] Kerberos principal doesn't match current user, " + "disconnecting"); + return FALSE; + } + case GRD_RUNTIME_MODE_SYSTEM: + case GRD_RUNTIME_MODE_HANDOVER: + g_assert_not_reached (); + } + + g_assert_not_reached (); +} + +static uint32_t +get_max_monitor_count (GrdSessionRdp *session_rdp) +{ + GrdContext *context = grd_session_get_context (GRD_SESSION (session_rdp)); + + switch (grd_context_get_runtime_mode (context)) + { + case GRD_RUNTIME_MODE_HEADLESS: + case GRD_RUNTIME_MODE_SYSTEM: + case GRD_RUNTIME_MODE_HANDOVER: + return MAX_MONITOR_COUNT_HEADLESS; + case GRD_RUNTIME_MODE_SCREEN_SHARE: + return MAX_MONITOR_COUNT_SCREEN_SHARE; + } + + g_assert_not_reached (); +} + +static BOOL +rdp_peer_capabilities (freerdp_peer *peer) +{ + RdpPeerContext *rdp_peer_context = (RdpPeerContext *) peer->context; + GrdSessionRdp *session_rdp = rdp_peer_context->session_rdp; + rdpSettings *rdp_settings = peer->context->settings; + HANDLE vcm = rdp_peer_context->vcm; + GrdRdpMonitorConfig *monitor_config; + g_autoptr (GError) error = NULL; + + if (!freerdp_settings_get_bool (rdp_settings, FreeRDP_SupportGraphicsPipeline)) + { + g_warning ("[RDP] Client did not advertise support for the Graphics " + "Pipeline, closing connection"); + return FALSE; + } + + if (freerdp_settings_get_bool (rdp_settings, FreeRDP_SupportGraphicsPipeline) || + freerdp_settings_get_bool (rdp_settings, FreeRDP_RemoteFxCodec) || + freerdp_settings_get_bool (rdp_settings, FreeRDP_RemoteFxImageCodec) || + freerdp_settings_get_bool (rdp_settings, FreeRDP_NSCodec)) + { + uint16_t supported_color_depths = + freerdp_settings_get_uint16 (rdp_settings, FreeRDP_SupportedColorDepths); + + if (!(supported_color_depths & RNS_UD_32BPP_SUPPORT)) + { + g_warning ("[RDP] Protocol violation: Client advertised support for " + "codecs or the Graphics Pipeline, but does not support " + "32-bit colour depth"); + return FALSE; + } + if (freerdp_settings_get_uint32 (rdp_settings, FreeRDP_ColorDepth) != 32) + { + g_debug ("[RDP] Client prefers colour depth %u, invalidated by support " + "of codecs or graphics pipeline", + freerdp_settings_get_uint32 (rdp_settings, FreeRDP_ColorDepth)); + freerdp_settings_set_uint32 (rdp_settings, FreeRDP_ColorDepth, 32); + } + } + + if (!freerdp_settings_get_bool (rdp_settings, FreeRDP_DesktopResize)) + { + g_warning ("Client doesn't support desktop resizing, closing connection"); + return FALSE; + } + + if (!WTSVirtualChannelManagerIsChannelJoined (vcm, DRDYNVC_SVC_CHANNEL_NAME)) + { + g_warning ("[RDP] Client doesn't support the DRDYNVC SVC, " + "closing connection"); + return FALSE; + } + + if (session_rdp->screen_share_mode == GRD_RDP_SCREEN_SHARE_MODE_EXTEND) + { + uint32_t max_monitor_count; + + max_monitor_count = get_max_monitor_count (session_rdp); + monitor_config = + grd_rdp_monitor_config_new_from_client_data (rdp_settings, + max_monitor_count, &error); + } + else + { + g_autoptr (GStrvBuilder) connector_builder = NULL; + char **connectors; + + monitor_config = g_new0 (GrdRdpMonitorConfig, 1); + + connector_builder = g_strv_builder_new (); + g_strv_builder_add (connector_builder, ""); + connectors = g_strv_builder_end (connector_builder); + + monitor_config->connectors = connectors; + monitor_config->monitor_count = 1; + } + if (!monitor_config) + { + g_warning ("[RDP] Received invalid monitor layout from client: %s, " + "closing connection", error->message); + return FALSE; + } + + if (session_rdp->screen_share_mode == GRD_RDP_SCREEN_SHARE_MODE_EXTEND) + { + freerdp_settings_set_uint32 (rdp_settings, FreeRDP_DesktopWidth, + monitor_config->desktop_width); + freerdp_settings_set_uint32 (rdp_settings, FreeRDP_DesktopHeight, + monitor_config->desktop_height); + } + + if (monitor_config->is_virtual) + g_debug ("[RDP] Remote Desktop session will use virtual monitors"); + else + g_debug ("[RDP] Remote Desktop session will mirror the primary monitor"); + + grd_rdp_layout_manager_submit_new_monitor_config (session_rdp->layout_manager, + monitor_config); + + return TRUE; +} + +static void +notify_post_connected (gpointer user_data) +{ + GrdSessionRdp *session_rdp = user_data; + GrdContext *grd_context = grd_session_get_context (GRD_SESSION (session_rdp)); + + g_mutex_lock (&session_rdp->notify_post_connected_mutex); + session_rdp->notify_post_connected_source_id = 0; + g_mutex_unlock (&session_rdp->notify_post_connected_mutex); + + grd_rdp_session_metrics_notify_phase_completion (session_rdp->session_metrics, + GRD_RDP_PHASE_POST_CONNECT); + if (grd_context_get_runtime_mode (grd_context) != GRD_RUNTIME_MODE_SYSTEM) + grd_session_start (GRD_SESSION (session_rdp)); + + g_signal_emit (session_rdp, signals[POST_CONNECTED], 0); +} + +static BOOL +rdp_peer_post_connect (freerdp_peer *peer) +{ + RdpPeerContext *rdp_peer_context = (RdpPeerContext *) peer->context; + GrdSessionRdp *session_rdp = rdp_peer_context->session_rdp; + rdpSettings *rdp_settings = peer->context->settings; + uint32_t rdp_version = + freerdp_settings_get_uint32 (rdp_settings, FreeRDP_RdpVersion); + uint32_t keyboard_type = + freerdp_settings_get_uint32 (rdp_settings, FreeRDP_KeyboardType); + uint32_t os_major_type = + freerdp_settings_get_uint32 (rdp_settings, FreeRDP_OsMajorType); + + if (freerdp_settings_get_uint32 (rdp_settings, FreeRDP_PointerCacheSize) <= 0) + { + g_warning ("Client doesn't have a pointer cache, closing connection"); + return FALSE; + } + if (!freerdp_settings_get_bool (rdp_settings, FreeRDP_FastPathOutput)) + { + g_warning ("Client does not support fastpath output, closing connection"); + return FALSE; + } + + g_debug ("New RDP client: [OS major type, OS minor type]: [%s, %s]", + freerdp_peer_os_major_type_string (peer), + freerdp_peer_os_minor_type_string (peer)); + g_debug ("[RDP] Maximum common RDP version 0x%08X", rdp_version); + g_debug ("[RDP] Client uses keyboard type %u", keyboard_type); + + g_debug ("[RDP] Virtual Channels: compression flags: %u, " + "compression level: %u, chunk size: %u", + freerdp_settings_get_uint32 (rdp_settings, FreeRDP_VCFlags), + freerdp_settings_get_uint32 (rdp_settings, FreeRDP_CompressionLevel), + freerdp_settings_get_uint32 (rdp_settings, FreeRDP_VCChunkSize)); + + if (freerdp_settings_get_bool (rdp_settings, FreeRDP_SupportGraphicsPipeline) && + !freerdp_settings_get_bool (rdp_settings, FreeRDP_NetworkAutoDetect)) + { + g_warning ("Client does not support autodetecting network characteristics " + "(RTT detection, Bandwidth measurement). " + "High latency connections will suffer!"); + } + if (freerdp_settings_get_bool (rdp_settings, FreeRDP_AudioPlayback) && + !freerdp_settings_get_bool (rdp_settings, FreeRDP_NetworkAutoDetect)) + { + g_warning ("[RDP] Client does not support autodetecting network " + "characteristics. Disabling audio output redirection"); + freerdp_settings_set_bool (rdp_settings, FreeRDP_AudioPlayback, FALSE); + } + if (freerdp_settings_get_bool (rdp_settings, FreeRDP_AudioPlayback) && + (os_major_type == OSMAJORTYPE_IOS || + os_major_type == OSMAJORTYPE_ANDROID)) + { + g_warning ("[RDP] Client cannot handle graphics and audio " + "simultaneously. Disabling audio output redirection"); + freerdp_settings_set_bool (rdp_settings, FreeRDP_AudioPlayback, FALSE); + } + + if (freerdp_settings_get_bool (rdp_settings, FreeRDP_SupportGraphicsPipeline)) + grd_rdp_renderer_notify_graphics_pipeline_reset (session_rdp->renderer); + + g_clear_pointer (&session_rdp->sam_file, grd_rdp_sam_free_sam_file); + + if (freerdp_settings_get_bool (rdp_settings, FreeRDP_SupportGraphicsPipeline) && + rdp_peer_context->network_autodetection) + { + grd_rdp_network_autodetection_ensure_rtt_consumer ( + rdp_peer_context->network_autodetection, + GRD_RDP_NW_AUTODETECT_RTT_CONSUMER_RDPGFX); + } + + set_rdp_peer_flag (session_rdp, RDP_PEER_ACTIVATED); + + g_mutex_lock (&session_rdp->notify_post_connected_mutex); + session_rdp->notify_post_connected_source_id = + g_idle_add_once (notify_post_connected, session_rdp); + g_mutex_unlock (&session_rdp->notify_post_connected_mutex); + + return TRUE; +} + +static BOOL +rdp_peer_activate (freerdp_peer *peer) +{ + g_debug ("Activating client"); + + return TRUE; +} + +static void +rdp_peer_context_free (freerdp_peer *peer, + RdpPeerContext *rdp_peer_context) +{ + if (!rdp_peer_context) + return; + + g_clear_object (&rdp_peer_context->dvc_handler); + + if (rdp_peer_context->vcm != INVALID_HANDLE_VALUE) + g_clear_pointer (&rdp_peer_context->vcm, WTSCloseServer); + + if (rdp_peer_context->encode_stream) + { + Stream_Free (rdp_peer_context->encode_stream, TRUE); + rdp_peer_context->encode_stream = NULL; + } + + g_clear_pointer (&rdp_peer_context->rfx_context, rfx_context_free); + + g_mutex_clear (&rdp_peer_context->channel_mutex); +} + +static BOOL +rdp_peer_context_new (freerdp_peer *peer, + RdpPeerContext *rdp_peer_context) +{ + g_mutex_init (&rdp_peer_context->channel_mutex); + + rdp_peer_context->rfx_context = rfx_context_new (TRUE); + if (!rdp_peer_context->rfx_context) + { + g_warning ("[RDP] Failed to create RFX context"); + return FALSE; + } +#if G_BYTE_ORDER == G_LITTLE_ENDIAN + rfx_context_set_pixel_format (rdp_peer_context->rfx_context, + PIXEL_FORMAT_BGRX32); +#else + rfx_context_set_pixel_format (rdp_peer_context->rfx_context, + PIXEL_FORMAT_XRGB32); +#endif + + rdp_peer_context->encode_stream = Stream_New (NULL, 64 * 64 * 4); + if (!rdp_peer_context->encode_stream) + { + g_warning ("[RDP] Failed to create encode stream"); + return FALSE; + } + + rdp_peer_context->vcm = WTSOpenServerA ((LPSTR) peer->context); + if (!rdp_peer_context->vcm || rdp_peer_context->vcm == INVALID_HANDLE_VALUE) + { + g_warning ("[RDP] Failed to create virtual channel manager"); + return FALSE; + } + + rdp_peer_context->dvc_handler = + grd_rdp_dvc_handler_new (rdp_peer_context->vcm); + + return TRUE; +} + +static gboolean +init_rdp_session (GrdSessionRdp *session_rdp, + const char *username, + const char *password, + GError **error) +{ + GrdContext *context = grd_session_get_context (GRD_SESSION (session_rdp)); + GrdSettings *settings = grd_context_get_settings (context); + GSocket *socket = g_socket_connection_get_socket (session_rdp->connection); + GrdRdpAuthMethods auth_methods = 0; + g_autofree char *server_cert = NULL; + g_autofree char *server_key = NULL; + gboolean use_client_configs; + freerdp_peer *peer; + rdpInput *rdp_input; + rdpAutoDetect *rdp_autodetect; + RdpPeerContext *rdp_peer_context; + rdpSettings *rdp_settings; + rdpCertificate *rdp_certificate = NULL; + rdpPrivateKey *rdp_private_key = NULL; + + use_client_configs = session_rdp->screen_share_mode == + GRD_RDP_SCREEN_SHARE_MODE_EXTEND; + + g_debug ("Initialize RDP session"); + + peer = freerdp_peer_new (g_socket_get_fd (socket)); + if (!peer) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create peer"); + return FALSE; + } + + peer->ContextSize = sizeof (RdpPeerContext); + peer->ContextFree = (psPeerContextFree) rdp_peer_context_free; + peer->ContextNew = (psPeerContextNew) rdp_peer_context_new; + + if (!freerdp_peer_context_new (peer)) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create peer context"); + freerdp_peer_free (peer); + return FALSE; + } + session_rdp->peer = peer; + + rdp_peer_context = (RdpPeerContext *) peer->context; + rdp_peer_context->session_rdp = session_rdp; + + rdp_settings = peer->context->settings; + g_object_get (G_OBJECT (settings), + "rdp-auth-methods", &auth_methods, + NULL); + g_assert (auth_methods); + + if (auth_methods & GRD_RDP_AUTH_METHOD_CREDENTIALS) + { + session_rdp->sam_file = grd_rdp_sam_create_sam_file (username, password); + if (!session_rdp->sam_file) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create SAM database"); + return FALSE; + } + freerdp_settings_set_string (rdp_settings, FreeRDP_NtlmSamFile, + session_rdp->sam_file->filename); + } + + if (auth_methods & GRD_RDP_AUTH_METHOD_KERBEROS && + !is_using_remote_login (session_rdp)) + { + g_autofree char *kerberos_keytab = NULL; + + g_object_get (G_OBJECT (settings), + "rdp-kerberos-keytab", &kerberos_keytab, + NULL); + + g_debug ("[RDP] Enabling Kerberos authentication using %s", kerberos_keytab); + + freerdp_settings_set_string (rdp_settings, FreeRDP_KerberosKeytab, + kerberos_keytab); + } + + g_object_get (G_OBJECT (settings), + "rdp-server-cert", &server_cert, + "rdp-server-key", &server_key, + NULL); + + if (server_cert) + rdp_certificate = freerdp_certificate_new_from_pem (server_cert); + if (!rdp_certificate) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create certificate from file"); + return FALSE; + } + freerdp_settings_set_pointer_len (rdp_settings, + FreeRDP_RdpServerCertificate, + rdp_certificate, 1); + + if (server_key) + rdp_private_key = freerdp_key_new_from_pem (server_key); + if (!rdp_private_key) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create private key from file"); + return FALSE; + } + freerdp_settings_set_pointer_len (rdp_settings, + FreeRDP_RdpServerRsaKey, + rdp_private_key, 1); + + freerdp_settings_set_bool (rdp_settings, FreeRDP_RdpSecurity, FALSE); + freerdp_settings_set_bool (rdp_settings, FreeRDP_TlsSecurity, FALSE); + freerdp_settings_set_bool (rdp_settings, FreeRDP_NlaSecurity, TRUE); + + if (grd_context_get_runtime_mode (context) == GRD_RUNTIME_MODE_HANDOVER) + { + freerdp_settings_set_string (rdp_settings, FreeRDP_Username, username); + freerdp_settings_set_string (rdp_settings, FreeRDP_Password, password); + freerdp_settings_set_bool (rdp_settings, FreeRDP_RdstlsSecurity, TRUE); + } + + freerdp_settings_set_uint32 (rdp_settings, FreeRDP_OsMajorType, + OSMAJORTYPE_UNIX); + freerdp_settings_set_uint32 (rdp_settings, FreeRDP_OsMinorType, + OSMINORTYPE_PSEUDO_XSERVER); + + freerdp_settings_set_uint32 (rdp_settings, FreeRDP_ColorDepth, 32); + + freerdp_settings_set_bool (rdp_settings, FreeRDP_GfxAVC444v2, FALSE); + freerdp_settings_set_bool (rdp_settings, FreeRDP_GfxAVC444, FALSE); + freerdp_settings_set_bool (rdp_settings, FreeRDP_GfxH264, FALSE); + freerdp_settings_set_bool (rdp_settings, FreeRDP_GfxSmallCache, FALSE); + freerdp_settings_set_bool (rdp_settings, FreeRDP_GfxThinClient, FALSE); + freerdp_settings_set_bool (rdp_settings, FreeRDP_SupportGraphicsPipeline, TRUE); + + freerdp_settings_set_bool (rdp_settings, FreeRDP_RemoteFxCodec, TRUE); + freerdp_settings_set_bool (rdp_settings, FreeRDP_RemoteFxImageCodec, TRUE); + freerdp_settings_set_bool (rdp_settings, FreeRDP_NSCodec, TRUE); + freerdp_settings_set_bool (rdp_settings, FreeRDP_SurfaceFrameMarkerEnabled, TRUE); + freerdp_settings_set_bool (rdp_settings, FreeRDP_FrameMarkerCommandEnabled, TRUE); + + freerdp_settings_set_uint32 (rdp_settings, FreeRDP_PointerCacheSize, 100); + + freerdp_settings_set_bool (rdp_settings, FreeRDP_FastPathOutput, TRUE); + freerdp_settings_set_bool (rdp_settings, FreeRDP_NetworkAutoDetect, TRUE); + freerdp_settings_set_bool (rdp_settings, FreeRDP_RefreshRect, FALSE); + freerdp_settings_set_bool (rdp_settings, FreeRDP_SupportMonitorLayoutPdu, + use_client_configs); + freerdp_settings_set_bool (rdp_settings, FreeRDP_SupportMultitransport, FALSE); + freerdp_settings_set_uint32 (rdp_settings, FreeRDP_VCFlags, VCCAPS_COMPR_SC); + freerdp_settings_set_uint32 (rdp_settings, FreeRDP_VCChunkSize, 16256); + + freerdp_settings_set_bool (rdp_settings, FreeRDP_HasExtendedMouseEvent, TRUE); + freerdp_settings_set_bool (rdp_settings, FreeRDP_HasHorizontalWheel, TRUE); + freerdp_settings_set_bool (rdp_settings, FreeRDP_HasRelativeMouseEvent, TRUE); + freerdp_settings_set_bool (rdp_settings, FreeRDP_HasQoeEvent, FALSE); + freerdp_settings_set_bool (rdp_settings, FreeRDP_UnicodeInput, TRUE); + + freerdp_settings_set_bool (rdp_settings, FreeRDP_AudioCapture, TRUE); + freerdp_settings_set_bool (rdp_settings, FreeRDP_AudioPlayback, TRUE); + freerdp_settings_set_bool (rdp_settings, FreeRDP_RemoteConsoleAudio, TRUE); + + peer->Logon = rdp_peer_logon; + peer->Capabilities = rdp_peer_capabilities; + peer->PostConnect = rdp_peer_post_connect; + peer->Activate = rdp_peer_activate; + + peer->context->update->SuppressOutput = rdp_suppress_output; + + rdp_input = peer->context->input; + rdp_input->SynchronizeEvent = rdp_input_synchronize_event; + rdp_input->MouseEvent = rdp_input_mouse_event; + rdp_input->ExtendedMouseEvent = rdp_input_extended_mouse_event; + rdp_input->RelMouseEvent = rdp_input_rel_mouse_event; + rdp_input->KeyboardEvent = rdp_input_keyboard_event; + rdp_input->UnicodeKeyboardEvent = rdp_input_unicode_keyboard_event; + + rdp_autodetect = peer->context->autodetect; + rdp_autodetect->OnConnectTimeAutoDetectBegin = + rdp_autodetect_on_connect_time_autodetect_begin; + + if (!peer->Initialize (peer)) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to initialize peer"); + return FALSE; + } + + return TRUE; +} + +gpointer +socket_thread_func (gpointer data) +{ + GrdSessionRdp *session_rdp = data; + freerdp_peer *peer; + RdpPeerContext *rdp_peer_context; + HANDLE vcm; + HANDLE channel_event; + HANDLE events[32] = {}; + uint32_t n_events; + uint32_t n_freerdp_handles; + + peer = session_rdp->peer; + rdp_peer_context = (RdpPeerContext *) peer->context; + vcm = rdp_peer_context->vcm; + channel_event = WTSVirtualChannelManagerGetEventHandle (vcm); + + while (TRUE) + { + GrdRdpNetworkAutodetection *network_autodetection = + rdp_peer_context->network_autodetection; + gboolean pending_bw_measure_stop = FALSE; + HANDLE bw_measure_stop_event = NULL; + + n_events = 0; + + events[n_events++] = session_rdp->stop_event; + if (network_autodetection) + { + bw_measure_stop_event = + grd_rdp_network_autodetection_get_bw_measure_stop_event_handle (network_autodetection); + events[n_events++] = bw_measure_stop_event; + } + + events[n_events++] = channel_event; + + n_freerdp_handles = peer->GetEventHandles (peer, &events[n_events], + 32 - n_events); + if (!n_freerdp_handles) + { + g_warning ("Failed to get FreeRDP transport event handles"); + handle_client_gone (session_rdp); + break; + } + n_events += n_freerdp_handles; + + WaitForMultipleObjects (n_events, events, FALSE, INFINITE); + + if (session_rdp->session_should_stop) + break; + + if (!peer->CheckFileDescriptor (peer)) + { + g_message ("[RDP] Network or intentional disconnect, stopping session"); + handle_client_gone (session_rdp); + break; + } + + if (peer->connected && + WTSVirtualChannelManagerIsChannelJoined (vcm, DRDYNVC_SVC_CHANNEL_NAME)) + { + GrdRdpDvcTelemetry *telemetry; + GrdRdpDvcGraphicsPipeline *graphics_pipeline; + GrdRdpDvcInput *input; + GrdRdpDvcAudioPlayback *audio_playback; + GrdRdpDvcDisplayControl *display_control; + GrdRdpDvcAudioInput *audio_input; + GrdRdpDvcCameraEnumerator *camera_enumerator; + + switch (WTSVirtualChannelManagerGetDrdynvcState (vcm)) + { + case DRDYNVC_STATE_NONE: + /* + * This ensures that WTSVirtualChannelManagerCheckFileDescriptor() + * will be called, which initializes the drdynvc channel + */ + SetEvent (channel_event); + break; + case DRDYNVC_STATE_READY: + g_mutex_lock (&rdp_peer_context->channel_mutex); + telemetry = rdp_peer_context->telemetry; + graphics_pipeline = rdp_peer_context->graphics_pipeline; + input = rdp_peer_context->input; + audio_playback = rdp_peer_context->audio_playback; + display_control = rdp_peer_context->display_control; + audio_input = rdp_peer_context->audio_input; + camera_enumerator = rdp_peer_context->camera_enumerator; + + if (telemetry && !session_rdp->session_should_stop) + grd_rdp_dvc_maybe_init (GRD_RDP_DVC (telemetry)); + if (graphics_pipeline && !session_rdp->session_should_stop) + grd_rdp_dvc_maybe_init (GRD_RDP_DVC (graphics_pipeline)); + if (input && !session_rdp->session_should_stop) + grd_rdp_dvc_maybe_init (GRD_RDP_DVC (input)); + if (audio_playback && !session_rdp->session_should_stop) + grd_rdp_dvc_maybe_init (GRD_RDP_DVC (audio_playback)); + if (display_control && !session_rdp->session_should_stop) + grd_rdp_dvc_maybe_init (GRD_RDP_DVC (display_control)); + if (audio_input && !session_rdp->session_should_stop) + grd_rdp_dvc_maybe_init (GRD_RDP_DVC (audio_input)); + if (camera_enumerator && !session_rdp->session_should_stop) + grd_rdp_dvc_maybe_init (GRD_RDP_DVC (camera_enumerator)); + g_mutex_unlock (&rdp_peer_context->channel_mutex); + break; + } + + if (session_rdp->session_should_stop) + break; + } + + if (bw_measure_stop_event) + { + pending_bw_measure_stop = WaitForSingleObject (bw_measure_stop_event, + 0) == WAIT_OBJECT_0; + } + + if (WaitForSingleObject (channel_event, 0) == WAIT_OBJECT_0 && + !WTSVirtualChannelManagerCheckFileDescriptor (vcm)) + { + g_message ("Unable to check VCM file descriptor, closing connection"); + handle_client_gone (session_rdp); + break; + } + + if (pending_bw_measure_stop) + grd_rdp_network_autodetection_bw_measure_stop (network_autodetection); + } + + return NULL; +} + +static void +on_view_only_changed (GrdSettings *settings, + GParamSpec *pspec, + GrdSessionRdp *session_rdp) +{ + g_object_get (G_OBJECT (settings), + "rdp-view-only", &session_rdp->is_view_only, + NULL); +} + +GrdSessionRdp * +grd_session_rdp_new (GrdRdpServer *rdp_server, + GSocketConnection *connection) +{ + g_autoptr (GrdSessionRdp) session_rdp = NULL; + GrdContext *context; + GrdSettings *settings; + char *username; + char *password; + g_autoptr (GError) error = NULL; + + context = grd_rdp_server_get_context (rdp_server); + settings = grd_context_get_settings (context); + if (!grd_settings_get_rdp_credentials (settings, + &username, &password, + &error)) + { + if (error) + g_warning ("[RDP] Couldn't retrieve RDP credentials: %s", error->message); + else + g_message ("[RDP] Credentials are not set, denying client"); + + return NULL; + } + + session_rdp = g_object_new (GRD_TYPE_SESSION_RDP, + "context", context, + NULL); + + session_rdp->server = rdp_server; + session_rdp->connection = g_object_ref (connection); + + g_object_get (G_OBJECT (settings), + "rdp-screen-share-mode", &session_rdp->screen_share_mode, + "rdp-view-only", &session_rdp->is_view_only, + NULL); + + g_signal_connect (settings, "notify::rdp-view-only", + G_CALLBACK (on_view_only_changed), + session_rdp); + + session_rdp->renderer = grd_rdp_renderer_new (session_rdp); + session_rdp->layout_manager = grd_rdp_layout_manager_new (session_rdp); + + if (!init_rdp_session (session_rdp, username, password, &error)) + { + g_warning ("[RDP] Couldn't initialize session: %s", error->message); + g_free (password); + g_free (username); + return NULL; + } + + session_rdp->socket_thread = g_thread_new ("RDP socket thread", + socket_thread_func, + session_rdp); + + g_free (password); + g_free (username); + + return g_steal_pointer (&session_rdp); +} + +static gboolean +has_session_close_queued (GrdSessionRdp *session_rdp) +{ + gboolean pending_session_closing; + + g_mutex_lock (&session_rdp->close_session_mutex); + pending_session_closing = session_rdp->close_session_idle_id != 0; + g_mutex_unlock (&session_rdp->close_session_mutex); + + return pending_session_closing; +} + +static void +clear_rdp_peer (GrdSessionRdp *session_rdp) +{ + g_clear_pointer (&session_rdp->sam_file, grd_rdp_sam_free_sam_file); + + if (session_rdp->peer) + { + freerdp_peer_context_free (session_rdp->peer); + g_clear_pointer (&session_rdp->peer, freerdp_peer_free); + } +} + +static void +grd_session_rdp_stop (GrdSession *session) +{ + GrdSessionRdp *session_rdp = GRD_SESSION_RDP (session); + freerdp_peer *peer = session_rdp->peer; + RdpPeerContext *rdp_peer_context = (RdpPeerContext *) peer->context; + + g_debug ("Stopping RDP session"); + + unset_rdp_peer_flag (session_rdp, RDP_PEER_ACTIVATED); + session_rdp->session_should_stop = TRUE; + SetEvent (session_rdp->stop_event); + + if (!has_session_close_queued (session_rdp)) + { + freerdp_set_error_info (peer->context->rdp, + ERRINFO_RPC_INITIATED_DISCONNECT); + } + else if (session_rdp->rdp_error_info) + { + freerdp_set_error_info (peer->context->rdp, session_rdp->rdp_error_info); + } + + if (rdp_peer_context->network_autodetection) + { + grd_rdp_network_autodetection_invoke_shutdown ( + rdp_peer_context->network_autodetection); + } + + grd_rdp_renderer_invoke_shutdown (session_rdp->renderer); + + g_mutex_lock (&rdp_peer_context->channel_mutex); + g_clear_object (&rdp_peer_context->camera_enumerator); + g_clear_object (&rdp_peer_context->audio_input); + g_clear_object (&rdp_peer_context->clipboard_rdp); + g_clear_object (&rdp_peer_context->audio_playback); + g_clear_object (&rdp_peer_context->display_control); + g_clear_object (&rdp_peer_context->input); + g_clear_object (&rdp_peer_context->graphics_pipeline); + g_clear_object (&rdp_peer_context->telemetry); + g_mutex_unlock (&rdp_peer_context->channel_mutex); + + g_clear_pointer (&session_rdp->socket_thread, g_thread_join); + g_clear_handle_id (&session_rdp->notify_post_connected_source_id, + g_source_remove); + + g_hash_table_foreach_remove (session_rdp->pressed_keys, + notify_keycode_released, + session_rdp); + g_hash_table_foreach_remove (session_rdp->pressed_unicode_keys, + notify_keysym_released, + session_rdp); + grd_rdp_event_queue_flush (session_rdp->rdp_event_queue); + + g_clear_object (&session_rdp->layout_manager); + + g_clear_object (&session_rdp->cursor_renderer); + g_clear_object (&session_rdp->renderer); + + peer->Close (peer); + grd_close_connection_and_notify (session_rdp->connection); + g_clear_object (&session_rdp->connection); + + g_clear_object (&rdp_peer_context->network_autodetection); + + peer->Disconnect (peer); + clear_rdp_peer (session_rdp); + + g_clear_handle_id (&session_rdp->close_session_idle_id, g_source_remove); +} + +static gboolean +close_session_idle (gpointer user_data) +{ + GrdSessionRdp *session_rdp = GRD_SESSION_RDP (user_data); + + grd_session_stop (GRD_SESSION (session_rdp)); + + session_rdp->close_session_idle_id = 0; + + return G_SOURCE_REMOVE; +} + +static void +initialize_graphics_pipeline (GrdSessionRdp *session_rdp) +{ + rdpContext *rdp_context = session_rdp->peer->context; + RdpPeerContext *rdp_peer_context = (RdpPeerContext *) rdp_context; + GrdHwAccelNvidia *hwaccel_nvidia = + grd_rdp_server_get_hwaccel_nvidia (session_rdp->server); + GrdRdpDvcGraphicsPipeline *graphics_pipeline; + GrdRdpDvcTelemetry *telemetry; + + g_assert (!rdp_peer_context->telemetry); + g_assert (!rdp_peer_context->graphics_pipeline); + + telemetry = grd_rdp_dvc_telemetry_new (session_rdp, + rdp_peer_context->dvc_handler, + rdp_peer_context->vcm, + rdp_context); + rdp_peer_context->telemetry = telemetry; + + graphics_pipeline = + grd_rdp_dvc_graphics_pipeline_new (session_rdp, + session_rdp->renderer, + rdp_peer_context->dvc_handler, + rdp_peer_context->vcm, + rdp_context, + rdp_peer_context->network_autodetection, + rdp_peer_context->encode_stream, + rdp_peer_context->rfx_context); + grd_rdp_dvc_graphics_pipeline_set_hwaccel_nvidia (graphics_pipeline, + hwaccel_nvidia); + rdp_peer_context->graphics_pipeline = graphics_pipeline; +} + +static void +initialize_remaining_virtual_channels (GrdSessionRdp *session_rdp) +{ + rdpContext *rdp_context = session_rdp->peer->context; + RdpPeerContext *rdp_peer_context = (RdpPeerContext *) rdp_context; + rdpSettings *rdp_settings = rdp_context->settings; + GrdRdpDvcHandler *dvc_handler = rdp_peer_context->dvc_handler; + HANDLE vcm = rdp_peer_context->vcm; + + rdp_peer_context->input = + grd_rdp_dvc_input_new (session_rdp->layout_manager, + session_rdp, dvc_handler, vcm); + + if (session_rdp->screen_share_mode == GRD_RDP_SCREEN_SHARE_MODE_EXTEND) + { + rdp_peer_context->display_control = + grd_rdp_dvc_display_control_new (session_rdp->layout_manager, + session_rdp, + dvc_handler, + rdp_peer_context->vcm, + get_max_monitor_count (session_rdp)); + } + if (WTSVirtualChannelManagerIsChannelJoined (vcm, CLIPRDR_SVC_CHANNEL_NAME)) + { + uint32_t os_major_type = + freerdp_settings_get_uint32 (rdp_settings, FreeRDP_OsMajorType); + gboolean peer_is_on_ms_windows; + + peer_is_on_ms_windows = os_major_type == OSMAJORTYPE_WINDOWS; + rdp_peer_context->clipboard_rdp = + grd_clipboard_rdp_new (session_rdp, vcm, !peer_is_on_ms_windows); + } + if (freerdp_settings_get_bool (rdp_settings, FreeRDP_AudioPlayback) && + !freerdp_settings_get_bool (rdp_settings, FreeRDP_RemoteConsoleAudio)) + { + rdp_peer_context->audio_playback = + grd_rdp_dvc_audio_playback_new (session_rdp, dvc_handler, vcm, + rdp_context); + } + if (freerdp_settings_get_bool (rdp_settings, FreeRDP_AudioCapture)) + { + rdp_peer_context->audio_input = + grd_rdp_dvc_audio_input_new (session_rdp, dvc_handler, vcm, + rdp_context); + } + + rdp_peer_context->camera_enumerator = + grd_rdp_dvc_camera_enumerator_new (session_rdp, dvc_handler, vcm, + rdp_context); +} + +static void +on_remote_desktop_session_ready (GrdSession *session) +{ + GrdSessionRdp *session_rdp = GRD_SESSION_RDP (session); + + grd_rdp_session_metrics_notify_phase_completion (session_rdp->session_metrics, + GRD_RDP_PHASE_SESSION_READY); + + session_rdp->cursor_renderer = + grd_rdp_cursor_renderer_new (session_rdp->renderer, + session_rdp->peer->context); + grd_rdp_cursor_renderer_notify_session_ready (session_rdp->cursor_renderer); + + initialize_graphics_pipeline (session_rdp); + initialize_remaining_virtual_channels (session_rdp); +} + +static void +on_remote_desktop_session_started (GrdSession *session) +{ + GrdSessionRdp *session_rdp = GRD_SESSION_RDP (session); + + if (!grd_rdp_renderer_start (session_rdp->renderer)) + { + grd_session_rdp_notify_error (session_rdp, + GRD_SESSION_RDP_ERROR_CLOSE_STACK_ON_DRIVER_FAILURE); + return; + } + + grd_rdp_session_metrics_notify_phase_completion (session_rdp->session_metrics, + GRD_RDP_PHASE_SESSION_STARTED); + grd_rdp_layout_manager_notify_session_started (session_rdp->layout_manager); + grd_rdp_event_queue_flush_synchronization (session_rdp->rdp_event_queue); +} + +static void +grd_session_rdp_on_stream_created (GrdSession *session, + uint32_t stream_id, + GrdStream *stream) +{ + GrdSessionRdp *session_rdp = GRD_SESSION_RDP (session); + GrdRdpStreamOwner *stream_owner = NULL; + + if (!g_hash_table_lookup_extended (session_rdp->stream_table, + GUINT_TO_POINTER (stream_id), + NULL, (gpointer) &stream_owner)) + g_assert_not_reached (); + + grd_rdp_stream_owner_notify_stream_created (stream_owner, stream_id, stream); +} + +static void +grd_session_rdp_on_caps_lock_state_changed (GrdSession *session, + gboolean state) +{ + GrdSessionRdp *session_rdp = GRD_SESSION_RDP (session); + GrdRdpEventQueue *rdp_event_queue = session_rdp->rdp_event_queue; + + grd_rdp_event_queue_update_caps_lock_state (rdp_event_queue, state); +} + +static void +grd_session_rdp_on_num_lock_state_changed (GrdSession *session, + gboolean state) +{ + GrdSessionRdp *session_rdp = GRD_SESSION_RDP (session); + GrdRdpEventQueue *rdp_event_queue = session_rdp->rdp_event_queue; + + grd_rdp_event_queue_update_num_lock_state (rdp_event_queue, state); +} + +static void +grd_session_rdp_dispose (GObject *object) +{ + GrdSessionRdp *session_rdp = GRD_SESSION_RDP (object); + + g_assert (!session_rdp->notify_post_connected_source_id); + g_assert (!session_rdp->cursor_renderer); + + g_clear_object (&session_rdp->layout_manager); + clear_rdp_peer (session_rdp); + + if (session_rdp->connection) + grd_close_connection_and_notify (session_rdp->connection); + g_clear_object (&session_rdp->connection); + + g_clear_object (&session_rdp->renderer); + + g_clear_object (&session_rdp->rdp_event_queue); + g_clear_object (&session_rdp->session_metrics); + + g_clear_pointer (&session_rdp->stream_table, g_hash_table_unref); + g_clear_pointer (&session_rdp->pressed_unicode_keys, g_hash_table_unref); + g_clear_pointer (&session_rdp->pressed_keys, g_hash_table_unref); + + g_clear_pointer (&session_rdp->stop_event, CloseHandle); + + G_OBJECT_CLASS (grd_session_rdp_parent_class)->dispose (object); +} + +static void +grd_session_rdp_finalize (GObject *object) +{ + GrdSessionRdp *session_rdp = GRD_SESSION_RDP (object); + + g_mutex_clear (&session_rdp->notify_post_connected_mutex); + g_mutex_clear (&session_rdp->close_session_mutex); + g_mutex_clear (&session_rdp->rdp_flags_mutex); + + G_OBJECT_CLASS (grd_session_rdp_parent_class)->finalize (object); +} + +static void +grd_session_rdp_init (GrdSessionRdp *session_rdp) +{ + session_rdp->stop_event = CreateEvent (NULL, TRUE, FALSE, NULL); + + session_rdp->pressed_keys = g_hash_table_new (NULL, NULL); + session_rdp->pressed_unicode_keys = g_hash_table_new (NULL, NULL); + session_rdp->stream_table = g_hash_table_new (NULL, NULL); + + g_mutex_init (&session_rdp->rdp_flags_mutex); + g_mutex_init (&session_rdp->close_session_mutex); + g_mutex_init (&session_rdp->notify_post_connected_mutex); + + session_rdp->session_metrics = grd_rdp_session_metrics_new (); + session_rdp->rdp_event_queue = grd_rdp_event_queue_new (session_rdp); + + g_signal_connect (session_rdp, "ready", + G_CALLBACK (on_remote_desktop_session_ready), NULL); + g_signal_connect (session_rdp, "started", + G_CALLBACK (on_remote_desktop_session_started), NULL); +} + +static void +grd_session_rdp_class_init (GrdSessionRdpClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GrdSessionClass *session_class = GRD_SESSION_CLASS (klass); + + object_class->dispose = grd_session_rdp_dispose; + object_class->finalize = grd_session_rdp_finalize; + + session_class->stop = grd_session_rdp_stop; + session_class->on_stream_created = grd_session_rdp_on_stream_created; + session_class->on_caps_lock_state_changed = + grd_session_rdp_on_caps_lock_state_changed; + session_class->on_num_lock_state_changed = + grd_session_rdp_on_num_lock_state_changed; + + signals[POST_CONNECTED] = g_signal_new ("post-connected", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, 0); +} diff --git a/grd-session-rdp.h b/grd-session-rdp.h new file mode 100644 index 0000000..0ae0a32 --- /dev/null +++ b/grd-session-rdp.h @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2020-2023 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include +#include + +#include "grd-enums.h" +#include "grd-session.h" +#include "grd-types.h" + +#define GRD_TYPE_SESSION_RDP (grd_session_rdp_get_type ()) +G_DECLARE_FINAL_TYPE (GrdSessionRdp, + grd_session_rdp, + GRD, SESSION_RDP, + GrdSession) + +typedef enum _GrdSessionRdpError +{ + GRD_SESSION_RDP_ERROR_NONE, + GRD_SESSION_RDP_ERROR_BAD_CAPS, + GRD_SESSION_RDP_ERROR_BAD_MONITOR_DATA, + GRD_SESSION_RDP_ERROR_CLOSE_STACK_ON_DRIVER_FAILURE, + GRD_SESSION_RDP_ERROR_GRAPHICS_SUBSYSTEM_FAILED, + GRD_SESSION_RDP_ERROR_SERVER_REDIRECTION, +} GrdSessionRdpError; + +typedef enum _GrdRdpChannel +{ + GRD_RDP_CHANNEL_NONE, + GRD_RDP_CHANNEL_AUDIO_INPUT, + GRD_RDP_CHANNEL_AUDIO_PLAYBACK, + GRD_RDP_CHANNEL_CAMERA, + GRD_RDP_CHANNEL_CAMERA_ENUMERATOR, + GRD_RDP_CHANNEL_DISPLAY_CONTROL, + GRD_RDP_CHANNEL_GRAPHICS_PIPELINE, + GRD_RDP_CHANNEL_INPUT, + GRD_RDP_CHANNEL_TELEMETRY, +} GrdRdpChannel; + +typedef struct rdp_context rdpContext; + +GrdSessionRdp *grd_session_rdp_new (GrdRdpServer *rdp_server, + GSocketConnection *connection); + +GrdRdpSessionMetrics *grd_session_rdp_get_session_metrics (GrdSessionRdp *session_rdp); + +void grd_session_rdp_notify_error (GrdSessionRdp *session_rdp, + GrdSessionRdpError error_info); + +void grd_session_rdp_tear_down_channel (GrdSessionRdp *session_rdp, + GrdRdpChannel channel); + +uint32_t grd_session_rdp_acquire_stream_id (GrdSessionRdp *session_rdp, + GrdRdpStreamOwner *stream_owner); + +void grd_session_rdp_release_stream_id (GrdSessionRdp *session_rdp, + uint32_t stream_id); + +gboolean grd_session_rdp_is_client_mstsc (GrdSessionRdp *session_rdp); + +gboolean grd_session_rdp_send_server_redirection (GrdSessionRdp *session_rdp, + const char *routing_token, + const char *username, + const char *password, + const char *certificate); + +GrdRdpServer *grd_session_rdp_get_server (GrdSessionRdp *session_rdp); + +GrdRdpRenderer *grd_session_rdp_get_renderer (GrdSessionRdp *session_rdp); + +GrdRdpCursorRenderer *grd_session_rdp_get_cursor_renderer (GrdSessionRdp *session_rdp); + +rdpContext *grd_session_rdp_get_rdp_context (GrdSessionRdp *session_rdp); + +GrdRdpDvcGraphicsPipeline *grd_session_rdp_get_graphics_pipeline (GrdSessionRdp *session_rdp); + +GrdRdpScreenShareMode grd_session_rdp_get_screen_share_mode (GrdSessionRdp *session_rdp); diff --git a/grd-session-vnc.c b/grd-session-vnc.c new file mode 100644 index 0000000..7cd41e9 --- /dev/null +++ b/grd-session-vnc.c @@ -0,0 +1,958 @@ +/* + * Copyright (C) 2015 Red Hat Inc. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + * Written by: + * Jonas Ådahl + */ + +#include "config.h" + +#include "grd-session-vnc.h" + +#include +#include +#include +#include + +#include "grd-clipboard-vnc.h" +#include "grd-context.h" +#include "grd-prompt.h" +#include "grd-settings.h" +#include "grd-stream.h" +#include "grd-utils.h" +#include "grd-vnc-server.h" +#include "grd-vnc-pipewire-stream.h" + +/* BGRx */ +#define BGRX_BITS_PER_SAMPLE 8 +#define BGRX_SAMPLES_PER_PIXEL 3 +#define BGRX_BYTES_PER_PIXEL 4 + +struct _GrdSessionVnc +{ + GrdSession parent; + + GSocketConnection *connection; + GSource *source; + rfbScreenInfoPtr rfb_screen; + rfbClientPtr rfb_client; + + gboolean pending_framebuffer_resize; + int pending_framebuffer_width; + int pending_framebuffer_height; + + guint close_session_idle_id; + + GrdVncAuthMethod auth_method; + + GrdPrompt *prompt; + GCancellable *prompt_cancellable; + + GrdVncPipeWireStream *pipewire_stream; + GrdStream *stream; + + int prev_x; + int prev_y; + int prev_button_mask; + GHashTable *pressed_keys; + + GrdClipboardVnc *clipboard_vnc; + GrdVncScreenShareMode screen_share_mode; + gboolean is_view_only; + GrdVncMonitorConfig *monitor_config; +}; + +G_DEFINE_TYPE (GrdSessionVnc, grd_session_vnc, GRD_TYPE_SESSION) + +static void +grd_session_vnc_detach_source (GrdSessionVnc *session_vnc); + +static gboolean +close_session_idle (gpointer user_data); + +static rfbBool +check_rfb_password (rfbClientPtr rfb_client, + const char *response_encrypted, + int len); + +static void +swap_uint8 (uint8_t *a, + uint8_t *b) +{ + uint8_t tmp; + + tmp = *a; + *a = *b; + *b = tmp; +} + +static void +update_server_format (GrdSessionVnc *session_vnc) +{ + rfbScreenInfoPtr rfb_screen = session_vnc->rfb_screen; + + /* + * Our format is hard coded to BGRX but LibVNCServer assumes it's RGBX; + * lets override that. + */ + swap_uint8 (&rfb_screen->serverFormat.redShift, + &rfb_screen->serverFormat.blueShift); +} + +static void +resize_vnc_framebuffer (GrdSessionVnc *session_vnc, + int width, + int height) +{ + rfbScreenInfoPtr rfb_screen = session_vnc->rfb_screen; + uint8_t *framebuffer; + + if (!session_vnc->rfb_client->useNewFBSize) + g_warning ("Client does not support NewFBSize"); + + g_free (rfb_screen->frameBuffer); + framebuffer = g_malloc0 (width * height * BGRX_BYTES_PER_PIXEL); + rfbNewFramebuffer (rfb_screen, + (char *) framebuffer, + width, height, + BGRX_BITS_PER_SAMPLE, + BGRX_SAMPLES_PER_PIXEL, + BGRX_BYTES_PER_PIXEL); + + update_server_format (session_vnc); + rfb_screen->setTranslateFunction (session_vnc->rfb_client); +} + +void +grd_session_vnc_queue_resize_framebuffer (GrdSessionVnc *session_vnc, + int width, + int height) +{ + rfbScreenInfoPtr rfb_screen = session_vnc->rfb_screen; + + if (rfb_screen->width == width && rfb_screen->height == height) + return; + + if (session_vnc->rfb_client->preferredEncoding == -1) + { + session_vnc->pending_framebuffer_resize = TRUE; + session_vnc->pending_framebuffer_width = width; + session_vnc->pending_framebuffer_height = height; + return; + } + + resize_vnc_framebuffer (session_vnc, width, height); +} + +void +grd_session_vnc_take_buffer (GrdSessionVnc *session_vnc, + void *data) +{ + if (session_vnc->pending_framebuffer_resize) + { + free (data); + return; + } + + free (session_vnc->rfb_screen->frameBuffer); + session_vnc->rfb_screen->frameBuffer = data; + + rfbMarkRectAsModified (session_vnc->rfb_screen, 0, 0, + session_vnc->rfb_screen->width, + session_vnc->rfb_screen->height); + rfbProcessEvents (session_vnc->rfb_screen, 0); +} + +void +grd_session_vnc_flush (GrdSessionVnc *session_vnc) +{ + rfbProcessEvents (session_vnc->rfb_screen, 0); +} + +void +grd_session_vnc_set_cursor (GrdSessionVnc *session_vnc, + rfbCursorPtr rfb_cursor) +{ + rfbSetCursor (session_vnc->rfb_screen, rfb_cursor); +} + +void +grd_session_vnc_move_cursor (GrdSessionVnc *session_vnc, + int x, + int y) +{ + if (session_vnc->rfb_screen->cursorX == x || + session_vnc->rfb_screen->cursorY == y) + return; + + LOCK (session_vnc->rfb_screen->cursorMutex); + session_vnc->rfb_screen->cursorX = x; + session_vnc->rfb_screen->cursorY = y; + UNLOCK (session_vnc->rfb_screen->cursorMutex); + + session_vnc->rfb_client->cursorWasMoved = TRUE; +} + +void +grd_session_vnc_set_client_clipboard_text (GrdSessionVnc *session_vnc, + char *text, + int text_length) +{ + rfbSendServerCutText(session_vnc->rfb_screen, text, text_length); +} + +static void +grd_vnc_monitor_config_free (GrdVncMonitorConfig *monitor_config) +{ + g_clear_pointer (&monitor_config->connectors, g_strfreev); + g_free (monitor_config->virtual_monitors); + g_free (monitor_config); +} + +static void +maybe_queue_close_session_idle (GrdSessionVnc *session_vnc) +{ + if (session_vnc->close_session_idle_id) + return; + + session_vnc->close_session_idle_id = + g_idle_add (close_session_idle, session_vnc); +} + +gboolean +grd_session_vnc_is_client_gone (GrdSessionVnc *session_vnc) +{ + return !session_vnc->rfb_client; +} + +static void +handle_client_gone (rfbClientPtr rfb_client) +{ + GrdSessionVnc *session_vnc = rfb_client->screen->screenData; + + g_debug ("VNC client gone"); + + grd_session_vnc_detach_source (session_vnc); + maybe_queue_close_session_idle (session_vnc); + session_vnc->rfb_client = NULL; +} + +static void +prompt_response_callback (GObject *source_object, + GAsyncResult *async_result, + gpointer user_data) +{ + GrdSessionVnc *session_vnc = GRD_SESSION_VNC (user_data); + g_autoptr(GError) error = NULL; + GrdPromptResponse response; + + if (!grd_prompt_query_finish (session_vnc->prompt, + async_result, + &response, + &error)) + { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + { + g_warning ("Failed to query user about session: %s", error->message); + rfbRefuseOnHoldClient (session_vnc->rfb_client); + return; + } + else + { + return; + } + } + + switch (response) + { + case GRD_PROMPT_RESPONSE_ACCEPT: + grd_session_start (GRD_SESSION (session_vnc)); + rfbStartOnHoldClient (session_vnc->rfb_client); + return; + case GRD_PROMPT_RESPONSE_CANCEL: + rfbRefuseOnHoldClient (session_vnc->rfb_client); + return; + } + + g_assert_not_reached (); +} + +static void +show_sharing_desktop_prompt (GrdSessionVnc *session_vnc, + const char *host) +{ + g_autoptr (GrdPromptDefinition) prompt_definition = NULL; + + session_vnc->prompt = g_object_new (GRD_TYPE_PROMPT, NULL); + session_vnc->prompt_cancellable = g_cancellable_new (); + + prompt_definition = g_new0 (GrdPromptDefinition, 1); + prompt_definition->summary = + g_strdup_printf (_("Do you want to share your desktop?")); + prompt_definition->body = + g_strdup_printf (_("A user on the computer '%s' is trying to remotely " + "view or control your desktop."), host); + prompt_definition->cancel_label = g_strdup_printf (_("Refuse")); + prompt_definition->accept_label = g_strdup_printf (_("Accept")); + + grd_prompt_query_async (session_vnc->prompt, + prompt_definition, + session_vnc->prompt_cancellable, + prompt_response_callback, + session_vnc); +} + +static enum rfbNewClientAction +handle_new_client (rfbClientPtr rfb_client) +{ + GrdSessionVnc *session_vnc = GRD_SESSION_VNC (rfb_client->screen->screenData); + GrdContext *context = grd_session_get_context (GRD_SESSION (session_vnc)); + GrdSettings *settings = grd_context_get_settings (context); + + g_debug ("New VNC client"); + + g_object_get (G_OBJECT (settings), + "vnc-auth-method", &session_vnc->auth_method, + NULL); + + session_vnc->rfb_client = rfb_client; + rfb_client->clientGoneHook = handle_client_gone; + + switch (session_vnc->auth_method) + { + case GRD_VNC_AUTH_METHOD_PROMPT: + show_sharing_desktop_prompt (session_vnc, rfb_client->host); + grd_session_vnc_detach_source (session_vnc); + return RFB_CLIENT_ON_HOLD; + case GRD_VNC_AUTH_METHOD_PASSWORD: + session_vnc->rfb_screen->passwordCheck = check_rfb_password; + /* + * authPasswdData needs to be non NULL in libvncserver to trigger + * password authentication. + */ + session_vnc->rfb_screen->authPasswdData = (gpointer) 1; + + return RFB_CLIENT_ACCEPT; + } + + g_assert_not_reached (); +} + +static void +handle_key_event (rfbBool down, + rfbKeySym keysym, + rfbClientPtr rfb_client) +{ + GrdSessionVnc *session_vnc = GRD_SESSION_VNC (rfb_client->screen->screenData); + GrdSession *session = GRD_SESSION (session_vnc); + + if (session_vnc->is_view_only) + return; + + if (down) + { + if (g_hash_table_add (session_vnc->pressed_keys, + GUINT_TO_POINTER (keysym))) + { + grd_session_notify_keyboard_keysym (session, keysym, + GRD_KEY_STATE_PRESSED); + } + } + else + { + if (g_hash_table_remove (session_vnc->pressed_keys, + GUINT_TO_POINTER (keysym))) + { + grd_session_notify_keyboard_keysym (session, keysym, + GRD_KEY_STATE_RELEASED); + } + } +} + +static gboolean +notify_keyboard_key_released (gpointer key, + gpointer value, + gpointer user_data) +{ + GrdSession *session = user_data; + uint32_t keysym = GPOINTER_TO_UINT (key); + + grd_session_notify_keyboard_keysym (session, keysym, + GRD_KEY_STATE_RELEASED); + + return TRUE; +} + +static void +handle_release_all_keys (rfbClientPtr rfb_client) +{ + GrdSessionVnc *session_vnc = rfb_client->screen->screenData; + + g_hash_table_foreach_remove (session_vnc->pressed_keys, + notify_keyboard_key_released, + session_vnc); +} + +static void +handle_set_clipboard_text (char *text, + int text_length, + rfbClientPtr rfb_client) +{ + GrdSessionVnc *session_vnc = rfb_client->screen->screenData; + GrdClipboardVnc *clipboard_vnc = session_vnc->clipboard_vnc; + + grd_clipboard_vnc_set_clipboard_text (clipboard_vnc, text, text_length); +} + +static void +grd_session_vnc_notify_axis (GrdSessionVnc *session_vnc, + int button_mask_bit_index) +{ + GrdSession *session = GRD_SESSION (session_vnc); + GrdPointerAxis axis; + int steps; + + switch (button_mask_bit_index) + { + case 3: + axis = GRD_POINTER_AXIS_VERTICAL; + steps = -1; + break; + + case 4: + axis = GRD_POINTER_AXIS_VERTICAL; + steps = 1; + break; + + case 5: + axis = GRD_POINTER_AXIS_HORIZONTAL; + steps = -1; + break; + + case 6: + axis = GRD_POINTER_AXIS_HORIZONTAL; + steps = 1; + break; + + default: + return; + } + + grd_session_notify_pointer_axis_discrete (session, axis, steps); +} + +static void +handle_pointer_event (int button_mask, + int x, + int y, + rfbClientPtr rfb_client) +{ + GrdSessionVnc *session_vnc = rfb_client->screen->screenData; + GrdSession *session = GRD_SESSION (session_vnc); + + if (session_vnc->is_view_only) + return; + + if (session_vnc->stream && + (x != session_vnc->prev_x || y != session_vnc->prev_y)) + { + rfbScreenInfoPtr rfb_screen = session_vnc->rfb_screen; + GrdEventMotionAbs motion_abs = {}; + + motion_abs.input_rect_width = rfb_screen->width; + motion_abs.input_rect_height = rfb_screen->height; + motion_abs.x = x; + motion_abs.y = y; + + grd_session_notify_pointer_motion_absolute (session, session_vnc->stream, + &motion_abs); + + session_vnc->prev_x = x; + session_vnc->prev_y = y; + } + + if (button_mask != session_vnc->prev_button_mask) + { + unsigned int i; + int buttons[] = { + BTN_LEFT, /* 0 */ + BTN_MIDDLE, /* 1 */ + BTN_RIGHT, /* 2 */ + 0, /* 3 - vertical scroll: up */ + 0, /* 4 - vertical scroll: down */ + 0, /* 5 - horizontal scroll: left */ + 0, /* 6 - horizontal scroll: right */ + BTN_SIDE, /* 7 */ + BTN_EXTRA, /* 8 */ + }; + + for (i = 0; i < G_N_ELEMENTS (buttons); i++) + { + int button = buttons[i]; + int prev_button_state = (session_vnc->prev_button_mask >> i) & 0x01; + int cur_button_state = (button_mask >> i) & 0x01; + + if (prev_button_state != cur_button_state) + { + if (button) + { + grd_session_notify_pointer_button (session, + button, + cur_button_state); + } + else + { + grd_session_vnc_notify_axis (session_vnc, i); + } + } + } + + session_vnc->prev_button_mask = button_mask; + } + + rfbDefaultPtrAddEvent (button_mask, x, y, rfb_client); +} + +static int +handle_set_desktop_size (int width, + int height, + int num_screens, + struct rfbExtDesktopScreen *screens, + rfbClientPtr rfb_client) +{ + GrdSessionVnc *session_vnc = rfb_client->screen->screenData; + GrdVncVirtualMonitor *monitor; + + if (!session_vnc->monitor_config->is_virtual) + { + g_warning ("[VNC] Ignoring SetDesktopSize request, mirror-primary mode is used"); + return rfbExtDesktopSize_ResizeProhibited; + } + + g_debug ("[VNC] Trying to set new size %dx%d", width, height); + + g_assert (session_vnc->monitor_config->monitor_count == 1); + g_assert (session_vnc->monitor_config->virtual_monitors); + + monitor = session_vnc->monitor_config->virtual_monitors; + monitor->width = GRD_VNC_CLAMP_DESKTOP_SIZE (width); + monitor->height = GRD_VNC_CLAMP_DESKTOP_SIZE (height); + grd_vnc_pipewire_stream_resize (session_vnc->pipewire_stream, monitor); + + return rfbExtDesktopSize_Success; +} + +static rfbBool +check_rfb_password (rfbClientPtr rfb_client, + const char *response_encrypted, + int len) +{ + GrdSessionVnc *session_vnc = rfb_client->screen->screenData; + GrdContext *context = grd_session_get_context (GRD_SESSION (session_vnc)); + GrdSettings *settings = grd_context_get_settings (context); + g_autofree char *password = NULL; + g_autoptr(GError) error = NULL; + uint8_t challenge_encrypted[CHALLENGESIZE]; + + switch (session_vnc->auth_method) + { + case GRD_VNC_AUTH_METHOD_PROMPT: + return TRUE; + case GRD_VNC_AUTH_METHOD_PASSWORD: + break; + } + + password = grd_settings_get_vnc_password (settings, &error); + if (!password) + { + if (error) + g_warning ("[VNC] Couldn't retrieve VNC password: %s", error->message); + else + g_message ("[VNC] Password is not set, denying client"); + + return FALSE; + } + + if (strlen (password) == 0) + { + g_warning ("VNC password was empty, denying"); + return FALSE; + } + + memcpy(challenge_encrypted, rfb_client->authChallenge, CHALLENGESIZE); + rfbEncryptBytes(challenge_encrypted, password); + + if (memcmp (challenge_encrypted, response_encrypted, len) == 0) + { + grd_session_start (GRD_SESSION (session_vnc)); + grd_session_vnc_detach_source (session_vnc); + return TRUE; + } + else + { + return FALSE; + } +} + +int +grd_session_vnc_get_stride_for_width (GrdSessionVnc *session_vnc, + int width) +{ + return width * BGRX_BYTES_PER_PIXEL; +} + +static void +init_vnc_session (GrdSessionVnc *session_vnc) +{ + GSocket *socket; + int screen_width; + int screen_height; + rfbScreenInfoPtr rfb_screen; + GrdVncMonitorConfig *monitor_config; + + /* Arbitrary framebuffer size, will get the proper size from the stream. */ + screen_width = GRD_VNC_DEFAULT_WIDTH; + screen_height = GRD_VNC_DEFAULT_HEIGHT; + rfb_screen = rfbGetScreen (0, NULL, + screen_width, screen_height, + 8, 3, 4); + session_vnc->rfb_screen = rfb_screen; + + update_server_format (session_vnc); + + session_vnc->clipboard_vnc = grd_clipboard_vnc_new (session_vnc); + + socket = g_socket_connection_get_socket (session_vnc->connection); + rfb_screen->inetdSock = g_socket_get_fd (socket); + rfb_screen->desktopName = "GNOME"; + rfb_screen->versionString = "GNOME Remote Desktop (VNC)"; + rfb_screen->neverShared = TRUE; + rfb_screen->newClientHook = handle_new_client; + rfb_screen->screenData = session_vnc; + + /* Amount of milliseconds to wait before sending an update */ + rfb_screen->deferUpdateTime = 0; + + rfb_screen->kbdAddEvent = handle_key_event; + rfb_screen->kbdReleaseAllKeys = handle_release_all_keys; + rfb_screen->setXCutText = handle_set_clipboard_text; + rfb_screen->ptrAddEvent = handle_pointer_event; + rfb_screen->setDesktopSizeHook = handle_set_desktop_size; + + rfb_screen->frameBuffer = g_malloc0 (screen_width * screen_height * 4); + memset (rfb_screen->frameBuffer, 0x1f, screen_width * screen_height * 4); + + session_vnc->monitor_config = g_new0 (GrdVncMonitorConfig, 1); + monitor_config = session_vnc->monitor_config; + monitor_config->virtual_monitors = NULL; + /* No multi-monitor support yet */ + monitor_config->monitor_count = 1; + + if (session_vnc->screen_share_mode == GRD_VNC_SCREEN_SHARE_MODE_EXTEND) + { + monitor_config->is_virtual = TRUE; + monitor_config->virtual_monitors = g_new0 (GrdVncVirtualMonitor, 1); + + monitor_config->virtual_monitors->pos_x = 0; + monitor_config->virtual_monitors->pos_y = 0; + monitor_config->virtual_monitors->width = GRD_VNC_CLAMP_DESKTOP_SIZE (screen_width); + monitor_config->virtual_monitors->height = GRD_VNC_CLAMP_DESKTOP_SIZE (screen_height); + } + else + { + monitor_config->is_virtual = FALSE; + g_autoptr (GStrvBuilder) connector_builder = NULL; + char **connectors; + + connector_builder = g_strv_builder_new (); + g_strv_builder_add (connector_builder, ""); + connectors = g_strv_builder_end (connector_builder); + + session_vnc->monitor_config->connectors = connectors; + } + + rfbInitServer (rfb_screen); + rfbProcessEvents (rfb_screen, 0); +} + +static gboolean +handle_socket_data (GSocket *socket, + GIOCondition condition, + gpointer user_data) +{ + GrdSessionVnc *session_vnc = user_data; + + if (condition & G_IO_IN) + { + if (rfbIsActive (session_vnc->rfb_screen)) + { + rfbProcessEvents (session_vnc->rfb_screen, 0); + + if (session_vnc->pending_framebuffer_resize && + session_vnc->rfb_client->preferredEncoding != -1) + { + resize_vnc_framebuffer (session_vnc, + session_vnc->pending_framebuffer_width, + session_vnc->pending_framebuffer_height); + session_vnc->pending_framebuffer_resize = FALSE; + + /** + * This is a workaround. libvncserver is unable to handle clipboard + * changes early and either disconnects the client or crashes g-r-d + * if it receives rfbSendServerCutText too early altough the + * authentification process is already done. + * Doing this after resizing the framebuffer, seems to work fine, + * so enable the clipboard here and not when the remote desktop + * session proxy is acquired. + */ + grd_clipboard_vnc_maybe_enable_clipboard (session_vnc->clipboard_vnc); + } + } + } + else + { + g_debug ("Unhandled socket condition %d\n", condition); + return G_SOURCE_REMOVE; + } + + return G_SOURCE_CONTINUE; +} + +static void +grd_session_vnc_attach_source (GrdSessionVnc *session_vnc) +{ + GSocket *socket; + + socket = g_socket_connection_get_socket (session_vnc->connection); + session_vnc->source = g_socket_create_source (socket, + G_IO_IN | G_IO_PRI, + NULL); + g_source_set_callback (session_vnc->source, + (GSourceFunc) handle_socket_data, + session_vnc, NULL); + g_source_attach (session_vnc->source, NULL); +} + +static void +grd_session_vnc_detach_source (GrdSessionVnc *session_vnc) +{ + if (!session_vnc->source) + return; + + g_source_destroy (session_vnc->source); + g_clear_pointer (&session_vnc->source, g_source_unref); +} + +static void +on_view_only_changed (GrdSettings *settings, + GParamSpec *pspec, + GrdSessionVnc *session_vnc) +{ + g_object_get (G_OBJECT (settings), + "vnc-view-only", &session_vnc->is_view_only, + NULL); +} + +GrdSessionVnc * +grd_session_vnc_new (GrdVncServer *vnc_server, + GSocketConnection *connection) +{ + GrdSessionVnc *session_vnc; + GrdContext *context; + GrdSettings *settings; + + context = grd_vnc_server_get_context (vnc_server); + session_vnc = g_object_new (GRD_TYPE_SESSION_VNC, + "context", context, + NULL); + + session_vnc->connection = g_object_ref (connection); + + settings = grd_context_get_settings (context); + g_object_get (G_OBJECT (settings), + "vnc-screen-share-mode", &session_vnc->screen_share_mode, + "vnc-view-only", &session_vnc->is_view_only, + NULL); + + g_signal_connect (settings, "notify::vnc-view-only", + G_CALLBACK (on_view_only_changed), + session_vnc); + + grd_session_vnc_attach_source (session_vnc); + + init_vnc_session (session_vnc); + + return session_vnc; +} + +static void +grd_session_vnc_dispose (GObject *object) +{ + GrdSessionVnc *session_vnc = GRD_SESSION_VNC (object); + + g_assert (!session_vnc->rfb_screen); + + g_clear_pointer (&session_vnc->pressed_keys, g_hash_table_unref); + + G_OBJECT_CLASS (grd_session_vnc_parent_class)->dispose (object); +} + +static void +grd_session_vnc_stop (GrdSession *session) +{ + GrdSessionVnc *session_vnc = GRD_SESSION_VNC (session); + + g_debug ("Stopping VNC session"); + + g_clear_object (&session_vnc->pipewire_stream); + if (session_vnc->stream) + { + grd_stream_disconnect_proxy_signals (session_vnc->stream); + g_clear_object (&session_vnc->stream); + } + + grd_session_vnc_detach_source (session_vnc); + + grd_close_connection_and_notify (session_vnc->connection); + g_clear_object (&session_vnc->connection); + g_clear_object (&session_vnc->clipboard_vnc); + g_clear_pointer (&session_vnc->rfb_screen->frameBuffer, g_free); + g_clear_pointer (&session_vnc->rfb_screen, rfbScreenCleanup); + g_clear_pointer (&session_vnc->monitor_config, grd_vnc_monitor_config_free); + if (session_vnc->prompt_cancellable) + { + g_cancellable_cancel (session_vnc->prompt_cancellable); + g_clear_object (&session_vnc->prompt_cancellable); + } + g_clear_object (&session_vnc->prompt); + + g_clear_handle_id (&session_vnc->close_session_idle_id, g_source_remove); +} + +static gboolean +close_session_idle (gpointer user_data) +{ + GrdSessionVnc *session_vnc = GRD_SESSION_VNC (user_data); + + grd_session_stop (GRD_SESSION (session_vnc)); + + session_vnc->close_session_idle_id = 0; + + return G_SOURCE_REMOVE; +} + +static void +on_remote_desktop_session_started (GrdSession *session) +{ + GrdSessionVnc *session_vnc = GRD_SESSION_VNC (session); + + if (session_vnc->monitor_config->is_virtual) + g_debug ("[VNC] Remote Desktop session will use virtual monitors"); + else + g_debug ("[VNC] Remote Desktop session will mirror the primary monitor"); + + /* Not supporting multi-monitor at the moment */ + g_assert (session_vnc->monitor_config->monitor_count == 1); + + if (session_vnc->monitor_config->is_virtual) + { + grd_session_record_virtual (session, 0, + GRD_SCREEN_CAST_CURSOR_MODE_METADATA, + TRUE); + } + else + { + const char *connector; + + connector = session_vnc->monitor_config->connectors[0]; + grd_session_record_monitor (session, 0, connector, + GRD_SCREEN_CAST_CURSOR_MODE_METADATA); + } +} + +static void +on_pipewire_stream_closed (GrdVncPipeWireStream *stream, + GrdSessionVnc *session_vnc) +{ + g_warning ("PipeWire stream closed, closing client"); + + maybe_queue_close_session_idle (session_vnc); +} + +static void +on_stream_ready (GrdStream *stream, + GrdSessionVnc *session_vnc) +{ + uint32_t pipewire_node_id; + GrdVncVirtualMonitor *virtual_monitor; + g_autoptr (GError) error = NULL; + + virtual_monitor = session_vnc->monitor_config->virtual_monitors; + pipewire_node_id = grd_stream_get_pipewire_node_id (stream); + session_vnc->pipewire_stream = grd_vnc_pipewire_stream_new (session_vnc, + pipewire_node_id, + virtual_monitor, + &error); + if (!session_vnc->pipewire_stream) + { + g_warning ("Failed to establish PipeWire stream: %s", error->message); + return; + } + + g_signal_connect (session_vnc->pipewire_stream, "closed", + G_CALLBACK (on_pipewire_stream_closed), + session_vnc); + + if (!session_vnc->source) + grd_session_vnc_attach_source (session_vnc); +} + +static void +grd_session_vnc_on_stream_created (GrdSession *session, + uint32_t stream_id, + GrdStream *stream) +{ + GrdSessionVnc *session_vnc = GRD_SESSION_VNC (session); + + g_assert (!session_vnc->stream); + + session_vnc->stream = stream; + g_signal_connect (stream, "ready", G_CALLBACK (on_stream_ready), + session_vnc); +} + +static void +grd_session_vnc_init (GrdSessionVnc *session_vnc) +{ + session_vnc->pressed_keys = g_hash_table_new (NULL, NULL); + + g_signal_connect (session_vnc, "started", + G_CALLBACK (on_remote_desktop_session_started), NULL); +} + +static void +grd_session_vnc_class_init (GrdSessionVncClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GrdSessionClass *session_class = GRD_SESSION_CLASS (klass); + + object_class->dispose = grd_session_vnc_dispose; + + session_class->stop = grd_session_vnc_stop; + session_class->on_stream_created = grd_session_vnc_on_stream_created; +} diff --git a/grd-session-vnc.h b/grd-session-vnc.h new file mode 100644 index 0000000..8fc7185 --- /dev/null +++ b/grd-session-vnc.h @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2015 Red Hat Inc. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + * Written by: + * Jonas Ådahl + */ + +#pragma once + +#include +#include +#include + +#include "grd-session.h" +#include "grd-types.h" +#include "grd-vnc-monitor-config.h" + +#define GRD_TYPE_SESSION_VNC (grd_session_vnc_get_type ()) +G_DECLARE_FINAL_TYPE (GrdSessionVnc, + grd_session_vnc, + GRD, SESSION_VNC, + GrdSession) + +GrdSessionVnc *grd_session_vnc_new (GrdVncServer *vnc_server, + GSocketConnection *connection); + +void grd_session_vnc_queue_resize_framebuffer (GrdSessionVnc *session_vnc, + int width, + int height); + +void grd_session_vnc_take_buffer (GrdSessionVnc *session_vnc, + void *data); + +void grd_session_vnc_flush (GrdSessionVnc *session_vnc); + +void grd_session_vnc_set_cursor (GrdSessionVnc *session_vnc, + rfbCursorPtr rfb_cursor); + +void grd_session_vnc_move_cursor (GrdSessionVnc *session_vnc, + int x, + int y); + +void grd_session_vnc_set_client_clipboard_text (GrdSessionVnc *session_vnc, + char *text, + int text_length); + +int grd_session_vnc_get_stride_for_width (GrdSessionVnc *session_vnc, + int width); + +gboolean grd_session_vnc_is_client_gone (GrdSessionVnc *session_vnc); diff --git a/grd-session.c b/grd-session.c new file mode 100644 index 0000000..c2a8682 --- /dev/null +++ b/grd-session.c @@ -0,0 +1,1989 @@ +/* + * Copyright (C) 2015 Red Hat Inc. + * Copyright (C) 2020-2021 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + * Written by: + * Jonas Ådahl + */ + +#include "config.h" + +#include "grd-session.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "grd-clipboard.h" +#include "grd-dbus-mutter-remote-desktop.h" +#include "grd-context.h" +#include "grd-private.h" +#include "grd-stream.h" +#include "grd-utils.h" + +enum +{ + PROP_0, + + PROP_CONTEXT, +}; + +enum +{ + READY, + STARTED, + STOPPED, + TOUCH_DEVICE_ADDED, + TOUCH_DEVICE_REMOVED, + + N_SIGNALS +}; + +static guint signals[N_SIGNALS]; + +typedef enum _EvdevButtonType +{ + EVDEV_BUTTON_TYPE_NONE, + EVDEV_BUTTON_TYPE_KEY, + EVDEV_BUTTON_TYPE_BUTTON, +} EvdevButtonType; + +typedef enum _ScreenCastType +{ + SCREEN_CAST_TYPE_NONE, + SCREEN_CAST_TYPE_MONITOR, + SCREEN_CAST_TYPE_WINDOW, + SCREEN_CAST_TYPE_AREA, + SCREEN_CAST_TYPE_VIRTUAL, +} ScreenCastType; + +typedef struct _AsyncDBusRecordCallContext +{ + GrdSession *session; + uint32_t stream_id; + ScreenCastType screen_cast_type; +} AsyncDBusRecordCallContext; + +typedef struct _GrdRegion +{ + struct ei_device *ei_device; + struct ei_region *ei_region; +} GrdRegion; + +struct _GrdTouchContact +{ + struct ei_touch *ei_touch_contact; +}; + +typedef struct _GrdEiPing +{ + GTask *task; + struct ei_ping *ping; +} GrdEiPing; + +typedef struct _GrdSessionPrivate +{ + GrdContext *context; + + GrdDBusMutterRemoteDesktopSession *remote_desktop_session; + GrdDBusMutterScreenCastSession *screen_cast_session; + + gboolean is_ready; + + GrdClipboard *clipboard; + + struct ei *ei; + struct ei_seat *ei_seat; + struct ei_device *ei_pointer; + struct ei_device *ei_abs_pointer; + struct ei_device *ei_keyboard; + struct ei_device *ei_touch; + uint32_t ei_sequence; + GSource *ei_source; + + GHashTable *abs_pointer_regions; + GHashTable *touch_regions; + + struct xkb_context *xkb_context; + struct xkb_keymap *xkb_keymap; + struct xkb_state *xkb_state; + + GCancellable *cancellable; + + gboolean started; + + gboolean locked_modifier_valid; + gboolean caps_lock_state; + gboolean num_lock_state; + gulong caps_lock_state_changed_id; + gulong num_lock_state_changed_id; + + GHashTable *pings; +} GrdSessionPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (GrdSession, grd_session, G_TYPE_OBJECT) + +static void +finish_and_free_ping (GrdEiPing *ping); + +GrdContext * +grd_session_get_context (GrdSession *session) +{ + GrdSessionPrivate *priv = grd_session_get_instance_private (session); + + return priv->context; +} + +static void +clear_ei (GrdSession *session) +{ + GrdSessionPrivate *priv = grd_session_get_instance_private (session); + + g_clear_pointer (&priv->xkb_state, xkb_state_unref); + g_clear_pointer (&priv->xkb_keymap, xkb_keymap_unref); + g_clear_pointer (&priv->xkb_context, xkb_context_unref); + + g_clear_pointer (&priv->touch_regions, g_hash_table_unref); + g_clear_pointer (&priv->abs_pointer_regions, g_hash_table_unref); + + g_clear_pointer (&priv->ei_touch, ei_device_unref); + g_clear_pointer (&priv->ei_keyboard, ei_device_unref); + g_clear_pointer (&priv->ei_abs_pointer, ei_device_unref); + g_clear_pointer (&priv->ei_pointer, ei_device_unref); + g_clear_pointer (&priv->ei_seat, ei_seat_unref); + g_clear_pointer (&priv->ei_source, g_source_destroy); + g_clear_pointer (&priv->ei, ei_unref); +} + +static void +clear_session (GrdSession *session) +{ + GrdSessionPrivate *priv = grd_session_get_instance_private (session); + + clear_ei (session); + + g_clear_signal_handler (&priv->caps_lock_state_changed_id, + priv->remote_desktop_session); + g_clear_signal_handler (&priv->num_lock_state_changed_id, + priv->remote_desktop_session); + + g_clear_object (&priv->remote_desktop_session); + g_clear_object (&priv->screen_cast_session); +} + +void +grd_session_stop (GrdSession *session) +{ + GrdSessionPrivate *priv = grd_session_get_instance_private (session); + + if (priv->cancellable && g_cancellable_is_cancelled (priv->cancellable)) + return; + + GRD_SESSION_GET_CLASS (session)->stop (session); + + clear_ei (session); + + if (priv->remote_desktop_session && priv->started) + { + GrdDBusMutterRemoteDesktopSession *proxy = priv->remote_desktop_session; + g_autoptr (GError) error = NULL; + + if (!grd_dbus_mutter_remote_desktop_session_call_stop_sync (proxy, NULL, &error)) + { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CLOSED) && + !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_TIMED_OUT)) + g_warning ("Failed to stop: %s", error->message); + } + } + + if (priv->cancellable) + g_cancellable_cancel (priv->cancellable); + + clear_session (session); + + g_signal_emit (session, signals[STOPPED], 0); +} + +gboolean +grd_session_flush_input_finish (GrdSession *session, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +void +grd_session_flush_input_async (GrdSession *session, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GrdSessionPrivate *priv = grd_session_get_instance_private (session); + GrdEiPing *ping; + + g_assert (priv->is_ready); + + ping = g_new0 (GrdEiPing, 1); + ping->task = g_task_new (session, + cancellable, + callback, + user_data); + ping->ping = ei_new_ping (priv->ei); + ei_ping (ping->ping); + + g_hash_table_insert (priv->pings, ping->ping, ping); +} + +static void +on_screen_cast_stream_start_finished (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + GrdDBusMutterScreenCastStream *stream_proxy = GRD_DBUS_MUTTER_SCREEN_CAST_STREAM (object); + g_autoptr (GError) error = NULL; + + if (!grd_dbus_mutter_screen_cast_stream_call_start_finish (stream_proxy, + result, + &error)) + { + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return; + + g_warning ("Failed to start screen cast stream: %s", error->message); + grd_session_stop (GRD_SESSION (user_data)); + return; + } +} + +static void +on_screen_cast_stream_proxy_acquired (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + g_autofree AsyncDBusRecordCallContext *async_context = user_data; + g_autoptr (GrdDBusMutterScreenCastStream) stream_proxy = NULL; + GrdSession *session; + GrdSessionPrivate *priv; + GrdSessionClass *klass; + g_autoptr (GError) error = NULL; + GrdStream *stream; + + stream_proxy = grd_dbus_mutter_screen_cast_stream_proxy_new_finish (result, &error); + if (!stream_proxy) + { + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return; + + g_warning ("Failed to acquire stream proxy: %s", error->message); + grd_session_stop (async_context->session); + return; + } + + session = async_context->session; + priv = grd_session_get_instance_private (session); + klass = GRD_SESSION_GET_CLASS (session); + + stream = grd_stream_new (async_context->stream_id, stream_proxy, + priv->cancellable, &error); + if (!stream) + { + g_warning ("Failed to create stream: %s", error->message); + grd_session_stop (async_context->session); + return; + } + + klass->on_stream_created (session, async_context->stream_id, stream); + + grd_dbus_mutter_screen_cast_stream_call_start (stream_proxy, + priv->cancellable, + on_screen_cast_stream_start_finished, + session); +} + +static const char * +screen_cast_type_to_string (ScreenCastType type) +{ + switch (type) + { + case SCREEN_CAST_TYPE_NONE: + return "none"; + case SCREEN_CAST_TYPE_MONITOR: + return "monitor"; + case SCREEN_CAST_TYPE_WINDOW: + return "window"; + case SCREEN_CAST_TYPE_AREA: + return "area"; + case SCREEN_CAST_TYPE_VIRTUAL: + return "virtual"; + } + + g_assert_not_reached (); +} + +static void +on_record_finished (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + GrdDBusMutterScreenCastSession *proxy = GRD_DBUS_MUTTER_SCREEN_CAST_SESSION (object); + g_autofree AsyncDBusRecordCallContext *async_context = user_data; + ScreenCastType screen_cast_type = async_context->screen_cast_type; + GrdSession *session; + GrdSessionPrivate *priv; + GDBusConnection *connection; + g_autofree char *stream_path = NULL; + g_autoptr (GError) error = NULL; + gboolean retval = FALSE; + + switch (screen_cast_type) + { + case SCREEN_CAST_TYPE_NONE: + g_assert_not_reached (); + break; + case SCREEN_CAST_TYPE_MONITOR: + retval = grd_dbus_mutter_screen_cast_session_call_record_monitor_finish ( + proxy, &stream_path, result, &error); + break; + case SCREEN_CAST_TYPE_WINDOW: + case SCREEN_CAST_TYPE_AREA: + g_assert_not_reached (); + break; + case SCREEN_CAST_TYPE_VIRTUAL: + retval = grd_dbus_mutter_screen_cast_session_call_record_virtual_finish ( + proxy, &stream_path, result, &error); + break; + } + + if (!retval) + { + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return; + + g_warning ("Failed to record %s: %s", + screen_cast_type_to_string (screen_cast_type), error->message); + grd_session_stop (async_context->session); + return; + } + + session = async_context->session; + priv = grd_session_get_instance_private (session); + connection = g_dbus_proxy_get_connection (G_DBUS_PROXY (proxy)); + + grd_dbus_mutter_screen_cast_stream_proxy_new (connection, + G_DBUS_PROXY_FLAGS_NONE, + MUTTER_SCREEN_CAST_BUS_NAME, + stream_path, + priv->cancellable, + on_screen_cast_stream_proxy_acquired, + g_steal_pointer (&async_context)); +} + +void +grd_session_record_monitor (GrdSession *session, + uint32_t stream_id, + const char *connector, + GrdScreenCastCursorMode cursor_mode) +{ + GrdSessionPrivate *priv = grd_session_get_instance_private (session); + GrdSessionClass *klass = GRD_SESSION_GET_CLASS (session); + GVariantBuilder properties_builder; + AsyncDBusRecordCallContext *async_context; + + g_assert (klass->on_stream_created); + + async_context = g_malloc0 (sizeof (AsyncDBusRecordCallContext)); + async_context->session = session; + async_context->stream_id = stream_id; + async_context->screen_cast_type = SCREEN_CAST_TYPE_MONITOR; + + g_variant_builder_init (&properties_builder, G_VARIANT_TYPE ("a{sv}")); + g_variant_builder_add (&properties_builder, "{sv}", + "cursor-mode", g_variant_new_uint32 (cursor_mode)); + + grd_dbus_mutter_screen_cast_session_call_record_monitor (priv->screen_cast_session, + connector ? connector : "", + g_variant_builder_end (&properties_builder), + priv->cancellable, + on_record_finished, + async_context); +} + +void +grd_session_record_virtual (GrdSession *session, + uint32_t stream_id, + GrdScreenCastCursorMode cursor_mode, + gboolean is_platform) +{ + GrdSessionPrivate *priv = grd_session_get_instance_private (session); + GrdSessionClass *klass = GRD_SESSION_GET_CLASS (session); + GVariantBuilder properties_builder; + AsyncDBusRecordCallContext *async_context; + + g_assert (klass->on_stream_created); + + async_context = g_malloc0 (sizeof (AsyncDBusRecordCallContext)); + async_context->session = session; + async_context->stream_id = stream_id; + async_context->screen_cast_type = SCREEN_CAST_TYPE_VIRTUAL; + + g_variant_builder_init (&properties_builder, G_VARIANT_TYPE ("a{sv}")); + g_variant_builder_add (&properties_builder, "{sv}", + "cursor-mode", g_variant_new_uint32 (cursor_mode)); + g_variant_builder_add (&properties_builder, "{sv}", + "is-platform", g_variant_new_boolean (is_platform)); + + grd_dbus_mutter_screen_cast_session_call_record_virtual (priv->screen_cast_session, + g_variant_builder_end (&properties_builder), + priv->cancellable, + on_record_finished, + async_context); +} + +void +grd_session_notify_keyboard_keycode (GrdSession *session, + uint32_t keycode, + GrdKeyState state) +{ + GrdSessionPrivate *priv = grd_session_get_instance_private (session); + + if (!priv->ei_keyboard) + return; + + ei_device_keyboard_key (priv->ei_keyboard, keycode, state); + ei_device_frame (priv->ei_keyboard, g_get_monotonic_time ()); +} + +static gboolean +pick_keycode_for_keysym_in_current_group (GrdSession *session, + uint32_t keysym, + uint32_t *keycode_out, + uint32_t *level_out) +{ + GrdSessionPrivate *priv = grd_session_get_instance_private (session); + struct xkb_keymap *xkb_keymap; + struct xkb_state *xkb_state; + uint32_t keycode, layout; + xkb_keycode_t min_keycode, max_keycode; + + g_assert (keycode_out); + + xkb_keymap = priv->xkb_keymap; + xkb_state = priv->xkb_state; + + layout = xkb_state_serialize_layout (xkb_state, XKB_STATE_LAYOUT_EFFECTIVE); + min_keycode = xkb_keymap_min_keycode (xkb_keymap); + max_keycode = xkb_keymap_max_keycode (xkb_keymap); + for (keycode = min_keycode; keycode < max_keycode; keycode++) + { + int num_levels, level; + + num_levels = xkb_keymap_num_levels_for_key (xkb_keymap, keycode, layout); + for (level = 0; level < num_levels; level++) + { + const xkb_keysym_t *syms; + int num_syms, sym; + + num_syms = xkb_keymap_key_get_syms_by_level (xkb_keymap, keycode, + layout, level, &syms); + for (sym = 0; sym < num_syms; sym++) + { + if (syms[sym] == keysym) + { + *keycode_out = keycode; + if (level_out) + *level_out = level; + return TRUE; + } + } + } + } + + return FALSE; +} + +static uint32_t +xkb_keycode_to_evdev (uint32_t xkb_keycode) +{ + return xkb_keycode - 8; +} + +static void +apply_level_modifiers (GrdSession *session, + uint64_t time_us, + uint32_t level, + uint32_t key_state) +{ + GrdSessionPrivate *priv = grd_session_get_instance_private (session); + uint32_t keysym, keycode, evcode; + + if (level == 0) + return; + + if (level == 1) + { + keysym = XKB_KEY_Shift_L; + } + else if (level == 2) + { + keysym = XKB_KEY_ISO_Level3_Shift; + } + else + { + g_warning ("Unhandled level: %d", level); + return; + } + + if (!pick_keycode_for_keysym_in_current_group (session, keysym, + &keycode, NULL)) + return; + + evcode = xkb_keycode_to_evdev (keycode); + + ei_device_keyboard_key (priv->ei_keyboard, evcode, key_state); + ei_device_frame (priv->ei_keyboard, time_us); +} + +static EvdevButtonType +get_button_type (uint16_t code) +{ + switch (code) + { + case BTN_TOOL_PEN: + case BTN_TOOL_RUBBER: + case BTN_TOOL_BRUSH: + case BTN_TOOL_PENCIL: + case BTN_TOOL_AIRBRUSH: + case BTN_TOOL_MOUSE: + case BTN_TOOL_LENS: + case BTN_TOOL_QUINTTAP: + case BTN_TOOL_DOUBLETAP: + case BTN_TOOL_TRIPLETAP: + case BTN_TOOL_QUADTAP: + case BTN_TOOL_FINGER: + case BTN_TOUCH: + return EVDEV_BUTTON_TYPE_NONE; + } + + if (code >= KEY_ESC && code <= KEY_MICMUTE) + return EVDEV_BUTTON_TYPE_KEY; + if (code >= BTN_MISC && code <= BTN_GEAR_UP) + return EVDEV_BUTTON_TYPE_BUTTON; + if (code >= KEY_OK && code <= KEY_LIGHTS_TOGGLE) + return EVDEV_BUTTON_TYPE_KEY; + if (code >= BTN_DPAD_UP && code <= BTN_DPAD_RIGHT) + return EVDEV_BUTTON_TYPE_BUTTON; + if (code >= KEY_ALS_TOGGLE && code <= KEY_KBDINPUTASSIST_CANCEL) + return EVDEV_BUTTON_TYPE_KEY; + if (code >= BTN_TRIGGER_HAPPY && code <= BTN_TRIGGER_HAPPY40) + return EVDEV_BUTTON_TYPE_BUTTON; + return EVDEV_BUTTON_TYPE_NONE; +} + +void +grd_session_notify_keyboard_keysym (GrdSession *session, + uint32_t keysym, + GrdKeyState state) +{ + GrdSessionPrivate *priv = grd_session_get_instance_private (session); + int64_t now_us; + uint32_t keycode = 0, level = 0, evcode = 0; + + if (!priv->xkb_state || !priv->ei_keyboard) + return; + + now_us = g_get_monotonic_time (); + + if (!pick_keycode_for_keysym_in_current_group (session, + keysym, + &keycode, &level)) + { + g_warning ("No keycode found for keyval %x in current group", keysym); + return; + } + + evcode = xkb_keycode_to_evdev (keycode); + if (get_button_type (evcode) != EVDEV_BUTTON_TYPE_KEY) + { + g_warning ("Unknown/invalid key 0x%x pressed", evcode); + return; + } + + if (state) + apply_level_modifiers (session, now_us, level, state); + + ei_device_keyboard_key (priv->ei_keyboard, evcode, state); + ei_device_frame (priv->ei_keyboard, now_us); + + if (!state) + apply_level_modifiers (session, now_us, level, state); +} + +void +grd_session_notify_pointer_button (GrdSession *session, + int32_t button, + GrdButtonState state) +{ + GrdSessionPrivate *priv = grd_session_get_instance_private (session); + + if (!priv->ei_abs_pointer) + return; + + ei_device_button_button (priv->ei_abs_pointer, button, state); + ei_device_frame (priv->ei_abs_pointer, g_get_monotonic_time ()); +} + +void +grd_session_notify_pointer_axis (GrdSession *session, + double dx, + double dy, + GrdPointerAxisFlags flags) +{ + GrdSessionPrivate *priv = grd_session_get_instance_private (session); + + if (!priv->ei_abs_pointer) + return; + + if (flags & GRD_POINTER_AXIS_FLAGS_FINISH) + ei_device_scroll_stop (priv->ei_abs_pointer, true, true); + else + ei_device_scroll_delta (priv->ei_abs_pointer, dx, dy); + + ei_device_frame (priv->ei_abs_pointer, g_get_monotonic_time ()); +} + +void +grd_session_notify_pointer_axis_discrete (GrdSession *session, + GrdPointerAxis axis, + int steps) +{ + GrdSessionPrivate *priv = grd_session_get_instance_private (session); + + if (!priv->ei_abs_pointer) + return; + + ei_device_scroll_discrete (priv->ei_abs_pointer, + axis == GRD_POINTER_AXIS_HORIZONTAL ? steps * 120 : 0, + axis == GRD_POINTER_AXIS_VERTICAL ? steps * 120 : 0); + ei_device_frame (priv->ei_abs_pointer, g_get_monotonic_time ()); +} + +void +grd_session_notify_pointer_motion (GrdSession *session, + double dx, + double dy) +{ + GrdSessionPrivate *priv = grd_session_get_instance_private (session); + + if (!priv->ei_pointer) + return; + + ei_device_pointer_motion (priv->ei_pointer, dx, dy); + ei_device_frame (priv->ei_pointer, g_get_monotonic_time ()); +} + +static gboolean +transform_position (GrdSession *session, + GHashTable *regions, + GrdStream *stream, + const GrdEventMotionAbs *motion_abs, + struct ei_device **ei_device, + double *x, + double *y) +{ + GrdRegion *region; + double scale_x; + double scale_y; + double scaled_x; + double scaled_y; + + g_assert (motion_abs->input_rect_width > 0); + g_assert (motion_abs->input_rect_height > 0); + + region = g_hash_table_lookup (regions, grd_stream_get_mapping_id (stream)); + if (!region) + return FALSE; + + scale_x = ((double) motion_abs->input_rect_width) / + ei_region_get_width (region->ei_region); + scale_y = ((double) motion_abs->input_rect_height) / + ei_region_get_height (region->ei_region); + scaled_x = motion_abs->x / scale_x; + scaled_y = motion_abs->y / scale_y; + + *ei_device = region->ei_device; + *x = ei_region_get_x (region->ei_region) + scaled_x; + *y = ei_region_get_y (region->ei_region) + scaled_y; + + return TRUE; +} + +void +grd_session_notify_pointer_motion_absolute (GrdSession *session, + GrdStream *stream, + const GrdEventMotionAbs *motion_abs) +{ + GrdSessionPrivate *priv = grd_session_get_instance_private (session); + struct ei_device *ei_device = NULL; + double x = 0; + double y = 0; + + if (!transform_position (session, priv->abs_pointer_regions, + stream, motion_abs, &ei_device, &x, &y)) + return; + + ei_device_pointer_motion_absolute (ei_device, x, y); + ei_device_frame (ei_device, g_get_monotonic_time ()); +} + +gboolean +grd_session_has_touch_device (GrdSession *session) +{ + GrdSessionPrivate *priv = grd_session_get_instance_private (session); + + return !!priv->ei_touch; +} + +GrdTouchContact * +grd_session_acquire_touch_contact (GrdSession *session) +{ + GrdSessionPrivate *priv = grd_session_get_instance_private (session); + GrdTouchContact *touch_contact; + + g_assert (priv->ei_touch); + + touch_contact = g_new0 (GrdTouchContact, 1); + touch_contact->ei_touch_contact = ei_device_touch_new (priv->ei_touch); + + return touch_contact; +} + +void +grd_session_release_touch_contact (GrdSession *session, + GrdTouchContact *touch_contact) +{ + g_clear_pointer (&touch_contact->ei_touch_contact, ei_touch_unref); + + g_free (touch_contact); +} + +void +grd_session_notify_touch_down (GrdSession *session, + const GrdTouchContact *touch_contact, + GrdStream *stream, + const GrdEventMotionAbs *motion_abs) +{ + GrdSessionPrivate *priv = grd_session_get_instance_private (session); + struct ei_device *ei_device = NULL; + double x = 0; + double y = 0; + + if (!transform_position (session, priv->touch_regions, stream, motion_abs, + &ei_device, &x, &y)) + return; + + ei_touch_down (touch_contact->ei_touch_contact, x, y); +} + +void +grd_session_notify_touch_motion (GrdSession *session, + const GrdTouchContact *touch_contact, + GrdStream *stream, + const GrdEventMotionAbs *motion_abs) +{ + GrdSessionPrivate *priv = grd_session_get_instance_private (session); + struct ei_device *ei_device = NULL; + double x = 0; + double y = 0; + + if (!transform_position (session, priv->touch_regions, stream, motion_abs, + &ei_device, &x, &y)) + return; + + ei_touch_motion (touch_contact->ei_touch_contact, x, y); +} + +void +grd_session_notify_touch_up (GrdSession *session, + GrdTouchContact *touch_contact) +{ + ei_touch_up (touch_contact->ei_touch_contact); +} + +void +grd_session_notify_touch_cancel (GrdSession *session, + GrdTouchContact *touch_contact) +{ + ei_touch_cancel (touch_contact->ei_touch_contact); +} + +void +grd_session_notify_touch_device_frame (GrdSession *session) +{ + GrdSessionPrivate *priv = grd_session_get_instance_private (session); + + g_assert (priv->ei_touch); + + ei_device_frame (priv->ei_touch, g_get_monotonic_time ()); +} + +static GVariant * +serialize_mime_type_tables (GList *mime_type_tables) +{ + GVariantBuilder builder; + GList *l; + + g_variant_builder_init (&builder, G_VARIANT_TYPE ("as")); + for (l = mime_type_tables; l; l = l->next) + { + GrdMimeTypeTable *mime_type_table = l->data; + GrdMimeType mime_type; + const char *mime_type_string; + + mime_type = mime_type_table->mime_type; + mime_type_string = grd_mime_type_to_string (mime_type); + g_variant_builder_add (&builder, "s", mime_type_string); + } + + return g_variant_builder_end (&builder); +} + +static GVariant * +serialize_clipboard_options (GList *mime_type_tables) +{ + GVariantBuilder builder; + + g_variant_builder_init (&builder, G_VARIANT_TYPE ("a{sv}")); + if (mime_type_tables) + { + g_variant_builder_add (&builder, "{sv}", "mime-types", + serialize_mime_type_tables (mime_type_tables)); + } + + return g_variant_builder_end (&builder); +} + +gboolean +grd_session_enable_clipboard (GrdSession *session, + GrdClipboard *clipboard, + GList *mime_type_tables) +{ + GrdSessionPrivate *priv = grd_session_get_instance_private (session); + GVariant *options_variant; + g_autoptr (GError) error = NULL; + + if (!priv->remote_desktop_session) + return FALSE; + + options_variant = serialize_clipboard_options (mime_type_tables); + if (!grd_dbus_mutter_remote_desktop_session_call_enable_clipboard_sync ( + priv->remote_desktop_session, options_variant, NULL, &error)) + { + g_warning ("Failed to enable clipboard: %s", error->message); + return FALSE; + } + priv->clipboard = clipboard; + + return TRUE; +} + +void +grd_session_disable_clipboard (GrdSession *session) +{ + GrdSessionPrivate *priv = grd_session_get_instance_private (session); + + priv->clipboard = NULL; + if (!priv->remote_desktop_session) + return; + + grd_dbus_mutter_remote_desktop_session_call_disable_clipboard ( + priv->remote_desktop_session, NULL, NULL, NULL); +} + +void +grd_session_set_selection (GrdSession *session, + GList *mime_type_tables) +{ + GrdSessionPrivate *priv = grd_session_get_instance_private (session); + GVariant *options_variant; + g_autoptr (GError) error = NULL; + + options_variant = serialize_clipboard_options (mime_type_tables); + + if (!grd_dbus_mutter_remote_desktop_session_call_set_selection_sync ( + priv->remote_desktop_session, options_variant, NULL, &error)) + g_warning ("Failed to set selection: %s", error->message); +} + +void +grd_session_selection_write (GrdSession *session, + unsigned int serial, + const uint8_t *data, + uint32_t size) +{ + GrdSessionPrivate *priv = grd_session_get_instance_private (session); + g_autoptr (GError) error = NULL; + g_autoptr (GVariant) fd_variant = NULL; + g_autoptr (GUnixFDList) fd_list = NULL; + int fd_idx = -1; + int fd; + + if (!data || !size) + { + grd_dbus_mutter_remote_desktop_session_call_selection_write_done ( + priv->remote_desktop_session, serial, FALSE, NULL, NULL, NULL); + return; + } + + if (!grd_dbus_mutter_remote_desktop_session_call_selection_write_sync ( + priv->remote_desktop_session, serial, NULL, &fd_variant, &fd_list, + NULL, &error)) + { + g_warning ("Failed to write selection for serial %u: %s", + serial, error->message); + return; + } + + g_variant_get (fd_variant, "h", &fd_idx); + if (!G_IS_UNIX_FD_LIST (fd_list) || + fd_idx < 0 || fd_idx >= g_unix_fd_list_get_length (fd_list)) + { + g_warning ("Failed to acquire file descriptor for serial %u: Invalid " + "file descriptor list sent by display server", serial); + return; + } + + fd = g_unix_fd_list_get (fd_list, fd_idx, &error); + if (fd == -1) + { + g_warning ("Failed to acquire file descriptor for serial %u: %s", + serial, error->message); + return; + } + + if (write (fd, data, size) < 0) + { + grd_dbus_mutter_remote_desktop_session_call_selection_write_done ( + priv->remote_desktop_session, serial, FALSE, NULL, NULL, NULL); + + close (fd); + return; + } + + grd_dbus_mutter_remote_desktop_session_call_selection_write_done ( + priv->remote_desktop_session, serial, TRUE, NULL, NULL, NULL); + + close (fd); +} + +int +grd_session_selection_read (GrdSession *session, + GrdMimeType mime_type) +{ + GrdSessionPrivate *priv = grd_session_get_instance_private (session); + g_autoptr (GError) error = NULL; + g_autoptr (GVariant) fd_variant = NULL; + g_autoptr (GUnixFDList) fd_list = NULL; + int fd_idx = -1; + int fd; + const char *mime_type_string; + + mime_type_string = grd_mime_type_to_string (mime_type); + if (!grd_dbus_mutter_remote_desktop_session_call_selection_read_sync ( + priv->remote_desktop_session, mime_type_string, NULL, &fd_variant, + &fd_list, NULL, &error)) + { + g_warning ("Failed to read selection: %s", error->message); + return -1; + } + + g_variant_get (fd_variant, "h", &fd_idx); + if (!G_IS_UNIX_FD_LIST (fd_list) || + fd_idx < 0 || fd_idx >= g_unix_fd_list_get_length (fd_list)) + { + g_warning ("Failed to acquire file descriptor: Invalid file descriptor " + "list sent by display server"); + return -1; + } + + fd = g_unix_fd_list_get (fd_list, fd_idx, &error); + if (fd == -1) + { + g_warning ("Failed to acquire file descriptor: %s", error->message); + return -1; + } + + return fd; +} + +static void +maybe_emit_started_signal (GrdSession *session) +{ + GrdSessionPrivate *priv = grd_session_get_instance_private (session); + + if (!priv->is_ready || + !priv->started) + return; + + g_signal_emit (session, signals[STARTED], 0); +} + +static void +on_session_start_finished (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + GrdDBusMutterRemoteDesktopSession *proxy; + GrdSession *session; + GrdSessionPrivate *priv; + g_autoptr (GError) error = NULL; + + proxy = GRD_DBUS_MUTTER_REMOTE_DESKTOP_SESSION (object); + if (!grd_dbus_mutter_remote_desktop_session_call_start_finish (proxy, + result, + &error)) + { + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return; + + g_warning ("Failed to start session: %s", error->message); + grd_session_stop (GRD_SESSION (user_data)); + return; + } + + session = GRD_SESSION (user_data); + priv = grd_session_get_instance_private (session); + + priv->started = TRUE; + maybe_emit_started_signal (session); +} + +static void +start_session (GrdSession *session) +{ + GrdSessionPrivate *priv = grd_session_get_instance_private (session); + GrdDBusMutterRemoteDesktopSession *proxy = priv->remote_desktop_session; + + grd_dbus_mutter_remote_desktop_session_call_start (proxy, + priv->cancellable, + on_session_start_finished, + session); +} + +static void +on_screen_cast_session_proxy_acquired (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + GrdDBusMutterScreenCastSession *session_proxy; + GrdSession *session; + GrdSessionPrivate *priv; + g_autoptr (GError) error = NULL; + + session_proxy = + grd_dbus_mutter_screen_cast_session_proxy_new_finish (result, &error); + if (!session_proxy) + { + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return; + + g_warning ("Failed to acquire screen cast session proxy: %s\n", + error->message); + grd_session_stop (GRD_SESSION (user_data)); + return; + } + + session = GRD_SESSION (user_data); + priv = grd_session_get_instance_private (session); + + priv->screen_cast_session = session_proxy; + + start_session (session); +} + +static void +on_screen_cast_session_created (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + GrdDBusMutterScreenCast *screen_cast_proxy; + GrdSession *session; + GrdSessionPrivate *priv; + GDBusConnection *connection; + g_autofree char *session_path = NULL; + g_autoptr (GError) error = NULL; + + screen_cast_proxy = GRD_DBUS_MUTTER_SCREEN_CAST (source_object); + if (!grd_dbus_mutter_screen_cast_call_create_session_finish (screen_cast_proxy, + &session_path, + res, + &error)) + { + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return; + + g_warning ("Failed to start screen cast session: %s\n", error->message); + grd_session_stop (GRD_SESSION (user_data)); + return; + } + + session = GRD_SESSION (user_data); + priv = grd_session_get_instance_private (session); + connection = g_dbus_proxy_get_connection (G_DBUS_PROXY (screen_cast_proxy)); + + grd_dbus_mutter_screen_cast_session_proxy_new (connection, + G_DBUS_PROXY_FLAGS_NONE, + MUTTER_SCREEN_CAST_BUS_NAME, + session_path, + priv->cancellable, + on_screen_cast_session_proxy_acquired, + session); +} + +static void +on_remote_desktop_session_closed (GrdDBusMutterRemoteDesktopSession *session_proxy, + GrdSession *session) +{ + g_debug ("Remote desktop session closed, stopping session"); + + clear_session (session); + grd_session_stop (session); +} + +static void +on_remote_desktop_session_selection_owner_changed (GrdDBusMutterRemoteDesktopSession *session_proxy, + GVariant *options_variant, + GrdSession *session) +{ + GrdSessionPrivate *priv = grd_session_get_instance_private (session); + GVariant *is_owner_variant, *mime_types_variant; + GVariantIter iter; + const char *mime_string; + GrdMimeType mime_type; + GList *mime_type_list = NULL; + + if (!priv->clipboard) + return; + + is_owner_variant = g_variant_lookup_value (options_variant, "session-is-owner", + G_VARIANT_TYPE ("b")); + if (is_owner_variant && g_variant_get_boolean (is_owner_variant)) + return; + + mime_types_variant = g_variant_lookup_value (options_variant, "mime-types", + G_VARIANT_TYPE ("(as)")); + if (!mime_types_variant) + return; + + g_variant_iter_init (&iter, g_variant_get_child_value (mime_types_variant, 0)); + while (g_variant_iter_loop (&iter, "s", &mime_string)) + { + mime_type = grd_mime_type_from_string (mime_string); + if (mime_type != GRD_MIME_TYPE_NONE) + { + mime_type_list = g_list_append (mime_type_list, + GUINT_TO_POINTER (mime_type)); + g_debug ("Clipboard[SelectionOwnerChanged]: Server advertises mime " + "type %s", mime_string); + } + else + { + g_debug ("Clipboard[SelectionOwnerChanged]: Server advertised unknown " + "mime type: %s", mime_string); + } + } + + if (mime_type_list) + grd_clipboard_update_client_mime_type_list (priv->clipboard, mime_type_list); +} + +static void +on_remote_desktop_session_selection_transfer (GrdDBusMutterRemoteDesktopSession *session_proxy, + char *mime_type_string, + unsigned int serial, + GrdSession *session) +{ + GrdSessionPrivate *priv = grd_session_get_instance_private (session); + GrdMimeType mime_type; + + if (!priv->clipboard) + return; + + mime_type = grd_mime_type_from_string (mime_type_string); + if (mime_type == GRD_MIME_TYPE_NONE) + { + grd_dbus_mutter_remote_desktop_session_call_selection_write_done ( + priv->remote_desktop_session, serial, FALSE, NULL, NULL, NULL); + return; + } + + grd_clipboard_request_client_content_for_mime_type (priv->clipboard, + mime_type, serial); +} + +static gboolean +grd_ei_source_prepare (gpointer user_data) +{ + GrdSession *session = GRD_SESSION (user_data); + GrdSessionPrivate *priv = grd_session_get_instance_private (session); + + return !!ei_peek_event (priv->ei); +} + +static gboolean +setup_xkb_keymap (GrdSession *session, + struct ei_keymap *keymap, + GError **error) +{ + GrdSessionPrivate *priv = grd_session_get_instance_private (session); + struct xkb_context *xkb_context = NULL; + struct xkb_keymap *xkb_keymap = NULL; + struct xkb_state *xkb_state = NULL; + size_t keymap_size; + g_autofree char *buf = NULL; + + g_clear_pointer (&priv->xkb_state, xkb_state_unref); + g_clear_pointer (&priv->xkb_keymap, xkb_keymap_unref); + g_clear_pointer (&priv->xkb_context, xkb_context_unref); + + xkb_context = xkb_context_new (XKB_CONTEXT_NO_FLAGS); + if (!xkb_context) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create XKB context"); + goto err; + } + + keymap_size = ei_keymap_get_size (keymap); + buf = g_malloc0 (keymap_size + 1); + while (TRUE) + { + int ret; + + ret = read (ei_keymap_get_fd (keymap), buf, keymap_size); + if (ret > 0) + { + break; + } + else if (ret == 0) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Keyboard layout was empty"); + goto err; + } + else if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK) + { + continue; + } + else + { + g_set_error (error, G_IO_ERROR, g_io_error_from_errno (errno), + "Failed to read layout: %s", g_strerror (errno)); + goto err; + } + } + + xkb_keymap = xkb_keymap_new_from_string (xkb_context, buf, + XKB_KEYMAP_FORMAT_TEXT_V1, + XKB_KEYMAP_COMPILE_NO_FLAGS); + if (!xkb_keymap) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create XKB keymap"); + goto err; + } + + xkb_state = xkb_state_new (xkb_keymap); + if (!xkb_state) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create XKB state"); + goto err; + } + + priv->xkb_context = xkb_context; + priv->xkb_keymap = xkb_keymap; + priv->xkb_state = xkb_state; + return TRUE; + +err: + g_clear_pointer (&xkb_state, xkb_state_unref); + g_clear_pointer (&xkb_keymap, xkb_keymap_unref); + g_clear_pointer (&xkb_context, xkb_context_unref); + return FALSE; +} + +static gboolean +process_keymap (GrdSession *session, + struct ei_device *device, + GError **error) +{ + struct ei_keymap *keymap; + enum ei_keymap_type type; + + keymap = ei_device_keyboard_get_keymap (device); + if (!keymap) + return TRUE; + + type = ei_keymap_get_type (keymap); + switch (type) + { + case EI_KEYMAP_TYPE_XKB: + return setup_xkb_keymap (session, keymap, error); + default: + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Unknown keyboard layout type"); + return FALSE; + } + + return TRUE; +} + +static void +grd_region_free (GrdRegion *region) +{ + ei_region_unref (region->ei_region); + ei_device_unref (region->ei_device); + g_free (region); +} + +static void +maybe_dispose_ei_abs_pointer (GrdSession *session) +{ + GrdSessionPrivate *priv = grd_session_get_instance_private (session); + + g_hash_table_remove_all (priv->abs_pointer_regions); + g_clear_pointer (&priv->ei_abs_pointer, ei_device_unref); +} + +static void +maybe_dispose_ei_touch (GrdSession *session) +{ + GrdSessionPrivate *priv = grd_session_get_instance_private (session); + + g_signal_emit (session, signals[TOUCH_DEVICE_REMOVED], 0); + g_hash_table_remove_all (priv->touch_regions); + g_clear_pointer (&priv->ei_touch, ei_device_unref); +} + +static void +process_regions (GrdSession *session, + struct ei_device *ei_device, + GHashTable *regions) +{ + size_t i = 0; + struct ei_region *ei_region; + + while ((ei_region = ei_device_get_region (ei_device, i++))) + { + const char *mapping_id; + GrdRegion *region; + + mapping_id = ei_region_get_mapping_id (ei_region); + if (!mapping_id) + continue; + + g_debug ("ei: New region: mapping-id: %s, %ux%u (%u, %u)", + mapping_id, + ei_region_get_width (ei_region), ei_region_get_height (ei_region), + ei_region_get_x (ei_region), ei_region_get_y (ei_region)); + + g_assert (ei_region_get_width (ei_region) > 0); + g_assert (ei_region_get_height (ei_region) > 0); + + region = g_new0 (GrdRegion, 1); + region->ei_device = ei_device_ref (ei_device); + region->ei_region = ei_region_ref (ei_region); + g_hash_table_insert (regions, g_strdup (mapping_id), region); + } +} + +static gboolean +grd_ei_source_dispatch (gpointer user_data) +{ + GrdSession *session = GRD_SESSION (user_data); + GrdSessionPrivate *priv = grd_session_get_instance_private (session); + struct ei_event *event; + + ei_dispatch (priv->ei); + + while ((event = ei_get_event (priv->ei))) + { + enum ei_event_type ei_event_type = ei_event_get_type (event); + gboolean handled = TRUE; + + switch (ei_event_type) + { + case EI_EVENT_CONNECT: + if (!priv->is_ready) + { + priv->is_ready = TRUE; + g_signal_emit (session, signals[READY], 0); + maybe_emit_started_signal (session); + } + break; + case EI_EVENT_DISCONNECT: + break; + case EI_EVENT_SEAT_ADDED: + if (priv->ei_seat) + break; + + priv->ei_seat = ei_seat_ref (ei_event_get_seat (event)); + ei_seat_bind_capabilities (priv->ei_seat, + EI_DEVICE_CAP_POINTER, + EI_DEVICE_CAP_KEYBOARD, + EI_DEVICE_CAP_POINTER_ABSOLUTE, + EI_DEVICE_CAP_BUTTON, + EI_DEVICE_CAP_SCROLL, + EI_DEVICE_CAP_TOUCH, + NULL); + break; + case EI_EVENT_SEAT_REMOVED: + if (ei_event_get_seat (event) == priv->ei_seat) + g_clear_pointer (&priv->ei_seat, ei_seat_unref); + break; + case EI_EVENT_DEVICE_ADDED: + { + struct ei_device *device = ei_event_get_device (event); + + if (ei_device_has_capability (device, EI_DEVICE_CAP_KEYBOARD)) + { + g_autoptr (GError) error = NULL; + + g_clear_pointer (&priv->ei_keyboard, ei_device_unref); + priv->ei_keyboard = ei_device_ref (device); + if (!process_keymap (session, priv->ei_keyboard, &error)) + { + g_warning ("Failed to handle keyboard layout: %s", + error->message); + ei_event_unref (event); + grd_session_stop (session); + return G_SOURCE_REMOVE; + } + } + if (ei_device_has_capability (device, EI_DEVICE_CAP_POINTER)) + { + g_clear_pointer (&priv->ei_pointer, ei_device_unref); + priv->ei_pointer = ei_device_ref (device); + } + if (ei_device_has_capability (device, EI_DEVICE_CAP_POINTER_ABSOLUTE)) + { + maybe_dispose_ei_abs_pointer (session); + priv->ei_abs_pointer = ei_device_ref (device); + process_regions (session, device, priv->abs_pointer_regions); + } + if (ei_device_has_capability (device, EI_DEVICE_CAP_TOUCH)) + { + maybe_dispose_ei_touch (session); + priv->ei_touch = ei_device_ref (device); + process_regions (session, device, priv->touch_regions); + } + break; + } + case EI_EVENT_DEVICE_RESUMED: + if (ei_event_get_device (event) == priv->ei_pointer) + ei_device_start_emulating (priv->ei_pointer, ++priv->ei_sequence); + if (ei_event_get_device (event) == priv->ei_abs_pointer) + ei_device_start_emulating (priv->ei_abs_pointer, ++priv->ei_sequence); + if (ei_event_get_device (event) == priv->ei_keyboard) + ei_device_start_emulating (priv->ei_keyboard, ++priv->ei_sequence); + if (ei_event_get_device (event) == priv->ei_touch) + { + ei_device_start_emulating (priv->ei_touch, ++priv->ei_sequence); + g_signal_emit (session, signals[TOUCH_DEVICE_ADDED], 0); + } + break; + case EI_EVENT_DEVICE_PAUSED: + break; + case EI_EVENT_DEVICE_REMOVED: + if (ei_event_get_device (event) == priv->ei_pointer) + g_clear_pointer (&priv->ei_pointer, ei_device_unref); + if (ei_event_get_device (event) == priv->ei_abs_pointer) + maybe_dispose_ei_abs_pointer (session); + if (ei_event_get_device (event) == priv->ei_keyboard) + g_clear_pointer (&priv->ei_keyboard, ei_device_unref); + if (ei_event_get_device (event) == priv->ei_touch) + maybe_dispose_ei_touch (session); + break; + case EI_EVENT_KEYBOARD_MODIFIERS: + if (priv->xkb_keymap) + { + GrdSessionClass *klass = GRD_SESSION_GET_CLASS (session); + uint32_t latched_mods = + ei_event_keyboard_get_xkb_mods_latched (event); + uint32_t locked_mods = + ei_event_keyboard_get_xkb_mods_locked (event); + gboolean caps_lock_state; + gboolean num_lock_state; + + caps_lock_state = + !!((latched_mods | locked_mods) & + (1 << xkb_keymap_mod_get_index (priv->xkb_keymap, + XKB_MOD_NAME_CAPS))); + num_lock_state = + !!((latched_mods | locked_mods) & + (1 << xkb_keymap_mod_get_index (priv->xkb_keymap, + XKB_MOD_NAME_NUM))); + + if (!priv->locked_modifier_valid || + caps_lock_state != priv->caps_lock_state) + { + g_debug ("Caps lock state: %s", + caps_lock_state ? "locked" : "unlocked"); + priv->caps_lock_state = caps_lock_state; + + if (klass->on_caps_lock_state_changed) + { + klass->on_caps_lock_state_changed (session, + caps_lock_state); + } + } + + if (!priv->locked_modifier_valid || + num_lock_state != priv->num_lock_state) + { + g_debug ("Num lock state: %s", + num_lock_state ? "locked" : "unlocked"); + priv->num_lock_state = num_lock_state; + + if (klass->on_num_lock_state_changed) + { + klass->on_num_lock_state_changed (session, + num_lock_state); + } + } + + priv->locked_modifier_valid = TRUE; + } + else + { + g_warning ("Couldn't update caps lock / num lock state, " + "no keymap"); + } + break; + case EI_EVENT_PONG: + { + GrdEiPing *ping = NULL; + + if (g_hash_table_steal_extended (priv->pings, + ei_event_pong_get_ping (event), + NULL, + (gpointer *) &ping)) + finish_and_free_ping (ping); + break; + } + default: + handled = FALSE; + break; + } + + if (handled) + { + g_debug ("ei: Handled event type %s", + ei_event_type_to_string (ei_event_type)); + } + else + { + g_debug ("ei: Didn't handle event type %s", + ei_event_type_to_string (ei_event_type)); + } + ei_event_unref (event); + } + + return G_SOURCE_CONTINUE; +} + +static void +on_eis_connected (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + GrdDBusMutterRemoteDesktopSession *proxy = + GRD_DBUS_MUTTER_REMOTE_DESKTOP_SESSION (object); + g_autoptr (GVariant) fd_variant = NULL; + g_autoptr (GUnixFDList) fd_list = NULL; + GrdSession *session; + GrdSessionPrivate *priv; + int fd_idx = -1; + int fd; + g_autoptr (GError) error = NULL; + const char *remote_desktop_session_id; + GrdDBusMutterScreenCast *screen_cast_proxy; + GVariantBuilder properties_builder; + GVariant *properties_variant; + int ret; + + if (!grd_dbus_mutter_remote_desktop_session_call_connect_to_eis_finish (proxy, + &fd_variant, + &fd_list, + result, + &error)) + { + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return; + + g_warning ("Failed to connect to EIS: %s", + error->message); + grd_session_stop (GRD_SESSION (user_data)); + return; + } + + session = GRD_SESSION (user_data); + priv = grd_session_get_instance_private (session); + + fd_idx = g_variant_get_handle (fd_variant); + if (!G_IS_UNIX_FD_LIST (fd_list) || + fd_idx < 0 || fd_idx >= g_unix_fd_list_get_length (fd_list)) + { + g_warning ("Failed to acquire file descriptor for EI backend: Invalid " + "file descriptor list sent by display server"); + grd_session_stop (GRD_SESSION (user_data)); + return; + } + + fd = g_unix_fd_list_get (fd_list, fd_idx, &error); + + priv->ei = ei_new_sender (session); + ei_configure_name (priv->ei, "gnome-remote-desktop"); + + ret = ei_setup_backend_fd (priv->ei, fd); + if (ret < 0) + { + g_warning ("Failed to setup libei backend: %s", g_strerror (-ret)); + + grd_session_stop (GRD_SESSION (user_data)); + return; + } + + priv->ei_source = grd_create_fd_source (ei_get_fd (priv->ei), + "libei", + grd_ei_source_prepare, + grd_ei_source_dispatch, + session, NULL); + g_source_attach (priv->ei_source, NULL); + g_source_unref (priv->ei_source); + + g_signal_connect (priv->remote_desktop_session, "closed", + G_CALLBACK (on_remote_desktop_session_closed), + session); + g_signal_connect (priv->remote_desktop_session, "selection-owner-changed", + G_CALLBACK (on_remote_desktop_session_selection_owner_changed), + session); + g_signal_connect (priv->remote_desktop_session, "selection-transfer", + G_CALLBACK (on_remote_desktop_session_selection_transfer), + session); + + remote_desktop_session_id = + grd_dbus_mutter_remote_desktop_session_get_session_id (priv->remote_desktop_session); + + g_variant_builder_init (&properties_builder, G_VARIANT_TYPE ("a{sv}")); + g_variant_builder_add (&properties_builder, "{sv}", + "remote-desktop-session-id", + g_variant_new_string (remote_desktop_session_id)); + g_variant_builder_add (&properties_builder, "{sv}", + "disable-animations", + g_variant_new_boolean (TRUE)); + properties_variant = g_variant_builder_end (&properties_builder); + + screen_cast_proxy = grd_context_get_mutter_screen_cast_proxy (priv->context); + grd_dbus_mutter_screen_cast_call_create_session (screen_cast_proxy, + properties_variant, + priv->cancellable, + on_screen_cast_session_created, + session); +} + +static void +on_remote_desktop_session_proxy_acquired (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + GrdDBusMutterRemoteDesktopSession *session_proxy; + GrdSession *session; + GrdSessionPrivate *priv; + g_autoptr (GError) error = NULL; + GVariantBuilder options_builder; + + session_proxy = + grd_dbus_mutter_remote_desktop_session_proxy_new_finish (result, &error); + if (!session_proxy) + { + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return; + + g_warning ("Failed to acquire remote desktop session proxy: %s", + error->message); + grd_session_stop (GRD_SESSION (user_data)); + return; + } + + session = GRD_SESSION (user_data); + priv = grd_session_get_instance_private (session); + + priv->remote_desktop_session = session_proxy; + + g_variant_builder_init (&options_builder, G_VARIANT_TYPE ("a{sv}")); + + grd_dbus_mutter_remote_desktop_session_call_connect_to_eis ( + priv->remote_desktop_session, + g_variant_builder_end (&options_builder), + NULL, + priv->cancellable, + on_eis_connected, + session); +} + +static void +on_remote_desktop_session_created (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + GrdDBusMutterRemoteDesktop *remote_desktop_proxy; + GrdSession *session; + GrdSessionPrivate *priv; + GDBusConnection *connection; + g_autofree char *session_path = NULL; + g_autoptr (GError) error = NULL; + + remote_desktop_proxy = GRD_DBUS_MUTTER_REMOTE_DESKTOP (source_object); + if (!grd_dbus_mutter_remote_desktop_call_create_session_finish (remote_desktop_proxy, + &session_path, + res, + &error)) + { + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return; + + g_warning ("Failed to start remote desktop session: %s\n", error->message); + grd_session_stop (GRD_SESSION (user_data)); + return; + } + + session = GRD_SESSION (user_data); + priv = grd_session_get_instance_private (session); + connection = g_dbus_proxy_get_connection (G_DBUS_PROXY (remote_desktop_proxy)); + + grd_dbus_mutter_remote_desktop_session_proxy_new (connection, + G_DBUS_PROXY_FLAGS_NONE, + MUTTER_REMOTE_DESKTOP_BUS_NAME, + session_path, + priv->cancellable, + on_remote_desktop_session_proxy_acquired, + session); +} + +void +grd_session_start (GrdSession *session) +{ + GrdSessionPrivate *priv = grd_session_get_instance_private (session); + GrdDBusMutterRemoteDesktop *remote_desktop_proxy; + + priv->cancellable = g_cancellable_new (); + + remote_desktop_proxy = grd_context_get_mutter_remote_desktop_proxy (priv->context); + + g_assert (remote_desktop_proxy); + + grd_dbus_mutter_remote_desktop_call_create_session (remote_desktop_proxy, + priv->cancellable, + on_remote_desktop_session_created, + session); +} + +gboolean +grd_session_is_ready (GrdSession *session) +{ + GrdSessionPrivate *priv = grd_session_get_instance_private (session); + + return priv->is_ready; +} + +static void +grd_session_finalize (GObject *object) +{ + GrdSession *session = GRD_SESSION (object); + GrdSessionPrivate *priv = grd_session_get_instance_private (session); + + if (priv->cancellable) + g_assert (g_cancellable_is_cancelled (priv->cancellable)); + g_clear_object (&priv->cancellable); + + g_assert (!priv->ei_source); + + g_clear_pointer (&priv->pings, g_hash_table_unref); + g_clear_pointer (&priv->touch_regions, g_hash_table_unref); + g_clear_pointer (&priv->abs_pointer_regions, g_hash_table_unref); + + g_assert (!priv->xkb_state); + g_assert (!priv->xkb_keymap); + g_assert (!priv->xkb_context); + + g_assert (!priv->ei_touch); + g_assert (!priv->ei_keyboard); + g_assert (!priv->ei_abs_pointer); + g_assert (!priv->ei_pointer); + g_assert (!priv->ei_seat); + g_assert (!priv->ei); + + g_assert (!priv->remote_desktop_session); + g_assert (!priv->screen_cast_session); + + G_OBJECT_CLASS (grd_session_parent_class)->finalize (object); +} + +static void +grd_session_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GrdSession *session = GRD_SESSION (object); + GrdSessionPrivate *priv = grd_session_get_instance_private (session); + + switch (prop_id) + { + case PROP_CONTEXT: + priv->context = g_value_get_object (value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +grd_session_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GrdSession *session = GRD_SESSION (object); + GrdSessionPrivate *priv = grd_session_get_instance_private (session); + + switch (prop_id) + { + case PROP_CONTEXT: + g_value_set_object (value, priv->context); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +grd_ei_ping_free (GrdEiPing *ping) +{ + g_object_unref (ping->task); + g_clear_pointer (&ping->ping, ei_ping_unref); + g_free (ping); +} + +static void +cancel_and_free_ping (gpointer user_data) +{ + GrdEiPing *ping = user_data; + GCancellable *cancellable; + + cancellable = g_task_get_cancellable (ping->task); + if (!g_cancellable_is_cancelled (cancellable)) + g_cancellable_cancel (cancellable); + + g_task_return_error_if_cancelled (ping->task); + grd_ei_ping_free (ping); +} + +static void +finish_and_free_ping (GrdEiPing *ping) +{ + if (!g_task_return_error_if_cancelled (ping->task)) + g_task_return_boolean (ping->task, TRUE); + grd_ei_ping_free (ping); +} + +static void +grd_session_init (GrdSession *session) +{ + GrdSessionPrivate *priv = grd_session_get_instance_private (session); + + priv->abs_pointer_regions = + g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, + (GDestroyNotify) grd_region_free); + priv->touch_regions = + g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, + (GDestroyNotify) grd_region_free); + + priv->pings = g_hash_table_new_full (NULL, NULL, + NULL, cancel_and_free_ping); +} + +static void +grd_session_class_init (GrdSessionClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = grd_session_finalize; + object_class->set_property = grd_session_set_property; + object_class->get_property = grd_session_get_property; + + g_object_class_install_property (object_class, + PROP_CONTEXT, + g_param_spec_object ("context", + "GrdContext", + "The GrdContext instance", + GRD_TYPE_CONTEXT, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + + signals[READY] = g_signal_new ("ready", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, 0); + signals[STARTED] = g_signal_new ("started", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, 0); + signals[STOPPED] = g_signal_new ("stopped", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, 0); + signals[TOUCH_DEVICE_ADDED] = g_signal_new ("touch-device-added", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, 0); + signals[TOUCH_DEVICE_REMOVED] = g_signal_new ("touch-device-removed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, 0); +} diff --git a/grd-session.h b/grd-session.h new file mode 100644 index 0000000..266c5b8 --- /dev/null +++ b/grd-session.h @@ -0,0 +1,192 @@ +/* + * Copyright (C) 2015 Red Hat Inc. + * Copyright (C) 2020-2021 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + * Written by: + * Jonas Ådahl + */ + +#pragma once + +#include +#include +#include + +#include "grd-mime-type.h" +#include "grd-types.h" + +#define GRD_TYPE_SESSION (grd_session_get_type ()) +G_DECLARE_DERIVABLE_TYPE (GrdSession, grd_session, GRD, SESSION, GObject) + +typedef enum _GrdScreenCastCursorMode +{ + GRD_SCREEN_CAST_CURSOR_MODE_HIDDEN = 0, + GRD_SCREEN_CAST_CURSOR_MODE_EMBEDDED = 1, + GRD_SCREEN_CAST_CURSOR_MODE_METADATA = 2, +} GrdScreenCastCursorMode; + +typedef enum _GrdKeyState +{ + GRD_KEY_STATE_RELEASED, + GRD_KEY_STATE_PRESSED +} GrdKeyState; + +typedef enum _GrdButtonState +{ + GRD_BUTTON_STATE_RELEASED, + GRD_BUTTON_STATE_PRESSED +} GrdButtonState; + +typedef enum _GrdPointerAxisFlags +{ + GRD_POINTER_AXIS_FLAGS_FINISH = 1 << 0, + GRD_POINTER_AXIS_FLAGS_SOURCE_WHEEL = 1 << 1, + GRD_POINTER_AXIS_FLAGS_SOURCE_FINGER = 1 << 2, + GRD_POINTER_AXIS_FLAGS_SOURCE_CONTINUOUS = 1 << 3, +} GrdPointerAxisFlags; + +typedef enum _GrdPointerAxis +{ + GRD_POINTER_AXIS_VERTICAL, + GRD_POINTER_AXIS_HORIZONTAL +} GrdPointerAxis; + +typedef struct +{ + /* Size of the input region */ + uint32_t input_rect_width; + uint32_t input_rect_height; + + /* Absolute pointer position in the input region */ + double x; + double y; +} GrdEventMotionAbs; + +struct _GrdSessionClass +{ + GObjectClass parent_class; + + void (*stop) (GrdSession *session); + + void (*on_stream_created) (GrdSession *session, + uint32_t stream_id, + GrdStream *stream); + + void (*on_caps_lock_state_changed) (GrdSession *session, + gboolean state); + void (*on_num_lock_state_changed) (GrdSession *session, + gboolean state); +}; + +GrdContext *grd_session_get_context (GrdSession *session); + +void grd_session_record_monitor (GrdSession *session, + uint32_t stream_id, + const char *connector, + GrdScreenCastCursorMode cursor_mode); + +void grd_session_record_virtual (GrdSession *session, + uint32_t stream_id, + GrdScreenCastCursorMode cursor_mode, + gboolean is_platform); + +void grd_session_notify_keyboard_keycode (GrdSession *session, + uint32_t keycode, + GrdKeyState state); + +void grd_session_notify_keyboard_keysym (GrdSession *session, + uint32_t keysym, + GrdKeyState state); + +void grd_session_notify_pointer_button (GrdSession *session, + int32_t button, + GrdButtonState state); + +void grd_session_notify_pointer_axis (GrdSession *session, + double dx, + double dy, + GrdPointerAxisFlags flags); + +void grd_session_notify_pointer_axis_discrete (GrdSession *session, + GrdPointerAxis axis, + int steps); + +void grd_session_notify_pointer_motion (GrdSession *session, + double dx, + double dy); + +void grd_session_notify_pointer_motion_absolute (GrdSession *session, + GrdStream *stream, + const GrdEventMotionAbs *motion_abs); + +gboolean grd_session_has_touch_device (GrdSession *session); + +GrdTouchContact *grd_session_acquire_touch_contact (GrdSession *session); + +void grd_session_release_touch_contact (GrdSession *session, + GrdTouchContact *touch_contact); + +void grd_session_notify_touch_down (GrdSession *session, + const GrdTouchContact *touch_contact, + GrdStream *stream, + const GrdEventMotionAbs *motion_abs); + +void grd_session_notify_touch_motion (GrdSession *session, + const GrdTouchContact *touch_contact, + GrdStream *stream, + const GrdEventMotionAbs *motion_abs); + +void grd_session_notify_touch_up (GrdSession *session, + GrdTouchContact *touch_contact); + +void grd_session_notify_touch_cancel (GrdSession *session, + GrdTouchContact *touch_contact); + +void grd_session_notify_touch_device_frame (GrdSession *session); + +gboolean grd_session_enable_clipboard (GrdSession *session, + GrdClipboard *clipboard, + GList *mime_type_tables); + +void grd_session_disable_clipboard (GrdSession *session); + +void grd_session_set_selection (GrdSession *session, + GList *mime_type_tables); + +void grd_session_selection_write (GrdSession *session, + unsigned int serial, + const uint8_t *data, + uint32_t size); + +int grd_session_selection_read (GrdSession *session, + GrdMimeType mime_type); + +void grd_session_start (GrdSession *session); + +void grd_session_stop (GrdSession *session); + +gboolean grd_session_is_ready (GrdSession *session); + +gboolean grd_session_flush_input_finish (GrdSession *session, + GAsyncResult *result, + GError **error); + +void grd_session_flush_input_async (GrdSession *session, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); diff --git a/grd-settings-handover.c b/grd-settings-handover.c new file mode 100644 index 0000000..2fa8bc1 --- /dev/null +++ b/grd-settings-handover.c @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2023 SUSE Software Solutions Germany GmbH + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + */ + +#include "config.h" + +#include "grd-settings-handover.h" + +struct _GrdSettingsHandover +{ + GrdSettings parent; +}; + +G_DEFINE_TYPE (GrdSettingsHandover, grd_settings_handover, GRD_TYPE_SETTINGS) + +GrdSettingsHandover * +grd_settings_handover_new (void) +{ + return g_object_new (GRD_TYPE_SETTINGS_HANDOVER, + "runtime-mode", GRD_RUNTIME_MODE_HANDOVER, + "rdp-enabled", TRUE, + "rdp-view-only", FALSE, + "rdp-screen-share-mode", GRD_RDP_SCREEN_SHARE_MODE_EXTEND, + NULL); +} + +static void +grd_settings_handover_init (GrdSettingsHandover *settings_handover) +{ +} + +static void +grd_settings_handover_class_init (GrdSettingsHandoverClass *klass) +{ +} diff --git a/grd-settings-handover.h b/grd-settings-handover.h new file mode 100644 index 0000000..7b85ecb --- /dev/null +++ b/grd-settings-handover.h @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2023 SUSE Software Solutions Germany GmbH + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + */ + +#pragma once + +#include "grd-settings.h" + +#define GRD_TYPE_SETTINGS_HANDOVER (grd_settings_handover_get_type ()) +G_DECLARE_FINAL_TYPE (GrdSettingsHandover, grd_settings_handover, + GRD, SETTINGS_HANDOVER, GrdSettings) + +GrdSettingsHandover *grd_settings_handover_new (void); diff --git a/grd-settings-headless.c b/grd-settings-headless.c new file mode 100644 index 0000000..50e641e --- /dev/null +++ b/grd-settings-headless.c @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2025 Red Hat Inc. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + */ + +#include "config.h" + +#include "grd-settings-headless.h" + +#include + +#define GRD_RDP_COMMON_SCHEMA_ID "org.gnome.desktop.remote-desktop.rdp" +#define GRD_RDP_SCHEMA_ID "org.gnome.desktop.remote-desktop.rdp.headless" +#define GRD_VNC_SCHEMA_ID "org.gnome.desktop.remote-desktop.vnc.headless" + +struct _GrdSettingsHeadless +{ + GrdSettings parent; + + GSettings *rdp_settings; + GSettings *common_rdp_settings; + GSettings *vnc_settings; +}; + +G_DEFINE_TYPE (GrdSettingsHeadless, + grd_settings_headless, + GRD_TYPE_SETTINGS) + +GrdSettingsHeadless * +grd_settings_headless_new (void) +{ + return g_object_new (GRD_TYPE_SETTINGS_HEADLESS, + "runtime-mode", GRD_RUNTIME_MODE_HEADLESS, + "rdp-screen-share-mode", GRD_RDP_SCREEN_SHARE_MODE_EXTEND, + "rdp-view-only", FALSE, + "vnc-screen-share-mode", GRD_RDP_SCREEN_SHARE_MODE_EXTEND, + "vnc-view-only", FALSE, + "vnc-auth-method", GRD_VNC_AUTH_METHOD_PASSWORD, + NULL); +} + +static void +grd_settings_headless_constructed (GObject *object) +{ + GrdSettingsHeadless *settings = GRD_SETTINGS_HEADLESS (object); + + g_settings_bind (settings->rdp_settings, "enable", + settings, "rdp-enabled", + G_SETTINGS_BIND_DEFAULT); + g_settings_bind (settings->rdp_settings, "port", + settings, "rdp-port", + G_SETTINGS_BIND_DEFAULT); + g_settings_bind (settings->rdp_settings, "negotiate-port", + settings, "rdp-negotiate-port", + G_SETTINGS_BIND_DEFAULT); + g_settings_bind (settings->rdp_settings, "auth-methods", + settings, "rdp-auth-methods", + G_SETTINGS_BIND_DEFAULT); + g_settings_bind (settings->rdp_settings, "kerberos-keytab", + settings, "rdp-kerberos-keytab", + G_SETTINGS_BIND_DEFAULT); + g_settings_bind (settings->common_rdp_settings, "tls-cert", + settings, "rdp-server-cert-path", + G_SETTINGS_BIND_DEFAULT); + g_settings_bind (settings->common_rdp_settings, "tls-key", + settings, "rdp-server-key-path", + G_SETTINGS_BIND_DEFAULT); + g_settings_bind (settings->vnc_settings, "enable", + settings, "vnc-enabled", + G_SETTINGS_BIND_DEFAULT); + g_settings_bind (settings->vnc_settings, "port", + settings, "vnc-port", + G_SETTINGS_BIND_DEFAULT); + g_settings_bind (settings->vnc_settings, "negotiate-port", + settings, "vnc-negotiate-port", + G_SETTINGS_BIND_DEFAULT); + + G_OBJECT_CLASS (grd_settings_headless_parent_class)->constructed (object); +} + +static void +grd_settings_headless_finalize (GObject *object) +{ + GrdSettingsHeadless *settings = GRD_SETTINGS_HEADLESS (object); + + g_clear_object (&settings->rdp_settings); + g_clear_object (&settings->common_rdp_settings); + g_clear_object (&settings->vnc_settings); + + G_OBJECT_CLASS (grd_settings_headless_parent_class)->finalize (object); +} + +static void +grd_settings_headless_init (GrdSettingsHeadless *settings) +{ + settings->rdp_settings = g_settings_new (GRD_RDP_SCHEMA_ID); + settings->common_rdp_settings = g_settings_new (GRD_RDP_COMMON_SCHEMA_ID); + settings->vnc_settings = g_settings_new (GRD_VNC_SCHEMA_ID); +} + +static void +grd_settings_headless_class_init (GrdSettingsHeadlessClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->constructed = grd_settings_headless_constructed; + object_class->finalize = grd_settings_headless_finalize; +} diff --git a/grd-settings-headless.h b/grd-settings-headless.h new file mode 100644 index 0000000..56aade3 --- /dev/null +++ b/grd-settings-headless.h @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2025 Red Hat Inc. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + */ + +#pragma once + +#include "grd-settings.h" + +#define GRD_TYPE_SETTINGS_HEADLESS (grd_settings_headless_get_type ()) +G_DECLARE_FINAL_TYPE (GrdSettingsHeadless, grd_settings_headless, + GRD, SETTINGS_HEADLESS, GrdSettings) + +GrdSettingsHeadless *grd_settings_headless_new (void); diff --git a/grd-settings-system.c b/grd-settings-system.c new file mode 100644 index 0000000..531732a --- /dev/null +++ b/grd-settings-system.c @@ -0,0 +1,634 @@ +/* + * Copyright (C) 2023 SUSE Software Solutions Germany GmbH + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + */ + +#include "config.h" + +#include "grd-settings-system.h" + +#include +#include + +#define GRD_SETTINGS_SYSTEM_GROUP_RDP "RDP" + +typedef enum +{ + GRD_SETTINGS_SOURCE_TYPE_INVALID = -1, + GRD_SETTINGS_SOURCE_TYPE_DEFAULT = 0, + GRD_SETTINGS_SOURCE_TYPE_CUSTOM = 1, + GRD_SETTINGS_SOURCE_TYPE_LOCAL_STATE = 2, + N_GRD_SETTINGS_SOURCES +} GrdSettingsSourceType; + +typedef struct +{ + GKeyFile *key_file; + GFileMonitor *file_monitor; + GrdSettingsSourceType type; +} GrdSettingsSource; + +typedef struct +{ + const char *file_key; + const char *settings_name; + void (* read_settings_value) (GrdSettingsSystem *settings_system, + GKeyFile *key_file, + const char *group, + const char *key, + const char *settings_name); + void (* write_settings_value) (GrdSettingsSystem *settings_system, + GKeyFile *key_file, + const char *group, + const char *key, + const char *settings_name); +} FileSetting; + +struct _GrdSettingsSystem +{ + GrdSettings parent; + + GrdSettingsSource *setting_sources[N_GRD_SETTINGS_SOURCES]; + + GKeyFile *key_file; + + gboolean use_local_state; +}; + +G_DEFINE_TYPE (GrdSettingsSystem, grd_settings_system, GRD_TYPE_SETTINGS) + +static void +grd_settings_system_reload_sources (GrdSettingsSystem *settings_system); + +static void +read_rdp_file_settings (GrdSettingsSystem *settings_system); + +static GrdSettingsSource * +grd_settings_source_new (GrdSettingsSourceType source_type, + const char *file_path) +{ + g_autofree GrdSettingsSource *source = NULL; + GKeyFileFlags flags = G_KEY_FILE_NONE; + g_autoptr (GKeyFile) key_file = NULL; + g_autoptr (GError) error = NULL; + g_autoptr (GFile) file = NULL; + + source = g_new0 (GrdSettingsSource, 1); + key_file = g_key_file_new (); + + if (source_type == GRD_SETTINGS_SOURCE_TYPE_CUSTOM) + flags |= G_KEY_FILE_KEEP_COMMENTS; + + if (!g_key_file_load_from_file (key_file, + file_path, + flags, + &error)) + { + g_debug ("Failed to load key file from '%s': %s", + file_path, error->message); + return NULL; + } + + source->key_file = g_steal_pointer (&key_file); + + file = g_file_new_for_path (file_path); + source->file_monitor = g_file_monitor_file (file, G_FILE_MONITOR_NONE, + NULL, &error); + if (!source->file_monitor) + { + g_warning ("Failed to monitor file '%s': %s", file_path, error->message); + return NULL; + } + + return g_steal_pointer (&source); +} + +static void +grd_settings_source_free (GrdSettingsSource *source) +{ + g_clear_pointer (&source->key_file, g_key_file_unref); + g_clear_object (&source->file_monitor); + g_clear_pointer (&source, g_free); +} + +static char * +get_local_state_conf (void) +{ + return g_build_filename (g_get_user_data_dir (), + "gnome-remote-desktop", "grd.conf", + NULL); +} + +void +grd_settings_system_use_local_state (GrdSettingsSystem *settings_system) +{ + g_assert (GRD_IS_SETTINGS_SYSTEM (settings_system)); + + settings_system->use_local_state = TRUE; +} + +static void +merge_descendant_keys (GrdSettingsSystem *settings_system, + GKeyFile *key_file, + GKeyFile *descendant_key_file) +{ + g_auto (GStrv) groups = NULL; + size_t group_count = 0; + size_t i, j; + + g_assert (key_file); + g_assert (descendant_key_file); + + groups = g_key_file_get_groups (descendant_key_file, &group_count); + for (i = 0; i < group_count; i++) + { + g_auto (GStrv) keys = NULL; + size_t key_count = 0; + + keys = g_key_file_get_keys (descendant_key_file, + groups[i], &key_count, + NULL); + for (j = 0; j < key_count; j++) + { + g_autofree char *value = NULL; + + value = g_key_file_get_value (descendant_key_file, + groups[i], keys[j], + NULL); + if (value) + g_key_file_set_value (key_file, groups[i], keys[j], value); + } + } +} + +static gboolean +prune_inherited_keys (GrdSettingsSystem *settings_system, + GrdSettingsSourceType source_type, + GKeyFile *key_file_to_prune) +{ + g_auto (GStrv) groups = NULL; + size_t group_count = 0; + size_t i, j; + + if (source_type == GRD_SETTINGS_SOURCE_TYPE_DEFAULT) + return TRUE; + + if (source_type != GRD_SETTINGS_SOURCE_TYPE_CUSTOM) + g_key_file_remove_comment (key_file_to_prune, NULL, NULL, NULL); + + groups = g_key_file_get_groups (key_file_to_prune, &group_count); + + for (i = 0; i < group_count; i++) + { + g_auto (GStrv) keys = NULL; + size_t key_count; + + if (source_type != GRD_SETTINGS_SOURCE_TYPE_CUSTOM) + g_key_file_remove_comment (key_file_to_prune, groups[i], NULL, NULL); + + keys = g_key_file_get_keys (key_file_to_prune, + groups[i], &key_count, + NULL); + + for (j = 0; j < key_count; j++) + { + g_autofree char *value_to_prune = NULL; + GrdSettingsSourceType ancestor_type; + gboolean should_prune_key = FALSE; + + value_to_prune = g_key_file_get_value (key_file_to_prune, + groups[i], keys[j], + NULL); + + if (source_type != GRD_SETTINGS_SOURCE_TYPE_CUSTOM) + { + g_key_file_remove_comment (key_file_to_prune, + groups[i], keys[j], + NULL); + } + + for (ancestor_type = source_type - 1; + ancestor_type >= GRD_SETTINGS_SOURCE_TYPE_DEFAULT; + ancestor_type--) + { + GrdSettingsSource *ancestor_source; + + ancestor_source = settings_system->setting_sources[ancestor_type]; + if (ancestor_source == NULL) + continue; + + if (g_key_file_has_key (ancestor_source->key_file, + groups[i], keys[j], + NULL)) + { + g_autofree char *ancestor_value = NULL; + + ancestor_value = g_key_file_get_value (ancestor_source->key_file, + groups[i], keys[j], + NULL); + + if (g_strcmp0 (value_to_prune, ancestor_value) == 0) + should_prune_key = TRUE; + + break; + } + } + + if (should_prune_key) + { + g_key_file_remove_key (key_file_to_prune, + groups[i], keys[j], + NULL); + } + } + g_clear_pointer (&keys, g_strfreev); + + keys = g_key_file_get_keys (key_file_to_prune, + groups[i], &key_count, + NULL); + if (key_count == 0) + g_key_file_remove_group (key_file_to_prune, groups[i], NULL); + } + g_clear_pointer (&groups, g_strfreev); + + groups = g_key_file_get_groups (key_file_to_prune, &group_count); + + if (group_count == 0) + return FALSE; + + return TRUE; +} + +static char ** +get_conf_paths (void) +{ + g_autofree char *local_state_conf = NULL; + char **paths = NULL; + + local_state_conf = get_local_state_conf (); + + paths = g_new (char *, N_GRD_SETTINGS_SOURCES + 1); + paths[GRD_SETTINGS_SOURCE_TYPE_DEFAULT] = g_strdup (GRD_DEFAULT_CONF); + paths[GRD_SETTINGS_SOURCE_TYPE_CUSTOM] = g_strdup (GRD_CUSTOM_CONF); + paths[GRD_SETTINGS_SOURCE_TYPE_LOCAL_STATE] = g_steal_pointer (&local_state_conf); + paths[N_GRD_SETTINGS_SOURCES] = NULL; + + return paths; +} + +static void +on_file_changed (GFileMonitor *file_monitor, + GFile *file, + GFile *other_file, + GFileMonitorEvent event_type, + gpointer user_data) +{ + GrdSettingsSystem *settings_system = GRD_SETTINGS_SYSTEM (user_data); + + if (event_type == G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT) + { + grd_settings_system_reload_sources (settings_system); + read_rdp_file_settings (settings_system); + } +} + +static void +grd_settings_system_reload_sources (GrdSettingsSystem *settings_system) +{ + g_autoptr (GKeyFile) key_file = NULL; + g_autoptr (GError) error = NULL; + g_auto (GStrv) paths = NULL; + size_t source_type; + + paths = get_conf_paths (); + key_file = g_key_file_new (); + + for (source_type = GRD_SETTINGS_SOURCE_TYPE_DEFAULT; + source_type < N_GRD_SETTINGS_SOURCES; + source_type++) + { + GrdSettingsSource *source = NULL; + + g_clear_pointer (&settings_system->setting_sources[source_type], + grd_settings_source_free); + + source = grd_settings_source_new (source_type, paths[source_type]); + if (!source) + continue; + + merge_descendant_keys (settings_system, key_file, source->key_file); + + g_signal_connect (source->file_monitor, + "changed", G_CALLBACK (on_file_changed), + settings_system); + + settings_system->setting_sources[source_type] = g_steal_pointer (&source); + } + + g_clear_pointer (&settings_system->key_file, g_key_file_unref); + settings_system->key_file = g_steal_pointer (&key_file); +} + +GrdSettingsSystem * +grd_settings_system_new (void) +{ + return g_object_new (GRD_TYPE_SETTINGS_SYSTEM, + "runtime-mode", GRD_RUNTIME_MODE_SYSTEM, + "rdp-view-only", FALSE, + "rdp-screen-share-mode", GRD_RDP_SCREEN_SHARE_MODE_EXTEND, + NULL); +} + +static void +write_string (GrdSettingsSystem *settings_system, + GKeyFile *key_file, + const char *group, + const char *key, + const char *settings_name) +{ + g_autofree char *value = NULL; + + g_object_get (G_OBJECT (settings_system), settings_name, &value, NULL); + + g_key_file_set_string (key_file, + group, + key, + value); +} + +static void +read_filename (GrdSettingsSystem *settings_system, + GKeyFile *key_file, + const char *group, + const char *key, + const char *settings_name) +{ + g_autofree char *value = NULL; + g_autoptr (GError) error = NULL; + + value = g_key_file_get_string (key_file, + group, + key, + &error); + if (error && error->code != G_KEY_FILE_ERROR_KEY_NOT_FOUND) + return; + + if (!value) + return; + + if (!g_file_test (value, G_FILE_TEST_IS_REGULAR)) + return; + + g_object_set (G_OBJECT (settings_system), settings_name, value, NULL); +} + +static void +read_int (GrdSettingsSystem *settings_system, + GKeyFile *key_file, + const char *group, + const char *key, + const char *settings_name) +{ + g_autoptr (GError) error = NULL; + int value; + + value = g_key_file_get_integer (key_file, + group, + key, + &error); + if (error) + return; + + g_object_set (G_OBJECT (settings_system), settings_name, value, NULL); +} + +static void +write_int (GrdSettingsSystem *settings_system, + GKeyFile *key_file, + const char *group, + const char *key, + const char *settings_name) +{ + int value = 0; + + g_object_get (G_OBJECT (settings_system), settings_name, &value, NULL); + + g_key_file_set_integer (key_file, + group, + key, + value); +} + +static void +read_boolean (GrdSettingsSystem *settings_system, + GKeyFile *key_file, + const char *group, + const char *key, + const char *settings_name) +{ + g_autoptr (GError) error = NULL; + gboolean value = FALSE; + + value = g_key_file_get_boolean (key_file, group, key, &error); + if (error) + return; + + g_object_set (G_OBJECT (settings_system), settings_name, value, NULL); +} + +static void +write_boolean (GrdSettingsSystem *settings_system, + GKeyFile *key_file, + const char *group, + const char *key, + const char *settings_name) +{ + gboolean value = FALSE; + + g_object_get (G_OBJECT (settings_system), settings_name, &value, NULL); + + g_key_file_set_boolean (key_file, + group, + key, + value); +} + + +static const FileSetting rdp_file_settings[] = +{ + { "enabled", "rdp-enabled", read_boolean, write_boolean }, + { "tls-cert", "rdp-server-cert-path", read_filename, write_string }, + { "tls-key", "rdp-server-key-path", read_filename, write_string }, + { "port", "rdp-port", read_int, write_int }, +}; + +static void +on_rdp_setting_changed (GrdSettingsSystem *settings_system, + GParamSpec *pspec, + FileSetting *file_setting) +{ + GrdSettingsSourceType settings_source_type = GRD_SETTINGS_SOURCE_TYPE_INVALID; + g_autofree char *local_state_conf = NULL; + g_autoptr (GKeyFile) key_file = NULL; + g_autoptr (GError) error = NULL; + g_autofree char *data = NULL; + const char *filename; + size_t length = 0; + + grd_settings_system_reload_sources (settings_system); + + file_setting->write_settings_value (settings_system, + settings_system->key_file, + GRD_SETTINGS_SYSTEM_GROUP_RDP, + file_setting->file_key, + file_setting->settings_name); + + data = g_key_file_to_data (settings_system->key_file, &length, NULL); + + key_file = g_key_file_new (); + if (!g_key_file_load_from_data (key_file, data, length, + G_KEY_FILE_KEEP_COMMENTS | + G_KEY_FILE_KEEP_TRANSLATIONS, + &error)) + { + g_warning ("Failed to copy loaded key file: %s", error->message); + return; + } + + if (settings_system->use_local_state) + settings_source_type = GRD_SETTINGS_SOURCE_TYPE_LOCAL_STATE; + else + settings_source_type = GRD_SETTINGS_SOURCE_TYPE_CUSTOM; + + local_state_conf = get_local_state_conf (); + switch (settings_source_type) + { + case GRD_SETTINGS_SOURCE_TYPE_LOCAL_STATE: + filename = local_state_conf; + break; + case GRD_SETTINGS_SOURCE_TYPE_CUSTOM: + g_remove (local_state_conf); + filename = GRD_CUSTOM_CONF; + break; + default: + g_assert_not_reached (); + } + + if (prune_inherited_keys (settings_system, + settings_source_type, + key_file)) + { + if (!g_key_file_save_to_file (key_file, filename, &error)) + { + g_warning ("Failed to write %s: %s", file_setting->file_key, + error->message); + } + } + else + { + g_remove (filename); + } +} + +static void +read_rdp_file_settings (GrdSettingsSystem *settings_system) +{ + g_autoptr (GKeyFile) key_file = NULL; + g_autoptr (GError) error = NULL; + int i; + + for (i = 0; i < G_N_ELEMENTS (rdp_file_settings); i++) + { + g_signal_handlers_block_by_func (G_OBJECT (settings_system), + G_CALLBACK (on_rdp_setting_changed), + (gpointer) &rdp_file_settings[i]); + + rdp_file_settings[i].read_settings_value ( + settings_system, + settings_system->key_file, + GRD_SETTINGS_SYSTEM_GROUP_RDP, + rdp_file_settings[i].file_key, + rdp_file_settings[i].settings_name); + + g_signal_handlers_unblock_by_func (G_OBJECT (settings_system), + G_CALLBACK (on_rdp_setting_changed), + (gpointer) &rdp_file_settings[i]); + } +} + +static void +register_write_rdp_file_settings (GrdSettingsSystem *settings_system) +{ + int i; + + for (i = 0; i < G_N_ELEMENTS (rdp_file_settings); i++) + { + g_autofree char *signal_name = NULL; + + signal_name = g_strdup_printf ("notify::%s", + rdp_file_settings[i].settings_name); + g_signal_connect (G_OBJECT (settings_system), + signal_name, + G_CALLBACK (on_rdp_setting_changed), + (gpointer) &rdp_file_settings[i]); + } +} + +static void +grd_settings_system_constructed (GObject *object) +{ + GrdSettingsSystem *settings_system = GRD_SETTINGS_SYSTEM (object); + + grd_settings_system_reload_sources (settings_system); + read_rdp_file_settings (settings_system); + + register_write_rdp_file_settings (settings_system); + + G_OBJECT_CLASS (grd_settings_system_parent_class)->constructed (object); +} + +static void +grd_settings_system_finalize (GObject *object) +{ + GrdSettingsSystem *settings_system = GRD_SETTINGS_SYSTEM (object); + size_t i; + + g_clear_pointer (&settings_system->key_file, g_key_file_unref); + + for (i = 0; i < N_GRD_SETTINGS_SOURCES; i++) + { + g_clear_pointer (&settings_system->setting_sources[i], + grd_settings_source_free); + } + + G_OBJECT_CLASS (grd_settings_system_parent_class)->finalize (object); +} + +static void +grd_settings_system_init (GrdSettingsSystem *settings_system) +{ +} + +static void +grd_settings_system_class_init (GrdSettingsSystemClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->constructed = grd_settings_system_constructed; + object_class->finalize = grd_settings_system_finalize; +} diff --git a/grd-settings-system.h b/grd-settings-system.h new file mode 100644 index 0000000..29b0d04 --- /dev/null +++ b/grd-settings-system.h @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2023 SUSE Software Solutions Germany GmbH + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + */ + +#pragma once + +#include "grd-settings.h" + +#define GRD_TYPE_SETTINGS_SYSTEM (grd_settings_system_get_type ()) +G_DECLARE_FINAL_TYPE (GrdSettingsSystem, grd_settings_system, + GRD, SETTINGS_SYSTEM, GrdSettings) + +GrdSettingsSystem *grd_settings_system_new (void); + +void grd_settings_system_use_local_state (GrdSettingsSystem *settings_system); diff --git a/grd-settings-user.c b/grd-settings-user.c new file mode 100644 index 0000000..e8412a9 --- /dev/null +++ b/grd-settings-user.c @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2018 Red Hat Inc. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + */ + +#include "config.h" + +#include "grd-settings-user.h" + +#include + +#define GRD_RDP_SCHEMA_ID "org.gnome.desktop.remote-desktop.rdp" +#define GRD_VNC_SCHEMA_ID "org.gnome.desktop.remote-desktop.vnc" + +struct _GrdSettingsUser +{ + GrdSettings parent; + + GSettings *rdp_settings; + GSettings *vnc_settings; +}; + +G_DEFINE_TYPE (GrdSettingsUser, + grd_settings_user, + GRD_TYPE_SETTINGS) + +GrdSettingsUser * +grd_settings_user_new (void) +{ + return g_object_new (GRD_TYPE_SETTINGS_USER, + "runtime-mode", GRD_RUNTIME_MODE_SCREEN_SHARE, + NULL); +} + +static void +grd_settings_user_constructed (GObject *object) +{ + GrdSettingsUser *settings = GRD_SETTINGS_USER (object); + + g_settings_bind (settings->rdp_settings, "enable", + settings, "rdp-enabled", + G_SETTINGS_BIND_DEFAULT); + g_settings_bind (settings->rdp_settings, "port", + settings, "rdp-port", + G_SETTINGS_BIND_DEFAULT); + g_settings_bind (settings->rdp_settings, "negotiate-port", + settings, "rdp-negotiate-port", + G_SETTINGS_BIND_DEFAULT); + g_settings_bind (settings->rdp_settings, "tls-cert", + settings, "rdp-server-cert-path", + G_SETTINGS_BIND_DEFAULT); + g_settings_bind (settings->rdp_settings, "tls-key", + settings, "rdp-server-key-path", + G_SETTINGS_BIND_DEFAULT); + g_settings_bind (settings->rdp_settings, "view-only", + settings, "rdp-view-only", + G_SETTINGS_BIND_DEFAULT); + g_settings_bind (settings->rdp_settings, "screen-share-mode", + settings, "rdp-screen-share-mode", + G_SETTINGS_BIND_DEFAULT); + g_settings_bind (settings->rdp_settings, "auth-methods", + settings, "rdp-auth-methods", + G_SETTINGS_BIND_DEFAULT); + g_settings_bind (settings->rdp_settings, "kerberos-keytab", + settings, "rdp-kerberos-keytab", + G_SETTINGS_BIND_DEFAULT); + g_settings_bind (settings->vnc_settings, "enable", + settings, "vnc-enabled", + G_SETTINGS_BIND_DEFAULT); + g_settings_bind (settings->vnc_settings, "port", + settings, "vnc-port", + G_SETTINGS_BIND_DEFAULT); + g_settings_bind (settings->vnc_settings, "negotiate-port", + settings, "vnc-negotiate-port", + G_SETTINGS_BIND_DEFAULT); + g_settings_bind (settings->vnc_settings, "auth-method", + settings, "vnc-auth-method", + G_SETTINGS_BIND_DEFAULT); + g_settings_bind (settings->vnc_settings, "view-only", + settings, "vnc-view-only", + G_SETTINGS_BIND_DEFAULT); + g_settings_bind (settings->vnc_settings, "screen-share-mode", + settings, "vnc-screen-share-mode", + G_SETTINGS_BIND_DEFAULT); + + G_OBJECT_CLASS (grd_settings_user_parent_class)->constructed (object); +} + +static void +grd_settings_user_finalize (GObject *object) +{ + GrdSettingsUser *settings = GRD_SETTINGS_USER (object); + + g_clear_object (&settings->rdp_settings); + g_clear_object (&settings->vnc_settings); + + G_OBJECT_CLASS (grd_settings_user_parent_class)->finalize (object); +} + +static void +grd_settings_user_init (GrdSettingsUser *settings) +{ + settings->rdp_settings = g_settings_new (GRD_RDP_SCHEMA_ID); + settings->vnc_settings = g_settings_new (GRD_VNC_SCHEMA_ID); +} + +static void +grd_settings_user_class_init (GrdSettingsUserClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->constructed = grd_settings_user_constructed; + object_class->finalize = grd_settings_user_finalize; +} diff --git a/grd-settings-user.h b/grd-settings-user.h new file mode 100644 index 0000000..222840d --- /dev/null +++ b/grd-settings-user.h @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2018 Red Hat Inc. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + */ + +#pragma once + +#include "grd-settings.h" + +#define GRD_TYPE_SETTINGS_USER (grd_settings_user_get_type ()) +G_DECLARE_FINAL_TYPE (GrdSettingsUser, grd_settings_user, + GRD, SETTINGS_USER, GrdSettings) + +GrdSettingsUser *grd_settings_user_new (void); diff --git a/grd-settings.c b/grd-settings.c new file mode 100644 index 0000000..b7a1024 --- /dev/null +++ b/grd-settings.c @@ -0,0 +1,802 @@ +/* + * Copyright (C) 2018 Red Hat Inc. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + */ + +#include "config.h" + +#include "grd-settings.h" + +#include + +#ifdef HAVE_RDP +#include +#endif + +#include "grd-credentials-file.h" +#include "grd-credentials-libsecret.h" +#include "grd-credentials-one-time.h" +#include "grd-credentials-tpm.h" +#include "grd-enum-types.h" + +#define GRD_DEFAULT_RDP_SERVER_PORT 3389 +#define GRD_DEFAULT_VNC_SERVER_PORT 5900 + +enum +{ + PROP_0, + + PROP_RUNTIME_MODE, + PROP_RDP_PORT, + PROP_RDP_NEGOTIATE_PORT, + PROP_VNC_PORT, + PROP_VNC_NEGOTIATE_PORT, + PROP_RDP_ENABLED, + PROP_VNC_ENABLED, + PROP_RDP_VIEW_ONLY, + PROP_VNC_VIEW_ONLY, + PROP_RDP_SCREEN_SHARE_MODE, + PROP_VNC_SCREEN_SHARE_MODE, + PROP_RDP_SERVER_CERT, + PROP_RDP_SERVER_FINGERPRINT, + PROP_RDP_SERVER_KEY, + PROP_RDP_SERVER_CERT_PATH, + PROP_RDP_SERVER_KEY_PATH, + PROP_VNC_AUTH_METHOD, + PROP_RDP_AUTH_METHODS, + PROP_RDP_KERBEROS_KEYTAB, +}; + +typedef struct _GrdSettingsPrivate +{ + GrdRuntimeMode runtime_mode; + GrdCredentials *credentials; + + struct { + int port; + gboolean negotiate_port; + gboolean is_enabled; + gboolean view_only; + GrdRdpScreenShareMode screen_share_mode; + GrdRdpAuthMethods auth_methods; + char *server_cert; + char *server_fingerprint; + char *server_key; + char *server_cert_path; + char *server_key_path; + char *kerberos_keytab; + } rdp; + struct { + int port; + gboolean negotiate_port; + gboolean is_enabled; + gboolean view_only; + GrdVncScreenShareMode screen_share_mode; + GrdVncAuthMethod auth_method; + } vnc; + + int max_parallel_connections; +} GrdSettingsPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (GrdSettings, grd_settings, G_TYPE_OBJECT) + +#ifdef HAVE_RDP +G_DEFINE_AUTOPTR_CLEANUP_FUNC (rdpCertificate, freerdp_certificate_free) +#endif + +GrdRuntimeMode +grd_settings_get_runtime_mode (GrdSettings *settings) +{ + GrdSettingsPrivate *priv = grd_settings_get_instance_private (settings); + + return priv->runtime_mode; +} + +void +grd_settings_override_max_parallel_connections (GrdSettings *settings, + int max_parallel_connections) +{ + GrdSettingsPrivate *priv = grd_settings_get_instance_private (settings); + + priv->max_parallel_connections = max_parallel_connections; +} + +int +grd_settings_get_max_parallel_connections (GrdSettings *settings) +{ + GrdSettingsPrivate *priv = grd_settings_get_instance_private (settings); + + return priv->max_parallel_connections; +} + +void +grd_settings_override_rdp_port (GrdSettings *settings, + int port) +{ + GrdSettingsPrivate *priv = grd_settings_get_instance_private (settings); + + priv->rdp.port = port; +} + +void +grd_settings_override_vnc_port (GrdSettings *settings, + int port) +{ + GrdSettingsPrivate *priv = grd_settings_get_instance_private (settings); + + priv->vnc.port = port; +} + +gboolean +grd_settings_get_rdp_credentials (GrdSettings *settings, + char **out_username, + char **out_password, + GError **error) +{ + GrdSettingsPrivate *priv = grd_settings_get_instance_private (settings); + const char *test_username_override; + const char *test_password_override; + g_autofree char *credentials_string = NULL; + g_autoptr (GVariant) credentials = NULL; + g_autofree char *username = NULL; + g_autofree char *password = NULL; + + test_username_override = g_getenv ("GNOME_REMOTE_DESKTOP_TEST_RDP_USERNAME"); + test_password_override = g_getenv ("GNOME_REMOTE_DESKTOP_TEST_RDP_PASSWORD"); + + if (test_username_override && test_password_override) + { + *out_username = g_strdup (test_username_override); + *out_password = g_strdup (test_password_override); + return TRUE; + } + + credentials = grd_credentials_lookup (priv->credentials, + GRD_CREDENTIALS_TYPE_RDP, + error); + if (!credentials) + return FALSE; + + if (!test_username_override) + { + g_variant_lookup (credentials, "username", "s", &username); + if (!username) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND, + "Username not set"); + return FALSE; + } + } + else + { + username = g_strdup (test_username_override); + } + + if (!test_password_override) + { + g_variant_lookup (credentials, "password", "s", &password); + if (!password) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND, + "Username not set"); + return FALSE; + } + } + else + { + password = g_strdup (test_password_override); + } + + *out_username = g_steal_pointer (&username); + *out_password = g_steal_pointer (&password); + return TRUE; +} + +gboolean +grd_settings_set_rdp_credentials (GrdSettings *settings, + const char *username, + const char *password, + GError **error) +{ + GrdSettingsPrivate *priv = grd_settings_get_instance_private (settings); + GVariantBuilder builder; + + g_assert (username); + g_assert (password); + + if (!g_utf8_validate (username, -1, NULL)) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, + "Username is not a valid UTF-8 string"); + return FALSE; + } + if (!g_utf8_validate (password, -1, NULL)) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, + "Password is not a valid UTF-8 string"); + return FALSE; + } + + g_variant_builder_init (&builder, G_VARIANT_TYPE ("a{sv}")); + g_variant_builder_add (&builder, "{sv}", + "username", g_variant_new_string (username)); + g_variant_builder_add (&builder, "{sv}", + "password", g_variant_new_string (password)); + + return grd_credentials_store (priv->credentials, + GRD_CREDENTIALS_TYPE_RDP, + g_variant_builder_end (&builder), + error); +} + +gboolean +grd_settings_clear_rdp_credentials (GrdSettings *settings, + GError **error) +{ + GrdSettingsPrivate *priv = grd_settings_get_instance_private (settings); + + return grd_credentials_clear (priv->credentials, + GRD_CREDENTIALS_TYPE_RDP, + error); +} + +char * +grd_settings_get_vnc_password (GrdSettings *settings, + GError **error) +{ + GrdSettingsPrivate *priv = grd_settings_get_instance_private (settings); + const char *test_password_override; + g_autoptr (GVariant) password = NULL; + + test_password_override = g_getenv ("GNOME_REMOTE_DESKTOP_TEST_VNC_PASSWORD"); + if (test_password_override) + return g_strdup (test_password_override); + + password = grd_credentials_lookup (priv->credentials, + GRD_CREDENTIALS_TYPE_VNC, + error); + if (!password) + return NULL; + + return g_variant_dup_string (password, NULL); +} + +gboolean +grd_settings_set_vnc_password (GrdSettings *settings, + const char *password, + GError **error) +{ + GrdSettingsPrivate *priv = grd_settings_get_instance_private (settings); + + g_assert (password); + + if (!g_utf8_validate (password, -1, NULL)) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, + "Password is not a valid UTF-8 string"); + return FALSE; + } + + return grd_credentials_store (priv->credentials, + GRD_CREDENTIALS_TYPE_VNC, + g_variant_new_string (password), + error); +} + +gboolean +grd_settings_clear_vnc_password (GrdSettings *settings, + GError **error) +{ + GrdSettingsPrivate *priv = grd_settings_get_instance_private (settings); + + return grd_credentials_clear (priv->credentials, + GRD_CREDENTIALS_TYPE_VNC, + error); +} + +static GrdCredentials * +create_headless_credentials (void) +{ + g_autoptr (GrdCredentials) credentials = NULL; + g_autoptr (GError) error = NULL; + + credentials = GRD_CREDENTIALS (grd_credentials_tpm_new (&error)); + if (credentials) + return g_steal_pointer (&credentials); + + g_warning ("Init TPM credentials failed because %s, using GKeyFile as fallback", + error->message); + + g_clear_error (&error); + credentials = GRD_CREDENTIALS (grd_credentials_file_new (&error)); + if (credentials) + return g_steal_pointer (&credentials); + + g_warning ("Init file credentials failed: %s", error->message); + + return NULL; +} + +static GrdCredentials * +create_credentials (GrdRuntimeMode runtime_mode) +{ + switch (runtime_mode) + { + case GRD_RUNTIME_MODE_HEADLESS: + case GRD_RUNTIME_MODE_SYSTEM: + return create_headless_credentials (); + case GRD_RUNTIME_MODE_SCREEN_SHARE: + return GRD_CREDENTIALS (grd_credentials_libsecret_new ()); + case GRD_RUNTIME_MODE_HANDOVER: + return GRD_CREDENTIALS (grd_credentials_one_time_new ()); + } + + g_assert_not_reached (); +} + +void +grd_settings_recreate_rdp_credentials (GrdSettings *settings) +{ + GrdSettingsPrivate *priv = grd_settings_get_instance_private (settings); + + g_clear_object (&priv->credentials); + priv->credentials = create_credentials (priv->runtime_mode); +} + +static void +grd_settings_constructed (GObject *object) +{ + GrdSettings *settings = GRD_SETTINGS (object); + GrdSettingsPrivate *priv = grd_settings_get_instance_private (settings); + + priv->credentials = create_credentials (priv->runtime_mode); + g_assert (priv->credentials); +} + +static void +grd_settings_finalize (GObject *object) +{ + GrdSettings *settings = GRD_SETTINGS (object); + GrdSettingsPrivate *priv = grd_settings_get_instance_private (settings); + + g_clear_pointer (&priv->rdp.server_cert, g_free); + g_clear_pointer (&priv->rdp.server_fingerprint, g_free); + g_clear_pointer (&priv->rdp.server_key, g_free); + g_clear_pointer (&priv->rdp.server_cert_path, g_free); + g_clear_pointer (&priv->rdp.server_key_path, g_free); + g_clear_object (&priv->credentials); + + G_OBJECT_CLASS (grd_settings_parent_class)->finalize (object); +} + +static void +grd_settings_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GrdSettings *settings = GRD_SETTINGS (object); + GrdSettingsPrivate *priv = grd_settings_get_instance_private (settings); + + switch (prop_id) + { + case PROP_RUNTIME_MODE: + g_value_set_enum (value, priv->runtime_mode); + break; + case PROP_RDP_PORT: + g_value_set_int (value, priv->rdp.port); + break; + case PROP_RDP_NEGOTIATE_PORT: + g_value_set_boolean (value, priv->rdp.negotiate_port); + break; + case PROP_VNC_PORT: + g_value_set_int (value, priv->vnc.port); + break; + case PROP_VNC_NEGOTIATE_PORT: + g_value_set_boolean (value, priv->vnc.negotiate_port); + break; + case PROP_RDP_ENABLED: + g_value_set_boolean (value, priv->rdp.is_enabled); + break; + case PROP_VNC_ENABLED: + g_value_set_boolean (value, priv->vnc.is_enabled); + break; + case PROP_RDP_VIEW_ONLY: + g_value_set_boolean (value, priv->rdp.view_only); + break; + case PROP_VNC_VIEW_ONLY: + g_value_set_boolean (value, priv->vnc.view_only); + break; + case PROP_RDP_SCREEN_SHARE_MODE: + g_value_set_enum (value, priv->rdp.screen_share_mode); + break; + case PROP_VNC_SCREEN_SHARE_MODE: + g_value_set_enum (value, priv->vnc.screen_share_mode); + break; + case PROP_RDP_SERVER_CERT: + g_value_set_string (value, priv->rdp.server_cert); + break; + case PROP_RDP_SERVER_FINGERPRINT: + g_value_set_string (value, priv->rdp.server_fingerprint); + break; + case PROP_RDP_SERVER_KEY: + g_value_set_string (value, priv->rdp.server_key); + break; + case PROP_RDP_SERVER_CERT_PATH: + g_value_set_string (value, priv->rdp.server_cert_path); + break; + case PROP_RDP_SERVER_KEY_PATH: + g_value_set_string (value, priv->rdp.server_key_path); + break; + case PROP_VNC_AUTH_METHOD: + if (g_getenv ("GNOME_REMOTE_DESKTOP_TEST_VNC_PASSWORD")) + g_value_set_enum (value, GRD_VNC_AUTH_METHOD_PASSWORD); + else + g_value_set_enum (value, priv->vnc.auth_method); + break; + case PROP_RDP_AUTH_METHODS: + g_value_set_flags (value, priv->rdp.auth_methods); + break; + case PROP_RDP_KERBEROS_KEYTAB: + g_value_set_string (value, priv->rdp.kerberos_keytab); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +update_rdp_server_fingerprint (GrdSettings *settings) +{ + GrdSettingsPrivate *priv = grd_settings_get_instance_private (settings); +#ifdef HAVE_RDP + g_autoptr (rdpCertificate) rdp_certificate = NULL; +#endif + g_autofree char *fingerprint = NULL; + + if (!priv->rdp.server_cert_path) + return; + +#ifdef HAVE_RDP + rdp_certificate = freerdp_certificate_new_from_file (priv->rdp.server_cert_path); + if (!rdp_certificate) + { + g_warning ("RDP server certificate is invalid"); + return; + } + + fingerprint = freerdp_certificate_get_fingerprint (rdp_certificate); + if (!fingerprint) + { + g_warning ("Could not compute RDP server certificate fingerprint"); + return; + } +#endif + + if (fingerprint) + g_object_set (G_OBJECT (settings), "rdp-server-fingerprint", fingerprint, NULL); +} + +static void +update_rdp_server_cert (GrdSettings *settings) +{ + GrdSettingsPrivate *priv = grd_settings_get_instance_private (settings); + g_autofree char *server_cert = NULL; + + if (!priv->rdp.server_cert_path) + return; + + g_file_get_contents (priv->rdp.server_cert_path, + &server_cert, + NULL, NULL); + g_object_set (G_OBJECT (settings), "rdp-server-cert", server_cert, NULL); +} + +static void +update_rdp_server_key (GrdSettings *settings) +{ + GrdSettingsPrivate *priv = grd_settings_get_instance_private (settings); + g_autofree char *server_key = NULL; + + if (!priv->rdp.server_key_path) + return; + + g_file_get_contents (priv->rdp.server_key_path, + &server_key, + NULL, NULL); + g_object_set (G_OBJECT (settings), "rdp-server-key", server_key, NULL); +} + +static void +grd_settings_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GrdSettings *settings = GRD_SETTINGS (object); + GrdSettingsPrivate *priv = grd_settings_get_instance_private (settings); + + switch (prop_id) + { + case PROP_RUNTIME_MODE: + priv->runtime_mode = g_value_get_enum (value); + break; + case PROP_RDP_PORT: + priv->rdp.port = g_value_get_int (value); + break; + case PROP_RDP_NEGOTIATE_PORT: + priv->rdp.negotiate_port = g_value_get_boolean (value); + break; + case PROP_VNC_PORT: + priv->vnc.port = g_value_get_int (value); + break; + case PROP_VNC_NEGOTIATE_PORT: + priv->vnc.negotiate_port = g_value_get_boolean (value); + break; + case PROP_RDP_ENABLED: + priv->rdp.is_enabled = g_value_get_boolean (value); + break; + case PROP_VNC_ENABLED: + priv->vnc.is_enabled = g_value_get_boolean (value); + break; + case PROP_RDP_VIEW_ONLY: + priv->rdp.view_only = g_value_get_boolean (value); + break; + case PROP_VNC_VIEW_ONLY: + priv->vnc.view_only = g_value_get_boolean (value); + break; + case PROP_RDP_SCREEN_SHARE_MODE: + priv->rdp.screen_share_mode = g_value_get_enum (value); + break; + case PROP_VNC_SCREEN_SHARE_MODE: + priv->vnc.screen_share_mode = g_value_get_enum (value); + break; + case PROP_RDP_SERVER_CERT: + g_clear_pointer (&priv->rdp.server_cert, g_free); + priv->rdp.server_cert = g_strdup (g_value_get_string (value)); + break; + case PROP_RDP_SERVER_FINGERPRINT: + g_clear_pointer (&priv->rdp.server_fingerprint, g_free); + priv->rdp.server_fingerprint = g_strdup (g_value_get_string (value)); + break; + case PROP_RDP_SERVER_KEY: + g_clear_pointer (&priv->rdp.server_key, g_free); + priv->rdp.server_key = g_strdup (g_value_get_string (value)); + break; + case PROP_RDP_SERVER_CERT_PATH: + g_clear_pointer (&priv->rdp.server_cert_path, g_free); + priv->rdp.server_cert_path = g_strdup (g_value_get_string (value)); + update_rdp_server_cert (settings); + update_rdp_server_fingerprint (settings); + break; + case PROP_RDP_SERVER_KEY_PATH: + g_clear_pointer (&priv->rdp.server_key_path, g_free); + priv->rdp.server_key_path = g_strdup (g_value_get_string (value)); + update_rdp_server_key (settings); + break; + case PROP_VNC_AUTH_METHOD: + priv->vnc.auth_method = g_value_get_enum (value); + break; + case PROP_RDP_AUTH_METHODS: + priv->rdp.auth_methods = g_value_get_flags (value); + break; + case PROP_RDP_KERBEROS_KEYTAB: + g_clear_pointer (&priv->rdp.kerberos_keytab, g_free); + priv->rdp.kerberos_keytab = g_value_dup_string (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +grd_settings_init (GrdSettings *settings) +{ +} + +static void +grd_settings_class_init (GrdSettingsClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->constructed = grd_settings_constructed; + object_class->finalize = grd_settings_finalize; + object_class->get_property = grd_settings_get_property; + object_class->set_property = grd_settings_set_property; + + g_object_class_install_property (object_class, + PROP_RUNTIME_MODE, + g_param_spec_enum ("runtime-mode", + "runtime mode", + "runtime mode", + GRD_TYPE_RUNTIME_MODE, + GRD_RUNTIME_MODE_SCREEN_SHARE, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT | + G_PARAM_STATIC_STRINGS)); + g_object_class_install_property (object_class, + PROP_RDP_PORT, + g_param_spec_int ("rdp-port", + "rdp port", + "rdp port", + 0, + G_MAXUINT16, + GRD_DEFAULT_RDP_SERVER_PORT, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT | + G_PARAM_STATIC_STRINGS)); + g_object_class_install_property (object_class, + PROP_RDP_NEGOTIATE_PORT, + g_param_spec_boolean ("rdp-negotiate-port", + "rdp negotiate port", + "rdp negotiate port", + FALSE, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT | + G_PARAM_STATIC_STRINGS)); + g_object_class_install_property (object_class, + PROP_VNC_PORT, + g_param_spec_int ("vnc-port", + "vnc port", + "vnc port", + 0, + G_MAXUINT16, + GRD_DEFAULT_VNC_SERVER_PORT, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT | + G_PARAM_STATIC_STRINGS)); + g_object_class_install_property (object_class, + PROP_VNC_NEGOTIATE_PORT, + g_param_spec_boolean ("vnc-negotiate-port", + "vnc negotiate port", + "vnc negotiate port", + FALSE, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT | + G_PARAM_STATIC_STRINGS)); + g_object_class_install_property (object_class, + PROP_RDP_ENABLED, + g_param_spec_boolean ("rdp-enabled", + "rdp enabled", + "rdp enabled", + FALSE, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT | + G_PARAM_STATIC_STRINGS)); + g_object_class_install_property (object_class, + PROP_VNC_ENABLED, + g_param_spec_boolean ("vnc-enabled", + "vnc enabled", + "vnc enabled", + FALSE, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT | + G_PARAM_STATIC_STRINGS)); + g_object_class_install_property (object_class, + PROP_RDP_VIEW_ONLY, + g_param_spec_boolean ("rdp-view-only", + "rdp view only", + "rdp view only", + FALSE, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT | + G_PARAM_STATIC_STRINGS)); + g_object_class_install_property (object_class, + PROP_VNC_VIEW_ONLY, + g_param_spec_boolean ("vnc-view-only", + "vnc view only", + "vnc view only", + FALSE, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT | + G_PARAM_STATIC_STRINGS)); + g_object_class_install_property (object_class, + PROP_RDP_SCREEN_SHARE_MODE, + g_param_spec_enum ("rdp-screen-share-mode", + "rdp screen share mode", + "rdp screen share mode", + GRD_TYPE_RDP_SCREEN_SHARE_MODE, + GRD_RDP_SCREEN_SHARE_MODE_MIRROR_PRIMARY, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT | + G_PARAM_STATIC_STRINGS)); + g_object_class_install_property (object_class, + PROP_VNC_SCREEN_SHARE_MODE, + g_param_spec_enum ("vnc-screen-share-mode", + "vnc screen share mode", + "vnc screen share mode", + GRD_TYPE_VNC_SCREEN_SHARE_MODE, + GRD_VNC_SCREEN_SHARE_MODE_MIRROR_PRIMARY, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT | + G_PARAM_STATIC_STRINGS)); + g_object_class_install_property (object_class, + PROP_RDP_SERVER_CERT, + g_param_spec_string ("rdp-server-cert", + "rdp server cert", + "rdp server cert", + NULL, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT | + G_PARAM_STATIC_STRINGS)); + g_object_class_install_property (object_class, + PROP_RDP_SERVER_FINGERPRINT, + g_param_spec_string ("rdp-server-fingerprint", + "rdp server fingerprint", + "rdp server fingerprint", + NULL, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT | + G_PARAM_STATIC_STRINGS)); + g_object_class_install_property (object_class, + PROP_RDP_SERVER_KEY, + g_param_spec_string ("rdp-server-key", + "rdp server key", + "rdp server key", + NULL, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT | + G_PARAM_STATIC_STRINGS)); + g_object_class_install_property (object_class, + PROP_RDP_SERVER_CERT_PATH, + g_param_spec_string ("rdp-server-cert-path", + "rdp server cert path", + "rdp server cert path", + NULL, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT | + G_PARAM_STATIC_STRINGS)); + g_object_class_install_property (object_class, + PROP_RDP_SERVER_KEY_PATH, + g_param_spec_string ("rdp-server-key-path", + "rdp server key path", + "rdp server key path", + NULL, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT | + G_PARAM_STATIC_STRINGS)); + g_object_class_install_property (object_class, + PROP_VNC_AUTH_METHOD, + g_param_spec_enum ("vnc-auth-method", + "vnc auth method", + "vnc auth method", + GRD_TYPE_VNC_AUTH_METHOD, + GRD_VNC_AUTH_METHOD_PROMPT, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT | + G_PARAM_STATIC_STRINGS)); + g_object_class_install_property (object_class, + PROP_RDP_AUTH_METHODS, + g_param_spec_flags ("rdp-auth-methods", + "rdp auth methods", + "rdp auth methods", + GRD_TYPE_RDP_AUTH_METHODS, + GRD_RDP_AUTH_METHOD_CREDENTIALS, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT | + G_PARAM_STATIC_STRINGS)); + g_object_class_install_property (object_class, + PROP_RDP_KERBEROS_KEYTAB, + g_param_spec_string ("rdp-kerberos-keytab", + "rdp kerberos keypath", + "rdp kerberos keypath", + NULL, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT | + G_PARAM_STATIC_STRINGS)); +} diff --git a/grd-settings.h b/grd-settings.h new file mode 100644 index 0000000..94dfd3b --- /dev/null +++ b/grd-settings.h @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2018 Red Hat Inc. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + */ + +#pragma once + +#include + +#include "grd-enums.h" + +typedef struct _GrdSettings GrdSettings; + +#define GRD_TYPE_SETTINGS (grd_settings_get_type ()) +G_DECLARE_DERIVABLE_TYPE (GrdSettings, grd_settings, + GRD, SETTINGS, GObject) + +struct _GrdSettingsClass +{ + GObjectClass parent_class; +}; + +GrdRuntimeMode grd_settings_get_runtime_mode (GrdSettings *settings); + +void grd_settings_override_max_parallel_connections (GrdSettings *settings, + int max_parallel_connections); + +int grd_settings_get_max_parallel_connections (GrdSettings *settings); + +void grd_settings_override_rdp_port (GrdSettings *settings, + int port); + +void grd_settings_override_vnc_port (GrdSettings *settings, + int port); + +gboolean grd_settings_get_rdp_credentials (GrdSettings *settings, + char **username, + char **password, + GError **error); + +gboolean grd_settings_set_rdp_credentials (GrdSettings *settings, + const char *username, + const char *password, + GError **error); + +gboolean grd_settings_clear_rdp_credentials (GrdSettings *settings, + GError **error); + +void grd_settings_recreate_rdp_credentials (GrdSettings *settings); + +char *grd_settings_get_vnc_password (GrdSettings *settings, + GError **error); + +gboolean grd_settings_set_vnc_password (GrdSettings *settings, + const char *password, + GError **error); + +gboolean grd_settings_clear_vnc_password (GrdSettings *settings, + GError **error); diff --git a/grd-shell-dialog.c b/grd-shell-dialog.c new file mode 100644 index 0000000..fc1587e --- /dev/null +++ b/grd-shell-dialog.c @@ -0,0 +1,236 @@ +/* + * Copyright (C) 2024 SUSE Software Solutions Germany GmbH + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "grd-shell-dialog.h" + +#define SHELL_NAME "org.gnome.Shell" +#define SHELL_PORTAL_DESKTOP_PATH "/org/freedesktop/portal/desktop" +#define SHELL_PORTAL_DESKTOP_GRD_PATH "/org/freedesktop/portal/desktop/grd" +#define SHELL_PORTAL_ACCESS_INTERFACE "org.freedesktop.impl.portal.Access" +#define SHELL_PORTAL_REQUEST_INTERFACE "org.freedesktop.impl.portal.Request" + +enum +{ + ACCEPTED, + CANCELLED, + + N_SIGNALS +}; + +static guint signals[N_SIGNALS]; + +typedef enum _ShellDialogResponse +{ + OK = 0, + CANCEL = 1, + CLOSED = 2, +} ShellDialogResponse; + +typedef struct _GrdShellDialog +{ + GObject parent; + + GDBusProxy *access_proxy; + GDBusProxy *handle_proxy; + + GCancellable *cancellable; +} GrdShellDialog; + +G_DEFINE_TYPE (GrdShellDialog, grd_shell_dialog, G_TYPE_OBJECT) + +static void +ensure_dialog_closed_sync (GrdShellDialog *shell_dialog) +{ + g_autoptr (GError) error = NULL; + + if (!shell_dialog->handle_proxy) + return; + + if (!g_dbus_proxy_call_sync (shell_dialog->handle_proxy, + "Close", + NULL, + G_DBUS_CALL_FLAGS_NONE, + -1, + shell_dialog->cancellable, + &error)) + g_warning ("Unable to close shell dialog: %s", error->message); + + g_clear_object (&shell_dialog->handle_proxy); +} + +static void +on_shell_access_dialog_finished (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + GrdShellDialog *shell_dialog = GRD_SHELL_DIALOG (user_data); + g_autoptr (GError) error = NULL; + ShellDialogResponse response = 0; + GVariant *response_variant; + + g_clear_object (&shell_dialog->handle_proxy); + + response_variant = g_dbus_proxy_call_finish (G_DBUS_PROXY (source), + result, + &error); + if (!response_variant) + { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("Shell dialog failed: %s", error->message); + + return; + } + + g_variant_get (response_variant, "(ua{sv})", &response, NULL); + switch (response) + { + case OK: + g_signal_emit (shell_dialog, signals[ACCEPTED], 0); + break; + case CANCEL: + case CLOSED: + g_signal_emit (shell_dialog, signals[CANCELLED], 0); + break; + } +} + +void +grd_shell_dialog_open (GrdShellDialog *shell_dialog, + const char *title, + const char *description, + const char *cancel_label, + const char *accept_label) +{ + g_autoptr (GDBusProxy) handle_proxy = NULL; + g_autoptr (GError) error = NULL; + g_autofree char *handle_path = NULL; + GVariantBuilder builder; + + ensure_dialog_closed_sync (shell_dialog); + + handle_path = g_strdup_printf (SHELL_PORTAL_DESKTOP_GRD_PATH "/%u", + g_random_int ()); + + g_variant_builder_init (&builder, G_VARIANT_TYPE ("a{sv}")); + g_variant_builder_add (&builder, "{sv}", + "deny_label", g_variant_new_string (cancel_label)); + g_variant_builder_add (&builder, "{sv}", + "grant_label", g_variant_new_string (accept_label)); + + g_dbus_proxy_call (shell_dialog->access_proxy, + "AccessDialog", + g_variant_new ("(osssssa{sv})", + handle_path, + "", + "", + title, + description, + "", + &builder), + G_DBUS_CALL_FLAGS_NONE, + G_MAXINT, + shell_dialog->cancellable, + on_shell_access_dialog_finished, + shell_dialog); + + handle_proxy = g_dbus_proxy_new_for_bus_sync (G_BUS_TYPE_SESSION, + G_DBUS_PROXY_FLAGS_NONE, + NULL, + SHELL_NAME, + handle_path, + SHELL_PORTAL_REQUEST_INTERFACE, + shell_dialog->cancellable, + &error); + if (!handle_proxy) + { + g_warning ("Could not connect to the shell Request interface: %s", + error->message); + return; + } + + g_clear_object (&shell_dialog->handle_proxy); + shell_dialog->handle_proxy = g_steal_pointer (&handle_proxy); +} + +GrdShellDialog * +grd_shell_dialog_new (GCancellable *cancellable) +{ + g_autoptr (GError) error = NULL; + GrdShellDialog *shell_dialog; + GDBusProxy *access_proxy; + + access_proxy = g_dbus_proxy_new_for_bus_sync (G_BUS_TYPE_SESSION, + G_DBUS_PROXY_FLAGS_NONE, + NULL, + SHELL_NAME, + SHELL_PORTAL_DESKTOP_PATH, + SHELL_PORTAL_ACCESS_INTERFACE, + cancellable, + &error); + if (!access_proxy) + { + g_message ("Could not connect to the shell Access interface: %s", + error->message); + return NULL; + } + + shell_dialog = g_object_new (GRD_TYPE_SHELL_DIALOG, NULL); + shell_dialog->cancellable = cancellable; + shell_dialog->access_proxy = access_proxy; + + return shell_dialog; +} + +static void +grd_shell_dialog_dispose (GObject *object) +{ + GrdShellDialog *shell_dialog = GRD_SHELL_DIALOG (object); + + ensure_dialog_closed_sync (shell_dialog); + + g_clear_object (&shell_dialog->access_proxy); + + G_OBJECT_CLASS (grd_shell_dialog_parent_class)->dispose (object); +} + +static void +grd_shell_dialog_init (GrdShellDialog *shell_dialog) +{ +} + +static void +grd_shell_dialog_class_init (GrdShellDialogClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = grd_shell_dialog_dispose; + + signals[ACCEPTED] = g_signal_new ("accepted", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, 0); + signals[CANCELLED] = g_signal_new ("cancelled", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, 0); +} diff --git a/grd-shell-dialog.h b/grd-shell-dialog.h new file mode 100644 index 0000000..841d0c1 --- /dev/null +++ b/grd-shell-dialog.h @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2024 SUSE Software Solutions Germany GmbH + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + */ + +#pragma once + +#include +#include + +#define GRD_TYPE_SHELL_DIALOG (grd_shell_dialog_get_type ()) +G_DECLARE_FINAL_TYPE (GrdShellDialog, grd_shell_dialog, + GRD, SHELL_DIALOG, GObject) + +GrdShellDialog *grd_shell_dialog_new (GCancellable *cancellable); + +void grd_shell_dialog_open (GrdShellDialog *shell_dialog, + const char *title, + const char *description, + const char *cancel_label, + const char *accept_label); diff --git a/grd-stream.c b/grd-stream.c new file mode 100644 index 0000000..dedea11 --- /dev/null +++ b/grd-stream.c @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2017 Red Hat Inc. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + */ + +#include "config.h" + +#include "grd-stream.h" + +enum +{ + READY, + + N_SIGNALS +}; + +static guint signals[N_SIGNALS]; + +typedef struct _GrdStreamPrivate +{ + uint32_t stream_id; + uint32_t pipewire_node_id; + char *mapping_id; + + GrdDBusMutterScreenCastStream *proxy; + GCancellable *cancellable; + + unsigned long pipewire_stream_added_id; +} GrdStreamPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (GrdStream, grd_stream, G_TYPE_OBJECT) + +uint32_t +grd_stream_get_stream_id (GrdStream *stream) +{ + GrdStreamPrivate *priv = grd_stream_get_instance_private (stream); + + return priv->stream_id; +} + +uint32_t +grd_stream_get_pipewire_node_id (GrdStream *stream) +{ + GrdStreamPrivate *priv = grd_stream_get_instance_private (stream); + + return priv->pipewire_node_id; +} + +const char * +grd_stream_get_mapping_id (GrdStream *stream) +{ + GrdStreamPrivate *priv = grd_stream_get_instance_private (stream); + + return priv->mapping_id; +} + +void +grd_stream_disconnect_proxy_signals (GrdStream *stream) +{ + GrdStreamPrivate *priv = grd_stream_get_instance_private (stream); + + g_clear_signal_handler (&priv->pipewire_stream_added_id, priv->proxy); +} + +static void +on_pipewire_stream_added (GrdDBusMutterScreenCastStream *proxy, + unsigned int node_id, + GrdStream *stream) +{ + GrdStreamPrivate *priv = grd_stream_get_instance_private (stream); + + priv->pipewire_node_id = (uint32_t) node_id; + + g_signal_emit (stream, signals[READY], 0); +} + +GrdStream * +grd_stream_new (uint32_t stream_id, + GrdDBusMutterScreenCastStream *proxy, + GCancellable *cancellable, + GError **error) +{ + GrdStream *stream; + GrdStreamPrivate *priv; + GVariant *parameters; + char *mapping_id = NULL; + + parameters = grd_dbus_mutter_screen_cast_stream_get_parameters (proxy); + if (!parameters) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to get stream parameters for stream %u", stream_id); + return NULL; + } + + g_variant_lookup (parameters, "mapping-id", "s", &mapping_id); + if (!mapping_id) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Missing mapping-id for stream %u", stream_id); + return NULL; + } + + g_debug ("PipeWire stream %u mapping ID: %s", stream_id, mapping_id); + + stream = g_object_new (GRD_TYPE_STREAM, NULL); + priv = grd_stream_get_instance_private (stream); + + priv->stream_id = stream_id; + priv->mapping_id = mapping_id; + priv->proxy = g_object_ref (proxy); + priv->cancellable = g_object_ref (cancellable); + priv->pipewire_stream_added_id = + g_signal_connect (proxy, "pipewire-stream-added", + G_CALLBACK (on_pipewire_stream_added), + stream); + + return stream; +} + +static void +on_screen_cast_stream_stop_finished (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + GrdStream *stream = user_data; + GrdStreamPrivate *priv = grd_stream_get_instance_private (stream); + + g_debug ("Stream %u stopped", priv->stream_id); + + g_object_run_dispose (G_OBJECT (stream)); + g_object_unref (stream); +} + +void +grd_stream_destroy (GrdStream *stream) +{ + GrdStreamPrivate *priv = grd_stream_get_instance_private (stream); + + g_assert (priv->proxy); + g_assert (priv->cancellable); + + grd_stream_disconnect_proxy_signals (stream); + + g_debug ("Stopping stream %u", priv->stream_id); + + grd_dbus_mutter_screen_cast_stream_call_stop (priv->proxy, priv->cancellable, + on_screen_cast_stream_stop_finished, + stream); +} + +static void +grd_stream_finalize (GObject *object) +{ + GrdStream *stream = GRD_STREAM (object); + GrdStreamPrivate *priv = grd_stream_get_instance_private (stream); + + g_clear_pointer (&priv->mapping_id, g_free); + + G_OBJECT_CLASS (grd_stream_parent_class)->finalize (object); +} + +static void +grd_stream_dispose (GObject *object) +{ + GrdStream *stream = GRD_STREAM (object); + GrdStreamPrivate *priv = grd_stream_get_instance_private (stream); + + g_clear_object (&priv->cancellable); + g_clear_object (&priv->proxy); + + G_OBJECT_CLASS (grd_stream_parent_class)->finalize (object); +} + +static void +grd_stream_init (GrdStream *stream) +{ +} + +static void +grd_stream_class_init (GrdStreamClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = grd_stream_dispose; + object_class->finalize = grd_stream_finalize; + + signals[READY] = g_signal_new ("ready", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, 0); +} diff --git a/grd-stream.h b/grd-stream.h new file mode 100644 index 0000000..dc2a71d --- /dev/null +++ b/grd-stream.h @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2017 Red Hat Inc. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + */ + +#pragma once + +#include +#include + +#include "grd-dbus-mutter-screen-cast.h" +#include "grd-types.h" + +#define GRD_TYPE_STREAM (grd_stream_get_type ()) +G_DECLARE_DERIVABLE_TYPE (GrdStream, grd_stream, GRD, STREAM, GObject) + +struct _GrdStreamClass +{ + GObjectClass parent_class; +}; + +GrdStream *grd_stream_new (uint32_t stream_id, + GrdDBusMutterScreenCastStream *proxy, + GCancellable *cancellable, + GError **error); + +void grd_stream_destroy (GrdStream *stream); + +uint32_t grd_stream_get_stream_id (GrdStream *stream); + +uint32_t grd_stream_get_pipewire_node_id (GrdStream *stream); + +const char * grd_stream_get_mapping_id (GrdStream *stream); + +void grd_stream_disconnect_proxy_signals (GrdStream *stream); diff --git a/grd-throttler.c b/grd-throttler.c new file mode 100644 index 0000000..8f19783 --- /dev/null +++ b/grd-throttler.c @@ -0,0 +1,515 @@ +/* + * Copyright (C) 2025 Red Hat + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-throttler.h" + +#include "grd-context.h" +#include "grd-settings.h" +#include "grd-utils.h" + +#define DEFAULT_MAX_CONNECTIONS_PER_PEER 5 +#define DEFAULT_MAX_PENDING_CONNECTIONS 5 +#define DEFAULT_MAX_ATTEMPTS_PER_SECOND 10 + +struct _GrdThrottlerLimits +{ + int max_global_connections; + int max_connections_per_peer; + int max_pending_connections; + int max_attempts_per_second; +}; + +#define PRUNE_TIME_CUTOFF_US (s2us (1)) + +typedef struct _GrdPeer +{ + GrdThrottler *throttler; + char *name; + int active_connections; + GArray *connect_timestamps; + GQueue *delayed_connections; + int64_t last_accept_us; +} GrdPeer; + +struct _GrdThrottler +{ + GObject parent; + + GrdThrottlerLimits *limits; + + int active_connections; + + GrdThrottlerAllowCallback allow_callback; + gpointer user_data; + + GHashTable *peers; + + GSource *delayed_connections_source; +}; + +G_DEFINE_TYPE (GrdThrottler, grd_throttler, G_TYPE_OBJECT) + +static GQuark quark_remote_address; + +static void +maybe_queue_timeout (GrdThrottler *throttler); + +static void +prune_old_timestamps (GArray *timestamps, + int64_t now_us) +{ + size_t i; + + if (!timestamps) + return; + + /* Prune all timestamps older than 1 second, so we can determine how many + * connections that happened the last second by looking at how many timestamps + * we have. + */ + for (i = 0; i < timestamps->len; i++) + { + int64_t ts_us = g_array_index (timestamps, int64_t, i); + + if (now_us - ts_us < PRUNE_TIME_CUTOFF_US) + break; + } + g_array_remove_range (timestamps, 0, i); +} + +static void +maybe_dispose_peer (GrdThrottler *throttler, + GrdPeer *peer, + GHashTableIter *iter) +{ + if (peer->active_connections > 0) + return; + + if (peer->connect_timestamps && peer->connect_timestamps->len > 0) + return; + + if (!g_queue_is_empty (peer->delayed_connections)) + return; + + if (iter) + g_hash_table_iter_remove (iter); + else + g_hash_table_remove (throttler->peers, peer->name); +} + +static void +grd_throttler_register_connection (GrdThrottler *throttler, + GrdPeer *peer) +{ + int64_t now_us; + + peer->active_connections++; + throttler->active_connections++; + + if (!peer->connect_timestamps) + peer->connect_timestamps = g_array_new (FALSE, FALSE, sizeof (int64_t)); + + now_us = g_get_monotonic_time (); + + prune_old_timestamps (peer->connect_timestamps, now_us); + g_array_append_val (peer->connect_timestamps, now_us); +} + +static void +grd_throttler_unregister_connection (GrdThrottler *throttler, + GSocketConnection *connection, + GrdPeer *peer) +{ + g_assert (peer->active_connections > 0); + g_assert (throttler->active_connections > 0); + + peer->active_connections--; + throttler->active_connections--; + + maybe_dispose_peer (throttler, peer, NULL); + maybe_queue_timeout (throttler); +} + +static void +grd_throttler_deny_connection (GrdThrottler *throttler, + const char *peer_name, + GSocketConnection *connection) +{ + g_debug ("Denying connection from %s", peer_name); + g_io_stream_close (G_IO_STREAM (connection), NULL, NULL); +} + +static void +on_connection_closed_changed (GSocketConnection *connection, + GParamSpec *pspec, + GrdThrottler *throttler) +{ + const char *peer_name; + GrdPeer *peer; + + g_assert (g_io_stream_is_closed (G_IO_STREAM (connection))); + + peer_name = g_object_get_qdata (G_OBJECT (connection), quark_remote_address); + peer = g_hash_table_lookup (throttler->peers, peer_name); + grd_throttler_unregister_connection (throttler, connection, peer); +} + +static void +grd_throttler_allow_connection (GrdThrottler *throttler, + GSocketConnection *connection, + GrdPeer *peer) +{ + g_debug ("Accepting connection from %s", peer->name); + + throttler->allow_callback (throttler, connection, throttler->user_data); + + peer->last_accept_us = g_get_monotonic_time (); + + g_object_set_qdata_full (G_OBJECT (connection), quark_remote_address, + g_strdup (peer->name), g_free); + grd_throttler_register_connection (throttler, peer); + g_signal_connect (connection, "notify::closed", + G_CALLBACK (on_connection_closed_changed), throttler); +} + +static gboolean +source_dispatch (GSource *source, + GSourceFunc callback, + gpointer user_data) +{ + g_source_set_ready_time (source, -1); + + return callback (user_data); +} + +static GSourceFuncs source_funcs = +{ + .dispatch = source_dispatch, +}; + +static void +prune_closed_connections (GQueue *queue) +{ + GList *l; + + l = queue->head; + while (l) + { + GSocketConnection *connection = G_SOCKET_CONNECTION (l->data); + GList *l_next = l->next; + + if (g_io_stream_is_closed (G_IO_STREAM (connection))) + { + g_queue_delete_link (queue, l); + g_object_unref (connection); + } + + l = l_next; + } +} + +static gboolean +is_connection_limit_reached (GrdThrottler *throttler, + GrdPeer *peer) +{ + GrdThrottlerLimits *limits = throttler->limits; + + if (peer->active_connections >= limits->max_connections_per_peer) + return TRUE; + + if (throttler->active_connections >= limits->max_global_connections) + return TRUE; + + return FALSE; +} + +static gboolean +is_new_connection_allowed (GrdThrottler *throttler, + GrdPeer *peer) +{ + GrdThrottlerLimits *limits = throttler->limits; + + if (is_connection_limit_reached (throttler, peer)) + return FALSE; + + if (peer->connect_timestamps && + peer->connect_timestamps->len >= limits->max_attempts_per_second) + return FALSE; + + return TRUE; +} + +static gboolean +dispatch_delayed_connections (gpointer user_data) +{ + GrdThrottler *throttler = GRD_THROTTLER (user_data); + GHashTableIter iter; + gpointer key, value; + + g_hash_table_iter_init (&iter, throttler->peers); + while (g_hash_table_iter_next (&iter, &key, &value)) + { + GrdPeer *peer = value; + GQueue *queue = peer->delayed_connections; + GSocketConnection *connection; + + prune_closed_connections (queue); + connection = g_queue_peek_head (queue); + + if (!connection) + { + maybe_dispose_peer (throttler, peer, &iter); + continue; + } + + if (is_new_connection_allowed (throttler, peer)) + { + g_queue_pop_head (queue); + grd_throttler_allow_connection (throttler, connection, peer); + g_object_unref (connection); + } + } + + maybe_queue_timeout (throttler); + + return G_SOURCE_CONTINUE; +} + +static void +ensure_delayed_connections_source (GrdThrottler *throttler) +{ + if (throttler->delayed_connections_source) + return; + + throttler->delayed_connections_source = g_source_new (&source_funcs, + sizeof (GSource)); + g_source_set_callback (throttler->delayed_connections_source, + dispatch_delayed_connections, throttler, NULL); + g_source_attach (throttler->delayed_connections_source, NULL); + g_source_unref (throttler->delayed_connections_source); +} + +static void +maybe_queue_timeout (GrdThrottler *throttler) +{ + GrdThrottlerLimits *limits = throttler->limits; + GHashTableIter iter; + gpointer key, value; + int64_t next_timeout_us = INT64_MAX; + + g_hash_table_iter_init (&iter, throttler->peers); + while (g_hash_table_iter_next (&iter, &key, &value)) + { + GrdPeer *peer = value; + + if (is_connection_limit_reached (throttler, peer)) + continue; + + if (g_queue_is_empty (peer->delayed_connections)) + continue; + + next_timeout_us = MIN (next_timeout_us, + peer->last_accept_us + + G_USEC_PER_SEC / limits->max_attempts_per_second); + } + + + if (next_timeout_us == INT64_MAX) + next_timeout_us = -1; + + if (next_timeout_us >= 0) + { + ensure_delayed_connections_source (throttler); + g_source_set_ready_time (throttler->delayed_connections_source, + next_timeout_us); + } +} + +static void +maybe_delay_connection (GrdThrottler *throttler, + GSocketConnection *connection, + GrdPeer *peer, + int64_t now_us) +{ + GrdThrottlerLimits *limits = throttler->limits; + GQueue *delayed_connections; + + delayed_connections = peer->delayed_connections; + if (!delayed_connections) + { + delayed_connections = g_queue_new (); + peer->delayed_connections = delayed_connections; + } + + if (g_queue_get_length (delayed_connections) > limits->max_pending_connections) + { + grd_throttler_deny_connection (throttler, peer->name, connection); + return; + } + + g_debug ("Delaying connection from %s", peer->name); + + g_queue_push_tail (delayed_connections, g_object_ref (connection)); + maybe_queue_timeout (throttler); +} + +static GrdPeer * +ensure_peer (GrdThrottler *throttler, + const char *peer_name) +{ + GrdPeer *peer; + + peer = g_hash_table_lookup (throttler->peers, peer_name); + if (peer) + return peer; + + peer = g_new0 (GrdPeer, 1); + peer->throttler = throttler; + peer->name = g_strdup (peer_name); + peer->delayed_connections = g_queue_new (); + + g_hash_table_insert (throttler->peers, + g_strdup (peer_name), peer); + + return peer; +} + +void +grd_throttler_handle_connection (GrdThrottler *throttler, + GSocketConnection *connection) +{ + g_autoptr (GError) error = NULL; + g_autoptr (GSocketAddress) remote_address = NULL; + GInetAddress *inet_address; + g_autofree char *peer_name = NULL; + GrdPeer *peer; + int64_t now_us; + + remote_address = g_socket_connection_get_remote_address (connection, &error); + if (!remote_address) + { + g_warning ("Failed to get remote address: %s", error->message); + grd_throttler_deny_connection (throttler, "unknown peer", connection); + return; + } + + inet_address = + g_inet_socket_address_get_address (G_INET_SOCKET_ADDRESS (remote_address)); + peer_name = g_inet_address_to_string (inet_address); + + g_debug ("New incoming connection from %s", peer_name); + + peer = ensure_peer (throttler, peer_name); + + prune_closed_connections (peer->delayed_connections); + + now_us = g_get_monotonic_time (); + prune_old_timestamps (peer->connect_timestamps, now_us); + if (is_new_connection_allowed (throttler, peer) && + g_queue_get_length (peer->delayed_connections) == 0) + { + grd_throttler_allow_connection (throttler, connection, peer); + return; + } + + maybe_delay_connection (throttler, connection, peer, now_us); +} + +void +grd_throttler_limits_set_max_global_connections (GrdThrottlerLimits *limits, + int limit) +{ + limits->max_global_connections = limit; +} + +GrdThrottlerLimits * +grd_throttler_limits_new (GrdContext *context) +{ + GrdSettings *settings = grd_context_get_settings (context); + GrdThrottlerLimits *limits; + + limits = g_new0 (GrdThrottlerLimits, 1); + limits->max_global_connections = + grd_settings_get_max_parallel_connections (settings); + limits->max_connections_per_peer = DEFAULT_MAX_CONNECTIONS_PER_PEER; + limits->max_pending_connections = DEFAULT_MAX_PENDING_CONNECTIONS; + limits->max_attempts_per_second = DEFAULT_MAX_ATTEMPTS_PER_SECOND; + + return limits; +} + +GrdThrottler * +grd_throttler_new (GrdThrottlerLimits *limits, + GrdThrottlerAllowCallback allow_callback, + gpointer user_data) +{ + GrdThrottler *throttler; + + g_assert (limits); + + throttler = g_object_new (GRD_TYPE_THROTTLER, NULL); + throttler->allow_callback = allow_callback; + throttler->user_data = user_data; + throttler->limits = limits; + + return throttler; +} + +static void +grd_peer_free (GrdPeer *peer) +{ + if (peer->delayed_connections) + g_queue_free_full (peer->delayed_connections, g_object_unref); + g_clear_pointer (&peer->connect_timestamps, g_array_unref); + g_clear_pointer (&peer->name, g_free); + g_free (peer); +} + +static void +grd_throttler_finalize (GObject *object) +{ + GrdThrottler *throttler = GRD_THROTTLER(object); + + g_clear_pointer (&throttler->delayed_connections_source, g_source_destroy); + g_clear_pointer (&throttler->peers, g_hash_table_unref); + g_clear_pointer (&throttler->limits, g_free); + + G_OBJECT_CLASS (grd_throttler_parent_class)->finalize (object); +} + +static void +grd_throttler_init (GrdThrottler *throttler) +{ + throttler->peers = + g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, (GDestroyNotify) grd_peer_free); +} + +static void +grd_throttler_class_init (GrdThrottlerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = grd_throttler_finalize; + + quark_remote_address = + g_quark_from_static_string ("grd-remote-address-string"); +} diff --git a/grd-throttler.h b/grd-throttler.h new file mode 100644 index 0000000..a955420 --- /dev/null +++ b/grd-throttler.h @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2025 Red Hat + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#ifndef GRD_THROTTLER_H +#define GRD_THROTTLER_H + +#include +#include + +#include "grd-types.h" + +typedef struct _GrdThrottlerLimits GrdThrottlerLimits; + +#define GRD_TYPE_THROTTLER (grd_throttler_get_type()) +G_DECLARE_FINAL_TYPE (GrdThrottler, grd_throttler, GRD, THROTTLER, GObject) + +typedef void (* GrdThrottlerAllowCallback) (GrdThrottler *throttler, + GSocketConnection *connection, + gpointer user_data); + +void +grd_throttler_handle_connection (GrdThrottler *throttler, + GSocketConnection *connection); + +void +grd_throttler_limits_set_max_global_connections (GrdThrottlerLimits *limits, + int limit); + +GrdThrottlerLimits * +grd_throttler_limits_new (GrdContext *context); + +GrdThrottler * +grd_throttler_new (GrdThrottlerLimits *limits, + GrdThrottlerAllowCallback allow_callback, + gpointer user_data); + +#endif /* GRD_THROTTLER_H */ diff --git a/grd-tpm.c b/grd-tpm.c new file mode 100644 index 0000000..7cce0bd --- /dev/null +++ b/grd-tpm.c @@ -0,0 +1,809 @@ +/* + * Copyright (C) 2022 Red Hat Inc. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + */ + +#include "config.h" + +#include "grd-tpm.h" + +#include +#include + +G_GNUC_BEGIN_IGNORE_DEPRECATIONS +#include +#include +#include +#include +G_GNUC_END_IGNORE_DEPRECATIONS + +#include "grd-debug.h" + +struct _GrdTpm +{ + GObject parent; + + TSS2_TCTI_CONTEXT *tcti_context; + ESYS_CONTEXT *esys_context; + + ESYS_TR hmac_session; + ESYS_TR trial_session; + ESYS_TR policy_session; +}; + +G_DEFINE_TYPE (GrdTpm, grd_tpm, G_TYPE_OBJECT) + +static gboolean +check_tpm_existence (GError **error) +{ + g_autoptr (GFile) tpm_dev = NULL; + g_autoptr (GFile) tpmrm_dev = NULL; + + tpm_dev = g_file_new_for_path ("/dev/tpm0"); + tpmrm_dev = g_file_new_for_path ("/dev/tpmrm0"); + if (!g_file_query_exists (tpm_dev, NULL) || + !g_file_query_exists (tpmrm_dev, NULL)) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND, + "No TPM device found"); + return FALSE; + } + + return TRUE; +} + +static gboolean +init_transmission_interface (GrdTpm *tpm, + GError **error) +{ + TSS2_RC rc; + + rc = Tss2_TctiLdr_Initialize (NULL, &tpm->tcti_context); + + if (rc != TSS2_RC_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to initialize transmission interface context: %s", + Tss2_RC_Decode (rc)); + return FALSE; + } + + return TRUE; +} + +static gboolean +init_tss2_esys (GrdTpm *tpm, + GError **error) +{ + TSS2_RC rc; + + rc = Esys_Initialize (&tpm->esys_context, tpm->tcti_context, NULL); + if (rc != TSS2_RC_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to initialize ESYS context: %s", + Tss2_RC_Decode (rc)); + return FALSE; + } + + return TRUE; +} + +static gboolean +start_hmac_session (GrdTpm *tpm, + GError **error) +{ + TPMT_SYM_DEF symmetric = { + .algorithm = TPM2_ALG_NULL, + }; + TSS2_RC rc; + + rc = Esys_StartAuthSession (tpm->esys_context, + ESYS_TR_NONE, + ESYS_TR_NONE, + ESYS_TR_NONE, + ESYS_TR_NONE, + ESYS_TR_NONE, + NULL, + TPM2_SE_HMAC, + &symmetric, + TPM2_ALG_SHA256, + &tpm->hmac_session); + if (rc != TSS2_RC_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to start auth session: %s (%d)", + Tss2_RC_Decode (rc), rc); + return FALSE; + } + + return TRUE; +} + +static gboolean +start_trial_session (GrdTpm *tpm, + GError **error) +{ + TPMT_SYM_DEF symmetric = { + .algorithm = TPM2_ALG_NULL, + }; + TSS2_RC rc; + + rc = Esys_StartAuthSession (tpm->esys_context, + ESYS_TR_NONE, + ESYS_TR_NONE, + ESYS_TR_NONE, + ESYS_TR_NONE, + ESYS_TR_NONE, + NULL, + TPM2_SE_TRIAL, + &symmetric, + TPM2_ALG_SHA256, + &tpm->trial_session); + if (rc != TSS2_RC_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to start policy session: %s (%d)", + Tss2_RC_Decode (rc), rc); + return FALSE; + } + + return TRUE; +} + +static gboolean +start_policy_session (GrdTpm *tpm, + GError **error) +{ + TPMT_SYM_DEF symmetric = { + .algorithm = TPM2_ALG_NULL, + }; + TSS2_RC rc; + + rc = Esys_StartAuthSession (tpm->esys_context, + ESYS_TR_NONE, + ESYS_TR_NONE, + ESYS_TR_NONE, + ESYS_TR_NONE, + ESYS_TR_NONE, + NULL, + TPM2_SE_POLICY, + &symmetric, + TPM2_ALG_SHA256, + &tpm->policy_session); + if (rc != TSS2_RC_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to start policy session: %s (%d)", + Tss2_RC_Decode (rc), rc); + return FALSE; + } + + return TRUE; +} + +gboolean +grd_tpm_create_primary (GrdTpm *tpm, + ESYS_TR *primary_handle_out, + GError **error) +{ + TPM2B_SENSITIVE_CREATE in_sensitive_primary = {}; + TPM2B_PUBLIC in_public = { + .publicArea = { + .type = TPM2_ALG_RSA, + .nameAlg = TPM2_ALG_SHA256, + .objectAttributes = (TPMA_OBJECT_RESTRICTED | + TPMA_OBJECT_DECRYPT | + TPMA_OBJECT_FIXEDTPM | + TPMA_OBJECT_FIXEDPARENT | + TPMA_OBJECT_SENSITIVEDATAORIGIN | + TPMA_OBJECT_USERWITHAUTH), + }, + }; + TPM2B_DATA outside_info = {}; + TPML_PCR_SELECTION creation_pcr = {}; + TSS2_RC rc; + + g_assert (tpm->hmac_session); + + in_public.publicArea.parameters.rsaDetail.keyBits = 2048; + in_public.publicArea.parameters.asymDetail.scheme.scheme = TPM2_ALG_NULL; + in_public.publicArea.parameters.symDetail.sym.algorithm = TPM2_ALG_AES; + in_public.publicArea.parameters.symDetail.sym.keyBits.sym = 128; + in_public.publicArea.parameters.symDetail.sym.mode.sym = TPM2_ALG_CFB; + + rc = Esys_CreatePrimary (tpm->esys_context, + ESYS_TR_RH_OWNER, + tpm->hmac_session, + ESYS_TR_NONE, + ESYS_TR_NONE, + &in_sensitive_primary, + &in_public, + &outside_info, + &creation_pcr, + primary_handle_out, + NULL, NULL, NULL, NULL); + if (rc != TSS2_RC_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Esys_CreatePrimary failed: %s", + Tss2_RC_Decode (rc)); + return FALSE; + } + + return TRUE; +} + +gboolean +grd_tpm_read_pcr (GrdTpm *tpm, + TPML_PCR_SELECTION **pcr_selection_out, + TPML_DIGEST **pcr_digest_out, + GError **error) +{ + TPML_PCR_SELECTION pcr_selection_in = { + .count = 1, + .pcrSelections = { + { + .hash = TPM2_ALG_SHA256, + .sizeofSelect = 3, + .pcrSelect = { 1 << 0 | 1 << 1 | 1 << 2 | 1 << 3, }, + }, + } + }; + TSS2_RC rc; + + rc = Esys_PCR_Read (tpm->esys_context, + ESYS_TR_NONE, + ESYS_TR_NONE, + ESYS_TR_NONE, + &pcr_selection_in, + NULL, + pcr_selection_out, + pcr_digest_out); + if (rc != TSS2_RC_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Esys_PCR_Read failed: %s", + Tss2_RC_Decode (rc)); + return FALSE; + } + + return TRUE; +} + +gboolean +do_create_policy (GrdTpm *tpm, + ESYS_TR session, + TPML_PCR_SELECTION *pcr_selection, + TPML_DIGEST *pcr_digest, + TPM2B_DIGEST **policy_digest_out, + GError **error) +{ + g_autoptr (GChecksum) checksum = NULL; + TPM2B_DIGEST pcr_hash_digest = {}; + size_t size; + int i; + TSS2_RC rc; + + checksum = g_checksum_new (G_CHECKSUM_SHA256); + for (i = 0; i < pcr_digest->count; i++) + { + g_checksum_update (checksum, + pcr_digest->digests[i].buffer, + pcr_digest->digests[i].size); + } + + size = G_N_ELEMENTS (pcr_hash_digest.buffer); + g_checksum_get_digest (checksum, + (uint8_t *) &pcr_hash_digest.buffer, + &size); + pcr_hash_digest.size = size; + + rc = Esys_PolicyPCR (tpm->esys_context, session, + ESYS_TR_NONE, + ESYS_TR_NONE, + ESYS_TR_NONE, + &pcr_hash_digest, + pcr_selection); + if (rc != TSS2_RC_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Esys_PolicyPCR failed: %s", + Tss2_RC_Decode (rc)); + return FALSE; + } + + rc = Esys_PolicyGetDigest (tpm->esys_context, session, + ESYS_TR_NONE, + ESYS_TR_NONE, + ESYS_TR_NONE, + policy_digest_out); + if (rc != TSS2_RC_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Esys_PolicyGetDigest failed: %s", + Tss2_RC_Decode (rc)); + return FALSE; + } + + return TRUE; +} + +gboolean +grd_tpm_store_secret (GrdTpm *tpm, + const char *secret, + ESYS_TR primary_handle, + TPML_PCR_SELECTION *pcr_selection, + TPML_DIGEST *pcr_digest, + TPMS_CONTEXT **secret_tpms_context_out, + GError **error) +{ + size_t secret_size; + size_t offset = 0; + g_autofree TPM2B_DIGEST *policy_digest = NULL; + TPM2B_TEMPLATE template = {}; + TPM2B_PUBLIC template_public; + TPM2B_SENSITIVE_CREATE in_sensitive; + ESYS_TR secret_handle; + TSS2_RC rc; + + g_assert (tpm->trial_session); + + secret_size = strlen (secret) + 1; + if (secret_size > G_N_ELEMENTS (in_sensitive.sensitive.data.buffer)) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Secret too large (%lu exceeds %lu max)", + secret_size, + G_N_ELEMENTS (in_sensitive.sensitive.data.buffer)); + return FALSE; + } + + if (!do_create_policy (tpm, tpm->trial_session, + pcr_selection, + pcr_digest, + &policy_digest, + error)) + return FALSE; + + template_public = (TPM2B_PUBLIC) { + .publicArea = { + .type = TPM2_ALG_KEYEDHASH, + .nameAlg = TPM2_ALG_SHA256, + .objectAttributes = TPMA_OBJECT_FIXEDTPM | TPMA_OBJECT_FIXEDPARENT, + .parameters.keyedHashDetail.scheme.scheme = TPM2_ALG_NULL, + }, + }; + memcpy (template_public.publicArea.authPolicy.buffer, + policy_digest->buffer, + policy_digest->size); + template_public.publicArea.authPolicy.size = policy_digest->size; + + rc = Tss2_MU_TPMT_PUBLIC_Marshal (&template_public.publicArea, + template.buffer, + G_N_ELEMENTS (template.buffer), + &offset); + if (rc != TSS2_RC_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to marshal public template: %s", + Tss2_RC_Decode (rc)); + return FALSE; + } + template.size = offset; + + in_sensitive = (TPM2B_SENSITIVE_CREATE) { + .sensitive.data.size = secret_size, + }; + memcpy (in_sensitive.sensitive.data.buffer, secret, secret_size); + + rc = Esys_CreateLoaded (tpm->esys_context, + primary_handle, + tpm->hmac_session, + ESYS_TR_NONE, + ESYS_TR_NONE, + &in_sensitive, + &template, + &secret_handle, + NULL, NULL); + if (rc != TSS2_RC_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Esys_CreateLoaded failed: %s", + Tss2_RC_Decode (rc)); + return FALSE; + } + + rc = Esys_ContextSave (tpm->esys_context, + secret_handle, + secret_tpms_context_out); + if (rc != TSS2_RC_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Esys_ContextSave failed: %s", + Tss2_RC_Decode (rc)); + return FALSE; + } + + return TRUE; +} + +GVariant * +grd_tpms_context_to_variant (TPMS_CONTEXT *tpms_context) +{ + g_autofree char *base64 = NULL; + GVariant *variant; + + base64 = g_base64_encode (tpms_context->contextBlob.buffer, + tpms_context->contextBlob.size); + + variant = g_variant_new ("(uutqs)", + tpms_context->hierarchy, + tpms_context->savedHandle, + tpms_context->sequence, + tpms_context->contextBlob.size, + base64); + + return variant; +} + +static gboolean +decode_variant (GVariant *variant, + TPMS_CONTEXT *tpms_context, + GError **error) +{ + g_autofree char *base64 = NULL; + g_autofree uint8_t *decoded = NULL; + size_t size; + + if (!g_variant_is_of_type (variant, G_VARIANT_TYPE ("(uutqs)"))) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Invalid tpms context variant type"); + return FALSE; + } + g_variant_get (variant, "(uutqs)", + &tpms_context->hierarchy, + &tpms_context->savedHandle, + &tpms_context->sequence, + &tpms_context->contextBlob.size, + &base64); + + decoded = g_base64_decode (base64, &size); + if (size != tpms_context->contextBlob.size) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Invalid tpms context blob length"); + return FALSE; + } + + memcpy (tpms_context->contextBlob.buffer, decoded, size); + + return TRUE; +} + +char * +grd_tpm_restore_secret (GrdTpm *tpm, + GVariant *variant, + TPML_PCR_SELECTION *pcr_selection, + TPML_DIGEST *pcr_digest, + GError **error) +{ + TSS2_RC rc; + TPMS_CONTEXT secret_tpms_context; + ESYS_TR loaded_secret_handle; + TPM2B_SENSITIVE_DATA *unsealed_data; + + g_assert (tpm->policy_session); + + if (!decode_variant (variant, &secret_tpms_context, error)) + return NULL; + + rc = Esys_ContextLoad (tpm->esys_context, &secret_tpms_context, + &loaded_secret_handle); + if (rc != TSS2_RC_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Esys_ContextLoad failed: %s", + Tss2_RC_Decode (rc)); + return NULL; + } + + if (!do_create_policy (tpm, tpm->policy_session, + pcr_selection, + pcr_digest, + NULL, + error)) + return NULL; + + rc = Esys_Unseal (tpm->esys_context, + loaded_secret_handle, + tpm->policy_session, + ESYS_TR_NONE, + ESYS_TR_NONE, + &unsealed_data); + if (rc != TSS2_RC_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Esys_Unseal failed: %s", + Tss2_RC_Decode (rc)); + return NULL; + } + + if (strnlen ((char *) unsealed_data->buffer, unsealed_data->size) != + unsealed_data->size - 1) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Secret not a zero terminated string"); + return NULL; + } + + return g_strdup ((char *) unsealed_data->buffer); +} + +static const char * +get_algorithm_name (uint32_t algorithm) +{ + switch (algorithm) + { + case TPM2_ALG_SHA256: + return "SHA-256"; + case TPM2_ALG_RSA: + return "RSA"; + default: + return "unknown"; + } +} + +static const char * +get_command_name (uint32_t command) +{ + switch (command) + { + case TPM2_CC_CreatePrimary: + return "CreatePrimary"; + case TPM2_CC_StartAuthSession: + return "StartAuthSession"; + case TPM2_CC_FlushContext: + return "FlushContext"; + case TPM2_CC_PCR_Read: + return "PCR_Read"; + case TPM2_CC_PolicyPCR: + return "PolicyPCR"; + case TPM2_CC_PolicyGetDigest: + return "PolicyGetDigest"; + case TPM2_CC_CreateLoaded: + return "CreateLoaded"; + case TPM2_CC_ContextSave: + return "ContextSave"; + case TPM2_CC_ContextLoad: + return "ContextLoad"; + case TPM2_CC_Unseal: + return "Unseal"; + default: + return "unkown"; + } +} + +gboolean +grd_tpm_check_capabilities (GrdTpm *tpm, + GError **error) +{ + const struct + { + TPM2_CAP capability; + uint32_t property; + int count; + } capabilities[] = { + { .capability = TPM2_CAP_ALGS, .property = TPM2_ALG_SHA256 }, + { .capability = TPM2_CAP_ALGS, .property = TPM2_ALG_RSA }, + { .capability = TPM2_CAP_COMMANDS, .property = TPM2_CC_CreatePrimary }, + { .capability = TPM2_CAP_COMMANDS, .property = TPM2_CC_StartAuthSession }, + { .capability = TPM2_CAP_COMMANDS, .property = TPM2_CC_FlushContext }, + { .capability = TPM2_CAP_COMMANDS, .property = TPM2_CC_PCR_Read }, + { .capability = TPM2_CAP_COMMANDS, .property = TPM2_CC_PolicyPCR }, + { .capability = TPM2_CAP_COMMANDS, .property = TPM2_CC_PolicyGetDigest }, + { .capability = TPM2_CAP_COMMANDS, .property = TPM2_CC_CreateLoaded }, + { .capability = TPM2_CAP_COMMANDS, .property = TPM2_CC_ContextSave }, + { .capability = TPM2_CAP_COMMANDS, .property = TPM2_CC_ContextLoad }, + }; + gboolean sha256_pcrs_found = FALSE; + uint32_t property = 0; + uint32_t count = TPM2_MAX_TPM_PROPERTIES; + int i; + + for (i = 0; i < G_N_ELEMENTS (capabilities); i++) + { + g_autofree TPMS_CAPABILITY_DATA *data = NULL; + TSS2_RC rc; + + rc = Esys_GetCapability (tpm->esys_context, + ESYS_TR_NONE, ESYS_TR_NONE, ESYS_TR_NONE, + capabilities[i].capability, + capabilities[i].property, + 1, + NULL, + &data); + if (rc != TSS2_RC_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to get capability: %s", + Tss2_RC_Decode (rc)); + return FALSE; + } + + switch (capabilities[i].capability) + { + case TPM2_CAP_ALGS: + g_warn_if_fail (data->data.algorithms.count == 1); + + if (capabilities[i].property != + data->data.algorithms.algProperties[0].alg) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, + "Algorithm %s not supported", + get_algorithm_name (capabilities[i].property)); + return FALSE; + } + + break; + + case TPM2_CAP_COMMANDS: + g_warn_if_fail (data->data.command.count == 1); + + if ((data->data.command.commandAttributes[0] & + TPMA_CC_COMMANDINDEX_MASK) != + capabilities[i].property) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, + "Command %s not supported", + get_command_name (capabilities[i].property)); + return FALSE; + } + + break; + + case TPM2_CAP_PCRS: + g_warn_if_fail (data->data.assignedPCR.count == 1); + break; + } + } + + while (TRUE) + { + TPMI_YES_NO more_data = TPM2_NO; + g_autofree TPMS_CAPABILITY_DATA *data = NULL; + TSS2_RC rc; + + rc = Esys_GetCapability (tpm->esys_context, + ESYS_TR_NONE, ESYS_TR_NONE, ESYS_TR_NONE, + TPM2_CAP_PCRS, + property, + count, + &more_data, + &data); + if (rc != TSS2_RC_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to get capability: %s", + Tss2_RC_Decode (rc)); + return FALSE; + } + + property += data->data.assignedPCR.count; + count -= data->data.assignedPCR.count; + + for (i = 0; i < data->data.assignedPCR.count; i++) + { + if (data->data.assignedPCR.pcrSelections[i].hash != TPM2_ALG_SHA256) + continue; + + if (data->data.assignedPCR.pcrSelections[i].pcrSelect[0] & + (1 << 0 | 1 << 1 | 1 << 2)) + { + sha256_pcrs_found = TRUE; + break; + } + } + + if (more_data == TPM2_NO) + break; + } + + if (!sha256_pcrs_found) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, + "SHA-256 PCRs not available"); + return FALSE; + } + + return TRUE; +} + +GrdTpm * +grd_tpm_new (GrdTpmMode mode, + GError **error) +{ + g_autoptr (GrdTpm) tpm = NULL; + + tpm = g_object_new (GRD_TYPE_TPM, NULL); + + if (!(grd_get_debug_flags () & GRD_DEBUG_TPM)) + g_setenv ("TSS2_LOGFILE", "/dev/null", TRUE); + + if (!check_tpm_existence (error)) + return NULL; + + if (!init_transmission_interface (tpm, error)) + return NULL; + + if (!init_tss2_esys (tpm, error)) + return NULL; + + switch (mode) + { + case GRD_TPM_MODE_NONE: + break; + case GRD_TPM_MODE_WRITE: + if (!start_hmac_session (tpm, error)) + return NULL; + if (!start_trial_session (tpm, error)) + return NULL; + break; + case GRD_TPM_MODE_READ: + if (!start_hmac_session (tpm, error)) + return NULL; + if (!start_policy_session (tpm, error)) + return NULL; + break; + } + + return g_steal_pointer (&tpm); +} + +static void +grd_tpm_finalize (GObject *object) +{ + GrdTpm *tpm = GRD_TPM (object); + + if (tpm->policy_session) + Esys_FlushContext (tpm->esys_context, tpm->policy_session); + if (tpm->trial_session) + Esys_FlushContext (tpm->esys_context, tpm->trial_session); + if (tpm->hmac_session) + Esys_FlushContext (tpm->esys_context, tpm->hmac_session); + if (tpm->esys_context) + Esys_Finalize (&tpm->esys_context); + if (tpm->tcti_context) + Tss2_TctiLdr_Finalize (&tpm->tcti_context); + + G_OBJECT_CLASS (grd_tpm_parent_class)->finalize (object); +} + +static void +grd_tpm_init (GrdTpm *tpm) +{ +} + +static void +grd_tpm_class_init (GrdTpmClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = grd_tpm_finalize; +} diff --git a/grd-tpm.h b/grd-tpm.h new file mode 100644 index 0000000..60c0eeb --- /dev/null +++ b/grd-tpm.h @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2022 Red Hat Inc. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + */ + +#pragma once + +#include +#include +#include + +typedef enum _GrdTpmMode +{ + GRD_TPM_MODE_NONE, + GRD_TPM_MODE_WRITE, + GRD_TPM_MODE_READ, +} GrdTpmMode; + +#define GRD_TYPE_TPM (grd_tpm_get_type ()) +G_DECLARE_FINAL_TYPE (GrdTpm, grd_tpm, GRD, TPM, GObject) + +GrdTpm * grd_tpm_new (GrdTpmMode mode, + GError **error); + +gboolean grd_tpm_create_primary (GrdTpm *tpm, + ESYS_TR *primary_handle_out, + GError **error); + +gboolean grd_tpm_read_pcr (GrdTpm *tpm, + TPML_PCR_SELECTION **pcr_selection_out, + TPML_DIGEST **pcr_digest_out, + GError **error); + +gboolean grd_tpm_store_secret (GrdTpm *tpm, + const char *secret, + ESYS_TR primary_handle, + TPML_PCR_SELECTION *pcr_selection, + TPML_DIGEST *pcr_digest, + TPMS_CONTEXT **secret_tpms_context_out, + GError **error); + +GVariant * grd_tpms_context_to_variant (TPMS_CONTEXT *tpms_context); + +char * grd_tpm_restore_secret (GrdTpm *tpm, + GVariant *secret_variant, + TPML_PCR_SELECTION *pcr_selection, + TPML_DIGEST *pcr_digest, + GError **error); + +gboolean grd_tpm_check_capabilities (GrdTpm *tpm, + GError **error); diff --git a/grd-types.h b/grd-types.h new file mode 100644 index 0000000..16f5a25 --- /dev/null +++ b/grd-types.h @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2015 Red Hat Inc. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + * Written by: + * Jonas Ådahl + */ + +#pragma once + +#include + +typedef struct _GrdAVCFrameInfo GrdAVCFrameInfo; +typedef struct _GrdBitstream GrdBitstream; +typedef struct _GrdClipboard GrdClipboard; +typedef struct _GrdClipboardRdp GrdClipboardRdp; +typedef struct _GrdClipboardVnc GrdClipboardVnc; +typedef struct _GrdContext GrdContext; +typedef struct _GrdCredentials GrdCredentials; +typedef struct _GrdEglThread GrdEglThread; +typedef struct _GrdEncodeContext GrdEncodeContext; +typedef struct _GrdEncodeSession GrdEncodeSession; +typedef struct _GrdHwAccelNvidia GrdHwAccelNvidia; +typedef struct _GrdHwAccelVaapi GrdHwAccelVaapi; +typedef struct _GrdHwAccelVulkan GrdHwAccelVulkan; +typedef struct _GrdImageView GrdImageView; +typedef struct _GrdLocalBuffer GrdLocalBuffer; +typedef struct _GrdRdpAudioOutputStream GrdRdpAudioOutputStream; +typedef struct _GrdRdpAudioVolumeData GrdRdpAudioVolumeData; +typedef struct _GrdRdpBuffer GrdRdpBuffer; +typedef struct _GrdRdpBufferInfo GrdRdpBufferInfo; +typedef struct _GrdRdpBufferPool GrdRdpBufferPool; +typedef struct _GrdRdpCameraStream GrdRdpCameraStream; +typedef struct _GrdRdpConnectTimeAutodetection GrdRdpConnectTimeAutodetection; +typedef struct _GrdRdpCursorRenderer GrdRdpCursorRenderer; +typedef struct _GrdRdpDamageDetector GrdRdpDamageDetector; +typedef struct _GrdRdpDvcAudioInput GrdRdpDvcAudioInput; +typedef struct _GrdRdpDvcAudioPlayback GrdRdpDvcAudioPlayback; +typedef struct _GrdRdpDvcCameraDevice GrdRdpDvcCameraDevice; +typedef struct _GrdRdpDvcCameraEnumerator GrdRdpDvcCameraEnumerator; +typedef struct _GrdRdpDvcDisplayControl GrdRdpDvcDisplayControl; +typedef struct _GrdRdpDvcGraphicsPipeline GrdRdpDvcGraphicsPipeline; +typedef struct _GrdRdpDvcHandler GrdRdpDvcHandler; +typedef struct _GrdRdpDvcInput GrdRdpDvcInput; +typedef struct _GrdRdpDvcTelemetry GrdRdpDvcTelemetry; +typedef struct _GrdRdpEventQueue GrdRdpEventQueue; +typedef struct _GrdRdpFrame GrdRdpFrame; +typedef struct _GrdRdpFrameStats GrdRdpFrameStats; +typedef struct _GrdRdpGfxFrameController GrdRdpGfxFrameController; +typedef struct _GrdRdpGfxFrameLog GrdRdpGfxFrameLog; +typedef struct _GrdRdpGfxFramerateLog GrdRdpGfxFramerateLog; +typedef struct _GrdRdpGfxSurface GrdRdpGfxSurface; +typedef struct _GrdRdpLayoutManager GrdRdpLayoutManager; +typedef struct _GrdRdpLegacyBuffer GrdRdpLegacyBuffer; +typedef struct _GrdRdpNetworkAutodetection GrdRdpNetworkAutodetection; +typedef struct _GrdRdpPwBuffer GrdRdpPwBuffer; +typedef struct _GrdRdpRenderContext GrdRdpRenderContext; +typedef struct _GrdRdpRenderState GrdRdpRenderState; +typedef struct _GrdRdpRenderer GrdRdpRenderer; +typedef struct _GrdRdpSAMFile GrdRdpSAMFile; +typedef struct _GrdRdpServer GrdRdpServer; +typedef struct _GrdRdpSessionMetrics GrdRdpSessionMetrics; +typedef struct _GrdRdpStreamOwner GrdRdpStreamOwner; +typedef struct _GrdRdpSurface GrdRdpSurface; +typedef struct _GrdRdpSurfaceRenderer GrdRdpSurfaceRenderer; +typedef struct _GrdRdpSwEncoderCa GrdRdpSwEncoderCa; +typedef struct _GrdRdpViewCreator GrdRdpViewCreator; +typedef struct _GrdSampleBuffer GrdSampleBuffer; +typedef struct _GrdSession GrdSession; +typedef struct _GrdSessionRdp GrdSessionRdp; +typedef struct _GrdSessionVnc GrdSessionVnc; +typedef struct _GrdStream GrdStream; +typedef struct _GrdTouchContact GrdTouchContact; +typedef struct _GrdVkBuffer GrdVkBuffer; +typedef struct _GrdVkDevice GrdVkDevice; +typedef struct _GrdVkImage GrdVkImage; +typedef struct _GrdVkMemory GrdVkMemory; +typedef struct _GrdVkPhysicalDevice GrdVkPhysicalDevice; +typedef struct _GrdVkQueue GrdVkQueue; +typedef struct _GrdVkSPIRVSources GrdVkSPIRVSources; +typedef struct _GrdVkSPIRVSource GrdVkSPIRVSource; +typedef struct _GrdVncServer GrdVncServer; + +typedef enum _GrdPixelFormat +{ + GRD_PIXEL_FORMAT_RGBA8888, +} GrdPixelFormat; + +typedef enum _GrdDBusError +{ + GRD_DBUS_ERROR_NO_HANDOVER, +} GrdDBusError; + +#define GRD_DBUS_ERROR grd_dbus_error_quark () +GQuark grd_dbus_error_quark (void); diff --git a/grd-utils.c b/grd-utils.c new file mode 100644 index 0000000..e082fcf --- /dev/null +++ b/grd-utils.c @@ -0,0 +1,494 @@ +/* + * Copyright (C) 2010 Intel Corp. + * Copyright (C) 2014 Jonas Ådahl + * Copyright (C) 2016-2022 Red Hat Inc. + * Copyright (C) 2022 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-utils.h" + +#include +#include +#include + +#define GRD_SERVER_PORT_RANGE 10 + +typedef struct _GrdFdSource +{ + GSource source; + + GSourceFunc prepare; + GSourceFunc dispatch; + gpointer user_data; + + GPollFD poll_fd; +} GrdFdSource; + +void +grd_sync_point_init (GrdSyncPoint *sync_point) +{ + g_cond_init (&sync_point->sync_cond); + g_mutex_init (&sync_point->sync_mutex); +} + +void +grd_sync_point_clear (GrdSyncPoint *sync_point) +{ + g_cond_clear (&sync_point->sync_cond); + g_mutex_clear (&sync_point->sync_mutex); +} + +void +grd_sync_point_reset (GrdSyncPoint *sync_point) +{ + sync_point->completed = FALSE; + sync_point->success = FALSE; +} + +void +grd_sync_point_complete (GrdSyncPoint *sync_point, + gboolean success) +{ + g_return_if_fail (!sync_point->completed); + + g_mutex_lock (&sync_point->sync_mutex); + sync_point->success = success; + + sync_point->completed = TRUE; + g_cond_signal (&sync_point->sync_cond); + g_mutex_unlock (&sync_point->sync_mutex); +} + +gboolean +grd_sync_point_wait_for_completion (GrdSyncPoint *sync_point) +{ + gboolean success; + + g_mutex_lock (&sync_point->sync_mutex); + while (!sync_point->completed) + g_cond_wait (&sync_point->sync_cond, &sync_point->sync_mutex); + + success = sync_point->success; + g_mutex_unlock (&sync_point->sync_mutex); + + return success; +} + +static gboolean +grd_fd_source_prepare (GSource *source, + int *timeout_ms) +{ + GrdFdSource *fd_source = (GrdFdSource *) source; + + *timeout_ms = -1; + + return fd_source->prepare (fd_source->user_data); +} + +static gboolean +grd_fd_source_check (GSource *source) +{ + GrdFdSource *fd_source = (GrdFdSource *) source; + + return !!(fd_source->poll_fd.revents & G_IO_IN); +} + +static gboolean +grd_fd_source_dispatch (GSource *source, + GSourceFunc callback, + gpointer user_data) +{ + GrdFdSource *fd_source = (GrdFdSource *) source; + + return fd_source->dispatch (fd_source->user_data); +} + +static void +grd_fd_source_finalize (GSource *source) +{ + GrdFdSource *fd_source = (GrdFdSource *) source; + + close (fd_source->poll_fd.fd); +} + +static GSourceFuncs fd_source_funcs = +{ + .prepare = grd_fd_source_prepare, + .check = grd_fd_source_check, + .dispatch = grd_fd_source_dispatch, + .finalize = grd_fd_source_finalize, +}; + +GSource * +grd_create_fd_source (int fd, + const char *name, + GSourceFunc prepare, + GSourceFunc dispatch, + gpointer user_data, + GDestroyNotify notify) +{ + GSource *source; + GrdFdSource *fd_source; + + source = g_source_new (&fd_source_funcs, sizeof (GrdFdSource)); + g_source_set_name (source, name); + fd_source = (GrdFdSource *) source; + + fd_source->poll_fd.fd = fd; + fd_source->poll_fd.events = G_IO_IN; + + fd_source->prepare = prepare; + fd_source->dispatch = dispatch; + fd_source->user_data = user_data; + + g_source_set_callback (source, dispatch, user_data, notify); + g_source_add_poll (source, &fd_source->poll_fd); + + return source; +} + +gboolean +grd_bind_socket (GSocketListener *server, + uint16_t port, + uint16_t *selected_port, + gboolean negotiate_port, + GError **error) +{ + gboolean is_bound = FALSE; + int i; + + if (!negotiate_port) + { + is_bound = g_socket_listener_add_inet_port (server, + port, + NULL, + error); + goto out; + } + + for (i = 0; i < GRD_SERVER_PORT_RANGE; i++, port++) + { + g_autoptr (GError) local_error = NULL; + + g_assert (port < G_MAXUINT16); + + is_bound = g_socket_listener_add_inet_port (server, + port, + NULL, + &local_error); + if (local_error) + { + g_debug ("\tServer could not be bound to TCP port %hu: %s", + port, local_error->message); + } + + if (is_bound) + break; + } + + if (!is_bound) + port = g_socket_listener_add_any_inet_port (server, NULL, error); + + is_bound = port != 0; + +out: + if (is_bound) + { + g_debug ("\tServer bound to TCP port %hu", port); + *selected_port = port; + } + + return is_bound; +} + +void +grd_rewrite_path_to_user_data_dir (char **path, + const char *subdir, + const char *fallback_path) +{ + const char *input_path = NULL; + g_autofree char *basename = NULL; + g_autofree char *output_path = NULL; + + g_assert (path); + g_assert (subdir); + g_assert (fallback_path); + g_assert (strstr (subdir, "..") == NULL); + + input_path = *path; + + if (!input_path || + input_path[0] == '\0' || + g_str_equal (input_path, ".") || + G_IS_DIR_SEPARATOR (input_path[strlen (input_path) - 1])) + input_path = fallback_path; + + basename = g_path_get_basename (input_path); + + g_assert (!G_IS_DIR_SEPARATOR (basename[0])); + + output_path = g_build_filename (g_get_user_data_dir (), + "gnome-remote-desktop", + subdir, + basename, + NULL); + + g_free (*path); + *path = g_steal_pointer (&output_path); +} + +gboolean +grd_write_fd_to_file (int fd, + const char *filename, + GCancellable *cancellable, + GError **error) +{ + g_autoptr (GInputStream) input_stream = NULL; + g_autoptr (GFileOutputStream) output_stream = NULL; + g_autoptr (GFile) file = NULL; + g_autofree char *dir = g_path_get_dirname (filename); + + g_mkdir_with_parents (dir, 0755); + + input_stream = G_INPUT_STREAM (g_unix_input_stream_new (fd, FALSE)); + + file = g_file_new_for_path (filename); + output_stream = g_file_replace (file, NULL, TRUE, G_FILE_CREATE_PRIVATE, + NULL, error); + if (!output_stream) + return FALSE; + + return g_output_stream_splice (G_OUTPUT_STREAM(output_stream), input_stream, + G_OUTPUT_STREAM_SPLICE_CLOSE_SOURCE | + G_OUTPUT_STREAM_SPLICE_CLOSE_TARGET, + cancellable, + error); +} + +gboolean +grd_test_fd (int fd, + ssize_t max_size, + GFileTest *test_results, + GError **error) +{ + struct stat stat_results; + int ret; + + do + ret = fstat (fd, &stat_results); + while (ret < 0 && errno == EINTR); + + if (ret < 0) + { + g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (errno), + "Failed to fstat file descriptor: %s", g_strerror (errno)); + return FALSE; + } + + if (max_size >= 0 && S_ISREG (stat_results.st_mode) && + stat_results.st_size > max_size) + { + g_autofree char *size_string = NULL; + + size_string = g_format_size_full (max_size, G_FORMAT_SIZE_LONG_FORMAT); + g_set_error (error, G_IO_ERROR, G_FILE_ERROR_FAILED, + "File size exceeds %s", size_string); + + return FALSE; + } + + *test_results = 0; + if (S_ISREG (stat_results.st_mode)) + *test_results |= G_FILE_TEST_IS_REGULAR; + else if (S_ISDIR (stat_results.st_mode)) + *test_results |= G_FILE_TEST_IS_DIR; + else if (S_ISLNK (stat_results.st_mode)) + *test_results |= G_FILE_TEST_IS_SYMLINK; + + if (stat_results.st_nlink > 0) + *test_results |= G_FILE_TEST_EXISTS; + + if ((stat_results.st_mode & S_IXUSR) || (stat_results.st_mode & S_IXGRP) || + (stat_results.st_mode & S_IXOTH)) + *test_results |= G_FILE_TEST_IS_EXECUTABLE; + + return TRUE; +} + +gboolean +grd_toggle_systemd_unit (GrdRuntimeMode runtime_mode, + gboolean enabled, + GError **error) +{ + g_autoptr (GStrvBuilder) builder = NULL; + g_autofree char *error_output = NULL; + g_auto (GStrv) new_argv = NULL; + g_autofree char *pid = NULL; + const char *type; + int wait_status; + gboolean success; + + switch (runtime_mode) + { + case GRD_RUNTIME_MODE_HEADLESS: + type = "headless"; + break; + case GRD_RUNTIME_MODE_SYSTEM: + type = "system"; + break; + case GRD_RUNTIME_MODE_SCREEN_SHARE: + type = "user"; + break; + default: + g_assert_not_reached (); + } + + builder = g_strv_builder_new (); + + if (runtime_mode == GRD_RUNTIME_MODE_SYSTEM) + g_strv_builder_add (builder, "pkexec"); + + g_strv_builder_add (builder, + GRD_LIBEXEC_DIR "/gnome-remote-desktop-enable-service"); + pid = g_strdup_printf ("%d", getpid ()); + g_strv_builder_add (builder, pid); + g_strv_builder_add (builder, type); + g_strv_builder_add (builder, enabled ? "true" : "false"); + + new_argv = g_strv_builder_end (builder); + + success = g_spawn_sync (NULL, + new_argv, + NULL, + G_SPAWN_SEARCH_PATH | + G_SPAWN_CHILD_INHERITS_STDOUT, + NULL, + NULL, + NULL, + &error_output, + &wait_status, + error); + if (!success) + return FALSE; + + if (!WIFEXITED (wait_status) || WEXITSTATUS (wait_status) != 0) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Could not %s %s service:\n%s", + enabled? "enable" : "disable", + type, error_output); + return FALSE; + } + + return success; +} + +gboolean +grd_systemd_get_unit (GBusType bus_type, + const char *unit, + GDBusProxy **proxy, + GError **error) +{ + g_autoptr (GDBusProxy) manager_proxy = NULL; + g_autoptr (GDBusProxy) unit_proxy = NULL; + g_autoptr (GVariant) result = NULL; + g_autofree char *object_path = NULL; + + manager_proxy = g_dbus_proxy_new_for_bus_sync ( + bus_type, + G_DBUS_PROXY_FLAGS_NONE, + NULL, + "org.freedesktop.systemd1", + "/org/freedesktop/systemd1", + "org.freedesktop.systemd1.Manager", + NULL, + error); + if (!manager_proxy) + return FALSE; + + result = g_dbus_proxy_call_sync (manager_proxy, + "LoadUnit", + g_variant_new ("(s)", unit), + G_DBUS_CALL_FLAGS_NONE, + -1, + NULL, + error); + if (!result) + return FALSE; + + g_variant_get (result, "(o)", &object_path); + + unit_proxy = g_dbus_proxy_new_for_bus_sync (bus_type, + G_DBUS_PROXY_FLAGS_NONE, + NULL, + "org.freedesktop.systemd1", + object_path, + "org.freedesktop.systemd1.Unit", + NULL, + error); + if (!unit_proxy) + return FALSE; + + *proxy = g_steal_pointer (&unit_proxy); + return TRUE; +} + +gboolean +grd_systemd_unit_get_active_state (GDBusProxy *unit_proxy, + GrdSystemdUnitActiveState *active_state, + GError **error) +{ + g_autoptr (GVariant) result = NULL; + g_autofree char *res_active_state = NULL; + + result = g_dbus_proxy_get_cached_property (unit_proxy, "ActiveState"); + if (!result) + { + g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_PROPERTY, + "An error occurred while getting the ActiveState property"); + return FALSE; + } + + g_variant_get (result, "s", &res_active_state); + + if (g_str_equal (res_active_state, "active")) + *active_state = GRD_SYSTEMD_UNIT_ACTIVE_STATE_ACTIVE; + else if (g_str_equal (res_active_state, "reloading")) + *active_state = GRD_SYSTEMD_UNIT_ACTIVE_STATE_RELOADING; + else if (g_str_equal (res_active_state, "inactive")) + *active_state = GRD_SYSTEMD_UNIT_ACTIVE_STATE_INACTIVE; + else if (g_str_equal (res_active_state, "failed")) + *active_state = GRD_SYSTEMD_UNIT_ACTIVE_STATE_FAILED; + else if (g_str_equal (res_active_state, "activating")) + *active_state = GRD_SYSTEMD_UNIT_ACTIVE_STATE_ACTIVATING; + else if (g_str_equal (res_active_state, "deactivating")) + *active_state = GRD_SYSTEMD_UNIT_ACTIVE_STATE_DEACTIVATING; + else + *active_state = GRD_SYSTEMD_UNIT_ACTIVE_STATE_UNKNOWN; + + return TRUE; +} + +void +grd_close_connection_and_notify (GSocketConnection *connection) +{ + g_io_stream_close (G_IO_STREAM (connection), NULL, NULL); + g_object_notify (G_OBJECT (connection), "closed"); +} diff --git a/grd-utils.h b/grd-utils.h new file mode 100644 index 0000000..d97c7d1 --- /dev/null +++ b/grd-utils.h @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2022 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include +#include + +#include "grd-enums.h" + +typedef enum +{ + GRD_SYSTEMD_UNIT_ACTIVE_STATE_UNKNOWN = 0, + GRD_SYSTEMD_UNIT_ACTIVE_STATE_ACTIVE, + GRD_SYSTEMD_UNIT_ACTIVE_STATE_RELOADING, + GRD_SYSTEMD_UNIT_ACTIVE_STATE_INACTIVE, + GRD_SYSTEMD_UNIT_ACTIVE_STATE_FAILED, + GRD_SYSTEMD_UNIT_ACTIVE_STATE_ACTIVATING, + GRD_SYSTEMD_UNIT_ACTIVE_STATE_DEACTIVATING, +} GrdSystemdUnitActiveState; + +typedef struct _GrdSyncPoint +{ + GCond sync_cond; + GMutex sync_mutex; + gboolean completed; + + gboolean success; +} GrdSyncPoint; + +static inline uint32_t +grd_get_aligned_size (uint32_t size, + uint32_t alignment) +{ + return size + (size % alignment ? alignment - size % alignment : 0); +} + +void grd_sync_point_init (GrdSyncPoint *sync_point); + +void grd_sync_point_clear (GrdSyncPoint *sync_point); + +void grd_sync_point_reset (GrdSyncPoint *sync_point); + +void grd_sync_point_complete (GrdSyncPoint *sync_point, + gboolean success); + +gboolean grd_sync_point_wait_for_completion (GrdSyncPoint *sync_point); + +GSource * grd_create_fd_source (int fd, + const char *name, + GSourceFunc prepare, + GSourceFunc dispatch, + gpointer user_data, + GDestroyNotify notify); + +gboolean grd_bind_socket (GSocketListener *server, + uint16_t port, + uint16_t *selected_port, + gboolean port_negotiation_enabled, + GError **error); + +void grd_rewrite_path_to_user_data_dir (char **path, + const char *subdir, + const char *fallback_path); + +gboolean grd_write_fd_to_file (int fd, + const char *filename, + GCancellable *cancellable, + GError **error); + +gboolean grd_test_fd (int fd, + ssize_t max_size, + GFileTest *test_results, + GError **error); + +gboolean grd_toggle_systemd_unit (GrdRuntimeMode runtime_mode, + gboolean enabled, + GError **error); + +gboolean grd_systemd_get_unit (GBusType bus_type, + const char *unit, + GDBusProxy **proxy, + GError **error); + +gboolean grd_systemd_unit_get_active_state (GDBusProxy *unit_proxy, + GrdSystemdUnitActiveState *active_state, + GError **error); + +void grd_close_connection_and_notify (GSocketConnection *connection); + +static inline int64_t +us (int64_t us) +{ + return us; +} + +static inline int64_t +ms2us (int64_t ms) +{ + return us (ms * 1000); +} + +static inline uint64_t +s2ns (uint64_t s) +{ + return s * G_USEC_PER_SEC * UINT64_C (1000); +} + +static inline int64_t +s2us (uint64_t s) +{ + return ms2us (s * 1000); +} diff --git a/grd-vk-buffer.c b/grd-vk-buffer.c new file mode 100644 index 0000000..a2a0a96 --- /dev/null +++ b/grd-vk-buffer.c @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2022 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-vk-buffer.h" + +#include + +#include "grd-vk-device.h" +#include "grd-vk-memory.h" + +struct _GrdVkBuffer +{ + GObject parent; + + GrdVkDevice *device; + + VkBuffer vk_buffer; + GrdVkMemory *memory; +}; + +G_DEFINE_TYPE (GrdVkBuffer, grd_vk_buffer, G_TYPE_OBJECT) + +VkBuffer +grd_vk_buffer_get_buffer (GrdVkBuffer *buffer) +{ + return buffer->vk_buffer; +} + +GrdVkMemory * +grd_vk_buffer_get_memory (GrdVkBuffer *buffer) +{ + return buffer->memory; +} + +GrdVkBuffer * +grd_vk_buffer_new (GrdVkDevice *device, + const GrdVkBufferDescriptor *buffer_descriptor, + GError **error) +{ + VkDevice vk_device = grd_vk_device_get_device (device); + g_autoptr (GrdVkBuffer) buffer = NULL; + VkBufferCreateInfo buffer_create_info = {}; + VkBufferMemoryRequirementsInfo2 memory_requirements_info_2 = {}; + VkMemoryRequirements2 memory_requirements_2 = {}; + GrdVkMemoryDescriptor memory_descriptor = {}; + VkBindBufferMemoryInfo bind_buffer_memory_info = {}; + VkDeviceMemory vk_memory; + VkResult vk_result; + + g_assert (vk_device != VK_NULL_HANDLE); + g_assert (buffer_descriptor->size > 0); + + buffer = g_object_new (GRD_TYPE_VK_BUFFER, NULL); + buffer->device = device; + + buffer_create_info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; + buffer_create_info.size = + grd_vk_device_get_aligned_size (device, buffer_descriptor->size); + buffer_create_info.usage = buffer_descriptor->usage_flags; + buffer_create_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + + vk_result = vkCreateBuffer (vk_device, &buffer_create_info, NULL, + &buffer->vk_buffer); + if (vk_result != VK_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create buffer: %i", vk_result); + return NULL; + } + g_assert (buffer->vk_buffer != VK_NULL_HANDLE); + + memory_requirements_info_2.sType = + VK_STRUCTURE_TYPE_BUFFER_MEMORY_REQUIREMENTS_INFO_2; + memory_requirements_info_2.buffer = buffer->vk_buffer; + memory_requirements_2.sType = VK_STRUCTURE_TYPE_MEMORY_REQUIREMENTS_2; + + vkGetBufferMemoryRequirements2 (vk_device, &memory_requirements_info_2, + &memory_requirements_2); + + memory_descriptor.memory_requirements_2 = &memory_requirements_2; + memory_descriptor.memory_flags = buffer_descriptor->memory_flags; + + buffer->memory = grd_vk_memory_new (device, &memory_descriptor, error); + if (!buffer->memory) + return NULL; + + vk_memory = grd_vk_memory_get_device_memory (buffer->memory); + + bind_buffer_memory_info.sType = VK_STRUCTURE_TYPE_BIND_BUFFER_MEMORY_INFO; + bind_buffer_memory_info.buffer = buffer->vk_buffer; + bind_buffer_memory_info.memory = vk_memory; + bind_buffer_memory_info.memoryOffset = 0; + + vk_result = vkBindBufferMemory2 (vk_device, 1, &bind_buffer_memory_info); + if (vk_result != VK_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to bind buffer memory: %i", vk_result); + return NULL; + } + + return g_steal_pointer (&buffer); +} + +static void +grd_vk_buffer_dispose (GObject *object) +{ + GrdVkBuffer *buffer = GRD_VK_BUFFER (object); + VkDevice vk_device = grd_vk_device_get_device (buffer->device); + + g_assert (vk_device != VK_NULL_HANDLE); + + if (buffer->vk_buffer != VK_NULL_HANDLE) + { + vkDestroyBuffer (vk_device, buffer->vk_buffer, NULL); + buffer->vk_buffer = VK_NULL_HANDLE; + } + + g_clear_object (&buffer->memory); + + G_OBJECT_CLASS (grd_vk_buffer_parent_class)->dispose (object); +} + +static void +grd_vk_buffer_init (GrdVkBuffer *buffer) +{ +} + +static void +grd_vk_buffer_class_init (GrdVkBufferClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = grd_vk_buffer_dispose; +} diff --git a/grd-vk-buffer.h b/grd-vk-buffer.h new file mode 100644 index 0000000..797f842 --- /dev/null +++ b/grd-vk-buffer.h @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2022 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include +#include + +#include "grd-types.h" + +#define GRD_TYPE_VK_BUFFER (grd_vk_buffer_get_type ()) +G_DECLARE_FINAL_TYPE (GrdVkBuffer, grd_vk_buffer, + GRD, VK_BUFFER, GObject) + +typedef struct +{ + VkBufferUsageFlags usage_flags; + + VkDeviceSize size; + VkMemoryPropertyFlagBits memory_flags; +} GrdVkBufferDescriptor; + +GrdVkBuffer *grd_vk_buffer_new (GrdVkDevice *device, + const GrdVkBufferDescriptor *buffer_descriptor, + GError **error); + +VkBuffer grd_vk_buffer_get_buffer (GrdVkBuffer *buffer); + +GrdVkMemory *grd_vk_buffer_get_memory (GrdVkBuffer *buffer); diff --git a/grd-vk-device.c b/grd-vk-device.c new file mode 100644 index 0000000..f6ad38f --- /dev/null +++ b/grd-vk-device.c @@ -0,0 +1,575 @@ +/* + * Copyright (C) 2022 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-vk-device.h" + +#include + +#include "grd-hwaccel-vulkan.h" +#include "grd-vk-physical-device.h" +#include "grd-vk-queue.h" +#include "grd-vk-utils.h" + +#define MAX_DEVICE_QUEUES 6 + +typedef struct +{ + GrdVkQueue *queue; + uint64_t ref_count; +} QueueInfo; + +struct _GrdVkDevice +{ + GObject parent; + + GrdVkPhysicalDevice *physical_device; + + VkDevice vk_device; + VkPipelineCache vk_pipeline_cache; + + uint32_t queue_family_idx; + uint32_t max_queues; + + GMutex queue_pool_mutex; + GHashTable *queue_table; + + float timestamp_period; + VkDeviceSize non_coherent_atom_size; + int64_t drm_render_node; + VkDriverId driver_id; + + GrdVkDeviceFuncs device_funcs; + GrdVkShaderModules shader_modules; +}; + +G_DEFINE_TYPE (GrdVkDevice, grd_vk_device, G_TYPE_OBJECT) + +GrdVkPhysicalDevice * +grd_vk_device_get_physical_device (GrdVkDevice *device) +{ + return device->physical_device; +} + +VkDevice +grd_vk_device_get_device (GrdVkDevice *device) +{ + return device->vk_device; +} + +VkPipelineCache +grd_vk_device_get_pipeline_cache (GrdVkDevice *device) +{ + return device->vk_pipeline_cache; +} + +float +grd_vk_device_get_timestamp_period (GrdVkDevice *device) +{ + return device->timestamp_period; +} + +int64_t +grd_vk_device_get_drm_render_node (GrdVkDevice *device) +{ + return device->drm_render_node; +} + +VkDriverId +grd_vk_device_get_driver_id (GrdVkDevice *device) +{ + return device->driver_id; +} + +GrdVkDeviceFuncs * +grd_vk_device_get_device_funcs (GrdVkDevice *device) +{ + return &device->device_funcs; +} + +const GrdVkShaderModules * +grd_vk_device_get_shader_modules (GrdVkDevice *device) +{ + return &device->shader_modules; +} + +VkDeviceSize +grd_vk_device_get_aligned_size (GrdVkDevice *device, + VkDeviceSize size) +{ + VkDeviceSize alignment = device->non_coherent_atom_size; + + return size + (size % alignment ? alignment - size % alignment : 0); +} + +GrdVkQueue * +grd_vk_device_acquire_queue (GrdVkDevice *device) +{ + /* Least-used-queue queue info */ + QueueInfo *luq_queue_info = NULL; + QueueInfo *queue_info; + GHashTableIter iter; + + g_assert (device->max_queues > 0); + + g_mutex_lock (&device->queue_pool_mutex); + g_hash_table_iter_init (&iter, device->queue_table); + while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &queue_info)) + { + if (!luq_queue_info || queue_info->ref_count < luq_queue_info->ref_count) + luq_queue_info = queue_info; + } + g_assert (luq_queue_info); + + g_assert (luq_queue_info->ref_count < UINT64_MAX); + ++luq_queue_info->ref_count; + g_mutex_unlock (&device->queue_pool_mutex); + + return luq_queue_info->queue; +} + +void +grd_vk_device_release_queue (GrdVkDevice *device, + GrdVkQueue *queue) +{ + uint32_t queue_idx = grd_vk_queue_get_queue_idx (queue); + QueueInfo *queue_info = NULL; + + g_mutex_lock (&device->queue_pool_mutex); + if (!g_hash_table_lookup_extended (device->queue_table, + GUINT_TO_POINTER (queue_idx), + NULL, (gpointer *) &queue_info)) + g_assert_not_reached (); + + g_assert (queue_info->ref_count > 0); + --queue_info->ref_count; + g_mutex_unlock (&device->queue_pool_mutex); +} + +static void +get_queue_family_properties (VkPhysicalDevice vk_physical_device, + VkQueueFamilyProperties2 **properties_2, + uint32_t *n_properties_2) +{ + uint32_t i; + + *properties_2 = NULL; + *n_properties_2 = 0; + + vkGetPhysicalDeviceQueueFamilyProperties2 (vk_physical_device, + n_properties_2, NULL); + g_assert (*n_properties_2 > 0); + + *properties_2 = g_new0 (VkQueueFamilyProperties2, *n_properties_2); + for (i = 0; i < *n_properties_2; ++i) + (*properties_2)[i].sType = VK_STRUCTURE_TYPE_QUEUE_FAMILY_PROPERTIES_2; + + vkGetPhysicalDeviceQueueFamilyProperties2 (vk_physical_device, + n_properties_2, *properties_2); +} + +static void +find_queue_family_with_most_queues (GrdVkDevice *device) +{ + VkPhysicalDevice vk_physical_device = + grd_vk_physical_device_get_physical_device (device->physical_device); + g_autofree VkQueueFamilyProperties2 *properties_2 = NULL; + uint32_t n_properties_2 = 0; + VkQueueFlags bitmask; + uint32_t i; + + g_assert (device->max_queues == 0); + + get_queue_family_properties (vk_physical_device, + &properties_2, &n_properties_2); + + bitmask = VK_QUEUE_COMPUTE_BIT | VK_QUEUE_TRANSFER_BIT; + + for (i = 0; i < n_properties_2; ++i) + { + VkQueueFamilyProperties *properties = + &properties_2[i].queueFamilyProperties; + + if (properties->queueFlags & bitmask && + properties->queueCount > device->max_queues) + { + device->queue_family_idx = i; + device->max_queues = properties->queueCount; + } + } + g_assert (device->max_queues > 0); +} + +static gboolean +create_pipeline_cache (GrdVkDevice *device, + GError **error) +{ + VkDevice vk_device = device->vk_device; + VkPipelineCacheCreateInfo cache_create_info = {}; + VkResult vk_result; + + cache_create_info.sType = VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO; + + vk_result = vkCreatePipelineCache (vk_device, &cache_create_info, NULL, + &device->vk_pipeline_cache); + if (vk_result != VK_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create pipeline cache: %i", vk_result); + return FALSE; + } + g_assert (device->vk_pipeline_cache != VK_NULL_HANDLE); + + return TRUE; +} + +static gboolean +load_device_funcs (GrdVkDevice *device, + GError **error) +{ + GrdVkDeviceFuncs *device_funcs = &device->device_funcs; + VkDevice vk_device = device->vk_device; + + /* VK_KHR_external_memory_fd */ + device_funcs->vkGetMemoryFdPropertiesKHR = (PFN_vkGetMemoryFdPropertiesKHR) + vkGetDeviceProcAddr (vk_device, "vkGetMemoryFdPropertiesKHR"); + if (!device_funcs->vkGetMemoryFdPropertiesKHR) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to get device function address for function " + "\"vkGetMemoryFdPropertiesKHR\""); + return FALSE; + } + + /* VK_KHR_synchronization2 */ + device_funcs->vkCmdPipelineBarrier2KHR = (PFN_vkCmdPipelineBarrier2KHR) + vkGetDeviceProcAddr (vk_device, "vkCmdPipelineBarrier2KHR"); + if (!device_funcs->vkCmdPipelineBarrier2KHR) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to get device function address for function " + "\"vkCmdPipelineBarrier2KHR\""); + return FALSE; + } + + device_funcs->vkCmdWriteTimestamp2KHR = (PFN_vkCmdWriteTimestamp2KHR) + vkGetDeviceProcAddr (vk_device, "vkCmdWriteTimestamp2KHR"); + if (!device_funcs->vkCmdWriteTimestamp2KHR) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to get device function address for function " + "\"vkCmdWriteTimestamp2KHR\""); + return FALSE; + } + + device_funcs->vkQueueSubmit2KHR = (PFN_vkQueueSubmit2KHR) + vkGetDeviceProcAddr (vk_device, "vkQueueSubmit2KHR"); + if (!device_funcs->vkQueueSubmit2KHR) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to get device function address for function " + "\"vkQueueSubmit2KHR\""); + return FALSE; + } + + return TRUE; +} + +static gboolean +create_shader_module (GrdVkDevice *device, + const GrdVkSPIRVSource *spirv_source, + VkShaderModule *shader_module, + GError **error) +{ + VkDevice vk_device = device->vk_device; + VkShaderModuleCreateInfo shader_module_create_info = {}; + VkResult vk_result; + + g_assert (shader_module); + g_assert (spirv_source->size % 4 == 0); + + shader_module_create_info.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO; + shader_module_create_info.codeSize = spirv_source->size; + shader_module_create_info.pCode = (const uint32_t *) spirv_source->data; + + vk_result = vkCreateShaderModule (vk_device, &shader_module_create_info, NULL, + shader_module); + if (vk_result != VK_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create shader module: %i", vk_result); + return FALSE; + } + g_assert (*shader_module != VK_NULL_HANDLE); + + return TRUE; +} + +static gboolean +create_shader_modules (GrdVkDevice *device, + const GrdVkSPIRVSources *spirv_sources, + GError **error) +{ + GrdVkShaderModules *shader_modules = &device->shader_modules; + + if (!create_shader_module (device, spirv_sources->avc_dual_view, + &shader_modules->create_avc_dual_view, error)) + return FALSE; + + return TRUE; +} + +static void +prepare_queue_pool (GrdVkDevice *device) +{ + uint32_t i; + + g_assert (device->max_queues > 0); + + for (i = 0; i < device->max_queues; ++i) + { + QueueInfo *queue_info; + + queue_info = g_new0 (QueueInfo, 1); + queue_info->queue = + grd_vk_queue_new (device, device->queue_family_idx, i); + + g_hash_table_insert (device->queue_table, + GUINT_TO_POINTER (i), queue_info); + } + g_assert (g_hash_table_size (device->queue_table) == device->max_queues); +} + +static void +fetch_device_properties (GrdVkDevice *device) +{ + VkPhysicalDevice vk_physical_device = + grd_vk_physical_device_get_physical_device (device->physical_device); + VkPhysicalDeviceProperties properties = {}; + VkPhysicalDeviceProperties2 properties_2 = {}; + VkPhysicalDeviceDrmPropertiesEXT drm_properties = {}; + VkPhysicalDeviceVulkan12Properties vk12_properties = {}; + + vkGetPhysicalDeviceProperties (vk_physical_device, &properties); + device->timestamp_period = properties.limits.timestampPeriod; + device->non_coherent_atom_size = properties.limits.nonCoherentAtomSize; + + g_assert (device->timestamp_period > 0); + + properties_2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2; + drm_properties.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRM_PROPERTIES_EXT; + vk12_properties.sType = + VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_PROPERTIES; + + grd_vk_append_to_chain (&properties_2, &drm_properties); + grd_vk_append_to_chain (&properties_2, &vk12_properties); + + vkGetPhysicalDeviceProperties2 (vk_physical_device, &properties_2); + device->drm_render_node = drm_properties.renderMinor; + device->driver_id = vk12_properties.driverID; +} + +GrdVkDevice * +grd_vk_device_new (GrdVkPhysicalDevice *physical_device, + const GrdVkSPIRVSources *spirv_sources, + GError **error) +{ + VkPhysicalDevice vk_physical_device = + grd_vk_physical_device_get_physical_device (physical_device); + GrdVkDeviceFeatures device_features = + grd_vk_physical_device_get_device_features (physical_device); + g_autoptr (GrdVkDevice) device = NULL; + VkDeviceCreateInfo device_create_info = {}; + VkPhysicalDeviceVulkan12Features vulkan12_features = {}; + VkPhysicalDeviceSynchronization2Features synchronization2_features = {}; + VkPhysicalDeviceZeroInitializeWorkgroupMemoryFeatures zero_init_features = {}; + VkDeviceQueueCreateInfo device_queue_create_info = {}; + float queue_priorities[MAX_DEVICE_QUEUES] = {}; + const char *extensions[7] = {}; + uint32_t n_extensions = 0; + VkResult vk_result; + uint32_t i; + + device = g_object_new (GRD_TYPE_VK_DEVICE, NULL); + device->physical_device = g_object_ref (physical_device); + + find_queue_family_with_most_queues (device); + device->max_queues = MIN (device->max_queues, MAX_DEVICE_QUEUES); + + for (i = 0; i < device->max_queues; ++i) + queue_priorities[i] = 1.0f; + + device_queue_create_info.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; + device_queue_create_info.queueFamilyIndex = device->queue_family_idx; + device_queue_create_info.queueCount = device->max_queues; + device_queue_create_info.pQueuePriorities = queue_priorities; + + extensions[n_extensions++] = VK_KHR_SYNCHRONIZATION_2_EXTENSION_NAME; + extensions[n_extensions++] = VK_KHR_EXTERNAL_MEMORY_FD_EXTENSION_NAME; + extensions[n_extensions++] = VK_EXT_EXTERNAL_MEMORY_DMA_BUF_EXTENSION_NAME; + extensions[n_extensions++] = VK_EXT_IMAGE_DRM_FORMAT_MODIFIER_EXTENSION_NAME; + extensions[n_extensions++] = VK_KHR_FORMAT_FEATURE_FLAGS_2_EXTENSION_NAME; + extensions[n_extensions++] = VK_KHR_ZERO_INITIALIZE_WORKGROUP_MEMORY_EXTENSION_NAME; + extensions[n_extensions++] = VK_EXT_PHYSICAL_DEVICE_DRM_EXTENSION_NAME; + + device_create_info.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; + device_create_info.queueCreateInfoCount = 1; + device_create_info.pQueueCreateInfos = &device_queue_create_info; + device_create_info.enabledExtensionCount = n_extensions; + device_create_info.ppEnabledExtensionNames = extensions; + + vulkan12_features.sType = + VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES; + + if (device_features & GRD_VK_DEVICE_FEATURE_UPDATE_AFTER_BIND_SAMPLED_IMAGE) + vulkan12_features.descriptorBindingSampledImageUpdateAfterBind = VK_TRUE; + if (device_features & GRD_VK_DEVICE_FEATURE_UPDATE_AFTER_BIND_STORAGE_IMAGE) + vulkan12_features.descriptorBindingStorageImageUpdateAfterBind = VK_TRUE; + + synchronization2_features.sType = + VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SYNCHRONIZATION_2_FEATURES; + synchronization2_features.synchronization2 = VK_TRUE; + + zero_init_features.sType = + VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ZERO_INITIALIZE_WORKGROUP_MEMORY_FEATURES; + zero_init_features.shaderZeroInitializeWorkgroupMemory = VK_TRUE; + + grd_vk_append_to_chain (&device_create_info, &vulkan12_features); + grd_vk_append_to_chain (&device_create_info, &synchronization2_features); + grd_vk_append_to_chain (&device_create_info, &zero_init_features); + + vk_result = vkCreateDevice (vk_physical_device, &device_create_info, NULL, + &device->vk_device); + if (vk_result != VK_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create device: %i", vk_result); + return NULL; + } + g_assert (device->vk_device != VK_NULL_HANDLE); + + if (!create_pipeline_cache (device, error)) + return NULL; + if (!load_device_funcs (device, error)) + return NULL; + if (!create_shader_modules (device, spirv_sources, error)) + return NULL; + + prepare_queue_pool (device); + fetch_device_properties (device); + + g_debug ("[HWAccel.Vulkan] Using device features: 0x%08X", device_features); + + return g_steal_pointer (&device); +} + +static void +clear_shader_module (GrdVkDevice *device, + VkShaderModule *vk_shader_module) +{ + VkDevice vk_device = device->vk_device; + + g_assert (vk_device != VK_NULL_HANDLE); + + if (*vk_shader_module == VK_NULL_HANDLE) + return; + + vkDestroyShaderModule (vk_device, *vk_shader_module, NULL); + vk_shader_module = VK_NULL_HANDLE; +} + +static void +destroy_shader_modules (GrdVkDevice *device) +{ + GrdVkShaderModules *shader_modules = &device->shader_modules; + + clear_shader_module (device, &shader_modules->create_avc_dual_view); +} + +static void +grd_vk_device_dispose (GObject *object) +{ + GrdVkDevice *device = GRD_VK_DEVICE (object); + + g_hash_table_remove_all (device->queue_table); + + if (device->vk_device != VK_NULL_HANDLE) + destroy_shader_modules (device); + + if (device->vk_pipeline_cache != VK_NULL_HANDLE) + { + vkDestroyPipelineCache (device->vk_device, device->vk_pipeline_cache, + NULL); + device->vk_pipeline_cache = VK_NULL_HANDLE; + } + if (device->vk_device != VK_NULL_HANDLE) + { + vkDestroyDevice (device->vk_device, NULL); + device->vk_device = VK_NULL_HANDLE; + } + + G_OBJECT_CLASS (grd_vk_device_parent_class)->dispose (object); +} + +static void +grd_vk_device_finalize (GObject *object) +{ + GrdVkDevice *device = GRD_VK_DEVICE (object); + + g_mutex_clear (&device->queue_pool_mutex); + + g_assert (g_hash_table_size (device->queue_table) == 0); + g_clear_pointer (&device->queue_table, g_hash_table_unref); + + g_clear_object (&device->physical_device); + + G_OBJECT_CLASS (grd_vk_device_parent_class)->finalize (object); +} + +static void +queue_info_free (gpointer data) +{ + QueueInfo *queue_info = data; + + g_assert (queue_info->ref_count == 0); + g_clear_object (&queue_info->queue); + + g_free (queue_info); +} + +static void +grd_vk_device_init (GrdVkDevice *device) +{ + g_assert (MAX_DEVICE_QUEUES > 0); + + device->queue_table = g_hash_table_new_full (NULL, NULL, + NULL, queue_info_free); + + g_mutex_init (&device->queue_pool_mutex); +} + +static void +grd_vk_device_class_init (GrdVkDeviceClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = grd_vk_device_dispose; + object_class->finalize = grd_vk_device_finalize; +} diff --git a/grd-vk-device.h b/grd-vk-device.h new file mode 100644 index 0000000..4abb655 --- /dev/null +++ b/grd-vk-device.h @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2022 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include +#include + +#include "grd-types.h" + +#define GRD_TYPE_VK_DEVICE (grd_vk_device_get_type ()) +G_DECLARE_FINAL_TYPE (GrdVkDevice, grd_vk_device, + GRD, VK_DEVICE, GObject) + +typedef struct +{ + /* VK_KHR_external_memory_fd */ + PFN_vkGetMemoryFdPropertiesKHR vkGetMemoryFdPropertiesKHR; + + /* VK_KHR_synchronization2 */ + PFN_vkCmdPipelineBarrier2KHR vkCmdPipelineBarrier2KHR; + PFN_vkCmdWriteTimestamp2KHR vkCmdWriteTimestamp2KHR; + PFN_vkQueueSubmit2KHR vkQueueSubmit2KHR; +} GrdVkDeviceFuncs; + +typedef struct +{ + VkShaderModule create_avc_dual_view; +} GrdVkShaderModules; + +GrdVkDevice *grd_vk_device_new (GrdVkPhysicalDevice *physical_device, + const GrdVkSPIRVSources *spirv_sources, + GError **error); + +GrdVkPhysicalDevice *grd_vk_device_get_physical_device (GrdVkDevice *device); + +VkDevice grd_vk_device_get_device (GrdVkDevice *device); + +VkPipelineCache grd_vk_device_get_pipeline_cache (GrdVkDevice *device); + +float grd_vk_device_get_timestamp_period (GrdVkDevice *device); + +int64_t grd_vk_device_get_drm_render_node (GrdVkDevice *device); + +VkDriverId grd_vk_device_get_driver_id (GrdVkDevice *device); + +GrdVkDeviceFuncs *grd_vk_device_get_device_funcs (GrdVkDevice *device); + +const GrdVkShaderModules *grd_vk_device_get_shader_modules (GrdVkDevice *device); + +VkDeviceSize grd_vk_device_get_aligned_size (GrdVkDevice *device, + VkDeviceSize size); + +GrdVkQueue *grd_vk_device_acquire_queue (GrdVkDevice *device); + +void grd_vk_device_release_queue (GrdVkDevice *device, + GrdVkQueue *queue); diff --git a/grd-vk-image.c b/grd-vk-image.c new file mode 100644 index 0000000..3de1823 --- /dev/null +++ b/grd-vk-image.c @@ -0,0 +1,330 @@ +/* + * Copyright (C) 2022 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-vk-image.h" + +#include +#include + +#include "grd-vk-device.h" +#include "grd-vk-memory.h" +#include "grd-vk-physical-device.h" +#include "grd-vk-utils.h" + +struct _GrdVkImage +{ + GObject parent; + + GrdVkDevice *device; + + VkImage vk_image; + GrdVkMemory *memory; + + VkImageView vk_image_view; + VkImageLayout vk_image_layout; +}; + +G_DEFINE_TYPE (GrdVkImage, grd_vk_image, G_TYPE_OBJECT) + +VkImage +grd_vk_image_get_image (GrdVkImage *image) +{ + return image->vk_image; +} + +VkImageView +grd_vk_image_get_image_view (GrdVkImage *image) +{ + return image->vk_image_view; +} + +VkImageLayout +grd_vk_image_get_image_layout (GrdVkImage *image) +{ + return image->vk_image_layout; +} + +void +grd_vk_image_set_image_layout (GrdVkImage *image, + VkImageLayout vk_image_layout) +{ + image->vk_image_layout = vk_image_layout; +} + +static gboolean +is_image_format_supported_by_device (GrdVkImage *image, + const GrdVkImageDescriptor *image_descriptor, + GError **error) +{ + GrdVkPhysicalDevice *physical_device = + grd_vk_device_get_physical_device (image->device); + VkPhysicalDevice vk_physical_device = + grd_vk_physical_device_get_physical_device (physical_device); + VkImageCreateInfo *image_create_info = image_descriptor->image_create_info; + VkPhysicalDeviceImageFormatInfo2 image_format_info_2 = {}; + VkPhysicalDeviceExternalImageFormatInfo external_image_format_info = {}; + VkPhysicalDeviceImageDrmFormatModifierInfoEXT drm_format_modifier_info = {}; + VkImageFormatProperties2 image_format_properties_2 = {}; + VkImageFormatProperties *image_format_properties; + VkResult vk_result; + + g_assert (image_create_info); + if (image_descriptor->has_drm_format_modifier) + g_assert (image_descriptor->drm_format_modifier != DRM_FORMAT_MOD_INVALID); + + image_format_info_2.sType = + VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGE_FORMAT_INFO_2; + image_format_info_2.format = image_create_info->format; + image_format_info_2.type = image_create_info->imageType; + image_format_info_2.tiling = image_create_info->tiling; + image_format_info_2.usage = image_create_info->usage; + image_format_info_2.flags = image_create_info->flags; + + external_image_format_info.sType = + VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTERNAL_IMAGE_FORMAT_INFO; + external_image_format_info.handleType = image_descriptor->import_handle_type; + + drm_format_modifier_info.sType = + VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGE_DRM_FORMAT_MODIFIER_INFO_EXT; + drm_format_modifier_info.drmFormatModifier = + image_descriptor->drm_format_modifier; + drm_format_modifier_info.sharingMode = image_create_info->sharingMode; + drm_format_modifier_info.queueFamilyIndexCount = + image_create_info->queueFamilyIndexCount; + drm_format_modifier_info.pQueueFamilyIndices = + image_create_info->pQueueFamilyIndices; + + if (image_descriptor->import_handle_type) + grd_vk_append_to_chain (&image_format_info_2, &external_image_format_info); + if (image_descriptor->has_drm_format_modifier) + grd_vk_append_to_chain (&image_format_info_2, &drm_format_modifier_info); + + image_format_properties_2.sType = VK_STRUCTURE_TYPE_IMAGE_FORMAT_PROPERTIES_2; + + vk_result = + vkGetPhysicalDeviceImageFormatProperties2 (vk_physical_device, + &image_format_info_2, + &image_format_properties_2); + if (vk_result == VK_ERROR_FORMAT_NOT_SUPPORTED) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Required image format not supported by device"); + return FALSE; + } + if (vk_result != VK_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to check image format properties: %i", vk_result); + return FALSE; + } + + image_format_properties = &image_format_properties_2.imageFormatProperties; + if (image_format_properties->maxExtent.width < image_create_info->extent.width || + image_format_properties->maxExtent.height < image_create_info->extent.height || + image_format_properties->maxExtent.depth < image_create_info->extent.depth || + image_format_properties->maxMipLevels < image_create_info->mipLevels || + image_format_properties->maxArrayLayers < image_create_info->arrayLayers || + !(image_format_properties->sampleCounts & image_create_info->samples)) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Required image format properties not supported by device: " + "maxExtent: [%u, %u, %u], maxMipLevels: %u, " + "maxArrayLayers %u, sampleCounts: 0x%08X", + image_format_properties->maxExtent.width, + image_format_properties->maxExtent.height, + image_format_properties->maxExtent.depth, + image_format_properties->maxMipLevels, + image_format_properties->maxArrayLayers, + image_format_properties->sampleCounts); + return FALSE; + } + + return TRUE; +} + +static gboolean +bind_image_memory (GrdVkImage *image, + const GrdVkImageDescriptor *image_descriptor, + GError **error) +{ + VkDevice vk_device = grd_vk_device_get_device (image->device); + VkImageMemoryRequirementsInfo2 memory_requirements_info_2 = {}; + VkMemoryRequirements2 memory_requirements_2 = {}; + GrdVkMemoryDescriptor memory_descriptor = {}; + VkBindImageMemoryInfo bind_image_memory_info = {}; + VkResult vk_result; + + memory_requirements_info_2.sType = + VK_STRUCTURE_TYPE_IMAGE_MEMORY_REQUIREMENTS_INFO_2; + memory_requirements_info_2.image = image->vk_image; + memory_requirements_2.sType = VK_STRUCTURE_TYPE_MEMORY_REQUIREMENTS_2; + + vkGetImageMemoryRequirements2 (vk_device, &memory_requirements_info_2, + &memory_requirements_2); + + memory_descriptor.memory_requirements_2 = &memory_requirements_2; + memory_descriptor.memory_flags = image_descriptor->memory_flags; + memory_descriptor.import_handle_type = image_descriptor->import_handle_type; + memory_descriptor.fd = image_descriptor->fd; + + image->memory = grd_vk_memory_new (image->device, &memory_descriptor, error); + if (!image->memory) + return FALSE; + + bind_image_memory_info.sType = VK_STRUCTURE_TYPE_BIND_IMAGE_MEMORY_INFO; + bind_image_memory_info.image = image->vk_image; + bind_image_memory_info.memory = + grd_vk_memory_get_device_memory (image->memory); + bind_image_memory_info.memoryOffset = 0; + + vk_result = vkBindImageMemory2 (vk_device, 1, &bind_image_memory_info); + if (vk_result != VK_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to bind image memory: %i", vk_result); + return FALSE; + } + + return TRUE; +} + +static gboolean +create_image_view_for_plane (GrdVkImage *image, + const GrdVkImageDescriptor *image_descriptor, + GError **error) +{ + VkDevice vk_device = grd_vk_device_get_device (image->device); + VkImageCreateInfo *image_create_info = image_descriptor->image_create_info; + VkImageViewCreateInfo image_view_create_info = {}; + VkImageSubresourceRange *subresource_range; + VkResult vk_result; + + image_view_create_info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + image_view_create_info.image = image->vk_image; + image_view_create_info.format = image_create_info->format; + image_view_create_info.components.r = VK_COMPONENT_SWIZZLE_IDENTITY; + image_view_create_info.components.g = VK_COMPONENT_SWIZZLE_IDENTITY; + image_view_create_info.components.b = VK_COMPONENT_SWIZZLE_IDENTITY; + image_view_create_info.components.a = VK_COMPONENT_SWIZZLE_IDENTITY; + + switch (image_descriptor->image_create_info->imageType) + { + case VK_IMAGE_TYPE_2D: + image_view_create_info.viewType = VK_IMAGE_VIEW_TYPE_2D; + break; + default: + g_assert_not_reached (); + } + + subresource_range = &image_view_create_info.subresourceRange; + subresource_range->aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + subresource_range->baseMipLevel = 0; + subresource_range->levelCount = VK_REMAINING_MIP_LEVELS; + subresource_range->baseArrayLayer = 0; + subresource_range->layerCount = VK_REMAINING_ARRAY_LAYERS; + + vk_result = vkCreateImageView (vk_device, &image_view_create_info, NULL, + &image->vk_image_view); + if (vk_result != VK_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create image view: %i", vk_result); + return FALSE; + } + g_assert (image->vk_image_view != VK_NULL_HANDLE); + + return TRUE; +} + +GrdVkImage * +grd_vk_image_new (GrdVkDevice *device, + const GrdVkImageDescriptor *image_descriptor, + GError **error) +{ + VkDevice vk_device = grd_vk_device_get_device (device); + g_autoptr (GrdVkImage) image = NULL; + VkResult vk_result; + + g_assert (vk_device != VK_NULL_HANDLE); + g_assert (image_descriptor->image_create_info); + + image = g_object_new (GRD_TYPE_VK_IMAGE, NULL); + image->device = device; + + if (!is_image_format_supported_by_device (image, image_descriptor, error)) + return NULL; + + vk_result = vkCreateImage (vk_device, image_descriptor->image_create_info, + NULL, &image->vk_image); + if (vk_result != VK_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create image: %i", vk_result); + return NULL; + } + g_assert (image->vk_image != VK_NULL_HANDLE); + + if (!bind_image_memory (image, image_descriptor, error)) + return NULL; + + if (!create_image_view_for_plane (image, image_descriptor, error)) + return NULL; + + return g_steal_pointer (&image); +} + +static void +grd_vk_image_dispose (GObject *object) +{ + GrdVkImage *image = GRD_VK_IMAGE (object); + VkDevice vk_device = grd_vk_device_get_device (image->device); + + g_assert (vk_device != VK_NULL_HANDLE); + + if (image->vk_image_view != VK_NULL_HANDLE) + { + vkDestroyImageView (vk_device, image->vk_image_view, NULL); + image->vk_image_view = VK_NULL_HANDLE; + } + if (image->vk_image != VK_NULL_HANDLE) + { + vkDestroyImage (vk_device, image->vk_image, NULL); + image->vk_image = VK_NULL_HANDLE; + } + + g_clear_object (&image->memory); + + G_OBJECT_CLASS (grd_vk_image_parent_class)->dispose (object); +} + +static void +grd_vk_image_init (GrdVkImage *image) +{ + image->vk_image_layout = VK_IMAGE_LAYOUT_UNDEFINED; +} + +static void +grd_vk_image_class_init (GrdVkImageClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = grd_vk_image_dispose; +} diff --git a/grd-vk-image.h b/grd-vk-image.h new file mode 100644 index 0000000..4903d05 --- /dev/null +++ b/grd-vk-image.h @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2022 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include +#include + +#include "grd-types.h" + +#define GRD_TYPE_VK_IMAGE (grd_vk_image_get_type ()) +G_DECLARE_FINAL_TYPE (GrdVkImage, grd_vk_image, + GRD, VK_IMAGE, GObject) + +typedef struct +{ + VkImageCreateInfo *image_create_info; + VkMemoryPropertyFlagBits memory_flags; + + /* Only for import operations */ + VkExternalMemoryHandleTypeFlagBits import_handle_type; + int fd; + + uint64_t drm_format_modifier; + gboolean has_drm_format_modifier; +} GrdVkImageDescriptor; + +GrdVkImage *grd_vk_image_new (GrdVkDevice *device, + const GrdVkImageDescriptor *image_descriptor, + GError **error); + +VkImage grd_vk_image_get_image (GrdVkImage *image); + +VkImageView grd_vk_image_get_image_view (GrdVkImage *image); + +VkImageLayout grd_vk_image_get_image_layout (GrdVkImage *image); + +void grd_vk_image_set_image_layout (GrdVkImage *image, + VkImageLayout vk_image_layout); diff --git a/grd-vk-memory.c b/grd-vk-memory.c new file mode 100644 index 0000000..e48435a --- /dev/null +++ b/grd-vk-memory.c @@ -0,0 +1,257 @@ +/* + * Copyright (C) 2022 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-vk-memory.h" + +#include +#include + +#include "grd-vk-device.h" +#include "grd-vk-physical-device.h" +#include "grd-vk-utils.h" + +struct _GrdVkMemory +{ + GObject parent; + + GrdVkDevice *device; + + VkDeviceMemory vk_memory; + VkMemoryPropertyFlagBits memory_flags; + + void *mapped_pointer; +}; + +G_DEFINE_TYPE (GrdVkMemory, grd_vk_memory, G_TYPE_OBJECT) + +VkDeviceMemory +grd_vk_memory_get_device_memory (GrdVkMemory *memory) +{ + return memory->vk_memory; +} + +VkMemoryPropertyFlagBits +grd_vk_memory_get_memory_flags (GrdVkMemory *memory) +{ + return memory->memory_flags; +} + +void * +grd_vk_memory_get_mapped_pointer (GrdVkMemory *memory, + GError **error) +{ + VkDevice vk_device = grd_vk_device_get_device (memory->device); + VkResult vk_result; + + g_assert (memory->memory_flags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT); + g_assert (!memory->mapped_pointer); + + vk_result = vkMapMemory (vk_device, memory->vk_memory, 0, VK_WHOLE_SIZE, 0, + &memory->mapped_pointer); + if (vk_result != VK_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to map memory: %i", vk_result); + return NULL; + } + g_assert (memory->mapped_pointer); + + return memory->mapped_pointer; +} + +static gboolean +prepare_fd_import (GrdVkMemory *memory, + const GrdVkMemoryDescriptor *memory_descriptor, + uint32_t *memory_type_bits, + VkImportMemoryFdInfoKHR *import_memory_fd_info, + GError **error) +{ + GrdVkDeviceFuncs *device_funcs = grd_vk_device_get_device_funcs (memory->device); + VkDevice vk_device = grd_vk_device_get_device (memory->device); + VkExternalMemoryHandleTypeFlagBits handle_type = + memory_descriptor->import_handle_type; + VkMemoryFdPropertiesKHR fd_properties = {}; + VkResult vk_result; + + g_assert (vk_device != VK_NULL_HANDLE); + g_assert (device_funcs->vkGetMemoryFdPropertiesKHR); + + g_assert (handle_type == VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT || + handle_type == VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT); + g_assert (memory_descriptor->fd != -1); + + fd_properties.sType = VK_STRUCTURE_TYPE_MEMORY_FD_PROPERTIES_KHR; + + vk_result = device_funcs->vkGetMemoryFdPropertiesKHR (vk_device, handle_type, + memory_descriptor->fd, + &fd_properties); + if (vk_result != VK_SUCCESS) + { + g_set_error (error, G_IO_ERROR, g_io_error_from_errno (errno), + "Failed to get memory fd properties: %s", + g_strerror (errno)); + return FALSE; + } + *memory_type_bits = fd_properties.memoryTypeBits; + + import_memory_fd_info->sType = VK_STRUCTURE_TYPE_IMPORT_MEMORY_FD_INFO_KHR; + import_memory_fd_info->handleType = handle_type; + + /* + * 11.2.5. File Descriptor External Memory + * + * Importing memory from a file descriptor transfers ownership of the file + * descriptor from the application to the Vulkan implementation. + * The application must not perform any operations on the file descriptor + * after a successful import. + * The imported memory object holds a reference to its payload. + */ + import_memory_fd_info->fd = dup (memory_descriptor->fd); + + if (import_memory_fd_info->fd == -1) + { + g_set_error (error, G_IO_ERROR, g_io_error_from_errno (errno), + "Failed to duplicate fd: %s", g_strerror (errno)); + return FALSE; + } + g_assert (import_memory_fd_info->fd != -1); + + return TRUE; +} + +static gboolean +get_memory_type_index (GrdVkMemory *memory, + uint32_t memory_type_bits, + VkMemoryPropertyFlagBits memory_flags, + uint32_t *memory_type_idx, + GError **error) +{ + GrdVkPhysicalDevice *physical_device = + grd_vk_device_get_physical_device (memory->device); + VkPhysicalDevice vk_physical_device = + grd_vk_physical_device_get_physical_device (physical_device); + VkPhysicalDeviceMemoryProperties phys_dev_mem_props = {}; + uint32_t i = 0; + + vkGetPhysicalDeviceMemoryProperties (vk_physical_device, &phys_dev_mem_props); + + for (i = 0; i < phys_dev_mem_props.memoryTypeCount; ++i) + { + VkMemoryType memory_type = phys_dev_mem_props.memoryTypes[i]; + + if (memory_type_bits & 1 << i && + (memory_type.propertyFlags & memory_flags) == memory_flags) + { + *memory_type_idx = i; + memory->memory_flags = memory_type.propertyFlags; + return TRUE; + } + } + + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to find memory type index"); + + return FALSE; +} + +GrdVkMemory * +grd_vk_memory_new (GrdVkDevice *device, + const GrdVkMemoryDescriptor *memory_descriptor, + GError **error) +{ + VkDevice vk_device = grd_vk_device_get_device (device); + const VkMemoryRequirements *memory_requirements = + &memory_descriptor->memory_requirements_2->memoryRequirements; + g_autoptr (GrdVkMemory) memory = NULL; + VkMemoryAllocateInfo memory_allocate_info = {}; + VkImportMemoryFdInfoKHR import_memory_fd_info = {}; + uint32_t memory_type_bits; + VkResult vk_result; + + import_memory_fd_info.fd = -1; + + memory = g_object_new (GRD_TYPE_VK_MEMORY, NULL); + memory->device = device; + + memory_allocate_info.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + memory_allocate_info.allocationSize = memory_requirements->size; + + memory_type_bits = memory_requirements->memoryTypeBits; + if (memory_descriptor->import_handle_type) + { + if (!prepare_fd_import (memory, memory_descriptor, &memory_type_bits, + &import_memory_fd_info, error)) + return NULL; + + grd_vk_append_to_chain (&memory_allocate_info, &import_memory_fd_info); + } + + if (!get_memory_type_index (memory, memory_type_bits, + memory_descriptor->memory_flags, + &memory_allocate_info.memoryTypeIndex, error)) + { + g_clear_fd (&import_memory_fd_info.fd, NULL); + return NULL; + } + + vk_result = vkAllocateMemory (vk_device, &memory_allocate_info, NULL, + &memory->vk_memory); + if (vk_result != VK_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to allocate device memory: %i", vk_result); + g_clear_fd (&import_memory_fd_info.fd, NULL); + return NULL; + } + g_assert (memory->vk_memory != VK_NULL_HANDLE); + + return g_steal_pointer (&memory); +} + +static void +grd_vk_memory_dispose (GObject *object) +{ + GrdVkMemory *memory = GRD_VK_MEMORY (object); + VkDevice vk_device = grd_vk_device_get_device (memory->device); + + g_assert (vk_device != VK_NULL_HANDLE); + + if (memory->vk_memory != VK_NULL_HANDLE) + { + vkFreeMemory (vk_device, memory->vk_memory, NULL); + memory->vk_memory = VK_NULL_HANDLE; + } + + G_OBJECT_CLASS (grd_vk_memory_parent_class)->dispose (object); +} + +static void +grd_vk_memory_init (GrdVkMemory *memory) +{ +} + +static void +grd_vk_memory_class_init (GrdVkMemoryClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = grd_vk_memory_dispose; +} diff --git a/grd-vk-memory.h b/grd-vk-memory.h new file mode 100644 index 0000000..f17ff96 --- /dev/null +++ b/grd-vk-memory.h @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2022 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include +#include + +#include "grd-types.h" + +#define GRD_TYPE_VK_MEMORY (grd_vk_memory_get_type ()) +G_DECLARE_FINAL_TYPE (GrdVkMemory, grd_vk_memory, + GRD, VK_MEMORY, GObject) + +typedef struct +{ + VkMemoryRequirements2 *memory_requirements_2; + VkMemoryPropertyFlagBits memory_flags; + + /* Only for import operations */ + VkExternalMemoryHandleTypeFlagBits import_handle_type; + int fd; +} GrdVkMemoryDescriptor; + +GrdVkMemory *grd_vk_memory_new (GrdVkDevice *device, + const GrdVkMemoryDescriptor *memory_descriptor, + GError **error); + +VkDeviceMemory grd_vk_memory_get_device_memory (GrdVkMemory *memory); + +VkMemoryPropertyFlagBits grd_vk_memory_get_memory_flags (GrdVkMemory *memory); + +void *grd_vk_memory_get_mapped_pointer (GrdVkMemory *memory, + GError **error); diff --git a/grd-vk-physical-device.c b/grd-vk-physical-device.c new file mode 100644 index 0000000..a18e337 --- /dev/null +++ b/grd-vk-physical-device.c @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2026 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-vk-physical-device.h" + +struct _GrdVkPhysicalDevice +{ + GObject parent; + + VkPhysicalDevice vk_physical_device; + + /* Physical device features */ + GrdVkDeviceFeatures device_features; +}; + +G_DEFINE_TYPE (GrdVkPhysicalDevice, grd_vk_physical_device, G_TYPE_OBJECT) + +VkPhysicalDevice +grd_vk_physical_device_get_physical_device (GrdVkPhysicalDevice *physical_device) +{ + return physical_device->vk_physical_device; +} + +GrdVkDeviceFeatures +grd_vk_physical_device_get_device_features (GrdVkPhysicalDevice *physical_device) +{ + return physical_device->device_features; +} + +GrdVkPhysicalDevice * +grd_vk_physical_device_new (VkPhysicalDevice vk_physical_device, + GrdVkDeviceFeatures device_features) +{ + GrdVkPhysicalDevice *physical_device; + + physical_device = g_object_new (GRD_TYPE_VK_PHYSICAL_DEVICE, NULL); + physical_device->vk_physical_device = vk_physical_device; + physical_device->device_features = device_features; + + return physical_device; +} + +static void +grd_vk_physical_device_init (GrdVkPhysicalDevice *device) +{ +} + +static void +grd_vk_physical_device_class_init (GrdVkPhysicalDeviceClass *klass) +{ +} diff --git a/grd-vk-physical-device.h b/grd-vk-physical-device.h new file mode 100644 index 0000000..2eab85e --- /dev/null +++ b/grd-vk-physical-device.h @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2026 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include +#include + +#define GRD_TYPE_VK_PHYSICAL_DEVICE (grd_vk_physical_device_get_type ()) +G_DECLARE_FINAL_TYPE (GrdVkPhysicalDevice, grd_vk_physical_device, + GRD, VK_PHYSICAL_DEVICE, GObject) + +typedef enum +{ + GRD_VK_DEVICE_FEATURE_UPDATE_AFTER_BIND_SAMPLED_IMAGE = 1 << 0, + GRD_VK_DEVICE_FEATURE_UPDATE_AFTER_BIND_STORAGE_IMAGE = 1 << 1, +} GrdVkDeviceFeatures; + +GrdVkPhysicalDevice *grd_vk_physical_device_new (VkPhysicalDevice vk_physical_device, + GrdVkDeviceFeatures device_features); + +VkPhysicalDevice grd_vk_physical_device_get_physical_device (GrdVkPhysicalDevice *physical_device); + +GrdVkDeviceFeatures grd_vk_physical_device_get_device_features (GrdVkPhysicalDevice *physical_device); diff --git a/grd-vk-queue.c b/grd-vk-queue.c new file mode 100644 index 0000000..4cbac20 --- /dev/null +++ b/grd-vk-queue.c @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2022 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-vk-queue.h" + +#include + +#include "grd-vk-device.h" + +struct _GrdVkQueue +{ + GObject parent; + + GrdVkDevice *device; + + uint32_t queue_family_idx; + uint32_t queue_idx; + + GMutex queue_mutex; + VkQueue vk_queue; +}; + +G_DEFINE_TYPE (GrdVkQueue, grd_vk_queue, G_TYPE_OBJECT) + +uint32_t +grd_vk_queue_get_queue_family_idx (GrdVkQueue *queue) +{ + return queue->queue_family_idx; +} + +uint32_t +grd_vk_queue_get_queue_idx (GrdVkQueue *queue) +{ + return queue->queue_idx; +} + +gboolean +grd_vk_queue_submit (GrdVkQueue *queue, + const VkSubmitInfo2 *submit_infos_2, + uint32_t n_submit_infos_2, + VkFence vk_fence, + GError **error) +{ + GrdVkDevice *device = queue->device; + GrdVkDeviceFuncs *device_funcs = grd_vk_device_get_device_funcs (device); + g_autoptr (GMutexLocker) locker = NULL; + VkResult vk_result; + + g_assert (device_funcs->vkQueueSubmit2KHR); + + locker = g_mutex_locker_new (&queue->queue_mutex); + vk_result = device_funcs->vkQueueSubmit2KHR (queue->vk_queue, + n_submit_infos_2, submit_infos_2, + vk_fence); + if (vk_result != VK_SUCCESS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to submit command buffers to queue: %i", vk_result); + return FALSE; + } + + return TRUE; +} + +GrdVkQueue * +grd_vk_queue_new (GrdVkDevice *device, + uint32_t queue_family_idx, + uint32_t queue_idx) +{ + VkDevice vk_device = grd_vk_device_get_device (device); + GrdVkQueue *queue; + VkDeviceQueueInfo2 device_queue_info_2 = {}; + + g_assert (vk_device != VK_NULL_HANDLE); + + queue = g_object_new (GRD_TYPE_VK_QUEUE, NULL); + queue->device = device; + queue->queue_family_idx = queue_family_idx; + queue->queue_idx = queue_idx; + + device_queue_info_2.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_INFO_2; + device_queue_info_2.queueFamilyIndex = queue_family_idx; + device_queue_info_2.queueIndex = queue_idx; + + vkGetDeviceQueue2 (vk_device, &device_queue_info_2, &queue->vk_queue); + g_assert (queue->vk_queue != VK_NULL_HANDLE); + + return queue; +} + +static void +grd_vk_queue_finalize (GObject *object) +{ + GrdVkQueue *queue = GRD_VK_QUEUE (object); + + g_mutex_clear (&queue->queue_mutex); + + G_OBJECT_CLASS (grd_vk_queue_parent_class)->finalize (object); +} + +static void +grd_vk_queue_init (GrdVkQueue *queue) +{ + g_mutex_init (&queue->queue_mutex); +} + +static void +grd_vk_queue_class_init (GrdVkQueueClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = grd_vk_queue_finalize; +} diff --git a/grd-vk-queue.h b/grd-vk-queue.h new file mode 100644 index 0000000..5d9b7be --- /dev/null +++ b/grd-vk-queue.h @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2022 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include +#include + +#include "grd-types.h" + +#define GRD_TYPE_VK_QUEUE (grd_vk_queue_get_type ()) +G_DECLARE_FINAL_TYPE (GrdVkQueue, grd_vk_queue, + GRD, VK_QUEUE, GObject) + +GrdVkQueue *grd_vk_queue_new (GrdVkDevice *device, + uint32_t queue_family_idx, + uint32_t queue_idx); + +uint32_t grd_vk_queue_get_queue_family_idx (GrdVkQueue *queue); + +uint32_t grd_vk_queue_get_queue_idx (GrdVkQueue *queue); + +gboolean grd_vk_queue_submit (GrdVkQueue *queue, + const VkSubmitInfo2 *submit_infos_2, + uint32_t n_submit_infos_2, + VkFence vk_fence, + GError **error); diff --git a/grd-vk-utils.c b/grd-vk-utils.c new file mode 100644 index 0000000..ed94329 --- /dev/null +++ b/grd-vk-utils.c @@ -0,0 +1,222 @@ +/* + * Copyright (C) 2022 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "grd-vk-utils.h" + +#include + +#include "grd-vk-device.h" +#include "grd-vk-image.h" + +typedef struct +{ + VkStructureType type; /* sType */ + const void *next; /* pNext */ +} GrdVkBaseStruct; + +void +grd_vk_append_to_chain (void *chain, + void *element) +{ + GrdVkBaseStruct *tmp; + + tmp = chain; + while (tmp->next) + tmp = (GrdVkBaseStruct *) tmp->next; + + tmp->next = element; +} + +void +grd_vk_clear_command_pool (GrdVkDevice *device, + VkCommandPool *vk_command_pool) +{ + VkDevice vk_device = grd_vk_device_get_device (device); + + g_assert (vk_device != VK_NULL_HANDLE); + + if (*vk_command_pool == VK_NULL_HANDLE) + return; + + vkDestroyCommandPool (vk_device, *vk_command_pool, NULL); + *vk_command_pool = VK_NULL_HANDLE; +} + +void +grd_vk_clear_descriptor_pool (GrdVkDevice *device, + VkDescriptorPool *vk_descriptor_pool) +{ + VkDevice vk_device = grd_vk_device_get_device (device); + + g_assert (vk_device != VK_NULL_HANDLE); + + if (*vk_descriptor_pool == VK_NULL_HANDLE) + return; + + vkDestroyDescriptorPool (vk_device, *vk_descriptor_pool, NULL); + *vk_descriptor_pool = VK_NULL_HANDLE; +} + +void +grd_vk_clear_descriptor_set_layout (GrdVkDevice *device, + VkDescriptorSetLayout *vk_descriptor_set_layout) +{ + VkDevice vk_device = grd_vk_device_get_device (device); + + g_assert (vk_device != VK_NULL_HANDLE); + + if (*vk_descriptor_set_layout == VK_NULL_HANDLE) + return; + + vkDestroyDescriptorSetLayout (vk_device, *vk_descriptor_set_layout, NULL); + *vk_descriptor_set_layout = VK_NULL_HANDLE; +} + +void +grd_vk_clear_fence (GrdVkDevice *device, + VkFence *vk_fence) +{ + VkDevice vk_device = grd_vk_device_get_device (device); + + g_assert (vk_device != VK_NULL_HANDLE); + + if (*vk_fence == VK_NULL_HANDLE) + return; + + vkDestroyFence (vk_device, *vk_fence, NULL); + *vk_fence = VK_NULL_HANDLE; +} + +void +grd_vk_clear_query_pool (GrdVkDevice *device, + VkQueryPool *vk_query_pool) +{ + VkDevice vk_device = grd_vk_device_get_device (device); + + g_assert (vk_device != VK_NULL_HANDLE); + + if (*vk_query_pool == VK_NULL_HANDLE) + return; + + vkDestroyQueryPool (vk_device, *vk_query_pool, NULL); + *vk_query_pool = VK_NULL_HANDLE; +} + +void +grd_vk_clear_sampler (GrdVkDevice *device, + VkSampler *vk_sampler) +{ + VkDevice vk_device = grd_vk_device_get_device (device); + + g_assert (vk_device != VK_NULL_HANDLE); + + if (*vk_sampler == VK_NULL_HANDLE) + return; + + vkDestroySampler (vk_device, *vk_sampler, NULL); + *vk_sampler = VK_NULL_HANDLE; +} + +gboolean +grd_vk_get_vk_format_from_drm_format (uint32_t drm_format, + VkFormat *vk_format, + GError **error) +{ + *vk_format = VK_FORMAT_UNDEFINED; + + switch (drm_format) + { + case DRM_FORMAT_ARGB8888: + case DRM_FORMAT_XRGB8888: + *vk_format = VK_FORMAT_B8G8R8A8_UNORM; + return TRUE; + } + + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "No VkFormat available for DRM format 0x%08X", drm_format); + + return FALSE; +} + +GrdVkImage * +grd_vk_dma_buf_image_new (GrdVkDevice *vk_device, + VkFormat vk_format, + uint32_t width, + uint32_t height, + VkImageUsageFlags usage_flags, + int fd, + VkDeviceSize offset, + VkDeviceSize row_pitch, + uint64_t drm_format_modifier, + GError **error) +{ + GrdVkImageDescriptor image_descriptor = {}; + VkImageCreateInfo image_create_info = {}; + VkExternalMemoryImageCreateInfo external_memory_image_create_info = {}; + VkImageDrmFormatModifierExplicitCreateInfoEXT explicit_create_info = {}; + VkSubresourceLayout subresource_layout = {}; + VkExternalMemoryHandleTypeFlagBits handle_type; + + g_assert (fd != -1); + g_assert (drm_format_modifier != DRM_FORMAT_MOD_INVALID); + + image_create_info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; + image_create_info.flags = 0; + image_create_info.imageType = VK_IMAGE_TYPE_2D; + image_create_info.format = vk_format; + image_create_info.extent.width = width; + image_create_info.extent.height = height; + image_create_info.extent.depth = 1; + image_create_info.mipLevels = 1; + image_create_info.arrayLayers = 1; + image_create_info.samples = VK_SAMPLE_COUNT_1_BIT; + image_create_info.tiling = VK_IMAGE_TILING_DRM_FORMAT_MODIFIER_EXT; + image_create_info.usage = usage_flags; + image_create_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + image_create_info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + + handle_type = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT; + + external_memory_image_create_info.sType = + VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO; + external_memory_image_create_info.handleTypes = handle_type; + + subresource_layout.offset = offset; + subresource_layout.rowPitch = row_pitch; + + explicit_create_info.sType = + VK_STRUCTURE_TYPE_IMAGE_DRM_FORMAT_MODIFIER_EXPLICIT_CREATE_INFO_EXT; + explicit_create_info.drmFormatModifier = drm_format_modifier; + explicit_create_info.drmFormatModifierPlaneCount = 1; + explicit_create_info.pPlaneLayouts = &subresource_layout; + + grd_vk_append_to_chain (&image_create_info, + &external_memory_image_create_info); + grd_vk_append_to_chain (&image_create_info, &explicit_create_info); + + image_descriptor.image_create_info = &image_create_info; + image_descriptor.import_handle_type = handle_type; + image_descriptor.fd = fd; + image_descriptor.drm_format_modifier = drm_format_modifier; + image_descriptor.has_drm_format_modifier = TRUE; + + return grd_vk_image_new (vk_device, &image_descriptor, error); +} diff --git a/grd-vk-utils.h b/grd-vk-utils.h new file mode 100644 index 0000000..009b1a0 --- /dev/null +++ b/grd-vk-utils.h @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2022 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#include +#include + +#include "grd-types.h" + +void grd_vk_append_to_chain (void *chain, + void *element); + +void grd_vk_clear_command_pool (GrdVkDevice *device, + VkCommandPool *vk_command_pool); + +void grd_vk_clear_descriptor_pool (GrdVkDevice *device, + VkDescriptorPool *vk_descriptor_pool); + +void grd_vk_clear_descriptor_set_layout (GrdVkDevice *device, + VkDescriptorSetLayout *vk_descriptor_set_layout); + +void grd_vk_clear_fence (GrdVkDevice *device, + VkFence *vk_fence); + +void grd_vk_clear_query_pool (GrdVkDevice *device, + VkQueryPool *vk_query_pool); + +void grd_vk_clear_sampler (GrdVkDevice *device, + VkSampler *vk_sampler); + +gboolean grd_vk_get_vk_format_from_drm_format (uint32_t drm_format, + VkFormat *vk_format, + GError **error); + +GrdVkImage *grd_vk_dma_buf_image_new (GrdVkDevice *vk_device, + VkFormat vk_format, + uint32_t width, + uint32_t height, + VkImageUsageFlags usage_flags, + int fd, + VkDeviceSize offset, + VkDeviceSize row_pitch, + uint64_t drm_format_modifier, + GError **error); diff --git a/grd-vnc-cursor.c b/grd-vnc-cursor.c new file mode 100644 index 0000000..c0cb1aa --- /dev/null +++ b/grd-vnc-cursor.c @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2018 Red Hat Inc. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + */ + +#include "config.h" + +#include "grd-vnc-cursor.h" + +#include +#include + +static void +get_pixel_components (uint32_t pixel, + GrdPixelFormat format, + uint8_t *a, + uint8_t *r, + uint8_t *g, + uint8_t *b) +{ + g_assert (format == GRD_PIXEL_FORMAT_RGBA8888); + + *a = (pixel & 0xff000000) >> 24; + *b = (pixel & 0xff0000) >> 16; + *g = (pixel & 0xff00) >> 8; + *r = pixel & 0xff; +} + +static gboolean +is_practically_black (uint8_t r, + uint8_t g, + uint8_t b) +{ + if (r <= 0x62 && + g <= 0x62 && + b <= 0x62) + return TRUE; + else + return FALSE; +} + +static gboolean +is_practically_opaque (uint8_t a) +{ + return a > 0xe0; +} + +rfbCursorPtr +grd_vnc_create_cursor (int width, + int height, + int stride, + GrdPixelFormat format, + uint8_t *buf) +{ + g_autofree char *cursor = NULL; + g_autofree char *mask = NULL; + int y; + + g_return_val_if_fail (format == GRD_PIXEL_FORMAT_RGBA8888, NULL); + + cursor = g_new0 (char, width * height); + mask = g_new0 (char, width * height); + + for (y = 0; y < height; y++) + { + uint32_t *pixel_row; + int x; + + pixel_row = (uint32_t *) &buf[y * stride]; + + for (x = 0; x < width; x++) + { + uint32_t pixel = pixel_row[x]; + uint8_t a, r, g, b; + + get_pixel_components (pixel, + format, + &a, &r, &g, &b); + + if (is_practically_opaque (a)) + { + if (is_practically_black (r, g, b)) + cursor[y * width + x] = ' '; + else + cursor[y * width + x] = 'x'; + + mask[y * width + x] = 'x'; + } + else + { + cursor[y * width + x] = ' '; + mask[y * width + x] = ' '; + } + } + } + + return rfbMakeXCursor (width, height, cursor, mask); +} + +rfbCursorPtr +grd_vnc_create_empty_cursor (int width, + int height) +{ + g_autofree char *cursor = NULL; + cursor = g_new0 (char, width * height); + + memset (cursor, ' ', width * height); + + return rfbMakeXCursor (width, height, cursor, cursor); +} diff --git a/grd-vnc-cursor.h b/grd-vnc-cursor.h new file mode 100644 index 0000000..ee7cc61 --- /dev/null +++ b/grd-vnc-cursor.h @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2018 Red Hat Inc. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + */ + +#pragma once + +#include + +#include "grd-types.h" + +rfbCursorPtr grd_vnc_create_cursor (int width, + int height, + int stride, + GrdPixelFormat format, + uint8_t *buf); + +rfbCursorPtr grd_vnc_create_empty_cursor (int width, + int height); diff --git a/grd-vnc-monitor-config.h b/grd-vnc-monitor-config.h new file mode 100644 index 0000000..d807ace --- /dev/null +++ b/grd-vnc-monitor-config.h @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2022 Pascal Nowack + * Copyright (C) 2022 SUSE Software Solutions Germany GmbH + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#pragma once + +#define GRD_VNC_CLAMP_DESKTOP_SIZE(value) MAX (MIN (value, 8192), 200) +#define GRD_VNC_DEFAULT_WIDTH 1920 +#define GRD_VNC_DEFAULT_HEIGHT 1080 + +typedef struct _GrdVncVirtualMonitor +{ + uint16_t pos_x; + uint16_t pos_y; + uint16_t width; /* Valid values are in range of [200, 8192] */ + uint16_t height; /* Valid values are in range of [200, 8192] */ +} GrdVncVirtualMonitor; + +typedef struct _GrdVncMonitorConfig +{ + gboolean is_virtual; + + /* Server configs only */ + char **connectors; + /* Client configs only */ + GrdVncVirtualMonitor *virtual_monitors; + /* Count of items in connectors or virtual_monitors */ + uint32_t monitor_count; +} GrdVncMonitorConfig; diff --git a/grd-vnc-pipewire-stream.c b/grd-vnc-pipewire-stream.c new file mode 100644 index 0000000..939230a --- /dev/null +++ b/grd-vnc-pipewire-stream.c @@ -0,0 +1,1115 @@ +/* + * Copyright (C) 2018 Red Hat Inc. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + */ + +#include "config.h" + +#include "grd-vnc-pipewire-stream.h" + +#include +#include +#include +#include +#include + +#include "grd-context.h" +#include "grd-egl-thread.h" +#include "grd-pipewire-utils.h" +#include "grd-utils.h" +#include "grd-vnc-cursor.h" + +#define MAX_FORMAT_PARAMS 2 + +enum +{ + CLOSED, + + N_SIGNALS +}; + +static guint signals[N_SIGNALS]; + +typedef struct _GrdVncFrame GrdVncFrame; + +typedef void (* GrdVncFrameReadyCallback) (GrdVncPipeWireStream *stream, + GrdVncFrame *frame, + gboolean success, + gpointer user_data); + +typedef struct +{ + GMutex buffer_mutex; + gboolean is_locked; +} BufferContext; + +struct _GrdVncFrame +{ + gatomicrefcount refcount; + + void *data; + + GrdVncPipeWireStream *stream; + GrdVncFrameReadyCallback callback; + gpointer callback_user_data; +}; + +typedef struct +{ + rfbCursorPtr rfb_cursor; + gboolean cursor_moved; + int cursor_x; + int cursor_y; +} VncPointer; + +struct _GrdVncPipeWireStream +{ + GObject parent; + + GrdSessionVnc *session; + GrdEglThreadSlot egl_slot; + + GMutex dequeue_mutex; + gboolean dequeuing_disallowed; + + GSource *pipewire_source; + struct pw_context *pipewire_context; + struct pw_core *pipewire_core; + + struct spa_hook pipewire_core_listener; + + GMutex frame_mutex; + GrdVncFrame *pending_frame; + GSource *pending_frame_source; + + GMutex pointer_mutex; + VncPointer *pending_pointer; + GSource *pending_pointer_source; + + struct pw_stream *pipewire_stream; + struct spa_hook pipewire_stream_listener; + + GHashTable *pipewire_buffers; + + uint32_t src_node_id; + + struct spa_video_info_raw spa_format; +}; + +G_DEFINE_TYPE (GrdVncPipeWireStream, grd_vnc_pipewire_stream, + G_TYPE_OBJECT) + +static void grd_vnc_frame_unref (GrdVncFrame *frame); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (GrdVncFrame, grd_vnc_frame_unref) + +static BufferContext * +buffer_context_new (void) +{ + BufferContext *buffer_context; + + buffer_context = g_new0 (BufferContext, 1); + g_mutex_init (&buffer_context->buffer_mutex); + + return buffer_context; +} + +static void +buffer_context_free (BufferContext *buffer_context) +{ + g_mutex_clear (&buffer_context->buffer_mutex); + + g_free (buffer_context); +} + +static void +acquire_pipewire_buffer_lock (GrdVncPipeWireStream *stream, + struct pw_buffer *buffer) +{ + BufferContext *buffer_context = NULL; + + if (!g_hash_table_lookup_extended (stream->pipewire_buffers, buffer, + NULL, (gpointer *) &buffer_context)) + g_assert_not_reached (); + + g_mutex_lock (&buffer_context->buffer_mutex); + g_assert (!buffer_context->is_locked); + buffer_context->is_locked = TRUE; +} + +static void +maybe_release_pipewire_buffer_lock (GrdVncPipeWireStream *stream, + struct pw_buffer *buffer) +{ + BufferContext *buffer_context = NULL; + + if (!g_hash_table_lookup_extended (stream->pipewire_buffers, buffer, + NULL, (gpointer *) &buffer_context)) + g_assert_not_reached (); + + if (!buffer_context->is_locked) + return; + + buffer_context->is_locked = FALSE; + g_mutex_unlock (&buffer_context->buffer_mutex); +} + +static void +vnc_pointer_free (VncPointer *vnc_pointer) +{ + g_clear_pointer (&vnc_pointer->rfb_cursor, rfbFreeCursor); + g_free (vnc_pointer); +} + +static void +add_common_format_params (struct spa_pod_builder *pod_builder, + enum spa_video_format spa_format, + const GrdVncVirtualMonitor *virtual_monitor) +{ + struct spa_rectangle min_rect; + struct spa_rectangle max_rect; + struct spa_fraction min_framerate; + struct spa_fraction max_framerate; + + min_rect = SPA_RECTANGLE (1, 1); + max_rect = SPA_RECTANGLE (INT32_MAX, INT32_MAX); + min_framerate = SPA_FRACTION (1, 1); + max_framerate = SPA_FRACTION (30, 1); + + spa_pod_builder_add (pod_builder, + SPA_FORMAT_mediaType, + SPA_POD_Id (SPA_MEDIA_TYPE_video), 0); + spa_pod_builder_add (pod_builder, + SPA_FORMAT_mediaSubtype, + SPA_POD_Id (SPA_MEDIA_SUBTYPE_raw), 0); + spa_pod_builder_add (pod_builder, + SPA_FORMAT_VIDEO_format, + SPA_POD_Id (spa_format), 0); + + if (virtual_monitor) + { + struct spa_rectangle virtual_monitor_rect; + + virtual_monitor_rect = SPA_RECTANGLE (virtual_monitor->width, + virtual_monitor->height); + spa_pod_builder_add (pod_builder, + SPA_FORMAT_VIDEO_size, + SPA_POD_Rectangle (&virtual_monitor_rect), 0); + } + else + { + spa_pod_builder_add (pod_builder, + SPA_FORMAT_VIDEO_size, + SPA_POD_CHOICE_RANGE_Rectangle (&min_rect, + &min_rect, + &max_rect), 0); + } + + spa_pod_builder_add (pod_builder, + SPA_FORMAT_VIDEO_framerate, + SPA_POD_Fraction (&SPA_FRACTION (0, 1)), 0); + spa_pod_builder_add (pod_builder, + SPA_FORMAT_VIDEO_maxFramerate, + SPA_POD_CHOICE_RANGE_Fraction (&max_framerate, + &min_framerate, + &max_framerate), 0); +} + +static uint32_t +add_format_params (GrdVncPipeWireStream *stream, + const GrdVncVirtualMonitor *virtual_monitor, + struct spa_pod_builder *pod_builder, + const struct spa_pod **params, + uint32_t n_available_params) +{ + GrdSession *session = GRD_SESSION (stream->session); + GrdContext *context = grd_session_get_context (session); + GrdEglThread *egl_thread = grd_context_get_egl_thread (context); + struct spa_pod_frame format_frame; + enum spa_video_format spa_format = SPA_VIDEO_FORMAT_BGRx; + gboolean need_fallback_format = FALSE; + uint32_t n_params = 0; + + g_assert (n_available_params >= 2); + + spa_pod_builder_push_object (pod_builder, &format_frame, + SPA_TYPE_OBJECT_Format, + SPA_PARAM_EnumFormat); + add_common_format_params (pod_builder, spa_format, virtual_monitor); + + if (egl_thread) + { + uint32_t drm_format; + int n_modifiers; + g_autofree uint64_t *modifiers = NULL; + + grd_get_spa_format_details (spa_format, &drm_format, NULL); + if (grd_egl_thread_get_modifiers_for_format (egl_thread, drm_format, + &n_modifiers, + &modifiers)) + { + struct spa_pod_frame modifier_frame; + int i; + + spa_pod_builder_prop (pod_builder, + SPA_FORMAT_VIDEO_modifier, + (SPA_POD_PROP_FLAG_MANDATORY | + SPA_POD_PROP_FLAG_DONT_FIXATE)); + + spa_pod_builder_push_choice (pod_builder, &modifier_frame, + SPA_CHOICE_Enum, 0); + spa_pod_builder_long (pod_builder, modifiers[0]); + + for (i = 0; i < n_modifiers; i++) + { + uint64_t modifier = modifiers[i]; + + spa_pod_builder_long (pod_builder, modifier); + } + spa_pod_builder_long (pod_builder, DRM_FORMAT_MOD_INVALID); + spa_pod_builder_pop (pod_builder, &modifier_frame); + + need_fallback_format = TRUE; + } + } + + params[n_params++] = spa_pod_builder_pop (pod_builder, &format_frame); + + if (need_fallback_format) + { + spa_pod_builder_push_object (pod_builder, &format_frame, + SPA_TYPE_OBJECT_Format, + SPA_PARAM_EnumFormat); + add_common_format_params (pod_builder, spa_format, virtual_monitor); + params[n_params++] = spa_pod_builder_pop (pod_builder, &format_frame); + } + + return n_params; +} + +void +grd_vnc_pipewire_stream_resize (GrdVncPipeWireStream *stream, + GrdVncVirtualMonitor *virtual_monitor) +{ + uint8_t params_buffer[1024]; + struct spa_pod_builder pod_builder; + const struct spa_pod *params[MAX_FORMAT_PARAMS] = {}; + uint32_t n_params = 0; + + pod_builder = SPA_POD_BUILDER_INIT (params_buffer, sizeof (params_buffer)); + + n_params += add_format_params (stream, virtual_monitor, &pod_builder, + params, MAX_FORMAT_PARAMS); + + g_assert (n_params > 0); + pw_stream_update_params (stream->pipewire_stream, params, n_params); +} + +static void +on_stream_state_changed (void *user_data, + enum pw_stream_state old, + enum pw_stream_state state, + const char *error) +{ + g_debug ("Pipewire stream state changed from %s to %s", + pw_stream_state_as_string (old), + pw_stream_state_as_string (state)); + + switch (state) + { + case PW_STREAM_STATE_ERROR: + g_warning ("PipeWire stream error: %s", error); + break; + case PW_STREAM_STATE_PAUSED: + case PW_STREAM_STATE_STREAMING: + case PW_STREAM_STATE_UNCONNECTED: + case PW_STREAM_STATE_CONNECTING: + break; + } +} + +static void +on_stream_param_changed (void *user_data, + uint32_t id, + const struct spa_pod *format) +{ + GrdVncPipeWireStream *stream = GRD_VNC_PIPEWIRE_STREAM (user_data); + GrdSession *session = GRD_SESSION (stream->session); + GrdContext *context = grd_session_get_context (session); + uint8_t params_buffer[1024]; + struct spa_pod_builder pod_builder; + int width; + int height; + enum spa_data_type allowed_buffer_types; + const struct spa_pod *params[3]; + + if (grd_session_vnc_is_client_gone (stream->session)) + return; + + if (!format || id != SPA_PARAM_Format) + return; + + spa_format_video_raw_parse (format, &stream->spa_format); + + pod_builder = SPA_POD_BUILDER_INIT (params_buffer, sizeof (params_buffer)); + + width = stream->spa_format.size.width; + height = stream->spa_format.size.height; + + g_debug ("[VNC] Stream parameters changed. New monitor size: [%u, %u]", + width, height); + + grd_session_vnc_queue_resize_framebuffer (stream->session, width, height); + + allowed_buffer_types = 1 << SPA_DATA_MemFd; + if (grd_context_get_egl_thread (context)) + allowed_buffer_types |= 1 << SPA_DATA_DmaBuf; + + params[0] = spa_pod_builder_add_object ( + &pod_builder, + SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers, + SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int (8, 1, 8), + SPA_PARAM_BUFFERS_dataType, SPA_POD_Int (allowed_buffer_types), + 0); + + params[1] = spa_pod_builder_add_object ( + &pod_builder, + SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta, + SPA_PARAM_META_type, SPA_POD_Id (SPA_META_Header), + SPA_PARAM_META_size, SPA_POD_Int (sizeof (struct spa_meta_header)), + 0); + + params[2] = spa_pod_builder_add_object( + &pod_builder, + SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta, + SPA_PARAM_META_type, SPA_POD_Id (SPA_META_Cursor), + SPA_PARAM_META_size, SPA_POD_CHOICE_RANGE_Int (CURSOR_META_SIZE (384, 384), + CURSOR_META_SIZE (1,1), + CURSOR_META_SIZE (384, 384)), + 0); + + pw_stream_update_params (stream->pipewire_stream, + params, G_N_ELEMENTS (params)); +} + +static void +on_stream_add_buffer (void *user_data, + struct pw_buffer *buffer) +{ + GrdVncPipeWireStream *stream = user_data; + + g_hash_table_insert (stream->pipewire_buffers, buffer, buffer_context_new ()); +} + +static void +on_stream_remove_buffer (void *user_data, + struct pw_buffer *buffer) +{ + GrdVncPipeWireStream *stream = user_data; + BufferContext *buffer_context = NULL; + g_autoptr (GMutexLocker) locker = NULL; + + if (!g_hash_table_lookup_extended (stream->pipewire_buffers, buffer, + NULL, (gpointer *) &buffer_context)) + g_assert_not_reached (); + + locker = g_mutex_locker_new (&stream->dequeue_mutex); + + /* Ensure buffer is not locked any more */ + g_mutex_lock (&buffer_context->buffer_mutex); + g_mutex_unlock (&buffer_context->buffer_mutex); + + g_hash_table_remove (stream->pipewire_buffers, buffer); +} + +static GrdVncFrame * +grd_vnc_frame_new (GrdVncPipeWireStream *stream, + GrdVncFrameReadyCallback callback, + gpointer callback_user_data) +{ + GrdVncFrame *frame; + + frame = g_new0 (GrdVncFrame, 1); + + g_atomic_ref_count_init (&frame->refcount); + frame->stream = stream; + frame->callback = callback; + frame->callback_user_data = callback_user_data; + + return frame; +} + +static GrdVncFrame * +grd_vnc_frame_ref (GrdVncFrame *frame) +{ + g_atomic_ref_count_inc (&frame->refcount); + return frame; +} + +static void +grd_vnc_frame_unref (GrdVncFrame *frame) +{ + if (g_atomic_ref_count_dec (&frame->refcount)) + { + g_free (frame->data); + g_free (frame); + } +} + +static gboolean +render_frame (gpointer user_data) +{ + GrdVncPipeWireStream *stream = GRD_VNC_PIPEWIRE_STREAM (user_data); + GrdVncFrame *frame; + + g_mutex_lock (&stream->frame_mutex); + frame = g_steal_pointer (&stream->pending_frame); + g_mutex_unlock (&stream->frame_mutex); + + if (!frame) + return G_SOURCE_CONTINUE; + + if (grd_session_vnc_is_client_gone (stream->session)) + { + grd_vnc_frame_unref (frame); + return G_SOURCE_CONTINUE; + } + + if (frame->data) + { + grd_session_vnc_take_buffer (stream->session, + g_steal_pointer (&frame->data)); + } + else + { + grd_session_vnc_flush (stream->session); + } + + grd_vnc_frame_unref (frame); + + return G_SOURCE_CONTINUE; +} + +static gboolean +render_mouse_pointer (gpointer user_data) +{ + GrdVncPipeWireStream *stream = user_data; + g_autoptr (GMutexLocker) locker = NULL; + VncPointer *vnc_pointer; + + locker = g_mutex_locker_new (&stream->pointer_mutex); + if (!stream->pending_pointer) + return G_SOURCE_CONTINUE; + + vnc_pointer = g_steal_pointer (&stream->pending_pointer); + g_clear_pointer (&locker, g_mutex_locker_free); + + if (vnc_pointer->rfb_cursor) + { + grd_session_vnc_set_cursor (stream->session, + g_steal_pointer (&vnc_pointer->rfb_cursor)); + } + if (vnc_pointer->cursor_moved) + { + grd_session_vnc_move_cursor (stream->session, + vnc_pointer->cursor_x, + vnc_pointer->cursor_y); + } + + grd_session_vnc_flush (stream->session); + + vnc_pointer_free (vnc_pointer); + + return G_SOURCE_CONTINUE; +} + +static void +process_mouse_pointer_bitmap (GrdVncPipeWireStream *stream, + struct spa_buffer *buffer, + VncPointer **vnc_pointer) +{ + struct spa_meta_cursor *spa_meta_cursor; + struct spa_meta_bitmap *spa_meta_bitmap; + GrdPixelFormat format; + + spa_meta_cursor = spa_buffer_find_meta_data (buffer, SPA_META_Cursor, + sizeof *spa_meta_cursor); + + g_assert (spa_meta_cursor); + g_assert (spa_meta_cursor_is_valid (spa_meta_cursor)); + g_assert (spa_meta_cursor->bitmap_offset); + + spa_meta_bitmap = SPA_MEMBER (spa_meta_cursor, + spa_meta_cursor->bitmap_offset, + struct spa_meta_bitmap); + + if (spa_meta_bitmap && + spa_meta_bitmap->size.width > 0 && + spa_meta_bitmap->size.height > 0 && + grd_spa_pixel_format_to_grd_pixel_format (spa_meta_bitmap->format, + &format)) + { + uint8_t *buf; + rfbCursorPtr rfb_cursor; + + buf = SPA_MEMBER (spa_meta_bitmap, spa_meta_bitmap->offset, uint8_t); + rfb_cursor = grd_vnc_create_cursor (spa_meta_bitmap->size.width, + spa_meta_bitmap->size.height, + spa_meta_bitmap->stride, + format, + buf); + rfb_cursor->xhot = spa_meta_cursor->hotspot.x; + rfb_cursor->yhot = spa_meta_cursor->hotspot.y; + + if (!(*vnc_pointer)) + *vnc_pointer = g_new0 (VncPointer, 1); + (*vnc_pointer)->rfb_cursor = rfb_cursor; + } + else if (spa_meta_bitmap) + { + if (!(*vnc_pointer)) + *vnc_pointer = g_new0 (VncPointer, 1); + (*vnc_pointer)->rfb_cursor = grd_vnc_create_empty_cursor (1, 1); + } +} + +static void +on_frame_ready (GrdVncPipeWireStream *stream, + GrdVncFrame *frame, + gboolean success, + gpointer user_data) +{ + struct pw_buffer *buffer = user_data; + + g_assert (frame); + g_assert (buffer); + + if (!success) + goto out; + + g_mutex_lock (&stream->frame_mutex); + g_clear_pointer (&stream->pending_frame, grd_vnc_frame_unref); + + stream->pending_frame = g_steal_pointer (&frame); + g_mutex_unlock (&stream->frame_mutex); + + g_source_set_ready_time (stream->pending_frame_source, 0); +out: + pw_stream_queue_buffer (stream->pipewire_stream, buffer); + maybe_release_pipewire_buffer_lock (stream, buffer); + + g_clear_pointer (&frame, grd_vnc_frame_unref); +} + +static void +copy_frame_data (GrdVncFrame *frame, + uint8_t *src_data, + int width, + int height, + int dst_stride, + int src_stride, + int bpp) +{ + int y; + + frame->data = g_malloc (height * dst_stride); + for (y = 0; y < height; y++) + { + memcpy (((uint8_t *) frame->data) + y * dst_stride, + ((uint8_t *) src_data) + y * src_stride, + width * bpp); + } +} + +static void +on_dma_buf_downloaded (gboolean success, + gpointer user_data) +{ + GrdVncFrame *frame = user_data; + + frame->callback (frame->stream, + frame, + success, + frame->callback_user_data); +} + +static void +process_frame_data (GrdVncPipeWireStream *stream, + struct pw_buffer *pw_buffer) +{ + struct spa_buffer *buffer = pw_buffer->buffer; + g_autoptr (GrdVncFrame) frame = NULL; + GrdVncFrameReadyCallback callback; + gpointer user_data; + int dst_stride; + uint32_t drm_format; + int bpp; + int width; + int height; + + g_assert (buffer->datas[0].chunk->size > 0); + + height = stream->spa_format.size.height; + width = stream->spa_format.size.width; + dst_stride = grd_session_vnc_get_stride_for_width (stream->session, + width); + grd_get_spa_format_details (stream->spa_format.format, + &drm_format, &bpp); + + frame = grd_vnc_frame_new (stream, on_frame_ready, pw_buffer); + callback = frame->callback; + user_data = frame->callback_user_data; + + if (buffer->datas[0].type == SPA_DATA_MemFd) + { + size_t size; + uint8_t *map; + int src_stride; + uint8_t *src_data; + + size = buffer->datas[0].maxsize + buffer->datas[0].mapoffset; + map = mmap (NULL, size, PROT_READ, MAP_PRIVATE, buffer->datas[0].fd, 0); + if (map == MAP_FAILED) + { + g_warning ("Failed to mmap buffer: %s", g_strerror (errno)); + callback (stream, g_steal_pointer (&frame), FALSE, user_data); + return; + } + + src_data = SPA_MEMBER (map, buffer->datas[0].mapoffset, uint8_t); + src_stride = buffer->datas[0].chunk->stride; + copy_frame_data (frame, src_data, + width, height, + dst_stride, src_stride, + bpp); + + munmap (map, size); + + callback (stream, g_steal_pointer (&frame), TRUE, user_data); + } + else if (buffer->datas[0].type == SPA_DATA_DmaBuf) + { + GrdSession *session = GRD_SESSION (stream->session); + GrdContext *context = grd_session_get_context (session); + GrdEglThread *egl_thread = grd_context_get_egl_thread (context); + int row_width; + int *fds; + uint32_t *offsets; + uint32_t *strides; + uint64_t *modifiers = NULL; + uint32_t n_planes; + unsigned int i; + uint8_t *dst_data; + + row_width = dst_stride / bpp; + + n_planes = buffer->n_datas; + fds = g_alloca (sizeof (int) * n_planes); + offsets = g_alloca (sizeof (uint32_t) * n_planes); + strides = g_alloca (sizeof (uint32_t) * n_planes); + if (stream->spa_format.modifier != DRM_FORMAT_MOD_INVALID) + modifiers = g_alloca (sizeof (uint64_t) * n_planes); + + for (i = 0; i < n_planes; i++) + { + fds[i] = buffer->datas[i].fd; + offsets[i] = buffer->datas[i].chunk->offset; + strides[i] = buffer->datas[i].chunk->stride; + if (modifiers) + modifiers[i] = stream->spa_format.modifier; + } + dst_data = g_malloc0 (height * dst_stride); + frame->data = dst_data; + + acquire_pipewire_buffer_lock (stream, pw_buffer); + grd_egl_thread_download (egl_thread, + stream->egl_slot, + dst_data, + row_width, + drm_format, + width, height, + n_planes, + fds, + strides, + offsets, + modifiers, + on_dma_buf_downloaded, + grd_vnc_frame_ref (g_steal_pointer (&frame)), + (GDestroyNotify) grd_vnc_frame_unref); + } + else + { + g_assert_not_reached (); + } +} + +static gboolean +render_source_dispatch (GSource *source, + GSourceFunc callback, + gpointer user_data) +{ + g_source_set_ready_time (source, -1); + + return callback (user_data); +} + +static GSourceFuncs render_source_funcs = +{ + .dispatch = render_source_dispatch, +}; + +static void +maybe_consume_pointer_position (struct pw_buffer *buffer, + gboolean *cursor_moved, + int *cursor_x, + int *cursor_y) +{ + struct spa_meta_cursor *spa_meta_cursor; + + spa_meta_cursor = spa_buffer_find_meta_data (buffer->buffer, SPA_META_Cursor, + sizeof *spa_meta_cursor); + if (spa_meta_cursor && spa_meta_cursor_is_valid (spa_meta_cursor)) + { + *cursor_x = spa_meta_cursor->position.x; + *cursor_y = spa_meta_cursor->position.y; + *cursor_moved = TRUE; + } +} + +static void +on_stream_process (void *user_data) +{ + GrdVncPipeWireStream *stream = GRD_VNC_PIPEWIRE_STREAM (user_data); + g_autoptr (GMutexLocker) locker = NULL; + g_autoptr (GrdVncFrame) frame = NULL; + struct pw_buffer *last_pointer_buffer = NULL; + struct pw_buffer *last_frame_buffer = NULL; + struct pw_buffer *next_buffer; + VncPointer *vnc_pointer = NULL; + gboolean cursor_moved = FALSE; + int cursor_x = 0; + int cursor_y = 0; + + locker = g_mutex_locker_new (&stream->dequeue_mutex); + if (stream->dequeuing_disallowed) + return; + + while ((next_buffer = pw_stream_dequeue_buffer (stream->pipewire_stream))) + { + struct spa_meta_header *spa_meta_header; + + spa_meta_header = spa_buffer_find_meta_data (next_buffer->buffer, + SPA_META_Header, + sizeof (struct spa_meta_header)); + if (spa_meta_header && + spa_meta_header->flags & SPA_META_HEADER_FLAG_CORRUPTED) + { + pw_stream_queue_buffer (stream->pipewire_stream, next_buffer); + continue; + } + + maybe_consume_pointer_position (next_buffer, &cursor_moved, + &cursor_x, &cursor_y); + + if (grd_pipewire_buffer_has_pointer_bitmap (next_buffer)) + { + if (last_pointer_buffer == last_frame_buffer) + last_pointer_buffer = NULL; + + if (last_pointer_buffer) + pw_stream_queue_buffer (stream->pipewire_stream, last_pointer_buffer); + last_pointer_buffer = next_buffer; + } + if (grd_pipewire_buffer_has_frame_data (next_buffer)) + { + if (last_pointer_buffer == last_frame_buffer) + last_frame_buffer = NULL; + + if (last_frame_buffer) + pw_stream_queue_buffer (stream->pipewire_stream, last_frame_buffer); + last_frame_buffer = next_buffer; + } + + if (next_buffer != last_pointer_buffer && + next_buffer != last_frame_buffer) + pw_stream_queue_buffer (stream->pipewire_stream, next_buffer); + } + if (!last_pointer_buffer && !last_frame_buffer && !cursor_moved) + return; + + if (cursor_moved) + { + vnc_pointer = g_new0 (VncPointer, 1); + vnc_pointer->cursor_moved = cursor_moved; + vnc_pointer->cursor_x = cursor_x; + vnc_pointer->cursor_y = cursor_y; + } + + if (last_pointer_buffer) + { + process_mouse_pointer_bitmap (stream, last_pointer_buffer->buffer, + &vnc_pointer); + if (last_pointer_buffer != last_frame_buffer) + pw_stream_queue_buffer (stream->pipewire_stream, last_pointer_buffer); + } + if (vnc_pointer) + { + g_mutex_lock (&stream->pointer_mutex); + g_clear_pointer (&stream->pending_pointer, vnc_pointer_free); + + stream->pending_pointer = vnc_pointer; + g_mutex_unlock (&stream->pointer_mutex); + + g_source_set_ready_time (stream->pending_pointer_source, 0); + } + if (!last_frame_buffer) + return; + + process_frame_data (stream, last_frame_buffer); +} + +static const struct pw_stream_events stream_events = { + PW_VERSION_STREAM_EVENTS, + .state_changed = on_stream_state_changed, + .param_changed = on_stream_param_changed, + .add_buffer = on_stream_add_buffer, + .remove_buffer = on_stream_remove_buffer, + .process = on_stream_process, +}; + +static gboolean +connect_to_stream (GrdVncPipeWireStream *stream, + const GrdVncVirtualMonitor *virtual_monitor, + GError **error) +{ + struct pw_stream *pipewire_stream; + uint8_t params_buffer[1024]; + struct spa_pod_builder pod_builder; + const struct spa_pod *params[MAX_FORMAT_PARAMS] = {}; + uint32_t n_params = 0; + int ret; + + pipewire_stream = pw_stream_new (stream->pipewire_core, + "grd-vnc-pipewire-stream", + NULL); + + pod_builder = SPA_POD_BUILDER_INIT (params_buffer, sizeof (params_buffer)); + + n_params += add_format_params (stream, virtual_monitor, &pod_builder, + params, MAX_FORMAT_PARAMS); + + stream->pipewire_stream = pipewire_stream; + + pw_stream_add_listener (pipewire_stream, + &stream->pipewire_stream_listener, + &stream_events, + stream); + + g_assert (n_params > 0); + ret = pw_stream_connect (stream->pipewire_stream, + PW_DIRECTION_INPUT, + stream->src_node_id, + (PW_STREAM_FLAG_RT_PROCESS | + PW_STREAM_FLAG_AUTOCONNECT), + params, n_params); + if (ret < 0) + { + g_set_error_literal (error, G_IO_ERROR, g_io_error_from_errno (-ret), + strerror (-ret)); + return FALSE; + } + + return TRUE; +} + +static void +on_core_error (void *user_data, + uint32_t id, + int seq, + int res, + const char *message) +{ + GrdVncPipeWireStream *stream = GRD_VNC_PIPEWIRE_STREAM (user_data); + + g_warning ("Pipewire core error: id:%u %s", id, message); + + if (id == PW_ID_CORE && res == -EPIPE) + g_signal_emit (stream, signals[CLOSED], 0); +} + +static const struct pw_core_events core_events = { + PW_VERSION_CORE_EVENTS, + .error = on_core_error, +}; + +GrdVncPipeWireStream * +grd_vnc_pipewire_stream_new (GrdSessionVnc *session_vnc, + uint32_t src_node_id, + const GrdVncVirtualMonitor *virtual_monitor, + GError **error) +{ + GrdSession *session = GRD_SESSION (session_vnc); + GrdContext *context = grd_session_get_context (session); + GrdEglThread *egl_thread = grd_context_get_egl_thread (context); + g_autoptr (GrdVncPipeWireStream) stream = NULL; + GrdPipeWireSource *pipewire_source; + GSource *source; + + stream = g_object_new (GRD_TYPE_VNC_PIPEWIRE_STREAM, NULL); + stream->session = session_vnc; + stream->src_node_id = src_node_id; + + if (egl_thread) + stream->egl_slot = grd_egl_thread_acquire_slot (egl_thread); + + pw_init (NULL, NULL); + + pipewire_source = grd_attached_pipewire_source_new ("VNC", error); + if (!pipewire_source) + return NULL; + + stream->pipewire_source = (GSource *) pipewire_source; + + stream->pipewire_context = pw_context_new (pipewire_source->pipewire_loop, + NULL, 0); + if (!stream->pipewire_context) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create PipeWire context"); + return NULL; + } + + stream->pipewire_core = pw_context_connect (stream->pipewire_context, NULL, 0); + if (!stream->pipewire_core) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to connect PipeWire context"); + return NULL; + } + + source = g_source_new (&render_source_funcs, sizeof (GSource)); + stream->pending_frame_source = source; + g_source_set_callback (source, render_frame, stream, NULL); + g_source_attach (source, NULL); + g_source_unref (source); + + source = g_source_new (&render_source_funcs, sizeof (GSource)); + stream->pending_pointer_source = source; + g_source_set_callback (source, render_mouse_pointer, stream, NULL); + g_source_attach (source, NULL); + g_source_unref (source); + + pw_core_add_listener (stream->pipewire_core, + &stream->pipewire_core_listener, + &core_events, + stream); + + if (!connect_to_stream (stream, virtual_monitor, error)) + return NULL; + + return g_steal_pointer (&stream); +} + +static void +on_sync_complete (gboolean success, + gpointer user_data) +{ + GrdSyncPoint *sync_point = user_data; + + grd_sync_point_complete (sync_point, success); +} + +static void +grd_vnc_pipewire_stream_finalize (GObject *object) +{ + GrdVncPipeWireStream *stream = GRD_VNC_PIPEWIRE_STREAM (object); + GrdSession *session = GRD_SESSION (stream->session); + GrdContext *context = grd_session_get_context (session); + GrdEglThread *egl_thread; + + g_mutex_lock (&stream->dequeue_mutex); + stream->dequeuing_disallowed = TRUE; + g_mutex_unlock (&stream->dequeue_mutex); + + egl_thread = grd_context_get_egl_thread (context); + if (egl_thread) + { + GrdSyncPoint sync_point = {}; + + grd_sync_point_init (&sync_point); + grd_egl_thread_sync (egl_thread, on_sync_complete, &sync_point, NULL); + + grd_sync_point_wait_for_completion (&sync_point); + grd_sync_point_clear (&sync_point); + } + + g_clear_pointer (&stream->pipewire_stream, pw_stream_destroy); + + g_clear_pointer (&stream->pipewire_core, pw_core_disconnect); + g_clear_pointer (&stream->pipewire_context, pw_context_destroy); + g_clear_pointer (&stream->pending_pointer_source, g_source_destroy); + g_clear_pointer (&stream->pending_frame_source, g_source_destroy); + if (stream->pipewire_source) + { + g_source_destroy (stream->pipewire_source); + g_clear_pointer (&stream->pipewire_source, g_source_unref); + } + + g_clear_pointer (&stream->pending_pointer, vnc_pointer_free); + g_clear_pointer (&stream->pending_frame, grd_vnc_frame_unref); + + g_mutex_clear (&stream->pointer_mutex); + g_mutex_clear (&stream->frame_mutex); + g_mutex_clear (&stream->dequeue_mutex); + + g_clear_pointer (&stream->pipewire_buffers, g_hash_table_unref); + + pw_deinit (); + + if (egl_thread) + grd_egl_thread_release_slot (egl_thread, stream->egl_slot); + + G_OBJECT_CLASS (grd_vnc_pipewire_stream_parent_class)->finalize (object); +} + +static void +grd_vnc_pipewire_stream_init (GrdVncPipeWireStream *stream) +{ + stream->pipewire_buffers = + g_hash_table_new_full (NULL, NULL, + NULL, (GDestroyNotify) buffer_context_free); + + g_mutex_init (&stream->dequeue_mutex); + g_mutex_init (&stream->frame_mutex); + g_mutex_init (&stream->pointer_mutex); +} + +static void +grd_vnc_pipewire_stream_class_init (GrdVncPipeWireStreamClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = grd_vnc_pipewire_stream_finalize; + + signals[CLOSED] = g_signal_new ("closed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, 0); +} diff --git a/grd-vnc-pipewire-stream.h b/grd-vnc-pipewire-stream.h new file mode 100644 index 0000000..5e2662e --- /dev/null +++ b/grd-vnc-pipewire-stream.h @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2018 Red Hat Inc. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + */ + +#pragma once + +#include +#include + +#include "grd-session-vnc.h" +#include "grd-vnc-monitor-config.h" + +#define GRD_TYPE_VNC_PIPEWIRE_STREAM grd_vnc_pipewire_stream_get_type () +G_DECLARE_FINAL_TYPE (GrdVncPipeWireStream, grd_vnc_pipewire_stream, + GRD, VNC_PIPEWIRE_STREAM, + GObject) + +GrdVncPipeWireStream *grd_vnc_pipewire_stream_new (GrdSessionVnc *session_vnc, + uint32_t src_node_id, + const GrdVncVirtualMonitor *virtual_monitor, + GError **error); + +void grd_vnc_pipewire_stream_resize (GrdVncPipeWireStream *stream, + GrdVncVirtualMonitor *virtual_monitor); diff --git a/grd-vnc-server.c b/grd-vnc-server.c new file mode 100644 index 0000000..49f1605 --- /dev/null +++ b/grd-vnc-server.c @@ -0,0 +1,301 @@ +/* + * Copyright (C) 2015 Red Hat Inc. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + * Written by: + * Jonas Ådahl + */ + +#include "config.h" + +#include "grd-vnc-server.h" + +#include +#include + +#include "grd-context.h" +#include "grd-debug.h" +#include "grd-session-vnc.h" +#include "grd-throttler.h" +#include "grd-utils.h" + +enum +{ + PROP_0, + + PROP_CONTEXT, +}; + +struct _GrdVncServer +{ + GSocketService parent; + + GrdThrottler *throttler; + + GList *sessions; + + GList *stopped_sessions; + guint cleanup_sessions_idle_id; + + GrdContext *context; +}; + +G_DEFINE_TYPE (GrdVncServer, grd_vnc_server, G_TYPE_SOCKET_SERVICE) + +static void +allow_connection_cb (GrdThrottler *throttler, + GSocketConnection *connection, + gpointer user_data); + +GrdContext * +grd_vnc_server_get_context (GrdVncServer *vnc_server) +{ + return vnc_server->context; +} + +GrdVncServer * +grd_vnc_server_new (GrdContext *context) +{ + GrdVncServer *vnc_server; + + vnc_server = g_object_new (GRD_TYPE_VNC_SERVER, + "context", context, + NULL); + + return vnc_server; +} + +static void +grd_vnc_server_cleanup_stopped_sessions (GrdVncServer *vnc_server) +{ + g_list_free_full (vnc_server->stopped_sessions, g_object_unref); + vnc_server->stopped_sessions = NULL; +} + +static gboolean +cleanup_stopped_sessions_idle (GrdVncServer *vnc_server) +{ + grd_vnc_server_cleanup_stopped_sessions (vnc_server); + vnc_server->cleanup_sessions_idle_id = 0; + + return G_SOURCE_REMOVE; +} + +static void +on_session_stopped (GrdSession *session, GrdVncServer *vnc_server) +{ + g_debug ("VNC session stopped"); + + vnc_server->stopped_sessions = g_list_append (vnc_server->stopped_sessions, + session); + vnc_server->sessions = g_list_remove (vnc_server->sessions, session); + if (!vnc_server->cleanup_sessions_idle_id) + { + vnc_server->cleanup_sessions_idle_id = + g_idle_add ((GSourceFunc) cleanup_stopped_sessions_idle, + vnc_server); + } +} + +static void +allow_connection_cb (GrdThrottler *throttler, + GSocketConnection *connection, + gpointer user_data) +{ + GrdVncServer *vnc_server = GRD_VNC_SERVER (user_data); + GrdSessionVnc *session_vnc; + + g_debug ("Creating new VNC session"); + + session_vnc = grd_session_vnc_new (vnc_server, connection); + vnc_server->sessions = g_list_append (vnc_server->sessions, session_vnc); + + g_signal_connect (session_vnc, "stopped", + G_CALLBACK (on_session_stopped), + vnc_server); +} + +static gboolean +on_incoming (GSocketService *service, + GSocketConnection *connection) +{ + GrdVncServer *vnc_server = GRD_VNC_SERVER (service); + + grd_throttler_handle_connection (vnc_server->throttler, + connection); + return TRUE; +} + +gboolean +grd_vnc_server_start (GrdVncServer *vnc_server, + GError **error) +{ + GrdSettings *settings = grd_context_get_settings (vnc_server->context); + int vnc_port; + uint16_t selected_vnc_port = 0; + gboolean negotiate_port; + GrdDBusRemoteDesktopVncServer *vnc_server_iface; + + g_object_get (G_OBJECT (settings), + "vnc-port", &vnc_port, + "vnc-negotiate-port", &negotiate_port, + NULL); + + g_debug ("[VNC] Trying to bind to TCP socket:"); + if (!grd_bind_socket (G_SOCKET_LISTENER (vnc_server), + vnc_port, + &selected_vnc_port, + negotiate_port, + error)) + return FALSE; + + g_assert (selected_vnc_port != 0); + + g_signal_connect (vnc_server, "incoming", G_CALLBACK (on_incoming), NULL); + + vnc_server_iface = grd_context_get_vnc_server_interface (vnc_server->context); + grd_dbus_remote_desktop_vnc_server_set_enabled (vnc_server_iface, 1); + grd_dbus_remote_desktop_vnc_server_set_port (vnc_server_iface, + selected_vnc_port); + + return TRUE; +} + +void +grd_vnc_server_stop (GrdVncServer *vnc_server) +{ + GrdDBusRemoteDesktopVncServer *vnc_server_iface; + + g_socket_service_stop (G_SOCKET_SERVICE (vnc_server)); + g_socket_listener_close (G_SOCKET_LISTENER (vnc_server)); + + vnc_server_iface = grd_context_get_vnc_server_interface (vnc_server->context); + grd_dbus_remote_desktop_vnc_server_set_enabled (vnc_server_iface, FALSE); + grd_dbus_remote_desktop_vnc_server_set_port (vnc_server_iface, -1); + + while (vnc_server->sessions) + { + GrdSession *session = vnc_server->sessions->data; + + grd_session_stop (session); + } + + grd_vnc_server_cleanup_stopped_sessions (vnc_server); + g_clear_handle_id (&vnc_server->cleanup_sessions_idle_id, + g_source_remove); + + g_clear_object (&vnc_server->throttler); +} + +static void +grd_vnc_server_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GrdVncServer *vnc_server = GRD_VNC_SERVER (object); + + switch (prop_id) + { + case PROP_CONTEXT: + vnc_server->context = g_value_get_object (value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +grd_vnc_server_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GrdVncServer *vnc_server = GRD_VNC_SERVER (object); + + switch (prop_id) + { + case PROP_CONTEXT: + g_value_set_object (value, vnc_server->context); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +grd_vnc_server_dispose (GObject *object) +{ + GrdVncServer *vnc_server = GRD_VNC_SERVER (object); + + g_assert (!vnc_server->sessions); + g_assert (!vnc_server->stopped_sessions); + g_assert (!vnc_server->cleanup_sessions_idle_id); + g_assert (!vnc_server->throttler); + + G_OBJECT_CLASS (grd_vnc_server_parent_class)->dispose (object); +} + +static void +grd_vnc_server_constructed (GObject *object) +{ + GrdVncServer *vnc_server = GRD_VNC_SERVER (object); + GrdThrottlerLimits *limits; + + if (grd_get_debug_flags () & GRD_DEBUG_VNC) + rfbLogEnable (1); + else + rfbLogEnable (0); + + limits = grd_throttler_limits_new (vnc_server->context); + /* TODO: Add the rfbScreen instance to GrdVncServer to support multiple + * sessions. */ + grd_throttler_limits_set_max_global_connections (limits, 1); + vnc_server->throttler = grd_throttler_new (limits, + allow_connection_cb, + vnc_server); + + G_OBJECT_CLASS (grd_vnc_server_parent_class)->constructed (object); +} + +static void +grd_vnc_server_init (GrdVncServer *vnc_server) +{ +} + +static void +grd_vnc_server_class_init (GrdVncServerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->set_property = grd_vnc_server_set_property; + object_class->get_property = grd_vnc_server_get_property; + object_class->dispose = grd_vnc_server_dispose; + object_class->constructed = grd_vnc_server_constructed; + + g_object_class_install_property (object_class, + PROP_CONTEXT, + g_param_spec_object ("context", + "GrdContext", + "The GrdContext instance", + GRD_TYPE_CONTEXT, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); +} diff --git a/grd-vnc-server.h b/grd-vnc-server.h new file mode 100644 index 0000000..19dbd05 --- /dev/null +++ b/grd-vnc-server.h @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2015 Red Hat Inc. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + * Written by: + * Jonas Ådahl + */ + +#pragma once + +#include +#include + +#include "grd-types.h" + +#define GRD_TYPE_VNC_SERVER (grd_vnc_server_get_type ()) +G_DECLARE_FINAL_TYPE (GrdVncServer, + grd_vnc_server, + GRD, VNC_SERVER, + GSocketService) + +GrdContext *grd_vnc_server_get_context (GrdVncServer *vnc_server); + +gboolean grd_vnc_server_start (GrdVncServer *vnc_server, GError **error); + +void grd_vnc_server_stop (GrdVncServer *vnc_server); + +GrdVncServer *grd_vnc_server_new (GrdContext *context); diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..185041e --- /dev/null +++ b/meson.build @@ -0,0 +1,452 @@ +src_includepath = include_directories('.') + +credentials_deps = [ + libsecret_dep, + tss2_esys_dep, + tss2_mu_dep, + tss2_rc_dep, + tss2_tctildr_dep, +] + +deps = [ + cairo_dep, + glib_dep, + gio_dep, + gio_unix_dep, + pipewire_dep, + libei_dep, + libnotify_dep, + krb5_dep, + epoxy_dep, + drm_dep, + xkbcommon_dep, + + credentials_deps, +] + +credentials_sources = files([ + 'grd-credentials.c', + 'grd-credentials.h', + 'grd-credentials-file.c', + 'grd-credentials-file.h', + 'grd-credentials-libsecret.c', + 'grd-credentials-libsecret.h', + 'grd-credentials-one-time.c', + 'grd-credentials-one-time.h', + 'grd-credentials-tpm.c', + 'grd-credentials-tpm.h', + 'grd-tpm.c', + 'grd-tpm.h', +]) + +daemon_sources = files([ + 'grd-clipboard.c', + 'grd-clipboard.h', + 'grd-context.c', + 'grd-context.h', + 'grd-daemon.c', + 'grd-daemon.h', + 'grd-daemon-user.c', + 'grd-daemon-user.h', + 'grd-damage-utils.c', + 'grd-damage-utils.h', + 'grd-debug.c', + 'grd-debug.h', + 'grd-egl-thread.c', + 'grd-egl-thread.h', + 'grd-mime-type.c', + 'grd-mime-type.h', + 'grd-pipewire-utils.c', + 'grd-pipewire-utils.h', + 'grd-private.h', + 'grd-prompt.c', + 'grd-prompt.h', + 'grd-session.c', + 'grd-session.h', + 'grd-settings.c', + 'grd-settings.h', + 'grd-settings-handover.c', + 'grd-settings-handover.h', + 'grd-settings-headless.c', + 'grd-settings-headless.h', + 'grd-settings-system.c', + 'grd-settings-system.h', + 'grd-settings-user.c', + 'grd-settings-user.h', + 'grd-stream.c', + 'grd-stream.h', + 'grd-throttler.c', + 'grd-throttler.h', + 'grd-types.h', + 'grd-utils.c', + 'grd-utils.h', +]) + +if have_rdp + daemon_sources += files([ + 'grd-avc-frame-info.c', + 'grd-avc-frame-info.h', + 'grd-bitstream.c', + 'grd-bitstream.h', + 'grd-clipboard-rdp.c', + 'grd-clipboard-rdp.h', + 'grd-damage-detector-sw.c', + 'grd-damage-detector-sw.h', + 'grd-decode-session.c', + 'grd-decode-session.h', + 'grd-decode-session-sw-avc.c', + 'grd-decode-session-sw-avc.h', + 'grd-encode-context.c', + 'grd-encode-context.h', + 'grd-encode-session.c', + 'grd-encode-session.h', + 'grd-encode-session-ca-sw.c', + 'grd-encode-session-ca-sw.h', + 'grd-encode-session-vaapi.c', + 'grd-encode-session-vaapi.h', + 'grd-frame-clock.c', + 'grd-frame-clock.h', + 'grd-hwaccel-nvidia.c', + 'grd-hwaccel-nvidia.h', + 'grd-hwaccel-vaapi.c', + 'grd-hwaccel-vaapi.h', + 'grd-hwaccel-vulkan.c', + 'grd-hwaccel-vulkan.h', + 'grd-image-view.c', + 'grd-image-view.h', + 'grd-image-view-nv12.c', + 'grd-image-view-nv12.h', + 'grd-image-view-rgb.c', + 'grd-image-view-rgb.h', + 'grd-local-buffer.c', + 'grd-local-buffer.h', + 'grd-local-buffer-copy.c', + 'grd-local-buffer-copy.h', + 'grd-local-buffer-wrapper-rdp.c', + 'grd-local-buffer-wrapper-rdp.h', + 'grd-nal-writer.c', + 'grd-nal-writer.h', + 'grd-rdp-audio-output-stream.c', + 'grd-rdp-audio-output-stream.h', + 'grd-rdp-buffer.c', + 'grd-rdp-buffer.h', + 'grd-rdp-buffer-info.h', + 'grd-rdp-buffer-pool.c', + 'grd-rdp-buffer-pool.h', + 'grd-rdp-camera-stream.c', + 'grd-rdp-camera-stream.h', + 'grd-rdp-connect-time-autodetection.c', + 'grd-rdp-connect-time-autodetection.h', + 'grd-rdp-cursor-renderer.c', + 'grd-rdp-cursor-renderer.h', + 'grd-rdp-damage-detector.c', + 'grd-rdp-damage-detector.h', + 'grd-rdp-damage-detector-cuda.c', + 'grd-rdp-damage-detector-cuda.h', + 'grd-rdp-damage-detector-memcmp.c', + 'grd-rdp-damage-detector-memcmp.h', + 'grd-rdp-dsp.c', + 'grd-rdp-dsp.h', + 'grd-rdp-dvc.c', + 'grd-rdp-dvc.h', + 'grd-rdp-dvc-audio-input.c', + 'grd-rdp-dvc-audio-input.h', + 'grd-rdp-dvc-audio-playback.c', + 'grd-rdp-dvc-audio-playback.h', + 'grd-rdp-dvc-camera-device.c', + 'grd-rdp-dvc-camera-device.h', + 'grd-rdp-dvc-camera-enumerator.c', + 'grd-rdp-dvc-camera-enumerator.h', + 'grd-rdp-dvc-display-control.c', + 'grd-rdp-dvc-display-control.h', + 'grd-rdp-dvc-graphics-pipeline.c', + 'grd-rdp-dvc-graphics-pipeline.h', + 'grd-rdp-dvc-handler.c', + 'grd-rdp-dvc-handler.h', + 'grd-rdp-dvc-input.c', + 'grd-rdp-dvc-input.h', + 'grd-rdp-dvc-telemetry.c', + 'grd-rdp-dvc-telemetry.h', + 'grd-rdp-event-queue.c', + 'grd-rdp-event-queue.h', + 'grd-rdp-frame.c', + 'grd-rdp-frame.h', + 'grd-rdp-frame-info.h', + 'grd-rdp-frame-stats.c', + 'grd-rdp-frame-stats.h', + 'grd-rdp-fuse-clipboard.c', + 'grd-rdp-fuse-clipboard.h', + 'grd-rdp-gfx-frame-controller.c', + 'grd-rdp-gfx-frame-controller.h', + 'grd-rdp-gfx-frame-log.c', + 'grd-rdp-gfx-frame-log.h', + 'grd-rdp-gfx-framerate-log.c', + 'grd-rdp-gfx-framerate-log.h', + 'grd-rdp-gfx-surface.c', + 'grd-rdp-gfx-surface.h', + 'grd-rdp-layout-manager.c', + 'grd-rdp-layout-manager.h', + 'grd-rdp-legacy-buffer.c', + 'grd-rdp-legacy-buffer.h', + 'grd-rdp-monitor-config.c', + 'grd-rdp-monitor-config.h', + 'grd-rdp-network-autodetection.c', + 'grd-rdp-network-autodetection.h', + 'grd-rdp-pipewire-stream.c', + 'grd-rdp-pipewire-stream.h', + 'grd-rdp-private.h', + 'grd-rdp-pw-buffer.c', + 'grd-rdp-pw-buffer.h', + 'grd-rdp-render-context.c', + 'grd-rdp-render-context.h', + 'grd-rdp-render-state.c', + 'grd-rdp-render-state.h', + 'grd-rdp-renderer.c', + 'grd-rdp-renderer.h', + 'grd-rdp-routing-token.c', + 'grd-rdp-routing-token.h', + 'grd-rdp-sam.c', + 'grd-rdp-sam.h', + 'grd-rdp-server.c', + 'grd-rdp-server.h', + 'grd-rdp-session-metrics.c', + 'grd-rdp-session-metrics.h', + 'grd-rdp-stream-owner.c', + 'grd-rdp-stream-owner.h', + 'grd-rdp-surface.c', + 'grd-rdp-surface.h', + 'grd-rdp-surface-renderer.c', + 'grd-rdp-surface-renderer.h', + 'grd-rdp-sw-encoder-ca.c', + 'grd-rdp-sw-encoder-ca.h', + 'grd-rdp-view-creator.c', + 'grd-rdp-view-creator.h', + 'grd-rdp-view-creator-avc.c', + 'grd-rdp-view-creator-avc.h', + 'grd-rdp-view-creator-gen-gl.c', + 'grd-rdp-view-creator-gen-gl.h', + 'grd-rdp-view-creator-gen-sw.c', + 'grd-rdp-view-creator-gen-sw.h', + 'grd-sample-buffer.c', + 'grd-sample-buffer.h', + 'grd-session-rdp.c', + 'grd-session-rdp.h', + 'grd-shell-dialog.c', + 'grd-shell-dialog.h', + 'grd-vk-buffer.c', + 'grd-vk-buffer.h', + 'grd-vk-device.c', + 'grd-vk-device.h', + 'grd-vk-image.c', + 'grd-vk-image.h', + 'grd-vk-memory.c', + 'grd-vk-memory.h', + 'grd-vk-physical-device.c', + 'grd-vk-physical-device.h', + 'grd-vk-queue.c', + 'grd-vk-queue.h', + 'grd-vk-utils.c', + 'grd-vk-utils.h', + ]) + + if libsystemd_dep.found() + daemon_sources += files([ + 'grd-daemon-handover.c', + 'grd-daemon-handover.h', + 'grd-daemon-system.c', + 'grd-daemon-system.h', + 'grd-daemon-utils.c', + 'grd-daemon-utils.h', + ]) + + deps += [ + libsystemd_dep, + ] + endif + + deps += [ + cuda_dep, + dl_dep, + fdk_aac_dep, + freerdp_dep, + freerdp_server_dep, + fuse_dep, + libva_dep, + libva_drm_dep, + m_dep, + opus_dep, + vulkan_dep, + winpr_dep, + ] +endif + +if have_vnc + daemon_sources += files([ + 'grd-clipboard-vnc.c', + 'grd-clipboard-vnc.h', + 'grd-session-vnc.c', + 'grd-session-vnc.h', + 'grd-vnc-cursor.c', + 'grd-vnc-cursor.h', + 'grd-vnc-monitor-config.h', + 'grd-vnc-pipewire-stream.c', + 'grd-vnc-pipewire-stream.h', + 'grd-vnc-server.c', + 'grd-vnc-server.h', + ]) + + deps += [ + libvncserver_dep, + ] +endif + +daemon_sources += credentials_sources + +gen_daemon_sources = [] + +gen_daemon_sources += gnome.gdbus_codegen('grd-dbus-mutter-screen-cast', + 'org.gnome.Mutter.ScreenCast.xml', + interface_prefix: 'org.gnome.Mutter.', + namespace: 'GrdDBusMutter') +gen_daemon_sources += gnome.gdbus_codegen('grd-dbus-mutter-remote-desktop', + 'org.gnome.Mutter.RemoteDesktop.xml', + interface_prefix: 'org.gnome.Mutter.', + namespace: 'GrdDBusMutter') +dbus_gen_remote_desktop = gnome.gdbus_codegen('grd-dbus-remote-desktop', + 'org.gnome.RemoteDesktop.xml', + object_manager: true, + interface_prefix: 'org.gnome.RemoteDesktop.', + namespace: 'GrdDBusRemoteDesktop') +gen_daemon_sources += dbus_gen_remote_desktop + +if have_rdp + gen_daemon_sources += gnome.gdbus_codegen('grd-dbus-gdm', + 'org.gnome.DisplayManager.xml', + object_manager: true, + interface_prefix: 'org.gnome.DisplayManager.', + namespace: 'GrdDBusGdm') +endif + +gen_enum_types = gnome.mkenums('grd-enum-types', + h_template: 'grd-enum-types.h.in', + c_template: 'grd-enum-types.c.in', + sources: 'grd-enums.h', +) + +daemon_sources += gen_daemon_sources + +daemon_sources += gen_enum_types + +control_sources = ([ + 'grd-control.c' +]) + +ctl_shared_sources = ([ + 'grd-debug.c', + 'grd-debug.h', + 'grd-settings.c', + 'grd-settings.h', + 'grd-settings-headless.c', + 'grd-settings-headless.h', + 'grd-settings-system.c', + 'grd-settings-system.h', + 'grd-utils.c', + 'grd-utils.h', + credentials_sources, + gen_enum_types, +]) + +ctl_sources = ([ + 'grd-ctl.c', + 'grd-settings-user.c', + 'grd-settings-user.h', + ctl_shared_sources, +]) + +configuration_sources = ([ + 'grd-configuration.c', + ctl_shared_sources, + dbus_gen_remote_desktop, +]) + +ctl_deps = [ + glib_dep, + gio_dep, + gio_unix_dep, + credentials_deps, +] + +if have_rdp + ctl_deps += [ + freerdp_dep, + polkit_dep, + ] + + enable_service_sources = ([ + 'grd-enable-service.c', + ]) + + enable_service_deps = [ + glib_dep, + gio_dep, + polkit_dep, + ] +endif + +executable('gnome-remote-desktop-daemon', + daemon_sources, + dependencies: deps, + include_directories: [configinc], + install: true, + install_dir: libexecdir) + +executable('gnome-remote-desktop-control', + control_sources, + dependencies: [glib_dep, gio_dep], + include_directories: [configinc], + install : false) + +executable('grdctl', + ctl_sources, + dependencies: ctl_deps, + include_directories: [configinc], + install: true) + +if have_rdp + executable('gnome-remote-desktop-enable-service', + enable_service_sources, + dependencies: enable_service_deps, + include_directories: [configinc], + install: true, + install_dir: libexecdir) + + executable('gnome-remote-desktop-configuration-daemon', + configuration_sources, + dependencies: ctl_deps, + include_directories: [configinc], + install: true, + install_dir: libexecdir) +endif + +custom_target('gsettings-enums', + input: 'grd-enums.h', + output: 'org.gnome.desktop.remote-desktop.enums.xml', + install: true, + install_dir: schemadir, + capture: true, + command: ['glib-mkenums', + '--comments', '', + '--fhead', '', + '--vhead', ' <@type@ id="org.gnome.desktop.remote-desktop.@EnumName@">', + '--vprod', ' ', + '--vtail', ' ', + '--ftail', '', + '@INPUT@']) +schema = 'org.gnome.desktop.remote-desktop.gschema.xml' +generated_schema = configure_file(output: schema, + input: schema + '.in', + copy: true) +install_data(generated_schema, + install_dir: schemadir) + +subdir('shaders') diff --git a/org.gnome.DisplayManager.xml b/org.gnome.DisplayManager.xml new file mode 100644 index 0000000..145fe21 --- /dev/null +++ b/org.gnome.DisplayManager.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/org.gnome.Mutter.RemoteDesktop.xml b/org.gnome.Mutter.RemoteDesktop.xml new file mode 100644 index 0000000..64f8f82 --- /dev/null +++ b/org.gnome.Mutter.RemoteDesktop.xml @@ -0,0 +1,364 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/org.gnome.Mutter.ScreenCast.xml b/org.gnome.Mutter.ScreenCast.xml new file mode 100644 index 0000000..4d8936c --- /dev/null +++ b/org.gnome.Mutter.ScreenCast.xml @@ -0,0 +1,240 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/org.gnome.RemoteDesktop.xml b/org.gnome.RemoteDesktop.xml new file mode 100644 index 0000000..d42bf93 --- /dev/null +++ b/org.gnome.RemoteDesktop.xml @@ -0,0 +1,212 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/org.gnome.desktop.remote-desktop.gschema.xml.in b/org.gnome.desktop.remote-desktop.gschema.xml.in new file mode 100644 index 0000000..e98a9b3 --- /dev/null +++ b/org.gnome.desktop.remote-desktop.gschema.xml.in @@ -0,0 +1,238 @@ + + + + + + 3389 + The port used by the RDP server + + The RDP client will connect to this port to use this RDP server. + + + + true + Search a different RDP port if the configured one is used + + When negotiate-port is set to 'true' the RDP server will attempt to + listen to the first available of the next 10 ports starting from the + configured one. + + + + false + Whether the RDP backend is enabled or not + + If set to 'true' the RDP backend will be initialized. + + + + 'mirror-primary' + Screenshare mode of RDP connections + + The screenshare mode specifies, whether the RDP backend mirrors the + primary screen, or whether a virtual monitor is created. + + For the initial resolution of the virtual monitor, the RDP backend uses + either the client core data ([MS-RDPBCGR] 2.2.1.3.2) or the client + monitor data ([MS-RDPBCGR] 2.2.1.3.6), depending on what is available. + + When using a remote desktop session with a virtual monitor, clients can + resize the resolution of the virtual monitor during a session with the + Display Update Virtual Channel Extension ([MS-RDPEDISP]). + + Allowed screenshare modes include: + + * mirror-primary - Record the primary monitor of the current user + session. + + * extend - Create a new virtual monitor and use it for the remote + desktop session. + The resolution of this virtual monitor is derived from the + monitor configuration, submitted by the remote desktop + client. + + + + '' + Path to the certificate file + + In order to be able to use RDP with TLS Security, both the private key + file and the certificate file need to be provided to the RDP server. + + + + '' + Path to the private key file + + In order to be able to use RDP with TLS Security, both the private key + file and the certificate file need to be provided to the RDP server. + + + + true + Only allow remote connections to view the screen content + + When view-only is true, remote RDP connections cannot manipulate input + devices (e.g. mouse and keyboard). + + + + ['credentials'] + Allowed authentication methods + + Allowed authentication methods. Multiple may be selected to allow + different authentication mechanisms. At least one authentication method + must be selected. + + + + '' + A path to a Kerberos keytab file + + When 'kerberos' is enabled as an authentication method, the TERMSRV + Kerberos service principal in this keytab file will be used for + authentication. + + + + + + 3389 + The port used by the RDP server + + The RDP client will connect to this port to use this RDP server. + + + + true + Search a different RDP port if the configured one is used + + When negotiate-port is set to 'true' the RDP server will attempt to + listen to the first available of the next 10 ports starting from the + configured one. + + + + false + Whether the RDP backend is enabled or not + + If set to 'true' the RDP backend will be initialized. + + + + ['credentials'] + Allowed authentication methods + + Allowed authentication methods. Multiple may be selected to allow + different authentication mechanisms. At least one authentication method + must be selected. + + + + '' + A path to a Kerberos keytab file + + When 'kerberos' is enabled as an authentication method, the TERMSRV + Kerberos service principal in this keytab file will be used for + authentication. + + + + + + 5900 + The port used by the VNC server + + The VNC client will connect to this port to use this VNC server. + + + + false + Search a different VNC port if the configured one is used + + When negotiate-port is set to 'true' the VNC server will attempt to + listen to the first available of the next 10 ports starting from the + configured one. + + + + false + Whether the VNC backend is enabled or not + + If set to 'true' the VNC backend will be initialized. + + + + true + Only allow remote connections to view the screen content + + When view-only is true, remote VNC connections cannot manipulate input + devices (e.g. mouse and keyboard). + + + + 'prompt' + Method used to authenticate VNC connections + + The VNC authentication method describes how a remote connection is + authenticated. It can currently be done in two different ways: + + * prompt - by prompting the user for each new connection, requiring a + person with physical access to the workstation to + explicitly approve the new connection. + * password - by requiring the remote client to provide a known password + + + + 'mirror-primary' + Screenshare mode of VNC connections + + The screenshare mode specifies, whether the VNC backend mirrors the + primary screen, or whether a virtual monitor is created. + + For the initial resolution of the virtual monitor, the VNC backend + uses a default size of 1920x1080 pixels. + + When using a remote desktop session with a virtual monitor, clients can + resize the resolution of the virtual monitor during a session with the + setDesktopSize PDU. + + Allowed screenshare modes include: + + * mirror-primary - Record the primary monitor of the current user + session. + + * extend - Create a new virtual monitor and use it for the remote + desktop session. The initial monitor resolution of this + virtual monitor is set to 1920x1080 pixels. Clients can + override the initial resolution with subsequent monitor + configuration updates. + + + + + + 5900 + The port used by the VNC server + + The VNC client will connect to this port to use this VNC server. + + + + false + Search a different VNC port if the configured one is used + + When negotiate-port is set to 'true' the VNC server will attempt to + listen to the first available of the next 10 ports starting from the + configured one. + + + + false + Whether the VNC backend is enabled or not + + If set to 'true' the VNC backend will be initialized. + + + + diff --git a/shaders/grd-avc-dual-view.comp b/shaders/grd-avc-dual-view.comp new file mode 100644 index 0000000..d0637f2 --- /dev/null +++ b/shaders/grd-avc-dual-view.comp @@ -0,0 +1,354 @@ +/* + * Copyright (C) 2024 Pascal Nowack + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#version 460 +#extension GL_EXT_null_initializer : require +#extension GL_EXT_shader_explicit_arithmetic_types_int32 : require + +layout (local_size_x = 16, local_size_y = 16) in; + +layout (constant_id = 0) const uint32_t SOURCE_WIDTH = 256; +layout (constant_id = 1) const uint32_t SOURCE_HEIGHT = 256; + +layout (constant_id = 2) const uint32_t TARGET_WIDTH = 256; +layout (constant_id = 3) const uint32_t TARGET_HEIGHT = 256; + +layout (constant_id = 4) const uint32_t PERFORM_DMG_DETECTION = 0; +layout (constant_id = 5) const uint32_t STATE_BUFFER_STRIDE = 0; + +layout (set = 0, binding = 0, r8) uniform writeonly image2D main_y_layer; +layout (set = 0, binding = 1, rg8) uniform writeonly image2D main_uv_layer; + +layout (set = 1, binding = 0, r8) uniform writeonly image2D aux_y_layer; +layout (set = 1, binding = 1, rg8) uniform writeonly image2D aux_uv_layer; + +layout (set = 2, binding = 0) buffer writeonly DamageBuffer +{ + uint32_t data[]; +} needs_update; + +layout (set = 2, binding = 1) buffer writeonly AuxiliaryViewInfoBuffer +{ + uint32_t data[]; +} needs_auxiliary_view; + +layout (set = 3, binding = 0) uniform sampler2D src_new_sampler; +layout (set = 3, binding = 1) uniform sampler2D src_old_sampler; + +float +rgb_to_y (int32_t r, + int32_t g, + int32_t b) +{ + return float ((54 * r + 183 * g + 18 * b) >> 8); +} + +float +rgb_to_u (int32_t r, + int32_t g, + int32_t b) +{ + return float (((-29 * r - 99 * g + 128 * b) >> 8) + 128); +} + +float +rgb_to_v (int32_t r, + int32_t g, + int32_t b) +{ + return float (((128 * r - 116 * g - 12 * b) >> 8) + 128); +} + +shared uint32_t have_block_damage = {}; +shared uint32_t have_chroma_offset = {}; +shared float block_u2[16][16] = {}; +shared float block_v2[16][16] = {}; + +void +main () +{ + const uint32_t x_2x2 = gl_GlobalInvocationID.x; + const uint32_t y_2x2 = gl_GlobalInvocationID.y; + const uint32_t local_x = gl_LocalInvocationID.x; + const uint32_t local_y = gl_LocalInvocationID.y; + const uint32_t tw_half = TARGET_WIDTH >> 1; + uint32_t x_1x1; + uint32_t y_1x1; + uint32_t x_4x4; + uint32_t x_64x64; + uint32_t y_64x64; + float y0, y1, y2, y3; + float u0, u1, u2, u3; + float v0, v1, v2, v3; + float u_filtered, v_filtered; + uint32_t dmg_p0, dmg_p1, dmg_p2, dmg_p3; + vec4 uv_filtered; + + x_1x1 = x_2x2 << 1; + y_1x1 = y_2x2 << 1; + x_4x4 = x_2x2 >> 1; + x_64x64 = x_2x2 >> 5; + y_64x64 = y_2x2 >> 5; + + dmg_p0 = dmg_p1 = dmg_p2 = dmg_p3 = 0; + + /* + * YUV444: + * + * Y U V + * ----------- ----------- ----------- + * | y0 | y1 | | u0 | u1 | | v0 | v1 | + * ----------- ----------- ----------- + * | y2 | y3 | | u2 | u3 | | v2 | v3 | + * ----------- ----------- ----------- + */ + if (x_1x1 < SOURCE_WIDTH && y_1x1 < SOURCE_HEIGHT) + { + vec4 bgrx_new; + vec4 bgrx_old; + int32_t b, g, r; + + bgrx_new = texture (src_new_sampler, ivec2 (x_1x1, y_1x1)); + bgrx_old = texture (src_old_sampler, ivec2 (x_1x1, y_1x1)); + + dmg_p0 = bgrx_new.bgr != bgrx_old.bgr ? 1 : 0; + + b = int32_t (bgrx_new.b * 255.0f); + g = int32_t (bgrx_new.g * 255.0f); + r = int32_t (bgrx_new.r * 255.0f); + + y0 = rgb_to_y (r, g, b); + u0 = rgb_to_u (r, g, b); + v0 = rgb_to_v (r, g, b); + } + else + { + y0 = 0.0f; + u0 = 128.0f; + v0 = 128.0f; + } + + if (x_1x1 + 1 < SOURCE_WIDTH && y_1x1 < SOURCE_HEIGHT) + { + vec4 bgrx_new; + vec4 bgrx_old; + int32_t b, g, r; + + bgrx_new = texture (src_new_sampler, ivec2 (x_1x1 + 1, y_1x1)); + bgrx_old = texture (src_old_sampler, ivec2 (x_1x1 + 1, y_1x1)); + + dmg_p1 = bgrx_new.bgr != bgrx_old.bgr ? 1 : 0; + + b = int32_t (bgrx_new.b * 255.0f); + g = int32_t (bgrx_new.g * 255.0f); + r = int32_t (bgrx_new.r * 255.0f); + + y1 = rgb_to_y (r, g, b); + u1 = rgb_to_u (r, g, b); + v1 = rgb_to_v (r, g, b); + } + else + { + y1 = y0; + u1 = u0; + v1 = v0; + } + + if (x_1x1 < SOURCE_WIDTH && y_1x1 + 1 < SOURCE_HEIGHT) + { + vec4 bgrx_new; + vec4 bgrx_old; + int32_t b, g, r; + + bgrx_new = texture (src_new_sampler, ivec2 (x_1x1, y_1x1 + 1)); + bgrx_old = texture (src_old_sampler, ivec2 (x_1x1, y_1x1 + 1)); + + dmg_p2 = bgrx_new.bgr != bgrx_old.bgr ? 1 : 0; + + b = int32_t (bgrx_new.b * 255.0f); + g = int32_t (bgrx_new.g * 255.0f); + r = int32_t (bgrx_new.r * 255.0f); + + y2 = rgb_to_y (r, g, b); + u2 = rgb_to_u (r, g, b); + v2 = rgb_to_v (r, g, b); + + if (x_1x1 + 1 < SOURCE_WIDTH) + { + bgrx_new = texture (src_new_sampler, ivec2 (x_1x1 + 1, y_1x1 + 1)); + bgrx_old = texture (src_old_sampler, ivec2 (x_1x1 + 1, y_1x1 + 1)); + + dmg_p3 = bgrx_new.bgr != bgrx_old.bgr ? 1 : 0; + + b = int32_t (bgrx_new.b * 255.0f); + g = int32_t (bgrx_new.g * 255.0f); + r = int32_t (bgrx_new.r * 255.0f); + + y3 = rgb_to_y (r, g, b); + u3 = rgb_to_u (r, g, b); + v3 = rgb_to_v (r, g, b); + } + else + { + y3 = y2; + u3 = u2; + v3 = v2; + } + } + else + { + y2 = y0; + u2 = u0; + v2 = v0; + y3 = y1; + u3 = u1; + v3 = v1; + } + + block_u2[local_x][local_y] = u2; + block_v2[local_x][local_y] = v2; + + u_filtered = (u0 + u1 + u2 + u3) / 4; + v_filtered = (v0 + v1 + v2 + v3) / 4; + + /* + * When decoding and reassembling the original frame, the client side should + * use the filtered value over the reversed value, when their value + * difference is lower than a specific threshold, due to potential artifacts + * stemming from the quantization process of the AVC encoding. + * As a threshold 3.3.8.3.3 YUV420p Stream Combination for YUV444v2 mode + * ([MS-RDPEGFX]) considers the value 30. + * This threshold can also serve the server side here by checking whether an + * auxiliary view is actually needed. + */ + if (abs (u_filtered - u0) > 30 || + abs (u_filtered - u1) > 30 || + abs (u_filtered - u2) > 30 || + abs (u_filtered - u3) > 30 || + abs (v_filtered - v0) > 30 || + abs (v_filtered - v1) > 30 || + abs (v_filtered - v2) > 30 || + abs (v_filtered - v3) > 30) + have_chroma_offset = 1; + + /* We cannot bail out early here due to the barrier() call */ + if (x_2x2 < TARGET_WIDTH >> 1 && y_2x2 < TARGET_HEIGHT >> 1) + { + if (PERFORM_DMG_DETECTION == 0 || + dmg_p0 != 0 || dmg_p1 != 0 || dmg_p2 != 0 || dmg_p3 != 0) + have_block_damage = 1; + } + + barrier (); + if (x_2x2 >= TARGET_WIDTH >> 1 || y_2x2 >= TARGET_HEIGHT >> 1) + return; + + if (local_y == 0 && + (local_x == 0 || local_x == 1)) + { + uint32_t state_pos; + + state_pos = y_64x64 * STATE_BUFFER_STRIDE + x_64x64; + + if (local_x == 0 && have_block_damage == 1) + needs_update.data[state_pos] = 1; + if (local_x == 1 && have_chroma_offset == 1 && have_block_damage == 1) + needs_auxiliary_view.data[state_pos] = 1; + } + + /* + * See also 3.3.8.3.3 YUV420p Stream Combination for YUV444v2 mode + * ([MS-RDPEGFX]) for the construction of the main and auxiliary view. + */ + + /* + * The main view is constructed the same way as in the AVC420 case + * (4 Luma values (4Y), 2 Chroma values (1U, 1V) per 2x2 BGRX block). + * + * The chroma values are calculated from the average U/V values. + */ + + imageStore (main_y_layer, ivec2 (x_1x1, y_1x1), vec4 (y0 / 255.0f)); + imageStore (main_y_layer, ivec2 (x_1x1 + 1, y_1x1), vec4 (y1 / 255.0f)); + imageStore (main_y_layer, ivec2 (x_1x1, y_1x1 + 1), vec4 (y2 / 255.0f)); + imageStore (main_y_layer, ivec2 (x_1x1 + 1, y_1x1 + 1), vec4 (y3 / 255.0f)); + + uv_filtered = vec4 (u_filtered / 255.0f, + v_filtered / 255.0f, + 0, + 0); + imageStore (main_uv_layer, ivec2 (x_2x2, y_2x2), uv_filtered); + + /* + * The auxiliary view is constructed as follows (simplified): + * + * Luma: + * ----------------------------------------------- + * | u1 | u1 | u1 | u1 | ... | v1 | v1 | v1 | v1 | + * ----------------------------------------------- + * | u3 | u3 | u3 | u3 | ... | v3 | v3 | v3 | v3 | + * ----------------------------------------------- + * | u1 | u1 | u1 | u1 | ... | v1 | v1 | v1 | v1 | + * ----------------------------------------------- + * | u3 | u3 | u3 | u3 | ... | v3 | v3 | v3 | v3 | + * ----------------------------------------------- + * ... + * + * Chroma U: + * ----------------------------------------------- + * | u2 | u2 | u2 | u2 | ... | v2 | v2 | v2 | v2 | + * ----------------------------------------------- + * ... + * + * Chroma V: + * ----------------------------------------------- + * | u2 | u2 | u2 | u2 | ... | v2 | v2 | v2 | v2 | + * ----------------------------------------------- + * ... + * + * If x_1x1 MOD 4 == 0, then u2 and v2 are written to U, + * otherwise they will be written to V. + */ + + imageStore (aux_y_layer, ivec2 (x_2x2, y_1x1), vec4 (u1 / 255.0f)); + imageStore (aux_y_layer, ivec2 (x_2x2 + tw_half, y_1x1), vec4 (v1 / 255.0f)); + imageStore (aux_y_layer, ivec2 (x_2x2, y_1x1 + 1), vec4 (u3 / 255.0f)); + imageStore (aux_y_layer, ivec2 (x_2x2 + tw_half, y_1x1 + 1), vec4 (v3 / 255.0f)); + + if (x_2x2 == x_4x4 << 1) + { + vec4 u2s; + + u2s = vec4 (block_u2[local_x][local_y] / 255.0f, + block_u2[local_x + 1][local_y] / 255.0f, + 0, + 0); + imageStore (aux_uv_layer, ivec2 (x_4x4, y_2x2), u2s); + } + else + { + vec4 v2s; + + v2s = vec4 (block_v2[local_x - 1][local_y] / 255.0f, + block_v2[local_x][local_y] / 255.0f, + 0, + 0); + imageStore (aux_uv_layer, ivec2 (x_4x4 + (tw_half >> 1), y_2x2), v2s); + } +} diff --git a/shaders/meson.build b/shaders/meson.build new file mode 100644 index 0000000..1f5266b --- /dev/null +++ b/shaders/meson.build @@ -0,0 +1,17 @@ +if have_rdp + shaders = [ + 'grd-avc-dual-view', + ] + + spirv_sources = [] + + foreach shader : shaders + run_command(glslc, '-o', shader + '.spv', shader + '.comp', check: true) + run_command(spirv_opt, '-o', shader + '_opt.spv', '-O', shader + '.spv', check: true) + spirv_sources += [shader + '_opt.spv'] + endforeach + + install_data(spirv_sources, + install_dir: grd_shaderdir + ) +endif