You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
ironrdp-web becomes unresponsive when remote framebuffer updated rapidly #790
When accessing testufo.com through a browser on a remote RDP server using ironrdp-web version 0.9.1, lag increases until the remote session is unresponsive. Similar issues occur when attempting to play video in the remote session.
The severity of lag correlates directly with the size and update frequency of the moving area displayed on screen. Session responsiveness can be restored by either obscuring the moving area with another window or activating the Windows ctrl-alt-del screen (which also obscures the moving area).
Note that the Microsoft native RDP client handles testufo.com and remote video content adequately under identical network conditions.
changed the title [-]ironrdp-web becomes unresponsive when remote framebuffer update rapidly[/-][+]ironrdp-web becomes unresponsive when remote framebuffer updated rapidly[/+]on May 15, 2025
I've been looking at this issue from a different angle. I maintain a server-side IronRDP implementation and have been evaluating ironrdp-web for a browser-based console client (Proxmox VE integration is my immediate target). WASM is a key technology for me i the long term, but I haven;t been able to buy in to the Ironrdp-web approach.
It's apparent the root cause of the issues you raise is architectural rather than a single fixable bug:
Single-threaded execution: all PDU parsing, decode, pixel conversion, and Canvas rendering runs on the main thread via spawn_local(). Under high framebuffer update rates, decode work starves input handling and requestAnimationFrame callbacks.
CPU-only rendering: the softbuffer -> Canvas path does manual RGBA to u32 conversion for every pixel. At 1080p30 that's ~6 MB/s of pixel data through JavaScript, with no GPU involvement.
No EGFX channel: ironrdp-web registers only DisplayControlClient as a DVC and advertises empty codec capabilities (client_codecs_capabilities(&[])). Servers fall back to legacy bitmap/RemoteFX, which is significantly less efficient than EGFX with H.264. The bandwidth difference noted in Add GFX ([MS-RDPEGFX]) support #427 (2.5 Mbps IronRDP vs 250 Kbps FreeRDP) is likely related -- FreeRDP negotiates EGFX/H.264 while ironrdp-web gets uncompressed bitmaps.
For my own use case I've decided to build a separate WASM client crate on top of the IronRDP protocol layer (ironrdp-connector, ironrdp-session, ironrdp-egfx) rather than extending ironrdp-web. The architecture I'm targeting:
Web Worker for PDU parsing and decode (keeps main thread responsive)
WebCodecs VideoDecoder for hardware-accelerated H.264 (GPU handles YUV->RGB)
This would address all three root causes above. Whether it makes sense to retrofit this into ironrdp-web or keep it as a separate crate is an open question, although I doubt that appeals to everyone as the right way to go, since the single-threaded model works fine for Cloudflare's server-side decode architecture, and the worker thread approach adds real complexity.
I plan to open a couple of issues about this once PR #1103 lands: one for EGFX channel registration in ironrdp-web (small, incremental), and one as an RFC for the WebCodecs/worker thread architecture (larger discussion).
I'd love to know your thoughts on these points as a maintainer of a working solution. This is definitely something I'm planning to do in the short term, and seeing how we can merge this existing implementation into a more flexible architecture, or simply working out how they best coexist, is a primary concern of mine.
Greg Lamberson (@glamberson) I’m really interested to see how your WASM client will turns out. You’re actually exploring spaces I’ve been willing to try out without finding the time to actually do it. For now, I think it’s good that you iterate with your own crate for maximum velocity (we have our own constrains requiring careful considerations, and a migration path), but I’m definitely interested in hearing and seeing the result you converge into. This will guide how we make ironrdp-web evolve into something better.
Benoît Cortier (@CBenoit) Thanks. First-class, robust WASM support is one of my secret obsessions actually, so stay tuned as I've been working on this for quite awhile now. I'm excited to make this public soon and see what we can do to advance this technology.
The WASM client is built. ~4,600 LOC, MIT-licensed, working E2E against a Linux RDP server with H.264 EGFX over Worker-thread decode and Canvas2D drawImage(VideoFrame) rendering. The three diagnostic conclusions from my March comment held up empirically: worker decode keeps the main thread responsive, WebCodecs plus drawImage(VideoFrame) on a Canvas2D context is GPU-fast (no wgpu needed for the baseline), and EGFX channel registration is what unlocks the bandwidth profile. Both RDCleanPath (proxied) and direct-connect modes work for the connector handshake. Builds clean against current IronRDP master (commit 0282d182).
I want to put this in your hands. Three reasonable submission shapes; the codebase supports any of them.
Submission options
Option 1: absorb as ironrdp-wasm. Move the crate into crates/ironrdp-wasm in the IronRDP workspace as a sibling to ironrdp-web. I rebrand and license-align, file the PR series that incrementally introduces the crate (probably 4-6 PRs: scaffold, EGFX handler, WebCodecs codec, transport, connection sequence, integration). Devolutions owns it from then on. ironrdp-web stays as-is for the existing Cloudflare gateway use case; ironrdp-wasm serves the direct-connect, EGFX, video-heavy use case.
Option 2: companion crate, more explicit positioning. Same as Option 1 but with documented positioning: ironrdp-web for the simple / gateway-decode model (Cloudflare's deployment shape); ironrdp-wasm for the worker-thread plus WebCodecs model. Both ship, README explains when to pick which. Possibly share helper modules.
Option 3: stays external (although I don't prefer it). I keep ownership under lamco-admin/lamco-rdp-wasm, contribute upstream changes to ironrdp-web and protocol crates as needed, you reference it as a working WASM client implementation.
My take: Option 1 or 2. The worker-thread, WebCodecs, no-wgpu-needed-for-baseline choices feel like they should have first-class IronRDP support rather than existing as a third-party implementation that has to track upstream. Option 3 is workable, but I'd rather not land there if Devolutions has the bandwidth for either of the first two.
Adjacent PR I could file separately
Independent of the placement decision: feat(web): register EGFX dynamic virtual channel in ironrdp-web is the narrow ironrdp-web-side change that would address the framebuffer-update lag described in this issue from the existing-architecture side. Gabriel Bauman (@gabrielbauman), this might be the more directly useful piece for ironrdp-web going forward, regardless of where lamco-rdp-wasm lands. The diff would be ~150-250 LOC: minimal GraphicsPipelineHandler impl, GraphicsPipelineClient registration alongside DisplayControlClient, populated client_codecs_capabilities. I have a working handler in the lamco-rdp-wasm POC that ports cleanly to ironrdp-web's threading model. Happy to file this as a PR if useful; let me know.
The crate itself
Currently lives in a private dev repository, per my standard practice for work-in-progress projects (same pattern as lamco-rdp-server-dev then lamco-rdp-server). It stays private until a placement decision is made. Once we have a direction:
Option 1 or 2: I push contents to a branch on Devolutions/IronRDP for review. No public Lamco mirror needed at any point.
Option 3: I publish lamco-admin/lamco-rdp-wasm via the same export pipeline I use for lamco-rdp-server.
In the meantime, happy to share the source through whatever private channel works (private repo invitation, tarball, anything). The architecture overview above plus the design-decisions doc and the SDS are the highest-leverage things to evaluate when picking a direction; I can share any subset privately on request.
What I need from you
A direction on placement (one of the three options, or a fourth I haven't thought of) and a yes/no on the adjacent PR. Whatever you decide, I'll execute.
I’m not sure what is the difference between option 1 and 2. I understand that in both cases we end up with two crates: ironrdp-web (current one) and ironrdp-wasm; is that correct?
In my mind, the ideal option would be a fourth one: we keep ironrdp-web and change the internals to use the worker-thread + wgpu approach, and crucially, we keep the current API surface of ironrdp-web.
Generally speaking, do you think there is a reason to keep the softbuffer backend? If the wgpu one works well enough, it may be the only one we keep supporting. I think it’s a reasonable requirement that modern browsers typically provide.
Furthermore, if you would rather have a separate API surface for the lamco web client, we could have ironrdp-wasm as a library which does not expose anything but the Rust code to be called from ironrdp-web, and ironrdp-web ends up being a thin layer connecting iron-remote-desktop and ironrdp-wasm together.
In that world, you could have your own lamcordp-web crate, as a thin layer on top of ironrdp-wasm.
Right, Options 1 and 2 were the same architecture under two different positioning frames. The goal was to give the existing ironrdp-web implementation the maximum flexibility about where it lives. Merging into a layered Option 4 is a better outcome.
To unpack the rendering stack: I want both wgpu and WebCodecs plus Canvas2D. They cover different content. WebCodecs decode plus drawImage(VideoFrame) on a Canvas2D context handles the H.264 EGFX path quickly in modern browsers without needing wgpu in the picture; the lamco-rdp-wasm POC empirically validates this. wgpu becomes the right choice when content does not arrive as a decodable video stream: non-EGFX bitmap updates, RemoteFX, raw framebuffer copies, mixed compositing. The two rendering paths are not exclusive. The right answer is a renderer abstraction that dispatches per content type. So agreed on the wgpu direction, just not as the only path.
On softbuffer: it is not in my codebase's path, so I have no attachment to keeping it. I would defer to Gabriel Bauman (@gabrielbauman) though. He has the deployed ironrdp-web use case, and if Cloudflare's gateway shape needs the softbuffer fallback for any reason (older browsers, environments with wgpu disabled, something I haven't thought of), that should drive the call rather than my preference.
On the layered shape: yes, I like the long-term flexibility. To make the design conversation concrete, here is the rough split I would propose, mapped against what is in the lamco-rdp-wasm codebase today:
ironrdp-wasm (pure Rust library, no wasm-bindgen exports):
Connector and connection sequence: wraps ironrdp-connector, ironrdp-session, and the channel registrations including EGFX and DisplayControl.
Codec capabilities negotiation and dispatch by content type.
Renderer trait with three implementations: Canvas2DVideoFrameRenderer (WebCodecs decode plus drawImage(VideoFrame)), WgpuRenderer (non-video content and future EGFX paths that do not use WebCodecs), and optionally SoftbufferRenderer (the existing path; ship or cut depending on Gabriel Bauman (@gabrielbauman)'s call).
Transport trait with implementations for direct WebSocket and RDCleanPath. The facade picks which.
Decode and render entry points designed to be called from a Worker context. The library does not spawn the Worker; it is structured so the facade can.
ironrdp-web (existing facade, thin layer over ironrdp-wasm):
Keeps the current iron-remote-desktop wasm-bindgen API surface verbatim. No consumer migration.
Owns the Worker thread spawning and the DOM-side integration: canvas binding, input event marshaling, lifecycle hooks.
Default config maps to the Cloudflare gateway shape: RDCleanPath transport, the renderer Gabriel Bauman (@gabrielbauman) wants for the gateway use case, no EGFX in the default capability set.
New lamco facade (separate crate, separately licensed and maintained on the lamco side):
Different wasm-bindgen API surface tailored to the lamco product UX: lifecycle hooks, error reporting model, multi-monitor handling, branding configuration knobs.
Same Worker plus DOM integration patterns, reusing ironrdp-wasm for everything below the facade.
Default config maps to the lamco deployment shape: direct WebSocket transport, EGFX enabled by default, WebCodecs renderer with wgpu fallback for non-video frames.
The split lets iron-remote-desktop stay stable for Cloudflare's existing consumers (no migration burden), lets the lamco product ship its own API without asking the IronRDP project to bend to UX needs specific to lamco, and keeps the protocol, decode, and render work in one place so improvements flow to every facade.
To answer my own earlier scoping question on the connector lifecycle: in this shape the library owns it, the facade just provides the transport and the renderer. That keeps the state machine work (which has security implications) uniform across facades and avoids each facade reimplementing it.
For next steps: if the shape above looks roughly right, I can post a file and module level proposal that maps the lamco-rdp-wasm source tree to these three crates concretely. Easier to settle the library and facade scope against an actual proposal than in the abstract. Or if you want to redraw the boundaries, that's fine; I will redraft against whatever shape you prefer.
Activity
[-]ironrdp-web becomes unresponsive when remote framebuffer update rapidly[/-][+]ironrdp-web becomes unresponsive when remote framebuffer updated rapidly[/+]glamberson commentedon Mar 13, 2026
I've been looking at this issue from a different angle. I maintain a server-side IronRDP implementation and have been evaluating ironrdp-web for a browser-based console client (Proxmox VE integration is my immediate target). WASM is a key technology for me i the long term, but I haven;t been able to buy in to the Ironrdp-web approach.
It's apparent the root cause of the issues you raise is architectural rather than a single fixable bug:
Single-threaded execution: all PDU parsing, decode, pixel conversion, and Canvas rendering runs on the main thread via
spawn_local(). Under high framebuffer update rates, decode work starves input handling and requestAnimationFrame callbacks.CPU-only rendering: the softbuffer -> Canvas path does manual RGBA to u32 conversion for every pixel. At 1080p30 that's ~6 MB/s of pixel data through JavaScript, with no GPU involvement.
No EGFX channel: ironrdp-web registers only DisplayControlClient as a DVC and advertises empty codec capabilities (
client_codecs_capabilities(&[])). Servers fall back to legacy bitmap/RemoteFX, which is significantly less efficient than EGFX with H.264. The bandwidth difference noted in Add GFX ([MS-RDPEGFX]) support #427 (2.5 Mbps IronRDP vs 250 Kbps FreeRDP) is likely related -- FreeRDP negotiates EGFX/H.264 while ironrdp-web gets uncompressed bitmaps.For my own use case I've decided to build a separate WASM client crate on top of the IronRDP protocol layer (ironrdp-connector, ironrdp-session, ironrdp-egfx) rather than extending ironrdp-web. The architecture I'm targeting:
This would address all three root causes above. Whether it makes sense to retrofit this into ironrdp-web or keep it as a separate crate is an open question, although I doubt that appeals to everyone as the right way to go, since the single-threaded model works fine for Cloudflare's server-side decode architecture, and the worker thread approach adds real complexity.
I plan to open a couple of issues about this once PR #1103 lands: one for EGFX channel registration in ironrdp-web (small, incremental), and one as an RFC for the WebCodecs/worker thread architecture (larger discussion).
I'd love to know your thoughts on these points as a maintainer of a working solution. This is definitely something I'm planning to do in the short term, and seeing how we can merge this existing implementation into a more flexible architecture, or simply working out how they best coexist, is a primary concern of mine.
Thanks,
Greg Lamberson
Lamco Development
CBenoit commentedon Mar 13, 2026
Greg Lamberson (@glamberson) I’m really interested to see how your WASM client will turns out. You’re actually exploring spaces I’ve been willing to try out without finding the time to actually do it. For now, I think it’s good that you iterate with your own crate for maximum velocity (we have our own constrains requiring careful considerations, and a migration path), but I’m definitely interested in hearing and seeing the result you converge into. This will guide how we make
ironrdp-webevolve into something better.glamberson commentedon Mar 13, 2026
Benoît Cortier (@CBenoit) Thanks. First-class, robust WASM support is one of my secret obsessions actually, so stay tuned as I've been working on this for quite awhile now. I'm excited to make this public soon and see what we can do to advance this technology.
glamberson commentedon May 16, 2026
Benoît Cortier (@CBenoit), Gabriel Bauman (@gabrielbauman): circling back as I said I would.
The WASM client is built. ~4,600 LOC, MIT-licensed, working E2E against a Linux RDP server with H.264 EGFX over Worker-thread decode and Canvas2D
drawImage(VideoFrame)rendering. The three diagnostic conclusions from my March comment held up empirically: worker decode keeps the main thread responsive, WebCodecs plusdrawImage(VideoFrame)on a Canvas2D context is GPU-fast (no wgpu needed for the baseline), and EGFX channel registration is what unlocks the bandwidth profile. Both RDCleanPath (proxied) and direct-connect modes work for the connector handshake. Builds clean against current IronRDP master (commit0282d182).I want to put this in your hands. Three reasonable submission shapes; the codebase supports any of them.
Submission options
Option 1: absorb as
ironrdp-wasm. Move the crate intocrates/ironrdp-wasmin the IronRDP workspace as a sibling toironrdp-web. I rebrand and license-align, file the PR series that incrementally introduces the crate (probably 4-6 PRs: scaffold, EGFX handler, WebCodecs codec, transport, connection sequence, integration). Devolutions owns it from then on.ironrdp-webstays as-is for the existing Cloudflare gateway use case;ironrdp-wasmserves the direct-connect, EGFX, video-heavy use case.Option 2: companion crate, more explicit positioning. Same as Option 1 but with documented positioning:
ironrdp-webfor the simple / gateway-decode model (Cloudflare's deployment shape);ironrdp-wasmfor the worker-thread plus WebCodecs model. Both ship, README explains when to pick which. Possibly share helper modules.Option 3: stays external (although I don't prefer it). I keep ownership under
lamco-admin/lamco-rdp-wasm, contribute upstream changes toironrdp-weband protocol crates as needed, you reference it as a working WASM client implementation.My take: Option 1 or 2. The worker-thread, WebCodecs, no-wgpu-needed-for-baseline choices feel like they should have first-class IronRDP support rather than existing as a third-party implementation that has to track upstream. Option 3 is workable, but I'd rather not land there if Devolutions has the bandwidth for either of the first two.
Adjacent PR I could file separately
Independent of the placement decision:
feat(web): register EGFX dynamic virtual channel in ironrdp-webis the narrow ironrdp-web-side change that would address the framebuffer-update lag described in this issue from the existing-architecture side. Gabriel Bauman (@gabrielbauman), this might be the more directly useful piece for ironrdp-web going forward, regardless of where lamco-rdp-wasm lands. The diff would be ~150-250 LOC: minimalGraphicsPipelineHandlerimpl,GraphicsPipelineClientregistration alongsideDisplayControlClient, populatedclient_codecs_capabilities. I have a working handler in the lamco-rdp-wasm POC that ports cleanly to ironrdp-web's threading model. Happy to file this as a PR if useful; let me know.The crate itself
Currently lives in a private dev repository, per my standard practice for work-in-progress projects (same pattern as
lamco-rdp-server-devthenlamco-rdp-server). It stays private until a placement decision is made. Once we have a direction:Devolutions/IronRDPfor review. No public Lamco mirror needed at any point.lamco-admin/lamco-rdp-wasmvia the same export pipeline I use forlamco-rdp-server.In the meantime, happy to share the source through whatever private channel works (private repo invitation, tarball, anything). The architecture overview above plus the design-decisions doc and the SDS are the highest-leverage things to evaluate when picking a direction; I can share any subset privately on request.
What I need from you
A direction on placement (one of the three options, or a fourth I haven't thought of) and a yes/no on the adjacent PR. Whatever you decide, I'll execute.
CBenoit commentedon May 18, 2026
Hi!
I’m not sure what is the difference between option 1 and 2. I understand that in both cases we end up with two crates:
ironrdp-web(current one) andironrdp-wasm; is that correct?In my mind, the ideal option would be a fourth one: we keep
ironrdp-weband change the internals to use the worker-thread + wgpu approach, and crucially, we keep the current API surface ofironrdp-web.Generally speaking, do you think there is a reason to keep the softbuffer backend? If the wgpu one works well enough, it may be the only one we keep supporting. I think it’s a reasonable requirement that modern browsers typically provide.
Furthermore, if you would rather have a separate API surface for the lamco web client, we could have
ironrdp-wasmas a library which does not expose anything but the Rust code to be called fromironrdp-web, andironrdp-webends up being a thin layer connectingiron-remote-desktopandironrdp-wasmtogether.In that world, you could have your own
lamcordp-webcrate, as a thin layer on top ofironrdp-wasm.How does that sound?
glamberson commentedon May 18, 2026
Right, Options 1 and 2 were the same architecture under two different positioning frames. The goal was to give the existing
ironrdp-webimplementation the maximum flexibility about where it lives. Merging into a layered Option 4 is a better outcome.To unpack the rendering stack: I want both wgpu and WebCodecs plus Canvas2D. They cover different content. WebCodecs decode plus
drawImage(VideoFrame)on a Canvas2D context handles the H.264 EGFX path quickly in modern browsers without needing wgpu in the picture; the lamco-rdp-wasm POC empirically validates this. wgpu becomes the right choice when content does not arrive as a decodable video stream: non-EGFX bitmap updates, RemoteFX, raw framebuffer copies, mixed compositing. The two rendering paths are not exclusive. The right answer is a renderer abstraction that dispatches per content type. So agreed on the wgpu direction, just not as the only path.On softbuffer: it is not in my codebase's path, so I have no attachment to keeping it. I would defer to Gabriel Bauman (@gabrielbauman) though. He has the deployed
ironrdp-webuse case, and if Cloudflare's gateway shape needs the softbuffer fallback for any reason (older browsers, environments with wgpu disabled, something I haven't thought of), that should drive the call rather than my preference.On the layered shape: yes, I like the long-term flexibility. To make the design conversation concrete, here is the rough split I would propose, mapped against what is in the lamco-rdp-wasm codebase today:
ironrdp-wasm(pure Rust library, no wasm-bindgen exports):ironrdp-connector,ironrdp-session, and the channel registrations including EGFX and DisplayControl.Canvas2DVideoFrameRenderer(WebCodecs decode plusdrawImage(VideoFrame)),WgpuRenderer(non-video content and future EGFX paths that do not use WebCodecs), and optionallySoftbufferRenderer(the existing path; ship or cut depending on Gabriel Bauman (@gabrielbauman)'s call).ironrdp-web(existing facade, thin layer overironrdp-wasm):iron-remote-desktopwasm-bindgen API surface verbatim. No consumer migration.New lamco facade (separate crate, separately licensed and maintained on the lamco side):
ironrdp-wasmfor everything below the facade.The split lets
iron-remote-desktopstay stable for Cloudflare's existing consumers (no migration burden), lets the lamco product ship its own API without asking the IronRDP project to bend to UX needs specific to lamco, and keeps the protocol, decode, and render work in one place so improvements flow to every facade.To answer my own earlier scoping question on the connector lifecycle: in this shape the library owns it, the facade just provides the transport and the renderer. That keeps the state machine work (which has security implications) uniform across facades and avoids each facade reimplementing it.
For next steps: if the shape above looks roughly right, I can post a file and module level proposal that maps the lamco-rdp-wasm source tree to these three crates concretely. Easier to settle the library and facade scope against an actual proposal than in the abstract. Or if you want to redraw the boundaries, that's fine; I will redraft against whatever shape you prefer.