diff --git a/configure.ac b/configure.ac index 6809a43e0..56be87224 100644 --- a/configure.ac +++ b/configure.ac @@ -1080,6 +1080,54 @@ 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) 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 3ec5517e9..f3f5d9c5d 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 bd7e65a71..d0266905a 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,20 +79,30 @@ 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_DEBUG, + "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 = { @@ -136,11 +159,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_DEBUG, + "RAIL client execute send completed: status=%u.", + (unsigned int) status); + return status; } @@ -149,7 +185,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 +203,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 +269,1855 @@ 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; + 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; + + /** + * The position of this window within the remote desktop. + */ + int x; + 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 dimensions of the Guacamole layer. + */ + int layer_width; + int layer_height; + + /** + * The latest dimensions reported by RAIL window orders. + */ + int order_width; + int order_height; + + /** + * The latest client-area bounds reported by RAIL window orders. + */ + int client_x; + int client_y; + int client_width; + int client_height; + int has_client_area; + + /** + * The dimensions of the mapped RDPGFX surface represented by the layer. + */ + int surface_width; + 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 + /** - * 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; +} + +/** + * 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; +} + +/** + * 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; +} + +/** + * 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 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 be stacked. + * + * @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; + + guac_display_layer_stack(rail_window->layer, z); + +} + +/** + * 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 has_no_surface_window = 0; + int highest_no_surface_z = 0; + int suppress_activation = 0; + int has_adjacent_nosurface_window = 0; + + 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_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; + } + + 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 (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; + + 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); + rdp_client->gdi_modified = 1; + } + } + + guac_common_list_unlock(rdp_client->rail_windows); + + 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; + + 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; + + 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; + } + + } + + } + + 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 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. + * + * @return + * The matching or newly-created RAIL window, or NULL if the window could not be created. + */ +static guac_rdp_rail_window* guac_rdp_rail_get_or_create_window( + guac_client* client, UINT64 window_id) { + + 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; + + guac_display_layer* parent_layer = + guac_rdp_rail_get_window_layer(client); + + if (parent_layer == NULL) + return NULL; + + rail_window = guac_mem_zalloc(sizeof(guac_rdp_rail_window)); + if (rail_window == NULL) { + guac_client_log(client, GUAC_LOG_WARNING, "RAIL window 0x%08x could " + "not be tracked. Window contents may not render.", + (unsigned int) window_id); + guac_client_log(client, GUAC_LOG_DEBUG, "RAIL window 0x%08x tracking failed: " + "could not allocate window state.", + (unsigned int) window_id); + return NULL; + } + + /* Allocate a child layer so window z-order stays within the RAIL group. */ + rail_window->layer = guac_display_alloc_layer(rdp_client->display, 1); + if (rail_window->layer == NULL) { + guac_client_log(client, GUAC_LOG_WARNING, "RAIL window 0x%08x could " + "not be tracked. Window contents may not render.", + (unsigned int) window_id); + guac_client_log(client, GUAC_LOG_DEBUG, "RAIL window 0x%08x tracking failed: " + "could not allocate display layer.", + (unsigned int) window_id); + guac_mem_free(rail_window); + return NULL; + } + + guac_display_layer_set_parent(rail_window->layer, parent_layer); + + rail_window->window_id = window_id; + rail_window->visible = 1; + + /* Keep the layer hidden until we have both a RAIL order (so we know where + * to put it) and at least one surface paint (so we have something to + * show). */ + guac_display_layer_set_opacity(rail_window->layer, 0); + + guac_common_list_add(rdp_client->rail_windows, rail_window); + + return rail_window; +} + +/** + * 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) { + + int opacity = (rail_window->visible + && rail_window->has_surface + && rail_window->has_rail_order) ? 0xFF : 0; + guac_display_layer_set_opacity(rail_window->layer, opacity); + +} + +/** + * Marks the full contents of the given RAIL window 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 rail_window + * The tracked RAIL window whose backing layer should be repainted. + */ +static void guac_rdp_rail_mark_window_dirty(guac_rdp_client* rdp_client, + guac_rdp_rail_window* rail_window) { + + guac_display_layer_raw_context* layer_context = + guac_display_layer_open_raw(rail_window->layer); + + if (layer_context == NULL) + return; + + guac_rect_extend(&layer_context->dirty, &layer_context->bounds); + guac_display_layer_close_raw(rail_window->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); + + guac_mem_free(rail_window); +} + +/** + * 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; + +} + +/** + * 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; + + guac_client_log(client, GUAC_LOG_TRACE, "RAIL window update callback: %d", orderInfo->fieldFlags); + + UINT32 fieldFlags = orderInfo->fieldFlags; + int repaint_surface = 0; + UINT16 repaint_surface_id = 0; + + /* Track window location and visibility for RDPGFX RemoteApp rendering. */ + if (guac_rdp_rail_uses_gfx(rdp_client)) { + + guac_common_list_lock(rdp_client->rail_windows); + + guac_rdp_rail_window* rail_window = + guac_rdp_rail_get_or_create_window(client, orderInfo->windowId); + + if (rail_window != NULL) { + + int had_rail_order = rail_window->has_rail_order; + int was_visible = rail_window->visible; + 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_TITLE) { + const WCHAR* title_string = + (const WCHAR*) windowState->titleInfo.string; + char* title = ConvertWCharNToUtf8Alloc(title_string, + windowState->titleInfo.length / sizeof(WCHAR), + NULL); + + free(title); + } + + if (fieldFlags & WINDOW_ORDER_FIELD_WND_OFFSET) { + rail_window->x = windowState->windowOffsetX; + rail_window->y = windowState->windowOffsetY; + guac_display_layer_move(rail_window->layer, + rail_window->x, rail_window->y); + 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); + rdp_client->gdi_modified = 1; + } + + if (rail_window->has_rail_order && rail_window->visible) + guac_rdp_rail_paint_background(rdp_client); + + if (rail_window->has_surface && rail_window->has_rail_order + && rail_window->visible) { + + if (!had_rail_order || needs_opacity_update) + guac_rdp_rail_mark_window_dirty(rdp_client, rail_window); + + 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); + rdp_client->gdi_modified = 1; + } + } + + if (needs_opacity_update) { + guac_rdp_rail_update_window_opacity(rdp_client, rail_window); + rdp_client->gdi_modified = 1; + } + + 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; + } + + } + + guac_common_list_unlock(rdp_client->rail_windows); + +#ifdef HAVE_RDPGFX_SURFACE_AREA_UPDATE + if (repaint_surface) + guac_rdp_rail_repaint_surface(client, repaint_surface_id); +#endif + + } + + /* 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; + + 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); + 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) + guac_rdp_rail_paint_background(rdp_client); + + 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); + rdp_client->gdi_modified = 1; + } + } + + } + + guac_common_list_unlock(rdp_client->rail_windows); + + 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_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); + + int repaint_surface = 0; + + if (rail_window != 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); + +#ifdef HAVE_RDPGFX_SURFACE_AREA_UPDATE + if (repaint_surface) + guac_rdp_rail_repaint_surface(client, surface_id); +#endif + } + + 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; + + 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; + + 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; + } + + guac_display_layer* layer = rail_window->layer; + 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 (needs_resize) { + guac_display_layer_resize(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; + } + + /* RDPGFX window surfaces are copied as opaque layer content. We don't + * want holes on our RemoteApp windows. Only copy RGB24. */ + UINT32 dst_format = guac_rdp_get_native_pixel_format(FALSE); + 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; + } + + 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; + 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; + } + + 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 (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 +2160,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 4f5bcee28..d3aadcac9 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,146 @@ */ 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 0fae972f6..64993e45e 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,57 @@ 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; - - if (!gdi_graphics_pipeline_init(gdi, rdpgfx)) + guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; + guac_rdp_settings* settings = rdp_client->settings; + BOOL gfx_initialized; + + rdp_client->rdpgfx_interface = rdpgfx; + + /* + * 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 + gfx_initialized = gdi_graphics_pipeline_init(gdi, rdpgfx); +#endif + + if (!gfx_initialized) { guac_client_log(client, GUAC_LOG_WARNING, "Rendering backend for RDPGFX " "channel could not be loaded. Graphics may not render at all!"); - else + } + else { +#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 +142,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 +165,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 8b2e683df..1f73a1165 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 68fe36e78..cabafb0ae 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,25 @@ #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); +} + void guac_rdp_gdi_mark_frame(rdpContext* context, int starting) { guac_client* client = ((rdp_freerdp_context*) context)->client; @@ -83,10 +104,16 @@ 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 (rdp_client->settings->remote_app == NULL + || !rdp_client->settings->enable_gfx + || !rdp_client->rail_background_painted) + guac_rdp_gdi_use_primary_buffer(current_context, gdi); return TRUE; @@ -128,14 +155,25 @@ 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 (rdp_client->settings->remote_app == NULL + || !rdp_client->settings->enable_gfx + || !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 +186,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 +206,49 @@ 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 while the external buffer + * details change. */ + if (rdp_client->settings->remote_app == NULL + || !rdp_client->settings->enable_gfx + || !rdp_client->rail_background_painted) + current_context = guac_display_layer_open_raw(default_layer); /* Resize FreeRDP's GDI buffer */ 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 (rdp_client->settings->remote_app == NULL + || !rdp_client->settings->enable_gfx + || !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 76994a8e2..5e68ecfe3 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" @@ -59,6 +60,8 @@ static void guac_rdp_handle_mouse_event(guac_rdp_client* rdp_client, guac_user* user = event->user; int x = event->details.mouse.x; int y = event->details.mouse.y; + int rdp_x = x; + int rdp_y = y; int mask = event->details.mouse.mask; guac_rwlock_acquire_read_lock(&(rdp_client->lock)); @@ -77,9 +80,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, rdp_x, rdp_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, + rdp_x, rdp_y); pthread_mutex_unlock(&(rdp_client->message_lock)); } @@ -92,6 +98,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, rdp_x, rdp_y); + } + + guac_rdp_rail_track_mouse_event(rdp_client, mask, rdp_x, rdp_y); + /* Release event */ if (released_mask & 0x07) { @@ -103,7 +119,7 @@ static void guac_rdp_handle_mouse_event(guac_rdp_client* rdp_client, pthread_mutex_lock(&(rdp_client->message_lock)); GUAC_RDP_CONTEXT(rdp_inst)->input->MouseEvent( - GUAC_RDP_CONTEXT(rdp_inst)->input, flags, x, y); + GUAC_RDP_CONTEXT(rdp_inst)->input, flags, rdp_x, rdp_y); pthread_mutex_unlock(&(rdp_client->message_lock)); } @@ -122,7 +138,7 @@ static void guac_rdp_handle_mouse_event(guac_rdp_client* rdp_client, /* Send event */ pthread_mutex_lock(&(rdp_client->message_lock)); GUAC_RDP_CONTEXT(rdp_inst)->input->MouseEvent( - GUAC_RDP_CONTEXT(rdp_inst)->input, flags, x, y); + GUAC_RDP_CONTEXT(rdp_inst)->input, flags, rdp_x, rdp_y); pthread_mutex_unlock(&(rdp_client->message_lock)); } @@ -134,7 +150,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, rdp_x, rdp_y); pthread_mutex_unlock(&(rdp_client->message_lock)); } @@ -142,7 +159,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, + rdp_x, rdp_y); pthread_mutex_unlock(&(rdp_client->message_lock)); } diff --git a/src/protocols/rdp/input.c b/src/protocols/rdp/input.c index 63c0be2d4..08fc88524 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 23ed9bc45..0ee47254f 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 065080a3b..e80ae0807 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,31 @@ typedef struct guac_rdp_client { */ RailClientContext* rail_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 7490d4d0b..1cc6f8562 100644 --- a/src/protocols/rdp/settings.c +++ b/src/protocols/rdp/settings.c @@ -1235,7 +1235,10 @@ 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); @@ -1544,6 +1547,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 +1695,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 +1794,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 "