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
Connect to an RDP profile (reproduced against a Windows Server VM using
a virtual/synthetic display driver — architecture matters here, see
below).
Once connected, toggle the session's "Show Events"/"Hide Events" panel,
which resizes the embedded RDP display widget.
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).
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.csilently 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.
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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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
a virtual/synthetic display driver — architecture matters here, see
below).
which resizes the embedded RDP display widget.
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)
separately (
RdpDisplayWidget::setRemoteDesktopSizenow clears theframe during a resize transition instead of drawing the old one scaled
incorrectly).
resize requests are now debounced (150ms), coalescing bursts (e.g. the
initial connect/layout-settling burst) into one request.
RefreshRectrequest after each resize completes as abest-effort nudge — did not fully resolve the issue in testing.
BeginPaint/EndPaint/DesktopResizeare hooked insrc/rdp_session_backend.cpp(no separate GFX/Progressive-codec pipelinethat could be bypassing our frame-capture) — the client appears to be
faithfully snapshotting whatever the server's own GDI buffer contains at
EndPainttime.Leading theory (unconfirmed)
The remote guest's actual desktop resolution may not be changing even
though
sendDisplayResize'sSendMonitorLayoutcall 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
display driver, to determine whether this is host/driver-specific.
orbitDesktopResize'sremoteDesktopSizeChangedsignal(src/rdp_session_backend.cpp, wired to
RdpDisplayWidget::setRemoteDesktopSizein src/session_tab.cpp) actuallyfires 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."
the
RefreshRectfollow-up (currently sent immediately) would give slowhosts 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).
Root cause found (via source-level tracing of FreeRDP, no live repro needed)
Traced why
update->DesktopResize(orbitDesktopResizeinsrc/rdp_session_backend.cpp, the only place we ever calledgdi_resize()) essentially never fires for our resize path:update->DesktopResizeis invoked from exactly two places in FreeRDP:libfreerdp/core/connection.cduring the Deactivation-Reactivation PDU sequence (a full protocol renegotiation), andlibfreerdp/gdi/gfx.con aRDPGFX_CMDID_RESETGRAPHICSPDU — which only exists on the GFX/Graphics Pipeline virtual channel.FreeRDP_SupportGraphicsPipelineis never set anywhere insrc/rdp_session_backend.cpp) — confirmed by grep, matches this issue's earlier note that "no separate GFX/Progressive-codec pipeline" is hooked.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) callsDesktopResize. Without GFX, that entire notification path doesn't exist for us.gdi->width/gdi->height(and the backinggdi->primary_buffer) were permanently stuck at the initial connect size for the rest of the session, no matter how many successfulSendMonitorLayoutcalls we made.intersect_rect()inlibfreerdp/gdi/gdi.csilently drops any incoming update whosedestRight/destBottomexceeds the stalegdi->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 earlierRefreshRectmitigation 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 afterSendMonitorLayoutreturnsCHANNEL_RC_OK, we now proactively callgdi_resize(instance->context->gdi, width, height)ourselves and emitremoteDesktopSizeChanged(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 whatgdi_resize()is designed for (it's the same call every reference FreeRDP client makes from its ownDesktopResizehandler) 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
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.
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.