<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Tugay PALA</title>
    <description>The latest articles on DEV Community by Tugay PALA (@cetus26).</description>
    <link>https://dev.to/cetus26</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4056773%2Fece79fa9-7ab0-4f0d-8e3a-a3168fe00410.jpg</url>
      <title>DEV Community: Tugay PALA</title>
      <link>https://dev.to/cetus26</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/cetus26"/>
    <language>en</language>
    <item>
      <title>I rendered FreeRDP through Metal to make remote Windows feel native on macOS</title>
      <dc:creator>Tugay PALA</dc:creator>
      <pubDate>Fri, 31 Jul 2026 14:24:51 +0000</pubDate>
      <link>https://dev.to/cetus26/i-rendered-freerdp-through-metal-to-make-remote-windows-feel-native-on-macos-5950</link>
      <guid>https://dev.to/cetus26/i-rendered-freerdp-through-metal-to-make-remote-windows-feel-native-on-macos-5950</guid>
      <description>&lt;h2&gt;
  
  
  The problem: remote Windows on a Mac feels like a slideshow
&lt;/h2&gt;

&lt;p&gt;I write networking software for a living, and my daily setup is a few Linux servers over SSH plus one Windows box I have to RDP into. On macOS the official option is Microsoft's Remote Desktop app (now "Windows App"), and it works — but under load it often feels like watching a slideshow. Scrolling a long log, dragging a window, anything with motion, and the latency is right there in your face.&lt;/p&gt;

&lt;p&gt;I wanted to know &lt;em&gt;why&lt;/em&gt;, and whether I could do better inside my own app. This post is the honest version of what I learned.&lt;/p&gt;

&lt;h2&gt;
  
  
  First attempts: CoreGraphics and IOSurface
&lt;/h2&gt;

&lt;p&gt;My first RDP prototype decoded frames from FreeRDP and pushed them to the screen as &lt;code&gt;CGImage&lt;/code&gt;s. It worked on paper and was miserable in practice: every frame meant a CPU-side copy and a full layer redraw. On a 1440p remote session the main thread was the bottleneck long before the network was.&lt;/p&gt;

&lt;p&gt;I moved to &lt;code&gt;IOSurface&lt;/code&gt; to cut some copies out. Better, but I was still fighting the compositor for every update, and partial-update regions (the thing that makes RDP efficient) weren't buying me much because I kept re-uploading whole frames.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually worked: CAMetalLayer + a Metal texture
&lt;/h2&gt;

&lt;p&gt;The version that finally felt right treats the remote desktop as what it is — a texture that changes over time.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;CAMetalLayer&lt;/code&gt; backs the view.&lt;/li&gt;
&lt;li&gt;FreeRDP decodes into a buffer; I upload it into an &lt;code&gt;MTLTexture&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Damaged regions from the RDP graphics pipeline become partial texture updates instead of full re-uploads.&lt;/li&gt;
&lt;li&gt;A trivial fragment shader draws the texture to a full-screen quad.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Suddenly the CPU is mostly idle, the GPU does the compositing it's built for, and remote Windows scrolls the way it should. Scrolling long logs and dragging windows at 1440p went from visibly choppy to fluid — the difference is obvious the moment you grab a window and shake it.&lt;/p&gt;

&lt;p&gt;The lesson wasn't "Metal is magic." It was: stop treating a video-like stream as a pile of images, and let the part of the machine designed for textures do the work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The SSH side had its own surprises
&lt;/h2&gt;

&lt;p&gt;For SSH I used Apple's &lt;code&gt;swift-nio-ssh&lt;/code&gt;. Clean, modern, pure-Swift — and missing &lt;code&gt;keyboard-interactive&lt;/code&gt; auth, which a lot of real-world servers still lean on. My workaround so far: when there's no saved password (or it's rejected), I pause the handshake and prompt the user for a password right there, then retry password auth — which covers most servers that would otherwise trip on it. True &lt;code&gt;keyboard-interactive&lt;/code&gt; support is still blocked at the library level. If you're building on NIO SSH, budget time for the auth edge cases; the happy path is lovely, the long tail is where the work is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why put SSH, SFTP and RDP in one window
&lt;/h2&gt;

&lt;p&gt;Once the RDP engine was smooth, keeping it in a separate app made no sense. My real workflow is: watch a deploy on three servers, drop a file onto one over SFTP, jump to the Windows box to check something. Three apps, three Cmd-Tabs.&lt;/p&gt;

&lt;p&gt;So I built the whole thing into one native SwiftUI app — sessions laid out in a grid so I can &lt;em&gt;see&lt;/em&gt; several at once, dual-pane SFTP with drag-and-drop, and RDP in the same window. I called it &lt;strong&gt;Noden&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;There's also an optional AI assistant, but with a deliberate constraint: it &lt;em&gt;proposes&lt;/em&gt; a command and waits for me to approve it. It never runs anything on its own. Bring your own API key or use the built-in one, and turn it off entirely if you don't want it.&lt;/p&gt;

&lt;p&gt;Honest limits, because this audience deserves them: it's macOS only, there's no VNC yet, and no iOS app. If you need cross-platform, other tools fit better.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it / tell me I'm wrong
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Site: &lt;a href="https://noden.useroamteknoloji.com" rel="noopener noreferrer"&gt;https://noden.useroamteknoloji.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Install: &lt;code&gt;brew install --cask useroamteknoloji/tap/noden&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Free up to 5 connections, Pro is $10/year (RDP is in the free tier).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've shipped an RDP or SSH client and solved these differently, I genuinely want to hear it — the Metal path was trial and error and I'm sure there's more to squeeze.&lt;/p&gt;

</description>
      <category>macos</category>
      <category>swift</category>
      <category>rdp</category>
      <category>ssh</category>
    </item>
  </channel>
</rss>
