RDP: guest desktop sometimes fails to re-render correctly after a resize (missing/misplaced taskbar) #18

Closed
opened 2026-09-14 21:22:56 -06:00 by Agent · 2 comments
Owner

Summary

During RDP sessions, resizing the window (e.g. toggling the session events
panel, which shrinks/grows the embedded display) can leave the remote
desktop showing stale or misaligned content — most visibly, the Windows
taskbar going missing entirely or appearing at the wrong vertical position,
sometimes with what looks like duplicate UI chrome from two different
moments composited into one frame.

Confirmed pre-existing (not introduced by the HiDPI/resize work in
v2026.9.14) — reproduced against the same commit with that work stashed out.

Reproduction

  1. Connect to an RDP profile (reproduced against a Windows Server VM using
    a virtual/synthetic display driver — architecture matters here, see
    below).
  2. Once connected, toggle the session's "Show Events"/"Hide Events" panel,
    which resizes the embedded RDP display widget.
  3. Observe: the taskbar is sometimes missing, or shows at an incorrect
    vertical position with visual artifacts, rather than the desktop
    cleanly re-rendering at the new size.

What's already been ruled out / mitigated (v2026.9.14)

  • Not a stale-frame-at-wrong-scale race on the client side — fixed
    separately (RdpDisplayWidget::setRemoteDesktopSize now clears the
    frame during a resize transition instead of drawing the old one scaled
    incorrectly).
  • Not (solely) caused by firing many rapid resize requests — outgoing
    resize requests are now debounced (150ms), coalescing bursts (e.g. the
    initial connect/layout-settling burst) into one request.
  • Added an explicit RefreshRect request after each resize completes as a
    best-effort nudge — did not fully resolve the issue in testing.
  • Only BeginPaint/EndPaint/DesktopResize are hooked in
    src/rdp_session_backend.cpp (no separate GFX/Progressive-codec pipeline
    that could be bypassing our frame-capture) — the client appears to be
    faithfully snapshotting whatever the server's own GDI buffer contains at
    EndPaint time.

Leading theory (unconfirmed)

The remote guest's actual desktop resolution may not be changing even
though sendDisplayResize's SendMonitorLayout call reports success —
i.e. the server acknowledges the monitor-layout change on the channel, but
the guest's own desktop/DWM doesn't actually re-render at the new
resolution, so we keep receiving (and displaying, scaled into the new
widget size) frames that are still logically the old dimensions. This
would explain content appearing at the "old" position relative to a
now-different-sized display area.

