From 025b31d46a6a148bb8d72c3e16bb06e73ff60582 Mon Sep 17 00:00:00 2001 From: Janne Heinonen Date: Thu, 14 May 2026 13:42:16 +0300 Subject: [PATCH] GUACAMOLE-2123: Implement support for RDPGFX-backed RemoteApp rendering by tracking RAIL windows as independent Guacamole layers. Synchronize RDPGFX surface updates with RAIL window metadata. Track surface-to-window mappings, window ownership, visibility, geometry, activation, and z-order to render RemoteApp windows independently of the hidden desktop. Add support for both FreeRDP 2.x UpdateSurfaceArea and FreeRDP 3.x UpdateWindowFromSurface, including HiDPI scaling. Improved input responsiveness. Preserve the hidden desktop framebuffer and black background during RemoteApp sessions. RDPGFX can be enabled for FreeRDP 3 only. --- configure.ac | 50 + src/common/surface.c | 4 - src/protocols/rdp/channels/rail.c | 2352 ++++++++++++++++++++++++++- src/protocols/rdp/channels/rail.h | 149 ++ src/protocols/rdp/channels/rdpgfx.c | 68 +- src/protocols/rdp/client.c | 4 + src/protocols/rdp/gdi.c | 120 +- src/protocols/rdp/input-queue.c | 23 +- src/protocols/rdp/input.c | 7 + src/protocols/rdp/rdp.c | 11 +- src/protocols/rdp/rdp.h | 32 + src/protocols/rdp/settings.c | 38 +- 12 files changed, 2778 insertions(+), 80 deletions(-) diff --git a/configure.ac b/configure.ac index 6809a43e0b..f64f96ef1b 100644 --- a/configure.ac +++ b/configure.ac @@ -1080,6 +1080,56 @@ then [#include ]) fi if test "x${have_freerdp}" = "xyes" +then + AC_MSG_CHECKING([whether FreeRDP supports RDPGFX surface area updates]) + AC_COMPILE_IFELSE([AC_LANG_SOURCE([[ + #include + #include + #include + #include + #include + + int main() { + RdpgfxClientContext* context = 0; + rdpGdi* gdi = 0; + gdiGfxSurface* surface = 0; + + (void) context->GetSurfaceData(context, 0); + (void) gdi_graphics_pipeline_init_ex(gdi, context, 0, 0, 0); + (void) freerdp_image_copy_no_overlap(0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0); + (void) surface->windowId; + (void) surface->mappedWidth; + (void) surface->mappedHeight; + (void) surface->outputTargetWidth; + (void) surface->outputTargetHeight; + + return 0; + } + ]])], + [AC_MSG_RESULT([yes])] + [AC_DEFINE([HAVE_RDPGFX_SURFACE_AREA_UPDATE],, + [Defined if FreeRDP supports RDPGFX surface area update callbacks])], + [AC_MSG_RESULT([no])]) +fi +if test "x${have_freerdp}" = "xyes" +then + AC_MSG_CHECKING([whether FreeRDP supports RDPGFX window surface updates]) + AC_COMPILE_IFELSE([AC_LANG_SOURCE([[ + #include + + int main() { + RdpgfxClientContext* context = 0; + context->UpdateWindowFromSurface = 0; + return 0; + } + ]])], + [AC_MSG_RESULT([yes])] + [AC_DEFINE([HAVE_RDPGFX_WINDOW_SURFACE_UPDATE],, + [Defined if FreeRDP supports RDPGFX updates for window-mapped surfaces])], + [AC_MSG_RESULT([no])]) +fi +if test "x${have_freerdp}" = "xyes" then # Check whether FreeRDP 3.x requires const for GetPluginData AC_MSG_CHECKING([whether GetPluginData requires const for the returned args]) diff --git a/src/common/surface.c b/src/common/surface.c index 3ec5517e98..f3f5d9c5d4 100644 --- a/src/common/surface.c +++ b/src/common/surface.c @@ -1895,8 +1895,6 @@ static void __guac_common_surface_flush(guac_common_surface* surface) { guac_common_surface_bitmap_rect* current = surface->bitmap_queue; int i, j; int original_queue_length; - int flushed = 0; - original_queue_length = surface->bitmap_queue_length; /* Sort updates to make combination less costly */ @@ -1943,8 +1941,6 @@ static void __guac_common_surface_flush(guac_common_surface* surface) { /* Flush as bitmap otherwise */ else if (surface->dirty) { - flushed++; - int opaque = __guac_common_surface_is_opaque(surface, &surface->dirty_rect); diff --git a/src/protocols/rdp/channels/rail.c b/src/protocols/rdp/channels/rail.c index bd7e65a715..f9df60e508 100644 --- a/src/protocols/rdp/channels/rail.c +++ b/src/protocols/rdp/channels/rail.c @@ -19,11 +19,22 @@ #include "config.h" +#include "color.h" #include "channels/rail.h" +#include "common/list.h" #include "plugins/channels.h" #include "rdp.h" #include "settings.h" +#ifdef HAVE_RDPGFX_SURFACE_AREA_UPDATE +#include +#include +#include +#include +#include +#include +#endif + #include #include #include @@ -31,10 +42,12 @@ #include #include #include +#include #include #include #include +#include #include /** @@ -66,24 +79,34 @@ static UINT guac_rdp_rail_complete_handshake(RailClientContext* rail) { guac_client* client = (guac_client*) rail->custom; guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; + /* Do not advertise TS_RAIL_CLIENTSTATUS_ALLOWLOCALMOVESIZE. Guacamole + * does not manage native local windows and does not send ClientWindowMove + * when a local move/resize completes. Advertising that capability can + * cause the server to enter a move/resize path that Guacamole cannot + * finish, leaving subsequent input ignored after dragging a RemoteApp + * window. */ RAIL_CLIENT_STATUS_ORDER client_status = { - .flags = - TS_RAIL_CLIENTSTATUS_ALLOWLOCALMOVESIZE - | TS_RAIL_CLIENTSTATUS_APPBAR_REMOTING_SUPPORTED + .flags = TS_RAIL_CLIENTSTATUS_APPBAR_REMOTING_SUPPORTED }; /* Send client status */ - guac_client_log(client, GUAC_LOG_TRACE, "Sending RAIL client status."); + guac_client_log(client, GUAC_LOG_TRACE, + "Sending RAIL client status: flags=0x%08x.", + (unsigned int) client_status.flags); pthread_mutex_lock(&(rdp_client->message_lock)); status = rail->ClientInformation(rail, &client_status); pthread_mutex_unlock(&(rdp_client->message_lock)); - if (status != CHANNEL_RC_OK) + if (status != CHANNEL_RC_OK) { + guac_client_log(client, GUAC_LOG_DEBUG, + "RAIL client status send failed: status=%u.", + (unsigned int) status); return status; + } RAIL_SYSPARAM_ORDER sysparam = { - .dragFullWindows = FALSE, + .dragFullWindows = rdp_client->settings->full_window_drag_enabled, .highContrast = { .flags = @@ -115,6 +138,9 @@ static UINT guac_rdp_rail_complete_handshake(RailClientContext* rail) { | SPI_MASK_SET_KEYBOARD_CUES | SPI_MASK_SET_KEYBOARD_PREF | SPI_MASK_SET_MOUSE_BUTTON_SWAP +#ifdef SPI_MASK_SET_DRAG_FULL_WINDOWS + | SPI_MASK_SET_DRAG_FULL_WINDOWS +#endif | SPI_MASK_SET_WORK_AREA }; @@ -136,11 +162,24 @@ static UINT guac_rdp_rail_complete_handshake(RailClientContext* rail) { }; /* Execute desired RemoteApp command */ - guac_client_log(client, GUAC_LOG_TRACE, "Executing remote application."); + guac_client_log(client, GUAC_LOG_TRACE, + "Executing remote application: program=\"%s\", args=\"%s\", " + "working-dir=\"%s\", flags=0x%04x.", + exec.RemoteApplicationProgram != NULL + ? exec.RemoteApplicationProgram : "", + exec.RemoteApplicationArguments != NULL + ? exec.RemoteApplicationArguments : "", + exec.RemoteApplicationWorkingDir != NULL + ? exec.RemoteApplicationWorkingDir : "", + (unsigned int) exec.flags); pthread_mutex_lock(&(rdp_client->message_lock)); status = rail->ClientExecute(rail, &exec); pthread_mutex_unlock(&(rdp_client->message_lock)); + guac_client_log(client, GUAC_LOG_TRACE, + "RAIL client execute send completed: status=%u.", + (unsigned int) status); + return status; } @@ -149,7 +188,7 @@ static UINT guac_rdp_rail_complete_handshake(RailClientContext* rail) { * A callback function that is invoked when the RDP server sends the result * of the Remote App (RAIL) execution command back to the client, so that the * client can handle any required actions associated with the result. - * + * * @param context * A pointer to the RAIL data structure associated with the current * RDP connection. @@ -167,6 +206,11 @@ static UINT guac_rdp_rail_execute_result(RailClientContext* context, guac_client* client = (guac_client*) context->custom; + if (execResult->execResult == RAIL_EXEC_E_NOT_IN_ALLOWLIST) { + guac_client_log(client, GUAC_LOG_WARNING, "RAIL command reported " + "application not in allowlist."); + } + if (execResult->execResult != RAIL_EXEC_S_OK) { guac_client_log(client, GUAC_LOG_DEBUG, "Failed to execute RAIL command on server: %d", execResult->execResult); guac_client_abort(client, GUAC_PROTOCOL_STATUS_UPSTREAM_UNAVAILABLE, "Failed to execute RAIL command."); @@ -228,54 +272,2281 @@ static UINT guac_rdp_rail_handshake_ex(RailClientContext* rail, return guac_rdp_rail_complete_handshake(rail); } +/* + * RemoteApp (RAIL) with the RDPGFX pipeline. + * + * When both RAIL and GFX are active, the RDP server delivers per-window + * graphical updates as RDPGFX surfaces tagged with a non-zero windowId + * (gdiGfxSurface->windowId). The default GDI graphics pipeline only knows how + * to paint into a single primary desktop buffer, so without intervention these + * window-mapped surfaces are silently dropped on the floor. + * + * This module tracks each RAIL window with its own guac_display_layer, + * positioned by the RAIL window-order stream and painted from the corresponding + * RDPGFX surface via either: + * + * - UpdateSurfaceArea (fallback for older FreeRDP without the window + * callback; called with rect lists for any surface update). + * + * - UpdateWindowFromSurface, when available. + */ + +/* + * Each guac_rdp_rail_window stores the per-window state needed to combine + * RAIL window ordering with RDPGFX surface updates and render the result as a + * Guacamole layer. + */ +typedef struct guac_rdp_rail_window { + + /** + * The RDP server-side ID of the RAIL window. + */ + UINT64 window_id; + + /** + * The RDPGFX surface ID currently mapped to this window, if known. + */ + UINT16 surface_id; + + /** + * Whether surface_id contains a valid mapped RDPGFX surface ID. + */ + int has_surface_id; + + /** + * The RDP server-side ID of the owning RAIL window, if any. + */ + UINT64 owner_window_id; + + /** + * The Guacamole layer that renders the contents of this window. + */ + guac_display_layer* layer; + + /** + * Whether a display layer allocation is already in progress for this + * window. + */ + int layer_pending; + + /** + * Optional Guacamole layer used only when this window paints a surface + * whose pixel format explicitly includes alpha. + */ + guac_display_layer* alpha_layer; + + /** + * Whether the currently active backing layer preserves alpha. + */ + int layer_alpha; + + /** + * The x coordinate of this window within the remote desktop. + */ + int x; + + /** + * The y coordinate of this window within the remote desktop. + */ + int y; + + /** + * Whether this window should be visible (per RAIL show state). + */ + int visible; + + /** + * Whether this window has received at least one graphical update from an + * RDPGFX surface. + */ + int has_surface; + + /** + * Whether an RDPGFX surface update arrived before the RAIL window order. + */ + int has_deferred_surface; + + /** + * Whether a RAIL WindowCreate/Update order has been observed for this + * window. Layers created speculatively from a surface update (before any + * RAIL order arrives) are held off-screen until this is set. + */ + int has_rail_order; + + /** + * Whether this window was included in the most recent server-provided + * monitored desktop z-order list. Used to avoid applying the raise to + * windows that already have authoritative z-order. + * We obey the server provided z-order, whenever it is available. + */ + int in_monitored_desktop_zorder; + + /** + * Whether this window has already been raised due to receiving its first + * visible paint after its RAIL order was known. If the window does not yet + * have authoritative z-order, we apply this fallback raise only once. + */ + int painted_raise_done; + + /** + * The last z-order assigned to the Guacamole layer for this window. + * + * Guacamole layer stacking is managed locally, separately from the server's + * RAIL window IDs. Tracking the assigned z-order lets us find the current + * topmost layer and raise another window above it without relying on list + * order or window IDs. + */ + int z_order; + + /** + * The current width of the Guacamole layer. + */ + int layer_width; + + /** + * The current height of the Guacamole layer. + */ + int layer_height; + + /** + * The latest window width reported by RAIL window orders. + */ + int order_width; + + /** + * The latest window height reported by RAIL window orders. + */ + int order_height; + + /** + * The latest client-area x coordinate reported by RAIL window orders. + */ + int client_x; + + /** + * The latest client-area y coordinate reported by RAIL window orders. + */ + int client_y; + + /** + * The latest client-area width reported by RAIL window orders. + */ + int client_width; + + /** + * The latest client-area height reported by RAIL window orders. + */ + int client_height; + + /** + * Whether client-area bounds have been reported by RAIL window orders. + */ + int has_client_area; + + /** + * The width of the mapped RDPGFX surface represented by the layer. + */ + int surface_width; + + /** + * The height of the mapped RDPGFX surface represented by the layer. + */ + int surface_height; + + /** + * Number of RDPGFX paint operations currently using this window's layer. + */ + int paint_refs; + + /** + * Whether this window has been removed from the tracked window list and + * should be freed once all outstanding paints finish. + */ + int deleted; + +} guac_rdp_rail_window; + +#ifdef HAVE_RDPGFX_SURFACE_AREA_UPDATE +static UINT guac_rdp_rail_paint_surface(guac_client* client, + gdiGfxSurface* surface, UINT32 rect_count, const RECTANGLE_16* rects); +#endif + +static void guac_rdp_rail_apply_window_layers( + guac_rdp_client* rdp_client); + /** - * A callback function that is executed when an update for a RAIL window is - * received from the RDP server. + * Returns whether RAIL window surfaces should be rendered through Guacamole + * layers for the given RDP client. * - * @param context - * A pointer to the rdpContext structure used by FreeRDP to handle the - * window update. + * RemoteApp windows are rendered as separate Guacamole layers only when both + * RemoteApp and RDPGFX are enabled. If GFX is disabled, rendering falls back to + * the normal GDI/primary-buffer path and the RAIL layer tracking code should + * stay inactive. * - * @param orderInfo - * A pointer to the data structure that contains information about what - * window was updated what updates were performed. + * @param rdp_client + * The RDP client whose settings should be checked. * - * @param windowState - * A pointer to the data structure that contains details of the updates - * to the window, as indicated by flags in the orderInfo field. + * @return + * Non-zero if RAIL window surfaces should be rendered through Guacamole layers. + */ +static int guac_rdp_rail_uses_gfx(guac_rdp_client* rdp_client) { + return rdp_client->settings != NULL + && rdp_client->settings->remote_app != NULL + && rdp_client->settings->enable_gfx + && rdp_client->rdpgfx_interface != NULL; +} + +/** + * Updates the RemoteApp window container layer to cover the default layer. + * + * @param rdp_client + * The RDP client whose RemoteApp window container should be resized. + */ +static void guac_rdp_rail_update_window_layer_bounds( + guac_rdp_client* rdp_client) { + + if (rdp_client->rail_window_layer == NULL) + return; + + guac_display_layer* default_layer = + guac_display_default_layer(rdp_client->display); + + guac_rect bounds; + guac_display_layer_get_bounds(default_layer, &bounds); + + int width = guac_rect_width(&bounds); + int height = guac_rect_height(&bounds); + + if (width > 0 && height > 0) + guac_display_layer_resize(rdp_client->rail_window_layer, + width, height); + +} + +/** + * Returns the layer used as the common parent for all RemoteApp window layers, + * creating it if necessary. This additional layer keeps RemoteApp window + * z-order local to the window group, preventing those windows from stacking + * above Guacamole's cursor layer. + * + * @param client + * The Guacamole client associated with the RDP session. * * @return - * TRUE if the client-side processing of the updates as successful; otherwise - * FALSE. This implementation always returns TRUE. + * The RemoteApp window container layer, or NULL if it could not be + * allocated. */ -static BOOL guac_rdp_rail_window_update(rdpContext* context, - RAIL_CONST WINDOW_ORDER_INFO* orderInfo, - RAIL_CONST WINDOW_STATE_ORDER* windowState) { +static guac_display_layer* guac_rdp_rail_get_window_layer( + guac_client* client) { - guac_client* client = ((rdp_freerdp_context*) context)->client; guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; - guac_client_log(client, GUAC_LOG_TRACE, "RAIL window update callback: %d", orderInfo->fieldFlags); + if (rdp_client->rail_window_layer != NULL) { + guac_rdp_rail_update_window_layer_bounds(rdp_client); + return rdp_client->rail_window_layer; + } - UINT32 fieldFlags = orderInfo->fieldFlags; + guac_display_layer* layer = + guac_display_alloc_layer(rdp_client->display, 0); - /* If the flag for window visibilty is set, check visibility. */ - if (fieldFlags & WINDOW_ORDER_FIELD_SHOW) { - guac_client_log(client, GUAC_LOG_TRACE, "RAIL window visibility change: %d", windowState->showState); + if (layer == NULL) { + guac_client_log(client, GUAC_LOG_WARNING, "RAIL window container " + "could not be allocated. Window contents may not render."); + return NULL; + } - /* State is either hidden or minimized - send restore command. */ - if (windowState->showState == GUAC_RDP_RAIL_WINDOW_STATE_MINIMIZED) { + guac_display_layer_set_parent(layer, + guac_display_default_layer(rdp_client->display)); + guac_display_layer_move(layer, 0, 0); + guac_display_layer_stack(layer, 0); - guac_client_log(client, GUAC_LOG_DEBUG, "RAIL window minimized, sending restore command."); + rdp_client->rail_window_layer = layer; + guac_rdp_rail_update_window_layer_bounds(rdp_client); - RAIL_SYSCOMMAND_ORDER syscommand; - syscommand.windowId = orderInfo->windowId; - syscommand.command = SC_RESTORE; - rdp_client->rail_interface->ClientSystemCommand(rdp_client->rail_interface, &syscommand); + return layer; + +} + +/** + * Returns the RAIL window having the given ID, or NULL if no such window is + * being tracked. The rail_windows list lock must be held. + * + * @param rdp_client + * The RDP client instance whose tracked RAIL windows should be searched. + * + * @param window_id + * The server-side ID of the RAIL window to retrieve. + * + * @return + * The matching RAIL window, or NULL if no such window is being tracked. + */ +static guac_rdp_rail_window* guac_rdp_rail_get_window( + guac_rdp_client* rdp_client, UINT64 window_id) { + + guac_common_list_element* current = rdp_client->rail_windows->head; + + while (current != NULL) { + guac_rdp_rail_window* rail_window = current->data; + + if (rail_window != NULL + && rail_window->window_id == window_id) + return rail_window; + + current = current->next; + } + + return NULL; +} + +#ifdef HAVE_RDPGFX_SURFACE_AREA_UPDATE +/** + * Returns the tracked RAIL window currently mapped to the given RDPGFX + * surface, or NULL if no such mapping is known. The rail_windows list lock + * must be held. + * + * @param rdp_client + * The RDP client whose tracked RAIL windows should be searched. + * + * @param surface_id + * The server-side ID of the RDPGFX surface to look up. + * + * @return + * The matching tracked RAIL window, or NULL if no such mapping exists. + */ +static guac_rdp_rail_window* guac_rdp_rail_get_window_for_surface( + guac_rdp_client* rdp_client, UINT16 surface_id) { + + guac_common_list_element* current = rdp_client->rail_windows->head; + + while (current != NULL) { + guac_rdp_rail_window* rail_window = current->data; + + if (rail_window != NULL + && rail_window->has_surface_id + && rail_window->surface_id == surface_id) + return rail_window; + + current = current->next; + } + + return NULL; +} +#endif + +/** + * Returns the list element containing the RAIL window having the given ID, or + * NULL if no such window is being tracked. The rail_windows list lock must be + * held. + * + * @param rdp_client + * The RDP client instance whose tracked RAIL windows should be searched. + * + * @param window_id + * The server-side ID of the RAIL window to retrieve. + * + * @return + * The list element containing the matching RAIL window, or NULL if no + * such window is being tracked. + */ +static guac_common_list_element* guac_rdp_rail_get_window_element( + guac_rdp_client* rdp_client, UINT64 window_id) { + + guac_common_list_element* current = rdp_client->rail_windows->head; + + while (current != NULL) { + guac_rdp_rail_window* rail_window = current->data; + + if (rail_window != NULL + && rail_window->window_id == window_id) + return current; + + current = current->next; + } + + return NULL; +} + +/** + * Updates the stacking order tracked for the given RAIL + * window. The rail_windows list lock must be held. + * + * @param rail_window + * The RAIL window whose tracked stacking order should be updated. + * + * @param z + * The new stacking order. + */ +static void guac_rdp_rail_stack_window(guac_rdp_rail_window* rail_window, + int z) { + + rail_window->z_order = z; + +} + +/** + * Snapshot of display-layer state to apply outside the rail_windows lock. + */ +typedef struct guac_rdp_rail_layer_update { + + guac_display_layer* layer; + guac_display_layer* alpha_layer; + + int x; + int y; + int z_order; + int opacity; + int layer_alpha; + +} guac_rdp_rail_layer_update; + +#ifdef HAVE_RDPGFX_SURFACE_AREA_UPDATE +/** + * Returns the optional alpha-capable layer for the given RAIL window, + * creating it if necessary. The rail_windows list lock must be held. + * + * @param client + * The Guacamole client associated with the current RDP session. + * + * @param rail_window + * The RAIL window whose alpha-capable layer should be retrieved. + * + * @return + * The alpha-capable layer, or NULL if it could not be allocated. + */ +static guac_display_layer* guac_rdp_rail_get_alpha_layer( + guac_client* client, guac_rdp_rail_window* rail_window) { + + if (rail_window->alpha_layer != NULL) + return rail_window->alpha_layer; + + guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; + + guac_display_layer* parent_layer = + guac_rdp_rail_get_window_layer(client); + + if (parent_layer == NULL) + return NULL; + + guac_display_layer* layer = + guac_display_alloc_layer(rdp_client->display, 0); + + if (layer == NULL) + return NULL; + + guac_display_layer_set_parent(layer, parent_layer); + guac_display_layer_move(layer, rail_window->x, rail_window->y); + guac_display_layer_stack(layer, rail_window->z_order); + guac_display_layer_set_opacity(layer, 0); + + if (rail_window->layer_width > 0 && rail_window->layer_height > 0) + guac_display_layer_resize(layer, + rail_window->layer_width, rail_window->layer_height); + + rail_window->alpha_layer = layer; + return layer; + +} +#endif + +/** + * Returns a z-order value above all currently tracked RAIL windows. The + * rail_windows list lock must be held. + * + * @param rdp_client + * The RDP client whose tracked windows should be counted. + * + * @return + * A z-order value above all currently tracked RAIL windows. + */ +static int guac_rdp_rail_get_top_z(guac_rdp_client* rdp_client) { + + int z = 1; + + for (guac_common_list_element* current = rdp_client->rail_windows->head; + current != NULL; current = current->next) { + guac_rdp_rail_window* rail_window = current->data; + + if (rail_window != NULL + && rail_window->z_order >= z) + z = rail_window->z_order + 1; + } + + return z; + +} + +/** + * Raises all tracked windows owned by the given parent window above that + * parent's layer. The rail_windows list lock must be held. + * This is needed for showing popup/dialog windows owned by another window, + * which might otherwise stay hidden behind their owner or other RemoteApp windows. + * + * @param rdp_client + * The RDP client whose tracked windows should be searched. + * + * @param owner_window_id + * The server-side ID of the owner window. + * + * @param z + * The z-order to assign to the first owned window. + * + * @return + * The next available z-order after all owned windows have been raised. + */ +static int guac_rdp_rail_raise_owned_windows(guac_rdp_client* rdp_client, + UINT64 owner_window_id, int z) { + + guac_common_list_element* current = rdp_client->rail_windows->head; + + while (current != NULL) { + guac_rdp_rail_window* rail_window = current->data; + + if (rail_window != NULL + && rail_window->has_rail_order + && rail_window->owner_window_id == owner_window_id + && rail_window->window_id != owner_window_id) { + guac_rdp_rail_stack_window(rail_window, z++); + z = guac_rdp_rail_raise_owned_windows(rdp_client, + rail_window->window_id, z); } + + current = current->next; } - return TRUE; + return z; + +} + +/** + * Raises the given window above its owner, if that owner is currently being + * tracked. The rail_windows list lock must be held. + * + * @param rdp_client + * The RDP client whose tracked windows should be searched. + * + * @param rail_window + * The window that should be raised above its owner. + */ +static void guac_rdp_rail_raise_window_above_owner( + guac_rdp_client* rdp_client, guac_rdp_rail_window* rail_window) { + + if (rail_window == NULL || rail_window->owner_window_id == 0) + return; + + guac_rdp_rail_window* owner = guac_rdp_rail_get_window(rdp_client, + rail_window->owner_window_id); + + if (owner == NULL) + return; + + int z = guac_rdp_rail_get_top_z(rdp_client); + + guac_rdp_rail_stack_window(rail_window, z); + +} + +/** + * Sends a RAIL Client Activate order for the given window. + * + * RAIL servers expect explicit activation notifications when the active + * RemoteApp window changes. + * + * Even if the window_id is 64 bits, MS RAIL protocol activation orders use a 32-bit window ID. + * Ignore any invalid window IDs that cannot be represented without truncating to 32-bits. + * * + * @param rdp_client + * The RDP client whose RAIL channel should be used. + * + * @param window_id + * The server-side ID of the window to activate or deactivate. + * + * @param enabled + * Non-zero to activate the window, zero to deactivate it. + * + * @return + * CHANNEL_RC_OK if the activation order was sent successfully, or a + * protocol-specific error code otherwise. + */ +static UINT guac_rdp_rail_send_activate(guac_rdp_client* rdp_client, + UINT64 window_id, BOOL enabled) { + + if (rdp_client->rail_interface == NULL + || rdp_client->rail_interface->ClientActivate == NULL) + return CHANNEL_RC_OK; + + if (window_id > UINT32_MAX) + return CHANNEL_RC_OK; + + RAIL_ACTIVATE_ORDER activate = { + .windowId = (UINT32) window_id, + .enabled = enabled + }; + + pthread_mutex_lock(&(rdp_client->message_lock)); + UINT status = rdp_client->rail_interface->ClientActivate( + rdp_client->rail_interface, &activate); + pthread_mutex_unlock(&(rdp_client->message_lock)); + + return status; + +} + +/** + * Sends the RAIL activation to switch focus to the given window. + * + * The previously active window is first deactivated, followed by activation + * of the requested window. The locally tracked active window ID is updated + * only if the activation request succeeds. + * + * @param rdp_client + * The RDP client whose RAIL channel should be used. + * + * @param window_id + * The server-side ID of the RAIL window that should be activated. + */ +static void guac_rdp_rail_activate_window(guac_rdp_client* rdp_client, + UINT64 window_id) { + + if (rdp_client->rail_active_window_id == window_id) + return; + + if (guac_rdp_rail_send_activate(rdp_client, window_id, TRUE) + == CHANNEL_RC_OK) + rdp_client->rail_active_window_id = window_id; + +} + +void guac_rdp_rail_raise_window_at(guac_rdp_client* rdp_client, + int x, int y) { + + if (!guac_rdp_rail_uses_gfx(rdp_client)) + return; + + if (rdp_client->rail_windows == NULL) + return; + + guac_common_list_lock(rdp_client->rail_windows); + + guac_rdp_rail_window* clicked_window = NULL; + UINT64 activate_window_id = 0; + int clicked_z = 0; + int apply_layer_updates = 0; + int has_no_surface_window = 0; + int highest_no_surface_z = 0; + int suppress_activation = 0; + int has_adjacent_nosurface_window = 0; + + /* Find the topmost painted window under the click, while separately + * tracking visible no-surface windows, which may be a menu + * popups whose drawable surface has not arrived yet. */ + for (guac_common_list_element* current = rdp_client->rail_windows->head; + current != NULL; current = current->next) { + + guac_rdp_rail_window* rail_window = current->data; + if (rail_window == NULL || rail_window->layer == NULL) + continue; + + /* Track the highest visible RAIL window still waiting for its drawable surface, + as it may be a transient popup. */ + if (rail_window->visible && rail_window->has_rail_order + && !rail_window->has_surface) { + + if (!has_no_surface_window + || rail_window->z_order > highest_no_surface_z) { + highest_no_surface_z = rail_window->z_order; + has_no_surface_window = 1; + } + + continue; + } + + /* Verify the window is fully renderable before continuing */ + if (!rail_window->visible || !rail_window->has_surface + || !rail_window->has_rail_order) + continue; + + /* Verify the click is within window borders */ + if (x < rail_window->x || y < rail_window->y + || x >= rail_window->x + rail_window->layer_width + || y >= rail_window->y + rail_window->layer_height) + continue; + + /* Use the topmost matching window */ + if (clicked_window == NULL || rail_window->z_order > clicked_z) { + clicked_window = rail_window; + clicked_z = rail_window->z_order; + } + + } + + if (clicked_window != NULL) { + int z = guac_rdp_rail_get_top_z(rdp_client); + + /* A higher no-surface RAIL window can represent a menu before its + * drawable popup surface arrives. Avoid reactivating the window below + * it, or the menu may close before the click is delivered. */ + if (has_no_surface_window + && highest_no_surface_z > clicked_window->z_order) { + suppress_activation = 1; + } + + /* Right-click context menus may not report an owner window, but in + * that case they're stacked immediately above a visible no-surface + * RAIL window (surface and window are separate). Treat that pairing + * as a menu popup and skip pre-click activation so menu-item clicks + * are not dismissed. */ + if (has_no_surface_window + && highest_no_surface_z == clicked_window->z_order - 1) { + suppress_activation = 1; + has_adjacent_nosurface_window = 1; + } + + /* Regular menu popups are owned windows. Activating them before the + * click can dismiss or disturb the menu selection. */ + if (clicked_window->owner_window_id != 0) { + suppress_activation = 1; + } + + if (!suppress_activation) + activate_window_id = clicked_window->window_id; + + /* Locally raise the clicked window unless server z-order or + transient menu state should take precedence. */ + if (!has_adjacent_nosurface_window + && !clicked_window->in_monitored_desktop_zorder + && clicked_window->z_order < z - 1) { + guac_rdp_rail_stack_window(clicked_window, z); + guac_rdp_rail_raise_owned_windows(rdp_client, + clicked_window->window_id, z + 1); + apply_layer_updates = 1; + rdp_client->gdi_modified = 1; + } + } + + guac_common_list_unlock(rdp_client->rail_windows); + + if (apply_layer_updates) + guac_rdp_rail_apply_window_layers(rdp_client); + + if (activate_window_id != 0) + guac_rdp_rail_activate_window(rdp_client, activate_window_id); + +} + +void guac_rdp_rail_track_mouse_event(guac_rdp_client* rdp_client, + int mask, int x, int y) { + + if (!guac_rdp_rail_uses_gfx(rdp_client)) + return; + + if (rdp_client->rail_windows == NULL) + return; + + guac_common_list_lock(rdp_client->rail_windows); + + guac_rdp_rail_window* target_window = NULL; + + /* Preserve the original mouse-down target while a drag is in progress. */ + if (rdp_client->mouse_button_mask != 0 + && rdp_client->rail_mouse_window_id != 0) + target_window = guac_rdp_rail_get_window(rdp_client, + rdp_client->rail_mouse_window_id); + + if (target_window == NULL) { + + int target_z = 0; + + /* If no drag target is active, find the topmost renderable window + * currently under the pointer. */ + for (guac_common_list_element* current = rdp_client->rail_windows->head; + current != NULL; current = current->next) { + + guac_rdp_rail_window* rail_window = current->data; + if (rail_window == NULL) + continue; + + if (!rail_window->visible || !rail_window->has_surface + || !rail_window->has_rail_order) + continue; + + if (x < rail_window->x || y < rail_window->y + || x >= rail_window->x + rail_window->layer_width + || y >= rail_window->y + rail_window->layer_height) + continue; + + if (target_window == NULL || rail_window->z_order > target_z) { + target_window = rail_window; + target_z = rail_window->z_order; + } + + } + + } + + /* Capture the target on mouse down and release it once all buttons are up. */ + if (target_window != NULL + && rdp_client->mouse_button_mask == 0 && mask != 0) + rdp_client->rail_mouse_window_id = target_window->window_id; + + if (mask == 0) + rdp_client->rail_mouse_window_id = 0; + + guac_common_list_unlock(rdp_client->rail_windows); + +} + +/** + * Returns the RAIL window state having the given ID, creating it if necessary. + * The rail_windows list lock must be held. + * + * @param client + * The Guacamole client associated with the RDP session. + * + * @param window_id + * The server-side ID of the RAIL window to retrieve or create. + * + * @param new_window + * Preallocated window state to use if no matching window exists, or NULL + * if allocation failed. + * + * @param new_element + * Preallocated list element to use if no matching window exists, or NULL + * if allocation failed. + * + * @return + * The matching or newly-created RAIL window state, or NULL if the state + * could not be created. + */ +static guac_rdp_rail_window* guac_rdp_rail_get_or_create_window( + guac_client* client, UINT64 window_id, + guac_rdp_rail_window* new_window, + guac_common_list_element* new_element) { + + guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; + + guac_rdp_rail_window* rail_window = + guac_rdp_rail_get_window(rdp_client, window_id); + + if (rail_window != NULL) + return rail_window; + + if (new_window == NULL || new_element == NULL) + return NULL; + + new_window->window_id = window_id; + new_window->visible = 1; + + new_element->data = new_window; + new_element->next = rdp_client->rail_windows->head; + new_element->_ptr = &(rdp_client->rail_windows->head); + + if (rdp_client->rail_windows->head != NULL) + rdp_client->rail_windows->head->_ptr = &(new_element->next); + + rdp_client->rail_windows->head = new_element; + + return new_window; + +} + +/** + * Ensures the given RAIL window has a display layer. The rail_windows list lock + * must NOT be held. + * + * @param client + * The Guacamole client associated with the RDP session. + * + * @param window_id + * The server-side ID of the RAIL window whose layer should exist. + * + * @return + * Non-zero if a layer was allocated and attached, zero otherwise. + */ +static int guac_rdp_rail_ensure_window_layer( + guac_client* client, UINT64 window_id) { + + guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; + + guac_common_list_lock(rdp_client->rail_windows); + + guac_rdp_rail_window* rail_window = + guac_rdp_rail_get_window(rdp_client, window_id); + + if (rail_window == NULL || rail_window->layer != NULL + || rail_window->layer_pending || rail_window->deleted) { + guac_common_list_unlock(rdp_client->rail_windows); + return 0; + } + + rail_window->layer_pending = 1; + + guac_common_list_unlock(rdp_client->rail_windows); + + guac_display_layer* parent_layer = + guac_rdp_rail_get_window_layer(client); + + guac_display_layer* layer = NULL; + if (parent_layer != NULL) { + layer = guac_display_alloc_layer(rdp_client->display, 1); + + if (layer != NULL) { + guac_display_layer_set_parent(layer, parent_layer); + guac_display_layer_set_opacity(layer, 0); + } + } + + guac_common_list_lock(rdp_client->rail_windows); + + rail_window = guac_rdp_rail_get_window(rdp_client, window_id); + int attached = 0; + + if (rail_window != NULL && !rail_window->deleted + && rail_window->layer == NULL && layer != NULL) { + rail_window->layer = layer; + rail_window->layer_pending = 0; + layer = NULL; + attached = 1; + } + else if (rail_window != NULL) + rail_window->layer_pending = 0; + + guac_common_list_unlock(rdp_client->rail_windows); + + if (layer != NULL) + guac_display_layer_set_opacity(layer, 0); + + return attached; + +} + +/** + * Updates the visibility of the Guacamole layer backing the given RAIL window. + * The rail_windows list lock must be held. + * + * @param rail_window + * The RAIL window whose backing Guacamole layer should have its opacity updated. + */ +static void guac_rdp_rail_update_window_opacity( + guac_rdp_client* rdp_client, + guac_rdp_rail_window* rail_window) { + + (void) rail_window; + rdp_client->gdi_modified = 1; + +} + +/** + * Applies all tracked RAIL window layer state to the corresponding Guacamole + * display layers. The rail_windows list lock must NOT be held. + * + * @param rdp_client + * The RDP client whose tracked RAIL window layers should be updated. + */ +static void guac_rdp_rail_apply_window_layers( + guac_rdp_client* rdp_client) { + + if (rdp_client->rail_windows == NULL) + return; + + guac_common_list_lock(rdp_client->rail_windows); + + size_t window_count = 0; + for (guac_common_list_element* current = rdp_client->rail_windows->head; + current != NULL; current = current->next) { + if (current->data != NULL) + window_count++; + } + + guac_common_list_unlock(rdp_client->rail_windows); + + if (window_count == 0) + return; + + guac_rdp_rail_layer_update* updates = + guac_mem_zalloc(sizeof(guac_rdp_rail_layer_update), window_count); + + if (updates == NULL) + return; + + guac_common_list_lock(rdp_client->rail_windows); + + size_t update_count = 0; + for (guac_common_list_element* current = rdp_client->rail_windows->head; + current != NULL && update_count < window_count; + current = current->next) { + + guac_rdp_rail_window* rail_window = current->data; + if (rail_window == NULL || rail_window->layer == NULL) + continue; + + int opacity = (rail_window->visible + && rail_window->has_surface + && rail_window->has_rail_order) ? 0xFF : 0; + + updates[update_count++] = (guac_rdp_rail_layer_update) { + .layer = rail_window->layer, + .alpha_layer = rail_window->alpha_layer, + .x = rail_window->x, + .y = rail_window->y, + .z_order = rail_window->z_order, + .opacity = opacity, + .layer_alpha = rail_window->layer_alpha + }; + + } + + guac_common_list_unlock(rdp_client->rail_windows); + + for (size_t i = 0; i < update_count; i++) { + + guac_rdp_rail_layer_update* update = &updates[i]; + + guac_display_layer_move(update->layer, update->x, update->y); + guac_display_layer_stack(update->layer, update->z_order); + guac_display_layer_set_opacity(update->layer, + update->layer_alpha ? 0 : update->opacity); + + if (update->alpha_layer != NULL) { + guac_display_layer_move(update->alpha_layer, + update->x, update->y); + guac_display_layer_stack(update->alpha_layer, + update->z_order); + guac_display_layer_set_opacity(update->alpha_layer, + update->layer_alpha ? update->opacity : 0); + } + + } + + guac_mem_free(updates); + +} + +/** + * Marks the full contents of the given display layer dirty so the next + * Guacamole frame flush repaints it. This is used when a window becomes + * visible or receives its first authoritative RAIL order after surface data + * has already arrived. + * + * @param rdp_client + * The RDP client whose frame state should be marked modified. + * + * @param layer + * The display layer that should be repainted. + */ +static void guac_rdp_rail_mark_layer_dirty(guac_rdp_client* rdp_client, + guac_display_layer* layer) { + + guac_display_layer_raw_context* layer_context = + guac_display_layer_open_raw(layer); + + if (layer_context == NULL) + return; + + guac_rect_extend(&layer_context->dirty, &layer_context->bounds); + guac_display_layer_close_raw(layer, layer_context); + + rdp_client->gdi_modified = 1; + +} + +#ifdef HAVE_RDPGFX_SURFACE_AREA_UPDATE +/** + * Repaints the full contents of the given mapped RDPGFX surface through the + * RAIL paint path. This is used when a surface was known before the + * corresponding RAIL order made the window visible/renderable. + * + * @param client + * The Guacamole client associated with the current RDP session. + * + * @param surface_id + * The server-side ID of the RDPGFX surface that should be repainted. + * + * @return + * CHANNEL_RC_OK if the repaint was ignored or completed successfully, or + * an error from the shared surface-paint path otherwise. + */ +static UINT guac_rdp_rail_repaint_surface(guac_client* client, + UINT16 surface_id) { + + guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; + RdpgfxClientContext* rdpgfx = rdp_client->rdpgfx_interface; + + if (rdpgfx == NULL || rdpgfx->GetSurfaceData == NULL) + return CHANNEL_RC_OK; + + gdiGfxSurface* surface = + (gdiGfxSurface*) rdpgfx->GetSurfaceData(rdpgfx, surface_id); + + if (surface == NULL) + return CHANNEL_RC_OK; + + UINT32 width = surface->mappedWidth > 0 + ? surface->mappedWidth : surface->width; + UINT32 height = surface->mappedHeight > 0 + ? surface->mappedHeight : surface->height; + + if (width == 0 || height == 0 + || width > UINT16_MAX || height > UINT16_MAX) + return CHANNEL_RC_OK; + + RECTANGLE_16 full_rect = { + .left = 0, + .top = 0, + .right = (UINT16) width, + .bottom = (UINT16) height + }; + + return guac_rdp_rail_paint_surface(client, surface, 1, &full_rect); + +} +#endif + +/** + * Paints an opaque black background into the default desktop layer once + * RemoteApp windows are about to become visible. This prevents transparent or + * uninitialized pixels from showing through behind per-window RAIL layers. + * + * @param rdp_client + * The RDP client whose default display layer should be initialized. + */ +static void guac_rdp_rail_paint_background(guac_rdp_client* rdp_client) { + + if (rdp_client->rail_background_painted) + return; + + guac_display_layer* default_layer = + guac_display_default_layer(rdp_client->display); + + guac_display_layer_raw_context* bg_ctx = + guac_display_layer_open_raw(default_layer); + + if (bg_ctx == NULL) + return; + + guac_rect full_bounds = bg_ctx->bounds; + int height = guac_rect_height(&full_bounds); + int width = guac_rect_width(&full_bounds); + + unsigned char* row = + GUAC_DISPLAY_LAYER_RAW_BUFFER(bg_ctx, full_bounds); + + for (int y = 0; y < height; y++) { + unsigned char* pixel = row; + for (int x = 0; x < width; x++) { + pixel[0] = 0x00; + pixel[1] = 0x00; + pixel[2] = 0x00; + pixel[3] = 0xFF; + pixel += 4; + } + row += bg_ctx->stride; + } + + guac_rect_extend(&bg_ctx->dirty, &full_bounds); + guac_display_layer_close_raw(default_layer, bg_ctx); + + rdp_client->rail_background_painted = 1; + rdp_client->gdi_modified = 1; + +} + +/** + * Frees the resources associated with the given RAIL window. + * + * @param data + * The RAIL window to free. + */ +static void guac_rdp_rail_free_window_data(void* data) { + + guac_rdp_rail_window* rail_window = data; + if (rail_window == NULL) + return; + + if (rail_window->layer != NULL) + guac_display_free_layer(rail_window->layer); + + if (rail_window->alpha_layer != NULL) + guac_display_free_layer(rail_window->alpha_layer); + + guac_mem_free(rail_window); +} + +#ifdef HAVE_RDPGFX_SURFACE_AREA_UPDATE +/** + * Forces the alpha channel of all pixels within the given rectangle of the + * given raw display layer context to fully opaque. + * + * @param context + * The raw display layer context whose pixels should be made opaque. + * + * @param rect + * The rectangle of pixels that should be made opaque. + */ +static void guac_rdp_rail_force_opaque( + guac_display_layer_raw_context* context, const guac_rect* rect) { + + int width = guac_rect_width(rect); + int height = guac_rect_height(rect); + + unsigned char* row = + GUAC_DISPLAY_LAYER_RAW_BUFFER(context, *rect); + + for (int y = 0; y < height; y++) { + + uint32_t* pixel = (uint32_t*) row; + + for (int x = 0; x < width; x++) + pixel[x] |= 0xFF000000; + + row += context->stride; + + } + +} + +/** + * Releases one active paint reference for the given RAIL window. + * + * Paint operations temporarily outlive membership in the rail_windows list, so + * deleted windows are freed only after their last outstanding paint completes. + * The rail_windows list lock must be held. If this returns non-zero, the + * caller must free the window after releasing the list lock. + * + * @param rail_window + * The tracked RAIL window whose outstanding paint count should be + * decremented. + * + * @return + * Non-zero if the window has been deleted and no more paint references + * remain, meaning the caller should free it after unlocking the list. + */ +static int guac_rdp_rail_release_paint_ref( + guac_rdp_rail_window* rail_window) { + + rail_window->paint_refs--; + + return rail_window->deleted && rail_window->paint_refs == 0; + +} +#endif + +/** + * Callback invoked if the RDP server requests client-side local movement or + * resizing of a RemoteApp window. + * + * @param rail + * The RailClientContext structure used by FreeRDP to handle the RAIL + * channel for the current RDP session. + * + * @param localMoveSize + * The local move/size request sent by the server. + * + * @return + * CHANNEL_RC_OK (zero) as the order has been ignored successfully. + */ +static UINT guac_rdp_rail_local_move_size(RailClientContext* rail, + const RAIL_LOCALMOVESIZE_ORDER* localMoveSize) { + + guac_client* client = (guac_client*) rail->custom; + guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; + + if (localMoveSize == NULL) { + guac_client_log(client, GUAC_LOG_DEBUG, + "RAIL local move/size ignored: null order."); + return CHANNEL_RC_OK; + } + + if (rail->ClientWindowMove == NULL) + return CHANNEL_RC_OK; + + if (localMoveSize->windowId > UINT32_MAX) + return CHANNEL_RC_OK; + + guac_common_list_lock(rdp_client->rail_windows); + + guac_rdp_rail_window* rail_window = + guac_rdp_rail_get_window(rdp_client, localMoveSize->windowId); + + RAIL_WINDOW_MOVE_ORDER windowMove = { 0 }; + int send_move = 0; + + if (rail_window != NULL) { + + int width = rail_window->order_width > 0 + ? rail_window->order_width : rail_window->layer_width; + int height = rail_window->order_height > 0 + ? rail_window->order_height : rail_window->layer_height; + + int right = rail_window->x + width; + int bottom = rail_window->y + height; + + if (width > 0 && height > 0 + && rail_window->x >= INT16_MIN && rail_window->x <= INT16_MAX + && rail_window->y >= INT16_MIN && rail_window->y <= INT16_MAX + && right >= INT16_MIN && right <= INT16_MAX + && bottom >= INT16_MIN && bottom <= INT16_MAX) { + windowMove.windowId = (UINT32) localMoveSize->windowId; + windowMove.left = (INT16) rail_window->x; + windowMove.top = (INT16) rail_window->y; + windowMove.right = (INT16) right; + windowMove.bottom = (INT16) bottom; + send_move = 1; + } + + } + + guac_common_list_unlock(rdp_client->rail_windows); + + if (!send_move) { + guac_client_log(client, GUAC_LOG_DEBUG, + "RAIL local move/size completion skipped: " + "window=0x%08x has no usable bounds.", + (unsigned int) localMoveSize->windowId); + return CHANNEL_RC_OK; + } + + pthread_mutex_lock(&(rdp_client->message_lock)); + UINT status = rail->ClientWindowMove(rail, &windowMove); + pthread_mutex_unlock(&(rdp_client->message_lock)); + + return status; + +} + +/** + * A callback function that is executed when an update for a RAIL window is + * received from the RDP server. + * + * @param context + * A pointer to the rdpContext structure used by FreeRDP to handle the + * window update. + * + * @param orderInfo + * A pointer to the data structure that contains information about what + * window was updated what updates were performed. + * + * @param windowState + * A pointer to the data structure that contains details of the updates + * to the window, as indicated by flags in the orderInfo field. + * + * @return + * TRUE if the client-side processing of the updates as successful; otherwise + * FALSE. This implementation always returns TRUE. + */ +static BOOL guac_rdp_rail_window_update(rdpContext* context, + RAIL_CONST WINDOW_ORDER_INFO* orderInfo, + RAIL_CONST WINDOW_STATE_ORDER* windowState) { + + guac_client* client = ((rdp_freerdp_context*) context)->client; + guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; + + UINT32 fieldFlags = orderInfo->fieldFlags; + +#ifdef HAVE_RDPGFX_SURFACE_AREA_UPDATE + int repaint_surface = 0; + UINT16 repaint_surface_id = 0; +#endif + int paint_background = 0; + int apply_layer_updates = 0; + int ensure_window_layer = 0; + guac_display_layer* dirty_layer = NULL; + + /* Track window location and visibility for RDPGFX RemoteApp rendering. */ + if (guac_rdp_rail_uses_gfx(rdp_client)) { + + guac_rdp_rail_window* new_window = + guac_mem_zalloc(sizeof(guac_rdp_rail_window)); + guac_common_list_element* new_element = + guac_mem_alloc(sizeof(guac_common_list_element)); + + guac_common_list_lock(rdp_client->rail_windows); + + guac_rdp_rail_window* rail_window = + guac_rdp_rail_get_or_create_window(client, orderInfo->windowId, + new_window, new_element); + + if (rail_window == new_window) { + new_window = NULL; + new_element = NULL; + } + + if (rail_window != NULL) { + + ensure_window_layer = rail_window->layer == NULL; + + int had_rail_order = rail_window->has_rail_order; +#ifdef HAVE_RDPGFX_SURFACE_AREA_UPDATE + int was_visible = rail_window->visible; +#endif + int needs_opacity_update = !had_rail_order; + rail_window->has_rail_order = 1; + int needs_raise = 0; + + if (fieldFlags & WINDOW_ORDER_FIELD_OWNER) { + rail_window->owner_window_id = windowState->ownerWindowId; + needs_raise = 1; + } + + if (fieldFlags & WINDOW_ORDER_FIELD_WND_OFFSET) { + rail_window->x = windowState->windowOffsetX; + rail_window->y = windowState->windowOffsetY; + apply_layer_updates = 1; + rdp_client->gdi_modified = 1; + } + + if (fieldFlags & WINDOW_ORDER_FIELD_WND_SIZE) { + rail_window->order_width = windowState->windowWidth; + rail_window->order_height = windowState->windowHeight; + } + + if (fieldFlags & WINDOW_ORDER_FIELD_CLIENT_AREA_OFFSET) { + rail_window->client_x = windowState->clientOffsetX; + rail_window->client_y = windowState->clientOffsetY; + } + + if (fieldFlags & WINDOW_ORDER_FIELD_CLIENT_AREA_SIZE) { + rail_window->client_width = windowState->clientAreaWidth; + rail_window->client_height = windowState->clientAreaHeight; + rail_window->has_client_area = + rail_window->client_width > 0 + && rail_window->client_height > 0; + } + + if (fieldFlags & WINDOW_ORDER_FIELD_SHOW) { + rail_window->visible = + windowState->showState != GUAC_RDP_RAIL_WINDOW_STATE_HIDDEN + && windowState->showState != GUAC_RDP_RAIL_WINDOW_STATE_MINIMIZED; + needs_opacity_update = 1; + } + + if (fieldFlags & WINDOW_ORDER_FIELD_ENFORCE_SERVER_ZORDER) + needs_raise = 1; + + if (needs_raise) { + guac_rdp_rail_raise_window_above_owner(rdp_client, + rail_window); + apply_layer_updates = 1; + rdp_client->gdi_modified = 1; + } + + if (rail_window->has_rail_order && rail_window->visible) + paint_background = 1; + + if (rail_window->has_surface && rail_window->has_rail_order + && rail_window->visible) { + + if (!had_rail_order || needs_opacity_update) + dirty_layer = rail_window->layer_alpha + && rail_window->alpha_layer != NULL + ? rail_window->alpha_layer + : rail_window->layer; + + if (!had_rail_order && rail_window->visible + && rail_window->owner_window_id == 0 + && !rail_window->in_monitored_desktop_zorder) { + int z = guac_rdp_rail_get_top_z(rdp_client); + guac_rdp_rail_stack_window(rail_window, z); + apply_layer_updates = 1; + rdp_client->gdi_modified = 1; + } + } + + if (needs_opacity_update) { + guac_rdp_rail_update_window_opacity(rdp_client, rail_window); + apply_layer_updates = 1; + rdp_client->gdi_modified = 1; + } + +#ifdef HAVE_RDPGFX_SURFACE_AREA_UPDATE + if (rail_window->visible && rail_window->has_surface_id + && (!rail_window->has_surface || !was_visible)) { + repaint_surface = 1; + repaint_surface_id = rail_window->surface_id; + } +#endif + + } + + guac_common_list_unlock(rdp_client->rail_windows); + + if (ensure_window_layer && guac_rdp_rail_ensure_window_layer( + client, orderInfo->windowId)) + apply_layer_updates = 1; + + if (apply_layer_updates) + guac_rdp_rail_apply_window_layers(rdp_client); + + if (dirty_layer != NULL) + guac_rdp_rail_mark_layer_dirty(rdp_client, dirty_layer); + +#ifdef HAVE_RDPGFX_SURFACE_AREA_UPDATE + if (repaint_surface) + guac_rdp_rail_repaint_surface(client, repaint_surface_id); +#endif + + if (paint_background) + guac_rdp_rail_paint_background(rdp_client); + + guac_mem_free(new_window); + guac_mem_free(new_element); + + } + + /* If the flag for window visibilty is set, check visibility. */ + if (fieldFlags & WINDOW_ORDER_FIELD_SHOW) { + guac_client_log(client, GUAC_LOG_TRACE, "RAIL window visibility change: %d", windowState->showState); + + /* State is either hidden or minimized - send restore command. */ + if (windowState->showState == GUAC_RDP_RAIL_WINDOW_STATE_MINIMIZED) { + + guac_client_log(client, GUAC_LOG_DEBUG, "RAIL window minimized, sending restore command."); + + RAIL_SYSCOMMAND_ORDER syscommand; + syscommand.windowId = orderInfo->windowId; + syscommand.command = SC_RESTORE; + rdp_client->rail_interface->ClientSystemCommand(rdp_client->rail_interface, &syscommand); + } + } + + return TRUE; + +} + +/** + * Handler for the RAIL WindowDelete order. Removes any tracked RAIL window + * with the given ID. + * + * @param context + * A pointer to the rdpContext struct associated with the current RDP connection. + * + * @param orderInfo + * A pointer to the struct containing information about the window that was deleted. + * + * @return + * TRUE if the window deletion was handled successfully, otherwise FALSE. + */ +static BOOL guac_rdp_rail_window_delete(rdpContext* context, + RAIL_CONST WINDOW_ORDER_INFO* orderInfo) { + + guac_client* client = ((rdp_freerdp_context*) context)->client; + guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; + guac_rdp_rail_window* free_window = NULL; + + if (!guac_rdp_rail_uses_gfx(rdp_client)) + return TRUE; + + guac_common_list_lock(rdp_client->rail_windows); + + guac_common_list_element* element = + guac_rdp_rail_get_window_element(rdp_client, orderInfo->windowId); + + if (element != NULL) { + guac_rdp_rail_window* rail_window = element->data; + + if (rdp_client->rail_active_window_id == rail_window->window_id) + rdp_client->rail_active_window_id = 0; + + guac_common_list_remove(rdp_client->rail_windows, element); + rail_window->deleted = 1; + + if (rail_window->paint_refs == 0) + free_window = rail_window; + + rdp_client->gdi_modified = 1; + } + + guac_common_list_unlock(rdp_client->rail_windows); + + guac_rdp_rail_free_window_data(free_window); + + return TRUE; + +} + +/** + * Handler for RAIL monitored desktop orders. These orders include the + * server-side z-order of RemoteApp windows as well setting active window. + * + * @param context + * A pointer to the rdpContext struct associated with the current RDP connection. + * + * @param orderInfo + * A pointer to the struct containing desktop order flags. + * + * @param monitoredDesktop + * A pointer to the monitored desktop state sent by the server. + * + * @return + * TRUE if the desktop order was handled successfully, otherwise FALSE. + */ +static BOOL guac_rdp_rail_monitored_desktop(rdpContext* context, + RAIL_CONST WINDOW_ORDER_INFO* orderInfo, + RAIL_CONST MONITORED_DESKTOP_ORDER* monitoredDesktop) { + + guac_client* client = ((rdp_freerdp_context*) context)->client; + guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; + + if (!guac_rdp_rail_uses_gfx(rdp_client)) + return TRUE; + + if (!(orderInfo->fieldFlags & (WINDOW_ORDER_FIELD_DESKTOP_ZORDER + | WINDOW_ORDER_FIELD_DESKTOP_ACTIVE_WND))) + return TRUE; + + int paint_background = 0; + int apply_layer_updates = 0; + + guac_common_list_lock(rdp_client->rail_windows); + + if ((orderInfo->fieldFlags & WINDOW_ORDER_FIELD_DESKTOP_ZORDER) + && monitoredDesktop->windowIds != NULL) { + + for (guac_common_list_element* current = rdp_client->rail_windows->head; + current != NULL; current = current->next) { + guac_rdp_rail_window* rail_window = current->data; + if (rail_window != NULL) + rail_window->in_monitored_desktop_zorder = 0; + } + + /* The server-provided list is authoritative for visual stacking. */ + guac_client_log(client, GUAC_LOG_TRACE, + "RAIL monitored desktop z-order: count=%u.", + (unsigned int) monitoredDesktop->numWindowIds); + + for (UINT32 i = 0; i < monitoredDesktop->numWindowIds; i++) { + + guac_rdp_rail_window* rail_window = + guac_rdp_rail_get_window(rdp_client, + monitoredDesktop->windowIds[i]); + + int z = (int) (monitoredDesktop->numWindowIds - i); + + if (rail_window != NULL && rail_window->has_rail_order) { + rail_window->in_monitored_desktop_zorder = 1; + guac_rdp_rail_stack_window(rail_window, z); + guac_rdp_rail_raise_owned_windows(rdp_client, + rail_window->window_id, z + 1); + apply_layer_updates = 1; + rdp_client->gdi_modified = 1; + } + + } + + } + + if ((orderInfo->fieldFlags & WINDOW_ORDER_FIELD_DESKTOP_ACTIVE_WND) + && monitoredDesktop->activeWindowId != 0 + && monitoredDesktop->activeWindowId != 0xFFFFFFFFU) { + + guac_client_log(client, GUAC_LOG_TRACE, + "RAIL monitored desktop active window: window=0x%08x.", + (unsigned int) monitoredDesktop->activeWindowId); + + /* + * Treat the server's active-window update as authoritative even when + * the matching RAIL order has not arrived yet. Some owned popups are + * activated before their window order, and re-sending activation on + * click can dismiss them before the click reaches the intended item. + * Rendering/stacking still waits for the RAIL order. + */ + rdp_client->rail_active_window_id = monitoredDesktop->activeWindowId; + + guac_rdp_rail_window* active_window = + guac_rdp_rail_get_window(rdp_client, + monitoredDesktop->activeWindowId); + + if (active_window != NULL) { + + if (active_window->has_rail_order + && active_window->has_surface && active_window->visible) + paint_background = 1; + + if (active_window->has_rail_order + && !active_window->in_monitored_desktop_zorder) { + int z = guac_rdp_rail_get_top_z(rdp_client); + guac_rdp_rail_stack_window(active_window, z); + guac_rdp_rail_raise_owned_windows(rdp_client, + active_window->window_id, z + 1); + apply_layer_updates = 1; + rdp_client->gdi_modified = 1; + } + } + + } + + guac_common_list_unlock(rdp_client->rail_windows); + + if (apply_layer_updates) + guac_rdp_rail_apply_window_layers(rdp_client); + + if (paint_background) + guac_rdp_rail_paint_background(rdp_client); + + return TRUE; + +} + +#ifdef HAVE_RDPGFX_SURFACE_AREA_UPDATE +UINT guac_rdp_rail_map_window_for_surface(RdpgfxClientContext* context, + UINT16 surface_id, UINT64 window_id) { + + if (context == NULL) + return CHANNEL_RC_OK; + + rdpGdi* gdi = (rdpGdi*) context->custom; + if (gdi != NULL && gdi->context != NULL) { + guac_client* client = ((rdp_freerdp_context*) gdi->context)->client; + guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; + + guac_rdp_rail_window* new_window = + guac_mem_zalloc(sizeof(guac_rdp_rail_window)); + guac_common_list_element* new_element = + guac_mem_alloc(sizeof(guac_common_list_element)); + + guac_common_list_lock(rdp_client->rail_windows); + + guac_rdp_rail_window* old_window = + guac_rdp_rail_get_window_for_surface(rdp_client, surface_id); + + if (old_window != NULL) { + old_window->has_surface_id = 0; + } + + guac_rdp_rail_window* rail_window = + guac_rdp_rail_get_or_create_window(client, window_id, + new_window, new_element); + + if (rail_window == new_window) { + new_window = NULL; + new_element = NULL; + } + + int repaint_surface = 0; + int ensure_window_layer = 0; + + if (rail_window != NULL) { + ensure_window_layer = rail_window->layer == NULL; + + /* Surface mappings can precede RAIL window orders. Track the + * surface now, but keep the layer hidden until the order arrives. */ + rail_window->surface_id = surface_id; + rail_window->has_surface_id = 1; + repaint_surface = rail_window->visible + && rail_window->has_rail_order + && !rail_window->has_surface; + guac_client_log(client, GUAC_LOG_TRACE, + "RAIL GFX surface mapped: surface=%u window=0x%08x " + "has-order=%i visible=%i.", + (unsigned int) surface_id, (unsigned int) window_id, + rail_window->has_rail_order, rail_window->visible); + } + else { + guac_client_log(client, GUAC_LOG_TRACE, + "RAIL GFX surface map ignored: surface=%u " + "window=0x%08x could not be tracked.", + (unsigned int) surface_id, (unsigned int) window_id); + } + + guac_common_list_unlock(rdp_client->rail_windows); + + if (ensure_window_layer) + guac_rdp_rail_ensure_window_layer(client, window_id); + +#ifdef HAVE_RDPGFX_SURFACE_AREA_UPDATE + if (repaint_surface) + guac_rdp_rail_repaint_surface(client, surface_id); +#endif + + guac_mem_free(new_window); + guac_mem_free(new_element); + } + + return CHANNEL_RC_OK; + +} + +UINT guac_rdp_rail_unmap_window_for_surface(RdpgfxClientContext* context, + UINT64 window_id) { + + if (context == NULL) + return CHANNEL_RC_OK; + + rdpGdi* gdi = (rdpGdi*) context->custom; + if (gdi != NULL && gdi->context != NULL) { + guac_client* client = ((rdp_freerdp_context*) gdi->context)->client; + guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; + + guac_common_list_lock(rdp_client->rail_windows); + + guac_rdp_rail_window* rail_window = + guac_rdp_rail_get_window(rdp_client, window_id); + + if (rail_window != NULL) + rail_window->has_surface_id = 0; + + guac_common_list_unlock(rdp_client->rail_windows); + } + + return CHANNEL_RC_OK; + +} + +/** + * Copies updated regions from the given RDPGFX surface to the Guacamole layer + * associated with that surface's RAIL window. The rail_windows list lock must NOT + * be held on entry; this function takes it internally. + * + * The invalidRegion of the surface is cleared only after a successful paint. + * FreeRDP's GDI pipeline owns that region and may consume or clear it independently. + * Deferred surfaces keep their invalid region so FreeRDP retries them after + * the matching RAIL window order arrives. + * + * @param client + * The Guacamole client associated with the RDP session. + * + * @param surface + * The RDPGFX surface containing the updated window contents. + * + * @param rect_count + * The number of rectangles within the rects array. + * + * @param rects + * The updated rectangular regions of the surface to copy. + * + * @return + * CHANNEL_RC_OK if the update was painted, ignored, or deferred + * successfully. ERROR_INTERNAL_ERROR if copying or scaling the surface + * data failed. + */ +static UINT guac_rdp_rail_paint_surface(guac_client* client, + gdiGfxSurface* surface, UINT32 rect_count, const RECTANGLE_16* rects) { + + if (surface == NULL) + return CHANNEL_RC_OK; + + if (surface->windowId == 0) { + guac_client_log(client, GUAC_LOG_DEBUG, + "RAIL GFX surface ignored: surface=%u has no window id.", + (unsigned int) surface->surfaceId); + return CHANNEL_RC_OK; + } + + if (rects == NULL || rect_count == 0) + return CHANNEL_RC_OK; + + guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; + int apply_layer_updates = 0; + + guac_common_list_lock(rdp_client->rail_windows); + + guac_rdp_rail_window* rail_window = + guac_rdp_rail_get_window(rdp_client, surface->windowId); + + if (rail_window == NULL) + rail_window = + guac_rdp_rail_get_window_for_surface(rdp_client, + surface->surfaceId); + + if (rail_window == NULL) { + guac_common_list_unlock(rdp_client->rail_windows); + return CHANNEL_RC_OK; + } + + rail_window->surface_id = surface->surfaceId; + rail_window->has_surface_id = 1; + + /* FreeRDP (gdi/gfx.c, xf_gfx.c) initializes mappedWidth/Height and + * outputTargetWidth/Height to identical values when a surface is created. + * They only differ if a MapSurfaceToScaledWindow order explicitly rescales + * the surface. Use an unscaled copy for the common 1:1 case. + * + * Size the layer from outputTargetWidth/Height (client surface), not + * RAIL order_width/height (we exclude borders/title bar), + * avoiding unnecessary freerdp_image_scale() calls. */ + int source_width = surface->mappedWidth; + int source_height = surface->mappedHeight; + int target_width = surface->outputTargetWidth; + int target_height = surface->outputTargetHeight; + + if (target_width <= 0) + target_width = source_width; + if (target_height <= 0) + target_height = source_height; + + /* Size the Guacamole layer to mappedWidth/Height, not + * outputTargetWidth/Height. The latter may include server-side DPI scaling, + * causing click coordinate drift because input is relative to the source + * pixel size. Guacamole applies display scaling independently. */ + int layer_width = source_width; + int layer_height = source_height; + + int preserve_alpha = surface->format == PIXEL_FORMAT_BGRA32; + + if (source_width <= 0 || source_height <= 0 + || target_width <= 0 || target_height <= 0 + || layer_width <= 0 || layer_height <= 0) { + rail_window->has_surface = 0; + guac_rdp_rail_update_window_opacity(rdp_client, rail_window); + guac_common_list_unlock(rdp_client->rail_windows); + return CHANNEL_RC_OK; + } + + rail_window->surface_width = source_width; + rail_window->surface_height = source_height; + + if (!rail_window->visible) { + rail_window->has_deferred_surface = 1; + guac_common_list_unlock(rdp_client->rail_windows); + return CHANNEL_RC_OK; + } + + int first_surface = !rail_window->has_surface; + + int scaled = (source_width != layer_width + || source_height != layer_height); + + if (!rail_window->has_rail_order) { + rail_window->has_deferred_surface = 1; + guac_common_list_unlock(rdp_client->rail_windows); + return CHANNEL_RC_OK; + } + + if (rail_window->layer == NULL) { + rail_window->has_deferred_surface = 1; + guac_common_list_unlock(rdp_client->rail_windows); + + if (guac_rdp_rail_ensure_window_layer(client, surface->windowId)) + return guac_rdp_rail_repaint_surface(client, surface->surfaceId); + + return CHANNEL_RC_OK; + } + + guac_display_layer* layer = rail_window->layer; + apply_layer_updates = rail_window->layer_alpha != preserve_alpha; + rail_window->layer_alpha = preserve_alpha; + + if (preserve_alpha) { + layer = guac_rdp_rail_get_alpha_layer(client, rail_window); + if (layer == NULL) { + guac_client_log(client, GUAC_LOG_WARNING, "RAIL window 0x%08x " + "could not allocate alpha layer for surface format 0x%08x.", + (unsigned int) rail_window->window_id, + (unsigned int) surface->format); + rail_window->layer_alpha = 0; + layer = rail_window->layer; + apply_layer_updates = 1; + } + } + + int old_layer_width = rail_window->layer_width; + int old_layer_height = rail_window->layer_height; + int needs_resize = old_layer_width != layer_width + || old_layer_height != layer_height; + + rail_window->paint_refs++; + + guac_common_list_unlock(rdp_client->rail_windows); + + if (apply_layer_updates) + guac_rdp_rail_apply_window_layers(rdp_client); + + if (needs_resize) { + guac_display_layer_resize(rail_window->layer, + layer_width, layer_height); + + if (rail_window->alpha_layer != NULL) + guac_display_layer_resize(rail_window->alpha_layer, + layer_width, layer_height); + } + + guac_display_layer_raw_context* layer_context = + guac_display_layer_open_raw(layer); + + if (layer_context == NULL) { + guac_rdp_rail_window* free_window = NULL; + + guac_common_list_lock(rdp_client->rail_windows); + if (guac_rdp_rail_release_paint_ref(rail_window)) + free_window = rail_window; + guac_common_list_unlock(rdp_client->rail_windows); + + guac_rdp_rail_free_window_data(free_window); + return CHANNEL_RC_OK; + } + + guac_rect source_bounds; + guac_rect_init(&source_bounds, 0, 0, source_width, source_height); + + RECTANGLE_16 full_rect = { + .left = 0, + .top = 0, + .right = source_width, + .bottom = source_height + }; + + const RECTANGLE_16* paint_rects = rects; + UINT32 paint_rect_count = rect_count; + + if (first_surface) { + paint_rects = &full_rect; + paint_rect_count = 1; + } + + /* Preserve alpha only for RDPGFX surfaces whose pixel format explicitly + * includes alpha. Other formats may leave the high byte undefined. */ + UINT32 dst_format = guac_rdp_get_native_pixel_format(preserve_alpha); + int copied = 0; + + for (UINT32 i = 0; i < paint_rect_count; i++) { + + const RECTANGLE_16* rect = &paint_rects[i]; + guac_rect src_rect; + guac_rect_init(&src_rect, rect->left, rect->top, + rect->right - rect->left, rect->bottom - rect->top); + + guac_rect_constrain(&src_rect, &source_bounds); + if (guac_rect_is_empty(&src_rect)) + continue; + + if (scaled && !first_surface) { + src_rect.left -= 2; + src_rect.top -= 2; + src_rect.right += 2; + src_rect.bottom += 2; + guac_rect_constrain(&src_rect, &source_bounds); + } + + guac_rect dst_rect; + if (scaled) { + dst_rect.left = + ((UINT64) src_rect.left * layer_width) / source_width; + dst_rect.top = + ((UINT64) src_rect.top * layer_height) / source_height; + dst_rect.right = + (((UINT64) src_rect.right * layer_width) + + source_width - 1) / source_width; + dst_rect.bottom = + (((UINT64) src_rect.bottom * layer_height) + + source_height - 1) / source_height; + } + else { + dst_rect = src_rect; + } + + guac_rect_constrain(&dst_rect, &layer_context->bounds); + if (guac_rect_is_empty(&dst_rect)) + continue; + + BOOL success; + if (!scaled) + success = freerdp_image_copy_no_overlap( + GUAC_DISPLAY_LAYER_RAW_BUFFER(layer_context, dst_rect), + dst_format, + layer_context->stride, + 0, 0, + guac_rect_width(&dst_rect), + guac_rect_height(&dst_rect), + surface->data, surface->format, surface->scanline, + src_rect.left, src_rect.top, + NULL, FREERDP_FLIP_NONE); + else + success = freerdp_image_scale( + GUAC_DISPLAY_LAYER_RAW_BUFFER(layer_context, dst_rect), + dst_format, + layer_context->stride, + 0, 0, + guac_rect_width(&dst_rect), + guac_rect_height(&dst_rect), + surface->data, surface->format, surface->scanline, + src_rect.left, src_rect.top, + guac_rect_width(&src_rect), + guac_rect_height(&src_rect)); + + if (!success) { + guac_rdp_rail_window* free_window = NULL; + + guac_client_log(client, GUAC_LOG_WARNING, "RAIL GFX paint failed"); + guac_display_layer_close_raw(layer, layer_context); + + guac_common_list_lock(rdp_client->rail_windows); + if (guac_rdp_rail_release_paint_ref(rail_window)) + free_window = rail_window; + guac_common_list_unlock(rdp_client->rail_windows); + + guac_rdp_rail_free_window_data(free_window); + return ERROR_INTERNAL_ERROR; + } + + if (!preserve_alpha) + guac_rdp_rail_force_opaque(layer_context, &dst_rect); + + guac_rect_extend(&layer_context->dirty, &dst_rect); + copied = 1; + + } + + guac_display_layer_close_raw(layer, layer_context); + + if (!copied) { + guac_rdp_rail_window* free_window = NULL; + + guac_common_list_lock(rdp_client->rail_windows); + if (!rail_window->deleted) { + rail_window->layer_width = layer_width; + rail_window->layer_height = layer_height; + } + + if (guac_rdp_rail_release_paint_ref(rail_window)) + free_window = rail_window; + guac_common_list_unlock(rdp_client->rail_windows); + + guac_rdp_rail_free_window_data(free_window); + return CHANNEL_RC_OK; + } + + region16_clear(&surface->invalidRegion); + + guac_common_list_lock(rdp_client->rail_windows); + + if (rail_window->deleted) { + guac_rdp_rail_window* free_window = NULL; + + if (guac_rdp_rail_release_paint_ref(rail_window)) + free_window = rail_window; + guac_common_list_unlock(rdp_client->rail_windows); + + guac_rdp_rail_free_window_data(free_window); + return CHANNEL_RC_OK; + } + + rail_window->layer_width = layer_width; + rail_window->layer_height = layer_height; + int update_opacity = !rail_window->has_surface; + apply_layer_updates = apply_layer_updates || update_opacity; + rail_window->has_surface = 1; + rail_window->has_deferred_surface = 0; + + int paint_background = rail_window->has_rail_order + && rail_window->visible; + + /* If a visible top-level window paints before authoritative z-order is + * available, raise it once so newly launched RemoteApps become visible. + * Later paints must not keep raising it above other windows. */ + if (!rail_window->painted_raise_done + && rail_window->has_rail_order && rail_window->visible + && !rail_window->in_monitored_desktop_zorder + && rail_window->owner_window_id == 0) { + int z = guac_rdp_rail_get_top_z(rdp_client); + guac_rdp_rail_stack_window(rail_window, z); + guac_rdp_rail_raise_owned_windows(rdp_client, + rail_window->window_id, z + 1); + rail_window->painted_raise_done = 1; + apply_layer_updates = 1; + } + + if (update_opacity) + guac_rdp_rail_update_window_opacity(rdp_client, rail_window); + rdp_client->gdi_modified = 1; + + guac_rdp_rail_window* free_window = NULL; + if (guac_rdp_rail_release_paint_ref(rail_window)) + free_window = rail_window; + + guac_common_list_unlock(rdp_client->rail_windows); + + if (apply_layer_updates) + guac_rdp_rail_apply_window_layers(rdp_client); + + if (paint_background) + guac_rdp_rail_paint_background(rdp_client); + + guac_rdp_rail_free_window_data(free_window); + + return CHANNEL_RC_OK; + +} + +UINT guac_rdp_rail_update_surface_area(RdpgfxClientContext* context, + UINT16 surface_id, UINT32 rect_count, const RECTANGLE_16* rects) { + + if (context == NULL) + return CHANNEL_RC_OK; + + rdpGdi* gdi = (rdpGdi*) context->custom; + if (gdi == NULL || gdi->context == NULL) + return CHANNEL_RC_OK; + + if (gdi->suppressOutput) + return CHANNEL_RC_OK; + + if (context->GetSurfaceData == NULL) + return CHANNEL_RC_OK; + + gdiGfxSurface* surface = + (gdiGfxSurface*) context->GetSurfaceData(context, surface_id); + + if (surface == NULL) + return CHANNEL_RC_OK; + + /* Only window-mapped surfaces are routed through us; non-RAIL surfaces + * are handled by FreeRDP's default GDI pipeline. We return early without + * touching invalidRegion so the default path can consume it normally on + * the next UpdateSurfaces tick. */ + if (surface->windowId == 0) + return CHANNEL_RC_OK; + + guac_client* client = ((rdp_freerdp_context*) gdi->context)->client; + return guac_rdp_rail_paint_surface(client, surface, rect_count, rects); + +} + +#ifdef HAVE_RDPGFX_WINDOW_SURFACE_UPDATE +UINT guac_rdp_rail_update_window_from_surface(RdpgfxClientContext* context, + gdiGfxSurface* surface) { + + if (context == NULL) + return CHANNEL_RC_OK; + + rdpGdi* gdi = (rdpGdi*) context->custom; + if (gdi == NULL || gdi->context == NULL) + return CHANNEL_RC_OK; + + if (gdi->suppressOutput) + return CHANNEL_RC_OK; + + if (surface == NULL) + return CHANNEL_RC_OK; + + if (surface->windowId == 0) + return CHANNEL_RC_OK; + + /* Snapshot the rect list before painting. region16_rects() returns a + * pointer into the region's internal storage, which is only valid until + * the region is next modified. FreeRDP's GDI pipeline clears + * invalidRegion after this callback returns. */ + UINT32 rect_count = 0; + const RECTANGLE_16* rects = + region16_rects(&surface->invalidRegion, &rect_count); + + guac_client* client = ((rdp_freerdp_context*) gdi->context)->client; + return guac_rdp_rail_paint_surface(client, surface, rect_count, rects); + +} +#endif +#endif + +void guac_rdp_rail_free_windows(guac_rdp_client* rdp_client) { + + if (rdp_client->rail_windows != NULL) { + guac_common_list_free(rdp_client->rail_windows, + guac_rdp_rail_free_window_data); + rdp_client->rail_windows = NULL; + } + + if (rdp_client->rail_window_layer != NULL) { + guac_display_free_layer(rdp_client->rail_window_layer); + rdp_client->rail_window_layer = NULL; + } } @@ -318,7 +2589,12 @@ static void guac_rdp_rail_channel_connected(rdpContext* context, rail->ServerExecuteResult = guac_rdp_rail_execute_result; rail->ServerHandshake = guac_rdp_rail_handshake; rail->ServerHandshakeEx = guac_rdp_rail_handshake_ex; + rail->ServerLocalMoveSize = guac_rdp_rail_local_move_size; + context->update->window->WindowCreate = guac_rdp_rail_window_update; context->update->window->WindowUpdate = guac_rdp_rail_window_update; + context->update->window->WindowDelete = guac_rdp_rail_window_delete; + context->update->window->MonitoredDesktop = + guac_rdp_rail_monitored_desktop; guac_client_log(client, GUAC_LOG_DEBUG, "RAIL (RemoteApp) channel " "connected."); diff --git a/src/protocols/rdp/channels/rail.h b/src/protocols/rdp/channels/rail.h index 4f5bcee280..122eaefbbb 100644 --- a/src/protocols/rdp/channels/rail.h +++ b/src/protocols/rdp/channels/rail.h @@ -23,6 +23,13 @@ #include #include +struct guac_rdp_client; + +#ifdef HAVE_RDPGFX_SURFACE_AREA_UPDATE +#include +#include +#endif + #ifdef FREERDP_RAIL_CALLBACKS_REQUIRE_CONST /** * FreeRDP 2.0.0-rc4 and newer requires the final arguments for RAIL @@ -60,5 +67,147 @@ */ void guac_rdp_rail_load_plugin(rdpContext* context); +/** + * Raises and activates the topmost tracked RAIL window containing the given + * display coordinates, if any. + * + * This is used as a local fallback for RemoteApp+GFX window ordering. Windows + * may accept a click on a RemoteApp window without sending a monitored desktop + * z-order or DESKTOP_ACTIVE_WND update. In that case, FreeRDP receives the + * input normally, but Guacamole's separate per-window layers would remain in + * the old visual order unless the clicked layer is raised locally. Sending + * ClientActivate mirrors native FreeRDP clients and makes sure the RDP server + * targets the clicked RemoteApp window before handling the mouse click. + * + * @param rdp_client + * The RDP client whose tracked RAIL windows should be searched. + * + * @param x + * The X coordinate of the mouse event. + * + * @param y + * The Y coordinate of the mouse event. + */ +void guac_rdp_rail_raise_window_at(struct guac_rdp_client* rdp_client, + int x, int y); + +/** + * Tracks which RAIL window owns the current mouse drag. + * + * FreeRDP sends RDP mouse input in desktop coordinates even for RemoteApp. This + * function records the clicked window so later drag/release events stay associated with it. + * + * @param rdp_client + * The RDP client whose tracked RAIL windows should be searched. + * + * @param mask + * The current Guacamole mouse button mask. + * + * @param x + * The X coordinate of the mouse event. + * + * @param y + * The Y coordinate of the mouse event. + */ +void guac_rdp_rail_track_mouse_event(struct guac_rdp_client* rdp_client, + int mask, int x, int y); + +#ifdef HAVE_RDPGFX_SURFACE_AREA_UPDATE +/** + * Handles notification that the given RDPGFX surface has been mapped to the + * given RAIL window. + * + * @param context + * The RDPGFX client context associated with the surface mapping. + * + * @param surface_id + * The server-side ID of the mapped RDPGFX surface. + * + * @param window_id + * The server-side ID of the RAIL window to which the surface was mapped. + * + * @return + * CHANNEL_RC_OK. Surface mapping failures are treated as non-fatal + * tracking failures. Window contents may not render (event gets logged), but the + * RDPGFX channel remains usable. + */ +UINT guac_rdp_rail_map_window_for_surface(RdpgfxClientContext* context, + UINT16 surface_id, UINT64 window_id); + +/** + * Handles notification that an RDPGFX surface is no longer mapped to the given + * RAIL window. + * + * @param context + * The RDPGFX client context associated with the surface unmapping. + * + * @param window_id + * The server-side ID of the RAIL window that was unmapped. + * + * @return + * CHANNEL_RC_OK is always returned. Surface unmapping failures are ignored + * as the window might not be tracked. + */ +UINT guac_rdp_rail_unmap_window_for_surface(RdpgfxClientContext* context, + UINT64 window_id); + +/** + * Updates the Guacamole display layer for a RAIL window from modified areas of + * the given RDPGFX surface. Non-RAIL (windowId == 0) surfaces are ignored and + * left for FreeRDP's default GDI pipeline. + * + * Used only as a fallback when UpdateWindowFromSurface is unavailable in the + * installed FreeRDP. Successful paints clear the invalidRegion of the surface, + * matching the shared paint path used by window-mapped RDPGFX updates. + * + * @param context + * The RDPGFX client context associated with the surface update. + * + * @param surface_id + * The server-side ID of the updated RDPGFX surface. + * + * @param rect_count + * The number of rectangles within the rects array. + * + * @param rects + * The updated rectangular regions of the surface. + * + * @return + * CHANNEL_RC_OK if the surface was ignored or painted successfully. + * ERROR_INTERNAL_ERROR if painting the surface failed. + */ +UINT guac_rdp_rail_update_surface_area(RdpgfxClientContext* context, + UINT16 surface_id, UINT32 rect_count, const RECTANGLE_16* rects); + +#ifdef HAVE_RDPGFX_WINDOW_SURFACE_UPDATE +/** + * Updates the Guacamole display layer for a RAIL window from the given + * window-mapped RDPGFX surface. FreeRDP's GDI pipeline calls this once per + * surface per frame, with the surface's accumulated invalidRegion, and + * subsequently clears that region itself. + * + * @param context + * The RDPGFX client context associated with the surface update. + * + * @param surface + * The RDPGFX surface containing the updated window contents. + * + * @return + * CHANNEL_RC_OK if the update was ignored or painted successfully. + * ERROR_INTERNAL_ERROR if painting the surface failed. + */ +UINT guac_rdp_rail_update_window_from_surface(RdpgfxClientContext* context, + gdiGfxSurface* surface); #endif +#endif + +/** + * Frees all RAIL window state tracked for RemoteApp rendering. Safe to call + * even when no RAIL windows are tracked. + * + * @param rdp_client + * The RDP client whose tracked RAIL windows should be freed. + */ +void guac_rdp_rail_free_windows(struct guac_rdp_client* rdp_client); +#endif diff --git a/src/protocols/rdp/channels/rdpgfx.c b/src/protocols/rdp/channels/rdpgfx.c index 0fae972f63..895d37471e 100644 --- a/src/protocols/rdp/channels/rdpgfx.c +++ b/src/protocols/rdp/channels/rdpgfx.c @@ -17,7 +17,10 @@ * under the License. */ +#include "config.h" + #include "channels/rdpgfx.h" +#include "channels/rail.h" #include "plugins/channels.h" #include "rdp.h" #include "settings.h" @@ -58,16 +61,72 @@ static void guac_rdp_rdpgfx_channel_connected(rdpContext* context, if (strcmp(args->name, RDPGFX_DVC_CHANNEL_NAME) != 0) return; - /* Init GDI-backed support for the Graphics Pipeline */ RdpgfxClientContext* rdpgfx = (RdpgfxClientContext*) args->pInterface; rdpGdi* gdi = context->gdi; + guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; + guac_rdp_settings* settings = rdp_client->settings; + BOOL gfx_initialized; + + if (!settings->enable_gfx) { + guac_client_log(client, GUAC_LOG_WARNING, "Ignoring RDPGFX channel " + "connection because RDPGFX is disabled."); + rdp_client->rdpgfx_interface = NULL; + return; + } + + /* + * Choose the RAIL+GFX hookup based on FreeRDP capabilities: + * + * If UpdateWindowFromSurface is available, use the normal graphics + * pipeline and render RemoteApp windows through that callback. + * + * If only UpdateSurfaceArea is available, use the _ex initializer and + * route RemoteApp window updates through our custom surface-area handler. + * + * On older FreeRDP versions, fall back to the default pipeline. In that + * case, RemoteApp with GFX will not render correctly and GFX must be + * disabled. + */ +#if defined(HAVE_RDPGFX_WINDOW_SURFACE_UPDATE) + gfx_initialized = (settings->remote_app != NULL) + ? gdi_graphics_pipeline_init_ex(gdi, rdpgfx, + guac_rdp_rail_map_window_for_surface, + guac_rdp_rail_unmap_window_for_surface, NULL) + : gdi_graphics_pipeline_init(gdi, rdpgfx); +#elif defined(HAVE_RDPGFX_SURFACE_AREA_UPDATE) + gfx_initialized = (settings->remote_app != NULL) + ? gdi_graphics_pipeline_init_ex(gdi, rdpgfx, + guac_rdp_rail_map_window_for_surface, + guac_rdp_rail_unmap_window_for_surface, + guac_rdp_rail_update_surface_area) + : gdi_graphics_pipeline_init(gdi, rdpgfx); +#else + if (settings->remote_app != NULL) { + guac_client_log(client, GUAC_LOG_WARNING, "RDPGFX is connected, but " + "this FreeRDP build does not expose RemoteApp surface update " + "callbacks. Falling back to GDI rendering."); + rdp_client->rdpgfx_interface = NULL; + return; + } - if (!gdi_graphics_pipeline_init(gdi, rdpgfx)) + gfx_initialized = gdi_graphics_pipeline_init(gdi, rdpgfx); +#endif + + if (!gfx_initialized) { + rdp_client->rdpgfx_interface = NULL; guac_client_log(client, GUAC_LOG_WARNING, "Rendering backend for RDPGFX " "channel could not be loaded. Graphics may not render at all!"); - else + } + else { + rdp_client->rdpgfx_interface = rdpgfx; +#ifdef HAVE_RDPGFX_WINDOW_SURFACE_UPDATE + if (settings->remote_app != NULL) + rdpgfx->UpdateWindowFromSurface = + guac_rdp_rail_update_window_from_surface; +#endif guac_client_log(client, GUAC_LOG_DEBUG, "RDPGFX channel will be used for " "the RDP Graphics Pipeline Extension."); + } } @@ -98,7 +157,9 @@ static void guac_rdp_rdpgfx_channel_disconnected(rdpContext* context, /* Un-init GDI-backed support for the Graphics Pipeline */ RdpgfxClientContext* rdpgfx = (RdpgfxClientContext*) args->pInterface; + guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; rdpGdi* gdi = context->gdi; + rdp_client->rdpgfx_interface = NULL; gdi_graphics_pipeline_uninit(gdi, rdpgfx); guac_client_log(client, GUAC_LOG_DEBUG, "RDPGFX channel support unloaded."); @@ -119,4 +180,3 @@ void guac_rdp_rdpgfx_load_plugin(rdpContext* context) { guac_freerdp_dynamic_channel_collection_add(context->settings, "rdpgfx", NULL); } - diff --git a/src/protocols/rdp/client.c b/src/protocols/rdp/client.c index 8b2e683df4..1f73a1165c 100644 --- a/src/protocols/rdp/client.c +++ b/src/protocols/rdp/client.c @@ -232,6 +232,7 @@ int guac_client_init(guac_client* client, int argc, char** argv) { /* Init required locks */ guac_rwlock_init(&(rdp_client->lock)); pthread_mutex_init(&(rdp_client->message_lock), &(rdp_client->attributes)); + rdp_client->rail_windows = guac_common_list_alloc(); /* Set handlers */ client->join_handler = guac_rdp_user_join_handler; @@ -319,6 +320,9 @@ int guac_rdp_client_free_handler(guac_client* client) { if (rdp_client->audio_input != NULL) guac_rdp_audio_buffer_free(rdp_client->audio_input); + /* Free any RAIL windows tracked for RemoteApp rendering. */ + guac_rdp_rail_free_windows(rdp_client); + guac_rwlock_destroy(&(rdp_client->lock)); pthread_mutex_destroy(&(rdp_client->message_lock)); diff --git a/src/protocols/rdp/gdi.c b/src/protocols/rdp/gdi.c index 68fe36e785..fec2c46b87 100644 --- a/src/protocols/rdp/gdi.c +++ b/src/protocols/rdp/gdi.c @@ -17,6 +17,8 @@ * under the License. */ +#include "config.h" + #include "color.h" #include "rdp.h" #include "settings.h" @@ -35,6 +37,40 @@ #include +/** + * Resynchronize default layer buffer details with FreeRDP's GDI. + * In RemoteApp sessions, this mapping would expose FreeRDP's hidden desktop + * buffer behind application windows, so this should only be used in full desktop mode. + * + * @param current_context + * The raw context that should reference the primary GDI buffer. + * + * @param gdi + * The FreeRDP GDI state whose primary buffer should be referenced. + */ +static void guac_rdp_gdi_use_primary_buffer( + guac_display_layer_raw_context* current_context, rdpGdi* gdi) { + + current_context->buffer = gdi->primary_buffer; + current_context->stride = gdi->stride; + guac_rect_init(¤t_context->bounds, 0, 0, gdi->width, gdi->height); +} + +/** + * Returns whether RemoteApp windows are being rendered through RDPGFX layers. + * + * @param rdp_client + * The RDP client whose rendering mode should be checked. + * + * @return + * Non-zero if RemoteApp GFX rendering is active, zero otherwise. + */ +static int guac_rdp_gdi_remoteapp_gfx_active(guac_rdp_client* rdp_client) { + return rdp_client->settings->remote_app != NULL + && rdp_client->settings->enable_gfx + && rdp_client->rdpgfx_interface != NULL; +} + void guac_rdp_gdi_mark_frame(rdpContext* context, int starting) { guac_client* client = ((rdp_freerdp_context*) context)->client; @@ -83,10 +119,15 @@ BOOL guac_rdp_gdi_begin_paint(rdpContext* context) { guac_display_layer_raw_context* current_context = guac_display_layer_open_raw(default_layer); rdp_client->current_context = current_context; - /* Resynchronize default layer buffer details with FreeRDP's GDI */ - current_context->buffer = gdi->primary_buffer; - current_context->stride = gdi->stride; - guac_rect_init(¤t_context->bounds, 0, 0, gdi->width, gdi->height); + /* + * For full desktop sessions, the default layer shares FreeRDP's primary + * GDI buffer. For RemoteApp sessions, keep the default layer on its own + * black buffer and allow FreeRDP to paint the hidden server desktop only + * into its own primary buffer. + */ + if (!guac_rdp_gdi_remoteapp_gfx_active(rdp_client) + || !rdp_client->rail_background_painted) + guac_rdp_gdi_use_primary_buffer(current_context, gdi); return TRUE; @@ -128,14 +169,24 @@ BOOL guac_rdp_gdi_end_paint(rdpContext* context) { * checking and bailing out here if an external bug breaks that. */ GUAC_ASSERT(w <= INT_MAX && h <= INT_MAX); - /* Mark modified region as dirty, but only within the bounds of the - * rendering surface */ - guac_rect dst_rect; - guac_rect_init(&dst_rect, x, y, w, h); - guac_rect_constrain(&dst_rect, ¤t_context->bounds); - guac_rect_extend(¤t_context->dirty, &dst_rect); - - rdp_client->gdi_modified = 1; + /* + * Only propagate dirty regions for full desktop sessions. + * In RemoteApp mode, once RemoteApp rendering has taken over, the default + * layer remains permanently black (filled once in + * guac_rdp_rail_paint_background()) and desktop updates are suppressed + * because application windows are rendered separately. + */ + if (!guac_rdp_gdi_remoteapp_gfx_active(rdp_client) + || !rdp_client->rail_background_painted) { + /* Mark modified region as dirty, but only within the bounds of the + * rendering surface */ + guac_rect dst_rect; + guac_rect_init(&dst_rect, x, y, w, h); + guac_rect_constrain(&dst_rect, ¤t_context->bounds); + guac_rect_extend(¤t_context->dirty, &dst_rect); + + rdp_client->gdi_modified = 1; + } paint_complete: @@ -148,7 +199,6 @@ BOOL guac_rdp_gdi_end_paint(rdpContext* context) { guac_display_layer_close_raw(default_layer, current_context); return TRUE; - } BOOL guac_rdp_gdi_desktop_resize(rdpContext* context) { @@ -169,27 +219,47 @@ BOOL guac_rdp_gdi_desktop_resize(rdpContext* context) { GUAC_ASSERT(rdp_client->current_context == NULL); #endif - /* All potential drawing operations must occur while holding an open context */ guac_display_layer* default_layer = guac_display_default_layer(rdp_client->display); - guac_display_layer_raw_context* current_context = guac_display_layer_open_raw(default_layer); + guac_display_layer_raw_context* current_context = NULL; + + /* Full desktop sessions share the resized GDI buffer with the default + * layer, and must hold an open raw context to acquire lock while the + * external buffer details change. */ + if (!guac_rdp_gdi_remoteapp_gfx_active(rdp_client) + || !rdp_client->rail_background_painted) + current_context = guac_display_layer_open_raw(default_layer); - /* Resize FreeRDP's GDI buffer */ + /* Resize FreeRDP's GDI buffer while buffer is locked */ BOOL retval = gdi_resize(context->gdi, width, height); GUAC_ASSERT(gdi->primary_buffer != NULL); - /* Update our reference to the GDI buffer, as well as any structural - * details, which may now all be different */ - current_context->buffer = gdi->primary_buffer; - current_context->stride = gdi->stride; - guac_rect_init(¤t_context->bounds, 0, 0, gdi->width, gdi->height); + if (!guac_rdp_gdi_remoteapp_gfx_active(rdp_client) + || !rdp_client->rail_background_painted) { + /* Full desktop render path: Update our reference to the GDI buffer, + * as well as any structural details, which may now all be different. */ + guac_rdp_gdi_use_primary_buffer(current_context, gdi); + + /* Resize layer to match new display dimensions and underlying buffer. */ + guac_display_layer_resize(default_layer, gdi->width, gdi->height); + guac_display_layer_close_raw(default_layer, current_context); + } + else { + /* RAIL path: Keep RemoteApp's default layer independent from the hidden desktop + * framebuffer, preserving the black background after resizes. */ + guac_display_layer_resize(default_layer, gdi->width, gdi->height); + + current_context = guac_display_layer_open_raw(default_layer); + guac_rect full_bounds = current_context->bounds; + guac_display_layer_raw_context_set(current_context, &full_bounds, + 0xFF000000); + guac_display_layer_close_raw(default_layer, current_context); + + rdp_client->gdi_modified = 1; + } - /* Resize layer to match new display dimensions and underlying buffer */ - guac_display_layer_resize(default_layer, gdi->width, gdi->height); guac_client_log(client, GUAC_LOG_DEBUG, "Server resized display to %ix%i", gdi->width, gdi->height); - guac_display_layer_close_raw(default_layer, current_context); - return retval; } diff --git a/src/protocols/rdp/input-queue.c b/src/protocols/rdp/input-queue.c index 76994a8e28..aafd238601 100644 --- a/src/protocols/rdp/input-queue.c +++ b/src/protocols/rdp/input-queue.c @@ -19,6 +19,7 @@ #include "config.h" +#include "channels/rail.h" #include "channels/disp.h" #include "channels/rdpei.h" #include "input.h" @@ -77,9 +78,12 @@ static void guac_rdp_handle_mouse_event(guac_rdp_client* rdp_client, /* If button mask unchanged, just send move event */ if (mask == rdp_client->mouse_button_mask) { + guac_rdp_rail_track_mouse_event(rdp_client, mask, x, y); + pthread_mutex_lock(&(rdp_client->message_lock)); GUAC_RDP_CONTEXT(rdp_inst)->input->MouseEvent( - GUAC_RDP_CONTEXT(rdp_inst)->input, PTR_FLAGS_MOVE, x, y); + GUAC_RDP_CONTEXT(rdp_inst)->input, PTR_FLAGS_MOVE, + x, y); pthread_mutex_unlock(&(rdp_client->message_lock)); } @@ -92,6 +96,16 @@ static void guac_rdp_handle_mouse_event(guac_rdp_client* rdp_client, /* Mouse buttons which have JUST become pressed */ int pressed_mask = ~rdp_client->mouse_button_mask & mask; + /* RAIL+GFX renders RemoteApp windows as separate Guacamole layers. + * FreeRDP tracks server-side ordering, but some servers do not send + * DESKTOP_ACTIVE_WND when a RemoteApp window is clicked, so raise and + * activate the clicked layer locally before mouse down. */ + if (pressed_mask & 0x07) { + guac_rdp_rail_raise_window_at(rdp_client, x, y); + } + + guac_rdp_rail_track_mouse_event(rdp_client, mask, x, y); + /* Release event */ if (released_mask & 0x07) { @@ -134,7 +148,8 @@ static void guac_rdp_handle_mouse_event(guac_rdp_client* rdp_client, if (pressed_mask & 0x08) { pthread_mutex_lock(&(rdp_client->message_lock)); GUAC_RDP_CONTEXT(rdp_inst)->input->MouseEvent( - GUAC_RDP_CONTEXT(rdp_inst)->input, PTR_FLAGS_WHEEL | 0x78, x, y); + GUAC_RDP_CONTEXT(rdp_inst)->input, + PTR_FLAGS_WHEEL | 0x78, x, y); pthread_mutex_unlock(&(rdp_client->message_lock)); } @@ -142,7 +157,9 @@ static void guac_rdp_handle_mouse_event(guac_rdp_client* rdp_client, if (pressed_mask & 0x10) { pthread_mutex_lock(&(rdp_client->message_lock)); GUAC_RDP_CONTEXT(rdp_inst)->input->MouseEvent( - GUAC_RDP_CONTEXT(rdp_inst)->input, PTR_FLAGS_WHEEL | PTR_FLAGS_WHEEL_NEGATIVE | 0x88, x, y); + GUAC_RDP_CONTEXT(rdp_inst)->input, + PTR_FLAGS_WHEEL | PTR_FLAGS_WHEEL_NEGATIVE | 0x88, + x, y); pthread_mutex_unlock(&(rdp_client->message_lock)); } diff --git a/src/protocols/rdp/input.c b/src/protocols/rdp/input.c index 63c0be2d42..08fc885249 100644 --- a/src/protocols/rdp/input.c +++ b/src/protocols/rdp/input.c @@ -108,6 +108,13 @@ int guac_rdp_user_size_handler(guac_user* user, int width, int height) { guac_rdp_settings* settings = rdp_client->settings; freerdp* rdp_inst = rdp_client->rdp_inst; + if (settings->remote_app != NULL) { + guac_client_log(client, GUAC_LOG_DEBUG, + "Ignoring display resize for RemoteApp session: %ix%i.", + width, height); + return 0; + } + /* Convert client pixels to remote pixels */ width = width * settings->resolution / user->info.optimal_resolution; height = height * settings->resolution / user->info.optimal_resolution; diff --git a/src/protocols/rdp/rdp.c b/src/protocols/rdp/rdp.c index 23ed9bc45e..0ee47254f5 100644 --- a/src/protocols/rdp/rdp.c +++ b/src/protocols/rdp/rdp.c @@ -33,7 +33,6 @@ #include "channels/rdpsnd/rdpsnd.h" #include "client.h" #include "color.h" -#include "config.h" #include "error.h" #include "fs.h" #include "gdi.h" @@ -454,8 +453,6 @@ static int rdp_guac_client_wait_for_events(guac_client* client, DWORD result = WaitForMultipleObjects(num_handles, handles, FALSE, timeout_msecs); - ResetEvent(rdp_client->input_event_queued); - /* Translate WaitForMultipleObjects() return values */ switch (result) { @@ -533,6 +530,7 @@ static int guac_rdp_handle_connection(guac_client* client) { /* Create display */ rdp_client->display = guac_display_alloc(client); + rdp_client->rail_background_painted = 0; guac_display_layer* default_layer = guac_display_default_layer(rdp_client->display); guac_display_layer_resize(default_layer, rdp_client->settings->width, rdp_client->settings->height); @@ -611,6 +609,8 @@ static int guac_rdp_handle_connection(guac_client* client) { guac_rwlock_release_lock(&(rdp_client->lock)); rdp_client->render_thread = guac_display_render_thread_create(rdp_client->display); + if (rdp_client->gdi_modified) + guac_display_render_thread_notify_modified(rdp_client->render_thread); /* Handle messages from RDP server while client is running */ while (client->state == GUAC_CLIENT_RUNNING @@ -627,6 +627,8 @@ static int guac_rdp_handle_connection(guac_client* client) { int connection_closing = 0; do { + /* Keep input responsive even if RDPGFX events keep arriving. */ + guac_rdp_handle_input_events(rdp_client); /* Handle any queued FreeRDP events (this may result in RDP messages * being sent), aborting later if FreeRDP event handling fails */ @@ -683,6 +685,9 @@ static int guac_rdp_handle_connection(guac_client* client) { guac_display_render_thread_destroy(rdp_client->render_thread); rdp_client->render_thread = NULL; + /* Free any RAIL windows tracked for RemoteApp rendering */ + guac_rdp_rail_free_windows(rdp_client); + /* Remove reference to FreeRDP's GDI buffer so that it can be safely freed * prior to freeing the guac_display */ guac_display_layer_raw_context* context = guac_display_layer_open_raw(default_layer); diff --git a/src/protocols/rdp/rdp.h b/src/protocols/rdp/rdp.h index 065080a3b3..d67738105c 100644 --- a/src/protocols/rdp/rdp.h +++ b/src/protocols/rdp/rdp.h @@ -40,6 +40,7 @@ #include #include +#include #include #include #include @@ -247,6 +248,37 @@ typedef struct guac_rdp_client { */ RailClientContext* rail_interface; + /** + * A pointer to the RDPGFX interface provided by the RDP client when the + * graphics pipeline is in use. + */ + RdpgfxClientContext* rdpgfx_interface; + + /** + * The list of RAIL windows being tracked for RemoteApp rendering. + */ + guac_common_list* rail_windows; + + /** + * The RAIL window currently activated by guacd, if any. + */ + UINT64 rail_active_window_id; + + /** + * The RAIL window receiving the current mouse drag, if any. + */ + UINT64 rail_mouse_window_id; + + /** + * Parent layer containing all RAIL window layers rendered for RemoteApp. + */ + guac_display_layer* rail_window_layer; + + /** + * Whether the RemoteApp background has been initialized behind RAIL windows. + */ + int rail_background_painted; + } guac_rdp_client; /** diff --git a/src/protocols/rdp/settings.c b/src/protocols/rdp/settings.c index 7490d4d0b4..f3edc3a5aa 100644 --- a/src/protocols/rdp/settings.c +++ b/src/protocols/rdp/settings.c @@ -42,6 +42,10 @@ #include #include +#ifndef GUAC_RDP_DISABLE_GFX_DEFAULT +#define GUAC_RDP_DISABLE_GFX_DEFAULT 0 +#endif + /** * A warning to log when NLA mode is selected while FIPS mode is active on the * guacd server. @@ -1235,10 +1239,22 @@ guac_rdp_settings* guac_rdp_parse_args(guac_user* user, settings->resize_method = GUAC_RESIZE_NONE; } - /* RDP Graphics Pipeline enable/disable */ + /* + * RDP Graphics Pipeline enable/disable. Guacamole 1.6 enables GFX by + * default when disable-gfx is omitted. + */ settings->enable_gfx = !guac_user_parse_args_boolean(user, GUAC_RDP_CLIENT_ARGS, argv, - IDX_DISABLE_GFX, 0); + IDX_DISABLE_GFX, GUAC_RDP_DISABLE_GFX_DEFAULT); + +#if !defined(HAVE_RDPGFX_WINDOW_SURFACE_UPDATE) \ + && !defined(HAVE_RDPGFX_SURFACE_AREA_UPDATE) + if (settings->remote_app != NULL && settings->enable_gfx) { + guac_user_log(user, GUAC_LOG_WARNING, "RDPGFX is not supported for " + "RemoteApp by this FreeRDP build. RDPGFX will be disabled."); + settings->enable_gfx = 0; + } +#endif /* Session color depth */ settings->color_depth = @@ -1544,6 +1560,19 @@ void guac_rdp_push_settings(guac_client* client, freerdp_settings_set_bool(rdp_settings, FreeRDP_SupportGraphicsPipeline, TRUE); freerdp_settings_set_bool(rdp_settings, FreeRDP_RemoteFxCodec, TRUE); + if (guac_settings->remote_app != NULL) { + /* + * MS-RDPEGFX +gfx-progressive + * Enable progressive RDPGFX only for RemoteApp. Full-desktop + * progressive updates can leave drag/resize artifacts (fix it), while RAIL + * windows are rendered as separate layers and repaint independently. + * TODO: Evaluate FreeRDP_GfxProgressiveV2 for high-resolution or + * high-throughput deployments. Keep it off by default until its + * behavior on low-bandwidth/high-latency connections is known. + */ + freerdp_settings_set_bool(rdp_settings, FreeRDP_GfxProgressive, TRUE); + freerdp_settings_set_bool(rdp_settings, FreeRDP_GfxThinClient, FALSE); + } if (freerdp_settings_get_uint32(rdp_settings, FreeRDP_ColorDepth) != RDP_GFX_REQUIRED_DEPTH) { guac_client_log(client, GUAC_LOG_WARNING, "Ignoring requested " @@ -1679,7 +1708,6 @@ void guac_rdp_push_settings(guac_client* client, freerdp_settings_set_string(rdp_settings, FreeRDP_CertificateAcceptedFingerprints, guac_strdup(guac_settings->certificate_fingerprints)); - /* RemoteApp */ if (guac_settings->remote_app != NULL) { freerdp_settings_set_bool(rdp_settings, FreeRDP_Workarea, TRUE); @@ -1779,6 +1807,10 @@ void guac_rdp_push_settings(guac_client* client, rdp_settings->SupportGraphicsPipeline = TRUE; rdp_settings->RemoteFxCodec = TRUE; + if (guac_settings->remote_app != NULL) { + rdp_settings->GfxProgressive = TRUE; + rdp_settings->GfxThinClient = FALSE; + } if (rdp_settings->ColorDepth != RDP_GFX_REQUIRED_DEPTH) { guac_client_log(client, GUAC_LOG_WARNING, "Ignoring requested "