• Skip to main content
  • Skip to primary sidebar
  • Skip to footer
  • Blog
  • Now
    • .plan
  • Resources
    • Tools List
    • Artists
  • Projects
  • Contact

M▵RK

TECHNOGENESIS // ON THE NATURE OF THINGS

Blog

March 2, 2025 by Mark Hamilton Leave a Comment

Headless NVIDIA 4K@120Hz Streaming on Ubuntu 24.04

Featured Image: 6th Generation iPad Mini running Cyberpunk 2077 over Moonlight

Here’s how to configure your Ubuntu 24.04 LTS computer for high framerate Moonlight streaming. This can be comfortable as a general remote desktop work, FPS gaming, or working with 3D graphics than SteamLink. For me, this was great for space saving and noise isolation since my host PC can now go in the closet with my networking gear.

This guide comes with a few caveats:

  1. This is an unusual setup and is not meant for beginners.
  2. I’ve had both Sunshine and Moonlight trigger kernel panics from not configuring them properly.
  3. This setup might open you up to vulnerabilities—the uinput configuration bothers me a little. If you have a relatively secure home network and a trusted computing setup you may be fine.
  4. You must keep the host on a wired connection. If your host computer is on wifi, consider this setup a non-starter. Clients may be wireless, however.
  5. I am able to get this to work over long distances with Tailscale, but it introduces instability issues that sometimes cause games and applications to crash (perhaps because of how NvFBC works or the MTU over VPN.)

This guide was compiled and verified using the following hardware:

  • NVIDIA GeForce RTX 4060 Ti
  • AMD Ryzen 7 7700 8-Core Processor
  • MikroTik 8-port Cloud Core Router, and a wired gigabit ethernet connection for the host PC
  • Generic Netgear router that’s being used in a WAP configuration for my wireless clients

The following software works flawlessly:

  • OS: Vanilla Ubuntu 24.04.1 LTS
  • Display Manager: GDM3
  • Applications: Cyberpunk 2077, Doom Eternal, Blender, Spotify, VSCode
  • WM: Wayland/GNOME desktop, xfce4.
  • Initialization: gdm3 or manually starting X.

It works with Moonlight clients on the latest versions of iOS, iPadOS, Ubuntu, Windows, and MacOS.

Similar Guides and Setups

  • Remote SSH Headless Sunshine Setup. This is a very good guide that uses a more manual setup for Xorg. I didn’t know about this guide when I first started, but I might have considered doing things this way instead. My guide diverges in subtle ways and favors a vanilla Wayland setup.
  • Headless SteamLink setup using a docker container. This will use SteamLink instead of Sunshine + Moonlight, which requires fewer configuration setups and might be more accessible.

Overview

We’ll first make sure we’re not accidentally running from onboard Graphics. Then we set up a dummy 4K display to give us a large framebuffer to work with, adjust permissions for uinput to get keyboard and mouse control, and finally we optimize Sunshine for low-latency, high-FPS streaming.

Step 1: Set NVIDIA as the Primary GPU to Disable AMD iGPU Rendering

This will ensure you’re using your NVIDIA card to render instead of onboard graphics. By default, Ubuntu may be using the AMD integrated graphics when no monitor is attached to the NVIDIA card. First, install the NVIDIA driver if you haven’t done so already:

sudo apt install nvidia-driver-535

We need to force the system to favor the NVIDIA GPU for all rendering:

sudo prime-select nvidia

Now your system should prefer the NVIDIA GPU. You can verify by running nvidia-smi or glxinfo | grep OpenGL. Proceed once NVIDIA is confirmed as the active renderer.

Step 2: Create a Dummy 4K@120Hz Display

The next step is to spoof a display being attached to your NVIDIA card. A lot of people online suggest using the software dummy display driver in X11. For performance sake, do not do this! You can easily get a dummy plug for your monitor, or just feed a custom EDID to the NVIDIA driver, tricking it into thinking a 4K 120Hz display is connected. Here’s how to do it the dongle-free way:

  1. Obtain a 4K120 EDID File. EDID files describe a monitor’s capabilities. You have a few options:
    • Use a pre-made EDID from an EDID repository. For example, the Linux TV EDID repository contains files for various displays. One known EDID that supports 4K@120Hz and HDR is from the Samsung Q800T (HDMI 2.1) TV.
    • Extract EDID from a real monitor/TV (if you have one that supports the target modes) using nvidia-settings on a system where that display is connected​ and save it in your X11 folder (e.g., /etc/X11/4k120.edid.)
    • Use this Virtual Display Driver Wizard to make a custom one if you’re on Windows.
  2. Xorg Configuration – Fake a Monitor. Create or edit your Xorg configuration at /etc/X11/xorg.conf to use the EDID on one of your card’s outputs. It’s a good idea to backup your Xorg config first. Here’s mine:
Section "ServerLayout"
    Identifier     "Layout0"
    Screen      0  "Screen0" 0 0
    InputDevice    "Keyboard0" "CoreKeyboard"
    InputDevice    "Mouse0" "CorePointer"
EndSection

Section "InputDevice"
    Identifier     "Mouse0"
    Driver         "mouse"
    Option         "Protocol" "auto"
    Option         "Device" "/dev/psaux"
    Option         "Emulate3Buttons" "no"
    Option         "ZAxisMapping" "4 5"
EndSection

Section "InputDevice"
    Identifier     "Keyboard0"
    Driver         "kbd"
EndSection

Section "Device"
    Identifier "NvidiaCard"
    Driver "nvidia"
    BusID "PCI:01:0:0"
    Option "ConnectedMonitor" "DFP-0"
    Option "CustomEDID" "DFP-0:/etc/X11/4k120.edid"
    Option "AllowEmptyInitialConfiguration" "true"
EndSection

Section "Screen"
    Identifier "Screen0"
    Device "NvidiaCard"
    Monitor "DummyMonitor"
    DefaultDepth 24
    SubSection "Display"
        Depth 24
        Virtual 3840 2160
    EndSubSection
EndSection

Section "Monitor"
    Identifier "DummyMonitor"
    HorizSync 28.0-160.0 
    VertRefresh 24.0-120.0
    Modeline "3840x2160_60.00" 533.25 3840 4072 4488 5136 2160 2163 2168 2222 -hsync +vsync
EndSection

The Breakdown:

  • BusID should match your NVIDIA GPU (find it via lspci | grep -i NVIDIA). This ensures X ties the config to the NVIDIA card. Use the same notation that I’m using above, with colons instead of dots.
  • Option "ConnectedMonitor" "DFP-0" tells the NVIDIA driver to pretend a digital display is connected on the first output.
  • Option "CustomEDID" points to the EDID file that we used earlier. This makes the driver load the given EDID instead of relying on a physical monitor. The EDID file will offer resolutions/refresh rates from that EDID as if a real monitor is attached. if you do not have a monitor, fake or otherwise, your framebuffer will be stuck at an unusably small size, like 8×8 or 640×480.
  • We include Virtual 3840 2160 under the Display sub-section to set an initial framebuffer size of 4K. This allocates a screen large enough for 4K. The EDID should take care of available modes; this just ensures X can accommodate it. You could also set Virtual 7680 4320 if using an 8K EDID.
  • The Monitor section sets an identifier and broad sync ranges covering up to 120 Hz. This might not be strictly necessary if the EDID is present since that will provide its own ranges and modelines, but it doesn’t hurt to include safe values.
  • The AllowEmptyInitialConfiguration option lets X start even with no real monitor attached. We’ve already marked the NVIDIA as primary, so this is just an extra safety to let X initialize headlessly.

Let’s Continue…

  • Reload and Test X: Reboot the system (or restart the X server with sudo systemctl restart gdm3). After reboot, Xorg should start using the dummy display. You can verify by checking /var/log/Xorg.0.log for lines indicating the EDID was loaded and modes registered. Also, running xrandr inside the X session should show a display with resolutions including 3840×2160@120 and 1920×1080@60, assuming those were in the EDID.

If you run xrandr and you get a message like, “Can’t open display” then try the following:

export DISPLAY=:0
xrandr
In this screenshot you can see I’m limited to 59 or 60 FPS because of the EDID I am using.

At this point, we have a virtual monitor attached to the NVIDIA GPU. This gives us a real X screen running at high resolution and refresh rate, which Sunshine can capture and stream. The X11 “dummy driver” solution is avoided; instead, the NVIDIA driver is doing the rendering using full hardware acceleration.

Step 3: Update Permissions for Uinput

We’re almost ready to install and run Sunshine, but first we have to update permissions for uinput so that we’re able to get mouse and keyboard inputs. If you do not do this, your mouse and keyboard will not work. On the host computer run this command:

sudo chown $(id -un):$(id -gn) /dev/uinput