This may be specific to the test VM's virtual display driver (VMs using
synthetic/indirect display drivers are known to sometimes mishandle rapid
or repeated resolution-change requests) rather than a universal RDP client
bug — needs testing against a different kind of RDP host (physical
Windows machine, or a different hypervisor's display driver) to narrow
down whether this is host-specific.

Suggested next steps

  • Reproduce against a non-VM Windows host, or a VM with a different
    display driver, to determine whether this is host/driver-specific.
  • Confirm whether orbitDesktopResize's remoteDesktopSizeChanged signal
    (src/rdp_session_backend.cpp, wired to
    RdpDisplayWidget::setRemoteDesktopSize in src/session_tab.cpp) actually
    fires with the new dimensions after a resize, or whether the server
    never sends that confirmation back — this would distinguish "client
    not receiving the resize confirmation" from "server/guest not actually
    resizing."
  • Consider whether a longer delay between the debounced resize request and
    the RefreshRect follow-up (currently sent immediately) would give slow
    hosts more time to actually complete the resize first.

Related

Fixed alongside this investigation in v2026.9.14: distorted/corrupted-
looking RDP text on HiDPI monitors (a genuinely different root cause —
missing DPI/scale-factor negotiation, unrelated to this resize issue).

## Summary During RDP sessions, resizing the window (e.g. toggling the session events panel, which shrinks/grows the embedded display) can leave the remote desktop showing stale or misaligned content — most visibly, the Windows taskbar going missing entirely or appearing at the wrong vertical position, sometimes with what looks like duplicate UI chrome from two different moments composited into one frame. Confirmed pre-existing (not introduced by the HiDPI/resize work in v2026.9.14) — reproduced against the same commit with that work stashed out. ## Reproduction 1. Connect to an RDP profile (reproduced against a Windows Server VM using a virtual/synthetic display driver — architecture matters here, see below). 2. Once connected, toggle the session's "Show Events"/"Hide Events" panel, which resizes the embedded RDP display widget. 3. Observe: the taskbar is sometimes missing, or shows at an incorrect vertical position with visual artifacts, rather than the desktop cleanly re-rendering at the new size. ## What's already been ruled out / mitigated (v2026.9.14) - Not a stale-frame-at-wrong-scale race on the client side — fixed separately (`RdpDisplayWidget::setRemoteDesktopSize` now clears the frame during a resize transition instead of drawing the old one scaled incorrectly). - Not (solely) caused by firing many rapid resize requests — outgoing resize requests are now debounced (150ms), coalescing bursts (e.g. the initial connect/layout-settling burst) into one request. - Added an explicit `RefreshRect` request after each resize completes as a best-effort nudge — did not fully resolve the issue in testing. - Only `BeginPaint`/`EndPaint`/`DesktopResize` are hooked in `src/rdp_session_backend.cpp` (no separate GFX/Progressive-codec pipeline that could be bypassing our frame-capture) — the client appears to be faithfully snapshotting whatever the server's own GDI buffer contains at `EndPaint` time. ## Leading theory (unconfirmed) The remote guest's actual desktop resolution may not be changing even though `sendDisplayResize`'s `SendMonitorLayout` call reports success — i.e. the server acknowledges the monitor-layout change on the channel, but the guest's own desktop/DWM doesn't actually re-render at the new resolution, so we keep receiving (and displaying, scaled into the new widget size) frames that are still logically the old dimensions. This would explain content appearing at the "old" position relative to a now-different-sized display area. This may be specific to the test VM's virtual display driver (VMs using synthetic/indirect display drivers are known to sometimes mishandle rapid or repeated resolution-change requests) rather than a universal RDP client bug — needs testing against a different kind of RDP host (physical Windows machine, or a different hypervisor's display driver) to narrow down whether this is host-specific. ## Suggested next steps - Reproduce against a non-VM Windows host, or a VM with a different display driver, to determine whether this is host/driver-specific. - Confirm whether `orbitDesktopResize`'s `remoteDesktopSizeChanged` signal (src/rdp_session_backend.cpp, wired to `RdpDisplayWidget::setRemoteDesktopSize` in src/session_tab.cpp) actually fires with the new dimensions after a resize, or whether the server never sends that confirmation back — this would distinguish "client not receiving the resize confirmation" from "server/guest not actually resizing." - Consider whether a longer delay between the debounced resize request and the `RefreshRect` follow-up (currently sent immediately) would give slow hosts more time to actually complete the resize first. ## Related Fixed alongside this investigation in v2026.9.14: distorted/corrupted- looking RDP text on HiDPI monitors (a genuinely different root cause — missing DPI/scale-factor negotiation, unrelated to this resize issue).
Author
Owner

Root cause found (via source-level tracing of FreeRDP, no live repro needed)

Traced why update->DesktopResize (orbitDesktopResize in src/rdp_session_backend.cpp, the only place we ever called gdi_resize()) essentially never fires for our resize path:

  • update->DesktopResize is invoked from exactly two places in FreeRDP: libfreerdp/core/connection.c during the Deactivation-Reactivation PDU sequence (a full protocol renegotiation), and libfreerdp/gdi/gfx.c on a RDPGFX_CMDID_RESETGRAPHICS PDU — which only exists on the GFX/Graphics Pipeline virtual channel.
  • We never enable the GFX pipeline (FreeRDP_SupportGraphicsPipeline is never set anywhere in src/rdp_session_backend.cpp) — confirmed by grep, matches this issue's earlier note that "no separate GFX/Progressive-codec pipeline" is hooked.
  • Our dynamic resize goes entirely through the Display Control channel (SendMonitorLayout, MS-RDPEDISP), which has no server acknowledgment PDU at all. On a GFX-pipeline client, the server's acceptance of a disp-channel resize is normally what triggers the ResetGraphics PDU that (indirectly) calls DesktopResize. Without GFX, that entire notification path doesn't exist for us.
  • Net effect: after the very first connect, gdi->width/gdi->height (and the backing gdi->primary_buffer) were permanently stuck at the initial connect size for the rest of the session, no matter how many successful SendMonitorLayout calls we made.
  • FreeRDP's own surface-bits handling makes the failure mode exactly match what was observed: intersect_rect() in libfreerdp/gdi/gdi.c silently drops any incoming update whose destRight/destBottom exceeds the stale gdi->width/gdi->height — so content in any newly-exposed area (e.g. a taskbar that moved because the guest did actually re-layout) never gets painted, leaving stale pixels behind. This also explains why the earlier RefreshRect mitigation didn't fully resolve it: re-requesting the same content just re-triggers the same rejection against the same stale bounds.

Fix (implemented locally, not yet committed — see below)

In RdpSessionBackend::sendDisplayResize (src/rdp_session_backend.cpp), right after SendMonitorLayout returns CHANNEL_RC_OK, we now proactively call gdi_resize(instance->context->gdi, width, height) ourselves and emit remoteDesktopSizeChanged(width, height) on success — instead of only doing this reactively from a callback that structurally can't fire for our channel/codec configuration. This mirrors exactly what gdi_resize() is designed for (it's the same call every reference FreeRDP client makes from its own DesktopResize handler) and is safe to call here since it runs on the same single worker thread that also drives FreeRDP's own event/paint processing — never concurrently with an in-flight paint.

On failure it now logs a session event instead of silently leaving stale content.

