Skip to content
← All articles
Project introduction

Artemis Switch: following a frame from the network to the screen

Listen to the audio story

Where did this frame wait? · 2:26 · AI-written script, synthetic voices

Transcript

Ava: A game stream can report a perfectly healthy bitrate and still feel late. So what's actually going on?

Andrew: I'd guess the network. It's always the network.

Ava: Not always. Antoine's project, Artemis Switch, is his fork of Moonlight-Switch for the Nintendo Switch, and it focuses on one thing: the path between receiving a frame and putting it on screen. The question isn't how fast the decoder is. It's where each frame waited.

Andrew: Okay, so what does the pipeline look like?

Ava: A PC host, Sunshine or Apollo, sends video over the Moonlight protocol. The Switch decodes it with FFmpeg and NVDEC, drops it in a queue, picks a frame, and presents it with deko3d. And that split matters. A wrong virtual display mode is a host problem. A decoded-frame queue that keeps growing is a client problem.

Andrew: That's a handy way to know who to blame.

Ava: It is. Now here's the subtle part. When the queue has a backlog, the code throws away old frames to favor freshness. But it checks the oldest frame's deadline first, before jumping to the newest one.

Andrew: Why does that order matter?

Ava: Picture a bouncer who resets the wait time every time someone new walks in. If the gate follows the newest frame, its deadline keeps moving forward. Frames keep arriving, but nothing ever gets shown. Keeping the oldest deadline avoids that starvation.

Andrew: Huh. That's a tiny rule with a big effect.

Ava: And there's a telemetry class that measures each stage separately: decode time, queue wait, render cost, GPU submission, real video bitrate. Antoine is careful about what it covers, though. The client pipeline starts after the network finishes. It doesn't include host encoding, display scanout or controller delay.

Andrew: So no magic latency number.

Ava: No. His advice for testing on hardware is to keep the host, scene, resolution and network fixed, change one pacing option at a time, and test handheld and docked separately. A lower average with worse spikes can feel worse.

Andrew: And compiling isn't the same as playing well, I assume.

Ava: Exactly. The README itself separates portable tests and release builds from real device validation. The takeaway: measure where the frame waits, not just how fast things decode. The full write-up has the pipeline diagram and links to the two source files.

Artemis Switch application library, from the repository README
Table of contents

A stream can report a healthy bitrate and still feel late. Artemis Switch is my Moonlight-Switch fork for Nintendo Switch, focused on the path between receiving a frame and presenting it. The useful question is not just “how fast is the decoder?” but “where did this frame wait?”

1. A client with a clear boundary

Artemis builds on XITRIX/Moonlight-Switch: Borealis for the interface, the Moonlight/GameStream protocol for transport, FFmpeg/NVDEC for decoding, and deko3d for Switch presentation. Sunshine and Apollo remain PC hosts. Host administration stays in their own interfaces.

This distinction matters when debugging. A wrong virtual-display mode is a host issue; a growing decoded-frame queue belongs to the client. The repository documents host integrations, but that is not evidence that every host/device combination has passed hardware validation.

2. The frame pipeline

flowchart TD
  H["PC host: Sunshine or Apollo"] --> N["Moonlight / GameStream transport"]
  N --> D["FFmpeg / NVDEC decode"]
  D --> Q["Decoded-frame queue"]
  Q --> S["Deadline and freshness selection"]
  S --> R["deko3d presentation"]
  R --> P["Switch display"]
  T["FramePipelineTelemetry"] -. measures .-> D
  T -. measures .-> Q
  T -. measures .-> R

This is a simplified reading of the streaming code, not a claim about measured end-to-end input latency.

3. The subtle part: when to discard old frames

LowLatencyFrameSelection.hpp[1] checks the oldest frame’s deadline before advancing to the newest frame. Once the queue is eligible, it discards older frames and favors freshness. A backlog can bypass the ordinary gate so a stalled renderer catches up.

Why preserve that oldest deadline? If the gate follows the newest arriving frame, its deadline can keep moving forward. The queue receives frames, yet presentation starves. This small policy is more informative than a generic “low latency” label.

4. Measure stages separately

FramePipelineTelemetry.hpp[2] records decode timing, queue wait, client pipeline timing, render and GPU submission cost, actual video bitrate, and first-packet/decode/present milestones. It uses a monotonic clock and protects shared state with a mutex.

“Client pipeline” begins after network completion in this implementation. It does not include every host encoding, network transit, display scanout, and controller delay.

The README also describes Fit/Fill/Stretch presentation, FSR-style upscaling, RCAS sharpening, named stream profiles, and a performance overlay. These are controls to investigate tradeoffs, not a universal FPS promise.

5. What I would compare on hardware

Keep the host, scene, resolution, and network fixed. Compare queue wait and present intervals with pacing options changed one at a time. Then test handheld and docked presentation separately. A lower average with worse spikes may still feel worse.

The project remains active. Its README explicitly separates portable tests and release builds from device validation. Read the installation and status notes before treating a compile result as a gameplay result.

6. Source trail

References

  1. antoinebou12. (). LowLatencyFrameSelection.hpp. [Source file or documentation] github.com.
  2. antoinebou12. (). FramePipelineTelemetry.hpp. [Source file or documentation] github.com.
  3. antoinebou12. (). Artemis-Switch. [Software repository] github.com.

References use this article’s publication date.