This will persist until a reboot happens. There are a few good reasons to not do things this way—mainly that it gives every application full access to your keyboard and mouse inputs. Limiting this to your sunshine sessions might be preferable, so I’ll refer back to this article for a better setup script. (Check out the Uinput Permissions Workaround section.)

Step 4: Install Sunshine. Then Optimize it for Low-Latency, High-FPS Streaming

Grab the Latest Release of Sunshine from their Github release page. If you are downloading the debian package, install it this way:

sudo dpkg -i release_file_name.deb

Sunshine is the game-streaming host that will capture the host desktop and stream to Moonlight clients. To achieve resolutions as high as 4K 120 FPS with low latency, we need to tune Sunshine’s capture and encoding settings:

  • Prefer NVIDIA’s NvENC for Encoding: Sunshine should automatically use NvENC (NVIDIA’s hardware video encoder) given you have an NVIDIA GPU. Ensure in Sunshine’s settings (Web UI under Video > Encoder) that NvENC is selected as the encoder.
  • Use the Low-Latency NvENC Preset: NVIDIA’s encoder has presets balancing quality vs latency. For minimal delay, choose the “Low Latency High Performance” preset​. This preset reduces buffering and disables most extra B-frames, trading some image quality for latency. If you want a bit more quality and your system can handle it, “Low Latency High Quality” is another option, but start with High Performance for fastest response. In Sunshine’s config, set:
    • NvENC Preset: LowLatencyHighPerformance (often labeled “llhp”)​
    • Rate Control: CBR (Constant Bitrate) – this helps keep frame delivery steady, which is important for smooth streaming.
    • Max Bitrate: Set a high bitrate appropriate for your network (for 4K 120fps, this could be 100 Mbps or more if your network can handle it, or lower for 1080p60). Ensure both Sunshine and Moonlight are set to the same or compatible bitrates.
  • Choose the Right Capture Method: Sunshine supports several capture backends. In order of performance on NVIDIA: NvFBC, KMS, then X11. By default, Sunshine will use the first available method in its priority list​.
    • NvFBC (NVIDIA Framebuffer Capture) is very fast​. This will capture frames directly from the NVIDIA GPU’s output.
    • KMS (Kernel Mode Setting capture) is another high-performance method, especially if you were running a Wayland session, need HDR, and NvFBC isn’t working for you. If Sunshine falls back to X11 or you want to force KMS:
      • Enable KMS access: sudo setcap cap_sys_admin+ep $(which sunshine) to give Sunshine the capability to use DRM capture (this avoids having to run Sunshine as root).
      • In Sunshine’s config, set capture = kms to force KMS. Keep in mind KMS capture might not work if a standard X11 session is active.
    • X11 Capture is the slowest and should be avoided if possible. It comes with latency and CPU penalties.
  • Sunshine Frame Rate and Resolution: In Sunshine’s configuration (Application or desktop streaming settings), set the host resolution and FPS to the desired values:
    • For 4K @ 120 Hz streaming, set host resolution to 3840×2160 and FPS to 120 in the Sunshine stream settings. Make sure your client and network can handle the high bandwidth.
    • For 1080p @ 60, you may either downscale on the client side or set Sunshine to stream at 1080p60 directly. Keep in mind Sunshine will dynamically switch if you launch a game in a different resolution.
    • You can change desktop resolution the way you normally would in your display settings, and the client will handle resolution switching automatically.
  • NvENC Rate Control and Quality: Experiment with your network. Use CBR and set a high bitrate or stick to VBR. You may also adjust QP (quantization parameter) or other advanced settings in Sunshine’s “Advanced” tab:
    • A lower QP value means less compression (better quality, more bitrate). Sunshine’s default might be fine, but if you have headroom, you can set a fixed QP (e.g., 20 or lower) or use CBR with a high bitrate to maintain quality.
    • No V-Sync: Make sure the game/app you stream isn’t artificially capped at low FPS by V-sync. Run games with V-sync off, or use G-Sync/FreeSync so that the GPU can output as many FPS as possible for Sunshine to capture. Sunshine itself will capture as fast as possible up to the set FPS limit.
  • Moonlight Client Settings: On the client side (Moonlight), optimize your performance this way:
    • Set Moonlight’s stream resolution and FPS to match what you configured on Sunshine (e.g., 4K120 or 1080p60).
    • In Moonlight settings, match the bitrate to what you set on Sunshine. If you experience stuttering, you can try Moonlight’s “Prefer Smoothest” streaming mode which may buffer a bit more to smooth out frames. For lowest latency, you typically want to minimize buffering. Balanced or Smoothest can reduce visible stutter at the cost of a few milliseconds latency. Experiment to see what feels best.
    • Ensure Moonlight is decoding via hardware.
  • Test Latency: For truly low latency, your network needs to be fast and stable (wired Gigabit Ethernet is ideal). Test the setup to gauge latency and smoothness at lower bitrates, then push them up to maximize quality. Turn on the performance overlay (Ctrl+Alt+Shift+S) to show stats like latency, FPS, packet loss, etc. Use that to fine-tune settings. If network is the bottleneck, you might increase Sunshine’s FEC – Forward Error Correction – setting to handle packet loss.

