Blog · Engineering · · 4 min read
Why we rewrote it in Rust
The tools for putting a PC on a TV were thrown together and never dialed in. So we built our own path from capture to screen, in Rust on both ends, and measured every millisecond.
We wanted one simple thing. A Windows PC, an Android TV across the room, and the TV acting like a real second monitor that you can actually work on. You drag a window over, it shows up, and it feels like it's plugged in.
We tried the tools built for this, and they all worked in the loosest sense of the word. Latency was "fine". 4K was "kind of". And when a frame showed up late, nobody could tell you why, because the honest answer was that the frame went through 4 or 5 programs that were glued together and none of them was keeping score.
That last part is what pushed us over. You can't fix what you can't see, and you can't see anything when your hot path is someone else's black box.
So we owned the whole path
We rewrote it end to end, in Rust, on both sides of the Wi-Fi.
On the PC, the sender captures the virtual monitor with Windows Graphics Capture or Desktop Duplication straight into D3D11. A small shader turns BGRA into NV12 on the GPU. Then the frame goes into the NVENC SDK through FFI we wrote by hand. There's no ffmpeg in the hot path and no copy back to the CPU. The frame leaves the GPU as HEVC, AV1 or H.264 and goes on the wire.
On the TV, the app is native Rust too, a NativeActivity with no Java and no Kotlin. It hands the stream to the hardware decoder and puts the picture straight onto the TV's 4K video plane, with the MediaTek low-latency path switched on.
Between them there's a protocol we can read in one sitting. A 24-byte header, one small TCP stream for video, UDP for audio, and a stats channel coming back from the TV. Every frame carries the time it was captured, so the TV can say exactly where the milliseconds went.
Why Rust, really
People expect a speech about memory safety here, and sure, that's part of it. The bigger reason is that Rust let us write the scary parts once and trust them.
A streamer like this lives on FFI. D3D11, the NVENC SDK, WASAPI on the PC, MediaCodec and AAudio on the TV. Every one of those is a C API that'll happily let you hand it a dangling pointer at 60 frames a second. In Rust, the unsafe calls sit in small wrappers that we audit, and everything above them is ordinary code that the compiler checks for us.
It also kept everything small. The sender is a 1.4 MB binary. The tray app is Tauri with plain HTML, CSS and JS. We said no to Electron on day 1, because shipping a whole browser to move a desktop across a room felt like a joke.
How we measure
Owning the path only matters if you measure it, so every real-TV run goes in a run book. It logs fps on the TV, decode time, frame gaps, GPU load and the full capture-to-screen latency, which covers capture, encode, Wi-Fi, decode and display.
The rule is simple. Real TV, real home network, never a fake receiver on the same PC. We learned that one the hard way, because a fake TV on your own machine makes everything look fast and tells you nothing.
Here's where 0.3.0 landed on our test rig, a MediaTek MT5895 Android TV and an RTX 5070 Ti laptop on home Wi-Fi, with a moving test page full screen:
| Mode | Codec | fps on the TV | TV decode | Capture → on screen |
|---|---|---|---|---|
| 1080p60 · 40 Mb/s | HEVC | 59.9 | 5.5 ms | 17.6 ms |
| 1080p60 · 40 Mb/s | AV1 | 59.9 | 7.0 ms | 22.6 ms |
| 4K60 · 100 Mb/s | HEVC | 59.9 | 10.9 ms | 36.3 ms |
| 4K60 · 100 Mb/s | H.264 | 59.9 | 14.7 ms | 43.3 ms |
17.6 ms from the moment Windows hands us a frame to the moment it's lit on the TV. That's about 1 frame at 60 Hz, which is the number we cared about from the start.
The catch
We'll keep saying this until people believe us. If your network is trash, TunnelRez isn't for you. It pushes 40 to 150 Mb/s at a fixed rate with no padding, so a crowded 2.4 GHz band or a mesh node in the attic will show up on screen, and no codec on Earth can hide that.
If you keep a clean 5 or 6 GHz link with power save off, or better, a cable, it's built to outrun anything else we've tried. That's the deal, and we think it's a fair one.
Next up we're writing about audio, which turned out to be its own little war: 255 ms to 55 ms.