Status / what's still needed

  • Builds clean (Linux), basic app smoke-test passed (launches, no crash).
  • Not yet verified against a live RDP session — needs an actual connect + resize to confirm the taskbar/corruption symptom is gone, which needs a password I don't have access to. Whoever picks this up next should reproduce the original repro steps (toggle the events panel a few times) with this change applied.
  • Not yet committed/pushed — pending review since I can't verify live.

If the guest's own display driver genuinely never applies the requested resolution at all (the original "VM synthetic driver" theory), this fix won't create new corruption — worst case is now blank/unfilled space in the extra area rather than dropped/garbled updates — but it also won't make the guest actually resize; that would need testing against a non-VM host to fully rule in/out, per the original issue's suggested next steps.

## Root cause found (via source-level tracing of FreeRDP, no live repro needed) Traced why `update->DesktopResize` (`orbitDesktopResize` in `src/rdp_session_backend.cpp`, the only place we ever called `gdi_resize()`) essentially never fires for our resize path: - `update->DesktopResize` is invoked from exactly two places in FreeRDP: `libfreerdp/core/connection.c` during the **Deactivation-Reactivation PDU sequence** (a full protocol renegotiation), and `libfreerdp/gdi/gfx.c` on a **`RDPGFX_CMDID_RESETGRAPHICS`** PDU — which only exists on the **GFX/Graphics Pipeline virtual channel**. - We never enable the GFX pipeline (`FreeRDP_SupportGraphicsPipeline` is never set anywhere in `src/rdp_session_backend.cpp`) — confirmed by grep, matches this issue's earlier note that "no separate GFX/Progressive-codec pipeline" is hooked. - Our dynamic resize goes entirely through the **Display Control channel** (`SendMonitorLayout`, MS-RDPEDISP), which has **no server acknowledgment PDU at all**. On a GFX-pipeline client, the server's acceptance of a disp-channel resize is normally what triggers the ResetGraphics PDU that (indirectly) calls `DesktopResize`. Without GFX, that entire notification path doesn't exist for us. - Net effect: after the very first connect, `gdi->width`/`gdi->height` (and the backing `gdi->primary_buffer`) were **permanently stuck at the initial connect size** for the rest of the session, no matter how many successful `SendMonitorLayout` calls we made. - FreeRDP's own surface-bits handling makes the failure mode exactly match what was observed: `intersect_rect()` in `libfreerdp/gdi/gdi.c` **silently drops** any incoming update whose `destRight`/`destBottom` exceeds the stale `gdi->width`/`gdi->height` — so content in any newly-exposed area (e.g. a taskbar that moved because the guest *did* actually re-layout) never gets painted, leaving stale pixels behind. This also explains why the earlier `RefreshRect` mitigation didn't fully resolve it: re-requesting the same content just re-triggers the same rejection against the same stale bounds. ## Fix (implemented locally, not yet committed — see below) In `RdpSessionBackend::sendDisplayResize` (`src/rdp_session_backend.cpp`), right after `SendMonitorLayout` returns `CHANNEL_RC_OK`, we now proactively call `gdi_resize(instance->context->gdi, width, height)` ourselves and emit `remoteDesktopSizeChanged(width, height)` on success — instead of only doing this reactively from a callback that structurally can't fire for our channel/codec configuration. This mirrors exactly what `gdi_resize()` is designed for (it's the same call every reference FreeRDP client makes from its own `DesktopResize` handler) and is safe to call here since it runs on the same single worker thread that also drives FreeRDP's own event/paint processing — never concurrently with an in-flight paint. On failure it now logs a session event instead of silently leaving stale content. ## Status / what's still needed - Builds clean (Linux), basic app smoke-test passed (launches, no crash). - **Not yet verified against a live RDP session** — needs an actual connect + resize to confirm the taskbar/corruption symptom is gone, which needs a password I don't have access to. Whoever picks this up next should reproduce the original repro steps (toggle the events panel a few times) with this change applied. - Not yet committed/pushed — pending review since I can't verify live. If the guest's own display driver genuinely never applies the requested resolution at all (the original "VM synthetic driver" theory), this fix won't create *new* corruption — worst case is now blank/unfilled space in the extra area rather than dropped/garbled updates — but it also won't make the guest actually resize; that would need testing against a non-VM host to fully rule in/out, per the original issue's suggested next steps.
Author
Owner

Verified fixed

User tested the local build with this fix against the original repro (resizing the window / toggling the events panel several times during an active RDP session): taskbar now renders correctly after each resize, no more missing/misplaced taskbar or stale composited content.

Still uncommitted locally per standing process (only committing on explicit request), but the fix in RdpSessionBackend::sendDisplayResize (src/rdp_session_backend.cpp) is confirmed to resolve this issue.

## Verified fixed User tested the local build with this fix against the original repro (resizing the window / toggling the events panel several times during an active RDP session): taskbar now renders correctly after each resize, no more missing/misplaced taskbar or stale composited content. Still uncommitted locally per standing process (only committing on explicit request), but the fix in `RdpSessionBackend::sendDisplayResize` (src/rdp_session_backend.cpp) is confirmed to resolve this issue.
Sign in to join this conversation.