Using the fastest capture method (NvFBC/KMS), and a wired ethernet connection you might be able to achieve < 10 ms of input-to-output latency. Nice!

Some Ideas to Take this a Step Further:

  • Set up Sunshine as a service so it’s always running.
  • Find a better fix for uinput permissions.
  • Get this running over Tailscale as your own private GeForce now server.

Filed Under: Linux

May 19, 2023 by Mark Hamilton Leave a Comment

Portfolio in Progress

I haven’t posted in a while but I’ve decided to write some longer form entries of larger projects I’ve worked on. First up is Producteev. I was hired in 2011 as a Windows desktop engineer to make their first Windows application, and this is my story about that.

Filed Under: Meta

May 26, 2020 by Mark Hamilton 9 Comments

Installation issues on Ubuntu 20.04 LTS

Last week I was given a free secondhand HP 17-x051nr, an inexpensive Intel i3-based laptop. For the price of a $30 replacement battery and an SSD I had on-hand I now have a newish laptop to use.

Getting the 20.04 LTS release of Ubuntu running on it had some issues, so here’s a guide to working around these and getting the latest Ubuntu LTS installed.

Symptoms

The installation screen would become unresponsive around the Installation type screen where I would select how I wanted to format my drive. I was able to open a terminal and run top and I noticed that systemd takes up 100% CPU usage. Eventually the whole system would freeze and leaving it overnight to finish installing didn’t help.

Switching over to the Try Ubuntu option wasn’t much better. I would see a TTY screen with flashing error messages that were scrolling too fast to read and it would eventually lock up entirely.

PCIe Bus Error Message
It would hang here forever.
pcieport 0000:00:1c.5: AER: PCIe Bus Error: severity=Corrected, type=Physical Layer, (Receiver ID)
pcieport 0000:00:1c.5:   device [8086:9d15] error status/mask=00000001/00002000
pcieport 0000:00:1c.5:    [ 0] RxErr

This laptop was released in August, 2016, which is not that old for a PC. It should still run a new Ubuntu version just fine. It turns out this is a relatively common issue on laptops.

The Fix

The first step is to change the GRUB boot line for the live Linux boot media. When the GRUB menu opens up press ‘e’ to edit the launch commands.

You’ll want to add pcie_aspm=off following directly after the splash parameter on the linux line.

Once you’re able to boot use the Try Ubuntu option. Maybe the regular installer works for some people but I wasn’t able to get it to run. You should not see any PCI express errors flashing on the screen at this point. Once the desktop environment opens, launch the installer from there. Do not connect to WiFi, and do not install any extra packages because it will cause the installation to fail. (I.e., use the Normal Installation option, but don’t install the third party software and do not download updates. You can do both of these later.)

As a side-note, I would recommend keeping UEFI enabled.

Once you finish the installation you may reboot and remove the installation medium. You’ll be getting PCIe errors again but it will eventually boot. Log in and use your favorite editor to open up /etc/default/grub as admin. We want to change this line:

GRUB_CMDLINE_LINUX_DEFAULT="quiet splash"

to this:

GRUB_CMDLINE_LINUX_DEFAULT="quiet splash pcie_aspm=off"

Save and then update GRUB:

sudo update-grub

I would recommend rebooting after this. Everything should be working smoothly now. Please drop a comment if this helps or if you’re still having issues.

Filed Under: Linux

March 22, 2020 by Mark Hamilton Leave a Comment

Thoughts after a year of daily Japanese practice

Last year I picked something that I had to stick to for a year no matter how hard it got. Today I hit day 400.

I would encourage virtually everyone to set aside some daily time for a habit that you can one day master, starting small and expanding it only as you grow more comfortable. It doesn’t matter what you do, as long as you care about it. These are my thoughts on learning a new language.

Surreal Mount Fuji with moon in background
Lesson 1: 15 opens the door. 「十五分はドアを開けます。」

In my high school days I had developed a passing interest in Japanese. I didn’t get very far into vocabulary or grammar, but I learned the Kana (ひらがなとカタカナ,) which count for two of the three Japanese writing forms. Kana is comprised of 46 letters for each set, but common Japanese writing also uses an additional 2,000+ Kanji. I didn’t get that far. Learning a new language requires an intense amount of study and it’s easy to get overwhelmed if you consider it all at once.

Years have passed since my high school days and common teaching methods have improved. I really recommend spaced repetition apps like Duolingo or Memrise which use a flash card style approach that allow you to study in short sprints. I think this is the key to getting hooked. In fact, 15 minute blocks of study seem to be the perfect, bite-sized duration to target for new language learners. Good lessons will make you want to do more, and grueling lessons won’t be long enough to set a bad tone for your day. Little by little your skills compound. Once a stronger habit takes hold your study time will pass by in a flash. It took around 6 months for my overall mindset to switch from “just putting in my time” to actively working to gain mastery, and I naturally adjusted to longer and more frequent blocks of study.

Abstract topological mountain with decorative dots overlaying it.
Lesson 2: Set your expectations. 「期待を設定して。」

You should be prepared to have some rough days. On bad days you can just fulfill your minimum obligation then set it aside. Remember that the discomfort of new lessons will pass and these will soon become easy and familiar landmarks in your mind. Consider this when you review old topics as well since those were all foreign to you at one point. Expect challenge but don’t worry; and time will take care of the hard parts.

You will eventually want to go deeper with your studies. I recommend you develop good principles to facilitate this: be patient with yourself, and make the best use of your time, (i.e., use time actively instead of passively.) This doesn’t mean to intentionally seek struggle. You want to learn the easiest way that you can, of course, but understand that there’s rarely a shortcut or perfect system. This applies to every practice.

My learning resources for Japanese

Duolingo, Wanikani, 8020japanese, and Tofugu are great for most learners. I recommend to go the cheap or free route when starting out with most new hobbies and these sites have a lot of free resources. There’s also a nearly unlimited amount of Japanese learning YouTube channels. You’ll want to spend some time finding teachers or communities that you resonate with. Treat it like you’re applying for an apprenticeship and you have to find someone that keeps your interest.

Takeaways

Being able to talk to my favorite Japanese photographers through Instagram this year felt like an amazing milestone for me. I’m also proud to feel more comfortable with screwing up and the vulnerability of the process. What a way to learn about yourself, and it starts out with such a small commitment.

Whatever you want to do for a year, I hope you not only stick to it, but do it consistently, and have a wonderful time with it. You never know where it could take you.

Ganbatte!

Filed Under: Personal

November 20, 2019 by Mark Hamilton Leave a Comment

Added a now page

This is an old idea that I liked from Derek Sivers. I’m going to keep it up to date with stuff that I’m working on, rather than announcing it in separate posts. Focus shifts pretty rapidly over here but I really only want to announce the finished stuff.

Filed Under: Meta

November 2, 2019 by Mark Hamilton Leave a Comment

New resource page — artists

Some of my work is inspired by the people I follow closely, and since I think others can benefit from them as well I’ve made a page with a small selection of artists I follow.

It takes a long time to go through my follow list and write descriptions on Instagram and YouTube so I kept the list short for now. I will be adding more to it later.

Filed Under: Creative, Meta

  • Page 1
  • Page 2
  • Go to Next Page »

Primary Sidebar

Get updates

  • Sign up for my newsletter

Recent Posts

  • Headless NVIDIA 4K@120Hz Streaming on Ubuntu 24.04
  • Portfolio in Progress
  • Installation issues on Ubuntu 20.04 LTS
  • Thoughts after a year of daily Japanese practice
  • Added a now page

Archives

  • March 2025
  • May 2023
  • May 2020
  • March 2020
  • November 2019
  • September 2019
  • August 2019
  • August 2017
  • July 2017

Categories

  • .plan
  • Creative
  • Linux
  • Meta
  • Personal

Footer

Thanks for visiting

Say hello,

I’m a designer, software engineer, and CTO living in
North Dakota.

Contact form
Tools I use
Github
Instagram

Page Last Modified: November 2, 2019 • © 2013–2025 • Log in

 

Loading Comments...